;(function(Form) {

    /**
     * List editor
     * 
     * An array editor. Creates a list of other editor items.
     *
     * Special options:
     * @param {String} [options.schema.itemType]          The editor type for each item in the list. Default: 'Text'
     * @param {String} [options.schema.confirmDelete]     Text to display in a delete confirmation dialog. If false, will not ask for confirmation.
     */
    Form.editors.List = Form.editors.Base.extend({

        events: {
            'click [data-action="add"]': function(event) {
                event.preventDefault();
                this.addItem(null, true);
            }
        },

        initialize: function(options) {
            options = options || {};

            var editors = Form.editors;

            editors.Base.prototype.initialize.call(this, options);

            var schema = this.schema;
            if (!schema) throw "Missing required option 'schema'";

            this.template = options.template || this.constructor.template;

            //Determine the editor to use
            this.Editor = (function() {
                var type = schema.itemType;

                //Default to Text
                if (!type) return editors.Text;

                //Use List-specific version if available
                if (editors.List[type]) return editors.List[type];

                //Or whichever was passed
                return editors[type];
            })();

            this.items = [];
        },

        render: function() {
            var self = this,
            value = this.value || [];

            if (this.$el.find('[data-items]').length === 0) {
                //Create main element for initialization
                var $el = $($.trim(this.template()));
            } else {
                //Use existing element if initialized.
                var $el = this.$el;
            }

            //Store a reference to the list (item container)
            this.$list = $el.is('[data-items]') ? $el : $el.find('[data-items]');

            //Add existing items, if value is array
            if (value.length) {
                numExistingItems = self.items.length;
                _.each(value, function(itemValue) {
                    self.addItem(itemValue);
                });
                for (i=0; i<numExistingItems; i++) {
                    self.removeItem(self.items[0]);
                }
            }

            //Add existing items, if value is object
            else if (Object.keys(value).length) {
                numExistingItems = self.items.length;
                _.each(value, function(itemValue, itemKey) {
                    self.addItem([itemKey, itemValue]);
                });
                for (i=0; i<numExistingItems; i++) {
                    self.removeItem(self.items[0]);
                }
            }

            //If no existing items create an empty one, unless the editor
            //specifies otherwise
            else {
                if (!this.Editor.isAsync) this.addItem();
            }

            this.setElement($el);
            this.$el.attr('id', this.id);
            this.$el.attr('name', this.key);

            if (this.hasFocus) this.trigger('blur', this);

            return this;
        },

        /**
         * Add a new item to the list
         * @param {Mixed} [value]           Value for the new item editor
         * @param {Boolean} [userInitiated] If the item was added by the user clicking 'add'
         */
        addItem: function(value, userInitiated) {
            var self = this,
            editors = Form.editors;

            //Create the item
            var item = new editors.List.Item({
                list: this,
                form: this.form,
                schema: this.schema,
                value: value,
                Editor: this.Editor,
                key: this.key
            }).render();

            var _addItem = function() {
                self.items.push(item);
                self.$list.append(item.el);
                if (userInitiated) {
                    $(item.el).hide();
                    $(item.el).show('slow');
                }

                item.editor.on('all', function(event) {
                    if (event === 'change') return;

                    // args = ["key:change", itemEditor, fieldEditor]
                    var args = _.toArray(arguments);
                    args[0] = 'item:' + event;
                    args.splice(1, 0, self);
                    // args = ["item:key:change", this=listEditor, itemEditor, fieldEditor]

                    editors.List.prototype.trigger.apply(this, args);
                }, self);

                item.editor.on('change', function() {
                    if (!item.addEventTriggered) {
                        item.addEventTriggered = true;
                        this.trigger('add', this, item.editor);
                    }
                    this.trigger('item:change', this, item.editor);
                    this.trigger('change', this);
                }, self);

                item.editor.on('focus', function() {
                    if (this.hasFocus) return;
                    this.trigger('focus', this);
                }, self);

                item.editor.on('blur', function() {
                    if (!this.hasFocus) return;
                    var self = this;
                    setTimeout(function() {
                        if (_.find(self.items, function(item) { return item.editor.hasFocus; })) return;
                        self.trigger('blur', self);
                    }, 0);
                }, self);

                if (userInitiated || value) {
                    item.addEventTriggered = true;
                }

                if (userInitiated) {
                    self.trigger('add', self, item.editor);
                    self.trigger('change', self);
                }
            };

            //Check if we need to wait for the item to complete before adding to the list
            if (this.Editor.isAsync) {
                item.editor.on('readyToAdd', _addItem, this);
            }

            //Most editors can be added automatically
            else {
                _addItem();
                item.editor.focus();
            }

            return item;
        },

        /**
         * Remove an item from the list
         * @param {List.Item} item
         */
        removeItem: function(item) {
            //Confirm delete
            var confirmMsg = this.schema.confirmDelete;
            if (confirmMsg && !confirm(confirmMsg)) return;

            var index = _.indexOf(this.items, item);

            if (this.items.length > 1) {
                $(this.items[index].$el).hide('slow', function() {});
            } else {
                // Do not remove the last editor in the list.
                return;
            }
            this.items.splice(index, 1);

            if (item.addEventTriggered) {
                this.trigger('remove', this, item.editor);
                this.trigger('change', this);
            }

            if (!this.items.length && !this.Editor.isAsync) this.addItem();
        },

        getValue: function() {
            var values = _.map(this.items, function(item) {
                return item.getValue();
            });

            //Filter empty items
            return _.without(values, undefined, '');
        },

        setValue: function(value) {
            this.value = value;
            this.render();
        },

        focus: function() {
            if (this.hasFocus) return;

            if (this.items[0]) this.items[0].editor.focus();
        },

        blur: function() {
            if (!this.hasFocus) return;

            var focusedItem = _.find(this.items, function(item) { return item.editor.hasFocus; });

            if (focusedItem) focusedItem.editor.blur();
        },

        /**
         * Override default remove function in order to remove item views
         */
        remove: function() {
            _.invoke(this.items, 'remove');

            Form.editors.Base.prototype.remove.call(this);
        },

        /**
         * Run validation
         * 
         * @return {Object|Null}
         */
        validate: function() {
            if (!this.validators) return null;

            //Collect errors
            var errors = _.map(this.items, function(item) {
                return item.validate();
            });

            //Check if any item has errors
            var hasErrors = _.compact(errors).length ? true : false;
            if (!hasErrors) return null;

            //If so create a shared error
            var fieldError = {
                    type: 'list',
                    message: 'Some of the items in the list failed validation',
                    errors: errors
            };

            return fieldError;
        }
    }, {

        //STATICS
        template: _.template('\
                <div class="row">\
                <div class="col-sm-10" data-items></div>\
                <div class="col-sm-2">\
                <button type="button" class="btn btn-default" style="width: 100%;" data-action="add">\
                <span class="glyphicon glyphicon-plus"></span>\
                </button>\
                </div>\
                </div>\
                ', null, Form.templateSettings)

    });


    /**
     * Similar to list, but return a dictionary upon getValue.
     */
    Form.editors.DictList = Form.editors.List.extend({
        getValue: function() {
            var values = _.map(this.items, function(item) {
                return item.getValue();
            });
            values = _.without(values, undefined, '');

            //Filter empty items
            return _.object(values);
        }
    });


    /**
     * A single item in the list
     *
     * @param {editors.List} options.list The List editor instance this item belongs to
     * @param {Function} options.Editor   Editor constructor function
     * @param {String} options.key        Model key
     * @param {Mixed} options.value       Value
     * @param {Object} options.schema     Field schema
     */
    Form.editors.List.Item = Form.editors.Base.extend({

        events: {
            'click [data-action="remove"]': function(event) {
                event.preventDefault();
                this.list.removeItem(this);
            },
            'keydown input[type=text]': function(event) {
                if(event.keyCode !== 13) return;
                event.preventDefault();
                this.list.addItem();
                this.list.$list.find("> li:last input").focus();
            }
        },

        initialize: function(options) {
            this.list = options.list;
            this.schema = options.schema || this.list.schema;
            this.value = options.value;
            this.Editor = options.Editor || Form.editors.Text;
            this.key = options.key;
            this.template = options.template || this.schema.itemTemplate || this.constructor.template;
            this.errorClassName = options.errorClassName || this.constructor.errorClassName;
            this.form = options.form;
        },

        render: function() {
            //Create editor
            this.editor = new this.Editor({
                key: this.key,
                schema: this.schema,
                value: this.value,
                list: this.list,
                item: this,
                form: this.form
            }).render();

            //Create main element
            var $el = $($.trim(this.template()));

            $el.find('[data-editor]').append(this.editor.el);

            //Replace the entire element so there isn't a wrapper tag
            this.setElement($el);

            return this;
        },

        getValue: function() {
            return this.editor.getValue();
        },

        setValue: function(value) {
            this.editor.setValue(value);
        },

        focus: function() {
            this.editor.focus();
        },

        blur: function() {
            this.editor.blur();
        },

        remove: function() {
            this.editor.remove();

            Backbone.View.prototype.remove.call(this);
        },

        validate: function() {
            var value = this.getValue(),
            formValues = this.list.form ? this.list.form.getValue() : {},
                    validators = this.schema.validators,
//                  getValidator = this.getValidator;
                    getValidator = new Backbone.Form.Editor().getValidator;
            if (!validators) return null;

            //Run through validators until an error is found
            var error = null;
            _.every(validators, function(validator) {
                error = getValidator(validator)(value, formValues);

                return error ? false : true;
            });

            //Show/hide error
            if (error){
                this.setError(error);
            } else {
                this.clearError();
            }

            //Return error to be aggregated by list
            return error ? error : null;
        },

        /**
         * Show a validation error
         */
        setError: function(err) {
            this.$el.addClass(this.errorClassName);
            this.$el.attr('title', err.message);
        },

        /**
         * Hide validation errors
         */
        clearError: function() {
            this.$el.removeClass(this.errorClassName);
            this.$el.attr('title', null);
        }
    }, {

        //STATICS
        template: _.template('\
                <div class="row" style="margin-bottom: 15px">\
                <div class="col-sm-10" data-editor></div>\
                <div class="col-sm-2">\
                <button type="button" class="btn btn-default" style="width: 100%;" data-action="remove">-</button>\
                </div>\
                </div>\
                ', null, Form.templateSettings),

                errorClassName: 'error'

    });


    /**
     * Base modal object editor for use with the List editor; used by Object 
     * and NestedModal list types
     */
    Form.editors.List.Modal = Form.editors.Base.extend({

        events: {
            'click': 'openEditor'
        },

        /**
         * @param {Object} options
         * @param {Form} options.form                       The main form
         * @param {Function} [options.schema.itemToString]  Function to transform the value for display in the list.
         * @param {String} [options.schema.itemType]        Editor type e.g. 'Text', 'Object'.
         * @param {Object} [options.schema.subSchema]       Schema for nested form,. Required when itemType is 'Object'
         * @param {Function} [options.schema.model]         Model constructor function. Required when itemType is 'NestedModel'
         */
        initialize: function(options) {
            options = options || {};

            Form.editors.Base.prototype.initialize.call(this, options);

            //Dependencies
            if (!Form.editors.List.Modal.ModalAdapter) throw 'A ModalAdapter is required';

            this.form = options.form;
            if (!options.form) throw 'Missing required option: "form"';

            //Template
            this.template = options.template || this.constructor.template;
        },

        /**
         * Render the list item representation
         */
        render: function() {
            var self = this;

            //New items in the list are only rendered when the editor has been OK'd
            if (_.isEmpty(this.value)) {
                this.openEditor();
            }

            //But items with values are added automatically
            else {
                this.renderSummary();

                setTimeout(function() {
                    self.trigger('readyToAdd');
                }, 0);
            }

            if (this.hasFocus) this.trigger('blur', this);

            return this;
        },

        /**
         * Renders the list item representation
         */
        renderSummary: function() {
            this.$el.html($.trim(this.template({
                summary: this.getStringValue()
            })));
        },

        /**
         * Function which returns a generic string representation of an object
         *
         * @param {Object} value
         * 
         * @return {String}
         */
        itemToString: function(value) {
            var createTitle = function(key) {
                var context = { key: key };

                return Form.Field.prototype.createTitle.call(context);
            };

            value = value || {};

            //Pretty print the object keys and values
            var parts = [];
            _.each(this.nestedSchema, function(schema, key) {
                var desc = schema.title ? schema.title : createTitle(key),
                        val = value[key];

                if (_.isUndefined(val) || _.isNull(val)) val = '';

                parts.push(desc + ': ' + val);
            });

            return parts.join('<br />');
        },

        /**
         * Returns the string representation of the object value
         */
        getStringValue: function() {
            var schema = this.schema,
            value = this.getValue();

            if (_.isEmpty(value)) return '[Empty]';

            //If there's a specified toString use that
            if (schema.itemToString) return schema.itemToString(value);

            //Otherwise use the generic method or custom overridden method
            return this.itemToString(value);
        },

        openEditor: function() {
            var self = this,
            ModalForm = this.form.constructor;

            var form = this.modalForm = new ModalForm({
                schema: this.nestedSchema,
                data: this.value
            });

            var modal = this.modal = new Form.editors.List.Modal.ModalAdapter({
                content: form,
                animate: true
            });

            modal.open();

            this.trigger('open', this);
            this.trigger('focus', this);

            modal.on('cancel', this.onModalClosed, this);

            modal.on('ok', _.bind(this.onModalSubmitted, this));
        },

        /**
         * Called when the user clicks 'OK'.
         * Runs validation and tells the list when ready to add the item
         */
        onModalSubmitted: function() {
            var modal = this.modal,
            form = this.modalForm,
            isNew = !this.value;

            //Stop if there are validation errors
            var error = form.validate();
            if (error) return modal.preventClose();

            //Store form value
            this.value = form.getValue();

            //Render item
            this.renderSummary();

            if (isNew) this.trigger('readyToAdd');

            this.trigger('change', this);

            this.onModalClosed();
        },

        /**
         * Cleans up references, triggers events. To be called whenever the modal closes
         */
        onModalClosed: function() {
            this.modal = null;
            this.modalForm = null;

            this.trigger('close', this);
            this.trigger('blur', this);
        },

        getValue: function() {
            return this.value;
        },

        setValue: function(value) {
            this.value = value;
        },

        focus: function() {
            if (this.hasFocus) return;

            this.openEditor();
        },

        blur: function() {
            if (!this.hasFocus) return;

            if (this.modal) {
                this.modal.trigger('cancel');
            }
        }
    }, {
        //STATICS
        template: _.template('\
                <div><%= summary %></div>\
                ', null, Form.templateSettings),

                //The modal adapter that creates and manages the modal dialog.
                //Defaults to BootstrapModal (http://github.com/powmedia/backbone.bootstrap-modal)
                //Can be replaced with another adapter that implements the same interface.
                ModalAdapter: Backbone.BootstrapModal,

                //Make the wait list for the 'ready' event before adding the item to the list
                isAsync: true
    });


    Form.editors.List.Object = Form.editors.List.Modal.extend({
        initialize: function () {
            Form.editors.List.Modal.prototype.initialize.apply(this, arguments);

            var schema = this.schema;

            if (!schema.subSchema) throw 'Missing required option "schema.subSchema"';

            this.nestedSchema = schema.subSchema;
        }
    });


    Form.editors.List.NestedModel = Form.editors.List.Modal.extend({
        initialize: function() {
            Form.editors.List.Modal.prototype.initialize.apply(this, arguments);

            var schema = this.schema;

            if (!schema.model) throw 'Missing required option "schema.model"';

            var nestedSchema = schema.model.prototype.schema;

            this.nestedSchema = (_.isFunction(nestedSchema)) ? nestedSchema() : nestedSchema;
        },

        /**
         * Returns the string representation of the object value
         */
        getStringValue: function() {
            var schema = this.schema,
            value = this.getValue();

            if (_.isEmpty(value)) return null;

            //If there's a specified toString use that
            if (schema.itemToString) return schema.itemToString(value);

            //Otherwise use the model
            return new (schema.model)(value).toString();
        }
    });

})(Backbone.Form);
