<?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\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Domain\DateTimeFactory;
use TYPO3\CMS\Core\Localization\DateFormatter;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * Determine the title of a record and write it to $result['recordTitle'].
 *
 * TCA ctrl fields like label and label_alt are evaluated and their
 * current values from databaseRow used to create the title.
 */
class TcaRecordTitle implements FormDataProviderInterface
{
    /**
     * Enrich the processed record information with the resolved title
     *
     * @param array $result Incoming result array
     * @return array Modified array
     */
    public function addData(array $result)
    {
        if (!isset($result['processedTca']['ctrl']['label'])) {
            throw new \UnexpectedValueException(
                'TCA of table ' . $result['tableName'] . ' misses required [\'ctrl\'][\'label\'] definition.',
                1443706103
            );
        }

        if ($result['isInlineChild'] && isset($result['processedTca']['ctrl']['formattedLabel_userFunc'])) {
            // inline child with formatted user func is first
            $parameters = [
                'table' => $result['tableName'],
                'row' => $result['databaseRow'],
                'title' => '',
                'isOnSymmetricSide' => $result['isOnSymmetricSide'],
                'options' => $result['processedTca']['ctrl']['formattedLabel_userFunc_options'] ?? [],
                'parent' => [
                    'uid' => $result['databaseRow']['uid'],
                    'config' => $result['inlineParentConfig'],
                ],
            ];
            // callUserFunction requires a third parameter, but we don't want to give $this as reference!
            $null = null;
            GeneralUtility::callUserFunction($result['processedTca']['ctrl']['formattedLabel_userFunc'], $parameters, $null);
            $result['recordTitle'] = $parameters['title'];
        } elseif ($result['isInlineChild'] && (isset($result['inlineParentConfig']['foreign_label'])
                || isset($result['inlineParentConfig']['symmetric_label']))
        ) {
            // inline child with foreign label or symmetric inline child with symmetric_label
            $fieldName = $result['isOnSymmetricSide']
                ? $result['inlineParentConfig']['symmetric_label']
                : $result['inlineParentConfig']['foreign_label'];
            $result['recordTitle'] = $this->getRecordTitleForField($fieldName, $result);
        } elseif (isset($result['processedTca']['ctrl']['label_userFunc'])) {
            // userFunc takes precedence over everything else
            $parameters = [
                'table' => $result['tableName'],
                'row' => $result['databaseRow'],
                'title' => '',
                'options' => $result['processedTca']['ctrl']['label_userFunc_options'] ?? [],
            ];
            $null = null;
            GeneralUtility::callUserFunction($result['processedTca']['ctrl']['label_userFunc'], $parameters, $null);
            $result['recordTitle'] = $parameters['title'];
        } else {
            // standard record
            $result = $this->getRecordTitleByLabelProperties($result);
        }

        return $result;
    }

    /**
     * Build the record title from label, label_alt and label_alt_force properties
     *
     * @param array $result Incoming result array
     * @return array Modified result array
     */
    protected function getRecordTitleByLabelProperties(array $result)
    {
        $titles = [];
        $titleByLabel = $this->getRecordTitleForField($result['processedTca']['ctrl']['label'], $result);
        if (!empty($titleByLabel)) {
            $titles[] = $titleByLabel;
        }

        $labelAltForce = isset($result['processedTca']['ctrl']['label_alt_force'])
            ? (bool)$result['processedTca']['ctrl']['label_alt_force']
            : false;
        if (!empty($result['processedTca']['ctrl']['label_alt']) && ($labelAltForce || empty($titleByLabel))) {
            // Dive into label_alt evaluation if label_alt_force is set or if label did not came up with a title yet
            $labelAltFields = GeneralUtility::trimExplode(',', $result['processedTca']['ctrl']['label_alt'], true);
            foreach ($labelAltFields as $fieldName) {
                $titleByLabelAlt = $this->getRecordTitleForField($fieldName, $result);
                if (!empty($titleByLabelAlt)) {
                    $titles[] = $titleByLabelAlt;
                }
                if (!$labelAltForce && !empty($titleByLabelAlt)) {
                    // label_alt_force creates a comma separated list of multiple fields.
                    // If not set, one found field with content is enough
                    break;
                }
            }
        }

        $result['recordTitle'] = implode(', ', $titles);
        return $result;
    }

    /**
     * Record title of a single field
     *
     * @param string $fieldName Field to handle
     * @param array $result Incoming result array
     * @return string
     */
    protected function getRecordTitleForField($fieldName, $result)
    {
        if ($fieldName === 'uid') {
            // uid return field content directly since it usually has not TCA definition
            return $result['databaseRow']['uid'];
        }

        if (!isset($result['processedTca']['columns'][$fieldName]['config']['type'])
            || !is_string($result['processedTca']['columns'][$fieldName]['config']['type'])
        ) {
            return '';
        }

        $recordTitle = '';
        $rawValue = null;
        if (array_key_exists($fieldName, $result['databaseRow'])) {
            $rawValue = $result['databaseRow'][$fieldName];
        }
        $fieldConfig = $result['processedTca']['columns'][$fieldName]['config'];
        switch ($fieldConfig['type']) {
            case 'radio':
                $recordTitle = $this->getRecordTitleForRadioType($rawValue, $fieldConfig);
                break;
            case 'inline':
            case 'file':
                $recordTitle = $this->getRecordTitleForInlineType(
                    $rawValue,
                    $result['processedTca']['columns'][$fieldName]['children'] ?? []
                );
                break;
            case 'select':
            case 'category':
                $recordTitle = $this->getRecordTitleForSelectType($rawValue, $fieldConfig);
                break;
            case 'group':
                $recordTitle = $this->getRecordTitleForGroupType($rawValue);
                break;
            case 'folder':
                $recordTitle = $this->getRecordTitleForFolderType($rawValue);
                break;
            case 'check':
                $recordTitle = $this->getRecordTitleForCheckboxType($rawValue, $fieldConfig);
                break;
            case 'input':
            case 'number':
            case 'uuid':
                $recordTitle = $rawValue ?? '';
                break;
            case 'country':
                $recordTitle = $this->getRecordTitleForCountryType($rawValue, $result, $fieldName);
                break;
            case 'text':
            case 'email':
            case 'link':
            case 'color':
                $recordTitle = $this->getRecordTitleForStandardTextField($rawValue);
                break;
            case 'datetime':
                $recordTitle = $this->getRecordTitleForDatetimeType($rawValue, $fieldConfig);
                break;
            case 'password':
                $recordTitle = $this->getRecordTitleForPasswordType($rawValue);
                break;
            case 'flex':
                // @todo: Check if and how a label could be generated from flex field data
                break;
            case 'json':
                // @todo: Check if and how a label could be generated from json field data
            default:
        }

        return $recordTitle;
    }

    /**
     * Return the record title for radio fields
     *
     * @param mixed $value Current database value of this field
     * @param array $fieldConfig TCA field configuration
     * @return string
     */
    protected function getRecordTitleForRadioType($value, $fieldConfig)
    {
        if (!isset($fieldConfig['items']) || !is_array($fieldConfig['items'])) {
            return '';
        }
        foreach ($fieldConfig['items'] as $item) {
            if ((string)$value === (string)$item['value']) {
                return $item['label'];
            }
        }
        return '';
    }

    /**
     * @param int $value
     * @return string
     */
    protected function getRecordTitleForInlineType($value, array $children)
    {
        foreach ($children as $child) {
            if ((int)$value === $child['vanillaUid']) {
                return $child['recordTitle'];
            }
        }

        return '';
    }

    /**
     * Return the record title for database records
     *
     * @param mixed $value Current database value of this field
     * @param array $fieldConfig TCA field configuration
     * @return string
     */
    protected function getRecordTitleForSelectType($value, $fieldConfig)
    {
        if (!is_array($value)) {
            return '';
        }
        $labelParts = [];
        if (!empty($fieldConfig['items'])) {
            $listOfValues = array_column($fieldConfig['items'], 'value');
            foreach ($value as $itemValue) {
                $itemKey = array_search($itemValue, $listOfValues);
                if ($itemKey !== false) {
                    $labelParts[] = $fieldConfig['items'][$itemKey]['label'];
                }
            }
        }
        $title = implode(', ', $labelParts);
        if (empty($title) && !empty($value)) {
            $title = implode(', ', $value);
        }
        return $title;
    }

    /**
     * Return the record title for database records of type "country"
     *
     * @param mixed $value Current database value of this field
     * @param array $result Incoming result array
     * @param string $fieldName Field to handle
     * @return string
     */
    protected function getRecordTitleForCountryType($value, $result, $fieldName)
    {
        // @todo - There probably is a better way to get all valid items
        //         for TcaCountry?!
        $tcaCountry = GeneralUtility::makeInstance(TcaCountry::class);
        $processedResult = $tcaCountry->addData($result);
        $countries = $processedResult['processedTca']['columns'][$fieldName]['config']['items'] ?? [];

        // Iterate all possible countries. Fetch the one that matches our $value.
        // Note that the 'label' option already resolved to the proper
        // possible keys (name, localizedName, officialName, localizedOfficialName, iso2, iso3)
        // due to the specifications store in [config] within $result.
        foreach ($countries as $country) {
            if ($country['value'] === $value) {
                return $country['label'];
            }
        }

        // Fallback if no country was resolved.
        // @todo - Should this better return an empty value instead?
        return $value;
    }

    /**
     * Return the record title for database records
     *
     * @param mixed $value Current database value of this field
     * @return string
     */
    protected function getRecordTitleForGroupType($value)
    {
        $labelParts = [];
        foreach ($value as $singleValue) {
            $labelParts[] = $singleValue['title'];
        }
        return implode(', ', $labelParts);
    }

    /**
     * Return the folder names
     *
     * @param array $value Current database value of this field
     */
    protected function getRecordTitleForFolderType(array $value): string
    {
        $labelParts = [];
        foreach ($value as $singleValue) {
            $labelParts[] = $singleValue['folder'];
        }
        return implode(', ', $labelParts);
    }

    /**
     * Returns the record title for checkbox fields
     *
     * @param mixed $value Current database value of this field
     * @param array $fieldConfig TCA field configuration
     * @return string
     */
    protected function getRecordTitleForCheckboxType($value, $fieldConfig)
    {
        $languageService = $this->getLanguageService();
        if (empty($fieldConfig['items']) || !is_array($fieldConfig['items'])) {
            $title = $value
                ? $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:yes')
                : $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:no');
        } else {
            $labelParts = [];
            foreach ($fieldConfig['items'] as $key => $val) {
                if ((int)$value & 2 ** $key) {
                    $labelParts[] = $val['label'];
                }
            }
            $title = implode(', ', $labelParts);
        }
        return $title;
    }

    /**
     * Returns the record title for not transformed text fields
     *
     * @param mixed $value Current database value of this field
     */
    protected function getRecordTitleForStandardTextField(mixed $value): string
    {
        if (!is_string($value)) {
            return '';
        }

        return trim(strip_tags($value));
    }

    protected function getRecordTitleForDatetimeType(?\DateTimeInterface $datetime, array $fieldConfig): string
    {
        if ($datetime === null) {
            return '';
        }
        $format = DateTimeFactory::getFormatFromTCAConfig($fieldConfig);
        if ($format === 'date') {
            $ageSuffix = '';
            // Generate age suffix as long as not explicitly suppressed
            if (!($fieldConfig['disableAgeDisplay'] ?? false)) {
                $now = DateTimeFactory::createFromTimestamp($GLOBALS['EXEC_TIME']);
                $ageSuffix = sprintf(' (%s)', (new DateFormatter())->formatDateInterval(
                    $now->diff($datetime),
                    $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
                ));
            }
            return BackendUtility::date($datetime->getTimestamp()) . $ageSuffix;
        }
        if ($format === 'time') {
            return $datetime->format('H:i');
        }
        if ($format === 'timesec') {
            return $datetime->format('H:i:s');
        }
        if ($format === 'datetime') {
            return BackendUtility::datetime($datetime->getTimestamp());
        }
        if ($format === 'datetimesec') {
            return BackendUtility::datetimesec($datetime->getTimestamp());
        }
        return '';
    }

    /**
     * Returns the record title for password fields
     *
     * @param mixed $value Current database value of this field
     */
    protected function getRecordTitleForPasswordType(mixed $value): string
    {
        return $value ? '********' : '';
    }

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