<?php

namespace CommerceGuys\Addressing;

use CommerceGuys\Addressing\Exception\UnknownLocaleException;

/**
 * Provides locale handling logic.
 *
 * Copied from commerceguys/intl to avoid a dependency.
 */
final class Locale
{
    /**
     * Locale aliases.
     *
     * @var array
     */
    protected static array $aliases = [
        'az-AZ' => 'az-Latn-AZ',
        'bs-BA' => 'bs-Latn-BA',
        'ha-GH' => 'ha-Latn-GH',
        'ha-NE' => 'ha-Latn-NE',
        'ha-NG' => 'ha-Latn-NG',
        'in' => 'id',
        'in-ID' => 'id-ID',
        'iw' => 'he',
        'iw-IL' => 'he-IL',
        'kk-KZ' => 'kk-Cyrl-KZ',
        'ks-IN' => 'ks-Arab-IN',
        'ky-KG' => 'ky-Cyrl-KG',
        'mn-MN' => 'mn-Cyrl-MN',
        'mo' => 'ro-MD',
        'ms-BN' => 'ms-Latn-BN',
        'ms-MY' => 'ms-Latn-MY',
        'ms-SG' => 'ms-Latn-SG',
        'no' => 'nb',
        'no-NO' => 'nb-NO',
        'no-NO-NY' => 'nn-NO',
        'pa-IN' => 'pa-Guru-IN',
        'pa-PK' => 'pa-Arab-PK',
        'sh' => 'sr-Latn',
        'sh-BA' => 'sr-Latn-BA',
        'sh-CS' => 'sr-Latn-RS',
        'sh-YU' => 'sr-Latn-RS',
        'shi-MA' => 'shi-Tfng-MA',
        'sr-BA' => 'sr-Cyrl-BA',
        'sr-ME' => 'sr-Latn-ME',
        'sr-RS' => 'sr-Cyrl-RS',
        'sr-XK' => 'sr-Cyrl-XK',
        'tl' => 'fil',
        'tl-PH' => 'fil-PH',
        'tzm-MA' => 'tzm-Latn-MA',
        'ug-CN' => 'ug-Arab-CN',
        'uz-AF' => 'uz-Arab-AF',
        'uz-UZ' => 'uz-Latn-UZ',
        'vai-LR' => 'vai-Vaii-LR',
        'zh-CN' => 'zh-Hans-CN',
        'zh-HK' => 'zh-Hant-HK',
        'zh-MO' => 'zh-Hant-MO',
        'zh-SG' => 'zh-Hans-SG',
        'zh-TW' => 'zh-Hant-TW',
    ];

    /**
     * Locale parents.
     *
     * @var array
     */
    protected static array $parents = [
        'en-150' => 'en-001',
        'en-AG' => 'en-001',
        'en-AI' => 'en-001',
        'en-AT' => 'en-150',
        'en-AU' => 'en-001',
        'en-BB' => 'en-001',
        'en-BE' => 'en-150',
        'en-BM' => 'en-001',
        'en-BS' => 'en-001',
        'en-BW' => 'en-001',
        'en-BZ' => 'en-001',
        'en-CC' => 'en-001',
        'en-CH' => 'en-150',
        'en-CK' => 'en-001',
        'en-CM' => 'en-001',
        'en-CX' => 'en-001',
        'en-CY' => 'en-001',
        'en-CZ' => 'en-150',
        'en-DE' => 'en-150',
        'en-DG' => 'en-001',
        'en-DK' => 'en-150',
        'en-DM' => 'en-001',
        'en-ER' => 'en-001',
        'en-ES' => 'en-150',
        'en-FI' => 'en-150',
        'en-FJ' => 'en-001',
        'en-FK' => 'en-001',
        'en-FM' => 'en-001',
        'en-FR' => 'en-150',
        'en-GB' => 'en-001',
        'en-GD' => 'en-001',
        'en-GG' => 'en-001',
        'en-GH' => 'en-001',
        'en-GI' => 'en-001',
        'en-GM' => 'en-001',
        'en-GS' => 'en-001',
        'en-GY' => 'en-001',
        'en-HK' => 'en-001',
        'en-HU' => 'en-150',
        'en-ID' => 'en-001',
        'en-IE' => 'en-001',
        'en-IL' => 'en-001',
        'en-IM' => 'en-001',
        'en-IN' => 'en-001',
        'en-IO' => 'en-001',
        'en-IT' => 'en-150',
        'en-JE' => 'en-001',
        'en-JM' => 'en-001',
        'en-KE' => 'en-001',
        'en-KI' => 'en-001',
        'en-KN' => 'en-001',
        'en-KY' => 'en-001',
        'en-LC' => 'en-001',
        'en-LR' => 'en-001',
        'en-LS' => 'en-001',
        'en-MG' => 'en-001',
        'en-MO' => 'en-001',
        'en-MS' => 'en-001',
        'en-MT' => 'en-001',
        'en-MU' => 'en-001',
        'en-MV' => 'en-001',
        'en-MW' => 'en-001',
        'en-MY' => 'en-001',
        'en-NA' => 'en-001',
        'en-NF' => 'en-001',
        'en-NG' => 'en-001',
        'en-NL' => 'en-150',
        'en-NO' => 'en-150',
        'en-NR' => 'en-001',
        'en-NU' => 'en-001',
        'en-NZ' => 'en-001',
        'en-PG' => 'en-001',
        'en-PK' => 'en-001',
        'en-PL' => 'en-150',
        'en-PN' => 'en-001',
        'en-PT' => 'en-150',
        'en-PW' => 'en-001',
        'en-RO' => 'en-150',
        'en-RW' => 'en-001',
        'en-SB' => 'en-001',
        'en-SC' => 'en-001',
        'en-SD' => 'en-001',
        'en-SE' => 'en-150',
        'en-SG' => 'en-001',
        'en-SH' => 'en-001',
        'en-SI' => 'en-150',
        'en-SK' => 'en-150',
        'en-SL' => 'en-001',
        'en-SS' => 'en-001',
        'en-SX' => 'en-001',
        'en-SZ' => 'en-001',
        'en-TC' => 'en-001',
        'en-TK' => 'en-001',
        'en-TO' => 'en-001',
        'en-TT' => 'en-001',
        'en-TV' => 'en-001',
        'en-TZ' => 'en-001',
        'en-UG' => 'en-001',
        'en-VC' => 'en-001',
        'en-VG' => 'en-001',
        'en-VU' => 'en-001',
        'en-WS' => 'en-001',
        'en-ZA' => 'en-001',
        'en-ZM' => 'en-001',
        'en-ZW' => 'en-001',
        'es-AR' => 'es-419',
        'es-BO' => 'es-419',
        'es-BR' => 'es-419',
        'es-BZ' => 'es-419',
        'es-CL' => 'es-419',
        'es-CO' => 'es-419',
        'es-CR' => 'es-419',
        'es-CU' => 'es-419',
        'es-DO' => 'es-419',
        'es-EC' => 'es-419',
        'es-GT' => 'es-419',
        'es-HN' => 'es-419',
        'es-MX' => 'es-419',
        'es-NI' => 'es-419',
        'es-PA' => 'es-419',
        'es-PE' => 'es-419',
        'es-PR' => 'es-419',
        'es-PY' => 'es-419',
        'es-SV' => 'es-419',
        'es-US' => 'es-419',
        'es-UY' => 'es-419',
        'es-VE' => 'es-419',
        'hi-Latn' => 'en-IN',
        'ht' => 'fr-HT',
        'nb' => 'no',
        'nn' => 'no',
        'pt-AO' => 'pt-PT',
        'pt-CH' => 'pt-PT',
        'pt-CV' => 'pt-PT',
        'pt-GQ' => 'pt-PT',
        'pt-GW' => 'pt-PT',
        'pt-LU' => 'pt-PT',
        'pt-MO' => 'pt-PT',
        'pt-MZ' => 'pt-PT',
        'pt-ST' => 'pt-PT',
        'pt-TL' => 'pt-PT',
        'az-Arab' => 'und',
        'az-Cyrl' => 'und',
        'bal-Latn' => 'und',
        'blt-Latn' => 'und',
        'bs-Cyrl' => 'und',
        'byn-Latn' => 'und',
        'en-Dsrt' => 'und',
        'en-Shaw' => 'und',
        'hi-Latn' => 'und',
        'iu-Latn' => 'und',
        'kk-Arab' => 'und',
        'ks-Deva' => 'und',
        'ku-Arab' => 'und',
        'ky-Arab' => 'und',
        'ky-Latn' => 'und',
        'ml-Arab' => 'und',
        'mn-Mong' => 'und',
        'mni-Mtei' => 'und',
        'ms-Arab' => 'und',
        'pa-Arab' => 'und',
        'sat-Deva' => 'und',
        'sd-Deva' => 'und',
        'sd-Khoj' => 'und',
        'sd-Sind' => 'und',
        'so-Arab' => 'und',
        'sr-Latn' => 'und',
        'sw-Arab' => 'und',
        'tg-Arab' => 'und',
        'uz-Arab' => 'und',
        'uz-Cyrl' => 'und',
        'yue-Hans' => 'und',
        'zh-Hant' => 'und',
        'zh-Hant-MO' => 'zh-Hant-HK',
    ];

    /**
     * Checks whether two locales match.
     */
    public static function match(string $firstLocale, string $secondLocale): bool
    {
        if (empty($firstLocale) || empty($secondLocale)) {
            return false;
        }

        return self::canonicalize($firstLocale) === self::canonicalize($secondLocale);
    }

    /**
     * Checks whether two locales have at least one common candidate.
     *
     * For example, "de" and "de-AT" will match because they both have
     * "de" in common. This is useful for partial locale matching.
     *
     * @return bool TRUE if there is a common candidate, FALSE otherwise.
     * @see self::getCandidates
     * TODO Check if typehints could be added (NULL)
     */
    public static function matchCandidates(?string $firstLocale, ?string $secondLocale): bool
    {
        if (empty($firstLocale) || empty($secondLocale)) {
            return false;
        }

        $firstLocale = self::canonicalize($firstLocale);
        $secondLocale = self::canonicalize($secondLocale);
        $firstLocaleCandidates = self::getCandidates($firstLocale);
        $secondLocaleCandidates = self::getCandidates($secondLocale);

        return (bool) array_intersect($firstLocaleCandidates, $secondLocaleCandidates);
    }

    /**
     * Resolves the locale from the available locales.
     *
     * Takes all locale candidates for the requested locale
     * and fallback locale, searches for them in the available
     * locale list. The first found locale is returned.
     * If no candidate is found in the list, an exception is thrown.
     *
     * @param array  $availableLocales The available locales.
     * @param string $locale           The requested locale (i.e. fr-FR).
     * @param string|null $fallbackLocale   A fallback locale (i.e "en").
     *
     * @throws UnknownLocaleException
     * @see self::getCandidates
     */
    public static function resolve(array $availableLocales, string $locale, ?string $fallbackLocale = null): string
    {
        $locale = self::canonicalize($locale);
        $resolvedLocale = null;
        foreach (self::getCandidates($locale, $fallbackLocale) as $candidate) {
            if (in_array($candidate, $availableLocales)) {
                $resolvedLocale = $candidate;
                break;
            }
        }
        // No locale could be resolved, stop here.
        if (!$resolvedLocale) {
            throw new UnknownLocaleException($locale);
        }

        return $resolvedLocale;
    }

    /**
     * Canonicalizes the given locale.
     *
     * Standardizes separators and capitalization, turning
     * a locale such as "sr_rs_latn" into "sr-RS-Latn".
     */
    public static function canonicalize(?string $locale): ?string
    {
        if (empty($locale)) {
            return $locale;
        }

        $locale = str_replace('_', '-', strtolower($locale));
        $localeParts = explode('-', $locale);
        foreach ($localeParts as $index => $part) {
            if ($index === 0) {
                // The language code should stay lowercase.
                continue;
            }

            if (strlen($part) === 4) {
                // Script code.
                $localeParts[$index] = ucfirst($part);
            } else {
                // Country or variant code.
                $localeParts[$index] = strtoupper($part);
            }
        }

        return implode('-', $localeParts);
    }

    /**
     * Gets locale candidates.
     *
     * For example, "bs-Cyrl-BA" has the following candidates:
     * 1) bs-Cyrl-BA
     * 2) bs-Cyrl
     * 3) bs
     *
     * The locale is de-aliased, e.g. the candidates for "sh" are:
     * 1) sr-Latn
     * 2) sr
     *
     * @param string $locale         The locale (i.e. fr-FR).
     * @param string|null $fallbackLocale A fallback locale (i.e "en").
     *
     * @return array An array of all variants of a locale.
     */
    public static function getCandidates(string $locale, ?string $fallbackLocale = null): array
    {
        $locale = self::replaceAlias($locale);
        $candidates = [$locale];
        while ($parent = self::getParent($locale)) {
            $candidates[] = $parent;
            $locale = $parent;
        }
        if (isset($fallbackLocale)) {
            $candidates[] = $fallbackLocale;
            while ($parent = self::getParent($fallbackLocale)) {
                $candidates[] = $parent;
                $fallbackLocale = $parent;
            }
        }

        return array_unique($candidates);
    }

    /**
     * Gets the parent for the given locale.
     */
    public static function getParent(string $locale): ?string
    {
        $parent = null;
        if (isset(self::$parents[$locale])) {
            $parent = self::$parents[$locale];
        } elseif (str_contains($locale, '-')) {
            $localeParts = explode('-', $locale);
            array_pop($localeParts);
            $parent = implode('-', $localeParts);
        }
        // The library doesn't have data for the empty 'und' locale, it
        // is more user friendly to use the configured fallback instead.
        if ($parent === 'und') {
            $parent = null;
        }

        return $parent;
    }

    /**
     * Replaces a locale alias with the real locale.
     *
     * For example, "zh-CN" is replaced with "zh-Hans-CN".
     */
    public static function replaceAlias(?string $locale): ?string
    {
        if (!empty($locale) && isset(self::$aliases[$locale])) {
            $locale = self::$aliases[$locale];
        }

        return $locale;
    }
}
