/// <reference path="jquery.mask.js" />
define([
    "handlebars",
    "app",
    "moment",
], function (Handlebars, app, moment) {
    var padLeft = function (str, pad) {
        str = "" + str;

        return pad.substring(0, pad.length - str.length) + str;
    };
    var fetchFromObject = function (o, s) {
        s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
        s = s.replace(/^\./, '');           // strip a leading dot
        var a = s.split('.');
        for (var i = 0, n = a.length; i < n; ++i) {
            var k = a[i];
            if (k in o) {
                o = o[k];
            } else {
                return;
            }
        }
        return o;
    }

    /*
    * Custom filters and handlers for Epoxy.js
    */
    Backbone.Epoxy.binding.addFilter("concat", function () {
        var result = "";
        for (var i = 0, len = arguments.length; i < arguments.length; i++) {
            try {
                result += arguments[i].toString();
            } catch (error) { }
        }
        return result;
    });
    Backbone.Epoxy.binding.addFilter("coalesce", {
        get: function (binding, value1, value2) {
            if (binding !== null) {
                return binding;
            }
            else {
                for (var i = 1, len = arguments.length; i < arguments.length; i++) {
                    var value = arguments[i];

                    if (value !== null)
                        return value;
                }
            }
        },
        set: function (value) {
            return value;
        },
    });
    Backbone.Epoxy.binding.addFilter("concatArrays", function (bindingArray, array1, array2) {
        var newArray = [];

        for (var i = 0, len = arguments.length; i < arguments.length; i++)
            newArray = newArray.concat(arguments[i]);

        return newArray;
    });
    Backbone.Epoxy.binding.addFilter("decimalFixed", {
        get: function (binding, decimals) {
            if (binding != null)
            {
                var parsed = parseFloat(binding);

                return (isNaN(parsed) == false)
                            ? parsed.toFixed(
                                (decimals == undefined)
                                    ? 2
                                    : decimals)
                            : null;
            }
            else {
                return null;
            }
        },
        set: function (value) {
            return (value != null)
                        ? parseFloat(value)
                        : null;
        },
    });
    Backbone.Epoxy.binding.addFilter("contains", function (binding, values, strict) {
        if (values) {
            if (_.isObject(binding) == false) {
                if (strict != true) {
                    for (var i = 0, len = values.length; i < len; i++)
                        if (binding == values[i]) return true;
                }
                else {
                    for (var i = 0, len = values.length; i < len; i++)
                        if (binding === values[i]) return true;
                }
            }
            else {
                return (_.findWhere(values, binding));
            }
        }

        return false;
    });
    Backbone.Epoxy.binding.addFilter("mathDiv", function (binding, num1, num2) {        
        var result = binding;

        for (var i = 1, len = arguments.length; i < arguments.length; i++) {
            result /= arguments[i];
        }

        return result;
    });
    Backbone.Epoxy.binding.addFilter("mathMul", function (binding, num1, num2) {
        var result = binding;

        for (var i = 1, len = arguments.length; i < arguments.length; i++) {
            result *= arguments[i];
        }

        return result;
    });
    Backbone.Epoxy.binding.addFilter("mathSub", function (binding, num1, num2) {
        var result = binding;

        for (var i = 1, len = arguments.length; i < arguments.length; i++) {
            result -= arguments[i];
        }

        return result;
    });
    Backbone.Epoxy.binding.addFilter("mathSum", function (binding, num1, num2) {
        var result = binding;

        for (var i = 1, len = arguments.length; i < arguments.length; i++) {
            result += arguments[i];
        }

        return result;
    });
    Backbone.Epoxy.binding.addFilter("notIn", function (binding, values, strict) {
        var match = false;

        if (values) {
            if (strict != true) {
                for (var i = 0, len = values.length; i < len; i++) {
                    if (binding == values[i]) {
                        match = true;
                        break;
                    }
                }
            }
            else {
                for (var i = 0, len = values.length; i < len; i++) {
                    if (binding === values[i]) {
                        match = true;
                        break;
                    }
                }
            }
        }

        return !match;
    });
    Backbone.Epoxy.binding.addFilter("equals", function (binding, value, strict) {
        return ((strict != true) && (binding == value))
                || ((strict == true) && (binding === value));
    });
    Backbone.Epoxy.binding.addFilter("equalsAny", function (binding, value1, value2) {
        for (var i = 1, len = arguments.length; i < arguments.length; i++) {
            var value = arguments[i];

            if (value === binding)
                return true;
        }

        return false;
    });
    Backbone.Epoxy.binding.addFilter("inRange", function (binding, min, max, includeLimits) {
        return (binding != null)
                && (((includeLimits != false) && (binding >= min) && (binding <= max))
                    || ((includeLimits === false) && (binding > min) && (binding < max)))
    });
    Backbone.Epoxy.binding.addFilter("inRangeMax", function (binding, max, includeLimits) {
        return (binding != null)
                && (((includeLimits != false) && (binding <= max))
                    || ((includeLimits === false) && (binding < max)))
    });
    Backbone.Epoxy.binding.addFilter("inRangeMin", function (binding, min, includeLimits) {
        return (binding != null)
                && (((includeLimits != false) && (binding >= min))
                    || ((includeLimits === false) && (binding > min)))
    });
    Backbone.Epoxy.binding.addFilter("duration", {
        get: function (value, inputFormat, outputFormat) {
            if (!value) value = 0;

            var dur = moment.duration(value, inputFormat),
                outFormatKeys = ['years', 'y', 'months', 'M', 'weeks', 'w', 'days', 'd', 'hours', 'h', 'minutes', 'm', 'seconds', 's', 'milliseconds', 'ms',];

            //If output format is not contained on the keys array is because is a complex format like HH:mm.
            //Not a simple format like just seconds or just minutes.
            if (outFormatKeys.indexOf(outputFormat) == -1) {
                switch (outputFormat) {
                    case 's.ms':
                        return outputFormat.replace(/ms/g, padLeft(dur.milliseconds(), '000'))
                                            .replace(/s/g, padLeft(Math.floor(dur.asSeconds()), '00'));
                    case 'm:s':
                        return outputFormat.replace(/s/g, padLeft(dur.seconds(), '00'))
                                            .replace(/m/g, padLeft(Math.floor(dur.asMinutes()), '00'));
                    case 'm:s.ms':
                        return outputFormat.replace(/ms/g, padLeft(dur.milliseconds(), '000'))
                                            .replace(/s/g, padLeft(dur.seconds(), '00'))
                                            .replace(/m/g, padLeft(Math.floor(dur.asMinutes()), '00'));
                    case 'h:m':
                        return outputFormat.replace(/m/g, padLeft(dur.minutes(), '00'))
                                            .replace(/h/g, padLeft(Math.floor(dur.asHours()), '00'));
                    case 'h:m:s':
                        return outputFormat.replace(/s/g, padLeft(dur.seconds(), '00'))
                                            .replace(/m/g, padLeft(dur.minutes(), '00'))
                                            .replace(/h/g, padLeft(Math.floor(dur.asHours()), '00'));
                    case 'h:m:s.ms':
                        return outputFormat.replace(/ms/g, padLeft(dur.milliseconds(), '000'))
                                            .replace(/s/g, padLeft(dur.seconds(), '00'))
                                            .replace(/m/g, padLeft(dur.minutes(), '00'))
                                            .replace(/h/g, padLeft(Math.floor(dur.asHours()), '00'));
                }
            }
            else {
                return dur.as(outputFormat);
            }
        },
        //get: function (value, inputFormat, outputFormat, biggerKey) {
        //    if ((value != undefined) && (value != null)) {
        //        var dur = moment.duration(value, inputFormat),
        //            outFormatKeys = ['years', 'y', 'months', 'M', 'weeks', 'w', 'days', 'd', 'hours', 'h', 'minutes', 'm', 'seconds', 's', 'milliseconds', 'ms',];

        //        //If output format is not contained on the keys array is because is a complex format like HH:mm.
        //        //Not a simple format like just seconds or just minutes.
        //        if (outFormatKeys.indexOf(outputFormat) == -1) {
        //            var output = outputFormat;

        //            if (!biggerKey)
        //                biggerKey = 'y';

        //            if (['ms', 'millisecond', 'milliseconds'].indexOf(biggerKey) != -1)
        //                return output.replace(/(ms)|(millisecond)|(milliseconds)/g, padLeft(dur.asMilliseconds(), '000');
        //            else
        //                output = output.replace(/(ms)|(millisecond)|(milliseconds)/g, padLeft(dur.milliseconds());

        //            if (['s', 'second', 'seconds'].indexOf(biggerKey) != -1)
        //                return output.replace(/(s)|(second)|(seconds)/g, padLeft(Math.floor(dur.asSeconds()));
        //            else
        //                output = output.replace(/(s)|(second)|(seconds)/g, padLeft(dur.seconds());

        //            if (['m', 'minute', 'minutes'].indexOf(biggerKey) != -1)
        //                return output.replace(/(m)|(minute)|(minutes)/g, padLeft(Math.floor(dur.asMinutes()));
        //            else
        //                output = output.replace(/(m)|(minute)|(minutes)/g, padLeft(dur.minutes());

        //            if (['h', 'hour', 'hours'].indexOf(biggerKey) != -1)
        //                return output.replace(/(h)|(hour)|(hours)/g, padLeft(Math.floor(dur.asHours()));
        //            else
        //                output = output.replace(/(h)|(hour)|(hours)/g, padLeft(dur.hours());

        //            if (['d', 'day', 'days'].indexOf(biggerKey) != -1)
        //                return output.replace(/(d)|(day)|(days)/g, padLeft(Math.floor(dur.asDays()));
        //            else
        //                output = output.replace(/(d)|(day)|(days)/g, padLeft(dur.days());

        //            if (['w', 'week', 'weeks'].indexOf(biggerKey) != -1)
        //                return output.replace(/(w)|(week)|(weeks)/g, Math.floor(dur.asWeeks()));
        //            else
        //                output = output.replace(/(w)|(week)|(weeks)/g, dur.weeks());

        //            if (['M', 'month', 'month'].indexOf(biggerKey) != -1)
        //                return output.replace(/(M)|(month)|(months)/g, Math.floor(dur.asMonths()));
        //            else
        //                output = output.replace(/(M)|(month)|(months)/g, dur.months());

        //            if (['y', 'year', 'years'].indexOf(biggerKey) != -1)
        //                return output.replace(/(t)|(year)|(years)/g, Math.floor(dur.asYears()));
        //            else
        //                output = output.replace(/(t)|(year)|(years)/g, dur.years());

        //            return output;

        //            //var totalms = dur.milliseconds(),
        //            //    x,
        //            //    output = outputFormat;

        //            //var milliseconds = Math.round(totalms % 1000);
        //            //x = totalms / 1000;
        //            //var seconds = Math.round(x % 60);
        //            //x /= 60;
        //            //var minutes = Math.round(x % 60);
        //            //x /= 60;
        //            //var hours = Math.round(x % 24);
        //            //x /= 24;
        //            //var days = Math.round(x);
        //            ////var days = Math.round(x % 365);
        //            ////x /= 365;
        //            ////var years = Math.round(x);

        //            //output.replace(/(millisecond)|(milliseconds)|(ms)/g, milliseconds)
        //            //    .replace(/(second)|(s)/g, seconds)
        //            //    .replace(/(second)|(minutes)|(m)/g, minutes)
        //            //    .replace(/(hour)|(hours)|(h)/g, hours)
        //            //    .replace(/(day)|(days)|(d)/g, days);
        //            ////.replace(/(years)|(y)/g, ms)

        //            //return output;
        //        }
        //        else {
        //            return dur.as(outputFormat);
        //        }
        //    }
        //},
        set: function (value) {
            return value;
        }
    });
    Backbone.Epoxy.binding.addFilter("isNotNull", function (binding) {
        return (binding !== null);
    });
    Backbone.Epoxy.binding.addFilter("isNull", function (binding) {
        return (binding === null);
    });
    Backbone.Epoxy.binding.addFilter("momentFormat", {
        get: function (binding, inputFormat, outputFormat) {
            return (binding) ? new moment(binding, inputFormat).format(outputFormat) : binding;
        },

        set: function (value) {
            return value;
        }
    });
    Backbone.Epoxy.binding.addFilter("notEquals", function (binding, value, strict) {
        return ((strict != true) && (binding != value))
                || ((strict == true) && (binding !== value));
    });
    Backbone.Epoxy.binding.addFilter("outRange", function (binding, min, max, includeLimits) {
        return (binding != null)
                && (((includeLimits != false) && ((binding < min) || (binding > max)))
                    || ((includeLimits === false) && ((binding <= min) || (binding >= max))))
    });
    Backbone.Epoxy.binding.addFilter("outRangeMax", function (binding, max, includeLimits) {
        return (binding != null)
                && (((includeLimits != false) && (binding > max))
                    || ((includeLimits === false) && (binding >= max)))
    });
    Backbone.Epoxy.binding.addFilter("outRangeMin", function (binding, min, includeLimits) {
        return (binding != null)
                && (((includeLimits != false) && (binding < min))
                    || ((includeLimits === false) && (binding <= min)))
    });
    Backbone.Epoxy.binding.addFilter("prop", function (binding, propPath) {
        return (binding != null)
                    ? fetchFromObject(binding, propPath)
                    : null;
    });
    Backbone.Epoxy.binding.addFilter("read", {
        get: function (binding) {
            return binding;
        },
        set: function (value) {
        }
    });
    Backbone.Epoxy.binding.addFilter("string", {
        get: function (binding) {
            return binding + '';
        },
        set: function (value) {
            return value + '';
        }
    });
    Backbone.Epoxy.binding.addFilter("trim", {
        get: function (value) {
            return ((value != undefined) && (value != null)) ? value.toString().trim() : value;
        },
        set: function (value) {
            return ((value != undefined) && (value != null)) ? value.toString().trim() : value;
        }
    });

    Backbone.Epoxy.binding.addHandler("clickSet", {
        binding: null,
        clickProxy: null,
        value: null,

        init: function ($element, value, bindings, context) {
            this.binding = bindings[value];
            this.clickProxy = _.bind(this.click, this);
            this.value = context.clickSetValue;

            $element.on('click', this.clickProxy);
        },
        clean: function () {
            this.$el.off('click', this.clickProxy);
        },

        click: function (e) {
            try {
                if (this.value !== null) {
                    if (this.value != 'null')
                        this.binding(this.value);
                    else
                        this.binding(null);
                }
                else {
                    this.binding(this.$element.val());
                }
            }
            catch (Error) { console.error(Error.stack); }
        },
    });
    Backbone.Epoxy.binding.allowedParams.clickSetValue = 1;
    Backbone.Epoxy.binding.addHandler("clickToggle", {
        binding: null,
        clickProxy: null,

        init: function ($element, value, bindings, context) {
            this.binding = bindings[value];
            this.clickProxy = _.bind(this.click, this);

            $element.on('click', this.clickProxy);
        },
        clean: function () {
            this.$el.off('click', this.clickProxy);
        },

        click: function (e) {
            try {
                this.binding(!this.binding());
            }
            catch (Error) { console.error(Error.stack); }
        },
    });
    Backbone.Epoxy.binding.addHandler("valueLockFocus", {
        currentValue: null,
        storedValue: null,

        init: function ($element, bindingName, bindings, context) {

            $element.on('blur', _.bind(this.blur, this));
            $element.on('focus', _.bind(this.focus, this));
        },
        clean: function () {
            this.$el.off('blur', _.bind(this.blur, this));
            this.$el.off('focus', _.bind(this.focus, this));
        },
        get: function ($element) {
            //if (this.currentValue != null) {
            //    var blurInput =
            //        $('<input>')
            //            .css({
            //                position: 'absolute',
            //                top: '-100000px',
            //                left: '-100000px',
            //            })
            //            .appendTo($('body'))
            //            .focus()
            //            .remove();

                //This line is for IE.
                this.currentValue = null;
            //}

            this.storedValue = null;

            return $element.val();
        },
        set: function ($element, value) {
            if (this.currentValue == null)
                $element.val(value);
            else
                this.storedValue = value;
        },

        blur: function (e) {
            try {
                this.currentValue = null;

                if (this.storedValue)
                    this.$el.val(this.storedValue);
            }
            catch (Error) { console.error(Error.stack); }
        },
        focus: function (e) {
            try {
                this.storedValue =
                this.currentValue = this.$el.val();
            }
            catch (Error) { console.error(Error.stack); }
        },
    });


    /*
    * This decorates Handlebars.js with the ability to load
    * templates from an external source, with light caching.
    * 
    * To render a template, pass a closure that will receive the 
    * template as a function parameter, eg, 
    *   T.render('templateName', function(t) {
    *       $('#somediv').html( t() );
    *   });
    */

    Handlebars.registerHelper('writeTagHref', function (url) {
        url = Handlebars.Utils.escapeExpression(url);

        var result = 'href="' + url + '"';

        return new Handlebars.SafeString(result);
    });

    Handlebars.registerHelper('customPartial', function (id, ctx) {
        var template = Handlebars.compile($('script#' + id).html());
        return template(ctx);
    });

    //this is a helper to use HandleBars templates within a handlebar templates that gets compiled before.
    //this way the expressions that are preceded by HBExp (eg. {{HBExp "name"}}) will be compiled first time as {{name}}
    //so you can then fetch the template inside the one already compiled and compile once again with this context.
    Handlebars.registerHelper('HBExp', function (text) {
        return new Handlebars.SafeString(
          "{{" + text + "}}"
        );
    });

    Handlebars.registerHelper('selected', function (val1, val2, caseSensitive, trimSensitive) {
        var toString = Object.prototype.toString;
        caseSensitive = (caseSensitive != null && caseSensitive != undefined && (caseSensitive === true || caseSensitive === false || toString.call(caseSensitive) === '[object Boolean]')) ? caseSensitive : false;
        trimSensitive = (trimSensitive != null && trimSensitive != undefined && (trimSensitive === true || trimSensitive === false || toString.call(trimSensitive) === '[object Boolean]')) ? trimSensitive : false;

        if (!caseSensitive) {
            if (val1 != null && val1 != undefined && val1.toString && (val1 = val1.toString().toUpperCase()));
            if (val2 != null && val2 != undefined && val2.toString && (val2 = val2.toString().toUpperCase()));
        }

        if (!trimSensitive) {
            if (val1 != null && val1 != undefined && val1.toString && (val1 = val1.toString().trim()));
            if (val2 != null && val2 != undefined && val2.toString && (val2 = val2.toString().trim()));
        }

        return val1 == val2 ? ' selected' : '';
    });

    Handlebars.registerHelper('checked', function (val) {
        return val == true ? ' checked' : '';
    });

    //if equals
    //used as {{#ifEquals a b}} (if a == b) then
    Handlebars.registerHelper('ifEquals', function () {
        var argsCount = arguments.length,
            options,
            v1 = arguments[0],
            v2 = arguments[1];

        options = arguments[argsCount - 1];

        if (argsCount == 3) {
            if (v1 === v2)
                return options.fn(this);
            else
                return options.inverse(this);
        }
        else if ((argsCount == 4) && (v2.toUpperCase() == 'NOT')) {
            var v3 = arguments[2];

            if (v1 !== v3)
                return options.fn(this);
            else
                return options.inverse(this);
        }
        else {
            return options.inverse(this);
        }
    });
    //if equals multiple
    //used as {{#ifEqualsM a b, c, d..., n-1, n}} (if a == b && c == d ... && n-1 == n) then
    Handlebars.registerHelper('ifEqualsM', function () {
        var argsCount = arguments.length,
            options;

        options = arguments[argsCount - 1];

        var equals = true;
        for (var i = 0; i < argsCount - 1; i += 2) {
            if (arguments[i] != arguments[i + 1]) {
                equals = false;
                break;
            }
        }

        if (equals == true)
            return options.fn(this);
        else
            return options.inverse(this);
    });

    Handlebars.registerHelper('ifEqualsAny', function () {
        var argsCount = arguments.length,
            options;

        options = arguments[argsCount - 1];

        var equals = false;
        var val = arguments[0];
        for (var i = 1; i < argsCount - 1; i++) {
            if (val == arguments[i]) {
                equals = true;
                break;
            }
        }

        if (equals == true)
            return options.fn(this);
        else
            return options.inverse(this);
    });

    Handlebars.registerHelper('addPartial', function (partialName, options) {
        if (!this.__partials)
            this.__partials = {};

        this.__partials[partialName] = options.fn(this);
    });
    Handlebars.registerHelper('getPartial', function (partialName, options) {
        if (this.__partials)
            return this.__partials[partialName];
    });
    Handlebars.registerHelper('removePartial', function (partialName, options) {
        if (this.__partials)
            delete this.__partials[partialName];
    });

    //helper for a space friendly string, specify maxchars and the text will be 
    //susbtringed to those chars including "..." if the text is longer than maxchars.
    Handlebars.registerHelper('spaceFriendlyString', function (text, maxChars) {
        if (text != null && text != undefined) {
            if (text.length > maxChars) {
                return text.substring(0, maxChars - 3) + "...";
            }
        }
        return text;
    });

    Handlebars.registerHelper('substringText', function (text, start, end) {
        if (text != null && text != undefined) {
            if (end > 0) {
                return text.substring(start, end);
            } else {
                return text.substring(start, text.length);
            }
        }
        return text;
    });

    var Template = function () {
        this.cached = {};

        //defining app root to be able to get the templates from any URL
        //on production enviroment this should often by just '/' to get templates from
        // '/app/templates/'

        this.appRoot = app.foldersRoot;
    };
    var T = new Template();
    $.extend(Template.prototype, {
        render: function (name, callback, customPath, sectionID) {
            if (T.isCached(name, customPath, sectionID)) {
                name = T.getNameCached(name, customPath, sectionID);
                callback(T.cached[name]);
            } else {
                var customPathScoped = customPath;
                var sectionIDScoped = sectionID;
                $.get(T.urlFor(name, customPath), function (raw) {
                    T.store(name, raw, customPathScoped, sectionIDScoped);
                    T.render(name, callback, customPathScoped, sectionIDScoped);
                });
                //$.ajax({
                //    url: T.urlFor(name, customPath),
                //    success: function (raw) {
                //        T.store(name, raw, customPathScoped, sectionIDScoped);
                //        T.render(name, callback, customPathScoped, sectionIDScoped);
                //    },
                //    async: false, 
                //}); 
            }
        },
        getNameCached: function (name, customPath, sectionID) {
            customPath = (customPath) ? customPath : "";
            sectionID = (sectionID) ? "_" + sectionID : "";
            var builtName = customPath + name + sectionID;
            return builtName;
        },
        renderSync: function (name, callback, customPath) {
            if (!T.isCached(name, customPath)) {
                T.fetch(name, customPath);
            }
            T.render(name, callback);
        },
        prefetch: function (name, customPath) {
            var customPathScoped = customPath;
            $.get(T.urlFor(name, customPath), function (raw) {
                T.store(name, raw, customPathScoped);
            });
        },
        fetch: function (name, customPath) {
            // synchronous, for those times when you need it.
            if (!T.isCached(name, customPath)) {
                var raw = $.ajax({ 'url': T.urlFor(name, customPath), 'async': false }).responseText;
                T.store(name, raw, customPath);
            }
        },
        isCached: function (name, customPath, sectionID) {
            name = T.getNameCached(name, customPath, sectionID);
            return !!T.cached[name];
        },
        store: function (name, raw, customPath, sectionID) {
            name = T.getNameCached(name, customPath, sectionID);

            if (sectionID) {
                var tempDiv = $("<div></div>");
                tempDiv.html(raw);
            }

            T.cached[name] = (sectionID) ? Handlebars.compile(tempDiv.find("#" + sectionID).html()) : Handlebars.compile(raw);
        },
        urlFor: function (name, customPath) {
            return ((customPath) ? T.appRoot + customPath : T.appRoot + "/app/templates/") + name + ".aspx";
        }
    });

    return T;

});