<?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\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;

/**
 * On "new" command, initialize new database row with default data
 */
class DatabaseRowInitializeNew implements FormDataProviderInterface
{
    /**
     * Initialize new row with default values from various sources
     * There are 4 sources of default values. Mind the order, the last takes precedence.
     *
     * @return array
     * @throws \UnexpectedValueException
     */
    public function addData(array $result)
    {
        if ($result['command'] !== 'new') {
            return $result;
        }
        if (!is_array($result['databaseRow'])) {
            throw new \UnexpectedValueException(
                'databaseRow of table ' . $result['tableName'] . ' is not an array',
                1444431128
            );
        }

        $result = $this->setDefaultsFromUserTsConfig($result);
        $result = $this->setDefaultsFromPageTsConfig($result);
        $result = $this->setDefaultsFromNeighborRow($result);
        $result = $this->setDefaultsFromDefaultValues($result);
        $result = $this->setDefaultsFromInlineRelations($result);
        $result = $this->setDefaultsFromInlineParentLanguage($result);
        $result = $this->setDefaultsFromInlineParentUid($result);
        $result = $this->setPid($result);

        return $result;
    }

    /**
     * Set defaults defined by user ts "TCAdefaults"
     * Supports both field-level defaults and type-specific defaults
     * TCAdefaults.tt_content.header_layout = 1 (field-level)
     * TCAdefaults.tt_content.header_layout.types.textmedia = 3 (type-specific)
     *
     * @param array $result Result array
     * @return array Modified result array
     */
    protected function setDefaultsFromUserTsConfig(array $result)
    {
        $tableNameWithDot = $result['tableName'] . '.';
        // Apply default values from user typo script "TCAdefaults" if any
        if (isset($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot])
            && is_array($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot])
        ) {
            $recordType = $this->getRecordTypeFromRow($result);
            $mergedDefaults = $this->mergeTypeSpecificTcaDefaults(
                $result['userTsConfig']['TCAdefaults.'][$tableNameWithDot],
                $recordType
            );

            foreach ($mergedDefaults as $fieldName => $fieldValue) {
                if (isset($result['processedTca']['columns'][$fieldName])) {
                    $result['databaseRow'][$fieldName] = $fieldValue;
                }
            }
        }
        return $result;
    }

    /**
     * Set defaults defined by page ts "TCAdefaults"
     * Supports both field-level defaults and type-specific defaults
     * TCAdefaults.tt_content.header_layout = 1 (field-level)
     * TCAdefaults.tt_content.header_layout.types.textmedia = 3 (type-specific)
     *
     * @param array $result Result array
     * @return array Modified result array
     */
    protected function setDefaultsFromPageTsConfig(array $result)
    {
        $tableNameWithDot = $result['tableName'] . '.';
        if (isset($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot])
            && is_array($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot])
        ) {
            $recordType = $this->getRecordTypeFromRow($result);
            $mergedDefaults = $this->mergeTypeSpecificTcaDefaults(
                $result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot],
                $recordType
            );

            foreach ($mergedDefaults as $fieldName => $fieldValue) {
                if (isset($result['processedTca']['columns'][$fieldName])) {
                    $result['databaseRow'][$fieldName] = $fieldValue;
                }
            }
        }
        return $result;
    }

    /**
     * If a neighbor row is given (if vanillaUid was negative), field can be initialized with values
     * from neighbor for fields registered in TCA['ctrl']['useColumnsForDefaultValues'].
     *
     * @param array $result Result array
     * @return array Modified result array
     */
    protected function setDefaultsFromNeighborRow(array $result)
    {
        if (is_array($result['neighborRow'])
            && !empty($result['processedTca']['ctrl']['useColumnsForDefaultValues'])
        ) {
            $defaultColumns = GeneralUtility::trimExplode(',', $result['processedTca']['ctrl']['useColumnsForDefaultValues'], true);
            foreach ($defaultColumns as $fieldName) {
                if (isset($result['processedTca']['columns'][$fieldName])
                    && isset($result['neighborRow'][$fieldName])
                ) {
                    $result['databaseRow'][$fieldName] = $result['neighborRow'][$fieldName];
                }
            }
        }
        return $result;
    }

    /**
     * Apply default values.
     * These are typically carried around as "defVals" GET vars and set by controllers
     * in $result['defaultValues'] array as init values.
     *
     * @param array $result Result array
     * @return array Modified result array
     */
    protected function setDefaultsFromDefaultValues(array $result)
    {
        $tableName = $result['tableName'];
        $defaultValues = $result['defaultValues'] ?? [];
        if (isset($defaultValues[$tableName]) && is_array($defaultValues[$tableName])) {
            foreach ($defaultValues[$tableName] as $fieldName => $fieldValue) {
                if (isset($result['processedTca']['columns'][$fieldName])) {
                    $result['databaseRow'][$fieldName] = $fieldValue;
                }
            }
        }
        return $result;
    }

    /**
     * Inline scenario if a new intermediate record to an existing child-child is
     * compiled. Set "foreign_selector" field of this intermediate row to given
     * "childChildUid". See TcaDataCompiler array comment of inlineChildChildUid
     * for more details.
     *
     * @param array $result Result array
     * @return array Modified result array
     * @throws \UnexpectedValueException
     */
    protected function setDefaultsFromInlineRelations(array $result)
    {
        if ($result['inlineChildChildUid'] === null) {
            return $result;
        }
        if (!is_int($result['inlineChildChildUid'])) {
            throw new \UnexpectedValueException(
                'An inlineChildChildUid is given for table ' . $result['tableName'] . ', but is not an integer',
                1444434103
            );
        }
        if (!isset($result['inlineParentConfig']['foreign_selector'])) {
            throw new \UnexpectedValueException(
                'An inlineChildChildUid is given for table ' . $result['tableName'] . ', but no foreign_selector in inlineParentConfig',
                1444434102
            );
        }
        $selectorFieldName = $result['inlineParentConfig']['foreign_selector'];
        $fieldType = (string)($result['processedTca']['columns'][$selectorFieldName]['config']['type'] ?? '');
        if (!in_array($fieldType, ['select', 'category', 'group'], true)) {
            throw new \UnexpectedValueException(
                $selectorFieldName . ' is target type of a foreign_selector field to table ' . $result['tableName'] . ' and must be either a select, category or group type field',
                1444434104
            );
        }

        if ($result['inlineChildChildUid']) {
            $result['databaseRow'][$selectorFieldName] = $result['inlineChildChildUid'];
        }

        return $result;
    }

    /**
     * If a new child is created in an inline relation via ajax, and if the parent is a localized record,
     * the child should have the same sys_language_uid set in the field declared in ['ctrl']['languageField']
     * if the child is localizable itself.
     * A localized parent transfers its sys_language_uid via inlineParentConfig['inline']['parentSysLanguageUid'],
     * use that value as default for the child record languageField.
     *
     * @param array $result Result array
     * @return array Modified result array
     * @throws \UnexpectedValueException
     */
    protected function setDefaultsFromInlineParentLanguage(array $result): array
    {
        if (!isset($result['inlineParentConfig']['inline']['parentSysLanguageUid'])
            || empty($result['processedTca']['ctrl']['languageField'])
            || empty($result['processedTca']['ctrl']['transOrigPointerField'])
        ) {
            return $result;
        }

        if (!MathUtility::canBeInterpretedAsInteger($result['inlineParentConfig']['inline']['parentSysLanguageUid'])) {
            throw new \UnexpectedValueException(
                'A sys_language_uid is set from inline parent config but the value is no integer',
                1490360772
            );
        }
        $parentSysLanguageUid = (int)$result['inlineParentConfig']['inline']['parentSysLanguageUid'];
        $languageFieldName = $result['processedTca']['ctrl']['languageField'];
        $result['databaseRow'][$languageFieldName] = $parentSysLanguageUid;

        return $result;
    }

    /**
     * Set the parent uid of inline relations created via ajax to the corresponding foreign field
     *
     * @param array $result Result array
     */
    protected function setDefaultsFromInlineParentUid(array $result): array
    {
        $isInlineChild = $result['isInlineChild'] ?? false;
        $parentField = $result['inlineParentConfig']['foreign_field'] ?? false;

        if ($isInlineChild && $parentField && !empty($result['inlineParentUid'])) {
            $result['databaseRow'][$parentField] = $result['inlineParentUid'];
        }

        return $result;
    }

    /**
     * Set the pid. This is either the vanillaUid (see description in FormDataCompiler),
     * or a pid given by page TSconfig for inline children.
     *
     * @param array $result Result array
     * @return array Modified result array
     * @throws \UnexpectedValueException
     */
    protected function setPid(array $result)
    {
        // Set pid to vanillaUid. This can be a negative value
        // if the record is added relative to another record.
        $result['databaseRow']['pid'] = $result['vanillaUid'];

        // In case a new inline record is created, the pid can be set to a different value
        // by page TSconfig, but not by user TSconfig. This overrides the above pid selection
        // and forces the pid of new inline children.
        $tableNameWithDot = $result['tableName'] . '.';
        if ($result['isInlineChild'] && isset($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'])) {
            if (!MathUtility::canBeInterpretedAsInteger($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'])) {
                throw new \UnexpectedValueException(
                    'page TSconfig setting TCAdefaults.' . $tableNameWithDot . 'pid must be a number, but given string '
                    . $result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'] . ' can not be interpreted as integer',
                    1461598332
                );
            }
            $result['databaseRow']['pid'] = (int)$result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'];
        }

        return $result;
    }

    /**
     * Determine the record type from the current database row
     * and additionally fall back to defaultValues as this is
     * basically the only source we have for new records at this point.
     *
     * @param array $result The form data result array
     * @return string The record type value
     */
    protected function getRecordTypeFromRow(array $result): string
    {
        // If recordTypeValue is already set, use it
        if (is_string($result['recordTypeValue'] ?? false) && $result['recordTypeValue'] !== '') {
            return $result['recordTypeValue'];
        }

        $recordTypeValue = '0';
        // Check if there's a type field defined in TCA
        if (is_string($result['processedTca']['ctrl']['type'] ?? false) && $result['processedTca']['ctrl']['type'] !== '') {
            $tcaTypeField = $result['processedTca']['ctrl']['type'];

            // Handle simple type field (not foreign field reference)
            if (!str_contains($tcaTypeField, ':')) {
                // First check if the type field exists in the database row and has a value
                if (array_key_exists($tcaTypeField, $result['databaseRow']) && $result['databaseRow'][$tcaTypeField] !== null) {
                    $recordTypeValue = (string)$result['databaseRow'][$tcaTypeField];
                } else {
                    // Check if the type field is set in defaultValues
                    $tableName = $result['tableName'];
                    if (isset($result['defaultValues'][$tableName][$tcaTypeField])) {
                        $recordTypeValue = (string)$result['defaultValues'][$tableName][$tcaTypeField];
                    }
                }
            }
        }

        // Apply the same fallback logic as DatabaseRecordTypeValue:
        // Check the determined value actually exists as types key, otherwise fall back to 0 or 1
        if (empty($result['processedTca']['types'][$recordTypeValue])) {
            $recordTypeValue = !empty($result['processedTca']['types']['0']) ? '0' : '1';
        }

        return $recordTypeValue;
    }

    /**
     * Merge type-specific TCAdefaults over field-level defaults
     * Similar to how TCEFORM type-specific settings work
     *
     * @param array $tcaDefaults The TCAdefaults configuration for the table
     * @param string $recordType The current record type value
     * @return array Merged defaults with type-specific values taking precedence
     */
    protected function mergeTypeSpecificTcaDefaults(array $tcaDefaults, string $recordType): array
    {
        $mergedDefaults = [];

        foreach ($tcaDefaults as $fieldKey => $fieldConfiguration) {
            if (str_ends_with($fieldKey, '.')) {
                // This is a field with sub-configuration (potentially types)
                $fieldName = rtrim($fieldKey, '.');
                if (!is_array($fieldConfiguration)) {
                    continue;
                }
                $fieldDefault = $fieldConfiguration;
                // Check if there are type-specific overrides
                if (!empty($fieldConfiguration['types.']) && is_array($fieldConfiguration['types.'])) {
                    $typeSpecificConfiguration = $fieldConfiguration['types.'];
                    unset($fieldDefault['types.']);

                    // If we have a matching type-specific configuration, merge it
                    if (!empty($typeSpecificConfiguration[$recordType . '.']) && is_array($typeSpecificConfiguration[$recordType . '.'])) {
                        ArrayUtility::mergeRecursiveWithOverrule($fieldDefault, $typeSpecificConfiguration[$recordType . '.']);
                    } elseif (!empty($typeSpecificConfiguration[$recordType])) {
                        // Simple value (not array)
                        $mergedDefaults[$fieldName] = $typeSpecificConfiguration[$recordType];
                        continue;
                    }
                }
                // If the field configuration is now empty (only had types.), skip it
                // Otherwise, use the first non-array value or the whole configuration
                foreach ($fieldDefault as $key => $value) {
                    if (!str_ends_with($key, '.') && !is_array($value)) {
                        $mergedDefaults[$fieldName] = $value;
                        break;
                    }
                }
            } else {
                // Simple field-level default
                $mergedDefaults[$fieldKey] = $fieldConfiguration;
            }
        }

        return $mergedDefaults;
    }
}
