<?php

/**
 * This file contains \QUI\Locale
 */

namespace QUI;

use ForceUTF8\Encoding;
use IntlDateFormatter;
use NumberFormatter;
use QUI;
use QUI\Utils\StringHelper;

use function DusanKasan\Knapsack\first;
use function explode;
use function file_exists;
use function in_array;
use function is_array;
use function is_numeric;
use function is_object;
use function is_string;
use function preg_replace;
use function setlocale;
use function shell_exec;
use function str_contains;
use function str_replace;
use function strftime;
use function strlen;
use function strtolower;
use function strtotime;
use function strtoupper;
use function trim;
use function usort;

/**
 * The locale object
 * translate the ui and all messages
 */
class Locale implements \Stringable
{
    /**
     * no translation flag
     */
    public bool $no_translation = false;

    /**
     * @var array<array-key, mixed>|false
     */
    protected array|false $dateFormats = false;

    /**
     * The current lang
     */
    protected string $current = 'en';

    /**
     * ini file objects
     *
     * @var array<array-key, mixed>
     */
    protected array $inis = [];

    /**
     * List of internal locale list for setlocale()
     *
     * @var array<string, array<int, string>>
     */
    protected array $localeList = [];

    /**
     * Saves the current language of this Locale if setTemporaryCurrent is used.
     */
    protected false|string $tempCurrent = false;

    public function __toString(): string
    {
        return 'Locale()';
    }

    /**
     * Sets the current language of this locale to $lang.
     *
     * WARNING: It's STRONGLY advised to use resetCurrent() immediately after
     * your use case. Changing the global current language longer than that may otherwise have
     * unforeseeable consequences!
     */
    public function setTemporaryCurrent(string $lang): void
    {
        if (empty($this->tempCurrent)) {
            $this->tempCurrent = $this->getCurrent();
        }

        $this->setCurrent($lang);
    }

    public function getCurrent(): string
    {
        return $this->current;
    }

    /**
     * Set the current language
     *
     * @param string $lang - en, en_EN, de, de_DE, de_AT
     */
    public function setCurrent(string $lang): void
    {
        $lang = preg_replace('/[^a-zA-Z_]/', '', $lang) ?? $lang;
        $lang = trim($lang);

        if (!empty($lang)) {
            $this->current = $lang;
        } else {
            $this->current = QUI::conf('globals', 'standardLanguage');
        }
    }

    /**
     * Resets the current language to the initial state. Useful only after setTemporaryCurrent()
     * was used!
     */
    public function resetCurrent(): void
    {
        if (!empty($this->tempCurrent)) {
            $this->setCurrent($this->tempCurrent);
            $this->tempCurrent = false;
        }
    }

    /**
     * @return array<array-key, mixed>|string
     */
    public function getDecimalSeparator(): array|string
    {
        return $this->get('quiqqer/core', 'numberFormat.decimal_separator');
    }

    /**
     * Get the translation
     *
     * @param false|array<array-key, mixed> $replace
     * @return ($value is string ? string : array<array-key, mixed>|string)
     */
    public function get(string $group, false|string $value = false, false|array $replace = false): array|string
    {
        $str = $this->getHelper($group, $value);

        if (empty($replace)) {
            return str_replace('{\n}', PHP_EOL, $str);
        }

        foreach ($replace as $key => $value) {
            if (is_array($value)) {
                continue;
            }

            if (is_object($value)) {
                continue;
            }

            $str = str_replace('[' . $key . ']', $value, $str);
        }

        return str_replace('{\n}', PHP_EOL, $str);
    }

    /**
     * Translation helper method
     *
     * @return ($value is string ? string : array<array-key, mixed>|string)
     *
     * @see ->get()
     */
    protected function getHelper(
        string $group,
        false|string $value = false,
        false|string $current = false
    ): array|string {
        if ($this->no_translation) {
            return '[' . $group . '] ' . $value;
        }

        if (!$current) {
            $current = $this->current;
        }

        $translation = LocaleRuntimeCache::get($current, $group, $value);

        if ($translation !== null) {
            return $translation;
        }

        try {
            $this->initConfig($group, $current);

            $translation = LocaleRuntimeCache::get($current, $group, $value);

            if ($translation !== null) {
                return $translation;
            }
        } catch (QUI\Exception $Exception) {
            QUI\System\Log::writeDebugException($Exception);
        }

        return '[' . $group . '] ' . $value;
    }

    /**
     * read a config
     *
     * @param string $group - translation group
     * @param string|null $lang - translation language
     *
     * @throws QUI\Exception
     */
    public function initConfig(string $group, ?string $lang = null): void
    {
        if (empty($lang)) {
            $lang = $this->current;
        }

        if (LocaleRuntimeCache::isCached($lang, $group)) {
            return;
        }

        $file = $this->getTranslationFile($lang, $group);

        if (!file_exists($file)) {
            return;
        }

        $Config = new QUI\Config($file);
        LocaleRuntimeCache::set($lang, $group, $Config->toArray());
    }

    /**
     * Get the translation file in dependence to the lang and group
     */
    public function getTranslationFile(string $lang, string $group): string
    {
        $lang = preg_replace('/[^a-zA-Z]/', '', $lang) ?? $lang;
        $locale = StringHelper::toLower($lang);//. '_' . StringHelper::toUpper($lang);
        $group = str_replace('/', '_', $group);

        return $this->dir() . '/' . $locale . '/LC_MESSAGES/' . $group . '.ini.php';
    }

    /**
     * Folder located the translations
     */
    public function dir(): string
    {
        return VAR_DIR . 'locale/';
    }

    /**
     * @deprecated
     *
     * Set translation
     *
     * @param array<string, mixed>|string $key
     */
    public function set(string $lang, string $group, array|string $key, false|string $value = false): void
    {
        if (!is_array($key)) {
            LocaleRuntimeCache::set($lang, $group, [$key => $value]);
            return;
        }

        LocaleRuntimeCache::set($lang, $group, $key);
    }

    public function getGroupingSeparator(): string
    {
        return $this->get('quiqqer/core', 'numberFormat.grouping_separator');
    }

    public function getDecimalPattern(): string
    {
        return $this->get('quiqqer/core', 'numberFormat.decimal_pattern');
    }

    public function getPercentPattern(): string
    {
        return $this->get('quiqqer/core', 'numberFormat.percent_pattern');
    }

    public function getCurrencyPattern(): string
    {
        return $this->get('quiqqer/core', 'numberFormat.currency_pattern');
    }

    public function getAccountingCurrencyPattern(): string
    {
        return $this->get('quiqqer/core', 'numberFormat.accounting_currency_pattern');
    }

    /**
     * Refresh the locale
     * Clears the locale
     *
     * @return void
     */
    public function refresh()
    {
    }

    public function formatNumber(string|float|int $number, int $format = NumberFormatter::DECIMAL): string
    {
        $localeCode = QUI::getLocale()->getLocalesByLang(
            QUI::getLocale()->getCurrent()
        );

        $Formatter = new NumberFormatter($localeCode[0], $format);

        if (is_string($number)) {
            $number = (float)$number;
        }

        $decimalSeparator = self::get('quiqqer/core', 'numberFormat.decimal_separator');
        $groupingSeparator = self::get('quiqqer/core', 'numberFormat.grouping_separator');
        $decimalPattern = self::get('quiqqer/core', 'numberFormat.decimal_pattern');

        if (!empty($decimalSeparator)) {
            $Formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $decimalSeparator);
        }

        if (!empty($groupingSeparator)) {
            $Formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $groupingSeparator);
        }

        if (!empty($decimalPattern)) {
            $Formatter->setPattern($decimalPattern);
        }

        //  numberFormat.numbering_system
        //  numberFormat.percent_pattern
        //  numberFormat.currency_pattern
        //  numberFormat.accounting_currency_pattern

        //  "numbering_system": "latn",
        //  "decimal_pattern": "#,##0.###",
        //  "percent_pattern": "#,##0%",

        return (string)$Formatter->format($number);
    }

    /**
     * Return the locale list for a language
     *
     * @param string $lang - Language code (de, en, fr ...)
     *
     * @return array<int, string>
     */
    public function getLocalesByLang(string $lang): array
    {
        if (isset($this->localeList[$lang])) {
            return $this->localeList[$lang];
        }

        // no shell
        if (!QUI\Utils\System::isShellFunctionEnabled('locale')) {
            // if we cannot read locale list, so we must guess
            $langCode = strtolower($lang) . '_' . strtoupper($lang);

            $this->localeList[$lang] = [
                $langCode,
                $langCode . '.utf8',
                $langCode . '.UTF-8',
                $langCode . '@euro'
            ];

            return $this->localeList[$lang];
        }


        // via shell
        $locales = shell_exec('locale -a');
        $locales = explode("\n", (string)$locales);

        $langList = [];

        foreach ($locales as $locale) {
            if (!str_starts_with($locale, $lang)) {
                continue;
            }

            $langList[] = $locale;
        }

        $langCode = strtolower($lang) . '_' . strtoupper($lang);

        // not the best solution
        if ($lang === 'en') {
            $langCode = 'en_GB';
        }

        // sort, main locale to the top
        usort(
            $langList,
            static function (string $a, string $b) use ($langCode): int {
                if ($a === $b) {
                    return 0;
                }

                $aIsMainLocale = str_starts_with($a, $langCode);
                $bIsMainLocale = str_starts_with($b, $langCode);

                if ($aIsMainLocale !== $bIsMainLocale) {
                    return $aIsMainLocale ? -1 : 1;
                }

                return $a <=> $b;
            }
        );

        $this->localeList[$lang] = $langList;

        return $this->localeList[$lang];
    }

    /**
     * Format a date timestamp
     *
     * @param int|string $timestamp
     * @param false|string $format - (optional) ;if not given, it uses the quiqqer system format
     *
     * @return string
     */
    public function formatDate(int|string $timestamp, false|string $format = false): string
    {
        $Formatter = self::getDateFormatter();
        $current = $this->getCurrent();

        if (!is_numeric($timestamp)) {
            $timestamp = (int)strtotime($timestamp);
        }

        // new stuff, compatible with php9
        if (empty($format)) {
            return Encoding::toUTF8($Formatter->format($timestamp));
        }

        if (mb_strpos($format, '%') === false) {
            $Formatter->setPattern($format);

            return Encoding::toUTF8(
                $Formatter->format($timestamp)
            );
        }

        // deprecate log
        QUI\System\Log::addDeprecated('Deprecated formatDate usage');

        // old stuff with strftime
        $locales = $this->getLocalesByLang($current);
        $localeCode = first($locales);

//        if ($format) {
        $oldLocale = setlocale(LC_TIME, "0");

        setlocale(LC_TIME, $localeCode);
        $result = strftime($format, (int)$timestamp);
        setlocale(LC_TIME, (string)$oldLocale);

        return Encoding::toUTF8($result);
//        }

//        $formats = $this->getDateFormats();
//
//        if (!empty($formats[$current])) {
//            $oldLocale = setlocale(LC_TIME, "0");
//
//            setlocale(LC_TIME, $localeCode);
//            $result = strftime($formats[$current], $timestamp);
//            setlocale(LC_TIME, $oldLocale);
//
//            return Encoding::toUTF8($result);
//        }
//
//        return Encoding::toUTF8(strftime('%D', $timestamp));
    }

    /**
     * Return a date formatter for the current language
     */
    public function getDateFormatter(
        int $dateType = IntlDateFormatter::SHORT,
        int $timeType = IntlDateFormatter::NONE
    ): IntlDateFormatter {
        $localeCode = $this->getLocalesByLang($this->getCurrent());

        return new IntlDateFormatter($localeCode[0], $dateType, $timeType);
    }

    /**
     * Exist the variable in the translation?
     *
     * @param string $group - language group
     * @param false|string $value - language group variable, optional
     *
     * @return boolean
     */
    public function exists(string $group, false|string $value = false): bool
    {
        $str = $this->getHelper($group, $value);

        if ($value === false) {
            if (empty($str)) {
                return false;
            }

            return true;
        }

        $_str = '[' . $group . '] ' . $value;

        if ($_str === $str) {
            return false;
        }

        return true;
    }

    /**
     * Exists the language in the locale?
     *
     * @param string $language
     */
    public function existsLang($language): bool
    {
        return in_array($language, QUI::availableLanguages());
    }

    /**
     * Get the translation from a specific language
     *
     * @param false|array<array-key, mixed> $replace
     *
     * @return ($value is string ? string : array<array-key, mixed>|string)
     */
    public function getByLang(
        string $lang,
        string $group,
        false|string $value = false,
        false|array $replace = false
    ): array|string {
        $str = $this->getHelper($group, $value, $lang);

        if (empty($replace)) {
            return str_replace('{\n}', PHP_EOL, $str);
        }

        foreach ($replace as $key => $value) {
            if (is_array($value)) {
                continue;
            }

            if (is_object($value)) {
                continue;
            }

            $str = str_replace('[' . $key . ']', $value, $str);
        }

        return str_replace('{\n}', PHP_EOL, $str);
    }

    /**
     * Parse a locale string and translate it
     * a locale strings looks like: [group/group] var.var.var
     *
     * @param array<array-key, mixed>|string $title
     *
     * @return array<array-key, mixed>|string
     */
    public function parseLocaleString(array|string $title): array|string
    {
        if (is_array($title)) {
            return $this->parseLocaleArray($title);
        }

        if (!$this->isLocaleString($title)) {
            return $title;
        }

        $parts = $this->getPartsOfLocaleString($title);

        if ($parts[0] === null || $parts[1] === null) {
            return $title;
        }

        return $this->get($parts[0], $parts[1]);
    }

    /**
     * Parse a locale array and translate it
     *
     * @param array<int, mixed> $locale - with group, translation var and replacement vars (optional)
     * @return array<array-key, mixed>|string
     */
    public function parseLocaleArray(array $locale): array|string
    {
        if (!isset($locale[0]) || !isset($locale[1])) {
            return '';
        }

        if (!isset($locale[2])) {
            return $this->get($locale[0], $locale[1]);
        }

        return $this->get($locale[0], $locale[1], $locale[2]);
    }

    /**
     * Verified the string if the string is a locale string
     * a locale strings looks like: [group/group] var.var.var
     */
    public function isLocaleString(string $str): bool
    {
        return preg_match('/^\[[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\] [A-Za-z0-9_.-]+$/', $str) === 1;
    }

    /**
     * Return the parts of a locale string
     * a locale strings looks like: [group/group] var.var.var
     *
     * @param string $str
     * @return array{0: string|null, 1: string|null} -  [0=>group, 1=>var]
     */
    public function getPartsOfLocaleString(string $str): array
    {
        if (
            preg_match(
                '/^\[([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\] ([A-Za-z0-9_.-]+)$/',
                $str,
                $matches
            ) !== 1
        ) {
            return [null, null];
        }

        return [$matches[1], $matches[2]];
    }

    /**
     * Return all available date formats
     *
     * @return array<array-key, mixed>
     */
    protected function getDateFormats(): array
    {
        if ($this->dateFormats) {
            return $this->dateFormats;
        }

        $this->dateFormats = QUI::conf('date_formats');

        if (!$this->dateFormats) {
            $this->dateFormats = [];
        }

        return $this->dateFormats;
    }
}
