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

/**
 * A representation of
 *    language key (based on ISO 639-1 / ISO 639-2)
 *   - the optional four-letter script code that can follow the language code according to the Unicode ISO 15924 Registry (e.g. Hans in zh_Hans)
 *   - region / country (based on ISO 3166-1)
 * separated with a "-".
 *
 * This conforms to IETF - RFC 5646 (see https://datatracker.ietf.org/doc/rfc5646/) in a simplified form.
 */
class Locale implements \Stringable
{
    // taken from https://meta.wikimedia.org/wiki/Template:List_of_language_names_ordered_by_code
    protected const RIGHT_TO_LEFT_LANGUAGE_CODES = [
        'ar', // Arabic
        'arc', // Aramaic
        'arz', // Egyptian Arabic
        'ckb', // Kurdish (Sorani)
        'dv', // Divehi
        'fa', // Persian
        'ha', // Hausa
        'he', // Hebrew
        'khw', // Khowar
        'ks', // Kashmiri
        'ps', // Pashto
        'sd', // Sindhi
        'ur', // Urdu
        'uz-AF', // Uzbeki Afghanistan
        'yi', // Yiddish
    ];
    protected string $locale;
    protected string $languageCode;
    protected ?string $languageScript = null;
    protected ?string $countryCode = null;
    protected ?string $codeSet = null;
    // see https://wiki.archlinux.org/title/locale#Generating_locales
    protected ?string $charsetModifier = null;

    /**
     * List of language dependencies for an actual language. This setting is used for local variants of a language
     * that depend on their "main" language, like Brazilian Portuguese or Canadian French.
     *
     * @var array<int, string>
     */
    protected array $dependencies = [];

    /**
     * Only use the constructor directly if you know what you are doing and want to create custom locales with custom dependencies.
     * Otherwise, instantiate a new Locale object via Locales->createLocale() as it deals with registered dependencies automatically.
     */
    public function __construct(
        string $locale = 'en',
        array $dependencies = []
    ) {
        $locale = $this->normalize($locale);
        if (str_contains($locale, '@')) {
            [$locale, $this->charsetModifier] = explode('@', $locale);
        }
        if (str_contains($locale, '.')) {
            [$locale, $this->codeSet] = explode('.', $locale);
        }
        if (strtolower($locale) === 'c') {
            $this->codeSet = 'C';
            $locale = 'en';
        } elseif (strtolower($locale) === 'posix') {
            $this->codeSet = 'POSIX';
            $locale = 'en';
        }
        if (str_contains($locale, '-')) {
            [$this->languageCode, $tail] = explode('-', $locale, 2);
            if (str_contains($tail, '-')) {
                [$this->languageScript, $this->countryCode] = explode('-', $tail);
            } elseif (strlen($tail) === 4) {
                $this->languageScript = $tail;
            } else {
                $this->countryCode = $tail ?: null;
            }
            $this->languageCode = strtolower($this->languageCode);
            $this->languageScript = $this->languageScript ? ucfirst(strtolower($this->languageScript)) : null;
            $this->countryCode = $this->countryCode ? strtoupper($this->countryCode) : null;
        } else {
            $this->languageCode = strtolower($locale);
        }

        $this->locale = $this->languageCode . ($this->languageScript ? '-' . $this->languageScript : '') . ($this->countryCode ? '-' . $this->countryCode : '');
        $this->dependencies = array_map($this->normalize(...), $dependencies);
    }

    public function getName(): string
    {
        return $this->locale;
    }

    public function getLanguageCode(): string
    {
        return $this->languageCode;
    }

    public function isRightToLeftLanguageDirection(): bool
    {
        return in_array($this->languageCode, self::RIGHT_TO_LEFT_LANGUAGE_CODES, true) || in_array($this->locale, self::RIGHT_TO_LEFT_LANGUAGE_CODES, true);
    }

    public function getLanguageScriptCode(): ?string
    {
        return $this->languageScript;
    }

    public function getCountryCode(): ?string
    {
        return $this->countryCode;
    }

    /**
     * Return the locale as ISO/IEC 15897 format, including a possible POSIX charset
     * "cs_CZ.UTF-8"
     * see https://en.wikipedia.org/wiki/ISO/IEC_15897
     * https://en.wikipedia.org/wiki/Locale_(computer_software)#POSIX_platforms
     * @internal
     */
    public function posixFormatted(): string
    {
        $charsetModifier = $this->charsetModifier ? '@' . $this->charsetModifier : '';
        if ($this->codeSet === 'C' || $this->codeSet === 'POSIX') {
            return $this->codeSet . $charsetModifier;
        }
        $formatted = $this->languageCode;
        if ($this->countryCode) {
            $formatted .= '_' . $this->countryCode;
        }
        if ($this->codeSet) {
            $formatted .= '.' . $this->codeSet;
        }
        return $formatted . $charsetModifier;
    }

    /**
     * @internal
     */
    public function getPosixCodeSet(): ?string
    {
        return $this->codeSet;
    }

    public function getDependencies(): array
    {
        return $this->dependencies;
    }

    protected function normalize(string $locale): string
    {
        if ($locale === 'default') {
            return 'en';
        }
        if (str_contains($locale, '_')) {
            $locale = str_replace('_', '-', $locale);
        }

        return $locale;
    }

    public function __toString(): string
    {
        return $this->locale;
    }
}
