<?php

namespace Google\Web_Stories_Dependencies\AmpProject\Optimizer;

/**
 * CSS rule that provides semantic handling of media queries, selectors and properties.
 *
 * This is used in conjunction with CssRules for deduplication of CSS when adding styles during transformations.
 *
 * Note: This is a simplistic representation of CSS rules built for a specific purpose.
 * Make sure it supports a given use case before including in new code!
 *
 * @package ampproject/amp-toolbox
 */
final class CssRule
{
    /**
     * Characters to use for trimming CSS values.
     *
     * @var string
     */
    const CSS_TRIM_CHARACTERS = " \t\n\r\x00\v;";
    /**
     * Placeholder to use in the rule to denote an ID that has yet to be defined.
     *
     * Use applyId() to finalize the CSS rule.
     *
     * @var string
     */
    const ID_PLACEHOLDER = '__ID__';
    /**
     * Selector(s) to use for the CSS rule.
     *
     * @var string[]
     */
    private $selectors;
    /**
     * Properties to apply to the selector(s).
     *
     * @var string[]
     */
    private $properties;
    /**
     * Media query that wraps the CSS rule.
     *
     * @var string
     */
    private $mediaQuery = '';
    /**
     * Rendered CSS cache.
     *
     * @var string|null
     */
    private $renderedCss;
    /**
     * Byte count cache.
     *
     * @var int|null
     */
    private $byteCount;
    /**
     * Instantiate a CssRule object.
     *
     * @param string|string[] $selectors  One or more selectors to use.
     * @param string|string[] $properties One or more properties to apply to the selector(s).
     */
    public function __construct($selectors, $properties)
    {
        $this->selectors = \array_values(\array_unique(\array_filter(\array_map([$this, 'normalizeSelector'], $this->separateSelectors($selectors)))));
        $this->properties = \array_values(\array_unique(\array_filter(\array_map([$this, 'normalizeProperty'], $this->separateProperties($properties)))));
        \sort($this->properties);
    }
    /**
     * Create a new CSS rule that is wrapped in a media query.
     *
     * @param string          $mediaQuery Media query to wrap the CSS rule in.
     * @param string|string[] $selectors  One or more selectors to use.
     * @param string|string[] $properties One or more properties to apply to the selector(s).
     * @return self CSS rule wrapped in a media query.
     */
    public static function withMediaQuery($mediaQuery, $selectors, $properties)
    {
        $cssRule = new self($selectors, $properties);
        $cssRule->mediaQuery = $mediaQuery;
        return $cssRule;
    }
    /**
     * Get the selector(s) for this CSS rule.
     *
     * @return string[] Selector(s) of the CSS rule.
     */
    public function getSelectors()
    {
        return $this->selectors;
    }
    /**
     * Get the properties for this CSS rule.
     *
     * @return string[] Properties of the CSS rule.
     */
    public function getProperties()
    {
        return $this->properties;
    }
    /**
     * Get the media query for this CSS rule.
     *
     * @return string Media query for the CSS rule or an empty string if none is set.
     */
    public function getMediaQuery()
    {
        return $this->mediaQuery;
    }
    /**
     * Get the CSS for this CSS rule.
     *
     * @return string CSS for this CSS rule.
     */
    public function getCss()
    {
        if ($this->renderedCss === null) {
            $selectors = \implode(',', $this->selectors);
            $properties = \implode(';', \array_map(static function ($property) {
                return \trim($property, self::CSS_TRIM_CHARACTERS);
            }, $this->properties));
            if (empty($selectors) || empty($properties)) {
                $this->renderedCss = '';
            } else {
                $this->renderedCss = "{$selectors}{{$properties}}";
                if (!empty($this->mediaQuery)) {
                    $this->renderedCss = "{$this->mediaQuery}{{$this->renderedCss}}";
                }
            }
        }
        return $this->renderedCss;
    }
    /**
     * Apply the provided ID across all ID placeholders.
     *
     * @param string $id ID to apply.
     * @return self
     */
    public function applyID($id)
    {
        $replacement_callback = static function ($css) use($id) {
            return \str_replace(self::ID_PLACEHOLDER, $id, $css);
        };
        $this->selectors = \array_map($replacement_callback, $this->selectors);
        $this->properties = \array_map($replacement_callback, $this->properties);
        // Reset caches so they will need to be rebuilt.
        $this->renderedCss = null;
        $this->byteCount = null;
        return $this;
    }
    /**
     * Check if the CSS rule can be merged with another provided CSS rule.
     *
     * @param CssRule $that CSS rule to check against.
     * @return bool Whether the two CSS rules can be merged.
     */
    public function canBeMerged(CssRule $that)
    {
        // As merging rules changes the CSS ordering, it is not a safe operation to do.
        // Therefore, we hard-code a single scenario here that we want to support (which is the most common case).
        // This is the only merge that is being done in the Node.js amp-toolbox, so we keep this for consistency
        // across implementations.
        if ($this->mediaQuery !== $that->mediaQuery) {
            return \false;
        }
        if (\count($this->properties) !== 1 || \count($that->properties) !== 1) {
            return \false;
        }
        if ($this->properties[0] !== 'display:none' || $that->properties[0] !== 'display:none') {
            return \false;
        }
        return \true;
    }
    /**
     * Merge this CSS rule with another CSS rule.
     *
     * This should only be done to same-properties rules within the same media query,
     * as the result will be wild otherwise.
     *
     * @param CssRule $that CSS rule to merge the current one with.
     * @return CssRule Merged Css rule.
     */
    public function mergeWith(CssRule $that)
    {
        $cssRule = new self(\array_merge($this->selectors, $that->selectors), \array_merge($this->properties, $that->properties));
        $cssRule->mediaQuery = $this->mediaQuery;
        return $cssRule;
    }
    /**
     * Get the byte count for the CSS rule.
     *
     * @return int Byte count of the CSS rule.
     */
    public function getByteCount()
    {
        if ($this->byteCount === null) {
            $this->byteCount = \strlen($this->getCss());
        }
        return $this->byteCount;
    }
    /**
     * Normalize a single selector.
     *
     * @param string $selector Selector to normalize.
     * @return string Normalized selector.
     */
    private function normalizeSelector($selector)
    {
        // Turn all series of whitespace into single spaces.
        $selector = \preg_replace('/\\s+/', ' ', $selector);
        // Remove spaces around selector qualifiers to keep properties compact.
        $selector = \preg_replace('/ ?([>+~]) ?/', '$1', $selector);
        // Remove leading and trailing whitespace and commas.
        $selector = \trim($selector, self::CSS_TRIM_CHARACTERS);
        return $selector;
    }
    /**
     * Normalize single property.
     *
     * @param string $property Property to normalize.
     * @return string Normalized property.
     */
    private function normalizeProperty($property)
    {
        // Turn all series of whitespace into single spaces.
        $property = \preg_replace('/\\s+/', ' ', $property);
        // Remove spaces around colons and semicolons to keep properties compact.
        $property = \preg_replace('/ ?([:;]) ?/', '$1', $property);
        // Deduplicate semicolons.
        $property = \preg_replace('/([;]+)/', ';', $property);
        // Remove leading and trailing whitespace and semicolons.
        $property = \trim($property, self::CSS_TRIM_CHARACTERS);
        return $property;
    }
    /**
     * Separate selectors into individual values.
     *
     * @param string|string[]|array[] $selectors Selectors to separate.
     * @return string[] Separated selectors.
     */
    private function separateSelectors($selectors)
    {
        $separatedSelectors = [];
        foreach ((array) $selectors as $selectorString) {
            if (\is_array($selectorString)) {
                $separatedSelectors = \array_merge($separatedSelectors, $this->separateSelectors($selectorString));
            } else {
                $separatedSelectors = \array_merge($separatedSelectors, \explode(',', $selectorString));
            }
        }
        return $separatedSelectors;
    }
    /**
     * Separate properties into individual values.
     *
     * @param string|string[]|array[] $properties Properties to separate.
     * @return string[] Separated properties.
     */
    private function separateProperties($properties)
    {
        $separatedProperties = [];
        foreach ((array) $properties as $propertyString) {
            if (\is_array($propertyString)) {
                $separatedProperties = \array_merge($separatedProperties, $this->separateProperties($propertyString));
            } else {
                $separatedProperties = \array_merge($separatedProperties, \explode(';', $propertyString));
            }
        }
        return $separatedProperties;
    }
}
