<?php

namespace DevOwl\RealCookieBanner\Vendor\DevOwl\HeadlessContentBlocker;

use DevOwl\RealCookieBanner\Vendor\DevOwl\FastHtmlTag\finder\match\AbstractMatch;
use DevOwl\RealCookieBanner\Vendor\DevOwl\FastHtmlTag\finder\match\SelectorSyntaxMatch;
use DevOwl\RealCookieBanner\Vendor\DevOwl\FastHtmlTag\finder\SelectorSyntaxAttributeFunctionVariableResolver;
use DevOwl\RealCookieBanner\Vendor\DevOwl\FastHtmlTag\finder\SelectorSyntaxFinder;
/**
 * Describe a blockable item by selector syntax and regular expressions (e.g. to be used in `href` and `src`).
 * @internal
 */
abstract class AbstractBlockable implements SelectorSyntaxAttributeFunctionVariableResolver
{
    /**
     * Key of blockable data to store the legal basis (multiple legal basis are possible)
     * as string array. If one of the legal basis is `legitimate-interest`, the blockable has
     * higher priority.
     */
    const DATA_KEY_LEGAL_BASIS = 'legalBasis';
    /**
     * See `SelectorSyntaxFinder`.
     *
     * @var SelectorSyntaxFinder[]
     */
    private $selectorSyntaxFinder = [];
    private $expressionToStrposCache = null;
    private $regexp = ['wildcard' => [], 'contains' => []];
    private $headlessContentBlocker;
    /**
     * Original rules from string rules.
     *
     * @var string[]
     */
    private $originalExpressions = [];
    /**
     * Variables can be passed as rule in format `:$myVar=...` and can be reused in selector syntax function arguments.
     *
     * @var string[]
     */
    private $variables = [];
    private $data = [];
    /**
     * C'tor.
     *
     * @param HeadlessContentBlocker $headlessContentBlocker
     */
    public function __construct($headlessContentBlocker)
    {
        $this->headlessContentBlocker = $headlessContentBlocker;
    }
    /**
     * Generate the custom element blockers and regular expressions and append
     * it to this blockable instance.
     *
     * @param string[] $blockers
     */
    public function appendFromStringArray($blockers)
    {
        // @codeCoverageIgnoreStart
        if (!\is_array($blockers)) {
            return;
        }
        // @codeCoverageIgnoreEnd
        // Filter out custom element expressions and variables
        foreach ($blockers as $idx => &$line) {
            $line = $this->headlessContentBlocker->runBlockableStringExpressionCallback($line, $this);
            // https://regex101.com/r/TfcqVi/1
            if (Utils::startsWith($line, ':$') && \preg_match('/^:\\$(\\w+)\\s*=(.*)$/m', $line, $matches)) {
                $this->variables[$matches[1]] = $matches[2];
                unset($blockers[$idx]);
                continue;
            }
            $selectorSyntaxFinder = SelectorSyntaxFinder::fromExpression($line);
            if ($selectorSyntaxFinder !== \false) {
                foreach ($selectorSyntaxFinder->getAttributes() as $attr) {
                    foreach ($attr->getFunctions() as $fn) {
                        $fn->setVariableResolver($this);
                    }
                }
                $selectorSyntaxFinder->setFastHtmlTag($this->headlessContentBlocker);
                unset($blockers[$idx]);
                $this->selectorSyntaxFinder[] = $selectorSyntaxFinder;
                $this->originalExpressions[] = $line;
            }
        }
        foreach ($blockers as $expression) {
            $this->regexp['wildcard'][$expression] = Utils::createRegexpPatternFromWildcardName($expression);
            $this->originalExpressions[] = $expression;
        }
        // Force to wildcard all hosts look like a `contains`
        foreach ($blockers as $host) {
            $this->regexp['contains'][$host] = Utils::createRegexpPatternFromWildcardName('*' . $host . '*');
        }
        $this->expressionToStrposCache = null;
    }
    /**
     * Check if the blockable matches the expression loosely.
     *
     * @param string $str
     */
    public function matchesLoose($str)
    {
        foreach ($this->getRegularExpressions() as $expression => $regexp) {
            if ($this->matchesExpressionLoose($expression, $str)) {
                return $expression;
            }
        }
        return null;
    }
    /**
     * It is a performance-boost to extract the searchable strings for this expression, so we can first check for simple `contains` pattern
     * with `strpos` instead of expensive `preg_match`.
     *
     * @param string $expression
     * @param string $str
     */
    public function matchesExpressionLoose($expression, $str)
    {
        if ($this->expressionToStrposCache === null) {
            $this->expressionToStrposCache = [];
            foreach (\array_keys($this->regexp['wildcard']) as $originalExpression) {
                if (\preg_match_all('/([^\\*]{1,})/m', $originalExpression, $expressionStrposMatch, \PREG_SET_ORDER, 0) && \count($expressionStrposMatch) > 0) {
                    $this->expressionToStrposCache[$originalExpression] = \array_column($expressionStrposMatch, 1);
                } else {
                    // @codeCoverageIgnoreStart
                    $this->expressionToStrposCache[$originalExpression] = \false;
                    // @codeCoverageIgnoreEnd
                }
            }
        }
        $expressionStrpos = $this->expressionToStrposCache[$expression] ?? \false;
        if ($expressionStrpos) {
            foreach ($expressionStrpos as $expressionStrposSingle) {
                if (\strpos($str, $expressionStrposSingle) === \false) {
                    return \false;
                }
            }
        }
        return \true;
    }
    /**
     * Find a `SyntaxSelectorFinder` for a given `AbstractMatch`.
     *
     * @param AbstractMatch $match
     */
    public function findSelectorSyntaxFinderForMatch($match)
    {
        foreach ($this->getSelectorSyntaxFinder() as $selectorSyntaxFinder) {
            /**
             * The match to use
             *
             * @var SelectorSyntaxMatch
             */
            $useMatch = null;
            // With the introduction of `findPotentialSelectorSyntaxFindersForMatch`, we have no test covering this
            // case but we need it for backward compatibility.
            // @codeCoverageIgnoreStart
            if ($match instanceof SelectorSyntaxMatch) {
                $useMatch = $match;
            } elseif (\count($match->getAttributes()) > 0) {
                $useMatch = new SelectorSyntaxMatch($selectorSyntaxFinder, $match->getOriginalMatch(), $match->getTag(), $match->getAttributes(), \array_keys($match->getAttributes())[0]);
            } else {
                // It can never `matchesAttributes` as we do not have any attribute
                return null;
            }
            // @codeCoverageIgnoreEnd
            if ($selectorSyntaxFinder->matchesAttributesLoose($useMatch->getOriginalMatch()) && $selectorSyntaxFinder->matchesAttributes($useMatch->getAttributes(), $useMatch)) {
                return $selectorSyntaxFinder;
            }
        }
        return null;
    }
    /**
     * Get the blocker ID. This is added as a custom HTML attribute to the blocked
     * element so your frontend can e.g. add a visual content blocker.
     *
     * @return int|string|null
     */
    public abstract function getBlockerId();
    /**
     * Get required IDs. This is added as a custom HTML attribute to the blocked
     * element so your frontend can determine which items by ID are needed so the
     * item can be unblocked.
     *
     * @return (int|string)[]
     */
    public abstract function getRequiredIds();
    /**
     * The criteria type. This is added as a custom HTML attribute to the blocked
     * element so your frontend can determine the origin for the `getRequiredIds`.
     * E.g. differ between TCF vendors and another custom criteria.
     *
     * @return string
     */
    public abstract function getCriteria();
    /**
     * Get the priority of the blockable. It gets reused in `BlockablesSorter` to sort the blockables.
     * If lower, the blockable gets sorted to the top.
     *
     * @return int
     */
    public function getPriority()
    {
        $legalBasis = $this->getData(self::DATA_KEY_LEGAL_BASIS);
        if (\is_array($legalBasis) && \in_array('legitimate-interest', $legalBasis, \true)) {
            return 5;
        }
        return 10;
    }
    /**
     * Determine if this blockable should be blocked.
     */
    public function hasBlockerId()
    {
        return $this->getBlockerId() !== null;
    }
    /**
     * Getter.
     *
     * @codeCoverageIgnore
     */
    public function getSelectorSyntaxFinder()
    {
        return $this->selectorSyntaxFinder;
    }
    /**
     * Getter.
     *
     * @return string[]
     */
    public function getRegularExpressions()
    {
        return $this->regexp['wildcard'] ?? [];
    }
    /**
     * Getter.
     *
     * @return string[]
     */
    public function getContainsRegularExpressions()
    {
        return $this->regexp['contains'] ?? [];
    }
    /**
     * Getter.
     *
     * @return string[]
     */
    public function getOriginalExpressions()
    {
        return $this->originalExpressions;
    }
    // Documented in SelectorSyntaxAttributeFunctionVariableResolver
    // @codeCoverageIgnoreStart
    public function getVariables()
    {
        return $this->variables;
    }
    // @codeCoverageIgnoreEnd
    // Documented in SelectorSyntaxAttributeFunctionVariableResolver
    public function getVariable($variableName, $default = '')
    {
        return $this->variables[$variableName] ?? $default;
    }
    /**
     * Allows to set additional data for this blockable.
     *
     * @param string $key
     * @param mixed $data
     */
    public function setData($key, $data)
    {
        $this->data[$key] = $data;
    }
    /**
     * Get additional data.
     *
     * @param string $key
     */
    public function getData($key)
    {
        return $this->data[$key] ?? null;
    }
}
