// Backbone.ModelBinder v1.1.0
// (c) 2015 Bart Wood
// Distributed Under MIT License

(function (factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['underscore', 'jquery', 'backbone'], factory);
    } else if(typeof module !== 'undefined' && module.exports) {
        // CommonJS
        module.exports = factory(
            require('underscore'),
            require('jquery'),
            require('backbone')
        );
    } else {
        // Browser globals
        factory(_, jQuery, Backbone);
    }
}(function(_, $, Backbone){

    if(!Backbone){
        throw new Error('Please include Backbone.js before Backbone.ModelBinder.js');
    }

    Backbone.ModelBinder = function(){
        _.bindAll.apply(_, [this].concat(_.functions(this)));
    };

    // Static setter for class level options
    Backbone.ModelBinder.SetOptions = function(options){
        Backbone.ModelBinder.options = options;
    };

    // Current version of the library.
    Backbone.ModelBinder.VERSION = '1.1.0';
    Backbone.ModelBinder.Constants = {};
    Backbone.ModelBinder.Constants.ModelToView = 'ModelToView';
    Backbone.ModelBinder.Constants.ViewToModel = 'ViewToModel';

    _.extend(Backbone.ModelBinder.prototype, {

        bind:function (model, rootEl, attributeBindings, options) {
            this.unbind();

            this._model = model;
            this._rootEl = rootEl;
            this._setOptions(options);

            if (!this._model) this._throwException('model must be specified');
            if (!this._rootEl) this._throwException('rootEl must be specified');

            if(attributeBindings){
                // Create a deep clone of the attribute bindings
                this._attributeBindings = $.extend(true, {}, attributeBindings);

                this._initializeAttributeBindings();
                this._initializeElBindings();
            }
            else {
                this._initializeDefaultBindings();
            }

            this._bindModelToView();
            this._bindViewToModel();
        },

        bindCustomTriggers: function (model, rootEl, triggers, attributeBindings, modelSetOptions) {
            this._triggers = triggers;
            this.bind(model, rootEl, attributeBindings, modelSetOptions);
        },

        unbind:function () {
            this._unbindModelToView();
            this._unbindViewToModel();

            if(this._attributeBindings){
                delete this._attributeBindings;
                this._attributeBindings = undefined;
            }
        },

        _setOptions: function(options){
            this._options = _.extend({
                boundAttribute: 'name'
            }, Backbone.ModelBinder.options, options);

            // initialize default options
            if(!this._options['modelSetOptions']){
                this._options['modelSetOptions'] = {};
            }
            this._options['modelSetOptions'].changeSource = 'ModelBinder';

            if(!this._options['changeTriggers']){
                this._options['changeTriggers'] = {'': 'change', '[contenteditable]': 'blur'};
            }

            if(!this._options['initialCopyDirection']){
                this._options['initialCopyDirection'] = Backbone.ModelBinder.Constants.ModelToView;
            }
        },

        // Converts the input bindings, which might just be empty or strings, to binding objects
        _initializeAttributeBindings:function () {
            var attributeBindingKey, inputBinding, attributeBinding, elementBindingCount, elementBinding;

            for (attributeBindingKey in this._attributeBindings) {
                inputBinding = this._attributeBindings[attributeBindingKey];

                if (_.isString(inputBinding)) {
                    attributeBinding = {elementBindings: [{selector: inputBinding}]};
                }
                else if (_.isArray(inputBinding)) {
                    attributeBinding = {elementBindings: inputBinding};
                }
                else if(_.isObject(inputBinding)){
                    attributeBinding = {elementBindings: [inputBinding]};
                }
                else {
                    this._throwException('Unsupported type passed to Model Binder ' + attributeBinding);
                }

                // Add a linkage from the element binding back to the attribute binding
                for(elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++){
                    elementBinding = attributeBinding.elementBindings[elementBindingCount];
                    elementBinding.attributeBinding = attributeBinding;
                }

                attributeBinding.attributeName = attributeBindingKey;
                this._attributeBindings[attributeBindingKey] = attributeBinding;
            }
        },

        // If the bindings are not specified, the default binding is performed on the specified attribute, name by default
        _initializeDefaultBindings: function(){
            var elCount, elsWithAttribute, matchedEl, name, attributeBinding;

            this._attributeBindings = {};
            elsWithAttribute = $('[' + this._options['boundAttribute'] + ']', this._rootEl);

            for(elCount = 0; elCount < elsWithAttribute.length; elCount++){
                matchedEl = elsWithAttribute[elCount];
                name = $(matchedEl).attr(this._options['boundAttribute']);

                // For elements like radio buttons we only want a single attribute binding with possibly multiple element bindings
                if(!this._attributeBindings[name]){
                    attributeBinding =  {attributeName: name};
                    attributeBinding.elementBindings = [{attributeBinding: attributeBinding, boundEls: [matchedEl]}];
                    this._attributeBindings[name] = attributeBinding;
                }
                else{
                    this._attributeBindings[name].elementBindings.push({attributeBinding: this._attributeBindings[name], boundEls: [matchedEl]});
                }
            }
        },

        _initializeElBindings:function () {
            var bindingKey, attributeBinding, bindingCount, elementBinding, foundEls, elCount, el;
            for (bindingKey in this._attributeBindings) {
                attributeBinding = this._attributeBindings[bindingKey];

                for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) {
                    elementBinding = attributeBinding.elementBindings[bindingCount];
                    if (elementBinding.selector === '') {
                        foundEls = $(this._rootEl);
                    }
                    else {
                        foundEls = $(elementBinding.selector, this._rootEl);
                    }

                    if (foundEls.length === 0) {
                        this._throwException('Bad binding found. No elements returned for binding selector ' + elementBinding.selector);
                    }
                    else {
                        elementBinding.boundEls = [];
                        for (elCount = 0; elCount < foundEls.length; elCount++) {
                            el = foundEls[elCount];
                            elementBinding.boundEls.push(el);
                        }
                    }
                }
            }
        },

        _bindModelToView: function () {
            this._model.on('change', this._onModelChange, this);

            if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ModelToView){
                this.copyModelAttributesToView();
            }
        },

        // attributesToCopy is an optional parameter - if empty, all attributes
        // that are bound will be copied.  Otherwise, only attributeBindings specified
        // in the attributesToCopy are copied.
        copyModelAttributesToView: function(attributesToCopy){
            var attributeName, attributeBinding;

            for (attributeName in this._attributeBindings) {
                if(attributesToCopy === undefined || _.indexOf(attributesToCopy, attributeName) !== -1){
                    attributeBinding = this._attributeBindings[attributeName];
                    this._copyModelToView(attributeBinding);
                }
            }
        },

        copyViewValuesToModel: function(){
            var bindingKey, attributeBinding, bindingCount, elementBinding, elCount, el;
            for (bindingKey in this._attributeBindings) {
                attributeBinding = this._attributeBindings[bindingKey];

                for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) {
                    elementBinding = attributeBinding.elementBindings[bindingCount];

                    if(this._isBindingUserEditable(elementBinding)){
                        if(this._isBindingRadioGroup(elementBinding)){
                            el = this._getRadioButtonGroupCheckedEl(elementBinding);
                            if(el){
                                this._copyViewToModel(elementBinding, el);
                            }
                        }
                        else {
                            for(elCount = 0; elCount < elementBinding.boundEls.length; elCount++){
                                el = $(elementBinding.boundEls[elCount]);
                                if(this._isElUserEditable(el)){
                                    this._copyViewToModel(elementBinding, el);
                                }
                            }
                        }
                    }
                }
            }
        },

        _unbindModelToView: function(){
            if(this._model){
                this._model.off('change', this._onModelChange);
                this._model = undefined;
            }
        },

        _bindViewToModel: function () {
            _.each(this._options['changeTriggers'], function (event, selector) {
                $(this._rootEl).on(event, selector, this._onElChanged);
            }, this);

            if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ViewToModel){
                this.copyViewValuesToModel();
            }
        },

        _unbindViewToModel: function () {
            if(this._options && this._options['changeTriggers']){
                _.each(this._options['changeTriggers'], function (event, selector) {
                    $(this._rootEl).off(event, selector, this._onElChanged);
                }, this);
            }
        },

        _onElChanged:function (event) {
            var el, elBindings, elBindingCount, elBinding;

            el = $(event.target)[0];
            elBindings = this._getElBindings(el);

            for(elBindingCount = 0; elBindingCount < elBindings.length; elBindingCount++){
                elBinding = elBindings[elBindingCount];
                if (this._isBindingUserEditable(elBinding)) {
                    this._copyViewToModel(elBinding, el);
                }
            }
        },

        _isBindingUserEditable: function(elBinding){
            return elBinding.elAttribute === undefined ||
                elBinding.elAttribute === 'text' ||
                elBinding.elAttribute === 'html';
        },

        _isElUserEditable: function(el){
            var isContentEditable = el.attr('contenteditable');
            return isContentEditable || el.is('input') || el.is('select') || el.is('textarea');
        },

        _isBindingRadioGroup: function(elBinding){
            var elCount, el;
            var isAllRadioButtons = elBinding.boundEls.length > 0;
            for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){
                el = $(elBinding.boundEls[elCount]);
                if(el.attr('type') !== 'radio'){
                    isAllRadioButtons = false;
                    break;
                }
            }

            return isAllRadioButtons;
        },

        _getRadioButtonGroupCheckedEl: function(elBinding){
            var elCount, el;
            for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){
                el = $(elBinding.boundEls[elCount]);
                if(el.attr('type') === 'radio' && el.prop('checked')){
                    return el;
                }
            }

            return undefined;
        },

        _getElBindings:function (findEl) {
            var attributeName, attributeBinding, elementBindingCount, elementBinding, boundElCount, boundEl;
            var elBindings = [];

            for (attributeName in this._attributeBindings) {
                attributeBinding = this._attributeBindings[attributeName];

                for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) {
                    elementBinding = attributeBinding.elementBindings[elementBindingCount];

                    for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) {
                        boundEl = elementBinding.boundEls[boundElCount];

                        if (boundEl === findEl) {
                            elBindings.push(elementBinding);
                        }
                    }
                }
            }

            return elBindings;
        },

        _onModelChange:function () {
            var changedAttribute, attributeBinding;

            for (changedAttribute in this._model.changedAttributes()) {
                attributeBinding = this._attributeBindings[changedAttribute];

                if (attributeBinding) {
                    this._copyModelToView(attributeBinding);
                }
            }
        },

        _copyModelToView:function (attributeBinding) {
            var elementBindingCount, elementBinding, boundElCount, boundEl, value, convertedValue;

            value = this._model.get(attributeBinding.attributeName);

            for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) {
                elementBinding = attributeBinding.elementBindings[elementBindingCount];

                for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) {
                    boundEl = elementBinding.boundEls[boundElCount];

                    if(!boundEl._isSetting){
                        convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value);
                        this._setEl($(boundEl), elementBinding, convertedValue);
                    }
                }
            }
        },

        _setEl: function (el, elementBinding, convertedValue) {
            if (elementBinding.elAttribute) {
                this._setElAttribute(el, elementBinding, convertedValue);
            }
            else {
                this._setElValue(el, convertedValue);
            }
        },

        _setElAttribute:function (el, elementBinding, convertedValue) {
            switch (elementBinding.elAttribute) {
                case 'html':
                    el.html(convertedValue);
                    break;
                case 'text':
                    el.text(convertedValue);
                    break;
                case 'enabled':
                    el.prop('disabled', !convertedValue);
                    break;
                case 'displayed':
                    el[convertedValue ? 'show' : 'hide']();
                    break;
                case 'hidden':
                    el[convertedValue ? 'hide' : 'show']();
                    break;
                case 'css':
                    el.css(elementBinding.cssAttribute, convertedValue);
                    break;
                case 'class':
                    var previousValue = this._model.previous(elementBinding.attributeBinding.attributeName);
                    var currentValue = this._model.get(elementBinding.attributeBinding.attributeName);
                    // is current value is now defined then remove the class the may have been set for the undefined value
                    if(!_.isUndefined(previousValue) || !_.isUndefined(currentValue)){
                        previousValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, previousValue);
                        el.removeClass(previousValue);
                    }

                    if(convertedValue){
                        el.addClass(convertedValue);
                    }
                    break;
                default:
                    el.attr(elementBinding.elAttribute, convertedValue);
            }
        },

        _setElValue:function (el, convertedValue) {
            if(el.attr('type')){
                switch (el.attr('type')) {
                    case 'radio':
                        el.prop('checked', el.val() === convertedValue);
                        break;
                    case 'checkbox':
                         el.prop('checked', !!convertedValue);
                        break;
                    case 'file':
                        break;
                    default:
                        el.val(convertedValue);
                }
            }
            else if(el.is('input') || el.is('select') || el.is('textarea')){
                el.val(convertedValue || (convertedValue === 0 ? '0' : ''));
            }
            else {
                el.text(convertedValue || (convertedValue === 0 ? '0' : ''));
            }
        },

        _copyViewToModel: function (elementBinding, el) {
            var result, value, convertedValue;

            if (!el._isSetting) {

                el._isSetting = true;
                result = this._setModel(elementBinding, $(el));
                el._isSetting = false;

                if(result && elementBinding.converter){
                    value = this._model.get(elementBinding.attributeBinding.attributeName);
                    convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value);
                    this._setEl($(el), elementBinding, convertedValue);
                }
            }
        },

        _getElValue: function(elementBinding, el){
            switch (el.attr('type')) {
                case 'checkbox':
                    return el.prop('checked') ? true : false;
                default:
                    if(el.attr('contenteditable') !== undefined){
                        return el.html();
                    }
                    else {
                        return el.val();
                    }
            }
        },

        _setModel: function (elementBinding, el) {
            var data = {};
            var elVal = this._getElValue(elementBinding, el);
            elVal = this._getConvertedValue(Backbone.ModelBinder.Constants.ViewToModel, elementBinding, elVal);
            data[elementBinding.attributeBinding.attributeName] = elVal;
            return this._model.set(data,  this._options['modelSetOptions']);
        },

        _getConvertedValue: function (direction, elementBinding, value) {

            if (elementBinding.converter) {
                value = elementBinding.converter(direction, value, elementBinding.attributeBinding.attributeName, this._model, elementBinding.boundEls);
            }
            else if(this._options['converter']){
                value = this._options['converter'](direction, value, elementBinding.attributeBinding.attributeName, this._model, elementBinding.boundEls);
            }

            return value;
        },

        _throwException: function(message){
            if(this._options.suppressThrows){
                if(typeof(console) !== 'undefined' && console.error){
                    console.error(message);
                }
            }
            else {
                throw new Error(message);
            }
        }
    });

    Backbone.ModelBinder.CollectionConverter = function(collection){
        this._collection = collection;

        if(!this._collection){
            throw new Error('Collection must be defined');
        }
        _.bindAll(this, 'convert');
    };

    _.extend(Backbone.ModelBinder.CollectionConverter.prototype, {
        convert: function(direction, value){
            if (direction === Backbone.ModelBinder.Constants.ModelToView) {
                return value ? value.id : undefined;
            }
            else {
                return this._collection.get(value);
            }
        }
    });

    // A static helper function to create a default set of bindings that you can customize before calling the bind() function
    // rootEl - where to find all of the bound elements
    // attributeType - probably 'name' or 'id' in most cases
    // converter(optional) - the default converter you want applied to all your bindings
    // elAttribute(optional) - the default elAttribute you want applied to all your bindings
    Backbone.ModelBinder.createDefaultBindings = function(rootEl, attributeType, converter, elAttribute){
        var foundEls, elCount, foundEl, attributeName;
        var bindings = {};

        foundEls = $('[' + attributeType + ']', rootEl);

        for(elCount = 0; elCount < foundEls.length; elCount++){
            foundEl = foundEls[elCount];
            attributeName = $(foundEl).attr(attributeType);

            if(!bindings[attributeName]){
                var attributeBinding =  {selector: '[' + attributeType + '="' + attributeName + '"]'};
                bindings[attributeName] = attributeBinding;

                if(converter){
                    bindings[attributeName].converter = converter;
                }

                if(elAttribute){
                    bindings[attributeName].elAttribute = elAttribute;
                }
            }
        }

        return bindings;
    };

    // Helps you to combine 2 sets of bindings
    Backbone.ModelBinder.combineBindings = function(destination, source){
        _.each(source, function(value, key){
            var elementBinding = {selector: value.selector};

            if(value.converter){
                elementBinding.converter = value.converter;
            }

            if(value.elAttribute){
                elementBinding.elAttribute = value.elAttribute;
            }

            if(!destination[key]){
                destination[key] = elementBinding;
            }
            else {
                destination[key] = [destination[key], elementBinding];
            }
        });

        return destination;
    };


    return Backbone.ModelBinder;

}));
