/*!
 * Copyright 2017 - 2020 by ChartIQ, Inc.
 * All rights reserved.
 */
import Logger from "../clients/logger";
import { System } from "./system";
import lodash from "lodash";
/**
 * Convenience function converts application.getManifest into an async function
 */
const getManifest = async () => new Promise((resolve) => {
    System.getManifest((manifest) => {
        resolve({ err: null, data: manifest });
    });
});
/**
 * Tries to resolve a custom variable first from the finsemble configuration object in manifest-local.json. If it can't
 * find it, tries the finsemble.custom object.
 *
 * This method also provides additional logging for when the variable can not be resolved.
 */
const getMacro = (manifest, macroName) => { var _a, _b; return (_a = manifest.finsemble[macroName]) !== null && _a !== void 0 ? _a : (_b = manifest.finsemble) === null || _b === void 0 ? void 0 : _b.custom[macroName]; };
/**
 * Called by resolveObject().
 * This function parses a string to find variables.
 * It looks up the value of any identified variables, replacing them in the string.
 * The completed string is then returned.
 * */
const resolveString = (manifest, configString) => {
    let replacedMacro = false;
    const delimiters = /[/\\:?=&\s]/; // delimiters in regex form
    const tokens = configString.split(delimiters);
    for (let i = 0; i < tokens.length; i++) {
        if (tokens[i][0] === "$") {
            // special variable character $ has to first char in string
            const macroName = tokens[i].substring(1); // string off the leading $
            const macroValue = getMacro(manifest, macroName);
            if (typeof macroValue === "undefined") {
                // Don't do macro replacement if undefined -- a second pass might resolve macros after import files
                Logger.system.warn("ConfigUtil.resolveString", `Missing [${macroName}] variable from finsemble.custom configuration object`);
            }
            else {
                const newValue = configString.replace(tokens[i], macroValue); // replace the variable reference with new value
                Logger.system.verbose("forceObjectsToLogger", "ConfigUtil.resolveManifestMacros:resolveString configString", tokens[i], macroName, macroValue, "old value=", configString, "value=", newValue);
                replacedMacro = true; // <<-- here is the only place needsAnotherPass is set, since still resolving variables
                configString = newValue;
            }
        }
    }
    return { configString, replacedMacro };
};
// process an array of config items looking for variables to resolve (a recursive routine)
const resolveArray = (manifest, configArray, recursionLevel) => {
    let replacedMacro = false;
    if (recursionLevel > 1000) {
        Logger.system.error("ConfigUtil resolveArray maxiumum recursion level reached");
        return { replacedMacro };
    }
    Logger.system.verbose("forceObjectsToLogger", "resolveArray", "recursionLevel", recursionLevel, "configArray:", configArray);
    for (let i = 0; i < configArray.length; i++) {
        const value = configArray[i];
        if (typeof value === "string" && value.indexOf("$") > -1) {
            const result = resolveString(manifest, value);
            configArray[i] = result.configString;
            replacedMacro = replacedMacro || result.replacedMacro;
        }
        else if (value instanceof Array) {
            resolveArray(manifest, value, recursionLevel + 1); // array reference passed so don't need return value
        }
        else if (typeof value === "object") {
            // eslint-disable-next-line @typescript-eslint/no-use-before-define
            resolveObject(manifest, value, recursionLevel + 1); // object reference passed so don't need return value
        }
    }
    return { replacedMacro };
};
/**
 * Expand "variables" within a config object. Variables are strings that begin with "$".
 * For instance, `finsemble.bar:"help", foo:$bar` would be expanded into `finsemble.bar:"help",foo:"help"`
 * This is a recursive routine.
 *
 */
const resolveObject = (manifest, configObject, recursionLevel) => {
    let replacedMacro = false;
    if (recursionLevel > 1000) {
        Logger.system.error("ConfigUtil resolveObject maxiumum recursion level reached");
        return { replacedMacro };
    }
    configObject = configObject || {}; // don't error on bad config
    Logger.system.verbose("forceObjectsToLogger", "ConfigUtil.resolveManifestMacros:resolveObject", "recursionLevel", recursionLevel, "configObject:", configObject);
    Object.keys(configObject).forEach((key) => {
        const value = configObject[key];
        if (typeof value === "string" && value.indexOf("$") > -1) {
            const result = resolveString(manifest, value);
            configObject[key] = result.configString;
            replacedMacro = replacedMacro || result.replacedMacro;
        }
        else if (value instanceof Array) {
            resolveArray(manifest, value, recursionLevel + 1); // array reference passed so don't need return value
        }
        else if (typeof value === "object") {
            resolveObject(manifest, value, recursionLevel + 1); // object reference passed so don't need return value
        }
    });
    return { replacedMacro };
};
export class ConfigUtil {
    /**
     * <h2>Finsemble Configuration Utility Functions</h2>
     * @private
     * @class ConfigUtil
     */
    /**
     * Recursively walks a config object tp resolves manifest macros (i.e. $applicationRoot)
     *
     * Note: electron-adapter's Manifest.ts has a smaller footprint implementation of
     * macro replacement (using traverse())
     *
     * @param finsembleConfig The manifest
     * @param startingConfigObject The section of code to process for substitutions
     */
    resolveManifestMacros(manifest, startingConfigObject) {
        let pass;
        const MAXIMUM_PASSES = 100;
        // Since macros may be nested we take multiple passes, replacing macros until no more are replaced.
        for (pass = 0; pass < MAXIMUM_PASSES; pass++) {
            const { replacedMacro } = resolveObject(manifest, startingConfigObject, 1);
            if (!replacedMacro)
                break;
        }
        if (pass >= MAXIMUM_PASSES) {
            Logger.system.error("forceObjectsToLogger", "resolveManifestMacros exceeded maximum passes so continuing -- probably a recursive macro definitions.  Look in this config for unresolved macro variables", manifest);
        }
    }
    // This does minimal processing of the manifest, just enough to support getting the router up, which is only expanding variables (e.g. moduleRoot) in the raw manifest
    async getExpandedRawManifest() {
        Logger.system.debug("ConfigUtil.getExpandedRawManifest starting");
        const { err, data: manifest } = await getManifest();
        if (err || !manifest) {
            Logger.system.error("ConfigUtil.getExpandedRawManifest:application.getManifest:err", err);
        }
        else {
            Logger.system.debug("forceObjectsToLogger", "ConfigUtil.getExpandedRawManifest:getExpandedRawManifest: manifest retrieved. Pre-variable resolution", manifest);
            this.resolveManifestMacros(manifest, manifest.finsemble); // resolve variables first time so can find config location
            Logger.system.debug("forceObjectsToLogger", "ConfigUtil.getExpandedRawManifest:getExpandedRawManifest:Complete. post-variable resolution", manifest);
        }
        return { err, data: manifest };
    }
    // Since could not read JSON file, try to read it as a plain text file and log contexts for diagnostics.
    async captureAndLogBadFileData(url) {
        let err = null;
        let data;
        Logger.system.debug(`captureAndLogBadFileData for ${url}`);
        try {
            const response = await fetch(url, {
                credentials: "include",
                cache: "no-cache",
            });
            if (response.status != 200) {
                err = `HTTP get error for ${url}: ${response.status} ${response.statusText}}`;
            }
            else {
                // capture the text data in the response
                data = await response.text();
            }
        }
        catch (error) {
            err = `Failure importing config from ${url} ${error.toString()}`;
        }
        if (err) {
            Logger.system.error("forceObjectsToLogger", `captureAndLogBadFileData failed for ${url}`, err);
        }
        else {
            Logger.system.error("forceObjectsToLogger", `captureAndLogBadFileData successful for ${url}`, data);
        }
    }
    // async read of JSON config file
    async fetchConfigFile(url) {
        let err = null;
        let data;
        Logger.system.debug(`fetching import ${url}`);
        try {
            const response = await fetch(url, {
                credentials: "include",
                cache: "no-cache",
            });
            if (response.status != 200) {
                err = `HTTP get error for ${url}: ${response.status} ${response.statusText}`;
            }
            else {
                data = await response.json();
            }
        }
        catch (error) {
            err = `Failure importing config from ${url} ${error.toString()}`;
        }
        if (err) {
            Logger.system.error("forceObjectsToLogger", err);
            // Since you can't ready the stream a second time as text for diagnostics, have to start all over to capture text data.
            this.captureAndLogBadFileData(url);
        }
        return { err, data };
    }
    graftImportOnToManifest(manifest, configImport) {
        Object.keys(configImport).forEach((key) => {
            if (key === "importConfig") {
                // add any importConfig items from the core to the existing importConfig
                manifest.finsemble.importConfig = manifest.finsemble.importConfig || [];
                for (let i = 0; i < configImport.importConfig.length; i++) {
                    manifest.finsemble.importConfig.unshift(configImport.importConfig[i]);
                }
            }
            else if (key === "importThirdPartyConfig") {
                // add any importThirdPartyConfig items from the core to the existing importConfig
                manifest.finsemble.importThirdPartyConfig = manifest.finsemble.importThirdPartyConfig || [];
                for (let i = 0; i < configImport.importThirdPartyConfig.length; i++) {
                    manifest.finsemble.importThirdPartyConfig.unshift(configImport.importThirdPartyConfig[i]);
                }
            }
            else {
                manifest.finsemble[key] = configImport[key];
            }
        });
        this.resolveManifestMacros(manifest, manifest.finsemble); // resolve variables with finsemble config
        Logger.system.debug("forceObjectsToLogger", "ConfigUtil.getInitialManifest:getCoreConfig:Initial Manifest after variables Resolved", manifest);
    }
    // This does a "first stage" processing of the manifest, providing enough config to start finsemble.
    // Pull in the initial manifest, which includes getting the "hidden" core config file along with its import definitions, and expand all variables.
    // However, the full config processing, including actually doing the imports, is only done in the Config Service.
    async getInitialManifest() {
        const { err, data: manifest } = await getManifest();
        if (!err && manifest) {
            // get raw manifest
            manifest.finsemble = manifest.finsemble || {}; // don't error on bad config
            this.resolveManifestMacros(manifest, manifest.finsemble); // resolve variables first time so can find config config location
            // if applicationRoot is not defined then a lot of error noise will be generated, so put in special check with dedicated error message
            if (!manifest.finsemble.applicationRoot) {
                Logger.system.error("FATAL ERROR: applicationRoot was not defined in manifest file.");
            }
            const coreConfigPath = `${manifest.finsemble.moduleRoot}/configs/core/config.json`; // <<<--- here is the "hidden" core config file
            const { err: errorImportingConfig, data: configImport } = await this.fetchConfigFile(coreConfigPath);
            // fetch the core config file
            if (!errorImportingConfig && configImport) {
                this.graftImportOnToManifest(manifest, configImport);
            }
            else {
                Logger.system.error(`ConfigUtil.getInitialManifest:getCoreConfig:failed importing into finsemble config. $moduleRoot=${manifest.finsemble.moduleRoot}; Full Path=${coreConfigPath}`, errorImportingConfig);
            }
        }
        return { err, data: manifest };
    }
    // output JSON object to file
    promptAndSaveJSONToLocalFile(filename, jsonObject) {
        Logger.system.debug("saveJSONToLocalFile", filename, jsonObject);
        const dataStr = JSON.stringify(jsonObject, null, "\t");
        const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`;
        const exportFileDefaultName = `${filename}.json`;
        const linkElement = document.createElement("a");
        linkElement.setAttribute("href", dataUri);
        linkElement.setAttribute("download", exportFileDefaultName);
        linkElement.click();
    }
    // utility function for future use
    configFormatForExport(typeOfConfig, configObject) {
        Logger.system.debug("configFormatForExport starting", typeOfConfig, configObject);
        let exportConfig = lodash.cloneDeep(configObject);
        if (typeOfConfig === "raw") {
            // do nothing since config is ready to export as is
        }
        else if (typeOfConfig === "all") {
            delete exportConfig.importConfig;
            delete exportConfig.comment;
        }
        else if (typeOfConfig === "application") {
            delete exportConfig.importConfig;
            delete exportConfig.comment;
            delete exportConfig.system;
            delete exportConfig.services;
        }
        else if (typeOfConfig === "workspace") {
            exportConfig = { workspace: exportConfig };
        }
        else if (typeOfConfig === "workspaceTemplate") {
            const workspaceDefinition = {};
            workspaceDefinition[exportConfig.name] = exportConfig;
            exportConfig = { workspaceTemplates: workspaceDefinition };
        }
        else if (typeOfConfig === "services") {
            exportConfig = exportConfig.services;
        }
        else if (typeOfConfig === "components") {
            exportConfig = exportConfig.components;
        }
        return exportConfig;
    }
    /**
     * Convenience function to get a default value from config.
     *
     * @param {object} base base path of config object
     * @param {string} path path string of config property
     * @param {any} defaultValue if path value not defined or null, then use default value
     *
     * @returns {object} return config value or default value
     *
     * @example
     *
     *		defaultAdaptor = ConfigUtil.getDefault(manifest, "manifest.finsemble.defaultStorage", "LocalStorageAdapter");
     *		sameDomainTransport = ConfigUtil.getDefault(finConfig, "finConfig.router.sameDomainTransport", "SharedWorker");
     *		var serverAddress = getDefault(params, "params.transportSettings.FinsembleTransport.serverAddress", "ws://127.0.0.1:3376");
     *
     */
    getDefault(base, path, defaultValue) {
        let result = defaultValue;
        if (base) {
            try {
                const properties = path.split(".");
                let currentValue = base;
                for (let i = 1; i < properties.length; i++) {
                    currentValue = currentValue[properties[i]];
                }
                result = currentValue;
            }
            catch (err) {
                result = defaultValue;
            }
            if (typeof result === "undefined")
                result = defaultValue;
        }
        return result;
    }
    /**
     * Returns log function to use for logging results of a deprecation operation
     *
     * @private
     * @param {string} userMessageType deprecated-config operation's message type which determines how to log the message (e.g. error, warn, log)
     * @returns {*} returns log function to use (e.g. logger.system.error, logger.system.warn, logger.system.log)
     * @memberof DeprecatedConfigUtil
     */
    logDeprecation(userMessageType) {
        let loggerEntryPoint = Logger.system.warn;
        if (userMessageType === "error") {
            loggerEntryPoint = Logger.system.error;
        }
        return loggerEntryPoint;
    }
}
export const ConfigUtilInstance = new ConfigUtil();
//# sourceMappingURL=configUtil.js.map