/** var $e = pulp.event; */
pulp.Modules.event = '$e';

(function() {

  var $p = pulp.base;
  
  /**
   * DOM Element events class. Note that all methods may be called statically or on an instance.
   * @name pulp.event
   * @requires base
   * @requires cls
   * @requires cls.event
   */
  var $e = pulp.event = pulp.cls.create(/** @lends pulp.event# */{
    /**
     * Return a pulp.event instance given an Event object
     * @param {Event}
     * @constructs
     */
    initialize: function(event) {
      /**
       * The raw event object
       * @type {Event}
       */
      this.raw = event || {};
    }
  }); 
  
  $e.extend(/** @scope pulp.event */{
    /** @type {Boolean} 
        True if browser reports itself as IE */
    isIE: !!(window.attachEvent && navigator.userAgent.indexOf('Opera') === -1),
    /** @type {Boolean} 
        True if browser reports itself as Webkit */
    isWebkit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
    /** @type {Boolean} 
        True if browser reports itself as Opera */
    isOpera: navigator.userAgent.indexOf('Opera') != -1,   
    /**
     * Allows plugging in custom events
     * @namespace
     */
    custom: {},
    /**
     * The internal cache of attached events to enable detachment. Example cache structure:
     * { myid: { mouseover: [<div id="myid">, mouseover, handlerOriginal, handlerToCall] } }
     * @type {Object}
     */
    _cache: {},
    /**
     * Get a node's id, assign one if it doesn't have one, and return <window> and <document> as appropriate
     * @param node  The DOM Element
     * @return {String}
     */
    _identify: $p.identifyNode,
    /**
     * Get an object by id
     * 
     * @param {String} id  a dom node id or dom node
     * @return {HTMLElement}
     */    
    _byId: function(id) {
      // TODO: different ids for different windows?
      if (typeof id === 'string') {
        id = (
          id === '<window>' ? pulp.window :
          id === '<document>' ? pulp.document :
          pulp.document.getElementById(id)
        );
      }
      return id;
    },
    /**
     * Attach a handler and add to pulp.event._cache
     * @param {HTMLElement} node  The node to observe
     * @param {String} type  The event name (e.g. mouseover, click, dblclick, etc.)
     * @param {Function} handlerOriginal  The function that will be used to detach
     * @param {Function} handlerToCall  The possibly altered function that will be called on the event
     * @return {undefined}
     */
    _attachAndCache: function(node, type, handlerOriginal, handlerToCall) {
      var id = $e._identify(node);
      if (!(id in $e._cache)) {
        $e._cache[id] = {};
      }
      if (!(type in $e._cache[id])) {
        $e._cache[id][type] = [];
        
        // standard events can only be made of letters and numbers
        // anything with a non-letter or number is considered a custom event
        // e.g. mouse:enter, shift+click
        if ((/^\w+$/).test(type)) {
          $e._cache[id][type].listener = $e._addDispatcher(node, type);
          
        } else {          
          arguments[3] = function(event) {
            if (!event.target) { // event may be a real event object as in dom:loaded 
              event.target = node;
              event.currentTarget = node;
              event.relatedTarget = undefined;
              event.timeStamp = new Date().getTime();
            }
            handlerOriginal.call(node, event);
          };
        }
      }
      $e._cache[id][type].push(arguments);
    },
    /**
     * Remove a handler and its entry in pulp.event._cache
     * @param {HTMLElement} node  The node that was observed
     * @param {String} type  The event name (e.g. mouseover, click, dblclick, etc.)
     * @param {Function} handlerOriginal  The function originally passed in the observe call
     * @return {undefined}
     */
    _detachAndCleanCache: function(node, type, handlerOriginal) {
      var i, len, obs;
      var id = $e._identify(node);
      if ((id in $e._cache) && (type in $e._cache[id])) {
        obs = $e._cache[id][type];
        for (i = 0, len = obs.length; i < len; i++) {
          if (obs[i][0] == node && obs[i][3] == handlerOriginal) {
            obs = obs.splice(obs, i);
          }
        }
        if ($e._cache[id][type].length == 0) {
          $e._removeDispatcher(node, type, $e._cache[id][type].listener);
          delete $e._cache[id][type];
        }
      }
    },
    /**
     * Create dispatcher and addEventListener (one dispatcher per element per event)
     * @param {HTMLElement} node  The node to observe
     * @param {String} type  The event name (e.g. mouseover, click, dblclick, etc.)
     * @return {Function}  The newly created dispatcher 
     */
    _addDispatcher: function(node, type) {
      var dispatcher = function(event) {
        $e.fire(node, type, event);
      };
      node.addEventListener(type, dispatcher, false);
      return dispatcher;
    },
    /**
     * Run removeEventListener for the given element an event
     * @param {HTMLElement} node  The node to observe
     * @param {String} type  The event name (e.g. mouseover, click, dblclick, etc.)
     * @param {Function} handler  The dispatcher function that was created in {@link pulp.event._addDispatcher}
     * @return {undefined}
     */    
    _removeDispatcher: function(node, type, handler) {
      node.removeEventListener(type, handler, false);
    },
    /**
     * An observable interface that is copied to pulp.event
     * @type {Object}
     */
    observable: /** @scope pulp.event */{
    // argument name legend:
    //   node = HTMLElement Object or a string representing the id of an HTMLElement
    //   type = event type (e.g. mouseover, click, dom:loaded)
    //   handler = The function to call when that event is triggered
      /**
       * Register a handler to be called on the given event for the given HTMLElement
       * @param {HTMLElement|String} node  The node to observe
       * @param {String} type  The event name (e.g. mouseover, click, dblclick, etc.)
       * @param {Function} handler  The function to call
       * @return {HTMLElement}  The HTMLElement given or the HTMLElement with the given id
       * @chainable
       */
      observe: function(node, type, handler) {
        node = $e._byId(node);
        var modified = false;
        var args = {
          node: node,
          type: type,
          handler: handler,
          originalHandler: handler
        };
        if ($e.custom[args.type] && $e.custom[args.type].add) {
          // custom event has one of the following return types
          // object with key modified
          // true: skips caching
          // falsy: caches using original args
          modified = $e.custom[type].add.apply($e, $p.makeArray(arguments));
          if (modified !== true && !!modified) {
            $p.extend(args, modified);
          }
        }
        if (modified !== true) {
          $e._attachAndCache(args.node, args.type, args.originalHandler, args.handler);
        }
        return node;
      },
      /**
       * Unegister a handler for the given event for the given HTMLElement
       * @param {HTMLElement|String} node  The node on which to unregister
       * @param {String} type  The event name (e.g. mouseover, click, dblclick, etc.)
       * @param {Function} handler  The function that was originally registered
       * @return {HTMLElement}  The HTMLElement given or the HTMLElement with the given id
       * @chainable
       */      
      stopObserving: function(node, type, handler) {
        if (!handler) {
          if (!type) {
          
          }
          
        }
        node = $e._byId(node);
        var modified = false;
        var args = {
          node: node,
          type: type,
          handler: handler,
          originalHandler: handler
        };
        if ($e.custom[args.type] && $e.custom[args.type].remove) {
          // custom event has one of the following return types
          // object with key modified
          // true: skips caching
          // falsy: caches using original args
          modified = $e.custom[args.type].remove.apply($e, $p.makeArray(arguments));
          if (modified !== true && !!modified) {
            $p.extend(args, modified);
          }
        }
        if (modified !== true) {
          $e._detachAndCleanCache(args.node, args.type, args.originalHandler);
        }
        return node;
      },
      /**
       * Manually trigger all handlers associated with the given node and event.
       * Optionally pass an object with data that is sent as the first argument to each handler.
       * @param {HTMLElement|String} node  The node on which to fire
       * @param {String} type  The event name (e.g. mouseover, click, dblclick, etc.)
       * @param {Object} [data]  Information to send to each handler
       * @return {HTMLElement}  The HTMLElement given or the HTMLElement with the given id
       * @chainable
       */
      fire: function(node, type, data) {
        var i, len, id, obs;
        id = $e._identify(node);
        if ($e._cache[id] && $e._cache[id][type]) {
          obs = $e._cache[id][type];
          for (i = 0, len = obs.length; i < len; i++) {
            if (obs[i][0] == node) {
              if (!data.stopped) {
                obs[i][3].call(node, data);
              }
            }
          }
        }
        return node;
      },
      /**
       * Return an Array of handlers registered to the given node for the given event type.
       * If type is not specified, all handlers for the node are returned.
       * If node is not specified, all handlers for any node are returned.
       * @param {HTMLElement|String} [node]  The node on which to look
       * @param {String} [type]  The event name (e.g. mouseover, click, dblclick, etc.)
       * @return {Function[]}  Array of functions that were registered
       */      
      listHandlers: function(node, type) {
        var i, len, id, obs, t, list;
        id = $e._identify(node);
        if (node && type) {
          list = [];
          if ($e._cache[id] && $e._cache[id][type]) {
            obs = $e._cache[id][type];
            for (i = 0, len = obs.length; i < len; i++) {
              if (obs[i][0] == node) {
                list.push(obs[i][2]);
              }
            }
          }          
        } else if (node) {
          list = [];
          if ($e._cache[id]) {
            for (t in $e._cache[id]) {
              list = list.concat($e.listHandlers(node, t));
            }
          }
        } else {
          list = [];
          for (id in $e._cache) {
            list = list.concat($e.listHandlers($e._byId(id)));
          }
        }
        return list;
      }      
      // TODO: add simulate
    },
    /**
     * Shorthand for register a handler to be fired on dom:loaded
     * @param {Function} handler  The handler to fire
     * @return {pulp.event}
     * @chainable
     */
    ready: function(handler) {
      $e.observe(pulp.document, 'dom:loaded', handler);
      return $e;
    },
    /**
     * A collection of methods to be called on an Event object.
     * These can be called statically as in "pulp.event.stop(event)"
     * or on an instance as in "new pulp.event(event).stop()"
     * @type {Object}
     */
    eventDetail: /** @scope pulp.event */{
      /**
       * Return true if the click event included the left click button
       * @param {Event} event
       * @return {Boolean}
       */
      isLeftClick: function(event) {
        return $e.isButton(event, 0);
      },
      /**
       * Return true if the click event included the middle click button
       * @param {Event} event
       * @return {Boolean}
       */  
      isMiddleClick: function(event) {
        return $e.isButton(event, 1);
      },
      /**
       * Return true if the click event included the right click button
       * @param {Event} event
       * @return {Boolean}
       */  
      isRightClick: function(event) {
        return $e.isButton(event, 2);
      },
      /**
       * Get the node from which the mouse came before the event fired as in mouseover.
       * This value can be null as in the case when the mouse moves into the node from outside the window.
       * @param {Event} event
       * @return {HTMLElement|null}
       */  
      getRelatedTarget: function(event) {
        // related element (routing of the event)
        return event[(event.target == event.fromElement ? 'to' : 'from') + 'Element'];
      },
      /**
       * Find the parentNode of the event target with the given tagName.
       * If tagName is omitted, the direct parent is returned.
       * If {@link pulp.cssQuery} is present, tagName may optionally be a css selector and
       *   the first parent matching the selector will be returned.
       * @param {Event} event
       * @param {String} [tagName]
       * @return {HTMLElement|null}
       */
      findParent: function(event, tagName) {
        var node = event.target;
        if (!tagName) {
          return node.parentNode;
        }
        if (tagName) {
          if ('cssQuery' in pulp) {
            parent = node;
            while ((parent = parent.parentNode)) {
              if (pulp.cssQuery.match(node, tagName)) {
                return parent;
              }
            }            
          } else {
            tagName = tagName.toUpperCase();
            parent = node;
            while ((parent = parent.parentNode)) {
              if (parent.tagName == tagName) {
                return parent;
              }
            }            
          }
        }
        return null;
      },
      /**
       * Return an object with properties x and y representing the coordinates
       *   of the mouse pointer at the time the event fired.
       * @param {Event} event
       * @return {Object}  Object with properties x and y which are both numbers
       */    
      pointer: function(event) {
        return {x: $e.pointerX(event), y: $e.pointerY(event)};
      },
      /**
       * Return the x coordinate of the mouse pointer at the time the event fired
       * @param {Event} event
       * @return {Number}
       */  
      pointerX: function(event) {
        var doc = event.target.ownerDocument || event.target.document;
        var body = doc.body || { scrollLeft: 0, scrollTop: 0 };
        return event.pageX || (event.clientX +
          (doc.scrollLeft || body.scrollLeft) -
          (doc.clientLeft || 0));
      },
      /**
       * Return the x coordinate of the mouse pointer at the time the event fired
       * @param {Event} event
       * @return {Number}
       */    
      pointerY: function(event) {
        var doc = event.target.ownerDocument || event.target.document;
        var body = doc.body || { scrollLeft: 0, scrollTop: 0 };
        return event.pageY || (event.clientY +
          (doc.scrollTop || body.scrollTop) -
          (doc.clientTop || 0));
      },
      /**
       * Prevent the default action and stop the event's propagation.
       * Stop a default action: do not follow link; do not submit form; etc.
       * Stop propagation: do not trigger other handlers on this element or any parent element.
       * @param {Event} event
       * @return {Event}
       * @chainable
       */  
      stop: function(event) {
        if (event.preventDefault) {
          event.preventDefault();
        } else {
          event.returnValue = false;
        }
        if (event.stopPropagation) {
          event.stopPropagation();
        } else {
          event.cancelBubble = true;
        }
        event.stopped = true;
        return $e;
      },
      /**
       * Return the Browser character code for the pressed key.
       * Should be used with the native JavaScript function String.fromCharCode().
       * Note that beyond alphanumeric characters, browsers are inconsistent with code meanings.
       * @param {Event} event
       * @return {Number}
       */  
      getKeyCode: function(event) {
        return event.keyCode || event.charCode;
      },
      /**
       * Return -1, 0, or 1 to represent the mouse wheel scrolling up, none, or down respectively.
       * @param {Event} event
       * @return {Number}
       */        
      getWheelDirection: function(event) {
        var d = event.detail ? event.detail * -1 : event.wheelDelta;
        return d ? (d > 0 ? 1 : -1) : 0;
      },  
      /**
       * Return event's target. If pulp.node is present, return a new pulp.node object
       * @param {Event} event
       * @return {Number}
       */        
      element: function(event) {
        if ('node' in pulp) {
          return pulp.node.getInstance(event.target);
        }
        return event.target;
      }
    }
    
  });
  
  // TODO: use feature testing using event dispatching?
  if ($e.isIE) {
    var buttonMap = {0: 1, 1: 4, 2: 2};
    /**
     * @name pulp.event.isButton
     * @function
     * @param {Event} event  The event object which to read
     * @param {Number} code  The button code number to test for (0=left, 1=middle, 2=right)
     * @return {Boolean}  If true, that button was pressed
     */
    $e.isButton = function(event, code) {
      return event.button == buttonMap[code];
    };

  } else if ($e.isWebkit) {
    /** @ignore */
    $e.isButton = function(event, code) {
      switch (code) {
        case 0: return event.which == 1 && !event.metaKey;
        case 1: return event.which == 1 && event.metaKey;
        default: return false;
      }
    };

  } else {
    /** @ignore */
    $e.isButton = function(event, code) {
      return event.which ? (event.which === code + 1) : (event.button === code);
    };
  }
    
  $e.extend($e.observable)
    .extend($e.eventDetail)
    .addMethods($e.eventDetail, 'raw');
  
  /**
   * alias of {@link pulp.event.listHandlers}, {@link pulp.event#listHandlers}, and {@link pulp.event.observable.listHandlers}
   * @function
   * @name pulp.event.listObservers
   */
  pulp.event.listObservers = $e.listHandlers;
  
})();

/**
 * var $e = pulp.event;
 */
pulp.Modules.event = '$e';
/**
 * var $ready = pulp.event.ready;
 */
pulp.Modules['event.ready'] = '$ready';


//pulp.event.custom.js plugin "mouse:enter" - (and "mouse:leave") fire on mouseenter or mouseleave
(function() {

  var $p = pulp.base;
  var $e = pulp.event;

  /** @ignore */
  $e._descendantOf = function(node, parent) {
    while (node) {
      if (node === parent) {
        return true;
      }
      node = node.parentNode;
    }
    return false;
  };

  /** @ignore */
  $e._enterLeaveWrapper = function(proceed, event) {
    // outside the browser window makes relatedTarget null
    var related = event.getRelatedTarget();
    if (related == this || !$e._descendantOf(related, this)) {
      proceed.call(this, event);
    }
  };

  var enterleave = 'onmouseenter' in document.documentElement;
  $p.extend(pulp.event.custom, /** @scope pulp.event.custom */{
    /**
     * Custom event mouse:enter that fires like :hover in CSS
     * @type {Object}
     * @name pulp.event.custom.mouse:enter
     * @example
     *   var $e = pulp.event;
     *   $e.observe('myDiv', 'mouse:enter', enterHandler);
     */
    'mouse:enter': {
      /** @ignore */
      add: function(node, type, handler) {
        if (enterleave) {
          return {type: 'mouseenter'};
        } else {
          return {type: 'mouseover', handler: $p.wrapFunction(handler, $e._enterLeaveWrapper)};
        }
      },
      /** @ignore */
      remove: function(node) {
        return {type: (enterleave ? 'mouseenter' : 'mouseover')};
      }
    },
    /**
     * Custom event mouse:leave that fires like turning off :hover in CSS
     * @type {Object}
     * @name pulp.event.custom.mouse:leave
     * @example
     *   var $e = pulp.event;
     *   $e.observe('myDiv', 'mouse:leave', leaveHandler);
     */    
    'mouse:leave': {
      /** @ignore */
      add: function(node, type, handler) {
        if (enterleave) {
          return {type: 'mouseleave'};
        } else {
          return {type: 'mouseout', handler: $p.wrapFunction(handler, $e._enterLeaveWrapper)};
        }
      },
      /** @ignore */
      remove: function(node, ev) {
        return {type: (enterleave ? 'mouseleave' : 'mouseout')};
      }
    }
  })
})();

/**
 * Custom event mouse:hover - behaves like the css :hover pseudoclass
 * @type {Object}
 * @name pulp.event.custom.mouse:hover
 * @example
 *   var $e = pulp.event;
 *   $e.observe('myDiv', 'mouse:hover', enterHandler, leaveHandler);
 */
pulp.event.custom['mouse:hover'] = {
  /** @ignore */
  add: function(node, ev, enter, leave) {
    pulp.event.observe(node, 'mouse:enter', enter);
    pulp.event.observe(node, 'mouse:leave', leave);
    return true;
  },
  /** @ignore */
  remove: function(node, ev, enter, leave) {
    pulp.event.stopObserving(node, 'mouse:enter', enter);
    pulp.event.stopObserving(node, 'mouse:leave', leave);
    return true;
  }
};

// dom:loaded
(function() {
  
  var $e = pulp.event;
  
  // TODO: set loadedEvent
  var isAttached = false, isLoaded = false, timer, loadedEvent;
  // Support for the DOMContentLoaded event is based on work by Dan Webb,
  // Matthias Miller, Dean Edwards, John Resig and Diego Perini
  /**
   * Custom event dom:loaded - fire when document has finished loading.
   * HTML and JavaScript is completely loaded, but CSS, Images, and IFRAME content may not be loaded yet.
   * @type {Object}
   * @name pulp.event.custom.dom:loaded
   * @example
   *   var $e = pulp.event;
   *   $e.observe('myDiv', 'dom:loaded', myHandler); 
   */
  $e.custom['dom:loaded'] = {
    /** @ignore */
    add: function(node, type, handler) {
      if (isLoaded) {
        handler.call(node, loadedEvent);
        return true;
      }
      function fireContentLoadedEvent(event) {
        if (isLoaded) {
          // failsafe to avoid firing handlers twice in race conditions or on WebKit's failsafe window onload handler
          return;
        }
        loadedEvent = event;
        isLoaded = true;
        if (timer) {
          // clear the IE or WebKit polling handler
          window.clearInterval(timer);
        }
        $e.fire(node, "dom:loaded", loadedEvent);
        // don't worry about de-registering handlers here--not really needed
      }

      if (!isAttached) {
        // attach handlers based on browsers
        if (document.addEventListener) {
          // standards-compliant browsers
          if ($e.isWebKit) {
            // WebKit work around
            timer = window.setInterval(function() {
              if (/loaded|complete/.test(pulp.document.readyState)) {
                fireContentLoadedEvent();
              }
            }, 0);

            // as a last resort, attach a handler to the window
            pulp.window.addEventListener("load", fireContentLoadedEvent, false);

          } else {
            // W3C standard  
            pulp.document.addEventListener("DOMContentLoaded",  fireContentLoadedEvent, false);
          }

        } else {
          // IE
          var ieOnReady = function() {
            if (pulp.document.readyState == "complete") {
              pulp.document.detachEvent("onreadystatechange", ieOnReady);
              fireContentLoadedEvent();
            }
          };
          pulp.document.attachEvent("onreadystatechange", ieOnReady);
          // THANKS TO DIEGO PERINI!
          if (pulp.window == top) {
            var ieDoScroll = function() {
              try {
                pulp.document.documentElement.doScroll("left");
              } catch(e) {
                setTimeout(ieDoScroll, 10);
                return;
              }
              fireContentLoadedEvent();
            };
            ieDoScroll();
          }
        }
      }
      isAttached = true;
      // instruct pulp to attach actual "dom:loaded" handlers to the document so we can call $e.fire() later
      return false;
    }

  };
  
  if (pulp.isIE6) {
    // remove circular references to avoid memory leaks in IE6
    window.attachEvent('onunload', function() {
      var id, type;
      for (id in $e._cache) {
        for (type in $e._cache[id]) {
          $e._byId(id).detachEvent(type, $e._cache[id].handler);
        }
      }
    });
  }
  
  // TODO: some type of feature testing
  if ($e.isIE) {
    /** @ignore */
    $e._addDispatcher = function(node, type) {
      var dispatcher = function(evt) {
        var event;
        if (node.nodeType == 1) {
          // make IE's event objects more standard's compliant
          event = evt || document.event;
          event.target = document.srcElement || event.target;
          event.currentTarget = node;
          event.relatedTarget = event[(event.target == event.fromElement ? 'to' : 'from') + 'Element'];
          event.timeStamp = new Date().getTime();
        } else {
          event = {};
        }
        $e.fire(node, type, event);
      };
      node.attachEvent('on' + type, dispatcher);
      return dispatcher;
    };
    /** @ignore */
    $e._removeDispatcher = function(node, type, handler) {
      node.detachEvent('on' + type, handler);
      
    };
  }
})();