/**
 * Interface EventListener
 *
 * The EventListener interface is the primary method for handling events.
 * Users implement the EventListener interface and register their listener
 * on an EventTarget using the AddEventListener method.
 * http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventListener
 * (see addEventListener for description of parameters)
 */
function EventListener(type, listener, useCapture) {
	this.type = type;
	this.listener = listener;
	this.useCapture = useCapture;

	/**
	 * handleEvent
	 * Calls the listener function if listening for the event (evt).
	 * @param evt - The Event contains contextual information about the event.
	 *              It also contains the stopPropagation and preventDefault
	 *              methods which are used in determining the event's flow
	 *              and default action.
	 * http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventListener-handleEvent
	 */
	this.handleEvent = function(evt) {
		// Check that this hasn't been removed.  See EventTarget.removeEventListener(); for more information
		if (!this.removed) {
			// Only bubbling phase is supported, don't handle capture events
			if (evt.type == this.type && !this.useCapture) {
				listener.apply(evt.currentTarget, [evt]);
			}
		}
	}
}

/*
 * Interface EventListenerList
 * Based on the proposed interface in DOM 3 Events
 * Used, for each EventTarget, to store the list of EventListeners
 */
function EventListenerList() {
	var list = new Array();
	list.item = function(index) {
		return list[index];
	}
	return list;
}

/**
 * Interface Event
 *
 * The Event interface is used to provide contextual information about an
 * event to the handler processing the event. An object which implements
 * the Event interface is generally passed as the first parameter to an
 * event handler.
 */
var Event = (function() {

	// Event Types defined in DOM2
	var eventType = new Array();
	// Mouse Events
	// http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-MouseEvent
	eventType["click"]     = {bubbles:true, cancelable:true};
	eventType["mousedown"] = {bubbles:true, cancelable:true};
	eventType["mouseup"]   = {bubbles:true, cancelable:true};
	eventType["mouseover"] = {bubbles:true, cancelable:true};
	eventType["mousemove"] = {bubbles:true, cancelable:false};
	eventType["mouseout"]  = {bubbles:true, cancelable:true};

	// Keyboard Events
	// Not defined in DOM2, but widely supported
	eventType["keydown"]   = {bubbles:true, cancelable:true};
	eventType["keyup"]     = {bubbles:true, cancelable:true};
	eventType["keypress"]  = {bubbles:true, cancelable:true};

	// HTML Events
	// http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-eventgroupings-htmlevents
	eventType["load"]      = {bubbles:false,cancelable:false};
	eventType["unload"]    = {bubbles:false,cancelable:false};
	eventType["abort"]     = {bubbles:true, cancelable:false};
	eventType["error"]     = {bubbles:true, cancelable:false};
	eventType["select"]    = {bubbles:true, cancelable:false};
	eventType["change"]    = {bubbles:true, cancelable:false};
	eventType["submit"]    = {bubbles:true, cancelable:true};
	eventType["reset"]     = {bubbles:true, cancelable:false};
	eventType["focus"]     = {bubbles:false,cancelable:false};
	eventType["blur"]      = {bubbles:false,cancelable:false};
	eventType["resize"]    = {bubbles:true, cancelable:false};
	eventType["scroll"]    = {bubbles:true, cancelable:false};

	/**
	 * Constructor for the Event interface
	 * @param currentTarget - Used to indicate the EventTarget whose EventListeners
	 *                        are currently being processed. This is particularly
	 *                        useful during capturing and bubbling.
	 */
	function constructor(currentTarget) {

		/*
		 * Event.bubbles
		 * Used to indicate whether or not an event is a bubbling event.
		 * If the event can bubble the value is true, else the value is false.
		 */
		if (this.bubbles == undefined) {
			if (eventType[this.type]) {
				this.bubbles = eventType[this.type].bubbles;
			} else {
				// Default to true for unknown events
				this.bubbles = true;
			}
		}

		/*
		 * Event.cancelable
		 * Used to indicate whether or not an event can have its default action prevented.
		 * If the default action can be prevented the value is true, else the value is false.
		 */
		if (this.cancelable == undefined) {
			if (eventType[this.type]) {
				this.cancelable = eventType[this.type].cancelable;
			} else {
				// Default to true for unknown events
				this.cancelable = true;
			}
		}

		/*
		 * Event.currentTarget
		 * Used to indicate the EventTarget whose EventListeners are currently being processed.
		 * This is particularly useful during capturing and bubbling.
		 */
		if (this.currentTarget == undefined) {
			if (currentTarget == window) {
				this.currentTarget = document;
			} else {
				this.currentTarget = currentTarget;
			}
		}

		/*
		 * Event.target
		 * Used to indicate the EventTarget to which the event was originally dispatched.
		 */
		if (!this.target && window.event) {
			if (window.event.srcElement) {
				this.target = window.event.srcElement;
			} else {
				this.target = document;
			}
		}

		/*
		 * Event.timeStamp
		 * Used to specify the time (in milliseconds relative to the epoch) at which the event was created.
		 * Note: At time of writing, although Gecko and Opera claim to support this, they 
		 * seem to always return 0.  There is no way to alter that value.
		 * The epoch is 1970-01-01T00:00:00Z
		 */
		if (this.timeStamp == undefined) {
			this.timeStamp = new Date().valueOf();
		}

		/*
		 * Event.eventPhase
		 * Used to indicate which phase of event flow is currently being evaluated.
		 *
		 * This assumes bubbling phase if the current target is not the event target.
		 * This is because obj.onevent properties use bubbling phase in all modern browsers.
		 * Although Netscape 4.x only supports capturing, who really cares about that old disaster?
		 *
		 * There is currently a bug in IE which, for some unkonw reason,
		 * this.currentTarget != this.tager, even though they are the same object
		 * when the event is AT_TARGET.  As a result, IE6 will always report
		 * BUBBLING_PHASE.
		 *
		 * All tests indicate that my logic is correct (it works in Opera),
		 * I'm convinced it's a bug in IE and I don't beleive there is a
		 * usable solution.  Please prove me wrong!
		 */
		if (!this.eventPhase) {
			var result = (this.currentTarget == this.target)
			if (result) {
				this.eventPhase = Event.AT_TARGET;
			} else {
				this.eventPhase = Event.BUBBLING_PHASE;
			}
		} else if (this.eventPhase == Event.AT_TARGET
				&& currentTarget != this.target) {
			this.eventPhase = Event.BUBBLING_PHASE;
		}

		/*
		 * If an event is cancelable, the preventDefault method is used to signify that
		 * the event is to be canceled, meaning any default action normally taken by
		 * the implementation as a result of the event will not occur.
		 * http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-Event-preventDefault
		 */
		if (!this.preventDefault) {
			this.preventDefault = function() {
				if (this.cancelable) this.returnValue = false;
			}
		}

		/*
		 * The stopPropagation method is used prevent further propagation of an event
		 * during event flow. If this method is called by any EventListener the event
		 * will cease propagating through the tree. The event will complete dispatch
		 * to all listeners on the current EventTarget before event flow stops. This
		 * method may be used during any stage of event flow.
		 * http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-Event-stopPropagation
		 */
		if (!this.stopPropagation) {
			this.stopPropagation = function() {
				this.cancelBubble = true;
			}
		}

		/*
		 * For Mouse and Keyboard events, apply any required fixes.
		 * See functions: MouseEvent() and KeyboardEvent()
		 */
		switch(this.type) {
			case "click":
			case "mousedown":
			case "mouseup":
			case "mouseover":
			case "mousemove":
			case "mouseout":
				MouseEvent.apply(this);
			case "keydown":
			case "keyup":
			case "keypress":
				KeyboardEvent.apply(this);
		}
	}
	return constructor;
})();

function PhaseType() {
	this.CAPTURING_PHASE = 1;
	this.AT_TARGET       = 2;
	this.BUBBLING_PHASE  = 3;
}
PhaseType.apply(Event);

/* Interface MouseEvent
 * This will eventually be filled out to make up for browser limitations with these 
 * properties, where necessary and where possible:
 *
 * altKey, button, clientX, clientY, ctrlKey, metaKey, relatedTarget, screenX,
 * screenY, shiftKey and initMouseEvent,
 * http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-MouseEvent
 */
function MouseEvent() {
}

/* Interface KeyboardEvent
 * To be written later, possibly based on the draft DOM 3 Keyboard Events
 */
function KeyboardEvent() {
}

/*
 * Interface EventTarget
 *
 * The interface allows registration and removal of EventListeners on an
 * EventTarget and dispatch of events to that EventTarget.
 * http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventTarget
 */
function EventTarget() {

	// Store each EventListenerList with its EventTarget
	this.eventListenerList = null;

	/*
	 * Only implement the the funciton if it's not supported by the browser.
	 * However, we need to overide document.addEventListener to add support
	 * for the "load" event in Gecko.  Although this will actually override
	 * it for all browsers, this will have no effect because
	 *   window.onload = fn;
	 * is exactly equivalent to
	 *   document.addEventListener("load", fn, false);
	 *
	 * CAPTURE_PHASE events are unsupported by these extensions, however
	 * this also does not prevent their registration in browsers that do.
	 * Thus, document.addEventListener("load", fn, true); will, if possible,
	 * register using _addEventLister(); (see below)
	 */
	if (this == document || !this.addEventListener) {

		// If 'this' is 'document' and the browser supports addEventLister,
		// store a reference to the function for later use.
		// This will always be null for an Element
		var _addEventListener = null;
		if (this == document) {
			_addEventListener = this.addEventListener || null;
		}

		/*
		 * EventTarget.addEventListener
		 *
		 * This method allows the registration of event listeners on the event
		 * target. If an EventListener is added to an EventTarget while it is
		 * processing an event, it will not be triggered by the current actions
		 * but may be triggered during a later stage of event flow, such as
		 * the bubbling phase.
		 * (Note: This restriction is enforced within dispatchEvent();  See below)
		 * http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventTarget-addEventListener
		 */
		this.addEventListener = function(type, listener, useCapture) {
			if (!_addEventListener || (this == document && type == "load" && !useCapture)) {

				// Need to attach load events to the window object,
				// other events can be applied to document.
				var obj = this;
				if (type == "load" && obj == document) obj = window;

				/* If no events have been registered for this EventTarget previously,
				 * it will not have an associated eventListenerList, so it needs to be
				 * created.
				 *
				 * Note: Even though "load" events for document will be registered on the window object,
				 * the eventListenerList will still be associated with the document object.
				 */
				if (!this.eventListenerList) this.eventListenerList = new EventListenerList();
				this.eventListenerList.push(new EventListener(type, listener, useCapture));

				// Prefix type with "on" and add assocate an event listener with that event for the object.
				var eventName = "on" + type;
				if (!obj[eventName]) {
					
					// This is like calling object.onEvent.
					// e.g. If object is a reference to an element and the type is "click",
					// it's like setting element["onclick"}, which is equivalent to element.onclick
					obj[eventName] = function(evt) {

						// currentTarget will be a reference to the window object for laod evets
						// This needs to be resent to document before event processing,
						// this is because its document.addEventListener, not window.addEventListener

						var currentTarget = this;
						if (currentTarget == window) currentTarget = document;
						currentTarget.dispatchEvent(evt);
					}
				}
			} else if (_addEventListener) {
				/*
				 * Ensure _addEventListener() really exists since the above condtion
				 * returns false in any browser when the parameters ("load", fn, true)
				 * are passed.
				 *
				 * CAPTURE_PHASE events are unsupported, so unless the browser support them
				 * itself, they won't be fired anyway, so not registering it has no effect.
				 */

				_addEventListener(type, listener, useCapture);
			}
		}
	}


	/*
	 * Only implement the the funciton if it's not supported by the browser.
	 * However, we need to overide document.removeEventListener to add support
	 * for the removing the "load" event in Gecko.  See the description for 
	 * addEventListener() for more information.
	 */
	if (this == document || !this.removeEventListener) {

		// If 'this' is 'document' and the browser supports removeEventLister,
		// store a reference to the function for later use.
		// This will always be null for an Element
		var _removeEventListener = null;
		if (this == document) {
			_removeEventListener = this.removeEventListener || null;
		}

		/*
		 * EventTarget.removeEventListener
		 *
		 * This method allows the removal of event listeners from the event target.
		 * If an EventListener is removed from an EventTarget while it is processing
		 * an event, it will not be triggered by the current actions. EventListeners
		 * can never be invoked after being removed.  Calling removeEventListener with
		 * arguments which do not identify any currently registered EventListener on
		 * the EventTarget has no effect.
		 * (Note: This restriction is enforced using the 'removed' flag.  See below)
		 *
		 * http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventTarget-removeEventListener
		 */
		this.removeEventListener = function(type, listener, useCapture) {
			function isMatch(element, type, listener, useCapture) {
				return (element.type == type
					 && element.listener == listener
					 && element.useCapture == useCapture); 
			}

			if (!_addEventListener || (this == document && type == "load")) {
				var filteredList = new EventListenerList()
				for (var i = 0; i < this.eventListenerList.length; i++) {
					if (isMatch(this.eventListenerList.item(i), type, listener, useCapture)) {
						// Flag this EventListener as removed to prevent triggering this listener
						// See the description of the removed flag in EventListenr.handleEvent();
						this.eventListenerList.item(i).removed = true;
					} else {
						filteredList.push(this.eventListenerList.item(i));
					}
				}
				this.eventListenerList = filteredList;
			} else {
				_removeEventListener(type, listener, useCapture);
			}
		}
	}


	/*
	 * Only implement the the funciton if it's not supported by the browser.
	 * However, we need to overide document.dispatchEvent to add support
	 * for the removing the "load" event in Gecko.  See the description for 
	 * addEventListener() for more information.
	 *
	 * Note: Even though this is overriding document.dispatchEvent, this function
	 * is never called for events other than "load" in browsers that support
	 * document.addEventListener() natively.
	 */
	if (this == document || !this.dispatchEvent) {
		this.dispatchEvent = function(evt) {
			if (!evt) evt = window.event;

			Event.apply(evt, [this]);
			if (this.eventListenerList) {

				/*
				 * Make a copy of the eventListeners array.
				 * This ensures that any events registered using addEventListener
				 * during this event processing, which would added this.eventListenerList,
				 * do not get executed, as defined in DOM2.
				 *
				 * The condition that removed event listeners don't get triggered is enforced
				 * in EventTarget.removeEventLister() and EventListener.handleEvent with the
				 * 'removed' flag.
				 */
				var list = new Array();
				list = list.concat(this.eventListenerList)

				// Call handleEvent for every event listener, it will decide
				// whether or not the listener is actually listening for this event.
				for (var i = 0; i < list.length; i++) {
					list[i].handleEvent(evt);
				}
			}
		}
	}
}
EventTarget.apply(document);
EventTarget.apply(Element.prototype);
