<?php

namespace Apexl\Io\Component\Components;

use Apexl\Io\Component\Collections\ComponentCollection;
use Apexl\Io\Component\Collections\ComponentPropertyCollection;
use Apexl\Io\Component\Factories\ComponentPropertyFactory;
use Apexl\Io\Component\Interfaces\ComponentInterface;
use Apexl\Io\HookManager;
use Arrayy\Type\StringCollection;
use JsonSerializable;
use ReflectionClass;

abstract readonly class Component implements JsonSerializable, ComponentInterface
{
    final public const string HOOK__CLASSES = 'component.classes';
    final public const string HOOK__STYLES = 'component.styles';
    private const string PROP__STYLE = 'style';

    public string $id;
    public string $name;
    private ComponentPropertyFactory $propertyFactory;
    public ComponentPropertyCollection $props;
    public ComponentCollection $components;
    public ComponentCollection $supportComponents;
    protected StringCollection $classes;

    public function __construct(
        ?ComponentPropertyFactory $propertyFactory = null,
        ?ComponentPropertyCollection $props = null,
        ?ComponentCollection $components = null,
        ?StringCollection $classes = null,
        ?string $componentId = null
    ) {
        // io-display components instantiate via the factory but still call
        // parent::__construct() with no arguments (the established pattern), and
        // some are created with `new`. Keep the collaborators optional and
        // self-resolving so both styles work, while still honouring explicit
        // DI/factory injection when supplied.
        $this->propertyFactory = $propertyFactory ?? \Apexl\Io\container()->get(ComponentPropertyFactory::class);
        $this->props = $props ?? new ComponentPropertyCollection();
        $this->components = $components ?? new ComponentCollection();
        $this->supportComponents = new ComponentCollection();
        $this->classes = $classes ?? new StringCollection();

        // `name` is set eagerly only when the class declares a NAME constant.
        // Otherwise it is left uninitialised so a component may still override it
        // once via setName() (a readonly property tolerates a single first-write
        // from class scope); the default is resolved lazily in toArray(). `id`
        // is derived from the resolved short name up-front.
        $shortName = defined('static::NAME')
            ? static::NAME
            : new ReflectionClass($this)->getShortName();
        if (defined('static::NAME')) {
            $this->name = static::NAME;
        }
        $this->id = $componentId ?? hash('XXH3', sprintf('%s_%s', $shortName, microtime()));
    }

    /**
     * Overrides the component name the frontend uses to pick a Vue component.
     * Honours readonly by writing once (no-op if already set).
     */
    public function setName(string $name): static
    {
        if (!isset($this->name)) {
            $this->name = $name;
        }

        return $this;
    }

    /**
     * Bootstrap column width, expressed as a `col-{size}-{cols}` class on the
     * component wrapper.
     */
    public function setColWidth(string $size, int|string $cols): static
    {
        return $this->addClass(sprintf('col-%s-%s', $size, $cols));
    }

    /**
     * Secondary components related to the parent (e.g. a CardTile header modal,
     * a table's filter form). Rendered alongside the main component tree under
     * `supportComponents`.
     */
    public function addSupportComponent(ComponentInterface $component): static
    {
        $this->supportComponents->append($component);

        return $this;
    }

    public function setProperty(
        string $name,
        mixed $value,
    ): static {
        $this->props->set(
            $name,
            $this->propertyFactory->make($name, $value),
        );

        return $this;
    }

    public function resetClasses(): static
    {
        $this->classes->clear();

        return $this;
    }

    public function addClass(string $class): static
    {
        StringCollection::createFromArray(explode(' ', $class))
            ->map('trim')
            ->filter()
            ->each(fn(string $class) => $this->classes->hasValue($class) || $this->classes->append($class));

        return $this;
    }

    public function removeProperty(string $name): static
    {
        $this->props->clear($name);

        return $this;
    }

    public function addComponents(ComponentCollection|array $components): static
    {
        foreach ($components as $component) {
            $this->addComponent($component);
        }

        return $this;
    }

    public function addComponent(ComponentInterface $component): static
    {
        $this->components->append($component);

        return $this;
    }

    public function getProperty(string $name): ?ComponentProperty
    {
        return $this->props->get($name);
    }

    public function hasProperty(string $name): bool
    {
        return $this->props->has($name);
    }

    public function jsonSerialize(): array
    {
        return $this->toArray();
    }

    protected function toArray(): array
    {
        // Resolve the lazy default for components that neither declare a NAME
        // constant nor call setName() (readonly single first-write).
        if (!isset($this->name)) {
            $this->name = new ReflectionClass($this)->getShortName();
        }

        $hookManager = \Apexl\Io\container()->get(HookManager::class);

        $classes = $hookManager->processHook(
            self::HOOK__CLASSES,
            $this->classes->toArray(),
            $this->name,
            $this->id,
        );

        $props = $this->props->toArray();
        $props[self::PROP__STYLE] = $hookManager->processHook(
            self::HOOK__STYLES,
            $props[self::PROP__STYLE] ?? [],
            $this->name,
            $this->id,
        );

        return [
            'name' => $this->name,
            'classes' => $classes,
            'id' => $this->id,
            'props' => $props,
            'components' => $this->components
                ->values()
                ->toArray(),
            'supportComponents' => $this->supportComponents
                ->values()
                ->toArray(),
        ];
    }

}
