<?php
namespace DevOwl\HeadlessContentBlocker\matcher;

use DevOwl\FastHtmlTag\finder\match\StyleInlineMatch;
use DevOwl\FastHtmlTag\finder\match\TagAttributeMatch;
use DevOwl\FastHtmlTag\Utils;
use DevOwl\HeadlessContentBlocker\BlockedResult;
use DevOwl\HeadlessContentBlocker\Constants;
use DevOwl\HeadlessContentBlocker\finder\match\StyleInlineAttributeMatch;
use DevOwl\HeadlessContentBlocker\Markup;
use Sabberworm\CSS\CSSList\Document;
use Sabberworm\CSS\Parser;
use Sabberworm\CSS\Property\Import;
use Sabberworm\CSS\RuleSet\AtRuleSet;
use Sabberworm\CSS\Value\CSSString;
use Sabberworm\CSS\Value\URL;

/**
 * Takes a match and checks for blocked URLs. If there got something blocked, you can
 * extract two CSS documents.
 */
class CssBlocker {
    const EXTRACT_COMPLETE_AT_RULE_INSTEAD_OF_SINGLE_PROPERTY = ['font-face'];

    const DEFAULT_DUMMY_URL = 'https://assets.devowl.io/packages/devowl-wp/headless-content-blocker/';

    private $extract;

    private $match;

    private $matcher;

    /**
     * Blocked URLs.
     *
     * @var string[]
     */
    private $blockedUrls = [];

    /**
     * C'tor.
     *
     * @param boolean $extract
     * @param StyleInlineAttributeMatch|StyleInlineMatch $match
     * @param AbstractMatcher $matcher
     */
    public function __construct($extract, $match, $matcher) {
        $this->extract = $extract;
        $this->match = $match;
        $this->matcher = $matcher;
    }

    /**
     * Parse a CSS and remove blocked URLs.
     */
    public function parse() {
        $this->blockedUrls = [];

        // Original document (only CSS rules without blocked URLs)
        $parser = new Parser($this->getStyle());
        $document = $parser->parse();

        // Extracted document (only CSS rules with blocked URLs)
        if ($this->extract) {
            $parser = new Parser($this->getStyle());
            $extractedDocument = $parser->parse();
        } else {
            $extractedDocument = null;
        }

        list(
            $setUrlChanges,
            $removedFromOriginalDocument,
            $removedRuleSetsFromOriginalDocument,
        ) = $this->generateLocationChangeSet($document);

        // Prepare extracted document
        if ($extractedDocument !== null) {
            CssHelper::removeNonBlockedRulesFromDocument(
                $extractedDocument,
                $removedFromOriginalDocument,
                $removedRuleSetsFromOriginalDocument
            );
        }

        // Finally, block the URLs
        CssHelper::applyLocationChangeSet($setUrlChanges, $extractedDocument === null ? $document : $extractedDocument);

        // Remove blanks
        CssHelper::removeBlanksFromCSSList($document);
        CssHelper::removeBlanksFromCSSList($extractedDocument);

        return [$document, $extractedDocument, $this->blockedUrls];
    }

    /**
     * Generate a list of changed `URL`s with their new URL. It also respects rule sets which needs to be completely
     * blocked and moved to the extracted document (e.g. `@font-face`).
     *
     * @param Document $document
     */
    protected function generateLocationChangeSet($document) {
        $removed = [];
        $removedRuleSets = [];

        // Delay the changes to the URLs so we can correctly extract the inline script (compare values)
        $setUrlChanges = [];

        // Iterate known rule-sets which need to be completely extracted when one value inside it is blocked (e.g. `@font-face`)
        foreach ($document->getAllRuleSets() as $ruleSet) {
            if (
                $ruleSet instanceof AtRuleSet &&
                in_array($ruleSet->atRuleName(), self::EXTRACT_COMPLETE_AT_RULE_INSTEAD_OF_SINGLE_PROPERTY, true)
            ) {
                foreach ($ruleSet->getRules() as $rule) {
                    foreach (CssHelper::flatRuleValues($rule) as $ruleValue) {
                        $ruleResult = $this->generateLocationChangeSetForSingleValue(
                            $document,
                            $ruleValue,
                            false,
                            $removed,
                            $setUrlChanges
                        );

                        if ($ruleResult) {
                            $removedRuleSets[] = $ruleSet;

                            // Special case: Extract the complete rule set
                            if ($this->extract) {
                                $document->remove($ruleSet);
                            }
                        }
                    }
                }
            }
        }

        // Iterate each value in our stylesheet
        CssHelper::walkFlatValues($document, function ($values) use ($document, &$removed, &$setUrlChanges) {
            foreach ($values as $val) {
                $this->generateLocationChangeSetForSingleValue(
                    $document,
                    $val,
                    $this->extract,
                    $removed,
                    $setUrlChanges
                );
            }
        });

        return [$setUrlChanges, $removed, $removedRuleSets];
    }

    /**
     * Generate a list of changed `URL`s with their new URL for a single value inside our parsed document.
     *
     * @param Document $document
     * @param Value $val
     * @param boolean $extract
     * @param array $removed
     * @param array $setUrlChanges
     */
    protected function generateLocationChangeSetForSingleValue($document, $val, $extract, &$removed, &$setUrlChanges) {
        /**
         * The found URL instance.
         *
         * @var URL
         */
        $location = null;

        if ($val instanceof Import) {
            $location = $val->getLocation();
            $dummyFileName = 'dummy.css';
        } elseif ($val instanceof URL) {
            $location = $val;
            $dummyFileName = 'dummy.png';
        }

        if ($location !== null) {
            $url = html_entity_decode($location->getURL()->getString());
            $ruleResult = $this->createResultForSingleUrl($url);

            if ($ruleResult->isBlocked()) {
                // Remove from original document
                if ($extract) {
                    foreach (CssHelper::removeValueFromDocument($val, $document) as $remove) {
                        $removed[] = $remove;
                    }
                }

                // Adjust URL
                $setUrlChanges[] = [
                    $location,
                    new CSSString($this->generateDummyUrl($ruleResult, $dummyFileName, $url)),
                ];
                return true;
            }
        }

        return false;
    }

    /**
     * Check if a given URL is blocked.
     *
     * @param string $url
     * @return BlockedResult
     */
    public function createResultForSingleUrl($url) {
        $cb = $this->matcher->getHeadlessContentBlocker();
        $result = new BlockedResult('style', [], Markup::persist($this->match->getOriginalMatch(), $cb));

        // Find all public content blockers and check URL
        foreach ($cb->getBlockables() as $blockable) {
            // Iterate all wildcarded URLs
            foreach ($blockable->getContainsRegularExpressions() as $expression => $regex) {
                // m: Enable multiline search
                if (preg_match($regex . 'm', $url)) {
                    // This link is definitely blocked by configuration
                    $result->setBlocked([$blockable]);
                    $result->setBlockedExpressions([$expression]);
                    $this->blockedUrls[] = $url;
                    $this->blockedUrls = array_values(array_unique($this->blockedUrls));
                    break 2;
                }
            }
        }

        return $cb->runInlineStyleBlockRuleCallback($result, $url, $this->matcher, $this->match);
    }

    /**
     * Get the new URL for a blocked value.
     *
     * @param BlockedResult $result
     * @param string $dummyFileName
     * @param string $originalUrl
     */
    protected function generateDummyUrl($result, $dummyFileName, $originalUrl) {
        $pseudoMatch = new TagAttributeMatch(null, null, 'style', [], null);

        $this->matcher->applyConsentAttributes($result, $pseudoMatch);
        $pseudoMatch->setAttribute(
            Constants::URL_QUERY_ARG_ORIGINAL_URL_IN_STYLE,
            sprintf('%s-', base64_encode($originalUrl))
        ); // add trailing `-` to avoid removal of `==`]

        $configuredUrl = $this->matcher->getHeadlessContentBlocker()->getInlineStyleDummyUrlPath();
        return Utils::addParametersToUrl(
            ($configuredUrl !== null ? $configuredUrl : self::DEFAULT_DUMMY_URL) . $dummyFileName,
            $pseudoMatch->getAttributes()
        );
    }

    /**
     * Get the style as string from our match.
     */
    protected function getStyle() {
        if ($this->match instanceof StyleInlineAttributeMatch) {
            return $this->match->getStyle(true);
        } else {
            return $this->match->getStyle();
        }
    }
}
