<?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\Core\DataHandling\Localization;

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;

/**
 * Entity for data-map item.
 */
class DataMapItem
{
    public const TYPE_PARENT = 'parent';
    public const TYPE_DIRECT_CHILD = 'directChild';
    public const TYPE_GRAND_CHILD = 'grandChild';

    public const SCOPE_PARENT = State::STATE_PARENT;
    public const SCOPE_SOURCE = State::STATE_SOURCE;
    public const SCOPE_EXCLUDE = 'exclude';

    /**
     * @var string
     */
    protected $tableName;

    /**
     * @var string|int
     */
    protected $id;

    /**
     * @var array
     */
    protected $suggestedValues;

    /**
     * @var array
     */
    protected $persistedValues;

    /**
     * @var array
     */
    protected $configurationFieldNames;

    /**
     * @var bool
     */
    protected $new;

    /**
     * @var string
     */
    protected $type;

    /**
     * @var State|null
     */
    protected $state;

    /**
     * @var string|int
     */
    protected $language;

    /**
     * @var string|int
     */
    protected $parent;

    /**
     * @var string|int
     */
    protected $source;

    /**
     * @var DataMapItem[][]
     */
    protected $dependencies = [];

    /**
     * Builds a data-map item. In addition to the constructor, the values
     * for language, parent and source record pointers are assigned as well.
     *
     * @param string|int $id
     * @return DataMapItem
     */
    public static function build(
        string $tableName,
        $id,
        array $suggestedValues,
        array $persistedValues,
        array $configurationFieldNames
    ) {
        $item = GeneralUtility::makeInstance(
            static::class,
            $tableName,
            $id,
            $suggestedValues,
            $persistedValues,
            $configurationFieldNames
        );

        $item->language = (int)($suggestedValues[$item->getLanguageFieldName()] ?? $persistedValues[$item->getLanguageFieldName()] ?? 0);
        $item->setParent($suggestedValues[$item->getParentFieldName()] ?? $persistedValues[$item->getParentFieldName()] ?? '');
        if ($item->getSourceFieldName() !== null) {
            $item->setSource($suggestedValues[$item->getSourceFieldName()] ?? $persistedValues[$item->getSourceFieldName()] ?? '');
        }

        return $item;
    }

    /**
     * @param string|int $id
     */
    public function __construct(
        string $tableName,
        $id,
        array $suggestedValues,
        array $persistedValues,
        array $configurationFieldNames
    ) {
        $this->tableName = $tableName;
        $this->id = $id;

        $this->suggestedValues = $suggestedValues;
        $this->persistedValues = $persistedValues;
        $this->configurationFieldNames = $configurationFieldNames;

        $this->new = !MathUtility::canBeInterpretedAsInteger($id);
    }

    /**
     * Gets the current table name of this data-map item.
     */
    public function getTableName(): string
    {
        return $this->tableName;
    }

    /**
     * Gets the id of this data-map item.
     *
     * @return int|string
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Gets the suggested values that were initially
     * submitted as the whole data-map to the DataHandler.
     */
    public function getSuggestedValues(): array
    {
        return $this->suggestedValues;
    }

    /**
     * Gets the persisted values that represent the persisted state
     * of the record this data-map item is a surrogate for - does only
     * contain relevant field values.
     */
    public function getPersistedValues(): array
    {
        return $this->persistedValues;
    }

    public function getConfigurationFieldNames(): array
    {
        return $this->configurationFieldNames;
    }

    public function getLanguageFieldName(): string
    {
        return $this->configurationFieldNames['language'];
    }

    public function getParentFieldName(): string
    {
        return $this->configurationFieldNames['parent'];
    }

    /**
     * @return string|null
     */
    public function getSourceFieldName()
    {
        return $this->configurationFieldNames['source'] ?? null;
    }

    public function isNew(): bool
    {
        return $this->new;
    }

    public function getType(): string
    {
        if ($this->type === null) {
            // implicit: default language, it's a parent
            if ($this->language === 0) {
                $this->type = static::TYPE_PARENT;
            } elseif (
                // implicit: having source value different to parent value, it's a 2nd or higher level translation
                $this->source !== 0
                && $this->source !== null
                && $this->source !== $this->parent
            ) {
                $this->type = static::TYPE_GRAND_CHILD;
            } else {
                // implicit: otherwise, it's a 1st level translation
                $this->type = static::TYPE_DIRECT_CHILD;
            }
        }
        return $this->type;
    }

    public function isParentType(): bool
    {
        return $this->getType() === static::TYPE_PARENT;
    }

    public function isDirectChildType(): bool
    {
        return $this->getType() === static::TYPE_DIRECT_CHILD;
    }

    public function isGrandChildType(): bool
    {
        return $this->getType() === static::TYPE_GRAND_CHILD;
    }

    public function getState(): ?State
    {
        if ($this->state === null && !$this->isParentType()) {
            $this->state = $this->buildState();
        }
        return $this->state;
    }

    /**
     * @return string|int
     */
    public function getLanguage()
    {
        return $this->language;
    }

    /**
     * @param string|int $language
     */
    public function setLanguage($language)
    {
        $this->language = $language;
    }

    /**
     * @return string|int
     */
    public function getParent()
    {
        return $this->parent;
    }

    /**
     * @param string|int $parent
     */
    public function setParent($parent)
    {
        $this->parent = $this->extractId($parent);
    }

    /**
     * @return string|int
     */
    public function getSource()
    {
        return $this->source;
    }

    /**
     * @param string|int $source
     */
    public function setSource($source)
    {
        $this->source = $this->extractId($source);
    }

    /**
     * @param string $scope
     * @return int|string
     */
    public function getIdForScope(string $scope)
    {
        if (
            $scope === static::SCOPE_PARENT
            || $scope === static::SCOPE_EXCLUDE
        ) {
            return $this->getParent();
        }
        if ($scope === static::SCOPE_SOURCE) {
            return $this->getSource();
        }
        throw new \RuntimeException('Invalid scope', 1486325248);
    }

    /**
     * @return DataMapItem[][]
     */
    public function getDependencies(): array
    {
        return $this->dependencies;
    }

    /**
     * @param DataMapItem[][] $dependencies
     */
    public function setDependencies(array $dependencies)
    {
        $this->dependencies = $dependencies;
    }

    /**
     * @return DataMapItem[]
     */
    public function findDependencies(string $scope): array
    {
        return $this->dependencies[$scope] ?? [];
    }

    /**
     * @return string[]
     */
    public function getApplicableScopes(): array
    {
        $scopes = [];
        if (!empty($this->getSourceFieldName())) {
            $scopes[] = static::SCOPE_SOURCE;
        }
        $scopes[] = static::SCOPE_PARENT;
        $scopes[] = static::SCOPE_EXCLUDE;
        return $scopes;
    }

    /**
     * Extracts real id from provided id-value, which can either be a real
     * integer value, a 'NEW...' id, or a combined identifier 'tt_content_13'.
     */
    protected function extractId(int|string $idValue): int|string
    {
        if (MathUtility::canBeInterpretedAsInteger($idValue)) {
            return $idValue;
        }
        $idValue = (string)$idValue;
        if (str_starts_with($idValue, 'NEW')) {
            return $idValue;
        }
        // @todo Handle if $tableName does not match $this->tableName
        $id = BackendUtility::splitTable_Uid($idValue)[1];
        return $id;
    }

    protected function buildState(): ?State
    {
        // build from persisted states
        if (!$this->isNew()) {
            $state = State::fromJSON(
                $this->tableName,
                $this->persistedValues['l10n_state'] ?? null
            );
        } elseif (is_string($this->suggestedValues['l10n_state'] ?? null)) {
            // use provided states for a new and copied element
            $state = State::fromJSON(
                $this->tableName,
                $this->suggestedValues['l10n_state']
            );
        } else {
            // provide the default states
            $state = State::create($this->tableName);
        }
        // switch "custom" to "source" state for 2nd level translations
        if ($this->isNew() && $this->isGrandChildType()) {
            $state->updateStates(State::STATE_CUSTOM, State::STATE_SOURCE);
        }
        // apply any provided updates to the states
        if (is_array($this->suggestedValues['l10n_state'] ?? null)) {
            $state->update($this->suggestedValues['l10n_state']);
        }
        return $state;
    }
}
