/*
 * This file is part of the Amalthea library.
 * Copyright (C) 2018-2022, 2024, 2026 Eugene 'Vindex' Stulin
 * Distributed under the BSL 1.0 or the GNU LGPL 3.0 or later.
 */

/**
 * The module implements the simplest system
 * for creating language localizations.
 */
module amalthea.langlocal;

import std.algorithm : startsWith;
import std.array;
import std.file : readText;
import std.json;
import std.process;

public import amalthea.libcore;
import amalthea.csv;


static string[][] localeStrings;
static string[string] langconformity;
static string currentLanguage = "en";


/*******************************************************************************
 * Current system language.
 */
string getSystemLanguage() {
    string lang;
    foreach(envvar; ["LC_ALL", "LC_MESSAGES", "LANG"]) {
        lang = environment.get(envvar, "");
        if (!lang.empty) {
            break;
        }
    }
    if (lang.empty) {
        return "en";
    }
    lang = lang.split(".")[0];
    lang = lang.split("@")[0];
    return lang;
}


/*******************************************************************************
 * Current application language by langlocal settings.
 */
string getCurrentLanguage() {
    return currentLanguage;
}


/*******************************************************************************
 * Initializes localization by a two-dimensional array of strings.
 * Titles of columns (first line - index 0) must be locales
 * in Linux style without encoding (en, en_GB, eo, fr_CA, ru_RU, etc.),
 * first locale is en or en_* always.
 *
 * Params:
 *     stringsWithLocalizations = Two-dimensional array containing columns
 *                                of strings in different languages.
 *     language = New current language (system language by default).
 *
 */
void initLocalization(
    in string[][] stringsWithLocalizations,
    string language = ""
) {
    localeStrings = cast(string[][])stringsWithLocalizations;
    string firstCell = localeStrings[0][0];
    if (firstCell != "en" && !firstCell.startsWith("en_")) {
        string errMsg = "Error: array in cell [0][0] must consist en or en_*";
        throw new LocalizationsTableException(errMsg);
    }
    if (language == "") {
        language = getSystemLanguage;
    }
    chooseLanguage(language);
}


/*******************************************************************************
 * Initializes localization by a CSV file with translations.
 * Titles of columns (first line - index 0) must be locales in Linux style
 * without encoding (en, en_US, eo, fr_CA, ru, ru_BY, etc.),
 * first locale is en or en_* always.
 *
 * Params:
 *     csvPath = A path to a CSV file with translations.
 *     language = A new current language (system language by default).
 *
 */
void initLocalizationWithCSV(string csvPath, string language = "") {
    auto csv = CSV(csvPath);
    initLocalization(csv.getTable(), language);
}
deprecated void initLocalization(string csvPath, string language = "") {
    initLocalizationWithCSV(csvPath, language);
}


/*******************************************************************************
 * Initializes localization by a JSON file with translations.
 * The JSON file must contain array of objects with languages as keys and with
 * text phrases as values.
 * Example: [{"en": "One", "eo": "Unu"},{"en": "Two", "eo": "Du"}]
 * Translations must contain "en" or "en_*" always.
 *
 * Params:
 *     jsonPath = A path to a JSON file with translations.
 *     language = A new current language (system language by default).
 */
void initLocalizationWithJSON(string jsonPath, string language = "") {
    JSONValue json = parseJSON(readText(jsonPath));
    return initLocalizationWithJSON(json, language);
}


/*******************************************************************************
 * Initializes localization by a std.json.JSON object with translations.
 * The JSON object must contain array of objects with languages as keys and with
 * text phrases as values.
 * Example: [{"en": "One", "eo": "Unu"},{"en": "Two", "eo": "Du"}]
 * Translations must contain "en" or "en_*" always.
 *
 * Params:
 *     json     = Filled JSON object with translations.
 *     language = A new current language (system language by default).
 */
void initLocalizationWithJSON(std.json.JSONValue json, string language = "") {
    size_t[string] langIndexes;
    size_t counter;
    string keyLang;
    foreach(key, _; json.array[0].object) {
        langIndexes[key] = counter;
        if (keyLang.empty && (key == "en" || key.startsWith("en_"))) {
            keyLang = key;
        }
        counter++;
    }
    if (keyLang.empty) {
        auto errMsg = `Error: translations must consist "en" or "en_*"`;
        throw new LocalizationsTableException(errMsg);
    } else {
        if (langIndexes[keyLang] != 0) {
            string keyWithZeroIndex;
            foreach(k, v; langIndexes) {
                if (v == 0) {
                    keyWithZeroIndex = k;
                    break;
                }
            }
            // swap
            langIndexes[keyWithZeroIndex] = langIndexes[keyLang];
            langIndexes.remove(keyLang);
            langIndexes[keyLang] = 0;  // "en" always in zero cell
        }
    }
    string[][] table;
    string[] languages = new string[langIndexes.length];
    foreach(langId, index; langIndexes) {
        languages[index] = langId;
    }
    table = [languages];
    foreach(JSONValue elem; json.array) {
        string[] row = new string[langIndexes.length];
        foreach(lang, text; elem.object) {
            if (lang !in langIndexes) {
                auto msg = "Unexpected language: " ~ lang;
                throw new LocalizationsTableException(msg);
            }
            row[langIndexes[lang]] = text.str;
        }
        table ~= row;
    }
    initLocalization(table, language);
}


/*******************************************************************************
 * This function allows to choose current locale for application.
 */
void chooseLanguage(string language) {
    if (localeStrings.empty) {
        return;
    }
    langconformity.clear();
    currentLanguage = "";
    foreach(i, locale; localeStrings[0]) {
        if (language != locale) {
            continue;
        }
        currentLanguage = language;
        size_t currentTableColumn = i;
        foreach(row; localeStrings) {
            if (row[currentTableColumn] == "") {
                langconformity[row[0]] = row[0];
            } else {
                langconformity[row[0]] = row[currentTableColumn];
            }
        }
        return;
    }
    if (currentLanguage.empty) {
        string[] splitted = language.split('_');
        if (splitted.length == 2) {
            language = splitted[0];  // without territory: "fr_CA" -> "fr"
            chooseLanguage(language);
            return;
        }
    }
    currentLanguage = "en";
}


/*******************************************************************************
 * This function returns string corresponding to the English version,
 * according to the current localization selected.
 */
string s_(string englishString) {
    return langconformity.get(englishString, englishString);
}
alias _s = s_;


unittest {
    initLocalization([
        ["en",       "ru",        "eo"],
        ["Hello",    "Привет",    "Saluton"],
        ["Orange",   "Апельсин",  "Oranĝo"],
        ["Language", "Язык",      "Lingvo"],
        ["Computer", "Компьютер", "Komputilo"]
    ]);

    chooseLanguage("ru");
    assert("Orange"._s == "Апельсин");
    assert("Untranslated string"._s == "Untranslated string");
    assert("Other text" == "Other text".s_);

    chooseLanguage("eo");
    assert("Language"._s == "Lingvo");
    chooseLanguage("Quenya");  // invalid
    assert(getCurrentLanguage == "en");

    // check fallback ("ru_RU" -> "ru")
    chooseLanguage("ru_RU");
    assert("Orange"._s == "Апельсин");

    // en
    chooseLanguage("en");
    assert("Orange"._s == "Orange");
}


/*******************************************************************************
 * Exception for incorrect localization tables.
 */
class LocalizationsTableException : Exception {
    this(string msg) {
        super(msg);
    }
}
