import * as Lodash from '../tree-shakers/lodash';

/**
 * Assignment engine.
 *
 * @param {Object}  target
 * @param {Object}  source
 * @param {boolean} shouldOverride
 * @param {boolean} recursive
 *
 * @return {object}
 */
function mergeTwo(target, source, shouldOverride, recursive) {

    Object.keys(source).forEach((key) => {

        let sourceVal = source[key];
        let targetVal = target[key];

        if (! target.hasOwnProperty(key)) {
            target[key] = sourceVal;

        } else if (recursive && Lodash.isObject(targetVal) && Lodash.isObject(sourceVal)) {
            target[key] = mergeTwo(targetVal, sourceVal, shouldOverride, recursive);

        } else if (shouldOverride) {
            target[key] = sourceVal;
        }
    });

    return target;
}

/**
 * Curry adapter for public API.
 *
 * @param {boolean} override
 * @param {boolean} recursive
 * @param {Object}  target
 * @param {Array}   sources
 *
 * @return {object}
 */
function extend(override, recursive, target, ...sources) {

    sources
        .filter((source) => Lodash.isObject(source))
        .forEach((source) => target = mergeTwo(target, source, override, recursive));

    return target;
}


/**
 * Public API: curry functions for object assignment approaches
 */
export const assign = {
    soft : {

        /**
         * Perform a recursive soft-assign.
         *
         * Nested object are merged recursively, and existing keys on the target object
         * will not be overwritten.
         *
         * @param {Object}  target
         * @param {...object}   sources
         * @return {object}
         */
        recursive(target, ...sources) {
            return extend(false, true, target, ...sources);
        },

        /**
         * Perform a flat soft-assign.
         *
         * Merges the objects with preservation of existing keys on the target object
         * and preceding source objects, regardless of value types.
         *
         * @param {Object}  target
         * @param {...object}   sources
         *
         * @return {object}
         */
        flat(target, ...sources) {
            return extend(false, false, target, ...sources);
        },
    },

    hard : {

        /**
         * Perform a recursive hard-assign.
         *
         * Nested object are merged recursively, and existing keys on the target object
         * will be overwritten with latest source values in the same key.
         *
         * @param {Object}  target
         * @param {...object}   sources
         * @return {object}
         */
        recursive(target, ...sources) {
            return extend(true, true, target, ...sources);
        },

        /**
         * Perform a flat hard-assign.
         *
         * Merges the objects without preservation of existing keys on the target object
         * and preceding source objects, regardless of value types.
         *
         * @param {Object}  target
         * @param {...object}   sources
         * @return {object}
         */
        flat(target, ...sources) {
            return extend(true, false, target, ...sources);
        },
    },
};

/**
 * Todo: implement
 *
 * @param {Object} object
 */
export function freezeRecursive(object = {}) {
    return object;
}

/**
 * Flat data storage: objects without prototype link containing only scalar values,
 * that are non-writable and non-configurable.
 */
export const flatDataStorage = Object.freeze({

    /**
     * Create an object without a prototype link, with properties that are non-writable and
     * non-configurable, from a passed object.
     *
     * @param {Object} object
     * @returns object
     */
    create(object)
    {
        if (object === Object(object)) {

            // 1. Create an object without prototype link
            let DMZ = Object.create(null);

            // 2. Assign the own enumerable properties of the passed object to it
            let assigned = Object.assign(DMZ, object);

            // 3. Freeze the object (make all properties non-writable and non-configurable)
            return Object.freeze(assigned);
        }

        // if passed argument is not an object, return the argument directly
        return object;
    },

    /**
     * Create a new object and assign the contents of a flatDataStorage, to be able to use
     * Object.prototype utilities for querying its contents. This makes sure that the
     * original object is not mutated.
     *
     * @param {Object}      flatStorageObject   - The flat storage object to query
     * @param {function}    callback            - the callback function that receives the
     *                                            Object.prototype-attached copy of flatStorageObject
     *
     * @param {Array}       additionalCallbackParams
     */
    query(flatStorageObject, callback, ...additionalCallbackParams)
    {
        callback(Object.assign({}, flatStorageObject), ...additionalCallbackParams);
    },
});
