<?php

namespace Solspace\Freeform\Fields\Implementations\Pro;

use craft\helpers\Html;
use GraphQL\Type\Definition\Type as GQLType;
use Solspace\Freeform\Attributes\Field\Type;
use Solspace\Freeform\Attributes\Property\DefaultValue;
use Solspace\Freeform\Attributes\Property\Implementations\Attributes\TableAttributesTransformer;
use Solspace\Freeform\Attributes\Property\Implementations\Table\TableTransformer;
use Solspace\Freeform\Attributes\Property\Input;
use Solspace\Freeform\Attributes\Property\Limitation;
use Solspace\Freeform\Attributes\Property\Section;
use Solspace\Freeform\Attributes\Property\Translatable;
use Solspace\Freeform\Attributes\Property\ValueTransformer;
use Solspace\Freeform\Attributes\Property\VisibilityFilter;
use Solspace\Freeform\Events\Fields\CompileFieldAttributesEvent;
use Solspace\Freeform\Fields\AbstractField;
use Solspace\Freeform\Fields\Interfaces\EncryptionInterface;
use Solspace\Freeform\Fields\Interfaces\ExtraFieldInterface;
use Solspace\Freeform\Fields\Interfaces\MultiDimensionalValueInterface;
use Solspace\Freeform\Fields\Interfaces\MultiValueInterface;
use Solspace\Freeform\Fields\Interfaces\TableInterface;
use Solspace\Freeform\Fields\Properties\Table\TableLayout;
use Solspace\Freeform\Fields\Traits\EncryptionTrait;
use Solspace\Freeform\Fields\Traits\MultipleValueTrait;
use Solspace\Freeform\Form\Form;
use Solspace\Freeform\Library\Attributes\Attributes;
use Solspace\Freeform\Library\Attributes\TableAttributesCollection;
use Symfony\Component\Serializer\Annotation\Ignore;
use yii\base\Event;

#[Type(
    name: 'Table',
    typeShorthand: 'table',
    iconPath: __DIR__.'/../Icons/table.svg',
    previewTemplatePath: __DIR__.'/../PreviewTemplates/table.ejs',
)]
class TableField extends AbstractField implements MultiValueInterface, MultiDimensionalValueInterface, ExtraFieldInterface, EncryptionInterface, TableInterface
{
    use EncryptionTrait;
    use MultipleValueTrait;

    public const EVENT_COMPILE_TABLE_ATTRIBUTES = 'compile-table-attributes';

    public const COLUMN_TYPE_STRING = 'text';
    public const COLUMN_TYPE_DROPDOWN = 'select';
    public const COLUMN_TYPE_CHECKBOX = 'checkbox';
    public const COLUMN_TYPE_RADIO = 'radio';
    public const COLUMN_TYPE_TEXTAREA = 'textarea';

    public const LIMIT_ROWS_NO_LIMIT = '';
    public const LIMIT_ROWS_MINIMUM = 'min';
    public const LIMIT_ROWS_MAXIMUM = 'max';
    public const LIMIT_ROWS_RANGE = 'range';
    public const LIMIT_ROWS_EXACT = 'exact';

    public array $columns = [];

    #[ValueTransformer(TableTransformer::class)]
    #[Translatable]
    #[Input\Table(
        label: 'Table Layout',
        value: [],
        options: [
            [
                'value' => self::COLUMN_TYPE_STRING,
                'label' => 'Text',
            ],
            [
                'value' => self::COLUMN_TYPE_TEXTAREA,
                'label' => 'Textarea',
            ],
            [
                'value' => self::COLUMN_TYPE_CHECKBOX,
                'label' => 'Checkbox',
            ],
            [
                'value' => self::COLUMN_TYPE_RADIO,
                'label' => 'Radios',
            ],
            [
                'value' => self::COLUMN_TYPE_DROPDOWN,
                'label' => 'Dropdown',
            ],
        ],
    )]
    protected TableLayout $tableLayout;

    #[Limitation('props.table', 'javascript')]
    #[DefaultValue('props.table.javascript')]
    #[Input\Boolean('Use built-in javascript for adding and removing rows')]
    protected bool $useScript = true;

    #[Section('limits', 'Limit Rows')]
    #[Limitation('props.table', 'limitRows')]
    #[DefaultValue('props.table.limitRows')]
    #[Input\Select(
        label: 'Limit Rows',
        instructions: 'Set limits on the number of rows, including minimum, maximum, exact number, or range.',
        options: [
            self::LIMIT_ROWS_NO_LIMIT => 'Do not limit',
            self::LIMIT_ROWS_MINIMUM => 'A minimum of…',
            self::LIMIT_ROWS_MAXIMUM => 'A maximum of…',
            self::LIMIT_ROWS_RANGE => 'A range of…',
            self::LIMIT_ROWS_EXACT => 'Exactly…',
        ]
    )]
    protected string $limitRows = '';

    #[Section('limits')]
    #[VisibilityFilter('properties.limitRows === "'.self::LIMIT_ROWS_MINIMUM.'" || properties.limitRows === "'.self::LIMIT_ROWS_RANGE.'"')]
    #[Limitation('props.table', 'minRows')]
    #[DefaultValue('props.table.minRows')]
    #[Input\Integer(
        label: 'Minimum number of rows',
        instructions: 'Set the minimum number of rows. At least this many rows will be shown by default; rows cannot be removed below this count.',
    )]
    protected ?int $minRows = null;

    #[Section('limits')]
    #[VisibilityFilter('properties.limitRows === "'.self::LIMIT_ROWS_MAXIMUM.'" || properties.limitRows === "'.self::LIMIT_ROWS_RANGE.'" || properties.limitRows === ""')]
    #[Limitation('props.table', 'maxRows')]
    #[DefaultValue('props.table.maxRows')]
    #[Input\Integer(
        label: 'Maximum number of rows',
        instructions: 'Set the maximum number of rows that can be added to the table.',
    )]
    protected ?int $maxRows = null;

    #[Section('limits')]
    #[VisibilityFilter('properties.limitRows === "'.self::LIMIT_ROWS_EXACT.'"')]
    #[Limitation('props.table', 'exactRows')]
    #[DefaultValue('props.table.exactRows')]
    #[Input\Integer(
        label: 'Exact number of rows',
        instructions: 'Set the exact number of rows. This many rows will be shown with no option to add or remove.',
    )]
    protected ?int $exactRows = null;

    #[Limitation('props.table', 'addButtonLabel')]
    #[DefaultValue('props.table.addButtonLabel')]
    #[Translatable]
    #[Input\Text(
        label: 'Add Button Label',
        instructions: 'Set the label for the add button.',
    )]
    protected string $addButtonLabel = 'Add';

    #[Limitation('props.table', 'addButtonMarkup')]
    #[DefaultValue('props.table.addButtonMarkup')]
    #[Translatable]
    #[Input\Text(
        label: 'Add Button Markup',
        instructions: 'Set the markup for the add button.',
    )]
    protected ?string $addButtonMarkup = null;

    #[Limitation('props.table', 'removeButtonLabel')]
    #[DefaultValue('props.table.removeButtonLabel')]
    #[Translatable]
    #[Input\Text(
        label: 'Remove Button Label',
        instructions: 'Set the label for the remove button.',
    )]
    protected string $removeButtonLabel = 'Remove';

    #[Limitation('props.table', 'removeButtonMarkup')]
    #[DefaultValue('props.table.removeButtonMarkup')]
    #[Translatable]
    #[Input\Text(
        label: 'Remove Button Markup',
        instructions: 'Set the markup for the remove button.',
    )]
    protected ?string $removeButtonMarkup = null;

    #[Section('attributes')]
    #[ValueTransformer(TableAttributesTransformer::class)]
    #[Limitation('layout.fields.attributes')]
    #[Limitation('props.table', 'tableAttributes')]
    #[Input\Attributes(
        instructions: 'Add attributes to your field elements.',
        tabs: [
            [
                'handle' => 'table',
                'label' => 'Table',
                'previewTag' => 'table',
            ],
            [
                'handle' => 'row',
                'label' => 'Row',
                'previewTag' => 'tr',
            ],
            [
                'handle' => 'column',
                'label' => 'Column',
                'previewTag' => 'td',
            ],
            [
                'handle' => 'label',
                'label' => 'Label',
                'previewTag' => 'label',
            ],
            [
                'handle' => 'input',
                'label' => 'Input',
                'previewTag' => 'input',
            ],
            [
                'handle' => 'dropdown',
                'label' => 'Dropdown',
                'previewTag' => 'select',
            ],
            [
                'handle' => 'checkbox',
                'label' => 'Checkbox',
                'previewTag' => 'input',
            ],
            [
                'handle' => 'addButton',
                'label' => 'Add Button',
                'previewTag' => 'button',
            ],
            [
                'handle' => 'removeButton',
                'label' => 'Remove Button',
                'previewTag' => 'button',
            ],
        ]
    )]
    protected TableAttributesCollection $tableAttributes;

    public function __construct(#[Ignore] Form $form)
    {
        $this->tableAttributes = new TableAttributesCollection();

        parent::__construct($form);
    }

    public function getType(): string
    {
        return self::TYPE_TABLE;
    }

    public function getTableLayout(): TableLayout
    {
        $layout = $this->tableLayout;
        $translation = $this->getTranslationTable()->get('tableLayout');
        if ($translation) {
            $layout = new TableLayout($translation);
        }

        return $layout;
    }

    public function isUseScript(): bool
    {
        return $this->useScript;
    }

    public function getLimitRows(): string
    {
        return $this->limitRows;
    }

    public function getMinRows(): ?int
    {
        return $this->minRows;
    }

    public function getMaxRows(): ?int
    {
        return $this->maxRows;
    }

    public function getExactRows(): ?int
    {
        return $this->exactRows;
    }

    public function getAddButtonLabel(): string
    {
        return $this->getTranslationTable()->get('addButtonLabel', $this->addButtonLabel);
    }

    public function getAddButtonMarkup(): ?string
    {
        return $this->getTranslationTable()->get('addButtonMarkup', $this->addButtonMarkup);
    }

    public function getRemoveButtonLabel(): string
    {
        return $this->getTranslationTable()->get('removeButtonLabel', $this->removeButtonLabel);
    }

    public function getRemoveButtonMarkup(): ?string
    {
        return $this->getTranslationTable()->get('removeButtonMarkup', $this->removeButtonMarkup);
    }

    public function getTableAttributes(): TableAttributesCollection
    {
        $event = new CompileFieldAttributesEvent(
            $this,
            $this->tableAttributes->clone(),
            TableAttributesCollection::class
        );

        Event::trigger($this, self::EVENT_COMPILE_ATTRIBUTES, $event);

        return $event->getAttributes();
    }

    public function setValue(mixed $value): self
    {
        $layout = $this->getTableLayout();

        $this->value = $values = [];
        if (!\is_array($value)) {
            return $this;
        }

        foreach ($value as $row) {
            if (!\is_array($row)) {
                continue;
            }

            $hasSingleValue = false;
            $rowValues = [];
            foreach ($layout as $index => $column) {
                $value = $row[$index] ?? '';
                if ($value) {
                    $hasSingleValue = true;
                }

                $rowValues[$index] = $value;
            }

            if (!$hasSingleValue) {
                continue;
            }

            $values[] = $rowValues;
        }

        $this->value = $values;

        return $this;
    }

    public function getContentGqlType(): array|GQLType
    {
        return GQLType::listOf(GQLType::listOf(GQLType::string()));
    }

    public function getContentGqlMutationArgumentType(): array|GQLType
    {
        $layout = [];
        $textValuesInclude = '';
        $dropdownValuesInclude = '';
        $checkboxValuesInclude = '';

        foreach ($this->getTableLayout() as $column) {
            $type = $column->type ?? self::COLUMN_TYPE_STRING;

            if (self::COLUMN_TYPE_DROPDOWN === $type) {
                $dropdownValues = [];
                $options = explode(';', $column->value);

                foreach ($options as $option) {
                    $dropdownValues[] = '"'.$option.'"';
                }

                if (!empty($dropdownValues)) {
                    $dropdownValuesInclude .= '- "'.$column->label.'" column:'."\n";
                    $dropdownValuesInclude .= '-- Single option value allowed.'."\n";
                    $dropdownValuesInclude .= '-- Options include '.implode(', ', $dropdownValues).'.';
                }

                $layout[] = '"'.$column->label.'"';
            } elseif (self::COLUMN_TYPE_CHECKBOX === $type) {
                $checkboxValuesInclude .= '- "'.$column->label.'" column:'."\n";
                $checkboxValuesInclude .= '-- Single option value allowed.'."\n";
                $checkboxValuesInclude .= '-- Option value is "'.$column->value.'".';

                $layout[] = '"'.$column->label.'"';
            } else {
                $textValuesInclude .= '- "'.$column->label.'" column:'."\n";
                $textValuesInclude .= '-- Single value allowed.'."\n";

                $layout[] = '"'.$column->label.'"';
            }
        }

        $description = [];
        $description[] = $this->getInstructions();
        $description[] = 'Expected layout [['.implode(', ', $layout).']].';
        $description[] = $textValuesInclude;
        $description[] = $dropdownValuesInclude;
        $description[] = $checkboxValuesInclude;
        $description = implode("\n", $description);

        return [
            'name' => $this->getContentGqlHandle(),
            'type' => $this->getContentGqlType(),
            'description' => trim($description),
        ];
    }

    protected function getInputHtml(): string
    {
        $layout = $this->getTableLayout();

        $handle = $this->getHandle();
        $values = $this->getValue();

        if (empty($values)) {
            $initialRowCount = 1;
            $limitRows = $this->getLimitRows();
            if (self::LIMIT_ROWS_EXACT === $limitRows) {
                $exactRows = $this->getExactRows();
                if (null !== $exactRows && $exactRows > 0) {
                    $initialRowCount = $exactRows;
                }
            } elseif (self::LIMIT_ROWS_MINIMUM === $limitRows || self::LIMIT_ROWS_RANGE === $limitRows) {
                $minRows = $this->getMinRows();
                if (null !== $minRows && $minRows > 0) {
                    $initialRowCount = max(1, $minRows);
                }
            }

            $defaultRow = [];
            foreach ($layout as $column) {
                match ($column->type) {
                    self::COLUMN_TYPE_CHECKBOX => $defaultRow[] = null,
                    default => $defaultRow[] = $column->value,
                };
            }

            $values = array_fill(0, $initialRowCount, $defaultRow);
        }

        $attributes = $this->getTableAttributes();

        $tableAttributes = $attributes
            ->getTable()
            ->clone()
            ->replace('data-freeform-table')
        ;

        if ($this->isUseScript()) {
            $tableAttributes->set('data-scripts-enabled', true);
            $tableAttributes->set('data-remove-button-label', $this->getParameters()->removeButtonLabel ?? $this->getRemoveButtonLabel());
        }

        $limitRows = $this->getLimitRows();
        if ('' !== $limitRows) {
            $tableAttributes->set('data-limit-rows', $limitRows);
        }

        switch ($limitRows) {
            case self::LIMIT_ROWS_MINIMUM:
                if (null !== $this->getMinRows()) {
                    $tableAttributes->set('data-min-rows', $this->getMinRows());
                }

                break;

            case self::LIMIT_ROWS_MAXIMUM:
                if (null !== $this->getMaxRows()) {
                    $tableAttributes->set('data-max-rows', $this->getMaxRows());
                }

                break;

            case self::LIMIT_ROWS_RANGE:
                if (null !== $this->getMinRows()) {
                    $tableAttributes->set('data-min-rows', $this->getMinRows());
                }
                if (null !== $this->getMaxRows()) {
                    $tableAttributes->set('data-max-rows', $this->getMaxRows());
                }

                break;

            case self::LIMIT_ROWS_EXACT:
                if (null !== $this->getExactRows()) {
                    $tableAttributes->set('data-exact-rows', $this->getExactRows());
                }

                break;

            default:
                if (null !== $this->getMaxRows()) {
                    $tableAttributes->set('data-max-rows', $this->getMaxRows());
                }

                break;
        }

        $rowAttributes = $attributes->getRow();

        $id = $this->getIdAttribute();
        $output = '<table'.$tableAttributes.'>';

        $output .= '<thead>';
        $output .= '<tr>';

        foreach ($layout as $column) {
            $label = $column->label;
            $defaultValue = $column->value;
            if (self::COLUMN_TYPE_CHECKBOX === $column->type) {
                $defaultValue = $column->checked ? '1' : '0';
            }

            $isRequired = $column->required;

            $thAttributes = new Attributes();
            $thAttributes
                ->merge($attributes->getLabel())
                ->set('data-default-value', $defaultValue)
                ->set('data-column-required', $isRequired)
                ->set(($isRequired ? '+' : '-').'class', 'freeform-required')
            ;

            $output .= '<th'.$thAttributes.'>'.htmlentities($label, \ENT_QUOTES | \ENT_SUBSTITUTE | \ENT_HTML401).'</th>';
        }
        $output .= '<th>&nbsp;</th></tr>';
        $output .= '</thead>';

        $columnAttributes = $attributes->getColumn();

        $output .= '<tbody>';
        $limitRowsForRow = $this->getLimitRows();
        $minRowsForRemove = (self::LIMIT_ROWS_MINIMUM === $limitRowsForRow || self::LIMIT_ROWS_RANGE === $limitRowsForRow)
            ? $this->getMinRows() : null;
        foreach ($values as $rowIndex => $row) {
            $output .= '<tr'.$rowAttributes.'>';

            foreach ($layout as $index => $column) {
                $type = $column->type;
                $defaultValue = $column->value;
                $value = $row[$index] ?? $defaultValue;

                $output .= '<td'.$columnAttributes.'>';

                $name = "{$handle}[{$rowIndex}][{$index}]";

                switch ($type) {
                    case self::COLUMN_TYPE_CHECKBOX:
                        $inputAttributes = $attributes
                            ->getCheckbox()
                            ->clone()
                            ->replace('type', 'checkbox')
                            ->replace('name', $name)
                            ->replace('value', '1')
                            ->replace('data-default-value', $column->checked ? '1' : '0')
                            ->replace('checked', $column->checked)
                        ;

                        $output .= Html::tag('input', '', $inputAttributes->toHtmlTagArray());

                        break;

                    case self::COLUMN_TYPE_DROPDOWN:
                        $dropdownAttributes = $attributes
                            ->getDropdown()
                            ->clone()
                            ->replace('data-default-value', $defaultValue)
                            ->replace('name', $name)
                        ;

                        $options = $column->options;
                        $output .= '<select'.$dropdownAttributes.'>';

                        foreach ($options as $option) {
                            $optionAttributes = (new Attributes())
                                ->set('value', $option)
                                ->set('selected', $option === $value)
                            ;

                            $output .= Html::tag('option', $option, $optionAttributes->toHtmlTagArray());
                        }

                        $output .= '</select>';

                        break;

                    case self::COLUMN_TYPE_RADIO:
                        $radioAttributes = $attributes
                            ->getRadios()
                            ->clone()
                            ->replace('type', 'radio')
                            ->replace('name', $name)
                        ;

                        $options = $column->options;
                        $output .= '<div>';

                        foreach ($options as $radioIndex => $option) {
                            $isChecked = $option === $value;
                            $radioId = 'labeled-'.$handle.'-'.$rowIndex.'-'.$index.'-'.$radioIndex;
                            $optionAttributes = $radioAttributes
                                ->clone()
                                ->replace('id', $radioId)
                                ->replace('value', $option)
                                ->replace('checked', $isChecked)
                                ->replace('data-default-value', $defaultValue)
                            ;

                            $output .= '<div>';
                            $output .= Html::tag('input', '', $optionAttributes->toHtmlTagArray());
                            $output .= Html::tag('label', $option, ['for' => $radioId]);
                            $output .= '</div>';
                        }

                        $output .= '</div>';

                        break;

                    case self::COLUMN_TYPE_TEXTAREA:
                        $inputAttributes = $attributes
                            ->getTextarea()
                            ->clone()
                            ->replace('type', 'text')
                            ->replace('name', $name)
                            ->replace('placeholder', $column->placeholder)
                            ->replace('data-default-value', $defaultValue)
                        ;

                        $output .= Html::tag('textarea', $value, $inputAttributes->toHtmlTagArray());

                        break;

                    case self::COLUMN_TYPE_STRING:
                    default:
                        $inputAttributes = $attributes
                            ->getInput()
                            ->clone()
                            ->replace('type', 'text')
                            ->replace('name', $name)
                            ->replace('value', $value)
                            ->replace('placeholder', $column->placeholder)
                            ->replace('data-default-value', $defaultValue)
                        ;

                        $output .= Html::tag('input', '', $inputAttributes->toHtmlTagArray());

                        break;
                }

                $output .= '</td>';
            }

            $showRemoveButton = self::LIMIT_ROWS_EXACT !== $limitRowsForRow
                && (null === $minRowsForRemove || $rowIndex >= $minRowsForRemove);
            if ($showRemoveButton && $this->getRemoveButtonMarkup()) {
                $output .= '<td'.$columnAttributes.'>';
                $output .= $this->getRemoveButtonMarkup();
                $output .= '</td>';
            } elseif ($showRemoveButton && $this->isUseScript()) {
                $output .= '<td'.$columnAttributes.'>';

                $buttonAttributes = $attributes
                    ->getRemoveButton()
                    ->clone()
                    ->replace('data-freeform-table-remove-row')
                    ->setIfEmpty('type', 'button')
                ;

                $output .= Html::tag(
                    'button',
                    $this->getParameters()->removeButtonLabel ?? $this->getRemoveButtonLabel(),
                    $buttonAttributes->toHtmlTagArray()
                );

                $output .= '</td>';
            } elseif (!$showRemoveButton) {
                $output .= '<td'.$columnAttributes.'></td>';
            }

            $output .= '</tr>';
        }
        $output .= '</tbody>';
        $output .= '</table>';

        $showAddButton = self::LIMIT_ROWS_EXACT !== $this->getLimitRows();
        if ($showAddButton && $this->getAddButtonMarkup()) {
            $output .= $this->getAddButtonMarkup();
        } elseif ($showAddButton && $this->isUseScript()) {
            $buttonAttributes = $attributes
                ->getAddButton()
                ->clone()
                ->replace('data-freeform-table-add-row')
                ->replace('data-target', $id)
                ->setIfEmpty('type', 'button')
            ;

            $output .= Html::tag('div', '');
            $output .= Html::tag(
                'button',
                $this->getParameters()->addButtonLabel ?? $this->getAddButtonLabel(),
                $buttonAttributes->toHtmlTagArray()
            );
        }

        return $output;
    }
}
