import { get as lodashGet, isEqual, unset as lodashUnset } from "lodash";
import { FEAGlobals } from "../../../common/Globals";
/**
 * This handles the initial mapping for us. It will crawl through all child objects and map them.
 * - Parent is the current location within the object(`parent.child`).
 * - Null is top level.
 *
 *  This method doesn't produce a mapping for the object passed in. Its use case is creating initial
 * mappings and the root of the store doesn't need one.
 *
 * The mapping is all flattened.
 */
export function initObject(object, parent, mapping) {
    let mapLocation;
    if (!parent) {
        parent = null;
    }
    if (typeof object !== "object") {
        mapLocation = parent ? `${parent}.${object}` : object;
        mapping[mapLocation] = parent;
        return;
    }
    for (const n in object) {
        if (typeof object[n] === "object" && object[n] !== undefined) {
            mapLocation = parent ? `${parent}.${n}` : n;
            mapping[mapLocation] = parent;
            initObject(object[n], mapLocation, mapping); // If we have another object, map it
        }
        else {
            mapLocation = parent ? `${parent}.${n}` : n;
            mapping[mapLocation] = parent;
        }
    }
}
/**
 * Generates a mapping string from a path array e.g. ["finsemble", "custom", "special.value", "subProperty"]
 *
 * The mapping string is used to map out a field that corresponds to an object so that we can trigger
 * listeners when a config value changes.
 *
 * This method tokenizes the . (dot) into $DOT" so that Finsemble is able to retrieve the right listener in the
 * event of a property containing a dot.
 *
 * @param pathArray
 * @returns a string object path with tokenized dots as $DOT$ - eg - finsemble.custom["special.field"].value = finsemble.custom.special$DOT$field.value
 */
export const generateMappingString = (pathArray) => {
    if (pathArray.length === 0)
        return "";
    let mappingString = pathArray[0].replace(".", "$DOT$");
    if (pathArray.length === 1)
        return mappingString;
    for (let index = 1; index < pathArray.length; index++) {
        mappingString = `${mappingString}.${pathArray[index].replace(".", "$DOT$")}`;
    }
    return mappingString;
};
/**
 * For a given path to map, finds the top-level existing mapping.
 *
 * @param pathToMap
 * @param mapping
 * @returns
 */
const getInitialMappingField = (pathToMap, mapping) => {
    if (pathToMap.length === 1) {
        return pathToMap;
    }
    // we need to create a copy here since pop will mutate pathToMap
    // array which could cause adverse side-effects
    const copyPathToMap = [...pathToMap];
    while (copyPathToMap.length > 1 && !mapping.hasOwnProperty(generateMappingString(copyPathToMap))) {
        copyPathToMap.pop();
    }
    return copyPathToMap;
};
/**
 * Recursively maps a object property field to its path.
 *
 * @param object The object being mapped
 * @param parentMapping The top-level existing mapping for the path to map
 * @param pathToMap The path to map
 * @param mapping The current mappings
 */
const mapFieldRecursive = (object, pathToMap, mapping) => {
    const mappingString = generateMappingString(pathToMap);
    const a = mappingString.split(".");
    const currentLocation = mappingString;
    // map the path being added
    if (!mapping.hasOwnProperty(currentLocation)) {
        let newString = null;
        if (a.length > 1) {
            a.pop();
            newString = a.join(".");
        }
        mapping[currentLocation] = newString;
    }
    // map any subfields of the object being mapped
    const newObject = lodashGet(object, pathToMap);
    if (newObject === "undefined") {
        return;
    } // If the location doesn't exist exit.
    if (typeof newObject === "object") {
        for (const key in newObject) {
            let newPath = [...pathToMap, key];
            mapFieldRecursive(object, newPath, mapping);
        }
    }
};
/**
 * Will map out a field in an object. This will prevent us from looping through the whole store values every time
 * we have a change in one of its values.
 *
 * @param object The store object
 * @param pathToMap The path to map
 * @param mapping The object that holds the current mappings
 */
export const mapField = (object, pathToMap, mapping) => {
    // Check if we have something to map
    if (pathToMap.length === 0) {
        FEAGlobals.FSBL.Clients.Logger.error("Empty path to map received");
        return;
    }
    // Check if all parent fields are mapped, if not, move up and start mapping from there
    const initialMapping = getInitialMappingField(pathToMap, mapping);
    // Recursively map all child fields
    mapFieldRecursive(object, initialMapping, mapping);
};
/**
 * Recursively adds to changes array all new paths for a given value.
 *
 * @param value The value to map
 * @param startPath The start path
 * @param changes The changes array that contains the changed properties
 */
const addPathsForAllProperties = (value, startPath, changes) => {
    changes.push(generateMappingString(startPath));
    if (typeof value === "object") {
        for (const key in value) {
            addPathsForAllProperties(value[key], [...startPath, key], changes);
        }
    }
};
/**
 * Recursively computes the changes of an object property path given the current and new values.
 *
 * @param currentValue The current value in the store
 * @param newValue The new value in the store
 * @param path The path related to the value
 * @returns an array of strings with the changes paths
 */
export const computeChanges = (currentValue, newValue, path) => {
    const changes = [];
    if (!isEqual(currentValue, newValue)) {
        changes.push(generateMappingString(path));
        if (typeof currentValue === "object") {
            for (const key in currentValue) {
                let newPath = [...path, key];
                const nextNewValue = newValue && typeof newValue === "object" ? newValue[key] : null;
                changes.push(...computeChanges(currentValue[key], nextNewValue, newPath));
            }
            // handle new values that did not exist before - this is needed as
            // we may have listeners for properties that did not exist in config and
            // said listeners should fire when a given property is added
            if (newValue && typeof newValue === "object") {
                // keys that are in newValue that are not in currentValue
                const newValueKeys = Object.keys(newValue).filter((key) => currentValue && !currentValue.hasOwnProperty(key));
                newValueKeys.forEach((key) => {
                    addPathsForAllProperties(newValue[key], [...path, key], changes);
                });
            }
        }
    }
    return changes;
};
/**
 * Determines what fields are being removed from the store by a particular change and
 * which mappings should therefore be removed. Removes all of the mappings that it returns.
 *
 * @param object The store object
 * @param field The field to check for
 * @param mapping The object that holds the current mappings
 * @returns an array with the removed mappings
 */
export const computeRemovals = (object, field, mapping) => {
    const objectReplacing = lodashGet(object, field);
    if (objectReplacing === null) {
        return [];
    }
    // gather all removals - including the passed in field
    const removals = [generateMappingString(field)];
    const lookField = `${generateMappingString(field)}.`; // we need to check if we are looking into subfields of "field"
    for (const map in mapping) {
        if (map.startsWith(lookField)) {
            removals.push(map);
            delete mapping[map];
        }
    }
    return removals;
};
/**
 * Clones an object to ensure immutability
 *
 * @param value The value to maybe clone
 * @returns a clone of an object or the value itself
 */
export const maybeCloneValue = (value) => {
    if (typeof value === "object") {
        value = JSON.parse(JSON.stringify(value));
    }
    return value;
};
/**
 * Removes the specified object key and value for a given array path.
 *
 * @param values The object to remove the value from
 * @param path The path array property to remove
 */
export const removeValue = (values, path) => {
    // when removing items we need to check if the item being removed is
    // an array item.
    // lodash does not support unset for an array and it would create
    // empty array entries eg. [app1, undefined, app3]
    // for that reason we need to check wether the parent element is an array
    // and splice the index passed in (that will be the last element on the path array)
    if (path.length > 1) {
        const parentValue = lodashGet(values, path.slice(0, -1));
        if (Array.isArray(parentValue)) {
            parentValue.splice(parseInt(path[path.length - 1]), 1);
        }
        else {
            lodashUnset(values, path);
        }
    }
    else {
        lodashUnset(values, path);
    }
};
//# sourceMappingURL=storeUtils.js.map