'use strict';

/**
 * Checks shallow equality
 * Re-export of shallow from shallow-equals
 */
export { default as isShallowEqual } from 'shallow-equals';

/**
 * Takes an object and returns a shallow copy without any keys
 * that fail the passed in filter function.
 * Does not modify the passed in object.
 *
 * @param  {object} obj regular javascript object
 * @return {object}     regular javascript object without null values or empty strings
 */
export function sanitize(obj, filterFn) {
    if (!filterFn) {
        // By matching null with a double equal, we can match undefined and null
        // http://stackoverflow.com/a/15992131
        filterFn = (val) => val == null || val === '';
    }

    return omitFromObject(obj, filterFn);
}

/**
 * Removes all falsy values (undefined, null, false, ...) from a list/array
 * @param  {array} l the array to sanitize
 * @return {array}   the sanitized array
 */
export function sanitizeList(l) {
    let sanitizedList = [];

    for(let i = 0; i < l.length; i++) {
        if(l[i]) {
            sanitizedList.push(l[i]);
        }
    }

    return sanitizedList;
}

/**
 * Sums up a list of numbers. Like a Epsilon-math-kinda-sum...
 */
export function sumNumList(l) {
    let sum = 0;
    l.forEach((num) => sum += parseFloat(num) || 0);
    return sum;
}

/*
    Taken from http://stackoverflow.com/a/4795914/1263876
    Behaves like C's format string function
*/
export function formatText() {
    let args = arguments,
    string = args[0],
    i = 1;
    return string.replace(/%((%)|s|d)/g, (m) => {
        // m is the matched format, e.g. %s, %d
        let val = null;
        if (m[2]) {
            val = m[2];
        } else {
            val = args[i];
            // A switch statement so that the formatter can be extended. Default is %s
            switch (m) {
                case '%d':
                    val = parseFloat(val);
                    if (isNaN(val)) {
                        val = 0;
                    }
                    break;
            }
            i++;
        }
        return val;
    });
}

/**
 * Checks a list of objects for key duplicates and returns a boolean
 */
function _doesObjectListHaveDuplicates(l) {
    let mergedList = [];

    l = l.map((obj) => {
        if(!obj) {
            throw new Error('The object you are trying to merge is null instead of an empty object');
        }

        return Object.keys(obj);
    });

    // Taken from: http://stackoverflow.com/a/10865042
    // How to flatten an array of arrays in javascript.
    // If two objects contain the same key, then these two keys
    // will actually be represented in the merged array
    mergedList = mergedList.concat.apply(mergedList, l);

    // Taken from: http://stackoverflow.com/a/7376645/1263876
    // By casting the array to a set, and then checking if the size of the array
    // shrunk in the process of casting, we can check if there were any duplicates
    return new Set(mergedList).size !== mergedList.length;
}

/**
 * Takes a list of object and merges their keys to one object.
 * Uses mergeOptions for two objects.
 * @param  {[type]} l [description]
 * @return {[type]}   [description]
 */
export function mergeOptions(...l) {
    // If the objects submitted in the list have duplicates,in their key names,
    // abort the merge and tell the function's user to check his objects.
    if (_doesObjectListHaveDuplicates(l)) {
        throw new Error('The objects you submitted for merging have duplicates. Merge aborted.');
    }

    return Object.assign({}, ...l);
}

/**
 * In place update of a dictionary
 */
export function update(a, ...l) {
    for(let i = 0; i < l.length; i++) {
        for (let attrname in l[i]) {
            a[attrname] = l[i][attrname];
        }
    }

    return a;
}

/**
 * Escape HTML in a string so it can be injected safely using
 * React's `dangerouslySetInnerHTML`
 *
 * @param s the string to be sanitized
 *
 * Taken from: http://stackoverflow.com/a/17546215/597097
 */
export function escapeHTML(s) {
    return document.createElement('div').appendChild(document.createTextNode(s)).parentNode.innerHTML;
}

/**
 * Returns a copy of the given object's own and inherited enumerable
 * properties, omitting any keys that pass the given filter function.
 */
function applyFilterOnObject(obj, filterFn) {
    const filteredObj = {};

    for (let key in obj) {
        const val = obj[key];
        if (filterFn == null || !filterFn(val, key)) {
            filteredObj[key] = val;
        }
    }

    return filteredObj;
}

/**
 * Abstraction for selectFromObject and omitFromObject
 * for DRYness
 * @param {boolean} isInclusion True if the filter should be for including the filtered items
 *                              (ie. selecting only them vs omitting only them)
 */
function filterFromObject(obj, filter, { isInclusion = true } = {}) {
    if (filter && filter.constructor === Array) {
        return applyFilterOnObject(obj, isInclusion ? ((_, key) => filter.indexOf(key) < 0)
                                                    : ((_, key) => filter.indexOf(key) >= 0));
    } else if (filter && typeof filter === 'function') {
        // Flip the filter fn's return if it's for inclusion
        return applyFilterOnObject(obj, isInclusion ? (...args) => !filter(...args)
                                                    : filter);
    } else {
        throw new Error('The given filter is not an array or function. Exclude aborted');
    }
}

/**
 * Similar to lodash's _.pick(), this returns a copy of the given object's
 * own and inherited enumerable properties, selecting only the keys in
 * the given array or whose value pass the given filter function.
 * @param  {object}         obj    Source object
 * @param  {array|function} filter Array of key names to select or function to invoke per iteration
 * @return {object}                The new object
*/
export function selectFromObject(obj, filter) {
    return filterFromObject(obj, filter);
}

/**
 * Similar to lodash's _.omit(), this returns a copy of the given object's
 * own and inherited enumerable properties, omitting any keys that are
 * in the given array or whose value pass the given filter function.
 * @param  {object}         obj    Source object
 * @param  {array|function} filter Array of key names to omit or function to invoke per iteration
 * @return {object}                The new object
*/
export function omitFromObject(obj, filter) {
    return filterFromObject(obj, filter, { isInclusion: false });
}

/**
 * Recursively tests an object against a "match" object to see if the
 * object is similar to the "match" object. In other words, this will
 * deeply traverse the "match" object's properties and check them
 * against the object by using the testFn.
 *
 * The object is considered a match if all primitive properties in the
 * "match" object are found and accepted in the object by the testFn.
 *
 * @param  {object}     obj    Object to test
 * @param  {object}     match  "Match" object to test against
 * @param  {(function)} testFn Function to use on each property test.
 *                             Return true to accept the match.
 *                             By default, applies strict equality using ===
 * @return {boolean}           True if obj matches the "match" object
 */
export function deepMatchObject(obj, match, testFn = (objProp, matchProp) => objProp === matchProp) {
    if (typeof match !== 'object') {
        throw new Error('Your specified match argument was not an object');
    }
    if (typeof testFn !== 'function') {
        throw new Error('Your specified test function was not a function');
    }

    return Object
            .keys(match)
            .reduce((result, matchKey) => {
                if (!result) { return false; }

                const objProp = obj && obj[matchKey];
                const matchProp = match[matchKey];

                if (typeof matchProp === 'object') {
                    return (typeof objProp === 'object') ? deepMatchObject(objProp, matchProp, testFn)
                                                         : false;
                } else {
                    return testFn(objProp, matchProp);
                }
            }, true);
}

/**
 * Takes a string and breaks it at the supplied index and replaces it
 * with a (potentially) short string that also has been provided
 * @param  {string} text        The string to truncate
 * @param  {number} charIndex   The char number at which the text should be truncated
 * @param  {String} replacement All text after charIndex will be replaced with this string
 * @return {string}             The truncated text
 */
export function truncateTextAtCharIndex(text, charIndex, replacement = '...') {
    let truncatedText = '';

    truncatedText = text.slice(0, charIndex);
    truncatedText += text.length > charIndex ? replacement : '';

    return truncatedText;
}

/**
 * @param index, int, the starting index of the substring to be replaced
 * @param character, substring to be replaced
 * @returns {string}
 */
export function replaceSubstringAtIndex(baseString, substrToReplace, stringToBePut) {
    let index = baseString.indexOf(substrToReplace);
    return baseString.substr(0, index) + stringToBePut + baseString.substr(index + substrToReplace.length);
}

/**
 * Extracts the user's subdomain from the browser's window.
 * If no subdomain is found (for example on a naked domain), the default "www" is just assumed.
 * @return {string} subdomain as a string
 */
export function getSubdomain() {
    let { host } = window.location;
    let tokens = host.split('.');
    return tokens.length > 2 ? tokens[0] : 'www';
}

/**
 * Takes two lists and returns their intersection as a list
 * @param  {Array} a
 * @param  {Array} b
 * @return {[Array]} Intersected list of a and b
 */
export function intersectLists(a, b) {
    return a.filter((val) => b.indexOf(val) > -1);
}