import { parseTooltipString } from "../../view/tooltip/gml/parser/parseTooltipString.js";
import { assert } from "../assert.js";
import { Cache } from "../cache/Cache.js";
import { computeHashArray } from "../collection/array/computeHashArray.js";
import { isArrayEqualStrict } from "../collection/array/isArrayEqualStrict.js";
import ObservedString from "../model/ObservedString.js";
import { seedVariablesIntoTemplateString } from "../parser/seedVariablesIntoTemplateString.js";
import { STRING_TEMPLATE_VARIABLE_REGEX } from "../parser/STRING_TEMPLATE_VARIABLE_REGEX.js";
import { computeStringHash } from "../primitives/strings/computeStringHash.js";
import { string_compute_similarity } from "../primitives/strings/string_compute_similarity.js";
import { LanguageMetadata } from "./LanguageMetadata.js";

/**
 * Validation utility method
 * @param {string} template
 * @return {string}
 */
function validationMockSeed(template) {

    const result = template.replace(STRING_TEMPLATE_VARIABLE_REGEX, function (match, varName) {
        return "0";
    });

    return result;
}

const EMPTY_SEED = Object.freeze({});

const DEFAULT_LANGUAGE_METADATA = Object.freeze(new LanguageMetadata());

export class Localization {
    constructor() {
        /**
         *
         * @type {AssetManager|null}
         */
        this.assetManager = null;

        this.json = {};

        /**
         * @protected
         * @type {LanguageMetadata}
         */
        this.language_metadata = DEFAULT_LANGUAGE_METADATA;

        /**
         *
         * @type {ObservedString}
         */
        this.locale = new ObservedString('');

        /**
         * In debug mode error messages are a lot more verbose and helpful
         * @type {boolean}
         */
        this.debug = true;

        /**
         * @type {Cache<Array<string>, string>}
         * @private
         */
        this.__failure_cache = new Cache({
            maxWeight: 1024,
            keyHashFunction: (key) => computeHashArray(key, computeStringHash),
            keyEqualityFunction: isArrayEqualStrict
        });
    }

    /**
     * Measured in characters per second
     * @deprecated use 'language_metadata' directly instead
     * @return {number}
     */
    get reading_speed() {
        return this.language_metadata.reading_speed;
    }

    /**
     * Time required to read a piece of text, in seconds
     * @param {string} text
     * @returns {number} time in seconds
     */
    estimateReadingTime(text) {
        assert.isString(text, 'text');

        return text.length / this.reading_speed;
    }

    /**
     *
     * @param {AssetManager} am
     */
    setAssetManager(am) {
        this.assetManager = am;
    }

    /**
     * @returns {boolean}
     * @param {function(key:string, error:*, original: string)} errorConsumer
     */
    validate(errorConsumer) {
        let result = true;

        for (let key in this.json) {
            const value = this.json[key];

            const seededValue = validationMockSeed(value);

            try {
                parseTooltipString(seededValue);
            } catch (e) {
                result = false;
                errorConsumer(key, e, value);
            }
        }

        return result;
    }

    /**
     * Request locale switch
     * Assumes a specific folder structure, each different locale is stored with its locale code as name, and there's also "languages.json" file expected to be in the folder
     * @param {string} locale
     * @param {string} path where to look for localization data
     * @returns {Promise}
     */
    loadLocale(locale, path = 'data/database/text') {
        assert.isString(locale, 'locale');
        assert.isString(path, 'path');

        const am = this.assetManager;

        if (am === null) {
            throw new Error('AssetManager is not set');
        }

        let _path = path.trim();

        while (_path.endsWith('/') || _path.endsWith('\\')) {
            // remove trailing slash if present
            _path.slice(0, _path.length - 1);
        }

        const pLoadData = am.promise(`${_path}/${locale}.json`, "json")
            .then(asset => {
                const json = asset.create();

                this.json = json;

                this.locale.set(locale);
            }, reason => {

                console.error(`Failed to load locale data for locale '${locale}' : ${reason}`);

                // reset data
                this.json = {};

            });

        const pLoadMetadata = am.promise(`${_path}/languages.json`, "json")
            .then(asset => {
                const languages_metadata = asset.create();

                this.language_metadata = LanguageMetadata.fromJSON(languages_metadata[locale]);
            }, reason => {
                console.error(`Failed to load language metadata: ${reason}`);

                this.language_metadata = DEFAULT_LANGUAGE_METADATA;
            });

        return Promise.all([pLoadData, pLoadMetadata]);
    }


    /**
     *
     * @param {number} value
     */
    formatIntegerByThousands(value) {
        const formatter = new Intl.NumberFormat(this.locale.getValue(), { useGrouping: true });

        return formatter.format(value);
    }

    /**
     *
     * @param {string} id
     * @param {Object} seed
     * @private
     */
    __debugMissingKey(id, seed) {
        const locale = this.locale.getValue();

        const seed_string = JSON.stringify(seed);

        const message = this.__failure_cache.getOrCompute([locale, id, seed_string], () => {

            //try to find similar keys
            const similarities = Object.keys(this.json).map(function (key) {
                const distance = string_compute_similarity(key, id);
                return {
                    key,
                    distance
                };
            });

            similarities.sort(function (a, b) {
                return a.distance - b.distance;
            });

            const suggestions = similarities.slice(0, 3).map(p => p.key);

            return `No localization value for id='${id}', seed=${seed_string}, best approximate matches: ${suggestions.join(', ')}`;
        });

        console.warn(message);
    }

    /**
     * Get a localized string by a key
     * @param {string} key
     * @param {object} [seed]
     *
     * @returns {string}
     */
    getString(key, seed = EMPTY_SEED) {
        assert.isString(key, 'id');

        const value = this.json[key];

        if (value === undefined) {

            if (this.debug) {
                this.__debugMissingKey(key, seed)
            }

            //no value, provide substitute
            return `@${key}`;

        }

        //value needs to be seeded
        return seedVariablesIntoTemplateString(value, seed);
    }

    /**
     * Does a given key exist?
     * @param {string} key
     * @return {boolean}
     */
    hasString(key) {
        return this.json[key] !== undefined;
    }
}
