<?php

namespace Livewire\Blaze\Folder;

use Illuminate\Support\Str;
use Livewire\Blaze\BladeRenderer;
use Livewire\Blaze\BladeService;
use Livewire\Blaze\Parser\Attribute;
use Livewire\Blaze\Parser\Nodes\ComponentNode;
use Livewire\Blaze\Parser\Nodes\SlotNode;
use Livewire\Blaze\Parser\Nodes\TextNode;
use Livewire\Blaze\Support\ComponentSource;

/**
 * Performs compile-time folding of a component by rendering it with placeholder substitution.
 *
 * Dynamic attributes and slot contents are replaced with placeholders before rendering,
 * then restored afterward so they evaluate at runtime.
 */
class Foldable
{
    protected array $attributeByPlaceholder = [];
    protected array $slotByPlaceholder = [];
    protected int $placeholderIndex = 0;

    protected ComponentNode $renderable;
    protected string $html;

    public function __construct(
        protected ComponentNode $node,
        protected ComponentSource $source,
        protected BladeRenderer $renderer,
        protected BladeService $blade,
    ) {
    }

    /**
     * Fold the component: render with placeholders, then restore dynamic values.
     */
    public function fold(): string
    {
        $this->renderable = new ComponentNode(
            name: $this->node->name,
            prefix: $this->node->prefix,
            attributeString: '',
            children: [],
            selfClosing: $this->node->selfClosing,
            parentsAttributes: $this->node->parentsAttributes,
        );

        $this->setupAttributes();
        $this->setupSlots();
        $this->mergeAwareProps();

        $this->html = $this->renderer->render($this->renderable, $this->source);
        
        $this->processUncompiledAttributes();
        $this->restorePlaceholders();
        $this->wrapWithAwareMacros();

        return $this->html;
    }

    /**
     * Replace dynamic attributes with placeholders, keep static ones as-is.
     */
    protected function setupAttributes(): void
    {
        foreach ($this->node->attributes as $key => $attribute) {
            if (! $attribute->isStaticValue()) {
                $placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_';

                $this->attributeByPlaceholder[$placeholder] = $attribute;

                $this->renderable->attributes[$key] = new Attribute(
                    name: $attribute->name,
                    value: $placeholder,
                    propName: $attribute->propName,
                    prefix: '',
                    dynamic: false,
                    quotes: '"',
                );
            } else {
                $this->renderable->attributes[$key] = clone $attribute;
            }
        }
    }

    /**
     * Replace slot children with placeholders for rendering, storing originals for restoration.
     */
    protected function setupSlots(): void
    {
        $slots = [];
        $looseContent = [];

        foreach ($this->node->children as $child) {
            if ($child instanceof SlotNode) {
                $placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_';

                $this->slotByPlaceholder[$placeholder] = $child;

                $slots[$child->name] = new SlotNode(
                    name: $child->name,
                    attributeString: $child->attributeString,
                    slotStyle: $child->slotStyle,
                    children: [new TextNode($placeholder)],
                    prefix: $child->prefix,
                    closeHasName: $child->closeHasName,
                    attributes: $child->attributes,
                );
            } else {
                $looseContent[] = $child;
            }
        }

        // Synthesize a default slot from loose content when there's not an explicit one
        if ($looseContent && ! isset($slots['slot'])) {
            $placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_';

            $defaultSlot = new SlotNode(
                name: 'slot',
                attributeString: '',
                slotStyle: 'standard',
                children: $looseContent,
                prefix: 'x-slot',
            );

            $this->slotByPlaceholder[$placeholder] = $defaultSlot;

            $slots['slot'] = new SlotNode(
                name: 'slot',
                attributeString: '',
                slotStyle: 'standard',
                children: [new TextNode($placeholder)],
                prefix: 'x-slot',
            );
        }

        $this->renderable->children = $slots;
    }

    /**
     * Merge @aware-declared props from parent attributes into the renderable node.
     */
    protected function mergeAwareProps(): void
    {
        $aware = $this->source->directives->array('aware') ?? [];
        
        foreach ($aware as $prop => $default) {
            if (is_int($prop)) {
                $prop = $default;
                $default = null;
            }

            if (isset($this->renderable->attributes[$prop])) {
                continue;
            }
            
            if (isset($this->node->parentsAttributes[$prop])) {
                $attribute = $this->node->parentsAttributes[$prop];

                if (! $attribute->isStaticValue()) {
                    $placeholder = 'BLAZE_PLACEHOLDER_' . $this->placeholderIndex++ . '_';

                    $this->attributeByPlaceholder[$placeholder] = $attribute;

                    $this->renderable->attributes[$prop] = new Attribute(
                        name: $prop,
                        value: $placeholder,
                        propName: $prop,
                        dynamic: false,
                    );
                } else {
                    $this->renderable->attributes[$prop] = new Attribute(
                        name: $attribute->name,
                        value: $attribute->value,
                        propName: $attribute->propName,
                        dynamic: $attribute->dynamic,
                        quotes: $attribute->quotes,
                        prefix: $attribute->prefix,
                    );
                }
            } else if ($default !== null) {
                // TODO: test this, we might not need to add the default attributes because they will be added inside the component?
                // When the value is null and no parent provides a value, we intentionally
                // skip adding the attribute. This lets @aware and @props handle defaults
                // at runtime, matching the non-folded behavior. Adding an attribute with
                // null value would render as prop="" in HTML, corrupting null to empty string.
                $this->renderable->attributes[$prop] = new Attribute(
                    name: $prop,
                    value: $default,
                    propName: $prop,
                    dynamic: false,
                );
            }
        }
    }

    /**
     * Convert [BLAZE_ATTR:...] markers into conditional PHP for dynamic attributes.
     */
    protected function processUncompiledAttributes(): void
    {
        $this->html = preg_replace_callback('/\[BLAZE_ATTR:(BLAZE_PLACEHOLDER_[0-9]+_):(.+?)\](\r?\n)?/', function ($matches) {
            $attribute = $this->attributeByPlaceholder[$matches[1]];
            $name = $matches[2];

            if ($attribute->bound()) {
                // x-data and wire:* get empty string for true, others get key name
                $booleanValue = ($name === 'x-data' || str_starts_with($name, 'wire:')) ? "''" : "'".addslashes($name)."'";

                return '<'.'?php if (($__blazeAttr = '.$attribute->value.') !== false && !is_null($__blazeAttr)): ?'.'>'
                . $name.'="<'.'?php echo e($__blazeAttr === true ? '.$booleanValue.' : $__blazeAttr); ?'.'>"'
                .'<'.'?php endif; unset($__blazeAttr); ?'.'>' . (isset($matches[3]) ? $matches[3] . $matches[3] : '');
            } else {
                return $name.'="'.$attribute->value.'"';
            }
        }, $this->html);
    }

    /**
     * Replace all placeholders with their original dynamic values or slot content.
     */
    protected function restorePlaceholders(): void
    {
        // Attribute placeholders inside PHP blocks need raw values
        $this->html = preg_replace_callback('/<\?php.*?\?>/s', function ($match) {
            $content = $match[0];

            foreach ($this->attributeByPlaceholder as $placeholder => $attribute) {
                $value = $attribute->bound() ? $attribute->value : $this->blade->compileAttributeEchos($attribute->value);

                $content = str_replace("'" . $placeholder . "'", $value, $content);
            }

            return $content;
        }, $this->html);

        // Attribute placeholders in HTML context need Blade echo syntax
        foreach ($this->attributeByPlaceholder as $placeholder => $attribute) {
            $value = $attribute->bound() ? '{{ ' . $attribute->value . ' }}' : $attribute->value;

            $this->html = str_replace($placeholder, $value, $this->html);
        }

        foreach ($this->slotByPlaceholder as $placeholder => $slot) {
            // In Blade slots are rendered using output buffer and echo syntax,
            // we need to replicate both here to handle whitespace correctly.
            $this->html = preg_replace_callback('/' . $placeholder . '(\r?\n)?/', function ($match) use ($slot) {
                $whitespace = $match[1] ?? '';

                return '<?php ob_start(); ?>' . $slot->content() . '<?php echo trim(ob_get_clean()); ?>' . $whitespace . $whitespace;
            }, $this->html);
        }
    }

    /**
     * Wrap output with pushConsumableComponentData calls if descendants use @aware.
     */
    protected function wrapWithAwareMacros(): void
    {
        if (! $this->renderable->attributes) {
            return;
        }

        if (! $this->node->hasAwareDescendants) {
            return;
        }

        $data = [];

        foreach ($this->node->attributes as $attribute) {
            $data[] = $this->blade->compileAttribute($attribute);
        }

        $dataString = implode(', ', $data);

        $this->html = Str::wrap($this->html,
            '<?php $__blaze->pushData(['.$dataString.']); $__env->pushConsumableComponentData(['.$dataString.']); ?>',
            '<?php $__blaze->popData(); $__env->popConsumableComponentData(); ?>',
        );
    }
}
