<?php

namespace Google\Web_Stories_Dependencies\AmpProject\Dom;

use Google\Web_Stories_Dependencies\AmpProject\Html\Attribute;
use Google\Web_Stories_Dependencies\AmpProject\Exception\MaxCssByteCountExceeded;
use Google\Web_Stories_Dependencies\AmpProject\Optimizer\CssRule;
use DOMAttr;
use DOMElement;
/**
 * Abstract away some convenience logic for handling DOMElement objects.
 *
 * @property Document $ownerDocument        The ownerDocument for these elements should always be a Dom\Document.
 * @property int      $inlineStyleByteCount Number of bytes that are consumed by the inline style attribute.
 *
 * @package ampproject/amp-toolbox
 */
final class Element extends DOMElement
{
    /**
     * Regular expression pattern to match events and actions within an 'on' attribute.
     *
     * @var string
     */
    const AMP_EVENT_ACTIONS_REGEX_PATTERN = '/((?<event>[^:;]+):(?<actions>(?:[^;,\\(]+(?:\\([^\\)]+\\))?,?)+))+?/';
    /**
     * Regular expression pattern to match individual actions within an event.
     *
     * @var string
     */
    const AMP_ACTION_REGEX_PATTERN = '/(?<action>[^(),\\s]+(?:\\([^\\)]+\\))?)+/';
    /**
     * Error message to use when the __get() is triggered for an unknown property.
     *
     * @var string
     */
    const PROPERTY_GETTER_ERROR_MESSAGE = 'Undefined property: AmpProject\\Dom\\Element::';
    /**
     * Associative array for lazily-created, cached properties for the document.
     *
     * @var array
     */
    private $properties = [];
    /**
     * Add CSS styles to the element as an inline style attribute.
     *
     * @param string $style   CSS style(s) to add to the inline style attribute.
     * @param bool   $prepend Optional. Whether to prepend the new style to existing styles or not. Defaults to false.
     * @return DOMAttr|false The new or modified DOMAttr or false if an error occurred.
     * @throws MaxCssByteCountExceeded If the allowed max byte count is exceeded.
     */
    public function addInlineStyle($style, $prepend = \false)
    {
        $style = \trim($style, CssRule::CSS_TRIM_CHARACTERS);
        $existingStyle = (string) \trim($this->getAttribute(Attribute::STYLE));
        if (!empty($existingStyle)) {
            $existingStyle = \rtrim($existingStyle, ';') . ';';
        }
        $newStyle = $prepend ? $style . ';' . $existingStyle : $existingStyle . $style;
        return $this->setAttribute(Attribute::STYLE, $newStyle);
    }
    /**
     * Sets or modifies an attribute.
     *
     * @link https://php.net/manual/en/domelement.setattribute.php
     * @param string $name  The name of the attribute.
     * @param string $value The value of the attribute.
     * @return DOMAttr|false The new or modified DOMAttr or false if an error occurred.
     * @throws MaxCssByteCountExceeded If the allowed max byte count is exceeded.
     */
    public function setAttribute($name, $value)
    {
        // Make sure $value is always a string and not null.
        $value = \strval($value);
        if ($name === Attribute::STYLE && $this->ownerDocument->isCssMaxByteCountEnforced()) {
            $newByteCount = \strlen($value);
            if ($this->ownerDocument->getRemainingCustomCssSpace() < $newByteCount - $this->inlineStyleByteCount) {
                throw MaxCssByteCountExceeded::forInlineStyle($this, $value);
            }
            $this->ownerDocument->addInlineStyleByteCount($newByteCount - $this->inlineStyleByteCount);
            $this->inlineStyleByteCount = $newByteCount;
            return parent::setAttribute(Attribute::STYLE, $value);
        }
        return parent::setAttribute($name, $value);
    }
    /**
     * Adds a boolean attribute without value.
     *
     * @param string $name  The name of the attribute.
     * @return DOMAttr|false The new or modified DOMAttr or false if an error occurred.
     * @throws MaxCssByteCountExceeded If the allowed max byte count is exceeded.
     */
    public function addBooleanAttribute($name)
    {
        $attribute = new DOMAttr($name);
        $result = $this->setAttributeNode($attribute);
        if (!$result instanceof DOMAttr) {
            return \false;
        }
        return $result;
    }
    /**
     * Copy one or more attributes from this element to another element.
     *
     * @param array|string $attributes       Attribute name or array of attribute names to copy.
     * @param Element      $target           Target Dom\Element to copy the attributes to.
     * @param string       $defaultSeparator Default separator to use for multiple values if the attribute is not known.
     */
    public function copyAttributes($attributes, Element $target, $defaultSeparator = ',')
    {
        foreach ((array) $attributes as $attribute) {
            if ($this->hasAttribute($attribute)) {
                $values = $this->getAttribute($attribute);
                if ($target->hasAttribute($attribute)) {
                    switch ($attribute) {
                        case Attribute::ON:
                            $values = self::mergeAmpActions($target->getAttribute($attribute), $values);
                            break;
                        case Attribute::CLASS_:
                            $values = $target->getAttribute($attribute) . ' ' . $values;
                            break;
                        default:
                            $values = $target->getAttribute($attribute) . $defaultSeparator . $values;
                    }
                }
                $target->setAttribute($attribute, $values);
            }
        }
    }
    /**
     * Register an AMP action to an event.
     *
     * If the element already contains one or more events or actions, the method
     * will assemble them in a smart way.
     *
     * @param string $event  Event to trigger the action on.
     * @param string $action Action to add.
     */
    public function addAmpAction($event, $action)
    {
        $eventActionString = "{$event}:{$action}";
        if (!$this->hasAttribute(Attribute::ON)) {
            // There's no "on" attribute yet, so just add it and be done.
            $this->setAttribute(Attribute::ON, $eventActionString);
            return;
        }
        $this->setAttribute(Attribute::ON, self::mergeAmpActions($this->getAttribute(Attribute::ON), $eventActionString));
    }
    /**
     * Merge two sets of AMP events & actions.
     *
     * @param string $first  First event/action string.
     * @param string $second First event/action string.
     * @return string Merged event/action string.
     */
    public static function mergeAmpActions($first, $second)
    {
        $events = [];
        foreach ([$first, $second] as $eventActionString) {
            $matches = [];
            $results = \preg_match_all(self::AMP_EVENT_ACTIONS_REGEX_PATTERN, $eventActionString, $matches);
            if (!$results || !isset($matches['event'])) {
                continue;
            }
            foreach ($matches['event'] as $index => $event) {
                $events[$event][] = $matches['actions'][$index];
            }
        }
        $valueStrings = [];
        foreach ($events as $event => $actionStringsArray) {
            $actionsArray = [];
            \array_walk($actionStringsArray, static function ($actions) use(&$actionsArray) {
                $matches = [];
                $results = \preg_match_all(self::AMP_ACTION_REGEX_PATTERN, $actions, $matches);
                if (!$results || !isset($matches['action'])) {
                    $actionsArray[] = $actions;
                    return;
                }
                $actionsArray = \array_merge($actionsArray, $matches['action']);
            });
            $actions = \implode(',', \array_unique(\array_filter($actionsArray)));
            $valueStrings[] = "{$event}:{$actions}";
        }
        return \implode(';', $valueStrings);
    }
    /**
     * Extract this element's HTML attributes and return as an associative array.
     *
     * @return string[] The attributes for the passed node, or an empty array if it has no attributes.
     */
    public function getAttributesAsAssocArray()
    {
        $attributes = [];
        if (!$this->hasAttributes()) {
            return $attributes;
        }
        foreach ($this->attributes as $attribute) {
            $attributes[$attribute->nodeName] = $attribute->nodeValue;
        }
        return $attributes;
    }
    /**
     * Add one or more HTML element attributes to this element.
     *
     * @param string[] $attributes One or more attributes for the node's HTML element.
     */
    public function setAttributes($attributes)
    {
        foreach ($attributes as $name => $value) {
            try {
                $this->setAttribute($name, $value);
            } catch (MaxCssByteCountExceeded $e) {
                /*
                 * Catch a "Invalid Character Error" when libxml is able to parse attributes with invalid characters,
                 * but it throws error when attempting to set them via DOM methods. For example, '...this' can be parsed
                 * as an attribute but it will throw an exception when attempting to setAttribute().
                 */
                continue;
            }
        }
    }
    /**
     * Magic getter to implement lazily-created, cached properties for the element.
     *
     * @param string $name Name of the property to get.
     * @return mixed Value of the property, or null if unknown property was requested.
     */
    public function __get($name)
    {
        switch ($name) {
            case 'inlineStyleByteCount':
                if (!isset($this->properties['inlineStyleByteCount'])) {
                    $this->properties['inlineStyleByteCount'] = \strlen((string) $this->getAttribute(Attribute::STYLE));
                }
                return $this->properties['inlineStyleByteCount'];
        }
        // Mimic regular PHP behavior for missing notices.
        \trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, \E_USER_NOTICE);
        return null;
    }
    /**
     * Magic setter to implement lazily-created, cached properties for the element.
     *
     * @param string $name Name of the property to set.
     * @param mixed  $value Value of the property.
     */
    public function __set($name, $value)
    {
        if ($name !== 'inlineStyleByteCount') {
            // Mimic regular PHP behavior for missing notices.
            \trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, \E_USER_NOTICE);
            return;
        }
        $this->properties[$name] = $value;
    }
    /**
     * Magic callback for lazily-created, cached properties for the element.
     *
     * @param string $name Name of the property to set.
     */
    public function __isset($name)
    {
        if ($name !== 'inlineStyleByteCount') {
            // Mimic regular PHP behavior for missing notices.
            \trigger_error(self::PROPERTY_GETTER_ERROR_MESSAGE . $name, \E_USER_NOTICE);
            return \false;
        }
        return isset($this->properties[$name]);
    }
}
