import { each as asyncEach, map as asyncMap } from "async";
import { toPath, get as lodashGet, set as lodashSet, isEmpty as lodashIsEmpty } from "lodash";
import * as storeUtils from "../platform/services/distributedStore/storeUtils";
import Logger from "./logger";
const INVALID_PATH_ERROR_MESSAGE = `Invalid path - MUST be a path array. e.g. for myObject.property use ['myObj', 'property']`;
const INVALID_ARRAY_OF_PATHS_ERROR_MESSAGE = `Invalid array of paths - MUST be an array of path array. e.g. for myObject.property 
and myObject.property2 use [['myObj', 'property'], ['myObj', 'property2']]`;
const INVALID_PATH_VALUES_ERROR_MESSAGE = `Invalid path and values - Must be an array of objects. eg. [{field: ['field1', 'property1'], value: "foo"}, {field: ['field2', 'property2'], value: "bar"}]`;
export class StoreModel {
    /**
     * @ignore
     */
    constructor(params, routerClient, removeStoreCallback) {
        var _a, _b;
        /**
         * @ignore
         */
        this.isGlobal = false;
        /**
         * @ignore
         */
        this.values = {};
        /**
         * @ignore
         */
        this.listeners = {};
        /**
         * @ignore
         * @deprecated
         */
        this.registeredDispatchListeners = [];
        /**
         * @ignore
         */
        this.mapping = {};
        /**
         * Prevents multiple listener requests on the same field from being sent to the DistributedStoreService
         * @ignore
         */
        this.subs = {};
        /** This is the Flux dispatcher. It can be used dispatch actions across stores. These action are not caught inside of the global store service. For more information, you can read the [Flux documentation](https://facebook.github.io/flux/docs/overview.html).
         *
         * Example:
         * ```
         * store.Dispatcher.register(function(action){
         * 	if(action.actionType === "ACTION1") {
         * 		// Do something with the action here.
         * 	}
         * })
         * ```
         * @ignore
         */
        this.Dispatcher = {
            register: (fn) => {
                this.registeredDispatchListeners.push(fn);
            },
            dispatch: (data) => {
                if (this.isGlobal) {
                    this.routerClient.transmit(`storeService.dispatch.${this.name}`, data);
                }
                else {
                    this.handleDispatchedMessages(null, { data });
                }
            },
        };
        /**
         * Handles all changes coming in from the service.
         * @private Do not call this function directly. It will be converted to private member in a future release
         * @ignore
         */
        this.handleChanges = (err, response) => {
            if (err) {
                Logger.system.error("DistributedStoreClient", err);
            }
            else {
                const { store, field, storeData, value } = response.data;
                if (store) {
                    const fieldName = field ? `.${field}` : "";
                    const combined = `${this.name}${fieldName}`;
                    const val = storeData ? storeData : value;
                    this.triggerListeners(combined, val);
                }
            }
        };
        this.routerClient = routerClient;
        this.removeStoreCallback = removeStoreCallback;
        this.isGlobal = !!params.global;
        // params.store is for backward compatibility. See notes in distributedStoreService.ts
        this.name = (_b = (_a = params.name) !== null && _a !== void 0 ? _a : params.store) !== null && _b !== void 0 ? _b : "StoreModel() invariant. store.name not provided.";
        if (params.values) {
            // We need to clone the store values passed in, otherwise
            // every time someone mutates a value outside the store
            // it will also mutate the value in the store. This can cause some tricky
            // bugs and undesirable side-effects (barrage of listeners being fired or not fired at all)
            this.values = JSON.parse(JSON.stringify(params.values));
        }
        storeUtils.initObject(this.values, null, this.mapping);
        // Add listeners for global stores. Not needed for local stores as everything happens internally.
        if (this.isGlobal) {
            this.routerClient.addListener(`storeService.dispatch.${this.name}`, (err, response) => {
                this.handleDispatchedMessages(err, response);
            });
        }
    }
    /**
     * @param {*} err
     * @param {*} message
     * @ignore
     */
    handleDispatchedMessages(err, message) {
        if (message === null || message === void 0 ? void 0 : message.data) {
            this.registeredDispatchListeners.forEach((listener) => {
                listener(message.data);
            });
        }
    }
    /**
     * @deprecated - Use [store.set](#set) instead
     *
     * Set a value in the store. Two events will be triggered with topics of: store and field.
     *
     * ```javascript
     * store.setValue({ field:'field1', value:"new value" });
     * ```
     * @param {String} params.field The name of the field where data will be stored
     * @param {String} params.value Value to be stored
     * @param {function} cb callback
     */
    setValue(params, cb) {
        Logger.system.deprecated("store.setValue is deprecated. Use store.set() instead.");
        if (!params.field) {
            Logger.system.error("DistributedStore.setValue:no field provided", params);
        }
        if (!params.hasOwnProperty("value")) {
            Logger.system.error("DistributedStore.setValue:no value provided", params);
        }
        // this ensures that any value passed in won't be mutated outside the store
        params.value = storeUtils.maybeCloneValue(params.value);
        if (this.isGlobal) {
            const data = {
                store: this.name,
                field: params.field,
                value: params.value,
            };
            this.routerClient.query("storeService.setValue", data, (err) => {
                if (cb)
                    cb(err);
            });
        }
        else {
            const path = toPath(params.field);
            const changes = storeUtils.computeChanges(lodashGet(this.values, path), params.value, path);
            lodashSet(this.values, params.field, params.value);
            storeUtils.mapField(this.values, path, this.mapping);
            // notify listeners for this field and lower-level fields
            if (changes.length > 0) {
                this.triggerMultipleListeners(changes);
            }
            // notify listeners for higher-level fields
            this.publishObjectUpdates(path, this.mapping);
            if (cb)
                cb(null);
        }
    }
    /**
     * Handles changes to the store. Will publish from the field that was changed and back.
     *
     * @private Do not call this function directly. It will be converted to private member in a future release
     * @ignore
     */
    publishObjectUpdates(fieldPath, mappings) {
        let mappedStartField = mappings[storeUtils.generateMappingString(fieldPath)];
        while (mappedStartField) {
            this.triggerListeners(`${this.name}.${mappedStartField}`, lodashGet(this.values, mappedStartField));
            mappedStartField = mappings[mappedStartField];
        }
    }
    /**
     * Send items that are no longer mapped or had their map change. If a value is remapped we'll send out the new value.
     *
     * @private Do not call this function directly. It will be converted to private member in a future release
     * @ignore
     */
    triggerMultipleListeners(removals) {
        for (const removal of removals) {
            this.triggerListeners(`${this.name}.${removal}`, lodashGet(this.values, removal));
        }
    }
    /**
     * This will set multiple values in the store.
     *
     * ```javascript
     * store.setValues([{ field:'field1', value:"new value" }]);
     * ```
     * @param {function} cb callback
     * @param {Array<{ field : string, value : any}>} fields An array where each element is like the object below.
     */
    setValues(fields, cb) {
        if (!fields) {
            return Logger.system.error("DistributedStore.setValues:no params given");
        }
        if (!Array.isArray(fields)) {
            return Logger.system.error("DistributedStore.setValues:params must be an array");
        }
        asyncEach(fields, (field, done) => {
            const path = toPath(field.field);
            this.set(path, field.value, (err, result) => {
                if (typeof err === "string")
                    done(new Error(err));
                else
                    done(err);
            });
        }, (err) => (cb ? cb(err) : null));
    }
    /**
     * @deprecated - Use [store.get](#get) instead.
     *
     * Get a value from the store. If global is not set, we'll check local first then we'll check global. Returns the value of the field. If no callback is given and the value is local, this will run synchronously.
     *
     * Be very careful not to `await` this function as it is not promisified. Use `await getGlobalValue` instead.
     *
     * ```javascript
     * store.getValue({ field: 'field1' }, function(err,value){});
     * store.getValue('field1', function(err,value){});
     * ```
     * @param {object|string} params The field where the value is stored.
     * @param {String} params.field The field where the value is stored.
     * @param {StandardCallback} cb Callback that will receive the value if found, null if not found or an error if the parameters to the call were not correct.
     * @returns any Don't use the return statement. Always use the cb.
     */
    getValue(params, cb) {
        Logger.system.deprecated("store.getValue is deprecated. Use store.get() instead.");
        if (typeof params === "string") {
            params = { field: params };
        }
        if (!params.field) {
            cb === null || cb === void 0 ? void 0 : cb("no field provided");
            return "no field provided";
        }
        if (this.isGlobal) {
            this.getGlobal(params.field.split("."), cb);
        }
        else {
            // it will return null. Several parts of our code rely on this misleading premise
            // (null is actually a value).
            let fieldValue = lodashGet(this.values, params.field);
            // when we are returning objects, we must return a clone, otherwise
            // if we mutate the returned object outside the store it would mutate
            // it in the store as well... the only way to mutate the store should
            // only be via set/remove
            fieldValue = storeUtils.maybeCloneValue(fieldValue);
            cb === null || cb === void 0 ? void 0 : cb(null, fieldValue); // Allow conversion from storeUtils to the requested type
            return fieldValue;
        }
        return null;
    }
    /**
     * Get the entire store.
     *
     * @param { Function} cb Will return the value if found
     * @returns object | void - The store in key/value form if it's a local store - otherwise void
     * @ignore
     */
    getAll(cb) {
        if (this.isGlobal) {
            this.getGlobalMultiple(null, cb);
        }
        else {
            if (cb)
                cb(null, this.values);
            return this.values;
        }
    }
    /**
     * @deprecated - Use [store.getMultiple](#getMultiple) instead.
     *
     * Get multiple values from the store. Returns an object of with the fields as keys.If no callback is given and the value is local, this will run synchronously. Returns an object of with the fields as keys.If no callback is given and the value is local, this will run synchronous
     *
     * ```javascript
     * store.getValues([{ field:'field1' }, { field:'field2' }], function(err,values){});
     * store.getValues(['field1', 'field2'], function(err,values){});
     * ```
     * @param {Array.<object>|Array.<String>} fields An Array of field objects. If there are no fields provided, or value is null, all values in the store are returned.
     * @param {string} fields.field The field where the value is stored.
     * @param {Function} [cb] Will return the value if found.
     * @returns {Object} - returns an object of with the fields as keys. If no callback is given and the value is local, this will run synchronous
     */
    getValues(fields, cb) {
        Logger.system.deprecated("store.getValues is deprecated. Use store.getMultiple() instead.");
        // If fields is null, treat it as though no fields property was passed.
        if (fields === null) {
            fields = cb;
        }
        if (typeof fields === "function") {
            cb = fields;
            if (this.isGlobal) {
                return this.getGlobalValues(null, cb);
            }
            if (!cb) {
                return this.values;
            }
            return cb(null, this.values);
        }
        // If the user sent a single value instead of an array (violating the typing rules for this function)
        if (!Array.isArray(fields)) {
            const result = this.get(toPath(fields), cb);
            return {
                [fields]: result,
            };
        }
        if (this.isGlobal) {
            return this.getGlobalValues(fields, cb);
        }
        const values = {};
        for (let i = 0; i < fields.length; i++) {
            const item = fields[i];
            const field = typeof item === "string" ? item : item.field;
            const fieldValue = lodashGet(this.values, field);
            values[field] = fieldValue;
        }
        if (!cb) {
            return values;
        }
        return cb(null, values);
    }
    /**
     * @deprecated - Use [store.getGlobal](#getglobal) instead.
     *
     * Get a single value from a *global* store.
     *
     * @param {string} field The field to fetch
     * @param {(err, data) => void} [cb] Optional callback to receive value. `err` will be string if an error occurred. `data` will contain the returned value or be undefined.
     * @return {Promise<{err, data}} `err` will be string if an error occurred. `data` will contain the returned value or be undefined.
     */
    async getGlobalValue(field, cb) {
        Logger.system.deprecated("store.getGlobalValue is deprecated. Use store.getGlobal() instead.");
        return this.getGlobal(toPath(field), cb);
    }
    /**
     * Get a single value from a *global* store.
     *
     * @param {PathArray} path The field where the value is stored. If value is null or empty array the whole store values will be returned.
     * @param {(err, data) => void} [cb] Optional callback to receive value. `err` will be string if an error occurred. `data` will contain the returned value or be undefined.
     * @return {Promise<{err, data}} `err` will be string if an error occurred. `data` will contain the returned value or be undefined.
     */
    async getGlobal(path, cb) {
        const { err, response } = await this.routerClient.query("storeService.getValue", {
            store: this.name,
            field: path,
        });
        cb === null || cb === void 0 ? void 0 : cb(err, response.data);
        return { err, data: response.data };
    }
    /**
     * Get values from the global store.
     *
     * @private Do not call this function directly. It will be converted to private member in a future release
     * @ignore
     *
     * @deprecated - use store.getGlobalMultiple
     */
    getGlobalValues(params, cb) {
        Logger.system.deprecated("store.getGlobalValues is deprecated. Use store.getGlobalMultiple() instead.");
        this.routerClient.query("storeService.getValues", {
            store: this.name,
            fields: params,
        }, (err, response) => {
            if (cb)
                cb(err, response.data);
        });
    }
    /**
     * Get multiple values from the *global* store.
     *
     * @param chains
     * @param cb
     */
    async getGlobalMultiple(chains, cb) {
        return await this.routerClient.query("storeService.getValues", {
            store: this.name,
            fields: chains,
        }, (err, response) => {
            if (cb)
                cb(err, response.data);
        });
    }
    /**
     * @deprecated - Use [store.remove](#remove) instead.
     *
     * Remove a value from the store.
     *
     * ```javascript
     * store.removeValue({ field: 'field1' }, function(err,bool){});
     * ```
     * @param {Object | String} params - Either an object (`{ field: string }`) or string
     * @param {String} param.field The name of the field
     * @param {Function} cb returns an error if there is one
     */
    removeValue(params, cb) {
        Logger.system.deprecated("store.removeValue is deprecated. Use store.remove() instead.");
        if (typeof params === "string") {
            params = {
                field: params,
            };
        }
        if (!params.field) {
            Logger.system.error("DistributedStore.removeValue:no field provided", params);
        }
        if (this.isGlobal) {
            const data = {
                store: this.name,
                field: params.field,
            };
            this.routerClient.query("storeService.removeValue", data, (err) => {
                if (cb)
                    cb(err);
            });
        }
        else {
            const path = toPath(params.field);
            const removals = storeUtils.computeRemovals(this.values, path, this.mapping);
            storeUtils.removeValue(this.values, path);
            // notify listeners for this field and lower-level fields
            if (removals.length > 0) {
                this.triggerMultipleListeners(removals);
            }
            // notify listeners for higher-level fields
            this.publishObjectUpdates(toPath(params.field), this.mapping);
            // 2022-09-30 - We never used to actually delete values. We were only
            // setting them to null.
            // removes the current field from the mappings
            delete this.mapping[params.field];
            if (cb)
                cb(null);
        }
    }
    /**
     * Removes multiple values from the store.
     *
     * ```javascript
     * store.removeValues([{ field: 'field1' }], function(err,bool){});
     * ```
     * @param {Object[] | String[]} params - An Array of field objects
     * @param {String} params.field - The name of the field
     * @param {Function} cb -  returns an error if there is one.
     */
    removeValues(params, cb) {
        if (!Array.isArray(params)) {
            return cb("The passed in parameter needs to be an array");
        }
        const strings = params.map((param) => (typeof param === "string" ? param : param.field));
        asyncMap(strings, this.removeValue, (err, data) => cb(err, data));
    }
    /**
     * Destroys the store.
     * @param {Function} cb Function to be invoked after the store is destroyed.
     */
    destroy(cb) {
        const params = {
            store: this.name,
            global: this.isGlobal,
        };
        this.removeStoreCallback(params, (err) => {
            if (err) {
                if (cb)
                    cb(err);
            }
            else {
                if (cb)
                    cb(null, true);
            }
        });
    }
    /**
     * Adds a subscription for the desired dynamic store changes
     * @ignore
     */
    changeSub(change) {
        if (!this.subs[change]) {
            if (this.isGlobal) {
                this.routerClient.subscribe(`storeService${change}`, this.handleChanges);
            }
            this.subs[change] = true;
        }
    }
    /**
     * Add a listener to the store at either the store or field level.
     * If no field is given, the store level is used. You can also listen to nested object (e.g., field1.nestedField).
     *
     * ```javascript
     * const myFunction = function(err,data) { ... }
     *
     * store.addListener({ field:'field1' }, myFunction, cb);
     * store.addListener(['field1', 'property1'], myFunction, cb); // field1.property1
     * ```
     * @param {{ field: string | null } | PathArray} params The piece of data that you want to listen on. If this is empty it listens to all changes of the store.
     * @param {ListenerFunction} fn the function to call when the data changes
     * @param {Function} cb callback to be invoked
     */
    addListener(params, fn, cb) {
        var _a;
        // sometimes we can get empty object. If that's the case, force function to be params
        // which will be handled by the backwards compatibility if branch
        if (lodashIsEmpty(params)) {
            params = fn;
        }
        let field = null;
        // Backward compatibility, addListener for entire store
        if (typeof params === "function") {
            fn = params;
            params = { field: null };
        }
        if (Array.isArray(params)) {
            field = storeUtils.generateMappingString(params);
        }
        else {
            field = params.field ? params.field : null;
        }
        const fieldName = field ? `.${field}` : "";
        const combined = `${this.name}${fieldName}`;
        this.listeners[combined] = [fn].concat((_a = this.listeners[combined]) !== null && _a !== void 0 ? _a : []);
        this.changeSub(combined);
        cb === null || cb === void 0 ? void 0 : cb();
    }
    /**
     * Add an array of listeners as objects, strings or path arrays.
     * If using strings, you must provide a function callback.
     *
     * ```javascript
     * const myFunction = function(err,data){ ... }
     * const fallbackListenerFn = function(err,data){ ... } // listener function used by field2
     * store.addListeners([{
     * 	field: 'field1',
     * 	listener: myFunction
     * },
     * {
     * 	field:'field2'
     * }],
     * fallbackListenerFn, cb);
     *
     * store.addListeners([{ field: 'field1' },{ field: 'field2', listener: myFunction }], myFunction, cb);
     * store.addListeners(['field1','field2'], myFunction, cb);
     * ```
     * @param {listenerParam[] | string[] | PathArrayListenerParam[]} params.field The piece of data that you want listen on. If this is empty it listen to all changes of the store.
     * @param {ListenerFunction} params.listener The function to call when the piece of data is modified. If this is empty, fn is used.
     * @param {function} fn The function to call when the piece of data is modified.
     * @param {function} cb callback to be invoked when the listeners are added.
     */
    addListeners(params, fn, cb) {
        var _a;
        // First overload is a single listener - deprecated
        if (!Array.isArray(params)) {
            Logger.deprecated("distributedStoreClient StoreModel - addListeners() with an object instead of an array is deprecated. Please use addListener() instead.");
            this.addListener(params, fn, cb);
            return;
        }
        const overrideFunction = typeof fn === "function" ? fn : null;
        for (let i = 0; i < params.length; i++) {
            const item = params[i];
            let field = "";
            if (typeof item === "string") {
                field = item;
            }
            else if (typeof item === "object" && Array.isArray(item.field)) {
                field = storeUtils.generateMappingString(item.field);
            }
            else if (typeof item === "object" && typeof item.field === "string") {
                field = item.field;
            }
            const ls = typeof item !== "string" ? item.listener : overrideFunction;
            if (ls) {
                const fieldName = field ? `.${field}` : "";
                const combined = `${this.name}${fieldName}`;
                this.listeners[combined] = [ls].concat((_a = this.listeners[combined]) !== null && _a !== void 0 ? _a : []);
                this.changeSub(combined);
            }
        }
        cb === null || cb === void 0 ? void 0 : cb();
    }
    /**
     * Remove a listener from store. If no field is given, we look for a store listener
     * @param {String} params.field - The data field with the listener that you want to remove.
     * @param {function} fn The handler passed into `addListener` or `addListeners`.
     * @param {function} cb returns true if it was successful in removing the listener.
     *
     * ```javascript
     * var myFunction = function(err,data){}
     * store.removeListener({ field: 'field1' }, MyFunction, function(bool){});
     * ```
     */
    removeListener(params, fn, cb) {
        const field = Array.isArray(params) ? storeUtils.generateMappingString(params) : params.field;
        const fieldName = field ? `.${field}` : "";
        const combined = `${this.name}${fieldName}`;
        if (this.listeners[combined]) {
            for (let i = 0; i < this.listeners[combined].length; i++) {
                if (this.listeners[combined][i] === fn) {
                    this.listeners[combined].splice(i, 1);
                    cb === null || cb === void 0 ? void 0 : cb(null, true);
                    return;
                }
            }
        }
        cb === null || cb === void 0 ? void 0 : cb(null, false);
    }
    /**
     * Remove an array of listeners from the store
     * @param {String} params.field The data field with the listener that you want to remove.
     * @param {String} params.listener The handler passed into `addListener` or `addListeners`.
     * @param {function} fn The handler passed into `addListener` or `addListeners`.
     * @param {function} cb returns true if it was successful in removing the listener.
     *
     * ```javascript
     * var myFunction = function(err,data){}
     * store.removeListeners({ field: 'field1' }, MyFunction, function(bool){});
     * store.removeListeners([{ field: 'field1', listener: MyFunction}], function(bool){});
     * store.removeListeners(['field1'], MyFunction, function(bool){});
     * ```
     */
    removeListeners(params, fn, cb) {
        if (!Array.isArray(params)) {
            if (typeof params === "function") {
                this.removeListener({}, params, cb);
            }
            else if (params.field && fn) {
                this.removeListener(params, fn, cb);
            }
            return cb === null || cb === void 0 ? void 0 : cb(null, true);
        }
        const overrideFunction = typeof fn === "function" ? fn : null;
        params.forEach((item) => {
            let field = "";
            if (Array.isArray(item)) {
                field = storeUtils.generateMappingString(item);
            }
            else if (typeof item === "string") {
                field = item;
            }
            else {
                field = item.field;
            }
            const ls = typeof item !== "string" ? item.listener : overrideFunction;
            const fieldName = field ? `.${field}` : "";
            const combined = `${this.name}${fieldName}`;
            this.listeners[combined] = this.listeners[combined].filter((listener) => listener !== ls);
        });
        cb === null || cb === void 0 ? void 0 : cb(null, true);
    }
    /**
     * Trigger any function that is listening for changes
     * @private Do not call this function directly. It will be converted to private member in a future release
     * @ignore
     */
    triggerListeners(listenerKey, data) {
        var _a;
        const listeners = (_a = this.listeners[listenerKey]) !== null && _a !== void 0 ? _a : [];
        listeners.forEach((listener) => {
            Logger.system.debug("DistributedStore.triggerListeners", listenerKey, data);
            listener(null, {
                field: listenerKey,
                value: data,
            });
        });
    }
    // ************************************************************************
    // New path array async API methods ***************************************
    // ************************************************************************
    /**
     * Set a value in the store.
     *
     * When a value is set in the store, it also generates events according to mappings - lower-level fields
     * that are modified, and higher-level objects containing this one.
     *
     * ```javascript
     * store.set(['field1', 'property1'], "new value"); // field1.property1
     * ```
     * @param {PathArray} path The path array that represents the field where data will be stored
     * @param {String} params.value The value to be stored
     * @param {function} cb callback
     */
    async set(path, value, cb) {
        // this ensures that any value passed in won't be mutated outside the store
        const valueToSet = storeUtils.maybeCloneValue(value);
        if (!path || path.length === 0) {
            Logger.system.error(INVALID_PATH_ERROR_MESSAGE, path);
            cb === null || cb === void 0 ? void 0 : cb(INVALID_PATH_ERROR_MESSAGE);
            return { err: INVALID_PATH_ERROR_MESSAGE };
        }
        if (this.isGlobal) {
            const data = {
                store: this.name,
                field: path,
                value: valueToSet,
            };
            const { err } = await this.routerClient.query("storeService.setValue", data);
            if (err) {
                cb === null || cb === void 0 ? void 0 : cb(err);
                return { err };
            }
            return { data: null, err: null };
        }
        else {
            const changes = storeUtils.computeChanges(lodashGet(this.values, path), valueToSet, path);
            lodashSet(this.values, path, valueToSet);
            storeUtils.mapField(this.values, path, this.mapping);
            // notify listeners for this field and lower-level fields
            if (changes.length > 0) {
                this.triggerMultipleListeners(changes);
            }
            // notify listeners for higher-level fields
            this.publishObjectUpdates(path, this.mapping);
            cb === null || cb === void 0 ? void 0 : cb(null);
            return { data: null, err: null };
        }
    }
    /**
     * Sets multiple values in the store.
     *
     * When a value is set in the store, it also generates events according to mappings - lower-level fields
     * that are modified, and higher-level objects containing this one.
     *
     * ```javascript
     * store.set([
     * 	{field: ['field1', 'property1'], value: "new value 1"},
     * 	{field: ['field2', 'property2'], value: "new value 2"}]); // field1.property1: new value 1 AND field2.property2: new value 2
     * ```
     * @param {PathArray} path The path array that represents the field where data will be stored
     * @param {String} params.value The value to be stored
     * @param {function} cb callback
     */
    async setMultiple(pathsAndValues, cb) {
        if (!Array.isArray(pathsAndValues)) {
            Logger.system.error(INVALID_PATH_VALUES_ERROR_MESSAGE, pathsAndValues);
            cb === null || cb === void 0 ? void 0 : cb(INVALID_PATH_VALUES_ERROR_MESSAGE);
            return { err: INVALID_PATH_VALUES_ERROR_MESSAGE };
        }
        const promises = pathsAndValues.map((pathAndValue) => this.set(pathAndValue.field, pathAndValue.value));
        try {
            const result = await Promise.all(promises);
            cb === null || cb === void 0 ? void 0 : cb(null, result);
            return { data: result, err: null };
        }
        catch (error) {
            cb === null || cb === void 0 ? void 0 : cb(error);
            return { err: error, data: undefined };
        }
    }
    /**
     * Get a value from the store.
     *
     * ```javascript
     * store.get(['field1', 'property1'], function(err,value){});
     * ```
     * @param {PathArray} path The field where the value is stored. If value is null or empty array the whole store values will be returned.
     * @param {StandardCallback} cb Callback that will receive the value if found, undefined if not found or an error if the parameters to the call were not correct.
     * @return {Promise<{err, data}} `err` will be string if an error occurred. `data` will contain the returned value or be undefined.
     */
    async get(path, cb) {
        if (this.isGlobal) {
            return this.getGlobal(path, cb);
        }
        else {
            if (path !== null && !Array.isArray(path)) {
                Logger.system.error(INVALID_PATH_ERROR_MESSAGE, path);
                cb === null || cb === void 0 ? void 0 : cb(INVALID_PATH_ERROR_MESSAGE);
                return { err: INVALID_PATH_ERROR_MESSAGE };
            }
            // if we do not have a path, return the entire store
            if (path === null || path.length === 0) {
                cb === null || cb === void 0 ? void 0 : cb(null, this.values);
                return { data: this.values };
            }
            let fieldValue = lodashGet(this.values, path);
            if (fieldValue !== undefined) {
                // when we are returning objects, we must return a clone, otherwise
                // if we mutate the returned object outside the store it would mutate
                // it in the store as well... the only way to mutate the store should
                // only be via set/remove
                fieldValue = storeUtils.maybeCloneValue(fieldValue);
                cb === null || cb === void 0 ? void 0 : cb(null, fieldValue);
                return { data: fieldValue };
            }
            else {
                Logger.system.info(`StoreModel.get - Field ${path.join(".")} not in store`);
                cb === null || cb === void 0 ? void 0 : cb(null, undefined);
                return { data: undefined, err: null };
            }
        }
    }
    /**
     * Gets multiple values from the store.
     *
     * ```javascript
     * store.get([['field1', 'property1'], ['field1', 'property2']], function(err,value){});
     * ```
     * @param {PathArray} path The field where the value is stored.
     * @param {StandardCallback} cb Callback that will receive the value if found, undefined if not found or an error if the parameters to the call were not correct.
     * @return {Promise<{err, data}} `err` will be string if an error occurred. `data` will contain the returned value or be undefined.
     */
    async getMultiple(paths, cb) {
        if (this.isGlobal) {
            return this.getGlobalMultiple(paths, cb);
        }
        else {
            if (!Array.isArray(paths)) {
                Logger.system.error(INVALID_ARRAY_OF_PATHS_ERROR_MESSAGE, paths);
                cb === null || cb === void 0 ? void 0 : cb(INVALID_ARRAY_OF_PATHS_ERROR_MESSAGE);
                return { err: INVALID_ARRAY_OF_PATHS_ERROR_MESSAGE };
            }
            const data = {};
            paths.forEach((path) => {
                const fieldValue = lodashGet(this.values, path);
                if (fieldValue === undefined) {
                    Logger.system.log(`StoreModel.getMultiple - Field ${path.join(".")} not in store`);
                }
                else {
                    data[path.join(".")] = fieldValue;
                }
            });
            if (lodashIsEmpty(data)) {
                Logger.system.log("StoreModel.getMultiple - Specified values not found in store");
                cb === null || cb === void 0 ? void 0 : cb(null, undefined);
                return { data: undefined, err: null };
            }
            else {
                cb === null || cb === void 0 ? void 0 : cb(null, data);
                return { err: null, data };
            }
        }
    }
    /**
     * Remove a value from the store.
     *
     * When a value is removed from the store, it also generates events according to mappings - lower-level fields
     * that are modified, and higher-level objects containing this one.
     *
     * ```javascript
     * store.remove(['field1', 'property1'], function(err,bool){});
     * ```
     * @param {PathArray} path - An array of strings representing a object property path (field)
     * @param {Function} cb returns an error if there is one
     * @return {Promise<{err, data}} `err` will be string if an error occurred. `data` will contain the returned value or be undefined.
     */
    async remove(path, cb) {
        if (!path || !Array.isArray(path) || path.length === 0) {
            Logger.system.error(INVALID_PATH_ERROR_MESSAGE, path);
            cb === null || cb === void 0 ? void 0 : cb(INVALID_PATH_ERROR_MESSAGE);
            return { err: INVALID_PATH_ERROR_MESSAGE };
        }
        if (this.isGlobal) {
            const data = {
                store: this.name,
                field: path,
            };
            const { err } = await this.routerClient.query("storeService.removeValue", data);
            if (err) {
                cb === null || cb === void 0 ? void 0 : cb(err);
                return { err };
            }
            return { data: null, err: null };
        }
        else {
            const removals = storeUtils.computeRemovals(this.values, path, this.mapping);
            storeUtils.removeValue(this.values, path);
            // notify listeners for this field and lower-level fields
            if (removals.length > 0) {
                this.triggerMultipleListeners(removals);
            }
            // notify listeners for higher-level fields
            this.publishObjectUpdates(path, this.mapping);
            // 2022-09-30 - We never used to actually delete values. We were only
            // setting them to null.
            // removes the current field from the mappings
            delete this.mapping[storeUtils.generateMappingString(path)];
            cb === null || cb === void 0 ? void 0 : cb(null);
            return { data: null, err: null };
        }
    }
    /**
     * Removes multiple values from the store.
     *
     * ```javascript
     * store.removeMultiple([['field1', 'property1'], ['field2', 'property2']], function(err,bool){});
     * ```
     * @param {PathArray} paths - An array of strings representing a object property path (field)
     * @param {Function} cb returns an error if there is one
     * @return {Promise<{err, data}} `err` will be string if an error occurred. `data` will contain the returned value or be undefined.
     */
    async removeMultiple(paths, cb) {
        if (!Array.isArray(paths)) {
            Logger.system.error(INVALID_ARRAY_OF_PATHS_ERROR_MESSAGE, paths);
            cb === null || cb === void 0 ? void 0 : cb(INVALID_ARRAY_OF_PATHS_ERROR_MESSAGE);
            return { err: INVALID_ARRAY_OF_PATHS_ERROR_MESSAGE };
        }
        const promises = paths.map((path) => this.remove(path));
        try {
            const result = await Promise.all(promises);
            cb === null || cb === void 0 ? void 0 : cb(null);
            return { data: result, err: null };
        }
        catch (error) {
            cb === null || cb === void 0 ? void 0 : cb(error);
            return { err: error, data: undefined };
        }
    }
}
export default StoreModel;
//# sourceMappingURL=StoreModel.js.map