Object.extend(Event, (function() {
  var cache = Event.cache;

  function getEventID(element) {
    // Event ID is stored as the 0th index in a one-item array so that it
    // won't get copied to a new node when cloneNode is called.
    if (element === window) return 1;
    if (element._prototypeEventID) return element._prototypeEventID[0];
    return (element._prototypeEventID = [arguments.callee.id++])[0];
  }
  getEventID.id = 2;

  function getDOMEventName(eventName) {
    if (eventName && eventName.include(':')) return "dataavailable";
    return eventName;
  }

  function createWrapperFunction(element, eventName, handler) {
    return function(event) {
      if (!Event || !Event.extend ||
       (event.eventName && event.eventName != eventName))
        return false;

      handler.call(element, Event.extend(event));
    };
  }

  if (!Prototype.Browser.IE) {
    var events = { enter: 'over', leave: 'out' },
     isEnterLeave = /^mouse(enter|leave)$/;

    getDOMEventName = getDOMEventName.wrap(function(proceed, eventName) {
      var EL = isEnterLeave.exec(eventName);
      if (EL) eventName = 'mouse' + events[EL[1]];
      return proceed(eventName);
    });

    createWrapperFunction = createWrapperFunction.wrap(function(proceed, element, eventName, handler) {
      var wrapper = proceed(element, eventName, handler);
      if (isEnterLeave.test(eventName)) {
        wrapper = wrapper.wrap(function(proceed, event) {
          var parent = event.relatedTarget;
          while (parent && parent != element) {
            try { parent = parent.parentNode; }
            catch(e) { parent = element; }
          }
          if (parent == element) return;
          proceed(event);
        });
      }
      return wrapper;
    });
  }

  function getCacheForID(id) {
    return cache[id] = cache[id] || { };
  }

  function getWrappersForEventName(id, eventName) {
    var c = getCacheForID(id);
    return c[eventName] = c[eventName] || [];
  }

  function createWrapper(element, eventName, handler) {
    var id = getEventID(element), c = getCacheForID(id);

    // Attach the element itself onto its cache entry so we can retrieve it for
    // cleanup on page unload.
    if (!c.element) c.element = element;

    var w = getWrappersForEventName(id, eventName);
    if (w.pluck("handler").include(handler)) return false;

    var wrapper = createWrapperFunction(element, eventName, handler);

    wrapper.handler = handler;
    w.push(wrapper);
    return wrapper;
  }

  function findWrapper(id, eventName, handler) {
    var w = getWrappersForEventName(id, eventName);
    return w.find(function(wrapper) { return wrapper.handler == handler });
  }

  function destroyWrapper(id, eventName, handler) {
    var c = getCacheForID(id);
    if (!c[eventName]) return false;
    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
  }
  
  // Loop through all elements and remove all handlers on page unload. IE
  // needs this in order to prevent memory leaks.
  function purgeListeners() {
    var element, entry;
    for (var i in Event.cache) {
      entry = Event.cache[i];
      Event.stopObserving(entry.element);
      entry.element = null;
    }
  }

  function onStop() {
    document.detachEvent("onstop", onStop);
    purgeListeners();
  }

  function onBeforeUnload() {
    if (document.readyState === "interactive") {
      document.attachEvent("onstop", onStop);
      (function() { document.detachEvent("onstop", onStop); }).defer();
    }
  }

  if (window.attachEvent && !window.addEventListener) {
    // Internet Explorer needs to remove event handlers on page unload
    // in order to avoid memory leaks.
    window.attachEvent("onunload", purgeListeners);

    // IE also doesn't fire the unload event if the page is navigated away
    // from before it's done loading. Workaround adapted from
    // http://blog.moxiecode.com/2008/04/08/unload-event-never-fires-in-ie/.
    window.attachEvent("onbeforeunload", onBeforeUnload);
  }

  // Safari has a dummy event handler on page unload so that it won't
  // use its bfcache. Safari <= 3.1 has an issue with restoring the "document"
  // object when page is returned to via the back button using its bfcache.
  else if (Prototype.Browser.WebKit) {
    window.addEventListener("unload", Prototype.emptyFunction, false);
  }

  return {
    observe: function(element, eventName, handler) {
      element = $(element);
      var name = getDOMEventName(eventName);

      var wrapper = createWrapper(element, eventName, handler);
      if (!wrapper) return element;

      if (element.addEventListener) {
        element.addEventListener(name, wrapper, false);
      } else {
        element.attachEvent("on" + name, wrapper);
      }

      return element;
    },

    stopObserving: function(element, eventName, handler) {
      element = $(element);
      eventName = Object.isString(eventName) ? eventName : null;
      var id = getEventID(element), c = cache[id];

      if (!c) {
        return element;
      }
      else if (!handler && eventName) {
        getWrappersForEventName(id, eventName).each(function(wrapper) {
          Event.stopObserving(element, eventName, wrapper.handler);
        });
        return element;
      }
      else if (!eventName) {
        Object.keys(c).without("element").each(function(eventName) {
          Event.stopObserving(element, eventName);
        });
        return element;
      }

      var wrapper = findWrapper(id, eventName, handler);
      if (!wrapper) return element;

      var name = getDOMEventName(eventName);
      if (element.removeEventListener) {
        element.removeEventListener(name, wrapper, false);
      } else {
        element.detachEvent("on" + name, wrapper);
      }
      destroyWrapper(id, eventName, handler);

      return element;
    },

    fire: function(element, eventName, memo) {
      element = $(element);
      if (element == document && document.createEvent && !element.dispatchEvent)
        element = document.documentElement;

      var event;
      if (document.createEvent) {
        event = document.createEvent("HTMLEvents");
        event.initEvent("dataavailable", true, true);
      } else {
        event = document.createEventObject();
        event.eventType = "ondataavailable";
      }

      event.eventName = eventName;
      event.memo = memo || { };

      if (document.createEvent) {
        element.dispatchEvent(event);
      } else {
        element.fireEvent(event.eventType, event);
      }

      return Event.extend(event);
    }
  };
})());

Object.extend(Event, Event.Methods);
Element.addMethods({
  fire:          Event.fire,
  observe:       Event.observe,
  stopObserving: Event.stopObserving
});

Object.extend(document, {
  fire:          Element.Methods.fire.methodize(),
  observe:       Element.Methods.observe.methodize(),
  stopObserving: Element.Methods.stopObserving.methodize(),
  loaded:        false
});

