<?php

namespace Filament\Schemas\Components\Concerns;

use Closure;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Schema;
use Illuminate\Contracts\Support\Htmlable;

trait HasChildComponents
{
    /**
     * @var array<string, array<Component | Action | ActionGroup | string | Htmlable> | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null>
     */
    protected array $childComponents = [];

    /**
     * @var array<Schema> | null
     */
    protected ?array $cachedDefaultChildSchemas = null;

    /**
     * @var array<string, Schema>
     */
    protected array $cachedChildSchemas = [];

    /**
     * @param  array<Component | Action | ActionGroup | string | Htmlable> | Closure  $components
     */
    public function components(array | Closure $components): static
    {
        $this->childComponents($components);

        return $this;
    }

    /**
     * @param  array<Component | Action | ActionGroup | string | Htmlable> | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null  $components
     */
    public function childComponents(array | Schema | Component | Action | ActionGroup | string | Htmlable | Closure | null $components, string $key = 'default'): static
    {
        $this->childComponents[$key] = $components;
        unset($this->cachedChildSchemas[$key]);

        return $this;
    }

    /**
     * @param  array<Component | Action | ActionGroup | string | Htmlable> | Closure  $components
     */
    public function schema(array | Closure $components): static
    {
        $this->childComponents($components);

        return $this;
    }

    /**
     * @return array<Component | Action | ActionGroup>
     */
    public function getChildComponents(?string $key = null): array
    {
        return $this->getChildSchema($key)->getComponents();
    }

    /**
     * @return array<Component | Action | ActionGroup | string | Htmlable> | Schema
     */
    public function getDefaultChildComponents(): array | Schema
    {
        return $this->evaluate($this->childComponents['default'] ?? []) ?? [];
    }

    /**
     * @param  array-key  $key
     */
    public function getChildSchema($key = null): ?Schema
    {
        if (filled($key) && ! array_key_exists($key, $this->childComponents)) {
            return $this->getCachedDefaultChildSchemas()[$key] ?? null;
        }

        if (filled($key) && array_key_exists($key, $cachedDefaultChildSchemas = $this->getCachedDefaultChildSchemas())) {
            return $cachedDefaultChildSchemas[$key];
        }

        $key ??= 'default';

        $isCacheable = ($key !== 'default')
            && filled($this->childComponents[$key] ?? null)
            && ! (($this->childComponents[$key] ?? null) instanceof Closure);

        if ($isCacheable && isset($this->cachedChildSchemas[$key])) {
            return $this->cachedChildSchemas[$key];
        }

        $components = ($key === 'default')
            ? $this->getDefaultChildComponents()
            : $this->evaluate($this->childComponents[$key] ?? []) ?? [];

        if (blank($components)) {
            return ($key === 'default')
                ? $this->configureChildSchema(
                    $this->makeChildSchema($key),
                    $key,
                )
                : null;
        }

        if ($components instanceof Schema) {
            $childSchema = $this->configureChildSchema(
                $components
                    ->livewire($this->getLivewire())
                    ->parentComponent($this),
                $key,
            );
        } else {
            $childSchema = $this->configureChildSchema(
                $this->makeChildSchema($key)
                    ->components($components),
                $key,
            );
        }

        if ($isCacheable) {
            $this->cachedChildSchemas[$key] = $childSchema;
        }

        return $childSchema;
    }

    /**
     * @deprecated Use `getChildSchema()` instead.
     *
     * @param  array-key  $key
     */
    public function getChildComponentContainer($key = null): ?Schema
    {
        return $this->getChildSchema($key);
    }

    protected function makeChildSchema(string $key): Schema
    {
        return Schema::make($this->getLivewire())
            ->parentComponent($this);
    }

    protected function configureChildSchema(Schema $schema, string $key): Schema
    {
        return $schema;
    }

    /**
     * @return array<Schema>
     */
    public function getChildSchemas(bool $withHidden = false): array
    {
        if ((! $withHidden) && $this->isHidden()) {
            return [];
        }

        return [
            ...(array_key_exists('default', $this->childComponents) ? $this->getCachedDefaultChildSchemas() : []),
            ...array_reduce(
                array_keys($this->childComponents),
                function (array $carry, string $key): array {
                    if ($key === 'default') {
                        return $carry;
                    }

                    if ($container = $this->getChildSchema($key)) {
                        $carry[$key] = $container;
                    }

                    return $carry;
                },
                initial: [],
            ),
        ];
    }

    /**
     * @deprecated Use `getChildSchemas()` instead.
     *
     * @return array<Schema>
     */
    public function getChildComponentContainers(bool $withHidden = false): array
    {
        return $this->getChildSchemas($withHidden);
    }

    /**
     * @return array<Schema>
     */
    public function getDefaultChildSchemas(): array
    {
        return ['default' => $this->getChildSchema()];
    }

    /**
     * @return array<Schema>
     */
    protected function getCachedDefaultChildSchemas(): array
    {
        if (($this->cachedDefaultChildSchemas !== null) && $this->areCachedDefaultChildSchemasFresh()) {
            return $this->cachedDefaultChildSchemas;
        }

        return $this->cachedDefaultChildSchemas = $this->getDefaultChildSchemas();
    }

    /**
     * Components whose child schemas are derived from state, such as repeaters,
     * can override this method to compare the current state against a snapshot
     * taken when the cache was built, so that the cache invalidates itself when
     * the state changes, without an explicit `clearCachedChildSchemas()` call.
     */
    protected function areCachedDefaultChildSchemasFresh(): bool
    {
        return true;
    }

    public function clearCachedChildSchemas(): void
    {
        $this->cachedDefaultChildSchemas = null;
        $this->cachedChildSchemas = [];
    }

    /**
     * @deprecated Use `clearCachedChildSchemas()` instead.
     */
    public function clearCachedDefaultChildSchemas(): void
    {
        $this->clearCachedChildSchemas();
    }

    protected function cloneChildComponents(): static
    {
        $this->cachedChildSchemas = [];

        foreach ($this->childComponents as $key => $childComponents) {
            if (is_array($childComponents)) {
                $this->childComponents[$key] = array_map(
                    fn (Component | Action | ActionGroup | string | Htmlable $component): Component | Action | ActionGroup | string | Htmlable => match (true) {
                        $component instanceof Component, $component instanceof Action, $component instanceof ActionGroup => $component->getClone(),
                        default => $component,
                    },
                    $childComponents,
                );
            } elseif (
                ($childComponents instanceof Component) ||
                ($childComponents instanceof Action) ||
                ($childComponents instanceof ActionGroup) ||
                ($childComponents instanceof Schema)
            ) {
                $this->childComponents[$key] = $childComponents->getClone();
            }
        }

        return $this;
    }
}
