/*! Phalanx - v0.0.5 ( 2013-10-07 ) - MIT */
(function(window) {

"use strict";

var Trait = {};

/**
 * `defineClass` is Helper function, to generate Object like Class with basic oop fetures
 *
 *     // e.g 1
 *     var Klass = Phalanx.defineClass{
 *       constructor: function() {
 *         console.log('Hello World');
 *       },
 *       foo: 'bar',
 *       baz: 'qux'
 *     });
 *
 *     // e.g 2
 *     function NewClass() {
 *       console.log('This is constructor');
 *     }
 *     Phalanx.defineClass(NewClass, {
 *       hoge: 'fuga',
 *       hige: 'piyo'
 *     };
 *
 * @param {Function|Object} constructor_or_members
 * @param {Object} [members]
 */
function defineClass(constructor_or_members, members) {

  /**
   * Base Class of OOP feature
   *
   * @abstract
   * @class Klass
   * @returns {*}
   */
  var Constructor;

  if (typeof constructor_or_members === 'function') {
    Constructor = constructor_or_members;
  } else {
    members = constructor_or_members;
    Constructor = members.hasOwnProperty('constructor') ? members.constructor
                                                        : function() {};
  }

  delete members.constructor;
  _.extend(Constructor.prototype, members);

  /**
   * By inheriting an existing class, you create a new class
   *
   *     var classDefinition = {
   *       // ...
   *     };
   *     var ExtendedClass = SomeClass.extend(classDefinition)
   *
   * @method extend
   * @see Backbone.View.extend
   * @param {Function|Object} constructor_or_child
   * @param {Object} [child]
   * @return {Klass}
   */
  Constructor.extend = Backbone.View.extend;

  /**
   * Mixin the trait in the `prototype` of the class.
   * Is a feature that is for a simple mixin, not the exact trait.
   * Not support multiple inheritance like "Squeak Smalltalk".
   *
   *     var classDefinition = {
   *       // ...
   *     };
   *     var ExtendedWithTrait = SomeClass.extend(classDefinition)
   *                                      .mixin(AsyncCallbackTrait)
   *                                      .mixin(ObservableTrait, {
   *                                        method: 'aliasedMethod'
   *                                      });
   *
   * @method mixin
   * @param {Object} trait
   * @param {Object} [aliases]
   * @return {Klass}
   */
  Constructor.mixin = __mixin;

  /**
   * Method which can be used instead of the `new` statement
   *
   *     var instance = Klass.create();
   *
   * @method create
   * @return {*}
   */
  Constructor.create = __create;

  /**
   * Call a specific method of the super class
   *
   *     var SuperClass = Klass.of({
   *       onCreate: function() {
   *         alert('Yup!');
   *       }
   *     });
   *     var SubClass = SuperClass.extends({
   *       onCreate: function() {
   *         this.callSuper('onCreate', arguments); // => alert('Yup!')
   *       }
   *     });
   *
   * @method callSuper
   * @param {String} methodName
   * @param {Object|Arguments} args
   * @type {Function}
   */
  Constructor.prototype.callSuper = __callSuper;

  return Constructor;
}

function __mixin(trait, aliases) {
  /*jshint validthis:true */
  var i = 0, keys = Object.keys(trait), iz = keys.length,
      prop, processed_trait = {};

  aliases || (aliases = {});

  for (; i<iz; i++) {
    prop = keys[i];
    if (aliases[prop]) {
      processed_trait[aliases[prop]] = trait[prop];
    } else {
      processed_trait[prop] = trait[prop];
    }
  }

  this.prototype = _.extend(processed_trait, this.prototype);
  return this;
}

function __create() {
  /*jshint validthis:true */
  var instance = Object.create(this.prototype);
  this.apply(instance, arguments);
  return instance;
}

function __callSuper(methodName, args) {
  /*jshint validthis:true */
  // TODO: this.callSuper() で連鎖的に先祖のメソッドを呼び出したい
  return this.constructor.__super__[methodName].apply(this, args);
}
/**
 * @class Phalanx.Trait.AsyncCallbacks
 */
Trait.AsyncCallbacks = {

  /**
   * @abstract
   */
  onSuccess: function() {},

  /**
   * @abstract
   */
  onFailure: function() {}
};
/**
 * @class Phalanx.Trait.EntityObserver
 */
Trait.EntityObserver = {

  /**
     * @property {Phalanx.Model|Phalanx.Collection}
   */
  entity: null,

  /**
   *     entitylListeners: {
   *       'change': 'modelChanged'
   *     }
   *     // model.trigger('change') => observer.modelChanged()
   *
   * @property {Object.<String, String>}
   */
  entitylListeners: {},

  /**
   * Listen to entity events
   */
  listenToEntity: function() {
    var i = 0, events = Object.keys(this.entitylListeners), iz = events.length,
        event, method;

    for (; i<iz; i++) {
      event  = events[i];
      method = this.entitylListeners[event];
      if (!_.isFunction(this[method])) {
        throw new Error('Method `' + method + '` is not exists this Entity');
      }
      this.listenTo(this.entity, event, this[method]);
    }
  },

  /**
   * Unlisten to bound model events
   */
  unlistenToEntity: function() {
    this.stopListening(this.entity);
  }
};
/**
 * @class Phalanx.Trait.LifecycleCallbacks
 */
Trait.LifecycleCallbacks = {

  /**
   * It is called the first instance is created.
   * @abstract
   */
  onCreate: function() {},

  /**
   * It is called as a common initialization process. (derived from Backbone)
   * In Layout, View, Component...After set initial element & Before event delegation.
   * @abstract
   */
  initialize: function() {},

  /**
   * It is called when new element assigned.
   * called only Layout, View, Component.
   * @abstract
   */
  onSetElement: function() {},

  /**
   * @abstract
   */
  onPause: function() {},

  /**
   * @abstract
   */
  onResume: function() {},

  /**
   * It is called when destroying the instance.
   * @abstract
   */
  onDestroy: function() {}
};
/**
 * @class Phalanx.Trait.Observable
 * @extends Backbone.Events
 */
Trait.Observable = Backbone.Events;
var UI_FIND_PLACEHOLDER = '[data-ui="{name}"]';

/**
 * @class Phalanx.Trait.UiLookupable
 */
Trait.UiLookupable = {
  /**
   * @property {HTMLElement|jQuery}
   */
  el: null,

  /**
   *     ui: {
   *       hoge: null
   *     }
   *     // view.ui.hoge => [data-ui="hoge"]
   *
   * @property {Object.<String, Null>}
   */
  ui: {},

  /**
   * @property {Object.<String, jQuery>}
   */
  $ui: {},

  /**
   * From the selector defined by this.ui, caching to explore the elements.
   */
  lookupUi: function() {
    var name, selector,
        $baseEl = this.el instanceof Backbone.$ ? this.el : Backbone.$(this.el),
        i = 0, keys = Object.keys(this.ui), iz = keys.length;

    this.ui  = {};
    this.$ui = {};

    for (; i<iz; i++) {
      name = keys[i];
      selector = UI_FIND_PLACEHOLDER.replace('{name}', name);
      this.$ui[name] = $baseEl.find(selector);
      this.ui[name]  = this.$ui[name].length ? this.$ui[name][0] : null;
    }
  },

  /**
   * Release ui elements reference.
   */
  releaseUi: function() {
    var name,
        i = 0, keys = Object.keys(this.ui), iz = keys.length;

    for (; i<iz; i++) {
      name = keys[i];
      this.$ui[name] = null;
      this.ui[name] = null;
    }
  }
};
/**
 * @abstract
 * @class Phalanx.Router
 * @extends Backbone.Router
 * @mixins Phalanx.Trait.LifecycleCallbacks
 */
var Router = defineClass({
  /**
   * @constructor
   * @param {Object} options
   */
  constructor: function(options) {
    options || (options = {});

    this.onCreate.apply(this, arguments);

    Backbone.Router.apply(this, arguments);
  }
});

Router.mixin(Trait.LifecycleCallbacks);

_.extend(Router.prototype, Backbone.Router.prototype, {
  /**
   * destroy
   */
  destroy: function() {
    this.onDestroy();
  }
});

/**
 * @abstract
 * @class Phalanx.View
 * @extends Backbone.View
 * @mixins Phalanx.Trait.Observable
 * @mixins Phalanx.Trait.UiLookupable
 * @mixins Phalanx.Trait.LifecycleCallbacks
 */
var View = defineClass({
  /**
   * @constructor
   * @param {Object} options
   */
  constructor: function(options) {
    // init own object
    this._processedListeners = {};
    this._createdComponents  = {};
    this._processListeners();

    this.onCreate.apply(this, arguments);

    Backbone.View.apply(this, arguments);
  }
});

View.mixin(Trait.UiLookupable)
    .mixin(Trait.LifecycleCallbacks);

var ATTR_COMPONENT     = 'data-component',
    ATTR_COMPONENT_UID = 'data-component-uid';

_.extend(View.prototype, Backbone.View.prototype, {

  /**
   *     events: {
   *       'click .js_event_selector': 'someMethodName'
   *     }
   *     // $('.js_event_selector').click() => someMethod()
   *
   * @property {Object.<String, String|Function>}
   */
  events: {},

  /**
   *     components: {
   *       likeBtn: LikeBtnComponent
   *     }
   *     // <button data-component="likeBtn"></button> => LikeBtnComponent
   *
   * @property {Object.<String, Phalanx.Component>}
   */
  components: {},

  /**
   *     listeners: {
   *       'customEvent likeBtn': 'receiveCustomEvent'
   *     }
   *     // component.trigger('customEvent') => view.receiveCustomEvent()
   *
   * @property {Object.<String, String>}
   */
  listeners: {},

  /**
   * @private
   * @property {Object.<Number, Phalanx.Component>}
   */
  _createdComponents: {},

  /**
   *     _processedListeners: {
   *       'likeBtn': {
   *         'customeEvent': 'receiveCustomEvent'
   *       }
   *     }
   * @private
   * @property {Object.<String, Object<String, String>>}
   */
  _processedListeners: {},

  /**
   * @property {Boolean}
   */
  persistent: false,

  /**
   * @property {Boolean}
   */
  paused: false,

  /**
   * @param {HTMLElement} element
   * @param {Boolean} delegate
   */
  setElement: function(element, delegate) {
    Backbone.View.prototype.setElement.apply(this, arguments);
    if (this.el && this.el.parentNode) {
      this.lookupUi();
      this.onSetElement();
    }
  },

  /**
   * @param {Object} [events]
   */
  delegateEvents: function(events) {
    var componentEvents;

    if (events == null) {
      componentEvents = this._getComponentEvents();
      Backbone.View.prototype.delegateEvents.apply(this, [_.extend(componentEvents, this.events)]);
    } else {
      Backbone.View.prototype.delegateEvents.apply(this, arguments);
    }
  },

  /**
   * @private
   * @return {Object.<String, String>}
   */
  _getComponentEvents: function() {
    var componentEvents = {},
        componentName, protoComponent,
        eventKeys, eventClosures,
        i = 0, keys = Object.keys(this.components), iz = keys.length;

    for (; i<iz; i++) {
      componentName  = keys[i];
      if (this.components[componentName] == null) {
        throw new Error(componentName + ' Class is not exists.');
      }
      protoComponent = this.components[componentName].prototype;
      eventKeys      = _.keys(protoComponent.events),
      eventClosures  = _.map(protoComponent.events, this._getComponentEventClosure);
      _.extend(componentEvents, _.object(eventKeys, eventClosures));
    }

    return componentEvents;
  },

  /**
   * @private
   * @param {String} methodName
   * @return {Function}
   */
  _getComponentEventClosure: function(methodName) {
    return function(evt) {
      var component = this.getComponent(evt.target);
      return component[methodName].apply(component, arguments);
    };
  },

  /**
   * Flywieght component getter.
   *
   * @param {HTMLElement} el
   * @return {Phalanx.Component}
   */
  getComponent: function(el) {
    var componentName, uid;

    do {
      componentName = el.getAttribute(ATTR_COMPONENT);
    } while(!componentName && (el = el.parentNode));

    if (!componentName) {
      throw new Error('Component name is not detected from `' + ATTR_COMPONENT + '`');
    }

    uid  = el.getAttribute(ATTR_COMPONENT_UID);

    if (this._createdComponents[uid]) {
      return this._createdComponents[uid];
    } else {
      return this._newComponent(componentName, el);
    }
  },

  /**
   * @private
   * @param {String} componentName
   * @param {HTMLElement} el
   * @return {Phalanx.Component}
   */
  _newComponent: function(componentName, el) {
    var component, uid;

    component = new this.components[componentName](el);
    uid = component.uid;

    this._listenToComponent(component, componentName);

    el.setAttribute(ATTR_COMPONENT_UID, uid);
    this._createdComponents[uid] = component;
    return component;
  },

  /**
   * @private
   * @param {Phalanx.Component} component
   * @param {String} componentName
   */
  _listenToComponent: function(component, componentName) {
    var listeners, i, events, iz,
        event, method;

    if ((listeners = this._processedListeners[componentName])) {
      i = 0;
      events = Object.keys(listeners);
      iz = events.length;

      for (; i<iz; i++) {
        event  = events[i];
        method = listeners[event];
        if (!_.isFunction(this[method])) {
          throw new Error('Method `' + method + '` is not exists this View');
        }
        this.listenTo(component, event, this[method]);
      }
    }
  },

  /**
   * Converted to processed the `listeners` property.
   * Call at once when view instance created.
   * @private
   */
  _processListeners: function() {
    var i = 0, listeners = Object.keys(this.listeners), iz = listeners.length,
        event_component, methodName, eventMap;

    for (; i<iz; i++) {
      event_component = listeners[i].split(/\s+/);
      methodName      = this.listeners[listeners[i]];
      eventMap        = this._processedListeners[event_component[1]];

      eventMap || (eventMap = this._processedListeners[event_component[1]] = {});
      eventMap[event_component[0]] = methodName;
    }
  },

  /**
   * Destroy all created componentns.
   */
  destroyComponents: function() {
    var uid, component,
        i = 0, keys = Object.keys(this._createdComponents), iz = keys.length;

    for (; i<iz; i++) {
      uid = keys[i];
      component = this._createdComponents[uid];
      component.destroy();
      this._createdComponents[uid] = null;
      delete this._createdComponents[uid];
    }
  },

  /**
   * Destory and teadown View.
   */
  destroy: function() {
    this.destroyComponents();
    this.undelegateEvents();
    this.stopListening();
    this.releaseUi();

    this.onDestroy();
    this.el = this.$el = null;
    this.model = this.collection = null;
    this.options = this._processedListeners = null;
  },

  /**
   * Pause events
   */
  pause: function() {
    this.paused = true;
    this.onPause();

    this.destroyComponents();
    this.undelegateEvents();
    this.stopListening();
    this.releaseUi();
  },

  /**
   * Resume events
   */
  resume: function() {
    this.paused = false;

    this.delegateEvents();
    this.lookupUi();

    this.onResume();
  },

  /**
   * @abstract
   */
  onSetElement: function() {},

  /**
   * @abstract
   * @chainable
   * @param {String} html
   * @return {*}
   */
  render: function(html) { this.$el.html(html); return this; }
});

/**
 * @abstract
 * @class Phalanx.Model
 * @extends Backbone.Model
 * @mixins Phalanx.Trait.LifecycleCallbacks
 */
var Model = defineClass({
  /**
   * @constructor
   * @param {Object} attributes
   * @param {Object} options
   */
  constructor: function(attributes, options) {
    options || (options = {});

    this.onCreate.apply(this, arguments);

    Backbone.Model.apply(this, arguments);
  }
});

Model.mixin(Trait.LifecycleCallbacks);

_.extend(Model.prototype, Backbone.Model.prototype, {
  /**
   * Default params for using sync method
   *
   *     // if request to xml/html resource
   *     syncParams: {
   *       contentType: 'application/xml',
   *       dataType: 'text'
   *     }
   *
   * @property {Object}
   */
  syncParam: {},

  /**
   * @param {String} method
   * @param {Phalanx.Model} model
   * @param {Object} options
   * @returns {*}
   */
  sync: function(method, model, options) {
    _.extend(options, this.syncParam);
    return Backbone.Model.prototype.sync.apply(this, arguments);
  },

  /**
   * destroy
   */
  destroy: function() {
    Backbone.Model.prototype.destroy.apply(this, arguments);
    this.onDestroy();
  }
});

/**
 * @abstract
 * @class Phalanx.Collection
 * @extends Backbone.Collection
 * @mixins Phalanx.Trait.LifecycleCallbacks
 */
var Collection = defineClass({
  /**
   * @constructor
   * @param {Object} attributes
   * @param {Object} options
   */
  constructor: function(attributes, options) {
    options || (options = {});

    this.onCreate.apply(this, arguments);

    Backbone.Collection.apply(this, arguments);
  }
});

Collection.mixin(Trait.LifecycleCallbacks);

_.extend(Collection.prototype, Backbone.Collection.prototype, {
  /**
   * Default params for using sync method
   *
   *     // if request to xml/html resource
   *     syncParams: {
   *       contentType: 'application/xml',
   *       dataType: 'text'
   *     }
   *
   * @property {Object}
   */
  syncParam: {},

  /**
   * @param {String} method
   * @param {Phalanx.Model} model
   * @param {Object} options
   * @returns {*}
   */
  sync: function(method, model, options) {
    _.extend(options, this.syncParam);
    return Backbone.Collection.prototype.sync.apply(this, arguments);
  },

  /**
   * destroy
   */
  destroy: function() {
    this.reset();
    this.onDestroy();
  }
});
/**
 * @abstract
 * @class Phalanx.Layout
 * @extends Phalanx.View
 * @mixins Phalanx.Trait.Observable
 * @mixins Phalanx.Trait.LifecycleCallbacks
 */
var Layout = defineClass({
  /**
   * @constructor
   * @param {Object} options
   */
  constructor: function(options) {
    // init own object
    this._assignedMap = {};

    View.apply(this, arguments);
  }
});

_.extend(Layout.prototype, View.prototype, {
  /**
   *     events: {
   *       'click .js_event_selector': 'someMethodName'
   *     }
   *     // $('.js_event_selector').click() => someMethod()
   *
   * @property {Object.<String, String|Function>}
   */
  events: {},

  /**
   * @property {Object.<String, String>}
   */
  regions: {},

  /**
   * Correspondence table of the region name and assigned View.
   *
   *     laput.assign(regionName, Phalanx.View);
   *     // layout._assignedMap => { regionName: Phalanx.View }
   *
   * @private
   * @property {Object.<String, Phalanx.View>}
   */
  _assignedMap: {},

  /**
   * Assign new View to element in layout.
   * And destroy old View automatically.
   *
   * @param {String} regionName
   * @param {Phalanx.View} newView
   */
  assign: function(regionName, newView) {
    var selector, oldView, assignToEl;

    selector = this.regions[regionName];
    oldView  = this.getRegionView(regionName);

    if (!this.$el) {
      // maybe already destroy
      return;
    }
    assignToEl = this.$el.find(selector)[0];

    if (!selector || !assignToEl) {
      throw new Error('Could not get element of `'+ selector +'` from the region ' + regionName);
    }

    // change
    this.onChange(regionName, newView, oldView);

    // old
    if (oldView) {
      if (oldView.persistent) {
        oldView.pause();
        oldView.$pausingCache = oldView.$el.children();
      } else {
        oldView.destroy();
      }
    }

    this._assignedMap[regionName] = newView;

    // new
    if (newView.persistent && newView.paused) {
      newView.$el.empty().append(newView.$pausingCache);
      newView.$pausingCache = null;
      newView.resume();
    } else {
      newView.setElement(assignToEl);
    }

  },

  /**
   * Copy to some specified attributes
   *
   * @private
   * @param {HTMLElement} fromEl
   * @param {HTMLElement} toEl
   */
  _copyAttrs: function(fromEl, toEl) {
    toEl.setAttribute('id',    fromEl.getAttribute('id')    || '');
    toEl.setAttribute('class', fromEl.getAttribute('class') || '');
    toEl.setAttribute('style', fromEl.getAttribute('style') || '');
  },

  /**
   * @param {String} regionName
   * @return {Boolean}
   */
  isRegionExists: function(regionName) {
    if (!(regionName in this.regions)) {
      throw new Error('Specified region `' + regionName + '` is not declared');
    }
    return !!this.$el.find(this.regions[regionName]).length;
  },

  /**
   * @param {String} regionName
   * @returns {Phalanx.View}
   */
  getRegionView: function(regionName) {
    if (!(regionName in this.regions)) {
      throw new Error('Specified region `' + regionName + '` is not declared');
    }
    return this._assignedMap[regionName];
  },

  /**
   * @param {String} regionName
   */
  withdraw: function(regionName) {
    var view = this.getRegionView(regionName);
    this._assignedMap[regionName] = null;
    view && view.destroy();
  },

  /**
   * @override Phalanx.View.destroy
   */
  destroy: function() {
    var i = 0, regions = Object.keys(this.regions),
        regionName;

    while ((regionName = regions[i++])) {
      this.withdraw(regionName);
    }
    View.prototype.destroy.apply(this, arguments);
  },

  /**
   * @override Phalanx.View.pause
   */
  pause: function() {
    var i = 0, regions = Object.keys(this.regions),
        regionName;

    while ((regionName = regions[i++])) {
      this.getRegionView(regionName) && this.getRegionView(regionName).pause();
    }
    View.prototype.pause.apply(this, arguments);
  },

  /**
   * @override Phalanx.View.resume
   */
  resume: function() {
    var i = 0, regions = Object.keys(this.regions),
        regionName;

    while ((regionName = regions[i++])) {
      this.getRegionView(regionName) && this.getRegionView(regionName).resume();
    }
    View.prototype.resume.apply(this, arguments);
  },

  /**
   * @abstract
   * @param {String} regionName
   * @param {Phalanx.View} newView
   * @param {Phalanx.View} oldView
   */
  onChange: function(regionName, newView, oldView) {}
});
var INCREMENT_COMPONENT_UID = 0;

/**
 * @abstract
 * @class  Phalanx.Component
 * @mixins Phalanx.Trait.Observable
 * @mixins Phalanx.Trait.UiLookupable
 * @mixins Phalanx.Trait.LifecycleCallbacks
 */
var Component = defineClass({
  /**
   *     events: {
   *       'click .js_event_selector': 'someMethod'
   *     }
   *     // $('.js_event_selector').click() => someMethod()
   *
   * @property {Object.<String, String|Function>}
   */
  events: {},

  /**
   * If exists element having `data-id` attribute
   *
   */
  id: null,

  /**
   * instance's unique id nubmer
   * @property {Number}
   */
  uid: null,

  /**
   * placeholder of instance parameters
   * @property {Object}
   */
  params: {},

  /**
   * @constructor
   * @param {HTMLElement} el
   */
  constructor: function(el) {
    this.uid = INCREMENT_COMPONENT_UID++;

    this.params = _.clone(this.params);

    this.onCreate.apply(this, arguments);

    this.setElement(el);

    this.initialize.apply(this, arguments);
  },

  /**
   * Set managing domain element
   * @param element
   */
  setElement: function(element) {
    this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
    this.el = this.$el[0];

    if (this.el && this.el.parentNode) {
      this.lookupUi();
      this.onSetElement();

      this.id = parseInt(this.el.getAttribute('data-id'), 10);
    }
  },

  /**
   * Destory this component
   */
  destroy: function() {
    this.releaseUi();

    this.onDestroy();

    this.el = this.$el = null;
  },

  /**
   * @abstract
   */
  onSetElement: function() {}
});

Component.mixin(Trait.Observable)
         .mixin(Trait.UiLookupable)
         .mixin(Trait.LifecycleCallbacks);

/**
 * @class Phalanx
 */
var Phalanx = {

  defineClass: defineClass,

  Router     : Router,
  Model      : Model,
  Collection : Collection,

  View       : View,
  Layout     : Layout,
  Component  : Component,
  Trait      : Trait
};

// for RequireJS
if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
  window.define(function() {
    return Phalanx;
  });
}
// for Node.js & browserify
else if (typeof module === 'object' && module &&
  typeof exports === 'object' && exports &&
  module.exports === exports
  ) {
  module.exports = Phalanx;
}
// for Browser
else {
  window.Phalanx = Phalanx;
}

})(this);