pulp.Modules['cls.event'] = true;

(function() {
  
  var $p = pulp.base;
  
  /**
   * Object event system that is mixed into {@link pulp.cls.Base#} 
   * and can be mixed into any class prototype.
   * Uses an advanced event model inspired by YUI 3 
   * 
   * @name pulp.cls.event
   * @namespace
   * @requires base
   * @requires cls
   */
  var ClassEvent = pulp.cls.event = {
    _definedEvents: {},
    /**
     * Define an event to have certain properties
     * 
     * @param {String} ev  The type of event (e.g. "my:custom")
     * @param {Object}    [options={}]               Properties to set on the event:
     *   @param {Object}  [options.target=this]      The target object
     *   @param {Boolean} [options.bubbles=true]     If false, observers cannot stop firing of subsequent handlers
     *   @param {Boolean} [options.cancelable=true]  If false, observers cannot stop firing of default action
     *   @param {Object}  [options.scope=this]       The scope (object) in which to call handlers
     *   @param {Boolean} [options.notifyOnce=false] If true, the event will notify only once; callbacks subscribed after firing will be called immediately
     * @return {Array|Object}
     */		
    defineEvent: function(ev, options, _internalCall) {
      var exec = function() {
        var defined = new ClassEvent.definedEvent(ev, options), old;
        if ((old = ClassEvent._definedEvents[ev])) {
          defined.onSubscribers = old.onSubscribers;
          defined.afterSubscribers = old.afterSubscribers;
        }
        ClassEvent._definedEvents[ev] = defined;
      };			
      if (_internalCall) {
        exec();
      } else {
        ClassEvent.aggregator.notify('event:define', exec, {eventType: ev, options: options});
      };
      return ClassEvent._definedEvents[ev];
    },
    /**
     * Return a reference to the defined event of the given event type
     * @param {String} type  The event type to return
     * @return {pulp.cls.event.definedEvent}
     */
    getDefinedEvent: function(ev) {
      return ClassEvent._definedEvents ? ClassEvent._definedEvents[ev] : null;
    },
    aggregator: new pulp.cls.Base
  };
  
  ClassEvent.mixin = /** @lends pulp.cls.Base# */{
    /**
     * Register a callback that should be triggered immediately before the given event
     * 
     * @param {String} type  The name of the event
     * @param {Function} callback  The function to call
     * @return {this}
     * @chainable
     */
    on: function(ev, fn, _after) {
      return this.notify('event:subscribe', function() {
        if (!ClassEvent._definedEvents[ev]) {
          ClassEvent._definedEvents[ev] = ClassEvent.defineEvent(ev, {target: this});
        }
        ClassEvent._definedEvents[ev][(_after === true ? 'after' : 'on')](fn);
      }, {subscription: {type: ev, callback: fn, eventPhase: (_after === true ? 3 : 1)}});
    },
    /**
     * Register a callback that should be triggered immediately after the given event
     * 
     * @param {String} type  The name of the event
     * @param {Function} callback  The function to call
     * @return {this}
     * @chainable
     */		
    after: function(ev, fn) {
      return this.on(ev, fn, true);
    },    
    /**
     * Remove a callback that was registered for the given event
     * 
     * @param {String} type  The name of the event
     * @param {Function} callback  The function that was registered
     * @return {this}
     * @chainable
     */    
    un: function(ev, fn) {
      return this.notify('event:unsubscribe', function() {
        var e = this._definedEvents;
        if (e && e[ev]) {
          e.un(fn);
        }
      }, {eventType: ev, callback: fn});
    },
    /**
     * Remove all subscribers to this object's events
     * 
     * @return {this}
     * @chainable
     */    		
    unAll: function() {
      var d = this._definedEvents;
      if (d) {
      	for (var ev in d) {
      		this.un(ev, d[ev]);
      	}
      }
      return this;
    },
    /**
     * Register callbacks by property name
     * 
     * @param {Object} options  The object to scan for callbacks
     * @return {this}
     * @chainable
     * @example
     * var options = {
     *   onSomeEvent: myFn,
     *   afterAnotherEvent: myOtherFn,
     *   property: 'something',
     *   onNonFnProperty: 'something else'
     * };
     * myObservable.subscribeInline(options); // observes onSomeEvent and onAnotherEvent
     */    
    subscribeInline: function(options) {
      options = options || this.options || {};
      var match;
      for (var name in options) {
        match = name.match(/(on|after)([A-Z0-9][\w_]+)/);
        if (match && (typeof options[name] == 'function')) {
          this[match[1]](match.slice(0, 1).toLowerCase() + match.slice(1), options[name]);
        } 
      }
      return this;
    },
    /**
     * Fire all callbacks registered for the given event
     * 
     * @param {String} type  The name of the event
     * @param {Function} [defaultAction]  The function to call after "on" events are triggered and event is not prevented
     * @param {Object} [data]  Additional data to pass (type, stop, and target properties will be overwritten)
     * @param {Object} [context=this]  The context in which to call the callback function
     * @return {this}
     * @chainable
     */    
    notify: function(ev, defaultAction, data, context) {
      var DE; // defined edvent object containing event firing options
      if (!this._definedEvents || !(DE = this._definedEvents[ev])) {
        DE = ClassEvent.defineEvent(ev, null, true);
      }

      // get event object
      var event = DE.createEvent(data);
      
      // get context
      context = context || event.scope || this;
      event.scope = context;			
      
      // debug
      if (this._debugObserver) {
        event.eventPhase = 0;
        event.eventPhaseName = 'debug';
        this._debugObserver.call(context, event);
      }
      
      // on
      DE.notify('on', event, context);
            
      // bail out if isCanceled
      if (event.isCanceled()) {
        return this;
      }
      
    	// default action
      if (defaultAction) {
        event.eventPhase = 2;
        event.eventPhaseName = 'defaultAction';
        defaultAction.call(context, event);
      }

      // after
      DE.notify('after', event, context);
      DE.notified = true;
      // event object should be garbage collected automatically unless it was cached somewhere
      return this;
    },
    /**
     * Return a list of registered "on" callbacks for the given event type
     * 
     * @param {String} [type]  The name of the event; If not given, an object with all events and observers is returned
     * @return {Array}
     */
    listHandlersOn: function(ev, _type) {
      var e = ClassEvent._definedEvents;
      if (e && e[ev]) {
        return $p.makeArray(e[(_type || 'on') + 'Listeners']);
      } else {
        return [];
      }
    },
    /**
     * Return a list of registered "after" callbacks for the given event type
     * 
     * @param {String} [type]  The name of the event; If not given, an object with all events and observers is returned
     * @return {Array}
     */
    listHandlersAfter: function(ev) {
      return this.listHandlersOn(ev, 'after');
    },		
    /**
     * Trigger a function immediately before every notified event
     * 
     * @param {Function} fn  The callback
     * @return {this}
     * @chainable
     */
    eventDebug: function(fn) {
      this._debugObserver = fn;
      return this;
    }
  };
  
  // mixin to base object
  pulp.cls.Base.extendPrototype(ClassEvent.mixin);

  /**
   * Create a new Event object that is passed to event subscribers
   * @param {Object} data
   * @param {pulp.cls.event.definedEvent} definedEvent
   * @constructor
   * @name pulp.cls.event.dispatched
   */
  ClassEvent.dispatched = /** @ignore */ function(data, definedEvent) {
    var isStopped = false;   // private, read only
    var isCanceled = false;  // private, read only
    var isSuspended = false; // private, read only
    /**
     * Get a reference to the defined event
     * @name pulp.cls.event.dispatched#getDefinedEvent
     * @return {pulp.cls.event.definedEvent}
     */
    this.getDefinedEvent = function() {
      return definedEvent;
    };
    /**
     * Prevent the default action from being called
     * @name pulp.cls.event.dispatched#preventDefault
     * @return this
     */
    this.preventDefault = function() {
      if (definedEvent.cancelable) { isCanceled = true; };
      return this;
    };
    /**
     * If in "on" phase, call no more "on" subscribers
     * If in "after" phase, call no more "after" subscribers
     * @name pulp.cls.event.dispatched#stopImmediatePropagation
     * @return this
     */
    this.stopImmediatePropagation = function() {
      if (definedEvent.bubbles) { isSuspended = true; };
      return this;
    };
    /**
     * Prevent other subscribers from executing, but still allow the default action
     * @name pulp.cls.event.dispatched#stopPropagation
     * @return this
     */
    this.stopPropagation = function() {
      if (definedEvent.bubbles) { isStopped = true; };
      return this;
    };
    /**
     * Return true if event is cancelled (i.e. if preventDefault() was called)
     * @name pulp.cls.event.dispatched#isCanceled
     * @return {Boolean}
     */
    this.isCanceled = function() { return isCanceled; };
    /**
     * Return true if event is completely stopped (i.e. halt() was called)
     * @name pulp.cls.event.dispatched#isStopped
     * @return {Boolean}
     */
    this.isStopped = function() { return isStopped; };
    /**
     * Return true if event is temporarily cancelled (i.e. if stopImmediatePropagation() was called)
     * @name pulp.cls.event.dispatched#isSuspended
     * @return {Boolean}
     */	
    this.isSuspended = function() { return definedEvent.eventPhase != 3 && isSuspended; };
    $p.extend(this, data || {});
  };
  
  /**
   * Return true if event is cancelled (i.e. if preventDefault() was called)
   * @return {Boolean}
   * @name pulp.cls.event.dispatched#halt
   */
  ClassEvent.dispatched.prototype.halt = /** @ignore */ function() {
    this.preventDefault();
    this.stopPropagation();
  };
  
  /**
   * Create a new Event definition that sets options for notified events
   * @param {String} type  The event to notify
   * @param {Object} opitons
   * @constructor
   * @name pulp.cls.event.definedEvent
   */	
  ClassEvent.definedEvent = /** @ignore */ function(ev, options) {
    $p.extend(this, ClassEvent.definedEvent.defaultOptions, options || {});
    this.type = ev;
    this.onSubscribers = [];
    this.afterSubscribers = [];
    this.notified = false;
  };
  
  $p.extend(ClassEvent.definedEvent.prototype, /** @scope pulp.cls.event.definedEvent# */{
    /**
     * Subscribe to the defined event
     * @param {Function} callback  The callback to subscribe
     * @return {undefined}
     */	
    on: function(fn) {
      if (this.notified && this.notifyOnce) {
        fn.call(this.scope, this.createEvent());
        return;
      }
      this.onSubscribers.push(fn);
    },
    /**
     * Subscribe to the defined event to notify after the default action
     * @param {Function} callback  The callback to subscribe
     * @return {undefined}
     */	
    after: function(fn) {
      if (this.notified && this.notifyOnce) {
        fn.call(this.scope, this.createEvent());
        return;
      }			
      this.afterSubscribers.push(fn);
    },
    /**
     * Unsubscribe all instances of the given callback from this event
     * @param {Function} callback  The callback to unsubscribe
     * @return {undefined}
     */			
    un: function(fn) {
      this.onSubscribers = $p.without(this.onSubscribers, fn);
      this.afterSubscribers = $p.without(this.afterSubscribers, fn);
    },
    /**
     * Fire the given event phase (on or after)
     * @param {String} timing  on|after
     * @param {pulp.cls.event.dispatched} event
     * @param {Object} context  The context in which to execute callbacks
     * @return {undefined}
     */
    notify: function(timing, event, context) {
      event.eventPhase = (timing == 'on' ? 1 : 3);
      event.eventPhaseName = timing;
      event.target = event.target || context;
      var fn, i = 0;
      while ((fn = this[timing + 'Subscribers'][i++])) {
        if (event.isStopped() || event.isSuspended()) { break; }
        fn.call(context, event);
      }
    },
    /**
     * Create a dispatchable event
     * @param {Object} data  Data to copy to the event
     * @return {pulp.cls.event.dispatched}
     */
    createEvent: function(data) {
      data = data || {};
      data.type = data.type || this.type;
      data.target = data.target || this.target;
      return new ClassEvent.dispatched(data, this);
    }
  });
  
  /**
   * Default options for new defined events
   * @name pulp.cls.event.definedEvent.defaultOptions
   */
  ClassEvent.definedEvent.defaultOptions = {
    bubbles: true,
    cancelable: true,
    scope: null,
    notifyOnce: false
  };
  
})();