import mergeWith from 'lodash/mergeWith';
import merge from 'lodash/merge';
import isArray from 'lodash/isArray';
import extend from 'lodash/extend';
import isObject from 'lodash/isObject';
import { AdaptableLogger } from '../../agGrid/AdaptableLogger';
import AdaptableHelper from '../../Utilities/Helpers/AdaptableHelper';
function customizer(objValue, srcValue) {
    if (isArray(objValue)) {
        if (!Array.isArray(srcValue)) {
            // TODO AFL: clarify if this is still relevant, after the state & options refactoring
            /**
             * the new value might be a function: eg: UserInterface.ContextMenuItems defaults to an array
             * in the redux state, while the user might provide a function
             * so in this case, make the user provided value win
             */
            return srcValue;
        }
        const length = srcValue ? srcValue.length : null;
        const result = mergeWith(objValue, srcValue, customizer);
        if (length != null) {
            // when merging arrays, lodash result has the length of the
            // longest array, but we don't want that to happen
            // so we restrict to the current length
            result.length = length;
        }
        return result;
    }
}
export function AddStateSource(stateObject = {}, source) {
    const traverseStateObject = (object) => {
        Object.values(object).forEach((value) => {
            // add a Source only to AdaptableObjects which do NOT already have a source defined
            if (AdaptableHelper.isAdaptableObject(value) && value.Source == null) {
                value.Source = source;
            }
            if (value !== null && typeof value === 'object') {
                traverseStateObject(value);
            }
        });
    };
    traverseStateObject(stateObject);
    return stateObject;
}
export function ProcessKeepUserDefinedRevision(configState, currentUserState) {
    const traverseStateObject = (configObject, stateObject) => {
        Object.entries(configObject).forEach(([key, configValue]) => {
            const stateValue = stateObject[key];
            if (Array.isArray(configValue) && Array.isArray(stateValue)) {
                const userDefinedItems = stateValue.filter((item) => {
                    return AdaptableHelper.isAdaptableObject(item) && item.Source !== 'InitialState';
                });
                configObject[key] = configValue.concat(userDefinedItems);
                // we probably don't need to call traverseStateObject on the array as well,
                // so we do the traversing on the else branch only
            }
            else {
                if (configValue !== null &&
                    typeof configValue === 'object' &&
                    typeof stateValue === 'object') {
                    traverseStateObject(configValue, stateValue);
                }
            }
        });
    };
    traverseStateObject(configState, currentUserState);
    return configState;
}
function deepClone(obj) {
    return JSON.parse(JSON.stringify(obj));
}
export function MergeStateFunction(oldState, newState) {
    // return MergeState(oldState, newState);
    if (newState === '') {
        newState = {};
    }
    // add source 'InitialState' only to initial state objects
    const initialState = AddStateSource(oldState, 'InitialState');
    // source 'User' will be added to all other objects, after the merge (see bottom of this function)
    let state = newState;
    if (!state || typeof state !== 'object') {
        // in case loadState returns something different than an empty object
        AdaptableLogger.consoleWarnBase('State is something different than expected; expected an object, but received: ', state);
        state = {};
    }
    // any Module in config that doesn't exist in state will be added
    for (const initialStateModuleName in initialState) {
        state[initialStateModuleName] = state[initialStateModuleName] ?? initialState[initialStateModuleName];
    }
    // any Module in state that has an older revision than the config will be replaced
    for (const stateModuleName in state) {
        if (initialState[stateModuleName] != undefined) {
            // explicitly use double equals, as we want to avoid null as well
            const stateRevision = state[stateModuleName].Revision ?? 0;
            const initialStateRevision = initialState[stateModuleName].Revision ?? 0;
            const stateRevisionKey = typeof stateRevision === 'object' ? stateRevision.Key : stateRevision;
            const initialStateRevisionBehaviour = typeof initialStateRevision === 'object' ? initialStateRevision.Behavior : 'Override';
            const initialStateRevisionKey = typeof initialStateRevision === 'object' ? initialStateRevision.Key : initialStateRevision;
            if (initialStateRevisionKey > stateRevisionKey) {
                if (initialStateRevisionBehaviour === 'Override') {
                    state[stateModuleName] = initialState[stateModuleName];
                }
                else {
                    state[stateModuleName] = ProcessKeepUserDefinedRevision(deepClone(initialState[stateModuleName]), state[stateModuleName]);
                }
            }
        }
    }
    // add 'User' source to all state objects which do NOT have 'InitialState'
    const finalState = AddStateSource(state, 'User');
    return finalState;
}
// main merge function
export function MergeState(oldState, newState) {
    const result = extend({}, oldState);
    for (const key in newState) {
        if (!newState.hasOwnProperty(key)) {
            continue;
        }
        const value = newState[key];
        // Assign if we don't need to merge at all
        if (!result.hasOwnProperty(key)) {
            result[key] = isObject(value) && !Array.isArray(value) ? merge({}, value) : value;
            continue;
        }
        const oldValue = result[key];
        if (isObject(value) && !Array.isArray(value)) {
            // use both lodash functions so that we can merge from State onto Initial Adaptable State where it exists but from the former where it doesnt.
            result[key] = mergeWith({}, oldValue, value, customizer);
        }
        else {
            result[key] = value;
        }
    }
    return result;
}
let initialState;
export const mergeReducer = (rootReducer, LOAD_STATE_TYPE) => {
    let finalReducer = rootReducer;
    finalReducer = (state, action) => {
        if (action.type === LOAD_STATE_TYPE) {
            initialState = initialState ?? state;
            state = MergeState(initialState, action.State);
            // put this new state on the action, since the root reducer further copies
            // keys from action.State to the new state
            action.State = state;
        }
        return rootReducer(state, action);
    };
    return finalReducer;
};
