<?php

declare(strict_types=1);

namespace FiloBlu\Refilo\Helper;

use Closure;
use DOMDocument;
use Exception;
use Magento\Framework\ObjectManagerInterface;
use Magento\PageBuilder\Model\Dom\Element;
use Magento\PageBuilder\Model\Dom\HtmlDocumentFactory;
use Psr\Log\LoggerInterface;
use Sabberworm\CSS\Parser;
use Sabberworm\CSS\Parsing\SourceException;
use function class_exists;
use function is_a;
use function method_exists;
use function str_replace;

/**
 *
 */
class PageBuilderConverter
{
    /** @var string UTF-32, big-endian */
    public const BOM_UTF_32_BE = b"\x00\x00\xFE\xFF";
    /** @var string UTF-32, little-endian */
    public const BOM_UTF_32_LE = b"\xFF\xFE\x00\x00";
    /** @var string UTF-16, big-endian */
    public const BOM_UTF_16_BE = b"\xFE\xFF";
    /** @var string UTF-16, little-endian */
    public const BOM_UTF_16_LE = b"\xFF\xFE";
    /** @var string UTF-8 */
    public const BOM_UTF8 = b"\xEF\xBB\xBF";
    /** @var null */
    public const PLAIN_TEXT = null;
    /**
     * @var LoggerInterface
     */
    protected $logger;
    /**
     * @var HtmlDocumentFactory | null
     */
    private $documentFactory;

    /**
     * @param ObjectManagerInterface $objectManager
     * @param LoggerInterface $logger
     */
    public function __construct(
        ObjectManagerInterface $objectManager,
        LoggerInterface        $logger
    )
    {
        if (class_exists(HtmlDocumentFactory::class)) {
            $this->documentFactory = $objectManager->get(HtmlDocumentFactory::class);
        }
        $this->logger = $logger;
    }

    /**
     * @param $data
     * @return array|mixed|string|string[]|null
     * @throws SourceException
     */
    public function convert($data)
    {
        if (empty($data)) {
            return $data;
        }

        $data = $this->replaceContentType($data, [
            'data-content-type="filoblu_slider"' => 'data-content-type="slider"',
            'data-content-type="filoblu_slide"' => 'data-content-type="slide"'
        ]);

        if (!$this->documentFactory) {
            return $data;
        }

        if (!method_exists(Element::class, 'getOriginalElement')) {
            return $data;
        }

        try {
            $doc = new DOMDocument('1.0', 'UTF-8');
            if ($this->getBom($data) === self::PLAIN_TEXT) {
                $doc->loadHTML(self::BOM_UTF8 . $data);
            } else {
                $doc->loadHTML($data);
            }
            $doc->normalizeDocument();
        } catch (Exception $throwable) {
            $this->logger->error($throwable->getMessage());
            // Degrade gracefully: keep the original content instead of wiping it.
            return $data;
        }

        $document = $this->documentFactory->create(['document' => $doc->saveHTML()]);

        //Workaround for new version of PageBuilder -Magento 2.4.7
        if (class_exists(\Magento\PageBuilder\Model\Dom\DomDocument::class)) {
            //Using Closure to add method open() to Magento PhpGt DOM Document wrapper class
            $documentOpen = Closure::bind(function () {
                $this->document->open();
            }, $document, $document);
            $documentOpen();
        }

        $body = $document->getContents();
        $styles = $document->querySelectorAll('style');

        $stylesMap = [];

        foreach ($styles as $style) {
            foreach ($this->getStyles($style->getOriginalElement()->nodeValue) as $id => $value) {
                if (isset($stylesMap[$id])) {
                    $stylesMap[$id] .= $value;
                    continue;
                }

                $stylesMap[$id] = $value;
            }
        }

        if (empty($stylesMap)) {
            return $data;
        }

        $toSearch = implode('|', array_keys($stylesMap));
        if (!preg_match('/<body>(.*)<\/body>/s', $body, $matches)) {
            // No <body> wrapper matched: nothing to transform, keep the original content.
            return $data;
        }

        return preg_replace_callback(
            "/data-pb-style=\"($toSearch)\"/m",
            static function ($matches) use ($stylesMap) {
                return 'style="' . trim($stylesMap[$matches[1]], '{}') . '"';
            },
            $matches[1]
        );
    }

    /**
     * @param $data
     * @param $replacement
     * @return array|string|string[]
     */
    public function replaceContentType($data, $replacement)
    {
        return str_replace(
            array_keys($replacement),
            array_values($replacement),
            $data
        );
    }

    /**
     * @param $document
     * @return string|null
     */
    public function getBom($document)
    {
        foreach ([
                     self::BOM_UTF_32_BE => 4,
                     self::BOM_UTF_32_LE => 4,
                     self::BOM_UTF_16_BE => 2,
                     self::BOM_UTF_16_LE => 2,
                     self::BOM_UTF8 => 3
                 ] as $bom => $bytes
        ) {
            if (strncmp($bom, $document, $bytes) === 0) {
                return $bom;
            }
        }

        return self::PLAIN_TEXT;
    }

    /**
     * @param string $css
     * @return array
     * @throws SourceException
     */
    public function getStyles(string $css): array
    {
        $parser = new Parser($css);
        $cssDocument = $parser->parse();

        $lookup = [];

        foreach ($cssDocument->getAllDeclarationBlocks() as $block) {
            $d = array_reduce($block->getRules(), static function ($carry, $a) {

                if(is_a($a, \Sabberworm\CSS\Rule\Rule::class) && !method_exists($carry, '__toString')) {
                   return $carry . $a->render(new \Sabberworm\CSS\OutputFormat());
                }

                return (string)$carry . $a;
            }, '');

            $d = trim($d, '"');
            foreach ($block->getSelectors() as $selector) {
                $selectorName = $selector->getSelector();
                $output = [];
                preg_match('/#html-body\s+\[data-pb-style=(.*)\]/', $selectorName, $output);

                if (!isset($output[1])) {
                    continue;
                }

                $selectorId = trim($output[1], '"');

                if (!isset($lookup[$selectorId])) {
                    $lookup[$selectorId] = $d;
                    continue;
                }

                $lookup[$selectorId] .= $d;
            }
        }

        return $lookup;
    }
}
