<?php

namespace Sabberworm\CSS;

/**
 * Class OutputFormat
 *
 * @method OutputFormat setSemicolonAfterLastRule(bool $bSemicolonAfterLastRule) Set whether semicolons are added after
 *     last rule.
 */
class OutputFormat
{
    /**
     * Value format: `"` means double-quote, `'` means single-quote
     *
     * @var string
     */
    public $sStringQuotingType = '"';

    /**
     * Output RGB colors in hash notation if possible
     *
     * @var string
     */
    public $bRGBHashNotation = true;

    /**
     * Declaration format
     *
     * Semicolon after the last rule of a declaration block can be omitted. To do that, set this false.
     *
     * @var bool
     */
    public $bSemicolonAfterLastRule = true;

    /**
     * Spacing
     * Note that these strings are not sanity-checked: the value should only consist of whitespace
     * Any newline character will be indented according to the current level.
     * The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`)
     */
    public $sSpaceAfterRuleName = ' ';

    /**
     * @var string
     */
    public $sSpaceBeforeRules = '';

    /**
     * @var string
     */
    public $sSpaceAfterRules = '';

    /**
     * @var string
     */
    public $sSpaceBetweenRules = '';

    /**
     * @var string
     */
    public $sSpaceBeforeBlocks = '';

    /**
     * @var string
     */
    public $sSpaceAfterBlocks = '';

    /**
     * @var string
     */
    public $sSpaceBetweenBlocks = "\n";

    /**
     * Content injected in and around at-rule blocks.
     *
     * @var string
     */
    public $sBeforeAtRuleBlock = '';

    /**
     * @var string
     */
    public $sAfterAtRuleBlock = '';

    /**
     * This is what’s printed before and after the comma if a declaration block contains multiple selectors.
     *
     * @var string
     */
    public $sSpaceBeforeSelectorSeparator = '';

    /**
     * @var string
     */
    public $sSpaceAfterSelectorSeparator = ' ';

    /**
     * This is what’s printed after the comma of value lists
     *
     * @var string
     */
    public $sSpaceBeforeListArgumentSeparator = '';

    /**
     * @var string
     */
    public $sSpaceAfterListArgumentSeparator = '';

    /**
     * @var string
     */
    public $sSpaceBeforeOpeningBrace = ' ';

    /**
     * Content injected in and around declaration blocks.
     *
     * @var string
     */
    public $sBeforeDeclarationBlock = '';

    /**
     * @var string
     */
    public $sAfterDeclarationBlockSelectors = '';

    /**
     * @var string
     */
    public $sAfterDeclarationBlock = '';

    /**
     * Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings.
     *
     * @var string
     */
    public $sIndentation = "\t";

    /**
     * Output exceptions.
     *
     * @var bool
     */
    public $bIgnoreExceptions = false;

    /**
     * Render comments for lists and RuleSets
     *
     * @var bool
     */
    public $bRenderComments = false;

    /**
     * @var OutputFormatter|null
     */
    private $oFormatter = null;

    /**
     * @var OutputFormat|null
     */
    private $oNextLevelFormat = null;

    /**
     * @var int
     */
    private $iIndentationLevel = 0;

    public function __construct()
    {
    }

    /**
     * @param string $sName
     *
     * @return string|null
     */
    public function get($sName)
    {
        $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i'];
        foreach ($aVarPrefixes as $sPrefix) {
            $sFieldName = $sPrefix . ucfirst($sName);
            if (isset($this->$sFieldName)) {
                return $this->$sFieldName;
            }
        }
        return null;
    }

    /**
     * @param array<array-key, string>|string $aNames
     * @param mixed $mValue
     *
     * @return self|false
     */
    public function set($aNames, $mValue)
    {
        $aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i'];
        if (is_string($aNames) && strpos($aNames, '*') !== false) {
            $aNames =
                [
                    str_replace('*', 'Before', $aNames),
                    str_replace('*', 'Between', $aNames),
                    str_replace('*', 'After', $aNames),
                ];
        } elseif (!is_array($aNames)) {
            $aNames = [$aNames];
        }
        foreach ($aVarPrefixes as $sPrefix) {
            $bDidReplace = false;
            foreach ($aNames as $sName) {
                $sFieldName = $sPrefix . ucfirst($sName);
                if (isset($this->$sFieldName)) {
                    $this->$sFieldName = $mValue;
                    $bDidReplace = true;
                }
            }
            if ($bDidReplace) {
                return $this;
            }
        }
        // Break the chain so the user knows this option is invalid
        return false;
    }

    /**
     * @param string $sMethodName
     * @param array<array-key, mixed> $aArguments
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function __call($sMethodName, array $aArguments)
    {
        if (strpos($sMethodName, 'set') === 0) {
            return $this->set(substr($sMethodName, 3), $aArguments[0]);
        } elseif (strpos($sMethodName, 'get') === 0) {
            return $this->get(substr($sMethodName, 3));
        } elseif (method_exists(OutputFormatter::class, $sMethodName)) {
            return call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments);
        } else {
            throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName);
        }
    }

    /**
     * @param int $iNumber
     *
     * @return self
     */
    public function indentWithTabs($iNumber = 1)
    {
        return $this->setIndentation(str_repeat("\t", $iNumber));
    }

    /**
     * @param int $iNumber
     *
     * @return self
     */
    public function indentWithSpaces($iNumber = 2)
    {
        return $this->setIndentation(str_repeat(" ", $iNumber));
    }

    /**
     * @return OutputFormat
     */
    public function nextLevel()
    {
        if ($this->oNextLevelFormat === null) {
            $this->oNextLevelFormat = clone $this;
            $this->oNextLevelFormat->iIndentationLevel++;
            $this->oNextLevelFormat->oFormatter = null;
        }
        return $this->oNextLevelFormat;
    }

    /**
     * @return void
     */
    public function beLenient()
    {
        $this->bIgnoreExceptions = true;
    }

    /**
     * @return OutputFormatter
     */
    public function getFormatter()
    {
        if ($this->oFormatter === null) {
            $this->oFormatter = new OutputFormatter($this);
        }
        return $this->oFormatter;
    }

    /**
     * @return int
     */
    public function level()
    {
        return $this->iIndentationLevel;
    }

    /**
     * Creates an instance of this class without any particular formatting settings.
     *
     * @return self
     */
    public static function create()
    {
        return new OutputFormat();
    }

    /**
     * Creates an instance of this class with a preset for compact formatting.
     *
     * @return self
     */
    public static function createCompact()
    {
        $format = self::create();
        $format->set('Space*Rules', "")
            ->set('Space*Blocks', "")
            ->setSpaceAfterRuleName('')
            ->setSpaceBeforeOpeningBrace('')
            ->setSpaceAfterSelectorSeparator('')
            ->setRenderComments(false);
        return $format;
    }

    /**
     * Creates an instance of this class with a preset for pretty formatting.
     *
     * @return self
     */
    public static function createPretty()
    {
        $format = self::create();
        $format->set('Space*Rules', "\n")
            ->set('Space*Blocks', "\n")
            ->setSpaceBetweenBlocks("\n\n")
            ->set('SpaceAfterListArgumentSeparator', ['default' => '', ',' => ' '])
            ->setRenderComments(true);
        return $format;
    }
}
