/*

  Agility.js    
  Licensed under the MIT license
  Copyright (c) Artur B. Adib, 2011
  http://agilityjs.com

*/

// Sandboxed, so kids don't get hurt. Inspired by jQuery's code:
//   Creates local ref to window for performance reasons (as JS looks up local vars first)
//   Redefines undefined as it could have been tampered with
(function(window, undefined){

  if (!window.jQuery) {
    throw "agility.js: jQuery not found";
  }
  
  // Local references
  var document = window.document,
      location = window.location,
  
  // In case $ is being used by another lib
  $ = jQuery,

  // Main agility object builder
  agility,

  // Internal utility functions
  util = {},
  
  // Default object prototype
  defaultPrototype = {},
  
  // Global object counter
  idCounter = 0,
  
  // Constant
  ROOT_SELECTOR = '&';

  //////////////////////////////////////////////////////////////////////////
  //
  //  Modernizing old JS
  //

  // Modified from Douglas Crockford's Object.create()
  // The condition below ensures we override other manual implementations (most are not adequate)
  if (!Object.create || Object.create.toString().search(/native code/i)<0) {
    Object.create = function(obj){
      var Aux = function(){};
      $.extend(Aux.prototype, obj); // simply setting Aux.prototype = obj somehow messes with constructor, so getPrototypeOf wouldn't work in IE
      return new Aux();
    };
  }
  
  // Modified from John Resig's Object.getPrototypeOf()
  // The condition below ensures we override other manual implementations (most are not adequate)
  if (!Object.getPrototypeOf || Object.getPrototypeOf.toString().search(/native code/i)<0) {
    if ( typeof "test".__proto__ === "object" ) {
      Object.getPrototypeOf = function(object){
        return object.__proto__;
      };
    } else {
      Object.getPrototypeOf = function(object){
        // May break if the constructor has been tampered with
        return object.constructor.prototype;
      };
    }
  }  


  //////////////////////////////////////////////////////////////////////////
  //
  //  util.*
  //
  
  // Checks if provided obj is an agility object
  util.isAgility = function(obj){
   return obj._agility === true;
  };
  
  // Scans object for functions (depth=2) and proxies their 'this' to dest.
  // * To ensure it works with previously proxied objects, we save the original function as 
  //   a '._preProxy' method and when available always use that as the proxy source.
  // * To skip a given method, create a sub-method called '_noProxy'.
  util.proxyAll = function(obj, dest){
    if (!obj || !dest) {
      throw "agility.js: util.proxyAll needs two arguments";
    }
    for (var attr1 in obj) {
      var proxied = obj[attr1];
      // Proxy root methods
      if (typeof obj[attr1] === 'function') {
        proxied = obj[attr1]._noProxy ? obj[attr1] : $.proxy(obj[attr1]._preProxy || obj[attr1], dest);
        proxied._preProxy = obj[attr1]._noProxy ? undefined : (obj[attr1]._preProxy || obj[attr1]); // save original
        obj[attr1] = proxied;
      }
      // Proxy sub-methods (model.*, view.*, etc)
      else if (typeof obj[attr1] === 'object') {
        for (var attr2 in obj[attr1]) {
          var proxied2 = obj[attr1][attr2];
          if (typeof obj[attr1][attr2] === 'function') {
            proxied2 = obj[attr1][attr2]._noProxy ? obj[attr1][attr2] : $.proxy(obj[attr1][attr2]._preProxy || obj[attr1][attr2], dest);
            proxied2._preProxy = obj[attr1][attr2]._noProxy ? undefined : (obj[attr1][attr2]._preProxy || obj[attr1][attr2]); // save original
            proxied[attr2] = proxied2;
          }
        } // for attr2
        obj[attr1] = proxied;
      } // if not func
    } // for attr1
  }; // proxyAll
  
  // Reverses the order of events attached to an object
  util.reverseEvents = function(obj, eventType){
    var events = $(obj).data('events');
    if (events !== undefined && events[eventType] !== undefined){
      // can't reverse what's not there
      var reverseEvents = [];
      for (var e in events[eventType]){
        if (!events[eventType].hasOwnProperty(e)) continue;
        reverseEvents.unshift(events[eventType][e]);
      }
      events[eventType] = reverseEvents;
    }
  }; //reverseEvents
  
  // Determines # of attributes of given object (prototype inclusive)
  util.size = function(obj){
    var size = 0, key;
    for (key in obj) {
      size++;
    }
    return size;
  };
  
  // Find controllers to be extended (with syntax '~'), redefine those to encompass previously defined controllers
  // Example:
  //   var a = $$({}, '<button>A</button>', {'click &': function(){ alert('A'); }});
  //   var b = $$(a, {}, '<button>B</button>', {'~click &': function(){ alert('B'); }});
  // Clicking on button B will alert both 'A' and 'B'.
  util.extendController = function(object) {
    for (var controllerName in object.controller) {
      (function(){ // new scope as we need one new function handler per controller
        var matches, extend, eventName,
            previousHandler, currentHandler, newHandler;

        if (typeof object.controller[controllerName] === 'function') {
          matches = controllerName.match(/^(\~)*(.+)/); // 'click button', '~click button', '_create', etc
          extend = matches[1];
          eventName = matches[2];
        
          if (!extend) return; // nothing to do

          // Redefine controller:
          // '~click button' ---> 'click button' = previousHandler + currentHandler
          previousHandler = object.controller[eventName] ? (object.controller[eventName]._preProxy || object.controller[eventName]) : undefined;
          currentHandler = object.controller[controllerName];
          newHandler = function() {
            if (previousHandler) previousHandler.apply(this, arguments);
            if (currentHandler) currentHandler.apply(this, arguments);
          };

          object.controller[eventName] = newHandler;
          delete object.controller[controllerName]; // delete '~click button'
        } // if function
      })();
    } // for controllerName
  };
  
  //////////////////////////////////////////////////////////////////////////
  //
  //  Default object prototype
  //
  
  defaultPrototype = {
    
    _agility: true,
    
    //////////////////////////////////////////////////////////////////////////
    //
    //  _container
    //
    //    API and related auxiliary functions for storing child Agility objects.
    //    Not all methods are exposed. See 'shortcuts' below for exposed methods.
    //
    
    _container: {

      // Adds child object to container, appends/prepends/etc view, listens for child removal
      _insertObject: function(obj, selector, method){
        var self = this;
        if (!util.isAgility(obj)) {
          throw "agility.js: append argument is not an agility object";
        }
        this._container.children[obj._id] = obj; // children is *not* an array; this is for simpler lookups by global object id
        this.trigger(method, [obj, selector]);
        obj._parent = this;
        // ensures object is removed from container when destroyed:
        obj.bind('destroy', function(event, id){ 
          self._container.remove(id);
        });
        return this;
      },

      append: function(obj, selector) { 
          return this._container._insertObject.call(this, obj, selector, 'append'); 
      },

      prepend: function(obj, selector) { 
          return this._container._insertObject.call(this, obj, selector, 'prepend'); 
      },

      after: function(obj, selector) { 
          return this._container._insertObject.call(this, obj, selector, 'after'); 
      },

      before: function(obj, selector) { 
          return this._container._insertObject.call(this, obj, selector, 'before'); 
      },
      
      // Removes child object from container
      remove: function(id){
        delete this._container.children[id];
        this.trigger('remove', id);
        return this;
      },

      // Iterates over all child objects in container
      each: function(fn){
        $.each(this._container.children, fn);
        return this; // for chainable calls
      },
      
      // Removes all objects in container
      empty: function(){
        this.each(function(){
          this.destroy();
        });
        return this;
      },
      
      // Number of children
      size: function() {
        return util.size(this._container.children);
      }
      
    },

    //////////////////////////////////////////////////////////////////////////
    //
    //  _events
    //
    //    API and auxiliary functions for handling events. Not all methods are exposed.
    //    See 'shortcuts' below for exposed methods.
    //

    _events: {

      // Parses event string unlike:
      //    'event'          : custom event
      //    'event selector' : DOM event using 'selector'
      // Returns { type:'event' [, selector:'selector'] }
      parseEventStr: function(eventStr){
        var eventObj = { type:eventStr }, 
            spacePos = eventStr.search(/\s/);
        // DOM event 'event selector', e.g. 'click button'
        if (spacePos > -1) {
          eventObj.type = eventStr.substr(0, spacePos);
          eventObj.selector = eventStr.substr(spacePos+1);
        }
        return eventObj;
      },

      // Binds eventStr to fn. eventStr is parsed as per parseEventStr()
      bind: function(eventStr, fn){
        var eventObj = this._events.parseEventStr(eventStr);
        // DOM event 'event selector', e.g. 'click button'
        if (eventObj.selector) {
          // Manually override root selector, as jQuery selectors can't select self object
          if (eventObj.selector === ROOT_SELECTOR) {
            this.view.$().bind(eventObj.type, fn);
          }
          else {          
            this.view.$().delegate(eventObj.selector, eventObj.type, fn);
          }
        }
        // Custom event
        else {
          $(this._events.data).bind(eventObj.type, fn);
        }
        return this; // for chainable calls
      }, // bind

      // Triggers eventStr. Syntax for eventStr is same as that for bind()
      trigger: function(eventStr, params){
        var eventObj = this._events.parseEventStr(eventStr);
        // DOM event 'event selector', e.g. 'click button'
        if (eventObj.selector) {
          // Manually override root selector, as jQuery selectors can't select self object
          if (eventObj.selector === ROOT_SELECTOR) {
            this.view.$().trigger(eventObj.type, params);
          }
          else {          
            this.view.$().find(eventObj.selector).trigger(eventObj.type, params);
          }
        }
        // Custom event
        else {
          $(this._events.data).trigger('_'+eventObj.type, params);
          // fire 'pre' hooks in reverse attachment order ( last first )
          util.reverseEvents(this._events.data, 'pre:' + eventObj.type);
          $(this._events.data).trigger('pre:' + eventObj.type, params);
          // put the order of events back
          util.reverseEvents(this._events.data, 'pre:' + eventObj.type);
          $(this._events.data).trigger(eventObj.type, params);
          if(this.parent())
            this.parent().trigger((eventObj.type.match(/^child:/) ? '' : 'child:') + eventObj.type, params);
          $(this._events.data).trigger('post:' + eventObj.type, params);
        }
        return this; // for chainable calls
      } // trigger
      
    }, // _events

    //////////////////////////////////////////////////////////////////////////
    //
    //  Model
    //
    //    Main model API. All methods are exposed, but methods starting with '_'
    //    are meant to be used internally only.
    //
       
    model: {

      // Setter
      set: function(arg, params) {
        var self = this;
        var modified = []; // list of modified model attributes
        if (typeof arg === 'object') {
          var _clone = false;
          if (params && params.reset) {
            _clone = this.model._data; // hold on to data for change events
            this.model._data = $.extend({}, arg); // erases previous model attributes without pointing to object
          }
          else {
            $.extend(this.model._data, arg); // default is extend
          }
          for (var key in arg) {
            delete _clone[ key ]; // no need to fire change twice
            modified.push(key);
          }
          for (key in _clone) {
            modified.push(key);
          }
        }
        else {
          throw "agility.js: unknown argument type in model.set()";
        }

        // Events
        if (params && params.silent===true) return this; // do not fire events
        this.trigger('change');
        $.each(modified, function(index, val){
          self.trigger('change:'+val);
        });
        return this; // for chainable calls
      },
      
      // Getter
      get: function(arg){
        // Full model getter
        if (arg === undefined) {
          return this.model._data;
        }
        // Attribute getter
        if (typeof arg === 'string') {            
          return this.model._data[arg];
        }
        throw 'agility.js: unknown argument for getter';
      },
      
      // Resetter (to initial model upon object initialization)
      reset: function(){
        this.model.set(this.model._initData, {reset:true});
        return this; // for chainable calls
      },
      
      // Number of model properties
      size: function(){
        return util.size(this.model._data);
      },
      
      // Convenience function - loops over each model property
      each: function(fn){
        $.each(this.model._data, fn);
        return this; // for chainable calls
      }
      
    }, // model prototype
  
    //////////////////////////////////////////////////////////////////////////
    //
    //  View
    //
    //    Main view API. All methods are exposed, but methods starting with '_'
    //    are meant to be used internally only.
    //
  
    view: {
        
      // Defaults
      format: '<div/>',
      style: '',
      
      // Shortcut to view.$root or view.$root.find(), depending on selector presence
      $: function(selector){
        return (!selector || selector === ROOT_SELECTOR) ? this.view.$root : this.view.$root.find(selector);
      },
      
      // Render $root
      // Only function to access $root directly other than $()
      render: function(){
        // Without format there is no view
        if (this.view.format.length === 0) {
          throw "agility.js: empty format in view.render()";
        }                
        if (this.view.$root.size() === 0) {
          this.view.$root = $(this.view.format);
        }
        else {
          this.view.$root.html( $(this.view.format).html() ); // can't overwrite $root as this would reset its presence in the DOM and all events already bound, and 
        }
        // Ensure we have a valid (non-empty) $root
        if (this.view.$root.size() === 0) {
          throw 'agility.js: could not generate html from format';
        }
        return this;
      }, // render
  
      // Parse data-bind string of the type '[attribute][=] variable[, [attribute][=] variable ]...'
      // If the variable is not an attribute, it must occur by itself
      //   all pairs in the list are assumed to be attributes
      // Returns { key:'model key', attr: [ {attr : 'attribute', attrVar : 'variable' }... ] }
      _parseBindStr: function(str){
        var obj = {key:null, attr:[]},
            pairs = str.split(','),
            regex = /([a-zA-Z0-9_\-]+)(?:[\s=]+([a-zA-Z0-9_\-]+))?/,
            keyAssigned = false,
            matched;
        
        if (pairs.length > 0) {
          for (var i = 0; i < pairs.length; i++) {
            matched = pairs[i].match(regex);
            // [ "attribute variable", "attribute", "variable" ]
            // or [ "attribute=variable", "attribute", "variable" ]
            // or
            // [ "variable", "variable", undefined ]
            // in some IE it will be [ "variable", "variable", "" ]
            // or
            // null
            if (matched) {
              if (typeof(matched[2]) === "undefined" || matched[2] === "") {
                if (keyAssigned) {
                  throw new Error("You may specify only one key (" + 
                    keyAssigned + " has already been specified in data-bind=" + 
                    str + ")");
                } else {
                  keyAssigned = matched[1];
                  obj.key = matched[1];
                }
              } else {
                obj.attr.push({attr: matched[1], attrVar: matched[2]});
              }
            } // if (matched)
          } // for (pairs.length)
        } // if (pairs.length > 0)
        
        return obj;
      },
      
      // Apply two-way (DOM <--> Model) bindings to elements with 'data-bind' attributes
      bindings: function(){
        var self = this;
        var $rootNode = this.view.$().filter('[data-bind]');
        var $childNodes = this.view.$('[data-bind]');
        var createAttributePairClosure = function(bindData, node, i) {
          var attrPair = bindData.attr[i]; // capture the attribute pair in closure
          return function() {
            node.attr(attrPair.attr, self.model.get(attrPair.attrVar));
          };
        };
        $rootNode.add($childNodes).each(function(){
          var $node = $(this);
          var bindData = self.view._parseBindStr( $node.data('bind') );

          var bindAttributesOneWay = function() {
            // 1-way attribute binding
            if (bindData.attr) {
              for (var i = 0; i < bindData.attr.length; i++) {
                self.bind('_change:'+bindData.attr[i].attrVar,
                  createAttributePairClosure(bindData, $node, i));
              } // for (bindData.attr)
            } // if (bindData.attr)
          }; // bindAttributesOneWay()
          
          // <input type="checkbox">: 2-way binding
          if ($node.is('input:checkbox')) {
            // Model --> DOM
            self.bind('_change:'+bindData.key, function(){
              $node.prop("checked", self.model.get(bindData.key)); // this won't fire a DOM 'change' event, saving us from an infinite event loop (Model <--> DOM)
            });            
            // DOM --> Model
            $node.change(function(){
              var obj = {};
              obj[bindData.key] = $(this).prop("checked");
              self.model.set(obj); // not silent as user might be listening to change events
            });
            // 1-way attribute binding
            bindAttributesOneWay();
          }
          
          // <select>: 2-way binding
          else if ($node.is('select')) {
            // Model --> DOM
            self.bind('_change:'+bindData.key, function(){
              var nodeName = $node.attr('name');
              var modelValue = self.model.get(bindData.key);
              $node.val(modelValue);
            });            
            // DOM --> Model
            $node.change(function(){
              var obj = {};
              obj[bindData.key] = $node.val();
              self.model.set(obj); // not silent as user might be listening to change events
            });
            // 1-way attribute binding
            bindAttributesOneWay();
          }
          
          // <input type="radio">: 2-way binding
          else if ($node.is('input:radio')) {
            // Model --> DOM
            self.bind('_change:'+bindData.key, function(){
              var nodeName = $node.attr('name');
              var modelValue = self.model.get(bindData.key);
              $node.siblings('input[name="'+nodeName+'"]').filter('[value="'+modelValue+'"]').prop("checked", true); // this won't fire a DOM 'change' event, saving us from an infinite event loop (Model <--> DOM)
            });            
            // DOM --> Model
            $node.change(function(){
              if (!$node.prop("checked")) return; // only handles check=true events
              var obj = {};
              obj[bindData.key] = $node.val();
              self.model.set(obj); // not silent as user might be listening to change events
            });
            // 1-way attribute binding
            bindAttributesOneWay();
          }
          
          // <input type="search"> (model is updated after every keypress event)
          else if ($node.is('input[type="search"]')) {
            // Model --> DOM
            self.bind('_change:'+bindData.key, function(){
              $node.val(self.model.get(bindData.key)); // this won't fire a DOM 'change' event, saving us from an infinite event loop (Model <--> DOM)
            });
            // Model <-- DOM
            $node.keypress(function(){
              // Without timeout $node.val() misses the last entered character
              setTimeout(function(){
                var obj = {};
                obj[bindData.key] = $node.val();
                self.model.set(obj); // not silent as user might be listening to change events
              }, 50);
            });
            // 1-way attribute binding
            bindAttributesOneWay();
          }

          // <input type="text">, <input>, and <textarea>: 2-way binding
          else if ($node.is('input:text, input[type!="search"], textarea')) {
            // Model --> DOM
            self.bind('_change:'+bindData.key, function(){
              $node.val(self.model.get(bindData.key)); // this won't fire a DOM 'change' event, saving us from an infinite event loop (Model <--> DOM)
            });            
            // Model <-- DOM
            $node.change(function(){
              var obj = {};
              obj[bindData.key] = $(this).val();
              self.model.set(obj); // not silent as user might be listening to change events
            });
            // 1-way attribute binding
            bindAttributesOneWay();
          }
          
          // all other <tag>s: 1-way binding
          else {
            if (bindData.key) {
              self.bind('_change:'+bindData.key, function(){
                var key = self.model.get(bindData.key);
                if (key || key===0) {
                  $node.text(self.model.get(bindData.key).toString());
                } else {
                  $node.text('');
                }
              });
            }
            bindAttributesOneWay();
          }
        }); // nodes.each()
        return this;
      }, // bindings()
      
      // Triggers _change and _change:* events so that view is updated as per view.bindings()
      sync: function(){
        var self = this;
        // Trigger change events so that view is updated according to model
        this.model.each(function(key, val){
          self.trigger('_change:'+key);
        });
        if (this.model.size() > 0) {
          this.trigger('_change');
        }
        return this;
      },

      // Applies style dynamically
      stylize: function(){
        var objClass,
            regex = new RegExp(ROOT_SELECTOR, 'g');
        if (this.view.style.length === 0 || this.view.$().size() === 0) {
          return;
        }
        // Own style
        // Object gets own class name ".agility_123", and <head> gets a corresponding <style>
        if (this.view.hasOwnProperty('style')) {
          objClass = 'agility_' + this._id;
          var styleStr = this.view.style.replace(regex, '.'+objClass);
          $('head', window.document).append('<style type="text/css">'+styleStr+'</style>');
          this.view.$().addClass(objClass);
        }
        // Inherited style
        // Object inherits CSS class name from first ancestor to have own view.style
        else {
          // Returns id of first ancestor to have 'own' view.style
          var ancestorWithStyle = function(object) {
            while (object !== null) {
              object = Object.getPrototypeOf(object);
              if (object.view.hasOwnProperty('style'))
                return object._id;
            }
            return undefined;
          }; // ancestorWithStyle

          var ancestorId = ancestorWithStyle(this);
          objClass = 'agility_' + ancestorId;
          this.view.$().addClass(objClass);
        }
        return this;
      }
      
    }, // view prototype
  
    //////////////////////////////////////////////////////////////////////////
    //
    //  Controller
    //
    //    Default controllers, i.e. event handlers. Event handlers that start
    //    with '_' are of internal use only, and take precedence over any other
    //    handler without that prefix. (See trigger()).
    //
   
    controller: {
  
      // Triggered after self creation
      _create: function(event){
        this.view.stylize();
        this.view.bindings(); // Model-View bindings
        this.view.sync(); // syncs View with Model
      },
  
      // Triggered upon removing self
      _destroy: function(event){
        // destroy any appended agility objects
        this._container.empty();
        // destroy self
        this.view.$().remove();
      },

      // Triggered after child obj is appended to container
      _append: function(event, obj, selector){
        this.view.$(selector).append(obj.view.$());
      },

      // Triggered after child obj is prepended to container
      _prepend: function(event, obj, selector){
        this.view.$(selector).prepend(obj.view.$());
      },

      // Triggered after child obj is inserted in the container
      _before: function(event, obj, selector){
        if (!selector) throw 'agility.js: _before needs a selector';
        this.view.$(selector).before(obj.view.$());
      },

      // Triggered after child obj is inserted in the container
      _after: function(event, obj, selector){
        if (!selector) throw 'agility.js: _after needs a selector';
        this.view.$(selector).after(obj.view.$());
      },

      // Triggered after a child obj is removed from container (or self-removed)
      _remove: function(event, id){        
      },

      // Triggered after model is changed
      '_change': function(event){
      }
      
    }, // controller prototype

    //////////////////////////////////////////////////////////////////////////
    //
    //  Shortcuts
    //
        
    //
    // Self
    //    
    destroy: function() {
      this.trigger('destroy', this._id); // parent must listen to 'remove' event and handle container removal!
      // can't return this as it might not exist anymore!
    },
    parent: function(){
      return this._parent;
    },
    
    //
    // _container shortcuts
    //
    append: function(){
      this._container.append.apply(this, arguments);
      return this; // for chainable calls
    },
    prepend: function(){
      this._container.prepend.apply(this, arguments);
      return this; // for chainable calls
    },
    after: function(){
      this._container.after.apply(this, arguments);
      return this; // for chainable calls
    },
    before: function(){
      this._container.before.apply(this, arguments);
      return this; // for chainable calls
    },
    remove: function(){
      this._container.remove.apply(this, arguments);
      return this; // for chainable calls
    },
    size: function(){
      return this._container.size.apply(this, arguments);
    },
    each: function(){
      return this._container.each.apply(this, arguments);
    },
    empty: function(){
      return this._container.empty.apply(this, arguments);
    },

    //
    // _events shortcuts
    //
    bind: function(){
      this._events.bind.apply(this, arguments);
      return this; // for chainable calls
    },
    trigger: function(){
      this._events.trigger.apply(this, arguments);
      return this; // for chainable calls
    }
      
  }; // prototype
  
  //////////////////////////////////////////////////////////////////////////
  //
  //  Main object builder
  //
  
  // Main agility object builder
  agility = function(){
    
    // Real array of arguments
    var args = Array.prototype.slice.call(arguments, 0),
    
    // Object to be returned by builder
    object = {},
    
    prototype = defaultPrototype;
            
    //////////////////////////////////////////////////////////////////////////
    //
    //  Define object prototype
    //

    // Inherit object prototype
    if (typeof args[0] === "object" && util.isAgility(args[0])) {
      prototype = args[0];    
      args.shift(); // remaining args now work as though object wasn't specified
    } // build from agility object
    
    // Build object from prototype as well as the individual prototype parts
    // This enables differential inheritance at the sub-object level, e.g. object.view.format
    object = Object.create(prototype);
    object.model = Object.create(prototype.model);
    object.view = Object.create(prototype.view);
    object.controller = Object.create(prototype.controller);
    object._container = Object.create(prototype._container);
    object._events = Object.create(prototype._events);

    // Fresh 'own' properties (i.e. properties that are not inherited at all)
    object._id = idCounter++;
    object._parent = null;
    object._events.data = {}; // event bindings will happen below
    object._container.children = {};
    object.view.$root = $(); // empty jQuery object

    // Cloned own properties (i.e. properties that are inherited by direct copy instead of by prototype chain)
    // This prevents children from altering parents models
    object.model._data = prototype.model._data ? $.extend(true, {}, prototype.model._data) : {};
    object._data = prototype._data ? $.extend(true, {}, prototype._data) : {};

    //////////////////////////////////////////////////////////////////////////
    //
    //  Extend model, view, controller
    //

    // Just the default prototype
    if (args.length === 0) {
    }
  
    // Prototype differential from single {model,view,controller} object
    else if (args.length === 1 && typeof args[0] === 'object' && (args[0].model || args[0].view || args[0].controller) ) {
      for (var prop in args[0]) {
        if (prop === 'model') {
          $.extend(object.model._data, args[0].model);
        }
        else if (prop === 'view') {
          $.extend(object.view, args[0].view);
        }
        else if (prop === 'controller') {
          $.extend(object.controller, args[0].controller);
          util.extendController(object);
        }
        // User-defined methods
        else {
          object[prop] = args[0][prop];
        }
      }
    } // {model, view, controller} arg
    
    // Prototype differential from separate {model}, {view}, {controller} arguments
    else {
      
      // Model from string
      if (typeof args[0] === 'object') {
        $.extend(object.model._data, args[0]);
      }
      else if (args[0]) {
        throw "agility.js: unknown argument type (model)";
      }

      // View format from shorthand string (..., '<div>whatever</div>', ...)
      if (typeof args[1] === 'string') {
        object.view.format = args[1]; // extend view with .format
      }  
      // View from object (..., {format:'<div>whatever</div>'}, ...)
      else if (typeof args[1] === 'object') {
        $.extend(object.view, args[1]);
      }      
      else if (args[1]) {
        throw "agility.js: unknown argument type (view)";
      }
      
      // View style from shorthand string (..., ..., 'p {color:red}', ...)
      if (typeof args[2] === 'string') {
        object.view.style = args[2];
        args.splice(2, 1); // so that controller code below works
      }
      
      // Controller from object (..., ..., {method:function(){}})
      if (typeof args[2] === 'object') {
        $.extend(object.controller, args[2]);
        util.extendController(object);
      }
      else if (args[2]) {
        throw "agility.js: unknown argument type (controller)";
      }
      
    } // ({model}, {view}, {controller}) args
    
    //////////////////////////////////////////////////////////////////////////
    //
    //  Bootstrap: Bindings, initializations, etc
    //
    
    // Save model's initial state (so it can be .reset() later)
    object.model._initData = $.extend({}, object.model._data);

    // object.* will have their 'this' === object. This should come before call to object.* below.
    util.proxyAll(object, object);

    // Initialize $root, needed for DOM events binding below
    object.view.render();
  
    // Bind all controllers to their events

    var bindEvent = function(ev, handler){
      if (typeof handler === 'function') {
        object.bind(ev, handler);
      }
    };

    for (var eventStr in object.controller) {
      var events = eventStr.split(';');
      var handler = object.controller[eventStr];
      $.each(events, function(i, ev){
        ev = $.trim(ev);
        bindEvent(ev, handler);
      });
    }


    // Auto-triggers create event
    object.trigger('create');    
    
    return object;
    
  }; // agility
  
  //////////////////////////////////////////////////////////////////////////
  //
  //  Global objects
  //
  
  // $$.document is a special Agility object, whose view is attached to <body>
  // This object is the main entry point for all DOM operations
  agility.document = agility({
    view: {
      $: function(selector){ return selector ? $(selector, 'body') : $('body'); }
    },
    controller: {
      // Override default controller
      // (don't render, don't stylize, etc)
      _create: function(){}
    }
  });
  
  // Shortcut to prototype for plugins
  agility.fn = defaultPrototype;
  
  // isAgility test
  agility.isAgility = function(obj) {
    if (typeof obj !== 'object') return false;
    return util.isAgility(obj);
  };

  // Globals
  window.agility = window.$$ = agility;

  //////////////////////////////////////////////////////////////////////////
  //
  //  Bundled plugin: persist
  //
  
  // Main initializer
  agility.fn.persist = function(adapter, params){
    var id = 'id'; // name of id attribute
        
    this._data.persist = $.extend({adapter:adapter}, params);
    this._data.persist.openRequests = 0;
    if (params && params.id) {
      id = params.id;
    }

    // Creates persist methods
    
    // .save()
    // Creates new model or update existing one, depending on whether model has 'id' property
    this.save = function(){
      var self = this;
      if (this._data.persist.openRequests === 0) {
        this.trigger('persist:start');
      }
      this._data.persist.openRequests++;
      this._data.persist.adapter.call(this, {
        type: this.model.get(id) ? 'PUT' : 'POST', // update vs. create
        id: this.model.get(id),
        data: this.model.get(),
        complete: function(){
          self._data.persist.openRequests--;
          if (self._data.persist.openRequests === 0) {
            self.trigger('persist:stop');
          }
        },
        success: function(data, textStatus, jqXHR){
          if (data[id]) {
            // id in body
            self.model.set({id:data[id]}, {silent:true});
          }
          else if (jqXHR.getResponseHeader('Location')) {
            // parse id from Location
            self.model.set({ id: jqXHR.getResponseHeader('Location').match(/\/([0-9]+)$/)[1] }, {silent:true});
          }
          self.trigger('persist:save:success');
        },
        error: function(){
          self.trigger('persist:error');
          self.trigger('persist:save:error');
        }
      });
      
      return this; // for chainable calls
    }; // save()
  
    // .load()
    // Loads model with given id
    this.load = function(){
      var self = this;
      if (this.model.get(id) === undefined) throw 'agility.js: load() needs model id';
    
      if (this._data.persist.openRequests === 0) {
        this.trigger('persist:start');
      }
      this._data.persist.openRequests++;
      this._data.persist.adapter.call(this, {
        type: 'GET',
        id: this.model.get(id),
        complete: function(){
          self._data.persist.openRequests--;
          if (self._data.persist.openRequests === 0) {
            self.trigger('persist:stop');
          }
        },
        success: function(data, textStatus, jqXHR){
          self.model.set(data);
          self.trigger('persist:load:success');
        },      
        error: function(){
          self.trigger('persist:error');
          self.trigger('persist:load:error');
        }
      });      

      return this; // for chainable calls
    }; // load()

    // .erase()
    // Erases model with given id
    this.erase = function(){
      var self = this;
      if (this.model.get(id) === undefined) throw 'agility.js: erase() needs model id';
    
      if (this._data.persist.openRequests === 0) {
        this.trigger('persist:start');
      }
      this._data.persist.openRequests++;
      this._data.persist.adapter.call(this, {
        type: 'DELETE',
        id: this.model.get(id),
        complete: function(){
          self._data.persist.openRequests--;
          if (self._data.persist.openRequests === 0) {
            self.trigger('persist:stop');
          }
        },
        success: function(data, textStatus, jqXHR){
          self.destroy();
          self.trigger('persist:erase:success');
        },      
        error: function(){
          self.trigger('persist:error');
          self.trigger('persist:erase:error');
        }
      });            

      return this; // for chainable calls
    }; // erase()

    // .gather()
    // Loads collection and appends/prepends (depending on method) at selector. All persistence data including adapter comes from proto, not self
    this.gather = function(proto, method, selectorOrQuery, query){      
      var selector, self = this;
      if (!proto) throw "agility.js plugin persist: gather() needs object prototype";
      if (!proto._data.persist) throw "agility.js plugin persist: prototype doesn't seem to contain persist() data";

      // Determines arguments
      if (query) {
        selector = selectorOrQuery;        
      }
      else {
        if (typeof selectorOrQuery === 'string') {
          selector = selectorOrQuery;
        }
        else {
          selector = undefined;
          query = selectorOrQuery;
        }
      }

      if (this._data.persist.openRequests === 0) {
        this.trigger('persist:start');
      }
      this._data.persist.openRequests++;
      proto._data.persist.adapter.call(proto, {
        type: 'GET',
        data: query,
        complete: function(){
          self._data.persist.openRequests--;
          if (self._data.persist.openRequests === 0) {
            self.trigger('persist:stop');
          }
        },
        success: function(data){
          $.each(data, function(index, entry){
            var obj = $$(proto, entry);
            if (typeof method === 'string') {
              self[method](obj, selector);
            }
          });
          self.trigger('persist:gather:success', {data:data});
        },
        error: function(){
          self.trigger('persist:error');
          self.trigger('persist:gather:error');
        }
      });
    
      return this; // for chainable calls
    }; // gather()
  
    return this; // for chainable calls
  }; // fn.persist()
  
  // Persistence adapters
  // These are functions. Required parameters:
  //    {type: 'GET' || 'POST' || 'PUT' || 'DELETE'}
  agility.adapter = {};

  // RESTful JSON adapter using jQuery's ajax()
  agility.adapter.restful = function(_params){
    var params = $.extend({
      dataType: 'json',
      url: (this._data.persist.baseUrl || 'api/') + this._data.persist.collection + (_params.id ? '/'+_params.id : '')
    }, _params);
    $.ajax(params);
  };
  
})(window);
