<?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\FormDataProvider;

use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Tree\TableConfiguration\ArrayTreeRenderer;
use TYPO3\CMS\Core\Tree\TableConfiguration\TableConfigurationTree;
use TYPO3\CMS\Core\Tree\TableConfiguration\TreeDataProviderFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;

/**
 * Data provider for type=select + renderType=selectTree fields.
 *
 * Used in combination with SelectTreeElement to create the base HTML for trees,
 * does a little bit of sanitation and preparation then.
 *
 * Used in combination with FormSelectTreeAjaxController to fetch the final tree list, this is
 * triggered if $result['selectTreeCompileItems'] is set to true. This way the tree item
 * calculation is only triggered if needed in this ajax context. Writes the prepared
 * item array to ['config']['items'] in this case.
 */
class TcaSelectTreeItems extends AbstractItemProvider implements FormDataProviderInterface
{
    public function __construct(private readonly IconFactory $iconFactory) {}

    /**
     * Sanitize config options and resolve select items if requested.
     *
     * @return array
     * @throws \UnexpectedValueException
     */
    public function addData(array $result)
    {
        $table = $result['tableName'];

        foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
            if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'select') {
                continue;
            }

            // Make sure we are only processing supported renderTypes
            if (!$this->isTargetRenderType($fieldConfig)) {
                continue;
            }

            $fieldConfig['config']['maxitems'] = MathUtility::forceIntegerInRange($fieldConfig['config']['maxitems'] ?? 0, 0, 99999);
            if ($fieldConfig['config']['maxitems'] === 0) {
                $fieldConfig['config']['maxitems'] = 99999;
            }

            $fieldConfig = $this->parseStartingPointsFromSiteConfiguration($result, $fieldConfig);

            // A couple of tree specific config parameters can be overwritten via page TS.
            // Pick those that influence the data fetching and write them into the config
            // given to the tree data provider. This is additionally used in SelectTreeElement, so always do that.
            if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['config.']['treeConfig.'])) {
                $pageTsConfig = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['config.']['treeConfig.'];
                if (isset($pageTsConfig['startingPoints'])) {
                    $fieldConfig['config']['treeConfig']['startingPoints']
                        = implode(',', array_unique(GeneralUtility::intExplode(',', (string)$pageTsConfig['startingPoints'])));
                }
                if (isset($pageTsConfig['appearance.']['expandAll'])) {
                    $fieldConfig['config']['treeConfig']['appearance']['expandAll'] = (bool)$pageTsConfig['appearance.']['expandAll'];
                }
                if (isset($pageTsConfig['appearance.']['maxLevels'])) {
                    $fieldConfig['config']['treeConfig']['appearance']['maxLevels'] = (int)$pageTsConfig['appearance.']['maxLevels'];
                }
                if (isset($pageTsConfig['appearance.']['nonSelectableLevels'])) {
                    $fieldConfig['config']['treeConfig']['appearance']['nonSelectableLevels'] = $pageTsConfig['appearance.']['nonSelectableLevels'];
                }
            }

            // Prepare the list of currently selected nodes using RelationHandler
            // This is needed to ensure a correct value initialization before the actual tree is loaded
            $result['databaseRow'][$fieldName] = $this->processDatabaseFieldValue($result['databaseRow'], $fieldName);
            $result['databaseRow'][$fieldName] = $this->processSelectFieldValue($result, $fieldName, []);

            // Preserve original TCA static items before overwriting with resolved items
            $originalTcaItems = $fieldConfig['config']['items'] ?? [];

            // Always resolve the full item list (static + foreign_table + TSconfig) with filtering.
            // This is needed by TcaColumnsRemoveEmptyRelations to determine if the field has any
            // selectable items, and is reused below for tree building in the AJAX context.
            $staticItems = $this->sanitizeItemArray($originalTcaItems, $table, $fieldName);
            $staticItems = array_merge($staticItems, $this->addItemsFromPageTsConfig($result, $fieldName, []));
            $staticItems = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $staticItems);
            $staticItems = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $staticItems);
            $dynamicItems = $this->addItemsFromForeignTable($result, $fieldName);
            $dynamicItems = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $dynamicItems);
            $dynamicItems = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $dynamicItems);
            $dynamicItems = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $dynamicItems);
            $dynamicItems = $this->removeItemsByUserAuthMode($result, $fieldName, $dynamicItems);
            $dynamicItems = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $dynamicItems);

            // Store flat items for downstream providers (will be overwritten with tree structure during AJAX)
            $fieldConfig['config']['items'] = array_merge($staticItems, $dynamicItems);

            if ($result['selectTreeCompileItems']) {
                $finalItems = [];

                // Prepare the list of "static" items if there are any.
                // "static" and "dynamic" is separated since the tree code only copes with "real" existing foreign nodes,
                // so this "static" stuff allows defining tree items that don't really exist in the tree.
                $itemsFromTca = $this->sanitizeItemArray($originalTcaItems, $table, $fieldName);

                // List of additional items defined by page ts config "addItems"
                $itemsFromPageTsConfig = $this->addItemsFromPageTsConfig($result, $fieldName, []);
                // Resolve pageTsConfig item icons to markup
                $finalPageTsConfigItems = [];
                foreach ($itemsFromPageTsConfig as $item) {
                    if ($item['icon'] !== null) {
                        $item['icon'] = $this->iconFactory->getIcon($item['icon'], IconSize::SMALL)->getMarkup('inline');
                    }
                    $finalPageTsConfigItems[] = $item;
                }

                if (!empty($itemsFromTca) || !empty($finalPageTsConfigItems)) {
                    // First apply "keepItems" to $itemsFromTca, this will restrict the tca item list to only
                    // those items that are defined in page ts "keepItems" if given
                    $itemsFromTca = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $itemsFromTca);
                    // Then, merge the items from page ts "addItems" into item list, since "addItems" should
                    // add additional items even if they are not in the "keepItems" list
                    $staticItems = array_merge($itemsFromTca, $finalPageTsConfigItems);
                    // Now apply page ts config "removeItems", so this is *after* addItems, so "removeItems" could
                    // possibly remove items again that were added via "addItems"
                    $staticItems = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $staticItems);
                    // Now, apply user and access right restrictions to this item list
                    $staticItems = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $staticItems);
                    $staticItems = $this->removeItemsByUserAuthMode($result, $fieldName, $staticItems);
                    $staticItems = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $staticItems);
                    // Call itemsProcFunc if given. Note this function does *not* see the "dynamic" list of items
                    if (!empty($fieldConfig['config']['itemsProcFunc']) || !empty($fieldConfig['config']['itemsProcessors'])) {
                        $staticItems = $this->resolveItemsProcessorFunction($result, $fieldName, $staticItems);
                        // itemsProcFunc must not be used anymore
                        unset(
                            $fieldConfig['config']['itemsProcFunc'],
                            $fieldConfig['config']['itemsProcessors']
                        );
                    }
                    // translate any labels
                    $staticItems = $this->translateLabels($result, $staticItems, $table, $fieldName);
                    // and add icons from the static list
                    $staticItems = $this->addIconFromAltIcons($result, $staticItems, $table, $fieldName);
                    // Now compile the target items using the same array structure as the "dynamic" list below
                    foreach ($staticItems as $item) {
                        if ($item['value'] === '--div--') {
                            // Skip divs that may occur here for whatever reason
                            continue;
                        }
                        $finalItems[] = [
                            'identifier' => $item['value'],
                            'name' => $item['label'],
                            'icon' => $item['icon'] ?? '',
                            'iconOverlay' => '',
                            'depth' => 0,
                            'hasChildren' => false,
                            'selectable' => true,
                            'checked' => in_array($item['value'], $result['databaseRow'][$fieldName]),
                        ];
                    }
                }

                // Reuse the already-fetched dynamic items to build uid whitelist for tree
                $uidListOfAllDynamicItems = [];
                foreach ($dynamicItems as $item) {
                    if ((int)$item['value'] > 0) {
                        $uidListOfAllDynamicItems[] = (int)$item['value'];
                    }
                }
                // Now kick in this tree stuff
                $treeDataProvider = TreeDataProviderFactory::getDataProvider(
                    $fieldConfig['config'],
                    $table,
                    $fieldName,
                    $result['databaseRow']
                );
                $treeDataProvider->setSelectedList(implode(',', $result['databaseRow'][$fieldName]));
                // Basically the tree foo fetches all tree nodes again (aaargs), then verifies if
                // a given rows uid is within this "list of allowed uids". It then creates an object
                // tree representing the nested tree, just to collapse all that to a flat array again. Yay ...
                $treeDataProvider->setItemWhiteList($uidListOfAllDynamicItems);
                $treeDataProvider->initializeTreeData();
                $treeRenderer = GeneralUtility::makeInstance(ArrayTreeRenderer::class);
                $tree = GeneralUtility::makeInstance(TableConfigurationTree::class);
                $tree->setDataProvider($treeDataProvider);
                $tree->setNodeRenderer($treeRenderer);

                // Merge tree nodes after calculated nodes from static items
                $fieldConfig['config']['items'] = array_merge($finalItems, $tree->render());
            }

            $result['processedTca']['columns'][$fieldName] = $fieldConfig;
        }

        return $result;
    }

    /**
     * Determines whether the current field is a valid target for this DataProvider
     *
     * @return bool
     */
    protected function isTargetRenderType(array $fieldConfig)
    {
        return ($fieldConfig['config']['renderType'] ?? '') === 'selectTree';
    }
}
