(function (window) {
    "use strict";
    /**
     options: {
        ready: {function} - callback when babel is ready.
        rootUrl: {string} - Url used to attain localization via GET that returns localization.json,
        dictionary: {object} - Alternatively to rootUrl, pass in the localization.json file.
        CustomLogger: {function} - Called instead of default logging function.
    }
     */
    var BabelFish = window.BabelFish = function (options) {
        var self = this;

        function initialize() {
            if (options.rootUrl) {
                var p = $.get(options.rootUrl);
                p.success(function (data) {
                    try {
                        var json = typeof(data) === "object" ? data : JSON.parse(data);
                        processDictionary(json);
                    }
                    catch (e) {
                        logException(e);
                    }
                });

                p.error(function (error) {
                    logException(error);
                });

                p.complete(options.ready);
            }
            else if (options.dictionary) {
                processDictionary(options.dictionary);
                options.ready && setTimeout(options.ready,0); //Call a small timeout to push the ready to the end of the stack
            }
            else {
                options.ready && setTimeout(options.ready,0);
            }

            if (typeof Handlebars != "undefined") {
                try {
                    Handlebars.registerHelper('babel', self.handlebarHelper);
                } catch (e) {
                    logException(e);
                }
            }
        }

        /**
         * Logs a message to the console or Custom Logger.
         * @param  {string} message String to log.
         */
        function log(message) {
            if (options.CustomLogger && typeof (options.CustomLogger) == "function") {
                options.CustomLogger.apply(this, arguments);
            } else {
                if (console && console.log) {
                    console.log(message);
                }
            }
        }

        /**
         * Logs an exception to the `console.error` function.
         * @param  {object} exception Exception to log.
         */
        function logException(exception) {
            if (console && console.error) {
                console.error(exception);
            } else if (console && console.log) {
                console.log(exception);
            }
        }

        /**
         * Return a localization string that has been registered with the application.
         * Split the english and translation into sepearate arrays for faster binary search
         *
         * @method babel
         * @param {String} The string to have ###{{value}}### removed from the display.
         */
        function processDictionary(dictionary) {
            self._dictionary = dictionary.sort(function (a, b) {
                if (a.text > b.text) {
                    return 1;
                }
                if (a.text < b.text) {
                    return -1;
                }
                // a must be equal to b
                return 0;
            });
            //self._dictionary = _.sortBy(dictionary, 'text');
            self._dictionaryEnglish = [];
            self._dictionaryForeign = [];

            for (var i = 0; i < self._dictionary.length; i++) {
                self._dictionaryEnglish.push(self._dictionary[i].text);
                self._dictionaryForeign.push(self._dictionary[i].translation);
            }
        }

        /**
         * Return a localization string that has been registered with the application.
         *
         * @method babel
         * @param {String} The string to have ###{{value}}### removed from the display.
         */
        function stripHashForDisplay(string) {
            return string.replace(/###([0-9a-zA-Z])*###(?!.*###[0-9a-zA-Z]*###)/g, '');
        }

        /**
         * Return a localization string that has been registered with the application.
         *
         * @method babel
         * @param {String} The string to have ###{{value}}### removed from the display.
         */
        function getHasValueForKey(string) {
            var regEx = /###([0-9a-zA-Z])*###(?!.*###[0-9a-zA-Z]*###)/g;
            var match = regEx.exec(string);

            return match.length > 1 ? match[1] : "";
        }

        /**
         * Return a localization string that has been registered with the application.
         *
         * @method babel
         * @param {String} Optional: key The localization key. These are defined on the server.
         * @param {String} the localization string. If no key is present, the string will be condensed to a key
         * @return {String} The localized string.
         */
        this.translate = function () {
            var params = Array.prototype.slice.call(arguments);
            var key, value, values;
            switch (params.length) {
                case 1:
                    key = params[0].replace(/[^a-zA-Z0-9]/g, "");
                    if (key.length > 75) {
                        key = key.substring(0, 74);
                    }
                    value = params[0];
                    break;
                case 2:
                    key = params[0];
                    value = params[1];
                    break;
                default:
                    key = params[0];
                    value = params[1];
                    values = params.slice(1);
                    break;
            }

            var localizedString;
            if (self._dictionary) {
                var index = sortedIndexOf(self._dictionaryEnglish, value);
                localizedString = self._dictionaryForeign[index];
                localizedString = isString(localizedString) ? localizedString : value;
            }
            if (localizedString && self._dictionary['ShowLocalization'] == true) {
                localizedString = "|" + localizedString + '|';
            }
            if (!localizedString) {
                localizedString = value;
            }
            if (self._dictionary && self._dictionary['ShowLocalizationKeys'] == true) {
                localizedString = key;
            }
            if (values) {
                values[0] = localizedString;
                localizedString = self.dotNetFormat.apply(_, values);
            }
            localizedString = stripHashForDisplay(localizedString);


            if (typeof localizedString != 'undefined' && localizedString != null) {
                return localizedString; //index ? localizedString[index] : localizedString;
            }
            else {
                return value;
            }
        },

            /**
             * Process .NET format strings.
             *
             * @method dotNetFormat
             * @param {String} formatString The format string to process (i.e. `'Hello, {0}!)'`);
             * @param [params]* The paramters for the format string.
             * @return The formatted string.
             */
            this.dotNetFormat = function () {
                var args = Array.prototype.slice.call(arguments);

                if (!args.length) {
                    return '';
                }

                var format = args[0];
                args.splice(0, 1);

                for (var i = 0; i < args.length; i++) {
                    var formatToken = new RegExp('\\{' + i + '\\}', 'gi');
                    format = format.replace(formatToken, args[i]);
                }

                return format;
            },

            /**
             * Exposes the localize function to Handlebars.js templates. See {{#crossLink "MediasiteApplication/babel"}}{{/crossLink}}.
             *
             * You may set the optional parameter html=true if the string must be returned as html to be rendered on the client. If not set,
             * the string will be escaped.
             *
             * @method localize
             *
             * @example
             { {babel "Local_Example"} }
             */
            this.handlebarHelper = function () {
                if (arguments.length == 1) {
                    Application.error('babel Helper', 'At least one argument must be given.');
                    return;
                }

                var options,
                    formatArgs = [],
                    argArray = Array.prototype.slice.call(arguments);

                for (var index = 0; index < argArray.length; index++) {
                    var argument = argArray[index];
                    if (index == argArray.length - 1) {
                        options = argument;
                        continue;
                    }

                    if (index == 0) {
                        formatArgs.push(argument.trim());
                        continue;
                    }

                    formatArgs.push(argument);
                }

                if (options.hash.html) {
                    return new Handlebars.SafeString(self.translate.apply(self, formatArgs));
                }
                else {
                    return Handlebars.Utils.escapeExpression(self.translate.apply(self, formatArgs));
                }
            };

        initialize();


        //----------------------LODASH BINARY SEARCH------------------------//

        var MAX_ARRAY_LENGTH = 4294967295;
        var HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1;
        var MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1;
        var nativeFloor = Math.floor;
        var nativeMin = Math.min;
        var symbolTag = '[object Symbol]';
        var objectProto = Object.prototype;
        var objectToString = objectProto.toString;
        var stringTag = '[object String]';
        var objectProto = Object.prototype;
        var objectToString = objectProto.toString;
        var isArray = function (obj) {
            return Object.prototype.toString.call(obj) === "[object Array]";
        }; 

        /**
         * This method is like `_.indexOf` except that it performs a binary
         * search on a sorted `array`.
         *
         * @static
         * @memberOf _
         * @since 4.0.0
         * @category Array
         * @param {Array} array The array to search.
         * @param {*} value The value to search for.
         * @returns {number} Returns the index of the matched value, else `-1`.
         * @example
         *
         * _.sortedIndexOf([1, 1, 2, 2], 2);
         * // => 2
         */
        function sortedIndexOf(array, value) {
            var length = array ? array.length : 0;
            if (length) {
                var index = baseSortedIndex(array, value);
                if (index < length && eq(array[index], value)) {
                    return index;
                }
            }
            return -1;
        }

        /**
         * The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which
         * performs a binary search of `array` to determine the index at which `value`
         * should be inserted into `array` in order to maintain its sort order.
         *
         * @private
         * @param {Array} array The sorted array to inspect.
         * @param {*} value The value to evaluate.
         * @param {boolean} [retHighest] Specify returning the highest qualified index.
         * @returns {number} Returns the index at which `value` should be inserted
         *  into `array`.
         */
        function baseSortedIndex(array, value, retHighest) {
            var low = 0,
                high = array ? array.length : low;

            if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) {
                while (low < high) {
                    var mid = (low + high) >>> 1,
                        computed = array[mid];

                    if (computed !== null && !isSymbol(computed) &&
                        (retHighest ? (computed <= value) : (computed < value))) {
                        low = mid + 1;
                    } else {
                        high = mid;
                    }
                }
                return high;
            }
            return baseSortedIndexBy(array, value, identity, retHighest);
        }

        /**
         * The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy`
         * which invokes `iteratee` for `value` and each element of `array` to compute
         * their sort ranking. The iteratee is invoked with one argument; (value).
         *
         * @private
         * @param {Array} array The sorted array to inspect.
         * @param {*} value The value to evaluate.
         * @param {Function} iteratee The iteratee invoked per element.
         * @param {boolean} [retHighest] Specify returning the highest qualified index.
         * @returns {number} Returns the index at which `value` should be inserted
         *  into `array`.
         */
        function baseSortedIndexBy(array, value, iteratee, retHighest) {
            value = iteratee(value);

            var low = 0,
                high = array ? array.length : 0,
                valIsNaN = value !== value,
                valIsNull = value === null,
                valIsSymbol = isSymbol(value),
                valIsUndefined = value === undefined;

            while (low < high) {
                var mid = nativeFloor((low + high) / 2),
                    computed = iteratee(array[mid]),
                    othIsDefined = computed !== undefined,
                    othIsNull = computed === null,
                    othIsReflexive = computed === computed,
                    othIsSymbol = isSymbol(computed);

                if (valIsNaN) {
                    var setLow = retHighest || othIsReflexive;
                } else if (valIsUndefined) {
                    setLow = othIsReflexive && (retHighest || othIsDefined);
                } else if (valIsNull) {
                    setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull);
                } else if (valIsSymbol) {
                    setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol);
                } else if (othIsNull || othIsSymbol) {
                    setLow = false;
                } else {
                    setLow = retHighest ? (computed <= value) : (computed < value);
                }
                if (setLow) {
                    low = mid + 1;
                } else {
                    high = mid;
                }
            }
            return nativeMin(high, MAX_ARRAY_INDEX);
        }

        /**
         * Performs a
         * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
         * comparison between two values to determine if they are equivalent.
         *
         * @static
         * @memberOf _
         * @since 4.0.0
         * @category Lang
         * @param {*} value The value to compare.
         * @param {*} other The other value to compare.
         * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
         * @example
         *
         * var object = { 'user': 'fred' };
         * var other = { 'user': 'fred' };
         *
         * _.eq(object, object);
         * // => true
         *
         * _.eq(object, other);
         * // => false
         *
         * _.eq('a', 'a');
         * // => true
         *
         * _.eq('a', Object('a'));
         * // => false
         *
         * _.eq(NaN, NaN);
         * // => true
         */
        function eq(value, other) {
            return value === other || (value !== value && other !== other);
        }

        /**
         * Checks if `value` is classified as a `Symbol` primitive or object.
         *
         * @static
         * @memberOf _
         * @since 4.0.0
         * @category Lang
         * @param {*} value The value to check.
         * @returns {boolean} Returns `true` if `value` is correctly classified,
         *  else `false`.
         * @example
         *
         * _.isSymbol(Symbol.iterator);
         * // => true
         *
         * _.isSymbol('abc');
         * // => false
         */
        function isSymbol(value) {
            return typeof value == 'symbol' ||
                (isObjectLike(value) && objectToString.call(value) == symbolTag);
        }

        /**
         * Checks if `value` is object-like. A value is object-like if it's not `null`
         * and has a `typeof` result of "object".
         *
         * @static
         * @memberOf _
         * @since 4.0.0
         * @category Lang
         * @param {*} value The value to check.
         * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
         * @example
         *
         * _.isObjectLike({});
         * // => true
         *
         * _.isObjectLike([1, 2, 3]);
         * // => true
         *
         * _.isObjectLike(_.noop);
         * // => false
         *
         * _.isObjectLike(null);
         * // => false
         */
        function isObjectLike(value) {
            return !!value && typeof value == 'object';
        }

        /**
         * This method returns the first argument given to it.
         *
         * @static
         * @since 0.1.0
         * @memberOf _
         * @category Util
         * @param {*} value Any value.
         * @returns {*} Returns `value`.
         * @example
         *
         * var object = { 'user': 'fred' };
         *
         * _.identity(object) === object;
         * // => true
         */
        function identity(value) {
            return value;
        }



        /**
         * Checks if `value` is classified as a `String` primitive or object.
         *
         * @static
         * @since 0.1.0
         * @memberOf _
         * @category Lang
         * @param {*} value The value to check.
         * @returns {boolean} Returns `true` if `value` is correctly classified,
         *  else `false`.
         * @example
         *
         * _.isString('abc');
         * // => true
         *
         * _.isString(1);
         * // => false
         */
        function isString(value) {
            return typeof value == 'string' ||
                (!isArray(value) && isObjectLike(value) && objectToString.call(value) == stringTag);
        }

        //-------------------------------------------------------------------
    }
})(window);