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

use Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Versioning\VersionState;

/**
 * Resolve and prepare files data.
 */
class TcaFiles extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
    private const string FILE_REFERENCE_TABLE = 'sys_file_reference';
    private const string FOREIGN_SELECTOR = 'uid_local';

    public function __construct(
        private readonly InlineStackProcessor $inlineStackProcessor,
        private readonly LoggerInterface $logger,
    ) {}

    public function addData(array $result): array
    {
        // inlineFirstPid is currently resolved by TcaInline
        // @todo check if duplicating the functionality makes sense to resolve dependencies

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

            if (!$this->getBackendUser()->check('tables_modify', self::FILE_REFERENCE_TABLE)) {
                // Early return if user is not allowed to modify the file reference table
                continue;
            }

            if (!$result['tcaSchemata']->has(self::FILE_REFERENCE_TABLE)) {
                throw new \RuntimeException('Table ' . self::FILE_REFERENCE_TABLE . ' does not exists', 1664364262);
            }
            $fileReferenceSchema = $result['tcaSchemata']->get(self::FILE_REFERENCE_TABLE);
            if (!$fileReferenceSchema->hasField(self::FOREIGN_SELECTOR)) {
                throw new \RuntimeException('Table ' . self::FILE_REFERENCE_TABLE . ' has no column ' . self::FOREIGN_SELECTOR, 1770975128);
            }

            $childField = $fileReferenceSchema->getField(self::FOREIGN_SELECTOR);
            if ($childField->getType() !== 'group' || !($childField->getConfiguration()['allowed'] ?? false)) {
                throw new \UnexpectedValueException(
                    'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points to field '
                    . self::FOREIGN_SELECTOR . ' of table ' . self::FILE_REFERENCE_TABLE . ', but this field '
                    . 'is either not defined, is not of type "group" or does not define the "allowed" option.',
                    1664364263
                );
            }

            $result['processedTca']['columns'][$fieldName]['children'] = [];

            $result = $this->initializeMinMaxItems($result, $fieldName);
            $result = $this->initializeParentSysLanguageUid($result, $fieldName);
            $result = $this->initializeAppearance($result, $fieldName);

            // If field is set to readOnly, set all fields of the relation to readOnly as well
            if ($result['inlineParentConfig']['readOnly'] ?? false) {
                foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
                    $result['processedTca']['columns'][$columnName]['config']['readOnly'] = true;
                }
            }

            // Resolve existing file references - this is usually always done except on ajax calls
            if ($result['inlineResolveExistingChildren']) {
                $result = $this->resolveFileReferences($result, $fieldName);
                if (!empty($fieldConfig['config']['selectorOrUniqueConfiguration'])) {
                    throw new \RuntimeException('selectorOrUniqueConfiguration not implemented for TCA type "file"', 1664380909);
                }
            }
        }

        return $result;
    }

    protected function initializeMinMaxItems(array $result, string $fieldName): array
    {
        $config = $result['processedTca']['columns'][$fieldName]['config'];
        $config['minitems'] = isset($config['minitems']) ? MathUtility::forceIntegerInRange($config['minitems'], 0) : 0;
        $config['maxitems'] = isset($config['maxitems']) ? MathUtility::forceIntegerInRange($config['maxitems'], 1) : 99999;
        $result['processedTca']['columns'][$fieldName]['config'] = $config;

        return $result;
    }

    protected function initializeParentSysLanguageUid(array $result, string $fieldName): array
    {
        if (($parentLanguageFieldName = (string)($result['processedTca']['ctrl']['languageField'] ?? '')) === ''
            || !$result['tcaSchemata']->get(self::FILE_REFERENCE_TABLE)->hasCapability(TcaSchemaCapability::Language)
            || isset($result['processedTca']['columns'][$fieldName]['config']['inline']['parentSysLanguageUid'])
            || !isset($result['databaseRow'][$parentLanguageFieldName])
        ) {
            return $result;
        }

        $result['processedTca']['columns'][$fieldName]['config']['inline']['parentSysLanguageUid']
            = is_array($result['databaseRow'][$parentLanguageFieldName])
                ? (int)($result['databaseRow'][$parentLanguageFieldName][0] ?? 0)
                : (int)$result['databaseRow'][$parentLanguageFieldName];

        return $result;
    }

    protected function initializeAppearance(array $result, string $fieldName): array
    {
        $result['processedTca']['columns'][$fieldName]['config']['appearance'] = array_replace_recursive(
            [
                'useSortable' => true,
                'headerThumbnail' => [
                    'height' => '45m',
                ],
                'enabledControls' => [
                    'edit' => true,
                    'info' => true,
                    'dragdrop' => true,
                    'sort' => false,
                    'hide' => true,
                    'delete' => true,
                    'localize' => true,
                ],
            ],
            $result['processedTca']['columns'][$fieldName]['config']['appearance'] ?? []
        );

        return $result;
    }

    /**
     * Substitute the value in databaseRow of this inline field with an array
     * that contains the databaseRows of currently connected records and some meta information.
     */
    protected function resolveFileReferences(array $result, string $fieldName): array
    {
        if ($result['defaultLanguageRow'] !== null) {
            return $this->resolveFileReferenceOverlays($result, $fieldName);
        }

        $fileReferenceUidsOfDefaultLanguageRecord = $this->resolveFileReferenceUids(
            $result['processedTca']['columns'][$fieldName]['config'],
            $result['tableName'],
            $result['databaseRow']['uid'],
            $result['databaseRow'][$fieldName]
        );
        $result['databaseRow'][$fieldName] = implode(',', $fileReferenceUidsOfDefaultLanguageRecord);

        if ($result['inlineCompileExistingChildren']) {
            $fileReferenceSchema = $result['tcaSchemata']->get(self::FILE_REFERENCE_TABLE);
            foreach ($this->getSubstitutedWorkspacedUids($fileReferenceUidsOfDefaultLanguageRecord, $fileReferenceSchema) as $uid) {
                try {
                    $compiledFileReference = $this->compileFileReference($result, $fieldName, $uid);
                    $result['processedTca']['columns'][$fieldName]['children'][] = $compiledFileReference;
                } catch (DatabaseRecordException $e) {
                    // Nothing to do here, missing file reference is just not being rendered.
                }
            }
        }
        return $result;
    }

    /**
     * Substitute the value in databaseRow of this file field with an array
     * that contains the databaseRows of currently connected file references
     * and some meta information.
     */
    protected function resolveFileReferenceOverlays(array $result, string $fieldName): array
    {
        $fileReferenceUidsOfLocalizedOverlay = [];
        $fieldConfig = $result['processedTca']['columns'][$fieldName]['config'];
        if ($result['command'] === 'edit') {
            $fileReferenceUidsOfLocalizedOverlay = $this->resolveFileReferenceUids(
                $fieldConfig,
                $result['tableName'],
                $result['databaseRow']['uid'],
                $result['databaseRow'][$fieldName]
            );
        }
        $result['databaseRow'][$fieldName] = implode(',', $fileReferenceUidsOfLocalizedOverlay);
        $fileReferenceSchema = $result['tcaSchemata']->get(self::FILE_REFERENCE_TABLE);
        $fileReferenceUidsOfLocalizedOverlay = $this->getSubstitutedWorkspacedUids($fileReferenceUidsOfLocalizedOverlay, $fileReferenceSchema);
        if ($result['inlineCompileExistingChildren']) {
            $tableNameWithDefaultRecords = $result['tableName'];
            $fileReferenceUidsOfDefaultLanguageRecord = $this->getSubstitutedWorkspacedUids(
                $this->resolveFileReferenceUids(
                    $fieldConfig,
                    $tableNameWithDefaultRecords,
                    $result['defaultLanguageRow']['uid'],
                    $result['defaultLanguageRow'][$fieldName]
                ),
                $fileReferenceSchema
            );

            // Find which records are localized, which records are not localized and which are localized but miss default language record
            $fieldNameWithDefaultLanguageUid = $fileReferenceSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName() ?? '';
            $showPossibleLocalizationRecords = $fieldConfig['appearance']['showPossibleLocalizationRecords'] ?? false;
            foreach ($fileReferenceUidsOfLocalizedOverlay as $localizedUid) {
                try {
                    $localizedRecord = $this->getRecordFromDatabase(self::FILE_REFERENCE_TABLE, $localizedUid);
                } catch (DatabaseRecordException $e) {
                    // The child could not be compiled, probably it was deleted and a dangling mm record exists
                    $this->logger->warning(
                        $e->getMessage(),
                        [
                            'table' => self::FILE_REFERENCE_TABLE,
                            'uid' => $localizedUid,
                            'exception' => $e,
                        ]
                    );
                    continue;
                }
                // Compile localized record
                $compiledFileReference = $this->compileFileReference($result, $fieldName, $localizedUid);
                $result['processedTca']['columns'][$fieldName]['children'][] = $compiledFileReference;
                // If that relation is configured to "showPossibleLocalizationRecords", this localized record
                // needs to be removed from the list of records that are pending to be localized.
                if ($showPossibleLocalizationRecords) {
                    $uidOfDefaultLanguageRecord = (int)$localizedRecord[$fieldNameWithDefaultLanguageUid];
                    if (in_array($uidOfDefaultLanguageRecord, $fileReferenceUidsOfDefaultLanguageRecord, true)) {
                        // This localized child has a default language record. Remove this record from list of default language records
                        $fileReferenceUidsOfDefaultLanguageRecord = array_diff($fileReferenceUidsOfDefaultLanguageRecord, [$uidOfDefaultLanguageRecord]);
                    }
                    $uidOfDefaultLanguageRecordWorkspaceVersionArray = $this->getSubstitutedWorkspacedUids([$uidOfDefaultLanguageRecord], $fileReferenceSchema);
                    if (!empty($uidOfDefaultLanguageRecordWorkspaceVersionArray)
                        && in_array($uidOfDefaultLanguageRecordWorkspaceVersionArray[0], $fileReferenceUidsOfDefaultLanguageRecord, true)
                    ) {
                        // In some situations 'l10n_parent' of a localized workspace record points to the live version
                        // of the default language record, and not to the workspace version, even though it exists.
                        // Filter those as well, since the interface would otherwise show the item as "can be localized/synchronized".
                        $fileReferenceUidsOfDefaultLanguageRecord = array_diff($fileReferenceUidsOfDefaultLanguageRecord, [$uidOfDefaultLanguageRecordWorkspaceVersionArray[0]]);
                    }
                }
            }
            if ($showPossibleLocalizationRecords) {
                foreach ($fileReferenceUidsOfDefaultLanguageRecord as $defaultLanguageUid) {
                    // If there are still uids in $connectedUidsOfDefaultLanguageRecord, these are records that
                    // exist in default language, but are not localized yet. Compile and mark those
                    try {
                        $compiledFileReference = $this->compileFileReference($result, $fieldName, $defaultLanguageUid, true);
                    } catch (DatabaseRecordException $e) {
                        // The child could not be compiled, probably it was deleted and a dangling mm record exists
                        $this->logger->warning(
                            $e->getMessage(),
                            [
                                'table' => self::FILE_REFERENCE_TABLE,
                                'uid' => $defaultLanguageUid,
                                'exception' => $e,
                            ]
                        );
                        continue;
                    }
                    $result['processedTca']['columns'][$fieldName]['children'][] = $compiledFileReference;
                }
            }
        }

        return $result;
    }

    protected function compileFileReference(array $result, string $parentFieldName, int $childUid, $isInlineDefaultLanguageRecordInLocalizedParentContext = false): array
    {
        $inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($result['inlineStructure'], 0) ?: [];
        return GeneralUtility::makeInstance(FormDataCompiler::class)
            ->compile(
                [
                    'request' => $result['request'],
                    'command' => 'edit',
                    'tableName' => self::FILE_REFERENCE_TABLE,
                    'vanillaUid' => $childUid,
                    'returnUrl' => $result['returnUrl'],
                    'isInlineChild' => true,
                    'isInlineDefaultLanguageRecordInLocalizedParentContext' => $isInlineDefaultLanguageRecordInLocalizedParentContext,
                    'inlineStructure' => $result['inlineStructure'],
                    'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
                    'inlineFirstPid' => $result['inlineFirstPid'],
                    'inlineParentConfig' => $result['processedTca']['columns'][$parentFieldName]['config'],
                    'inlineParentUid' => $result['databaseRow']['uid'],
                    'inlineParentTableName' => $result['tableName'],
                    'inlineParentFieldName' => $parentFieldName,
                    'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'] ?? '',
                    'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'] ?? '',
                    'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'] ?? '',
                    // pass through schemata as they are immutable once they are set
                    'tcaSchemata' => $result['tcaSchemata'],
                    // pass through fullTca as it is immutable once set
                    'fullTca' => $result['fullTca'],
                ],
                GeneralUtility::makeInstance(TcaDatabaseRecord::class)
            );
    }

    /**
     * Substitute given list of uids with corresponding workspace uids - if needed
     *
     * @param int[] $connectedUids List of file reference uids
     * @return int[] List of substituted uids
     */
    protected function getSubstitutedWorkspacedUids(array $connectedUids, TcaSchema $fileReferenceSchema): array
    {
        $workspace = $this->getBackendUser()->workspace;
        if ($workspace === 0 || !$fileReferenceSchema->hasCapability(TcaSchemaCapability::Workspace)) {
            return $connectedUids;
        }
        $substitutedUids = [];
        foreach ($connectedUids as $uid) {
            $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord(
                $workspace,
                self::FILE_REFERENCE_TABLE,
                $uid,
                'uid,t3ver_state'
            );
            if (!empty($workspaceVersion)) {
                $versionState = VersionState::tryFrom($workspaceVersion['t3ver_state'] ?? 0);
                if ($versionState === VersionState::DELETE_PLACEHOLDER) {
                    continue;
                }
                $uid = $workspaceVersion['uid'];
            }
            $substitutedUids[] = (int)$uid;
        }
        return $substitutedUids;
    }

    /**
     * Resolve file reference uids using the RelationHandler
     *
     * @return int[]
     */
    protected function resolveFileReferenceUids(
        array $parentConfig,
        $parentTableName,
        $parentUid,
        $parentFieldValue
    ): array {
        $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
        $relationHandler->start(
            $parentFieldValue,
            self::FILE_REFERENCE_TABLE,
            '',
            BackendUtility::getLiveVersionIdOfRecord($parentTableName, $parentUid) ?? $parentUid,
            $parentTableName,
            $parentConfig
        );
        return array_map(intval(...), $relationHandler->getValueArray());
    }

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

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