<?php

declare(strict_types=1);

/**
 * @defgroup i18n I18N
 * Implements localization concerns such as locale files, time zones, and country lists.
 */

/**
 * @file classes/i18n/LocaleMetadata.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2000-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class LocaleMetadata
 *
 * @ingroup i18n
 *
 * @brief Holds metadata about a system locale
 */

namespace PKP\i18n;

use DomainException;
use Exception;
use PKP\core\ExportableTrait;
use PKP\core\PKPString;
use PKP\facades\Locale;
use PKP\i18n\interfaces\LocaleInterface;
use Sokil\IsoCodes\Database\Languages\Language;

class LocaleMetadata
{
    use ExportableTrait;

    /**
     * The following constants define how the locale information will be presented
     *
     * LANGUAGE_LOCALE_WITHOUT : The locale will be presented in the current selected language
     * So, if English and French is available and user selected locale is French, it will be
     * shown as Français | Anglais
     *
     * LANGUAGE_LOCALE_WITH : The locale will be presented in current selected language along
     * with each locale's own translated name . So, if English and French is available and user
     * selected locale is French, it will be shown as Français/French | Anglais/English
     *
     * LANGUAGE_LOCALE_ONLY : The locale will be presented only in each locale's translated
     * name . So, if English and French is available and user
     * selected locale is French, it will be shown as Français | English
     */
    public const LANGUAGE_LOCALE_WITHOUT = 1;
    public const LANGUAGE_LOCALE_WITH = 2;
    public const LANGUAGE_LOCALE_ONLY = 3;

    private ?object $_parsedLocale = null;

    /**
     * Constructor
     */
    public function __construct(
        /** Locale identification */
        public ?string $locale = null
    ) {
    }

    /**
     * Get the language locale conversion status
     *
     */
    public static function getLanguageLocaleStatuses(): array
    {
        return [
            self::LANGUAGE_LOCALE_WITHOUT,
            self::LANGUAGE_LOCALE_WITH,
            self::LANGUAGE_LOCALE_ONLY
        ];
    }

    /**
     * Retrieves the display name
     *
     * @param string    $locale             The locale code
     * @param bool      $withCountry        Whether to append the country name to language
     * @param int       $langLocaleStatus   The language locale conversion value specified by const LocaleMetadata::LANGUAGE_LOCALE_*
     *
     * @return string The fully qualified locale with/without own translated locale and with/without country name
     */
    public function getDisplayName(?string $locale = null, bool $withCountry = false, int $langLocaleStatus = self::LANGUAGE_LOCALE_WITHOUT): string
    {
        if (!in_array($langLocaleStatus, static::getLanguageLocaleStatuses())) {
            throw new Exception(
                sprintf(
                    'Invalid language locale conversion status %s given, must be among [%s]',
                    $langLocaleStatus,
                    implode(',', static::getLanguageLocaleStatuses())
                )
            );
        }

        $name = PKPString::regexp_replace(
            '/\s*\([^)]*\)\s*/',
            '',
            PKPString::ucfirst(
                $this
                    ->_getLanguage(
                        $langLocaleStatus === self::LANGUAGE_LOCALE_ONLY ? $this->locale : $locale,
                        $langLocaleStatus === self::LANGUAGE_LOCALE_WITH
                    )
                    ->getLocalName()
            )
        );

        if ($langLocaleStatus === self::LANGUAGE_LOCALE_WITH) {
            // Get the translated language name in language's own locale
            $nameInLangLocale = PKPString::regexp_replace(
                '/\s*\([^)]*\)\s*/',
                '',
                PKPString::ucfirst($this->_getLanguage($this->locale)->getLocalName())
            );

            $name = __(
                'common.withForwardSlash',
                [
                    'item' => $name,
                    'afterSlash' => $nameInLangLocale,
                ]
            );
        }

        if (!$withCountry) {
            return $name;
        }

        $country = $this->getCountry($locale);

        if (!$country) {
            return $name;
        }

        if ($langLocaleStatus !== self::LANGUAGE_LOCALE_WITHOUT) {
            $localizedCountryName = $this->getCountry($this->locale);

            if ($langLocaleStatus === self::LANGUAGE_LOCALE_ONLY) {
                $country = $localizedCountryName;
            } else {
                if (strcmp($localizedCountryName, $country) !== 0) {
                    $country = __(
                        'common.withForwardSlash',
                        [
                            'item' => $country,
                            'afterSlash' => $localizedCountryName
                        ]
                    );
                }
            }
        }

        return __(
            'common.withParenthesis',
            [
                'item' => $name,
                'inParenthesis' => $country,
            ]
        );
    }

    /**
     * Retrieves the language name
     */
    public function getLanguage(?string $locale = null): string
    {
        return $this->_getLanguage($locale)->getLocalName();
    }

    /**
     * Retrieves the country name
     */
    public function getCountry(?string $locale = null): ?string
    {
        return $this->_parse()->country ? Locale::getCountries($locale)->getByAlpha2($this->_parse()->country)?->getLocalName() : null;
    }

    /**
     * Retrieves the script name
     */
    public function getScript(?string $locale = null): ?string
    {
        $script = ucfirst($this->_parse()->script ?? '');
        return $script ? Locale::getScripts($locale)->getByAlpha4($script)->getLocalName() : null;
    }

    /**
     * Whether the locale expects text on the right-to-left format
     */
    public function isRightToLeft(): bool
    {
        $locale = $this->_parse();
        $language = strtolower($locale->language ?? '');
        $script = strtolower($locale->script ?? '');
        $rightToLeftLanguages = array_fill_keys(['ar', 'dv', 'fa', 'he', 'ku', 'nqo', 'prs', 'ps', 'sd', 'syr', 'ug', 'ur', 'yi'], true);
        $languageScriptExceptions = ['sd-deva' => false, 'tzm-arab' => true, 'pa-arab' => true];
        return $languageScriptExceptions["{$language}-{$script}"]
            ?? $rightToLeftLanguages[$language]
            ?? $languageScriptExceptions[$this->getIsoAlpha3() . "-{$script}"]
            ?? $rightToLeftLanguages[$this->getIsoAlpha3()]
            ?? false;
    }

    /**
     * Compares two locales and retrieves the completeness ratio (source locale keys which are present in the reference)
     * If a locale isn't supplied, LocaleInterface::DEFAULT_LOCALE will be used as reference
     */
    public function getCompletenessRatio(string $referenceLocale = null): float
    {
        $destiny = Locale::getBundle($referenceLocale ?? LocaleInterface::DEFAULT_LOCALE)->getTranslator()->getEntries();
        $source = Locale::getBundle($this->locale, false)->getTranslator()->getEntries();
        $intersection = array_intersect_key($source, $destiny);
        return min(1, count($intersection) / max(1, count($destiny)));
    }

    /**
     * Retrieves whether the locale can be considered complete respecting a threshold level of completeness
     */
    public function isComplete(float $minimumThreshold = 0.9, ?string $referenceLocale = null): bool
    {
        return $this->getCompletenessRatio($referenceLocale) >= $minimumThreshold;
    }

    /**
     * Retrieves the ISO639-1 representation
     */
    public function getIsoAlpha2(): string
    {
        return $this->_getLanguage()->getAlpha2();
    }

    /**
     * Retrieves the ISO639-3 representation
     */
    public function getIsoAlpha3(): string
    {
        return $this->_getLanguage()->getAlpha3();
    }

    /**
     * Retrieves the language
     */
    private function _getLanguage(?string $locale = null, bool $fromCache = true): ?Language
    {
        return Locale::getLanguages($locale, $fromCache)->getByAlpha2($this->_parse()->language);
    }

    /**
     * Parses the locale string and retrieve its pieces
     */
    private function _parse(): object
    {
        if (isset($this->_parsedLocale)) {
            return $this->_parsedLocale;
        }
        if (!preg_match(LocaleInterface::LOCALE_EXPRESSION, $this->locale, $matches)) {
            throw new DomainException("Invalid locale \"{$this->locale}\"");
        }
        return $this->_parsedLocale = (object) [
            'language' => $matches['language'],
            'country' => $matches['country'] ?? null,
            // Updates our script definitions to match the ISO 15924
            'script' => isset($matches['script']) ? str_replace(['cyrillic', 'latin'], ['latn', 'cyrl'], strtolower($matches['script'])) : null
        ];
    }
}
