import _ from 'lodash';
import '../../../babel/localeData.js';
import '../../../assert/js/assert.js';
import '../../../globals.js';
// Copyright 2022-2024, University of Colorado Boulder
/**
 * Sets up a system of Properties to handle translation fallback and phet-io support for a single translated string.
 *
 * @author Jonathan Olson <jonathan.olson>
 */
import TinyProperty from '../../../axon/js/TinyProperty.js';
import localeProperty from '../../../joist/js/i18n/localeProperty.js';
import arrayRemove from '../../../phet-core/js/arrayRemove.js';
import Tandem from '../../../tandem/js/Tandem.js';
import chipper from './chipper.js';
import { localizedStrings } from './getStringModule.js';
import LocalizedStringProperty from './LocalizedStringProperty.js';
// constants
const FALLBACK_LOCALE = 'en';
const localeData = phet.chipper.localeData;
assert && assert(localeData, 'localeData expected but global has not been set');
class LocalizedString {
    constructor(stringKey, 
    // Store initial values, so we can handle state deltas
    localeToTranslationMap, tandem, metadata) {
        this.stringKey = stringKey;
        this.localeToTranslationMap = localeToTranslationMap;
        // Uses lazy creation of locales
        this.localePropertyMap = new Map();
        // Store initial values, so we can handle state deltas
        this.initialValues = {};
        this.property = new LocalizedStringProperty(this, tandem, metadata);
        // Add to a global list to support PhET-iO serialization and internal testing
        localizedStrings.push(this);
    }
    /**
     * Returns an object that shows the changes of strings from their initial values. This includes whether strings are
     * marked as "overridden"
     */
    getStateDelta() {
        const result = {};
        this.usedLocales.forEach(locale => {
            const initialValue = this.initialValues[locale];
            const currentValue = this.getLocaleSpecificProperty(locale).value;
            if (currentValue !== initialValue) {
                result[locale] = currentValue;
            }
        });
        return result;
    }
    /**
     * Take a state from getStateDelta, and apply it.
     */
    setStateDelta(state) {
        // Create potential new locales (since locale-specific Properties are lazily created as needed
        Object.keys(state).forEach(locale => this.getLocaleSpecificProperty(locale));
        this.usedLocales.forEach(locale => {
            const localeSpecificProperty = this.getLocaleSpecificProperty(locale);
            const initialValue = this.initialValues[locale];
            assert && assert(initialValue !== undefined);
            const stateValue = state[locale] !== undefined ? state[locale] : null;
            localeSpecificProperty.value = stateValue ?? initialValue;
        });
    }
    get usedLocales() {
        return [...this.localePropertyMap.keys()];
    }
    /**
     * Returns the locale-specific Property for any locale (lazily creating it if necessary)
     */
    getLocaleSpecificProperty(locale) {
        // Lazy creation
        if (!this.localePropertyMap.has(locale)) {
            // Locales in order of fallback
            const orderedLocales = LocalizedString.getLocaleFallbacks(locale);
            // Find the first-defined value
            let initialValue = null;
            for (const locale of orderedLocales) {
                if (this.localeToTranslationMap[locale] !== undefined) {
                    initialValue = this.localeToTranslationMap[locale];
                    break;
                }
            }
            // Should be guaranteed because of `en` as a fallback
            assert && assert(initialValue !== undefined, 'no initial value found for', locale);
            this.initialValues[locale] = initialValue;
            this.localePropertyMap.set(locale, new TinyProperty(initialValue));
        }
        return this.localePropertyMap.get(locale);
    }
    dispose() {
        this.property.dispose();
        arrayRemove(localizedStrings, this);
    }
    /**
     * Reset to the initial value for the specified locale, used for testing.
     */
    restoreInitialValue(locale) {
        assert && assert(typeof this.initialValues[locale] === 'string', 'initial value expected for', locale);
        this.property.value = this.initialValues[locale];
    }
    static getLocaleFallbacks(locale = localeProperty.value) {
        return _.uniq([
            locale,
            ...(localeData[locale].fallbackLocales || []),
            FALLBACK_LOCALE
        ]);
    }
    /**
     * Computes a nested map of LocalizedStringProperties from a nested input structure of strings.
     * The string Properties will change values based on the current locale
     * (stored in localeProperty).
     *
     * For instance, if we have an example StringMap:
     *
     * const stringMap = {
     *   en: {
     *     a: 'a',
     *     nest: {
     *       x: 'x',
     *       y: 'y'
     *     }
     *   },
     *   hi: {
     *     'a': 'ए',
     *     'b': 'ब',
     *     nest: {
     *       x: 'एक्स',
     *     }
     *   }
     * }
     *
     * Then the resulting LocalizedString.getNestedStringProperties( stringMap ) would
     * have the structure:
     *
     * {
     *   aStringProperty: LocalizedStringProperty,
     *   bStringProperty: LocalizedStringProperty,
     *   nest: {
     *     xStringProperty: LocalizedStringProperty,
     *     yStringProperty: LocalizedStringProperty
     *   }
     * }
     *
     * It is recommended to put each translation file into a separate JSON file,
     * and import them into a single file that will be passed to this function.
     *
     * @param stringData
     */
    static getNestedStringProperties(stringData) {
        const locales = Object.keys(stringData);
        // We will stuff data into here.
        const result = {};
        // Given an array of keys (e.g. [ 'a', 'b', 'c' ]), look up the value in the stringData.
        // E.g. stringData[ locale ].a?.b?.c
        const lookupWithLocale = (keys, locale) => {
            let object = stringData[locale];
            for (const key of keys) {
                if (object) {
                    // @ts-expect-error
                    object = object[key];
                }
            }
            return object;
        };
        // Recursively construct the map, appending more keys as we go.
        const recur = (keys) => {
            // All potential keys of the next level.
            // Some will be for strings, some will be for nested objects.
            const potentialKeys = _.sortBy(_.uniq(locales.map(locale => {
                const object = lookupWithLocale(keys, locale);
                return object ? Object.keys(object) : [];
            }).flat()));
            for (const key of potentialKeys) {
                const nextKeys = [...keys, key];
                // Is the key for a string (and if so, what locales is it defined for?)
                let isString = true;
                const stringLocales = [];
                for (const locale of locales) {
                    const object = lookupWithLocale(keys, locale);
                    if (object) {
                        const value = object[key];
                        if (typeof value === 'string') {
                            stringLocales.push(locale);
                        }
                        else {
                            isString = false;
                        }
                    }
                }
                if (isString) {
                    assert && assert(stringLocales.length > 0);
                    // Build the map with string values
                    const map = {};
                    for (const locale of stringLocales) {
                        map[locale] = lookupWithLocale(nextKeys, locale);
                    }
                    // If there is no "fallback" entry, put it in as a fallback manually
                    // (so we will always have a string)
                    if (!stringLocales.includes(FALLBACK_LOCALE)) {
                        map[FALLBACK_LOCALE] = map[stringLocales[0]];
                    }
                    const stringProperty = new LocalizedStringProperty(new LocalizedString(nextKeys.join('.'), map, Tandem.OPT_OUT), Tandem.OPT_OUT);
                    _.set(result, [...keys, `${key}StringProperty`], stringProperty);
                }
                else {
                    recur(nextKeys);
                }
            }
        };
        recur([]);
        return result;
    }
}
chipper.register('LocalizedString', LocalizedString);
export default LocalizedString;
//# sourceMappingURL=LocalizedString.js.map