<?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\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.
 *
 * @internal
 */
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';

    protected string $tableName;
    protected string|int $id;
    protected int $liveId;
    protected array $suggestedValues;
    protected array $persistedValues;
    protected array $configurationFieldNames;
    protected bool $new;
    protected ?string $type = null;
    protected ?State $state = null;
    protected string|int $language = 0;
    protected string|int $parent = '';
    protected string|int|null $source = null;
    /** @var DataMapItem[][] */
    protected array $dependencies = [];

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

        // assign language specific settings of item
        $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()] ?? '');
        }
        // assign live-id of item if available
        $versionSourceFieldName = $item->getVersionSourceFieldName();
        if ($versionSourceFieldName !== null && !empty($persistedValues[$versionSourceFieldName])) {
            $item->liveId = $persistedValues[$versionSourceFieldName];
        }
        return $item;
    }

    public function __construct(
        string $tableName,
        string|int $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.
     */
    public function getId(): string|int
    {
        return $this->id;
    }

    public function getLiveId(): int|string
    {
        return $this->liveId ?? $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'];
    }

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

    protected function getVersionSourceFieldName(): ?string
    {
        return $this->configurationFieldNames['versionSource'] ?? 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;
    }

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

    public function setLanguage(string|int $language): void
    {
        $this->language = $language;
    }

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

    public function setParent(string|int $parent): void
    {
        $this->parent = $this->extractId($parent);
    }

    public function getSource(): string|int|null
    {
        return $this->source;
    }

    public function setSource(string|int $source): void
    {
        $this->source = $this->extractId($source);
    }

    public function getIdForScope(string $scope): string|int
    {
        if (
            $scope === static::SCOPE_PARENT
            || $scope === static::SCOPE_EXCLUDE
        ) {
            return $this->getParent();
        }
        if ($scope === static::SCOPE_SOURCE) {
            // $source is guaranteed to be non-null when SCOPE_SOURCE is applicable:
            // getApplicableScopes() only includes it when getSourceFieldName() !== null,
            // which means setSource() was called during build().
            return $this->source;
        }
        throw new \RuntimeException('Invalid scope', 1486325248);
    }

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

    /**
     * @param DataMapItem[][] $dependencies
     */
    public function setDependencies(array $dependencies): void
    {
        $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;
    }
}
