<?php

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace TYPO3\CMS\Backend\Form\Element;

use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;

/**
 * Creates a widget with check box elements.
 *
 * This is rendered for config type=select, renderType=selectCheckBox
 */
class SelectCheckBoxElement extends AbstractFormElement
{
    /**
     * Default field wizards enabled for this element.
     *
     * @var array
     */
    protected $defaultFieldWizard = [
        'localizationStateSelector' => [
            'renderType' => 'localizationStateSelector',
        ],
        'otherLanguageContent' => [
            'renderType' => 'otherLanguageContent',
            'after' => [
                'localizationStateSelector',
            ],
        ],
        'defaultLanguageDifferences' => [
            'renderType' => 'defaultLanguageDifferences',
            'after' => [
                'otherLanguageContent',
            ],
        ],
    ];

    public function __construct(
        private readonly IconFactory $iconFactory,
    ) {}

    /**
     * Render check boxes
     *
     * @return array As defined in initializeResultArray() of AbstractNode
     */
    public function render(): array
    {
        $resultArray = $this->initializeResultArray();

        // Field configuration from TCA:
        $parameterArray = $this->data['parameterArray'];
        $config = $parameterArray['fieldConf']['config'];
        $readOnly = (bool)($config['readOnly'] ?? false);

        $selectItems = $config['items'] ?? [];
        if (empty($selectItems)) {
            // Early return in case the field does not contain any items
            return $resultArray;
        }

        // Get item value as array and make unique, which is fine because there can be no duplicates anyway.
        $itemArray = array_flip($parameterArray['itemFormElValue']);

        // Initialize variables and traverse the items
        $groups = [];
        $currentGroup = 0;
        $counter = 0;
        $elementId = StringUtility::getUniqueId('formengine-select-checkbox-');
        foreach ($selectItems as $item) {
            // Non-selectable element:
            if ($item['value'] === '--div--') {
                $selIcon = '';
                if (isset($item['icon']) && $item['icon'] !== 'empty-empty') {
                    $selIcon = FormEngineUtility::getIconHtml($item['icon']);
                }
                $currentGroup++;
                $groups[$currentGroup]['header'] = [
                    'icon' => $selIcon,
                    'title' => $item['label'],
                ];
            } else {
                // Check if some help text is available
                // Help text is expected to be an associative array
                // with two key, "title" and "description"
                // For the sake of backwards compatibility, we test if the help text
                // is a string and use it as a description (this could happen if items
                // are modified with an itemsProcFunc)
                $help = '';
                if (!empty($item['description'])) {
                    if (is_array($item['description'])) {
                        $helpArray = $item['description'];
                    } else {
                        $helpArray['description'] = $item['description'];
                    }
                    $help = $this->wrapInHelp($helpArray);
                }

                // Check if current item is selected. If found, unset the key in the $itemArray.
                $checked = isset($itemArray[$item['value']]);
                if ($checked) {
                    unset($itemArray[$item['value']]);
                }

                // Build item array
                $groups[$currentGroup]['items'][] = [
                    'id' => $elementId . '-item-' . $counter,
                    'name' => $parameterArray['itemFormElName'] . '[' . $counter . ']',
                    'value' => $item['value'],
                    'checked' => $checked,
                    'icon' => FormEngineUtility::getIconHtml(!empty($item['icon']) ? $item['icon'] : 'empty-empty'),
                    'title' => $item['label'],
                    'help' => $help,
                ];
                $counter++;
            }
        }

        $fieldInformationResult = $this->renderFieldInformation();
        $fieldInformationHtml = $fieldInformationResult['html'];
        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);

        $fieldWizardResult = $this->renderFieldWizard();
        $fieldWizardHtml = $fieldWizardResult['html'];
        $resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);

        $html[] = '<div class="formengine-field-item t3js-formengine-field-item" data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '">';
        $html[] = $fieldInformationHtml;
        $html[] =   '<div class="form-wizards-wrap">';
        $html[] =       '<div class="form-wizards-item-element">';

        if (!$readOnly) {
            // Add an empty hidden field which will send a blank value if all items are unselected.
            $html[] = '<input type="hidden" class="select-checkbox" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="">';
        }

        // Building the checkboxes
        foreach ($groups as $groupKey => $group) {
            $groupId = htmlspecialchars($elementId . '-group-' . $groupKey);
            $groupCollapsibleId = $groupId . '-collapse';

            $hasGroupHeader = is_array($group['header'] ?? false);

            $html[] = '<div id="' . $groupId . '" class="panel panel-default" data-multi-record-selection-identifier="' . $groupId . '">';
            if ($hasGroupHeader) {
                $expanded = ($config['appearance']['expandAll'] ?? false);
                $html[] = '<div class="panel-heading" role="tab">';
                $html[] =   '<div class="panel-heading-row">';
                $html[] =     '<button type="button" class="panel-button' . (!$expanded ? ' collapsed' : '') . '" aria-expanded="' . ($expanded ? 'true' : 'false') . '"';
                $html[] =       ' aria-controls="' . $groupCollapsibleId . '" data-bs-target="#' . $groupCollapsibleId . '" data-bs-toggle="collapse">';
                $html[] =       '<div class="panel-icon">';
                $html[] =         $group['header']['icon'];
                $html[] =       '</div>';
                $html[] =       '<div class="panel-title">';
                $html[] =         htmlspecialchars($group['header']['title']);
                $html[] =       '</div>';
                $html[] =       '<span class="caret"></span>';
                $html[] =     '</button>';
                $html[] =   '</div>';
                $html[] = '</div>';
            }
            if (is_array($group['items'] ?? null)) {
                $tableRows = [];

                // Render rows
                foreach ($group['items'] as $item) {
                    $inputElementAttrs = [
                        'type' => 'checkbox',
                        'class' => 'form-check-input t3js-multi-record-selection-check',
                        'id' => $item['id'],
                        'name' => $item['name'],
                        'value' => $item['value'],
                    ];

                    if ($item['checked']) {
                        $inputElementAttrs['checked'] = 'checked';
                    }

                    if ($readOnly) {
                        // Disable item if the element is readonly
                        $inputElementAttrs['disabled'] = 'disabled';
                    } else {
                        // Add fieldChange attributes if element is not readOnly
                        $inputElementAttrs = array_merge(
                            $inputElementAttrs,
                            $this->getOnFieldChangeAttrs('click', $parameterArray['fieldChangeFunc'] ?? [])
                        );
                    }

                    $tableRows[] = '<tr data-multi-record-selection-element="true">';
                    $tableRows[] =    '<td class="col-checkbox">';
                    $tableRows[] =        '<span class="form-check form-check-type-toggle">';
                    $tableRows[] =            '<input ' . GeneralUtility::implodeAttributes($inputElementAttrs, true, true) . '>';
                    $tableRows[] =        '</span>';
                    $tableRows[] =    '</td>';
                    $tableRows[] =    '<td class="col-title">';
                    $tableRows[] =        '<label class="label-block nowrap-disabled" for="' . $item['id'] . '">';
                    $tableRows[] =            '<span class="inline-icon">' . $item['icon'] . '</span>';
                    $tableRows[] =            htmlspecialchars($this->appendValueToLabelInDebugMode($item['title'], $item['value']), ENT_COMPAT, 'UTF-8', false);
                    $tableRows[] =        '</label>';
                    $tableRows[] =    '</td>';
                    $tableRows[] =    '<td class="text-end">' . $item['help'] . '</td>';
                    $tableRows[] = '</tr>';
                }

                if ($hasGroupHeader) {
                    $expandAll = ($config['appearance']['expandAll'] ?? false) ? 'show' : '';
                    $html[] = '<div id="' . $groupCollapsibleId . '" class="panel-collapse collapse ' . $expandAll . '" role="tabpanel">';
                }

                $html[] =    '<div class="table-fit">';
                $html[] =        '<table class="table table-hover">';
                if (!$readOnly) {
                    // Add table header with actions, in case the element is not readOnly
                    $html[] =            '<thead>';
                    $html[] =                '<tr>';
                    $html[] =                    '<th class="col-checkbox">' . $this->getRecordSelectionCheckActions() . '</th>';
                    $html[] =                    '<th class="col-title" colspan="2">' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.th.name')) . '</th>';
                    $html[] =                '</tr>';
                    $html[] =            '</thead>';

                    // Add JavaScript module. This is only needed, in case the element
                    // is not readOnly, since otherwise no checkbox changes take place.
                    $resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/multi-record-selection.js');
                }
                $html[] =            '<tbody>' . implode(LF, $tableRows) . '</tbody>';
                $html[] =        '</table>';
                $html[] =    '</div>';
                if ($hasGroupHeader) {
                    $html[] = '</div>';
                }
            }
            $html[] = '</div>';
        }

        $html[] =       '</div>';
        if (!$readOnly && !empty($fieldWizardHtml)) {
            $html[] =   '<div class="form-wizards-item-bottom">';
            $html[] =       $fieldWizardHtml;
            $html[] =   '</div>';
        }
        $html[] =   '</div>';
        $html[] = '</div>';

        $resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
        return $resultArray;
    }

    /**
     * A function that creates an icon with a help text. If a user clicks on
     * the icon, the help text will show up as tooltip
     *
     * @param array $overloadHelpText Array with text to overload help text
     * @return string the HTML code ready to render
     */
    protected function wrapInHelp(array $overloadHelpText = []): string
    {
        // If there's a help text or some overload information, proceed with preparing an output
        if (empty($overloadHelpText)) {
            return '';
        }
        $text = $this->iconFactory->getIcon('actions-system-help-open', IconSize::SMALL)->render();
        $abbrClassAdd = ' help-teaser-icon';
        $text = '<abbr class="help-teaser' . $abbrClassAdd . '">' . $text . '</abbr>';
        $wrappedText = '<span class="help-link" data-bs-content="<p></p>"';
        // The overload array may provide a title and a description
        // If either one is defined, add them to the "data" attributes
        if (isset($overloadHelpText['title'])) {
            $wrappedText .= ' data-title="' . htmlspecialchars($overloadHelpText['title']) . '"';
        }
        if (isset($overloadHelpText['description'])) {
            $wrappedText .= ' data-description="' . htmlspecialchars($overloadHelpText['description']) . '"';
        }
        $wrappedText .= '>' . $text . '</span>';
        return $wrappedText;
    }

    protected function getRecordSelectionCheckActions(): string
    {
        $lang = $this->getLanguageService();
        return '
            <div class="dropdown">
                <button type="button" class="dropdown-toggle dropdown-toggle-link t3js-multi-record-selection-check-actions-toggle" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false" aria-label="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openSelectionOptions')) . '">
                    ' . $this->iconFactory->getIcon('actions-selection', IconSize::SMALL)->render() . '
                </button>
                <ul class="dropdown-menu t3js-multi-record-selection-check-actions">
                    <li>
                        <button type="button" class="dropdown-item" disabled data-multi-record-selection-check-action="check-all" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')) . '">
                            <span class="dropdown-item-columns">
                                <span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
                                    ' . $this->iconFactory->getIcon('actions-selection-elements-all', IconSize::SMALL)->render() . '
                                </span>
                                <span class="dropdown-item-column dropdown-item-column-title">
                                    ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')) . '
                                </span>
                            </span>
                        </button>
                    </li>
                    <li>
                        <button type="button" class="dropdown-item" disabled data-multi-record-selection-check-action="check-none" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')) . '">
                            <span class="dropdown-item-columns">
                                <span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
                                    ' . $this->iconFactory->getIcon('actions-selection-elements-none', IconSize::SMALL)->render() . '
                                </span>
                                <span class="dropdown-item-column dropdown-item-column-title">
                                    ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')) . '
                                </span>
                            </span>
                        </button>
                    </li>
                    <li>
                        <button type="button" class="dropdown-item" data-multi-record-selection-check-action="toggle" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')) . '">
                            <span class="dropdown-item-columns">
                                <span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
                                    ' . $this->iconFactory->getIcon('actions-selection-elements-invert', IconSize::SMALL)->render() . '
                                </span>
                                <span class="dropdown-item-column dropdown-item-column-title">
                                    ' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')) . '
                                </span>
                            </span>
                        </button>
                    </li>
                </ul>
            </div>';
    }
}
