<?php

namespace KupShop\FeedGeneratorBundle\Configuration;

use KupShop\FeedGeneratorBundle\Configuration\Nodes\AttributeNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\ConditionNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\CycleNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\DescriptionNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\ElementNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\ExpressionNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\INode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\MultilineNode;
use KupShop\FeedGeneratorBundle\Configuration\Nodes\ValueNode;

class V8BuildingVisitor implements IConfigurationVisitor
{
    protected bool $indent = false;

    protected $outputJs;

    protected $indentDepth = 0;

    public function setIndent(bool $indent): self
    {
        $this->indent = $indent;

        return $this;
    }

    public function initialize(): void
    {
        $this->indentDepth = 0;
        $this->outputJs = '';

        // define __PHP object for relevant properties from PHP
        $this->addLine('var __PHP = {};');
        $this->addLine('__PHP.__properties = PHP.__properties;');
        $this->addLine('__PHP.__methods = PHP.__methods;');
        $this->addLine('__PHP.__contextProperties = PHP.__contextProperties;');

        $this->addLine('var __buildXML = function() {');

        // clear custom global variables (see https://github.com/phpv8/v8js/pull/340)
        // skip properties, methods and contextProperties and: exit,sleep,print,var_dump,require,global,PHP,isFalse,strval,__XMLWriter,__buildXML,__reportCounters,console
        $this->addLine("for (const prop in global) { if (!Object.keys(__PHP.__properties).includes(prop) && !Object.keys(__PHP.__methods).includes(prop) && !Object.keys(__PHP.__contextProperties).includes(prop) && !['exit', 'sleep', 'print', 'var_dump', 'require', 'global', '__PHP', 'isFalse', 'strval' , '__XMLWriter', '__buildXML', '__reportCounters', 'reportCounter', 'console', 'dayjs', 'utils'].includes(prop)) { delete global[prop]; }}");

        $this->addLine('var __xmlWriter = new __XMLWriter('.($this->indent ? 'true' : 'false').');');
    }

    public function getOutput(): string
    {
        $this->indentDepth = 0;
        $this->addLine('return  __xmlWriter.toString();');
        $this->addLine('};');

        return $this->outputJs;
    }

    /**
     * @param INode[] $nodes
     */
    private function processNodes(array $nodes, int $depth = 0)
    {
        $close = true;
        foreach ($nodes as $node) {
            $this->indentDepth = $depth;
            if ($node->accept($this) && $node->hasNodes()) {
                $this->processNodes($node->getNodes(), $depth + 1);
            }
            $this->indentDepth = $depth;
            $node->accept($this, $close);
        }
    }

    public function visitConfiguration(Configuration $configuration, &$handle = null)
    {
        $this->processNodes($configuration->getNodes());
    }

    public function visitAttribute(AttributeNode $node, &$handle)
    {
        if (empty($node->getValue())) {
            return null;
        }
        if ($handle) {
            $this->addLine('__xmlWriter.endAttribute();');

            return null;
        }
        $this->addLine('__xmlWriter.startAttribute('.json_encode($node->getValue()).');');

        return true;
    }

    public function visitCondition(ConditionNode $node, &$handle)
    {
        if (empty($node->getValue())) {
            return false;
        }
        if ($handle) {
            $this->addLine('}');

            return false;
        }

        // Immediately invoked function execution
        $this->addLine('if ((function(){');
        // maintain backward compatibility: zero in a string should evaluate to false
        $this->addLine('    const __condResult = '.$node->getValue().';');
        $this->addLine('    return __condResult === "0" ? false : __condResult;');
        $this->addLine('})()) {');

        return true;
    }

    public function visitCycle(CycleNode $node, &$handle)
    {
        if (empty($node->getValue())) {
            return false;
        }
        if ($handle) {
            $this->addLine('}');
            //            $this->addLine('}})();');

            return false;
        }

        // temporary variables approach
        // for `photos.slice(0, Math.min(photos.length, 10))` to work is still required to have separate wrapper for each object in MultiWrapper
        //        $iterableName = '___iterable'.$this->indentDepth;
        //        $indexName = !empty($node->getIndexAccessName()) ? $node->getIndexAccessName() : '___index'.$this->indentDepth;;
        //        $this->addLine('(function(){ const '.$iterableName.' = (function(){ return '.$node->getValue().'; })()');
        //        $this->addLine('for (const '.$indexName.' in '.$iterableName.') {');
        //        if (!empty($node->getValueAccessName())) {
        //            $this->addLine('const '.$node->getValueAccessName().' = '.$iterableName.'['.$indexName.'];');
        //        }

        // Object.entries() approach
        if (empty($node->getValueAccessName()) && empty($node->getIndexAccessName())) {
            // iterate without access to index/value
            $this->addLine('for (const [] of Object.entries((function(){');
            $this->addLine('    return '.$node->getValue().';');
            $this->addLine('})())) {');
        } else {
            $this->addLine('for (const ['.($node->getIndexAccessName() ?: 'undefined').', '.($node->getValueAccessName() ?: 'undefined').'] of Object.entries((function(){');
            $this->addLine('    return '.$node->getValue().';');
            $this->addLine('})())) {');
        }

        return true;
    }

    public function visitElement(ElementNode $node, &$handle)
    {
        if (empty($node->getValue())) {
            return false;
        }
        if ($handle) {
            $this->addLine('__xmlWriter.endElement();');

            return false;
        }

        // TODO: support dynamic element/attribute names?
        $this->addLine('__xmlWriter.startElement('.json_encode($node->getValue()).');');

        return true;
    }

    public function visitExpression(ExpressionNode $node, &$handle): bool
    {
        if ($handle || empty($node->getValue())) {
            return false;
        }
        // Immediately invoked function execution
        $this->addLine('__xmlWriter.text((function(){');
        $this->addLine('    return '.$node->getValue().';');
        $this->addLine('})());');

        return false;
    }

    public function visitMultiline(MultilineNode $node, &$handle): bool
    {
        if ($handle || empty($node->getValue())) {
            return false;
        }
        // Immediately invoked function execution
        $this->addLine('__xmlWriter.text((function(){');
        // indent content
        $this->addLine('    '.join(PHP_EOL.str_repeat('    ', $this->indentDepth + 1), explode(PHP_EOL, $node->getValue())));
        $this->addLine('    return "";');
        $this->addLine('})());');

        return false;
    }

    public function visitValue(ValueNode $node, &$handle)
    {
        if (!$handle) {
            $this->addLine('__xmlWriter.text('.json_encode($node->getValue() ?? '').');');
        }

        return false;
    }

    public function visitDescription(DescriptionNode $node, &$handle)
    {
        return true;
    }

    protected function addLine(string $line): void
    {
        $this->outputJs .= PHP_EOL.str_repeat('    ', $this->indentDepth).$line;
    }
}
