<?php

namespace Filament\Actions;

use BackedEnum;
use Closure;
use Filament\Actions\Concerns\InteractsWithRecord;
use Filament\Actions\View\ActionsIconAlias;
use Filament\Support\Components\Contracts\HasEmbeddedView;
use Filament\Support\Components\ViewComponent;
use Filament\Support\Concerns\HasBadge;
use Filament\Support\Concerns\HasBadgeTooltip;
use Filament\Support\Concerns\HasColor;
use Filament\Support\Concerns\HasExtraAttributes;
use Filament\Support\Concerns\HasIcon;
use Filament\Support\Concerns\HasIconPosition;
use Filament\Support\Concerns\HasIconSize;
use Filament\Support\Concerns\HasTooltip;
use Filament\Support\Contracts\ScalableIcon;
use Filament\Support\Enums\IconSize;
use Filament\Support\Enums\Width;
use Filament\Support\Facades\FilamentIcon;
use Filament\Support\Icons\Heroicon;
use Filament\Support\View\ComponentAttributeBag as FilamentComponentAttributeBag;
use Filament\Support\View\Concerns\CanGenerateBadgeHtml;
use Filament\Support\View\Concerns\CanGenerateButtonHtml;
use Filament\Support\View\Concerns\CanGenerateDropdownItemHtml;
use Filament\Support\View\Concerns\CanGenerateIconButtonHtml;
use Filament\Support\View\Concerns\CanGenerateLinkHtml;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Illuminate\View\ComponentAttributeBag;
use LogicException;

class ActionGroup extends ViewComponent implements Arrayable, HasEmbeddedView
{
    use CanGenerateBadgeHtml;
    use CanGenerateButtonHtml;
    use CanGenerateDropdownItemHtml;
    use CanGenerateIconButtonHtml;
    use CanGenerateLinkHtml;
    use Concerns\BelongsToGroup;
    use Concerns\BelongsToLivewire;
    use Concerns\BelongsToSchemaComponent;
    use Concerns\BelongsToTable;
    use Concerns\CanBeHidden {
        isHidden as baseIsHidden;
    }
    use Concerns\CanBeLabeledFrom;
    use Concerns\CanBeOutlined;
    use Concerns\HasDropdown;
    use Concerns\HasGroupedIcon;
    use Concerns\HasLabel;
    use Concerns\HasSize;
    use HasBadge;
    use HasBadgeTooltip;
    use HasColor;
    use HasExtraAttributes;
    use HasIcon {
        HasIcon::getIcon as getBaseIcon;
    }
    use HasIconPosition;
    use HasIconSize;
    use HasTooltip;
    use InteractsWithRecord;

    public const BADGE_VIEW = 'filament::components.badge';

    public const BUTTON_VIEW = 'filament::components.button.index';

    public const BUTTON_GROUP_VIEW = 'filament::components.button.group';

    public const GROUPED_VIEW = 'filament::components.dropdown.list.item';

    public const ICON_BUTTON_VIEW = 'filament::components.icon-button';

    public const LINK_VIEW = 'filament::components.link';

    /**
     * @var array<Action | ActionGroup>
     */
    protected array $actions;

    /**
     * @var array<string, Action>
     */
    protected array $flatActions;

    protected string $evaluationIdentifier = 'group';

    protected string $viewIdentifier = 'group';

    /**
     * @var view-string
     */
    protected string $triggerView;

    /**
     * @var view-string | Closure | null
     */
    protected string | Closure | null $defaultTriggerView = null;

    /**
     * @var array<array<mixed> | Closure>
     */
    protected array $extraDropdownAttributes = [];

    /**
     * @param  array<Action | ActionGroup>  $actions
     */
    public function __construct(array $actions)
    {
        $this->actions($actions);
    }

    /**
     * @param  array<Action | ActionGroup>  $actions
     */
    public static function make(array $actions): static
    {
        $static = app(static::class, ['actions' => $actions]);
        $static->configure();

        return $static;
    }

    protected function setUp(): void
    {
        parent::setUp();

        $this->defaultTriggerView(static::ICON_BUTTON_VIEW);
    }

    /**
     * @param  array<Action | ActionGroup>  $actions
     */
    public function actions(array $actions): static
    {
        $this->actions = [];
        $this->flatActions = [];

        foreach ($actions as $action) {
            $action->group($this);

            if ($action instanceof ActionGroup) {
                $action->defaultDropdownPlacement('right-top');

                $this->flatActions = [
                    ...$this->flatActions,
                    ...$action->getFlatActions(),
                ];
            } else {
                $this->flatActions[$action->getName()] = $action;
            }

            $this->actions[] = $action;
        }

        return $this;
    }

    public function isBadge(): bool
    {
        return $this->getTriggerView() === static::BADGE_VIEW;
    }

    public function badge(string | Closure | null $badge = null): static
    {
        if (func_num_args() === 0) {
            /** @phpstan-ignore-next-line */
            return $this->triggerView(static::BADGE_VIEW);
        }

        $this->badge = $badge;

        return $this;
    }

    public function button(): static
    {
        return $this->triggerView(static::BUTTON_VIEW);
    }

    public function buttonGroup(): static
    {
        return $this->triggerView(static::BUTTON_GROUP_VIEW);
    }

    public function isButton(): bool
    {
        return $this->getTriggerView() === static::BUTTON_VIEW;
    }

    public function isButtonGroup(): bool
    {
        return $this->getTriggerView() === static::BUTTON_GROUP_VIEW;
    }

    public function grouped(): static
    {
        return $this->triggerView(static::GROUPED_VIEW);
    }

    public function iconButton(): static
    {
        return $this->triggerView(static::ICON_BUTTON_VIEW);
    }

    public function isIconButton(): bool
    {
        return $this->getTriggerView() === static::ICON_BUTTON_VIEW;
    }

    public function link(): static
    {
        return $this->triggerView(static::LINK_VIEW);
    }

    public function isLink(): bool
    {
        return $this->getTriggerView() === static::LINK_VIEW;
    }

    public function getLabel(): string | Htmlable | null
    {
        $label = $this->evaluate($this->label) ?? __('filament-actions::group.trigger.label');

        return is_string($label) && $this->shouldTranslateLabel
            ? __($label)
            : $label;
    }

    /**
     * @return array<Action | ActionGroup>
     */
    public function getActions(): array
    {
        return array_map(
            fn (Action | ActionGroup $action) => match (true) {
                $action instanceof Action => $action->defaultView($this->isButtonGroup() ? $action::BUTTON_VIEW : $action::GROUPED_VIEW),
                $action instanceof ActionGroup => $action->defaultTriggerView($this->isButtonGroup() ? $action::BUTTON_VIEW : $action::GROUPED_VIEW),
            },
            $this->actions,
        );
    }

    /**
     * @return array<string, Action>
     */
    public function getFlatActions(): array
    {
        return $this->flatActions;
    }

    public function hasNonBulkAction(): bool
    {
        foreach ($this->getFlatActions() as $action) {
            if (! $action->isBulk()) {
                return true;
            }
        }

        return false;
    }

    public function getIcon(): string | BackedEnum | Htmlable
    {
        return $this->getBaseIcon() ?? FilamentIcon::resolve(ActionsIconAlias::ACTION_GROUP) ?? Heroicon::EllipsisVertical;
    }

    public function isHidden(): bool
    {
        if (! $this->hasTable()) {
            return $this->resolveIsHidden();
        }

        if (! $this->prepareVisibilityCache()) {
            return $this->resolveIsHidden();
        }

        return $this->cachedIsHidden ??= $this->resolveIsHidden();
    }

    protected function resolveIsHidden(): bool
    {
        if ($this->baseIsHidden()) {
            return true;
        }

        foreach ($this->getActions() as $action) {
            if ($action->isHiddenInGroup()) {
                continue;
            }

            return false;
        }

        return true;
    }

    /**
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        $icon = $this->getIcon();

        if ($icon instanceof ScalableIcon) {
            $icon = $icon->getIconForSize($this->getIconSize() ?? IconSize::Medium);
        } elseif ($icon instanceof BackedEnum) {
            $icon = $icon->value;
        }

        return [
            'actions' => collect($this->getActions())->toArray(),
            'color' => $this->getColor(),
            'dropdownMaxHeight' => $this->getDropdownMaxHeight(),
            'dropdownOffset' => $this->getDropdownOffset(),
            'dropdownPlacement' => $this->getDropdownPlacement(),
            'dropdownWidth' => $this->getDropdownWidth(),
            'extraAttributes' => $this->getExtraAttributes(),
            'hasDropdown' => $this->hasDropdown(),
            'hasDropdownFlip' => $this->hasDropdownFlip(),
            'hasDropdownTeleport' => $this->hasDropdownTeleport(),
            'icon' => $icon,
            'iconPosition' => $this->getIconPosition(),
            'iconSize' => $this->getIconSize(),
            'isOutlined' => $this->isOutlined(),
            'label' => $this->getLabel(),
            'size' => $this->getSize(),
            'tooltip' => $this->getTooltip(),
            'triggerView' => $this->getTriggerView(),
            'view' => $this->hasView() ? $this->getView() : null,
        ];
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public static function fromArray(array $data): static
    {
        $static = static::make(
            array_map(
                fn (array $action): Action | ActionGroup => match (array_key_exists('actions', $action)) {
                    true => ActionGroup::fromArray($action),
                    false => Action::fromArray($action),
                },
                $data['actions'] ?? [],
            ),
        );

        $view = $data['view'] ?? null;

        if (filled($view) && ($static->getView() !== $view) && static::isViewSafe($view)) {
            $static->view($view);
        }

        $triggerView = $data['triggerView'] ?? null;

        if (filled($triggerView) && ($static->getTriggerView() !== $triggerView) && static::isViewSafe($triggerView)) {
            $static->triggerView($triggerView);
        }

        if (filled($size = $data['size'] ?? null)) {
            $static->size($size);
        }

        $static->color($data['color'] ?? null);
        $static->dropdown($data['hasDropdown'] ?? true);
        $static->dropdownFlip($data['hasDropdownFlip'] ?? false);
        $static->dropdownTeleport($data['hasDropdownTeleport'] ?? false);
        $static->dropdownMaxHeight($data['dropdownMaxHeight'] ?? null);
        $static->dropdownOffset($data['dropdownOffset'] ?? null);
        $static->dropdownPlacement($data['dropdownPlacement'] ?? null);
        $static->dropdownWidth($data['dropdownWidth'] ?? null);
        $static->extraAttributes($data['extraAttributes'] ?? []);
        $static->icon($data['icon'] ?? null);
        $static->iconPosition($data['iconPosition'] ?? null);
        $static->iconSize($data['iconSize'] ?? null);
        $static->label($data['label'] ?? null);
        $static->outlined($data['isOutlined'] ?? false);
        $static->tooltip($data['tooltip'] ?? null);

        return $static;
    }

    /**
     * @param  view-string  $view
     */
    protected static function isViewSafe(string $view): bool
    {
        return Str::startsWith($view, 'filament::');
    }

    protected function resolveDefaultClosureDependencyForEvaluationByName(string $parameterName): array
    {
        return match ($parameterName) {
            'livewire' => [$this->getLivewire()],
            'model' => [$this->getModel()],
            'mountedActions' => [$this->getLivewire()->getMountedActions()],
            'record' => [$this->getRecord()],
            'schema' => [$this->getSchemaContainer()],
            'schemaComponent', 'component' => [$this->getSchemaComponent()],
            'schemaOperation', 'context', 'operation' => [$this->getSchemaContainer()?->getOperation() ?? $this->getSchemaComponent()?->getContainer()->getOperation()],
            'schemaGet', 'get' => [$this->getSchemaComponent()->makeGetUtility()],
            'schemaComponentState', 'state' => [$this->getSchemaComponentState()],
            'schemaState' => [$this->getSchemaState()],
            'table' => [$this->getTable()],
            default => parent::resolveDefaultClosureDependencyForEvaluationByName($parameterName),
        };
    }

    public function getSchemaComponentState(): mixed
    {
        $schemaContainer = $this->getSchemaContainer();

        while ($schemaContainer) {
            $parentComponent = $schemaContainer->getParentComponent();

            if (! $parentComponent) {
                break;
            }

            if ($parentComponent->hasStatePath()) {
                return $parentComponent->getState();
            }

            $schemaContainer = $parentComponent->getContainer();
        }

        return $this->getSchemaComponent()?->getState();
    }

    public function getSchemaState(): mixed
    {
        $schemaContainer = $this->getSchemaContainer();

        while ($schemaContainer) {
            if (filled($schemaContainer->getStatePath(isAbsolute: false))) {
                return $schemaContainer->getStateSnapshot();
            }

            $parentComponent = $schemaContainer->getParentComponent();

            if (! $parentComponent) {
                return $schemaContainer->getStateSnapshot();
            }

            $schemaContainer = $parentComponent->getContainer();
        }

        return null;
    }

    protected function resolveDefaultClosureDependencyForEvaluationByType(string $parameterType): array
    {
        $record = is_a($parameterType, Model::class, allow_string: true) ? $this->getRecord() : null;

        return match ($parameterType) {
            Model::class, ($record instanceof Model) ? $record::class : null => [$record],
            default => parent::resolveDefaultClosureDependencyForEvaluationByType($parameterType),
        };
    }

    public function toEmbeddedHtml(): string
    {
        if ($this->isButtonGroup()) {
            ob_start(); ?>

            <div class="fi-btn-group">
                <?php foreach ($this->getActions() as $action) { ?>
                    <?php if ($action->isVisible()) { ?>
                        <?= $action->toHtml() ?>
                    <?php } ?>
                <?php } ?>
            </div>

            <?php return ob_get_clean();
        }

        if (! $this->hasDropdown()) {
            return collect($this->getActions())
                ->filter(fn (Action | ActionGroup $action): bool => $action->isVisible())
                ->map(fn (Action | ActionGroup $action): string => $action->toHtml())
                ->implode('');
        }

        $actionLists = [];
        $singleActions = [];

        foreach ($this->getActions() as $action) {
            if ($action->isHidden()) {
                continue;
            }

            if ($action instanceof ActionGroup && (! $action->hasDropdown())) {
                if (count($singleActions)) {
                    $actionLists[] = $singleActions;
                    $singleActions = [];
                }

                $actionLists[] = array_filter(
                    $action->getActions(),
                    fn ($action): bool => $action->isVisible(),
                );
            } else {
                $singleActions[] = $action;
            }
        }

        if (count($singleActions)) {
            $actionLists[] = $singleActions;
        }

        $maxHeight = $this->getDropdownMaxHeight();
        $width = $this->getDropdownWidth();

        $panelAttributes = (new FilamentComponentAttributeBag)
            ->class([
                'fi-dropdown-panel',
                ($width instanceof Width) ? "fi-width-{$width->value}" : (is_string($width) ? $width : ''),
                'fi-scrollable' => $maxHeight,
            ])
            ->style([
                ('max-height: ' . e($maxHeight)) => $maxHeight,
            ]);

        ob_start(); ?>

        <div
            x-data="filamentDropdown"
            <?= $this->getExtraDropdownAttributeBag()->class(['fi-dropdown'])->toHtml() ?>
        >
            <div
                x-on:keyup.enter="toggle($event)"
                x-on:keyup.space="toggle($event)"
                x-on:mousedown="if ($event.button === 0) toggle($event)"
                class="fi-dropdown-trigger"
            >
                <?= $this->toTriggerHtml() ?>
            </div>

            <div
                x-cloak
                x-float.placement.<?= $this->getDropdownPlacement() ?? 'bottom-start' ?><?= $this->hasDropdownFlip() ? '.flip' : '' ?><?= $this->hasDropdownTeleport() ? '.teleport' : '' ?>.offset="{ offset: <?= $this->getDropdownOffset() ?? 8 ?> }"
                x-ref="panel"
                x-transition:enter-start="fi-opacity-0"
                x-transition:leave-end="fi-opacity-0"
                <?= $panelAttributes->toHtml() ?>
            >
                <?php foreach ($actionLists as $actions) { ?>
                    <div class="fi-dropdown-list">
                        <?php foreach ($actions as $action) { ?>
                            <?= $action->toHtml() ?>
                        <?php } ?>
                    </div>
                <?php } ?>
            </div>
        </div>

        <?php return ob_get_clean();
    }

    /**
     * @param  view-string | null  $view
     */
    public function triggerView(?string $view): static
    {
        if ($view === null) {
            return $this;
        }

        $this->triggerView = $view;

        return $this;
    }

    /**
     * @param  view-string | Closure | null  $view
     */
    public function defaultTriggerView(string | Closure | null $view): static
    {
        $this->defaultTriggerView = $view;

        return $this;
    }

    /**
     * @return view-string
     */
    public function getTriggerView(): string
    {
        if (isset($this->triggerView)) {
            return $this->triggerView;
        }

        if (filled($defaultView = $this->getDefaultTriggerView())) {
            return $defaultView;
        }

        throw new LogicException('Class [' . static::class . '] extends [' . ActionGroup::class . '] but does not have a [$triggerView] property defined.');
    }

    /**
     * @return view-string | null
     */
    public function getDefaultTriggerView(): ?string
    {
        return $this->evaluate($this->defaultTriggerView);
    }

    public function toTriggerHtml(): string
    {
        return match ($this->getTriggerView()) {
            static::BADGE_VIEW => $this->toBadgeTriggerHtml(),
            static::BUTTON_VIEW => $this->toButtonTriggerHtml(),
            static::GROUPED_VIEW => $this->toGroupedTriggerHtml(),
            static::ICON_BUTTON_VIEW => $this->toIconButtonTriggerHtml(),
            static::LINK_VIEW => $this->toLinkTriggerHtml(),
            default => $this->renderTrigger()->render(),
        };
    }

    protected function toBadgeTriggerHtml(): string
    {
        return $this->generateBadgeHtml(
            attributes: (new FilamentComponentAttributeBag)
                ->merge($this->getExtraAttributes(), escape: false)
                ->class(['fi-ac-badge-group']),
            color: $this->getColor(),
            icon: $this->getIcon(),
            iconPosition: $this->getIconPosition(),
            iconSize: $this->getIconSize(),
            label: $this->getLabel(),
            size: $this->getSize(),
            tag: 'button',
            tooltip: $this->getTooltip(),
        );
    }

    protected function toButtonTriggerHtml(): string
    {
        return $this->generateButtonHtml(
            attributes: (new FilamentComponentAttributeBag)
                ->merge($this->getExtraAttributes(), escape: false)
                ->class(['fi-ac-btn-group']),
            badge: $badge = $this->getBadge(),
            badgeColor: $this->getBadgeColor($badge),
            color: $this->getColor(),
            icon: $this->getIcon(),
            iconPosition: $this->getIconPosition(),
            iconSize: $this->getIconSize(),
            isLabelSrOnly: $this->isLabelHidden(),
            isOutlined: $this->isOutlined(),
            label: $this->getLabel(),
            labeledFromBreakpoint: $this->getLabeledFromBreakpoint(),
            size: $this->getSize(),
            tag: 'button',
            tooltip: $this->getTooltip(),
        );
    }

    protected function toGroupedTriggerHtml(): string
    {
        return $this->generateDropdownItemHtml(
            attributes: (new FilamentComponentAttributeBag)
                ->merge($this->getExtraAttributes(), escape: false)
                ->class(['fi-ac-grouped-group']),
            badge: $badge = $this->getBadge(),
            badgeColor: $this->getBadgeColor($badge),
            badgeTooltip: $this->getBadgeTooltip($badge),
            color: $this->getColor(),
            icon: $this->getIcon(),
            iconSize: $this->getIconSize(),
            label: $this->getLabel(),
            tag: 'button',
            tooltip: $this->getTooltip(),
        );
    }

    protected function toIconButtonTriggerHtml(): string
    {
        return $this->generateIconButtonHtml(
            attributes: (new FilamentComponentAttributeBag)
                ->merge($this->getExtraAttributes(), escape: false)
                ->class(['fi-ac-icon-btn-group']),
            badge: $badge = $this->getBadge(),
            badgeColor: $this->getBadgeColor($badge),
            color: $this->getColor(),
            icon: $this->getIcon(),
            iconSize: $this->getIconSize(),
            label: $this->getLabel(),
            size: $this->getSize(),
            tag: 'button',
            tooltip: $this->getTooltip(),
        );
    }

    protected function toLinkTriggerHtml(): string
    {
        return $this->generateLinkHtml(
            attributes: (new FilamentComponentAttributeBag)
                ->merge($this->getExtraAttributes(), escape: false)
                ->class(['fi-ac-link-group']),
            badge: $badge = $this->getBadge(),
            badgeColor: $this->getBadgeColor($badge),
            color: $this->getColor(),
            icon: $this->getIcon(),
            iconPosition: $this->getIconPosition(),
            iconSize: $this->getIconSize(),
            isLabelSrOnly: $this->isLabelHidden(),
            label: $this->getLabel(),
            size: $this->getSize(),
            tag: 'button',
            tooltip: $this->getTooltip(),
        );
    }

    public function renderTrigger(): View
    {
        return view(
            $this->getTriggerView(),
            [
                'attributes' => new FilamentComponentAttributeBag,
                ...$this->extractPublicMethods(),
                $this->viewIdentifier => $this,
                ...$this->viewData,
            ],
        );
    }

    public function getClone(): static
    {
        $clone = clone $this;
        $clone->cloneActions();

        return $clone;
    }

    protected function cloneActions(): void
    {
        $this->actions = array_map(
            fn (Action | ActionGroup $action): Action | ActionGroup => $action->getClone()->group($this),
            $this->actions,
        );

        $this->flatActions = array_map(
            fn (Action $action): Action => $action->getClone()->group($this),
            $this->flatActions,
        );
    }

    /**
     * @param  array<mixed> | Closure  $attributes
     */
    public function extraDropdownAttributes(array | Closure $attributes, bool $merge = false): static
    {
        // Security: Attribute values are not escaped when rendered. Never
        // pass unsanitized user input as attribute names or values.

        if ($merge) {
            $this->extraDropdownAttributes[] = $attributes;
        } else {
            $this->extraDropdownAttributes = [$attributes];
        }

        return $this;
    }

    /**
     * @return array<mixed>
     */
    public function getExtraDropdownAttributes(): array
    {
        $temporaryAttributeBag = new FilamentComponentAttributeBag;

        foreach ($this->extraDropdownAttributes as $extraDropdownAttributes) {
            $temporaryAttributeBag = $temporaryAttributeBag->merge($this->evaluate($extraDropdownAttributes), escape: false);
        }

        return $temporaryAttributeBag->getAttributes();
    }

    public function getExtraDropdownAttributeBag(): ComponentAttributeBag
    {
        return new FilamentComponentAttributeBag($this->getExtraDropdownAttributes());
    }
}
