/**
 * Model base object
 */
Xteam.Model = function(fields) {
    this.fields = fields;
    this.owners = [];

    // action queue
    this.queue_running = false;
    this.queued_actions = [];
};

Xteam.Model.prototype = {
    /**
     * Queue an action to be run ASAP
     * @param {function} action
     */
    queueAction: function(action) {
        // NOTE: these actions are assumed to be sequential
        this.queued_actions.push(action);
        this.executeQueuedActions();
    },

    /**
     * Attempt to run the action queue
     */
    executeQueuedActions: function() {
        // nothing in the queue
        if (!this.queued_actions.length) {
            return;
        }

        // queue is already running, so we'll have to wait our turn
        if (this.queue_running) {
            return;
        }

        this.queue_running = true;
        var nextAction = this.queued_actions.shift();
        nextAction();
        this.queue_running = false;

        // recurse
        var self = this;
        window.setTimeout(function() {
                              self.executeQueuedActions();
                          }, 1);
    },

    /**
     * Tell the model that someone owns it
     * @param {object} owner
     * @param {string} name Name by which the owner knows the model
     */
    addOwner: function(callback, name) {
        var self = this;
        var action = function() {
            self.owners.push({callback: callback,
                              name: name || ''});
        };
        this.queueAction(action);
    },

    /**
     * Tell the model's owners that changes have occurred
     * @param {string} event
     * @param {hash} changes
     */
    notifyOwners: function(event, changes) {
        var newOwners = [];
        var i = this.owners.length;
        var keepOwner;
        while (--i >= 0) {
            keepOwner = this.notifyOwner(event, changes, this.owners[i]);
            if (keepOwner !== false) {
                newOwners.push(this.owners[i]);
            }
        }
        this.owners = newOwners;
    },

    /**
     * Notify a single owner
     * @param {string} event
     * @param {hash} changes
     * @param {hash} owner
     * @return {boolean} Keep
     */
    notifyOwner: function(event, changes, owner) {
        var callback = owner.callback;
        var name = owner.name;

        // ----
        // function callback

        if (typeof callback === 'function') {
            return callback(event, changes);
        }

        // ----
        // object callback

        // if the model is named, we'll prefix the callback
        var funcPrefix = name ? name + '_' : '';
        var func;

        // broadcast bulk change
        if (event == 'changed') {
            func = funcPrefix + 'set';
            if (callback[func]) {
                callback[func](changes);
                return true;
            }
        }

        // broadcast individual changes
        for (var key in changes) {
            func = [funcPrefix, 'event_', event, '_', key].join('');
            if (callback[func]) {
                callback[func](changes[key]);
            }
        }

        return true;
    },

    /**
     * Update the model's data
     * @param {hash} data
     */
    update: function(data) {
        var self = this;
        var action = function() {
            var changes = {};
            var hasChanged = false;

            for (var key in data) {
                if (self.fields[key] !== data[key]) {
                    changes[key] = data[key];

                    self.fields[key] = data[key];
                    hasChanged = true;
                }
            }

            // notify if any changes have occurred
            if (hasChanged) {
                var action = function() {
                    self.notifyOwners('changed', changes);
                };
                self.queueAction(action);
            }
        };
        this.queueAction(action);
    },

    /**
     * Get a copy of a field's value
     * @param {string} key
     * @return {mixed}
     */
    field: function(key) {
        var data = this.fields[key];
        if (!data) {
            return data;
        }

        var type = typeof data;
        if (type === 'string' || type == 'number') {
            return data;
        }
        else if (data.length) {
            return $.extend([], data);
        }
        else if (type === 'object') {
            return $.extend({}, data);
        }
        else {
            return data;
        }
    }
};

// ----

/**
 * View base object
 * @param {Model} model
 */
Xteam.View = function(model) {
    if (model) {
        this.setModel(model);
    }
};

Xteam.View.prototype = {
    /**
     * Set the model that this view will use
     * @param {Model} model
     */
    setModel: function(model) {
        this.model = model;
        model.addOwner(this);
    },

    /**
     * Setup
     */
    setup: function() {
        // make sure setup only gets called once
        this.setup = function() { };
    },

    /**
     * Apply data to the view
     * @param {hash} data
     */
    set: function(data) {
        var undefined;

        var func;
        for (var key in data) {
            func = 'event_changed_'+key;
            if (this[func]) {
                this[func](data[key]);
            }
        }
    }
};

// ----

/**
 * View that is linked to a DOM element
 */
Xteam.DomView = function(model) {
    this._elms = {};

    // base view constructor
    Xteam.View.apply(this, [model]);
};

Xteam.DomView.prototype = $.extend(
    {},
    Xteam.View.prototype,
    {
        /**
         * Setup the view
         */
        setup: function() {
            Xteam.View.prototype.setup.apply(this);

            var base = this.findBaseElm();
            this._elms = this.nameElms(base);
            this._elms.base = base;

            this.setupEvents();

            // apply the model's data to the view
            this.set(this.model.fields);
        },

        /**
         * Get a DOM element of the view
         * @param {string} name Name of the element
         * @return {DOMelement}
         */
        elm: function(name) {
            // get the base element if nothing is specified
            name = name || 'base';

            // setup
            this.setup();

            // lazy-find
            if (typeof (this._elms[name]) === 'string') {
                // special case: if a part uses the keyword 'base',
                // use the base element
                if (this._elms[name] === 'base') {
                    this._elms[name] = this._elms.base;
                }
                // find by css selector
                else {
                    this._elms[name] = this._elms.base.find(this._elms[name]);
                }
            }

            return this._elms[name];
        },

        /**
         * Find the base element
         * @return {DOMelement}
         */
        findBaseElm: function() {
            console.error("Xteam.DomView.findBaseElm must be overridden");
        },

        /**
         * Name the elements
         * @param {DOMelement} base
         * @return {hash}
         */
        nameElms: function(base) {
            return {};
        },

        /**
         * Setup DOM event handlers for the view
         * @return {hash}
         */
        setupEvents: function() {
            // ignore
        }
    });

// ----

/**
 * Represents a group of objects
 * @param {array} items Array of models
 */
Xteam.GroupModel = function(data) {
    var fields = {selected: null};
    if (data.length) {
        fields.items = data || [];
    } else {
        fields = data;
        fields.items = fields.items || [];
    }

    Xteam.Model.apply(this, [fields]);
};
Xteam.GroupModel.prototype = $.extend(
    {},
    Xteam.Model.prototype,
    {
        /**
         * Pluck a value from each item in the group
         * @param {string} fieldname
         * @return {array}
         */
        pluckField: function(fieldname) {
            var pluckFunc = function(item) {
                return item.fields[fieldname];
            };
            return $.map(this.fields.items, pluckFunc);
        },

        itemsEqual: function(item1, item2) {
            return item1 === item2;
        },

        addItem: function(item) {
            this.fields.items.push(item);
            this.notifyOwners('added', {item: item});
        },

        removeItem: function(item) {
            var self = this;
            var filterFunc = function(i) {
                if (self.itemsEqual(i, item)) {
                    self.notifyOwners('removed', {item: item});
                    return false;
                }
                else {
                    return true;
                }
            };
            this.fields.items = $.grep(this.field('items'), filterFunc);
        }
    });

// ----

/**
 * Base group view
 * @param {GroupModel} model
 * @param {function} viewConstructor Constructor used for making views
 * @param {string} name Name of the group
 */
Xteam.GroupView = function(model) {
    Xteam.DomView.apply(this, [model]);
};
Xteam.GroupView.prototype = $.extend(
    {},
    Xteam.DomView.prototype,
    {
        /**
         * Setup the view
         */
        setup: function() {
            Xteam.DomView.prototype.setup.apply(this);

            // add items to the view
            var self = this;
            var addFunc = function(item) {
                self.event_added_item(item);
            };
            this.model.fields.items.forEach(addFunc);
        },

        addItem: function(data) {
            this.model.addItem(new Model(data));
        },

        removeItem: function(view) {
            this.model.removeItem(view.model);
        },

        // ----
        // model event handlers

        /**
         * Item was added
         * @param {Model} item
         */
        event_added_item: function(item) {
            this.model.notifyOwners('changed', {items: this.model.fields.items});
        },

        /**
         * Item was removed
         * @param {Model} item
         */
        event_removed_item: function(item) {
            this.model.notifyOwners('changed', {items: this.model.fields.items});
        }
    });
