import { makeMetatype } from "@jet/environment/util/metatype";
import { AmpLocalization } from "../amp-localization/amp-localization";
import { isNullOrEmpty } from "../json-parsing/server-data";
import { Wrapper } from "./wrapper";
import { isSome } from "@jet/environment/types/optional";
import * as validation from "@jet/environment/json/validation";
/**
 * Wrapper around an object or `Localization` type.
 * The wrapped object is implemented as part of a native app.
 */
export class LocalizationWrapper extends Wrapper {
    constructor(loc, objectGraph) {
        super(loc);
        /**
         * Path to localization file.
         * The path is set when the localization file is loaded
         * for the first time.
         */
        this.locFile = null;
        /**
         * Instance of `AMPLocalization` providing server-side
         * values for localized stirngs.
         */
        this.ampLoc = new AmpLocalization();
        /**
         * Localization strings cache.
         * Cache and reuse values returned by
         * wrapped `Localization` implementation.
         */
        this.LOC_STRING_CACHE = {};
        this.objectGraph = objectGraph;
    }
    /**
     * Returns the wrapped localization's identifier.
     */
    get identifier() {
        return this.implementation.identifier;
    }
    /**
     * Returns the wrapped localization's safe identifier.
     */
    get safeIdentifier() {
        return this.implementation.identifier.split("_")[0];
    }
    // endregion
    // region Localization
    /**
     * Localizes a string replacing placehoders in key with values in the parameters dictionary
     * @param key The loc key to look up
     * @param params Parameters to replace in the loc string
     * @return The localized string
     */
    string(key, defaultValue) {
        return this.implementation.string(key);
    }
    /**
     * Localizes a string, and logs & throws an error if the key is a screamer.
     * @param key The loc key to look up
     * @return The localized string
     */
    tryString(key) {
        const value = this.implementation.string(key);
        if (value === key || value === `**${key}**`) {
            validation.context("tryString", () => {
                validation.unexpectedType("coercedValue", "Localization key", key, null);
            });
            throw new Error(`No value exists for localization key '${key}'`);
        }
        return value;
    }
    /**
     * Localizes a string, and logs an error if the key is a screamer.
     * @param key The loc key to look up
     * @param fallback The fallback value for unlocalized keys, e.g. English value
     * @return The localized string
     */
    stringWithFallback(key, fallback) {
        const value = this.implementation.string(key);
        return value === `**AppStore.${key}**` ? fallback : value;
    }
    /**
     * Localizes a string using a preferred locale.
     *
     * The implementation of this function is similar to `string(key:)` above, but with the option
     * to prefer a particular locale. If one is provided, we augment the lookup key in the cache
     * with the `locale` value. If one is not provided, we fallback to `string(key:)`. This makes it
     * much easier to use this function directly, in place of `string(key:)` where necessary.

     * @param objectGraph The object graph, used to forward this call on if the required native function is unavailable.
     * Can be removed for 2023.
     * @param key The loc key to look up.
     * @param locale The preferred locale to use for look up. Falls back to default, if unavailable.
     * @param defaultValue  A default value to use if nothing is found.
     */
    stringForPreferredLocale(objectGraph, key, locale, defaultValue) {
        if (isNullOrEmpty(locale)) {
            return this.string(key, defaultValue);
        }
        const cacheKey = `${key}_${locale}`;
        let value = this.LOC_STRING_CACHE[cacheKey];
        if (!value) {
            value = this.implementation.stringForPreferredLocale(key, locale);
            if (value && value !== key) {
                this.LOC_STRING_CACHE[cacheKey] = value;
            }
            else {
                const serverValue = this.ampLoc.localize(key);
                if (serverValue !== key) {
                    value = serverValue;
                }
                else if (defaultValue) {
                    value = defaultValue;
                }
                else {
                    value = key;
                }
            }
        }
        return value;
    }
    /**
     * Localize with appropriate plural form based on the count.
     *
     * Some languages have more plural forms than others.
     * The full set of categories is "zero", "one", "two", "few", "many", and "other".
     *
     * The base loc key is used for "other" (the default).
     * Otherwise the category is appended to the base loc key with a "." separator.
     *
     * http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
     * http://cldr.unicode.org/index/cldr-spec/plural-rules#TOC-Determining-Plural-Categories
     * http://unicode.org/repos/cldr/trunk/specs/ldml/tr35-numbers.html#Language_Plural_Rules
     *
     * English loc keys
     * key = "@@count@@ dogs";      // default, aka "plural"
     * key.zero = "no dogs";        // used when count is 0
     * key.one = "@@count@@ dog";   // used when count is 1, aka "singular"
     *
     * @param key     base loc key
     * @param count   number used to determine the plural form
     * @param params  (optional) substitution keys and values, "count" will be added if it's not already present
     * @return        localized string
     */
    stringWithCount(key, count, params) {
        let value = this.implementation.stringWithCount(key, count);
        if (!value || value === key) {
            const serverValue = this.ampLoc.localizeWithCount(this.objectGraph, key, count, params);
            if (serverValue) {
                value = serverValue;
            }
        }
        return value;
    }
    /**
     * A variation of `stringWithCount` that supports multiple plural forms.
     * @param key     base loc key
     * @param counts  numbers used to determine the plural forms
     * @param params  (optional) substitution keys and values, "count" will be added if it's not already present
     * @return        localized string
     */
    stringWithCounts(key, counts, params) {
        return this.implementation.stringWithCounts(key, counts);
    }
    /**
     * Converts a string into its uppercased form.
     * @param value The string to apply the uppercase to.
     * @returns {string} The loacle-specific, uppercased form of the string. If for whatever reason the upper-casing cannot
     * be applied, this function simply returns the input value.
     */
    uppercased(value) {
        if (!value) {
            return null;
        }
        return value.toLocaleUpperCase(this.safeIdentifier);
    }
    /**
     *  Converts a number into a localized string
     *
     * @param n The number to convert
     * @param decimalPlaces The number of decimal places to include.
     * @return {string} The localized version of the number
     */
    decimal(n, decimalPlaces) {
        let value = this.implementation.decimal(n, decimalPlaces);
        if (!value) {
            if (typeof n === "number") {
                value = `* ${n.toString()} *`;
            }
            else {
                value = this.nullString();
            }
        }
        return value;
    }
    /**
     *  Converts a number of bytes into a localized file size string
     *
     * @param bytes The number of bytes to convert
     * @return The localized file size string
     */
    fileSize(bytes) {
        let value = this.implementation.fileSize(bytes);
        if (!value) {
            value = this.nullString();
        }
        return value;
    }
    /**
     *  Converts a number of bytes into a localized file size string
     *
     * @param count The number of bytes to convert
     * @return The localized file size string
     */
    formattedCount(count) {
        let value = this.implementation.formattedCount(count);
        if (!value) {
            value = this.nullString();
        }
        return value;
    }
    /**
     *  Converts a number into a formatted string representation using the preferred locale identifier.
     *
     * @param count The number to format.
     * @param locale The locale identifier to prefer for lookup.
     * @return The localized string.
     */
    formattedCountForPreferredLocale(objectGraph, count, locale) {
        if (isNullOrEmpty(locale)) {
            return this.formattedCount(count);
        }
        let value = this.implementation.formattedCountForPreferredLocale(count, locale);
        if (!value) {
            value = this.nullString();
        }
        return value;
    }
    /**
     *  Converts a date into a time ago label, showing how long ago
     *  the date occurred
     *
     * @param date The date object to convert
     * @param context The context in which the date should be formatted
     * @return The localized string representing the amount of time that has passed
     */
    timeAgoWithContext(date, context) {
        let value = this.implementation.timeAgoWithContext(date, context);
        if (!value) {
            value = this.nullString();
        }
        return value;
    }
    /**
     *  Converts a date into a localized date string using the provided format
     *
     * @param format The format string describing how the date should be formatted
     * @param date The date object to convert
     * @return The localized string representing the date
     */
    formatDate(format, date) {
        let value = this.implementation.formatDate(format, date);
        if (!value) {
            value = this.nullString();
        }
        return value;
    }
    formatDateWithContext(format, date, context) {
        let value = this.implementation.formatDateWithContext(format, date, context);
        if (!value) {
            value = this.nullString();
        }
        return value;
    }
    formatDateInSentence(sentence, format, date) {
        let value = this.implementation.formatDateInSentence(sentence, format, date);
        if (!value) {
            value = this.nullString();
        }
        return value;
    }
    /**
     *  Converts a date into a relative date, showing how long ago
     *  the date occurred
     *
     * @param date The date object to convert
     * @return The localized string representing the amount of time that has passed
     */
    relativeDate(date) {
        let value = this.implementation.relativeDate(date);
        if (!value) {
            value = this.nullString();
        }
        return value;
    }
    formatDuration(value, unit) {
        let result = this.implementation.formatDuration(value, unit);
        if (!result) {
            result = this.nullString();
        }
        return result;
    }
    // endregion
    // region Private Methods
    /**
     * Applies a new localization file and localization dictionary as the new set of localizations
     *
     * @param file The file name of the file that is loaded
     * @param localizations The localizations dictionary to use
     * @param locale Current normalized locale which includes language code
     */
    applyLocalizations(file, localizations, locale) {
        if (this.isLocFileLoaded(file)) {
            return;
        }
        this.locFile = file;
        // The first 2 characters of normalized locale are the language code.
        this.ampLoc.updateLocalizationData(localizations, locale.slice(0, 2));
    }
    /**
     * Checks if the named loc file is already loaded
     * @param file The file name to check
     * @return {boolean} True or false depending on whether the file name is loaded
     */
    isLocFileLoaded(file) {
        return this.locFile === file;
    }
    /**
     * Normalizes the given bag language into a locale.
     * @param language The language to normalize.
     * @param storefrontIdentifier The storefront identifier to use.
     * @return A locale to use.
     */
    normalizedLocale(objectGraph, language, storefrontIdentifier) {
        language = language.toLowerCase();
        switch (language) {
            case "yue-hant": {
                // Country code from the bag is not available in JS, so we use the storefront.
                const macauStoreFrontIdentifier = objectGraph.props.asString("macauStorefrontIdentifier");
                if (typeof storefrontIdentifier === "string" &&
                    typeof macauStoreFrontIdentifier === "string" &&
                    storefrontIdentifier.indexOf(macauStoreFrontIdentifier) !== -1) {
                    return "zh-ma";
                }
                else {
                    return "zh-hk";
                }
            }
            default:
                return language;
        }
    }
    nullString() {
        return "* null *";
    }
    // endregion
    // region API
    /**
     * Loads the localizations from the JetPack.
     */
    load(objectGraph) {
        // Sanity check
        if (objectGraph.bag.language === undefined || objectGraph.bag.language === null) {
            throw new Error("Bag language is not available. Unable to load localizations.");
        }
        const locale = this.normalizedLocale(objectGraph, objectGraph.bag.language, objectGraph.client.storefrontIdentifier);
        const locName = `local/${locale}`;
        // Load localizations if needed
        if (!this.isLocFileLoaded(locName)) {
            const localizations = objectGraph.props.asDictionary(`localizations.${locale}`);
            if (localizations !== undefined && localizations !== null) {
                this.applyLocalizations(locName, localizations, locale);
            }
            else {
                // Fallback to english
                const fallbackLocalizations = objectGraph.props.asDictionary(`localizations.en-us`);
                if (fallbackLocalizations !== undefined && fallbackLocalizations !== null) {
                    this.applyLocalizations(locName, fallbackLocalizations, locale);
                }
            }
        }
    }
    /**
     * Returns the localized name for the provided device type.
     * The company policy seems to be to always use the branded,
     * non-localized text, but this is just-in-case.
     * @returns {string} The display name for the device.
     */
    deviceDisplayName(objectGraph) {
        if (objectGraph.client.isVision && isSome(objectGraph.host.deviceMarketingFamilyName)) {
            return objectGraph.host.deviceMarketingFamilyName;
        }
        if (objectGraph.host.deviceLocalizedModel) {
            return objectGraph.host.deviceLocalizedModel;
        }
        // TODO: <rdar://problem/33575833> Jet: Remove brand loc fallbacks
        switch (objectGraph.client.deviceType) {
            case "phone":
                const localizedPhoneName = this.string("IPHONE_BRAND_NAME");
                if (localizedPhoneName === "IPHONE_BRAND_NAME") {
                    return "iPhone";
                }
                return localizedPhoneName;
            case "pad":
                const localizedPadName = this.string("IPAD_BRAND_NAME");
                if (localizedPadName === "IPAD_BRAND_NAME") {
                    return "iPad";
                }
                return localizedPadName;
            case "tv":
                const localizedTvName = this.string("APPLE_TV_BRAND_NAME");
                if (localizedTvName === "APPLE_TV_BRAND_NAME") {
                    return "Apple\u00a0TV";
                }
                return localizedTvName;
            case "watch":
                const localizedWatchName = this.string("APPLE_WATCH_BRAND_NAME");
                if (localizedWatchName === "APPLE_WATCH_BRAND_NAME") {
                    return "Apple\u00a0Watch";
                }
                return localizedWatchName;
            case "mac":
                const localizedMacName = this.string("MAC_BRAND_NAME");
                if (localizedMacName === "MAC_BRAND_NAME") {
                    return "Mac";
                }
                return localizedMacName;
            default:
                return null;
        }
    }
}
// region Properties
/**
 * Localization wrapper metatype to use with object graph.
 */
LocalizationWrapper.type = makeMetatype("app-store:loc-wrapper");
//# sourceMappingURL=localization.js.map