/**
 * Developer: Stepan Burguchev
 * Date: 8/21/2014
 * Copyright: 2009-2016 Comindware®
 *       All Rights Reserved
 * Published under the MIT license
 */

"use strict";

import 'lib';
import LocalizationService from '../services/LocalizationService';

let timeoutCache = {};

let queueCache = {};

let getPluralFormIndex = null;

export default /** @lends module:core.utils.helpers */ {
    /**
     * Deprecated. Use <code>_.debounce()</code> instead. Defers invoking the function until after `delay` milliseconds
     * have elapsed since the last time it was invoked.
     * @param {String} someUniqueId Function identifier.
     * @param {Function} callback The function tobe called after delay.
     * @param {Number} delay Callback delay in milliseconds.
     * @deprecated
     * */
    setUniqueTimeout: function (someUniqueId, callback, delay)
    {
        var timeoutId = timeoutCache[someUniqueId];
        if (timeoutId) {
            clearTimeout(timeoutId);
        }
        var handler = setTimeout(function ()
        {
            callback();
            delete timeoutCache[someUniqueId];
        }, delay);
        timeoutCache[someUniqueId] = handler;
        return handler;
    },

    /**
     * Deprecated. Use <code>_.defer()</code> instead. Defers invoking the function until the current call stack has cleared.
     * @param {Function} callback Callback to be called when the current call stack has cleared.
     * @deprecated
     * */
    nextTick: function (callback)
    {
        return setTimeout(callback, 10);
    },

    /**
     * Creates and returns a new function that maps the passed comparator onto the specified attribute of Backbone.Model.
     * Look at the example for details.
     * @example
     * var referenceComparator = core.utils.helpers.comparatorFor(core.utils.comparators.stringComparator2Asc, 'text');
     * var a = new Backbone.Model({ id: 2, text: '1' });
     * var b = new Backbone.Model({ id: 1, text: '2' });
     * // returns -1
     * var result = referenceComparator(a, b);
     * @param {Function} comparatorFn Wrapped comparator function. 1 or 2 arguments.
     * @param {String} propertyName Attribute of a Backbone.Model to which the function is mapped.
     * @return {Function} Result function.
     * */
    comparatorFor: function (comparatorFn, propertyName) {
        if (comparatorFn.length === 1) {
            return function (a) {
                return comparatorFn(a.get(propertyName));
            };
        } else if (comparatorFn.length === 2) {
            return function (a, b) {
                return comparatorFn(a.get(propertyName), b.get(propertyName));
            };
        } else {
            throw new Error('Invalid arguments count in comparator function.');
        }
    },

    /**
     * Accepts string and duplicates it into every field of LocalizedText object.
     * The LocalizedText  looks like this: <code>{ en: 'foo', de: 'foo', ru: 'foo' }</code>.
     * @param {String} defaultText A text that is set into each field of the resulting LocalizedText object.
     * @return {Object} LocalizedText object like <code>{ en, de, ru }</code>.
     * */
    createLocalizedText: function (defaultText) {
        return {
            en: defaultText,
            de: defaultText,
            ru: defaultText
        };
    },

    /**
     * Javascript version of the Microsoft .NET framework method <code>string.Format</code>.
     * @example
     * // returns 'Hello, Javascript!'
     * core.utils.helpers.format('Hello, {0}!', 'Javascript');
     * @param {String} text Formatted text that contains placeholders like <code>{i}</code>.
     * Where <code>i</code> - index of the inserted argument (starts from zero).
     * @param {...*} arguments Arguments that will replace the placeholders in text.
     * @return {String} Resulting string.
     * */
    format: function(text) {
        if (!_.isString(text)) {
            return '';
        }
        for (var i = 1; i < arguments.length; i++) {
            var regexp = new RegExp('\\{'+(i-1)+'\\}', 'gi');
            text = text.replace(regexp, arguments[i]);
        }
        return text;
    },

    /**
     * Takes a number and array of strings and then returns a valid plural form.
     * Works with complex cases and valid for all supported languages (by default for en, de and ru).
     * The core algorithm is located in localization text `CORE.SERVICES.LOCALIZATION.PLURALFORM`.
     * @function
     * @example
     * // returns 'car'
     * core.utils.helpers.getPluralForm(1, 'car,cars');
     * // returns 'cars'
     * core.utils.helpers.getPluralForm(10, 'car,cars');
     * @param {Number} n A number which requires a correct work form.
     * @param {String} texts Comma separated string of word forms.
     * (2 word forms for en and de, 3 word forms for ru).
     * @return {String} Resulting string.
     * */
    getPluralForm: function (n, texts) {
        if (!getPluralFormIndex) {
            var formula = LocalizationService.get('CORE.SERVICES.LOCALIZATION.PLURALFORM');
            getPluralFormIndex = new Function('n', 'var r = ' + formula + ';return typeof r !== \'boolean\' ? r : r === true ? 1 : 0;'); // jshint ignore:line
        }
        return texts.split(',')[getPluralFormIndex(n)];
    },

    /**
     * Creates a queue of asynchronous operations.
     * New operation function is executed only after the previous function with the same id has executed.
     * @example
     * var save = form.save.bind(form);
     * // Three sequential calls
     * var promise1 = core.utils.helpers.enqueueOperation(save, 42);
     * var promise2 = core.utils.helpers.enqueueOperation(save, 42);
     * var promise3 = core.utils.helpers.enqueueOperation(save, 42);
     * promise3.then(function () {
     *     // Will be called only when all the 'save' operations has been fired and returned success.
     * });
     * @param {Function} operation A function that triggers asynchronous operation and returns a Promise object.
     * @param {String} queueId String identifier of operations queue.
     * */
    enqueueOperation: function (operation, queueId) {
        if (queueCache[queueId] && queueCache[queueId].isPending()) {
            queueCache[queueId] = queueCache[queueId].then(function() {
                return _.isFunction(operation) ? operation() : operation;
            });
        } else {
            queueCache[queueId] = Promise.resolve(_.isFunction(operation) ? operation() : operation);
        }
        return queueCache[queueId];
    },

    /**
     * Sequentially applies passed Behavior objects on to the given instance.
     * The method has nothing to do with Marionette.Behavior.
     * @example
     * core.utils.helpers.applyBehavior(this, core.models.behaviors.SelectableBehavior);
     * @param {Object} target Target instance that is getting behaviors applied.
     * @param {...Function} arguments 1 or more Behavior objects (constructor functions).
     * */
    applyBehavior: function (target) {
        var behaviors = _.rest(arguments, 1);
        _.each(behaviors, function (Behavior) {
            _.extend(target, new Behavior(target));
        });
    },

    /**
     * Allows to perform validation of input options. The method is usually used in constructor or initializer methods.
     * Allows to check both direct and nested properties of the options object.
     * Throws <code>MissingOptionError</code> if the attribute is undefined.
     * @example
     * // Checks that property options.model exists.
     * core.utils.helpers.ensureOption(options, 'model');
     * // Checks that property options.property1.subProperty exists.
     * core.utils.helpers.ensureOption(options, 'property1.subProperty');
     * @param {Object} options Options object to check.
     * @param {String} optionName Property name or dot-separated property path.
     * */
    ensureOption: function (options, optionName) {
        if (!options) {
            this.throwError('The options object is required.', 'MissingOptionError');
        }
        if (optionName.indexOf('.') !== -1) {
            var selector = optionName.split('.');
            for (var i = 0, len = selector.length; i < len; i++) {
                optionName = selector[i];
                if (options[optionName] === undefined) {
                    optionName = _.take(selector, i + 1).join('.');
                    this.throwError('The option `' + optionName + '` is required.', 'MissingOptionError');
                }
                options = options[optionName];
            }
        } else {
            if (options[optionName] === undefined) {
                this.throwError('The option `' + optionName + '` is required.', 'MissingOptionError');
            }
        }
    },

    /**
     * Allows to perform validation of property in an object. Allows to check both direct and nested properties of the object.
     * Throws <code>MissingOptionError</code> if the attribute is undefined.
     * @example
     * // Checks that property this.view.moduleRegion exists.
     * core.utils.helpers.ensureOption(this.view, 'moduleRegion');
     * @param {Object} object An object to check.
     * @param {String} propertyName Property name or dot-separated property path.
     * */
    ensureProperty: function (object, propertyName) {
        if (!object) {
            this.throwError('The object is null.', 'NullObjectError');
        }
        if (propertyName.indexOf('.') !== -1) {
            var selector = propertyName.split('.');
            for (var i = 0, len = selector.length; i < len; i++) {
                propertyName = selector[i];
                if (object[propertyName] === undefined) {
                    propertyName = _.take(selector, i + 1).join('.');
                    this.throwError('The property `' + propertyName + '` is required.', 'MissingPropertyError');
                }
                object = object[propertyName];
            }
        } else {
            if (object[propertyName] === undefined) {
                this.throwError('The property `' + propertyName + '` is required.', 'MissingPropertyError');
            }
        }
    },

    /**
     * Allows to retrieve a property (or subproperty) of an object. Does not throw any error if one of the properties along the way are missing.
     * Doesn't throw if the object itself is undefined.
     * @example
     * var foo = { a: {} };
     * // returns undefined (doesn't throw an error)
     * core.utils.helpers.getPropertyOrDefault(foo, 'a.b.c.d');
     * @param {String} propertyPath propertyName Property name or dot-separated property path.
     * @param {Object} obj An object to get the property from.
     * */
    getPropertyOrDefault: function (propertyPath, obj) {
        return [obj].concat(propertyPath.split('.')).reduce(function(prev, curr) {
            return prev === undefined ? undefined : prev[curr];
        });
    },

    /**
     * Pre-validation helper that allows to check that function argument is not falsy.
     * Falsy value means that the value is one of the following: <code>undefined, null, 0, '', false</code>.
     * Throws <code>ArgumentFalsyError</code> if validation is failed.
     * @example
     * core.utils.helpers.assertArgumentNotFalsy(argument1, 'argument1');
     * @param {*} argumentValue Value to check.
     * @param {String} argumentName Name of the checked argument. Needs to specify in the exception text.
     * */
    assertArgumentNotFalsy: function (argumentValue, argumentName) {
        if (!argumentValue) {
            this.throwError('Argument `' + argumentName + '` is falsy.', 'ArgumentFalsyError');
        }
    },

    /**
     * Simplified way to throw an error. Throws an Error object with the specified name and message.
     * @example
     * core.utils.helpers.throwError('Request is invalid.');
     * @param {String} message Error message.
     * @param {String} [name='Error'] Error name (`name` attribute of Error object).
     * */
    throwError: function (message, name) {
        var error = new Error(message);
        error.name = name || 'Error';
        throw error;
    },

    /**
     * Throws InvalidOperationError. The exception should be thrown when a class is in invalid state to call the checked method.
     * @example
     * // Inside of implementation of some Marionette.View.
     * addKeyboardListener: function (key, callback) {
     *     if (!this.keyListener) {
     *         utils.helpers.throwInvalidOperationError('You must apply keyboard listener after \'render\' event has happened.');
     *     }
     *     var keys = key.split(',');
     *     _.each(keys, function (k) {
     *         this.keyListener.simple_combo(k, callback);
     *     }, this);
     * },
     * // ...
     * @param {String} [message='Invalid operation'] Error message.
     * */
    throwInvalidOperationError: function (message) {
        this.throwError(message || 'Invalid operation', 'InvalidOperationError');
    },

    /**
     * Throws FormatError. The exception should be thrown when the format of an argument is invalid, or when a is not well formed.
     * @example
     * function (url, parameterNames, parameters, callback) {
     *     // Some code here ...
     *     if (parameters.Length !== parameterNames.length) {
     *         utils.helpers.throwFormatError('The arrays `parameters` and `parameterNames` should have identical length.');
     *     }
     *     // Some code here ...
     * @param {String} [message='Invalid format'] Error message.
     * */
    throwFormatError: function (message) {
        this.throwError(message || 'Invalid format', 'FormatError');
    },

    /**
     * Throws ArgumentError. The exception should be thrown when one of the arguments provided to a method is not valid.
     * Should be thrown only when one particular argument is invalid. If a combination of arguments is invalid use <code>FormatError</code>.
     * @example
     * function (url, parameterNames, parameters, callback) {
     *     // Some code here ...
     *     if (parameterNames.Length !== 2) {
     *         utils.helpers.throwArgumentError('The array `parameterNames` should contain exactly 2 elements.');
     *     }
     *     // Some code here ...
     * @param {String} [message='Invalid argument'] Error message.
     * */
    throwArgumentError: function (message) {
        this.throwError(message || 'Invalid argument', 'ArgumentError');
    },

    /**
     * Throws NotSupportedError. The exception should be thrown when an invoked method is not supported.
     * For example: some class doesn't support all the methods of the interface it implements.
     * @example
     * // Inside of implementation of some Stream class
     * seek() {
     *     // Some code here ...
     *     utils.helpers.throwNotSupportedError('The network stream doesn't support `seek`.');
     *     // Some code here ...
     * }
     * @param {String} [message='The operation is not supported'] Error message.
     * */
    throwNotSupportedError: function (message) {
        this.throwError(message || 'The operation is not supported', 'NotSupportedError');
    },

    /**
     * Throws NotImplementedError. The exception should be thrown when a requested method or operation is not implemented.
     * For example: a base class could have abstract methods that throws such error.
     * @example
     * // Inside of implementation of some base controller class.
     * navigate() {
     *     utils.this.throwNotImplementedError();
     * }
     * @param {String} [message='The operation is not implemented'] Error message.
     * */
    throwNotImplementedError: function (message) {
        this.throwError(message || 'The operation is not implemented', 'NotImplementedError');
    },

    /**
     * Throws NotFoundError. The exception should be thrown when a requested object could not be found.
     * For example: we looked up in the database and could find a person with requested id.
     * @param {String} [message='Object not found'] Error message.
     * */
    throwNotFoundError: function (message) {
        this.throwError(message || 'Object not found', 'NotFoundError');
    }
};
