<?php

declare(strict_types=1);

/*
 * 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\Controller;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Attribute\AsController;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\View\ViewInterface;

/**
 * Controller for handling the display column selection for records, typically executed from list modules.
 *
 * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API.
 */
#[AsController]
readonly class ColumnSelectorController
{
    private const array PSEUDO_FIELDS = ['_REF_', '_PATH_'];
    private const array EXCLUDE_FILE_FIELDS = [
        'pid', // Not relevant as all records are on pid=0
        'identifier', // Handled manually in listing
        'name', // Handled manually in listing
        'metadata', // The reference to the meta data is not relevant
        'file', // The reference to the file is not relevant
        'sys_language_uid', // Not relevant in listing since only default is displayed
        'l10n_parent', // Not relevant in listing
        't3ver_state', // Not relevant in listing
        't3ver_wsid', // Not relevant in listing
        't3ver_oid', // Not relevant in listing
    ];

    public function __construct(
        protected ResponseFactoryInterface $responseFactory,
        protected BackendViewFactory $backendViewFactory,
        protected TcaSchemaFactory $tcaSchemaFactory,
    ) {}

    /**
     * Update the columns to be displayed for the given table
     */
    public function updateVisibleColumnsAction(ServerRequestInterface $request): ResponseInterface
    {
        $parsedBody = $request->getParsedBody();
        $table = (string)($parsedBody['table'] ?? '');
        $selectedColumns = $parsedBody['selectedColumns'] ?? [];

        if ($table === '' || !is_array($selectedColumns)) {
            return $this->jsonResponse([
                'success' => false,
                'message' => htmlspecialchars(
                    $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_column_selector.xlf:updateColumnView.nothingUpdated')
                ),
            ]);
        }

        $backendUser = $this->getBackendUserAuthentication();
        $displayFields = $backendUser->getModuleData('list/displayFields');
        $displayFields[$table] = $selectedColumns;
        $backendUser->pushModuleData('list/displayFields', $displayFields);

        return $this->jsonResponse(['success' => true]);
    }

    /**
     * Generate the show columns selector form
     */
    public function showColumnsSelectorAction(ServerRequestInterface $request): ResponseInterface
    {
        $queryParams = $request->getQueryParams();
        $table = (string)($queryParams['table'] ?? '');

        if ($table === '') {
            throw new \RuntimeException('No table was given for selecting columns', 1625169125);
        }
        $view = $this->backendViewFactory->create($request);
        $view->assignMultiple([
            'table' => $table,
            'columns' => $this->getColumns($table, (int)($queryParams['id'] ?? 0)),
        ]);

        return $this->htmlResponse($view);
    }

    /**
     * Retrieve all columns for the table, which can be selected
     */
    protected function getColumns(string $table, int $pageId): array
    {
        $tsConfig = BackendUtility::getPagesTSconfig($pageId);

        // Current fields selection
        $displayFields = $this->getBackendUserAuthentication()->getModuleData('list/displayFields')[$table] ?? [];

        if ($table === '_FILE') {
            // Special handling for _FILE (merging sys_file and sys_file_metadata together)
            $fields = $this->getFileFields();
        } else {
            // Request fields from table and add pseudo fields
            $fields = array_merge(BackendUtility::getAllowedFieldsForTable($table), self::PSEUDO_FIELDS);
        }

        $columns = $specialColumns = $disabledColumns = [];
        foreach ($fields as $fieldName) {
            $concreteTableName = $table;

            // In case we deal with _FILE, the field name is prefixed with the
            // concrete table name, which is either sys_file or sys_file_metadata.
            if ($table === '_FILE') {
                [$concreteTableName, $fieldName] = explode('|', $fieldName);
            }

            // Hide field if disabled
            if ($tsConfig['TCEFORM.'][$concreteTableName . '.'][$fieldName . '.']['disabled'] ?? false) {
                continue;
            }
            $schema = $this->tcaSchemaFactory->get($concreteTableName);
            $labelFieldName = false;
            if ($schema->hasCapability(TcaSchemaCapability::Label)) {
                $labelFieldName = $schema->getCapability(TcaSchemaCapability::Label)->getPrimaryFieldName();
            }

            // Determine if the column should be disabled (Meaning it is always selected and can not be turned off)
            $isDisabled = $fieldName === $labelFieldName;

            // Determine field label
            $label = ($schema->hasField($fieldName) ? $schema->getField($fieldName)->getLabel() : '') ?: null;
            $label = $this->getLanguageService()->translateLabel(
                $tsConfig['TCEFORM.'][$concreteTableName . '.'][$fieldName . '.']['label.'] ?? [],
                $tsConfig['TCEFORM.'][$concreteTableName . '.'][$fieldName . '.']['label']
                    ?? $label
                    ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $fieldName
            );

            // Add configuration for this column
            $columnConfiguration = [
                'name' => $fieldName,
                'selected' => $isDisabled || in_array($fieldName, $displayFields, true),
                'disabled' => $isDisabled,
                'pseudo' => in_array($fieldName, self::PSEUDO_FIELDS, true),
                'label' => $label,
            ];

            // Add column configuration to the correct group
            if ($columnConfiguration['disabled']) {
                $disabledColumns[] = $columnConfiguration;
            } elseif (!$columnConfiguration['label']) {
                $specialColumns[] = $columnConfiguration;
            } else {
                $columns[] = $columnConfiguration;
            }
        }

        // Sort standard columns by their resolved label
        usort($columns, static fn($a, $b) => $a['label'] <=> $b['label']);

        // Disabled columns go first, followed by standard columns
        // and special columns, which do not have a label.
        return array_merge($disabledColumns, $columns, $specialColumns);
    }

    /**
     * Get file related fields by merging sys_file and sys_file_metadata together
     * and adding the corresponding table as prefix (needed for labels processing).
     */
    protected function getFileFields(): array
    {
        // Get all sys_file fields expect excluded ones
        $fileFields = array_filter(
            BackendUtility::getAllowedFieldsForTable('sys_file'),
            static fn(string $field): bool => !in_array($field, self::EXCLUDE_FILE_FIELDS, true)
        );

        // Always add crdate and tstamp fields for files
        $fileFields = array_unique(array_merge($fileFields, ['crdate', 'tstamp']));

        // Update the exclude fields with the fields, already added through sys_file, since those take precedence
        $excludeFields = array_merge($fileFields, self::EXCLUDE_FILE_FIELDS);

        // Get all sys_file_metadata fields expect excluded ones
        $fileMetaDataFields = array_filter(
            BackendUtility::getAllowedFieldsForTable('sys_file_metadata'),
            static fn(string $field): bool => !in_array($field, $excludeFields, true)
        );

        // Merge sys_file and sys_file_metadata fields together, while adding the table name as prefix
        return array_merge(
            array_map(static fn(string $value): string => 'sys_file|' . $value, $fileFields),
            array_map(static fn(string $value): string => 'sys_file_metadata|' . $value, $fileMetaDataFields),
        );
    }

    protected function htmlResponse(ViewInterface $view): ResponseInterface
    {
        $response = $this->responseFactory
            ->createResponse()
            ->withHeader('Content-Type', 'text/html; charset=utf-8');
        $response->getBody()->write($view->render('ColumnSelector'));
        return $response;
    }

    protected function jsonResponse(array $data): ResponseInterface
    {
        $response = $this->responseFactory
            ->createResponse()
            ->withAddedHeader('Content-Type', 'application/json; charset=utf-8');

        $response->getBody()->write(json_encode($data));
        return $response;
    }

    protected function getBackendUserAuthentication(): BackendUserAuthentication
    {
        return $GLOBALS['BE_USER'];
    }

    protected function getLanguageService(): LanguageService
    {
        return $GLOBALS['LANG'];
    }
}
