/**
 *
 * Phi Event - A multi-paradigm JavaScript library
 *
 *
 *
 * Copyright (c) 2010 Peter Nederlof, Olivier Hermanus, Arjen Geerse.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 *
 */

(function() {

	/**
	 * Constants
	 * 
	 */
	
	var TYPE_STRING = 'string';
	var TYPE_FUNCTION = 'function';

	/**
	 * Shorthands
	 * 
	 */

	var Class = phi.core.Class;

	/**
	 * Event namespace
	 * 
	 */

	phi.event = {

		defaultActions: {},
		eventProxies: {},
	
		/**
		 * adds a event listener to a node
		 * 
		 */

		addListener: function(node, type, listener) {

			if(!Class.implements(listener, EventListener)) {
				throw Error('object does not implement the EventListener interface');
			}
			
			// get the node's EventTarget wrapper, if not found: create it.
			var target = this.getEventTarget(node);
			if(!target) {
				target = new EventTarget(node);
				this.setEventTarget(node, target);				
			}
			
			// only 1 handler per type is ever created for an individual node. 
			var handler = target.getEventHandler(type);
			if(!handler) {
				
				handler = function(e) {
					phi.event.triggerEvent(e.target || e.srcElement || window, type, e, {});
				};

				target.setEventHandler(type, handler);

				if(node.addEventListener) {
					node.addEventListener(type, handler, false);
				} else if(node.attachEvent){
					node.attachEvent('on' + type, handler);
				}
			}

			// if a specific event requires patching,
			var proxy = this.getProxy(type);
			if(proxy) {
				proxy.setup(node);
			}

			// finally; register the listener to the target.
			target.addEventListener(type, listener);
		},

		/**
		 * removes an event listener from a node
		 * 
		 */

		removeListener: function(node, type, listener) {
			var target = this.getEventTarget(node);
			if(target) {
				target.removeEventListener(type, listener);
			}

			// todo: if target has no more listeners: remove dom event and wrapper
		},


		/**
		 * Triggers an event without firing default actions
		 * 
		 */

		triggerEvent: function(node, type, e, data) {
			var element = node;
			var event = new Event(node, type, e, data);

			while (element) {
				var target = this.getEventTarget(element);
				if(target) {
					target.dispatchEvent(event);
				}

				if(event.isPropagationStopped()) {
					break;
				}

				// fallback to window, perhaps do this differently?
				if(element === document) {
					element = window;
				} else {
					element = element.parentNode;
				}
			}

			return event;
		},


		/**
		 * Dispatches an event and fires the default action unless prevented
		 * 
		 */

		dispatchEvent: function(node, type, e, data) {
			var event = this.triggerEvent(node, type, e, data);

			if(!event.isDefaultPrevented()) {
				var action = this.defaultActions[type];
				if(action) {
					action.handleEvent(event);
				} else {
					try {
						node[type]();
					} catch (ignored) {
					}
				}
			}

			return event.returnValue;
		},


		/**
		 * defines a default action for a custom event 
		 * 
		 */

		defineEvent: function(type, listener) {
			if(!Class.implements(listener, EventListener)) {
				throw Error('object does not implement the EventListener interface');
			}
			
			if(this.defaultActions[type]) {
				throw Error('Default listener for "'+ type + '" is already defined.');
			}

			this.defaultActions[type] = listener;
		},

		/**
		 * Set and get EventTarget wrappers, may need a better place than here.
		 * 
		 */

		setEventTarget: function(node, target) {
			var data = phi.data.getData(node);
			data.set('EventTarget', target);
		},
		
		getEventTarget: function(node) {
			var data = phi.data.getData(node);
			return data.get('EventTarget');
		},

		setProxy: function(type, proxy) {
			if(!(proxy instanceof EventProxy)) {
				throw Error('proxy for "'+type+'" is not an EventProxy instance.')
			}

			this.eventProxies[type] = proxy;
		},

		getProxy: function(type) {
			return this.eventProxies[type];
		},

		// http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
		supports: function(type) { 
			var node = document.createElement('div');
			type = 'on' + type;

			var supported = (type in node);
			if(!supported) {
				node.setAttribute(type, "return;");
				supported = typeof node[type] === TYPE_FUNCTION;
			}

			delete node;
			return supported;
		}
	};


	/**
	 * EventTarget, basically a custom implementation of the DOM level 2 EventTarget interface for 
	 * nodes. Allows adding and removing listeners to any type of event, either native or custom.
	 * 
	 */

	var EventTarget = phi.event.EventTarget = new Class({
		__init: function(node) {
			this.listeners = {};
			this.handlers = {};
		},

		/**
		 * adds a listener to this event target
		 * 
		 */

		addEventListener: function(type, listener) {
			var listeners = this.listeners[type];
			if(!listeners) {
				listeners = this.listeners[type] = [];
			}

			listeners.push(listener);
		},

		/**
		 * removes a listener from this event target
		 * 
		 */

		removeEventListener: function(type, listener) {
			var listeners = this.listeners[type];
			if(listeners) {
				var l = listeners.length;
				for(var i=0; i<l; i++) {
					if(listeners[i] === listener) {
						listeners.splice(i, 1);
						break;
					}
				}
			}
		},

		/**
		 * dispatches an event to the target's listeners that share the type
		 * 
		 */

		dispatchEvent: function(event) {
			var type = event.type;
			var listeners = this.listeners[type];
			if(listeners) {
				var l = listeners.length;
				for(var i=0; i<l; i++) {
					listeners[i].handleEvent(event);
				}
			}
		},

		/**
		 * Set and get individual handlers, may need a better place than here.
		 * 
		 */
		
		setEventHandler: function(type, handler) {
			this.handlers[type] = handler;
		},

		getEventHandler: function(type) {
			return this.handlers[type];
		}
	});

	phi.install('EventTarget', EventTarget);


	/**
	 * EventListener interface
	 * 
	 */

	var EventListener = phi.event.EventListener = new Interface({
		handleEvent: function() {}
	});

	phi.install('EventListener', EventListener);


	/**
	 * Event 
	 * 
	 */

	var Event = phi.event.Event = new Class({

		__init: function(target, type, nativeEvent, data) {

			// copy event properties to the wrapper, except for functions
			for(var i in nativeEvent) {
				try {
					if(typeof i === TYPE_STRING && !(nativeEvent[i] instanceof Function)) {
						this[i] = nativeEvent[i];
					}
				} catch (ignore) {
				}
			}

			// then set a minimal amount properties 
			this.target = target;
			this.type = type;
			this.e = nativeEvent;
			this.data = data;
			this.returnValue = true;
			this.stopped = false;
		},

		/**
		 * Prevent the event's default action
		 * 
		 */

		preventDefault: function() {
			this.returnValue = false;
			if(this.e) {
				try {
					this.e.preventDefault();	
				} catch (isIE) {
					this.e.returnValue = false;
				}
			}
		},

		/**
		 * Check whether the event's default action is prevented
		 * 
		 */

		isDefaultPrevented: function() {
			return !this.returnValue;
		},

		/**
		 * Stops the propagation of native events
		 * 
		 */

		stopPropagation: function() {
			this.stopped = true;
			if(this.e) {
				try {
					this.e.stopPropagation();
				} catch (isIE) {
					this.e.cancelBubble = true;
				}
			}
		},
		
		/**
		 * checks whether propagation of the event is stopped
		 * 
		 */
		
		isPropagationStopped: function() {
			return this.stopped;
		}

	});

	phi.install('Event', Event);



	/**
	 * Proxies for various events
	 * 
	 */

	var EventProxy = phi.event.EventProxy = new Class({
		implements: EventListener,

		__init: function(event, target, proxy) {
			this.blank = { handleEvent: function() { }};
			this.event = event;
			this.target = target;
			this.proxy = proxy;
		},

		setup: function(node) {
			// only patch if node is an unsupported type. 
			if(!this.target.test(node.nodeName)) {
				phi.event.addListener(node, this.proxy, this);
			}
		},

		handleEvent:function(e) {
			var node = e.target;
			while(node) {
				if(this.target.test(node.nodeName)) {
					var data = phi.data.getData(node);
					var proxy = this.event + 'Proxy';
					
					if(!data.get(proxy)) {
						// bind a dud listener, which causes the event to bubble in phi.event
						phi.event.addListener(node, this.event, this.blank);
						data.set(proxy, true);
					}
					return;
				}
				node = node.parentNode;
			}
		}
	});

	phi.install('EventProxy', EventProxy);


	// check for IE specifically, which false positives on submit and change bubbling
	var ielte8 = (document.documentMode && document.documentMode < 9);
	
	// bind a local submit via a click proxy
	if(!phi.event.supports('submit') || ielte8) {
		phi.event.setProxy('submit', new EventProxy('submit', /form/i, 'click'));
	}
	
	// bind a local change via a mousedown proxy
	if(!phi.event.supports('change') || ielte8) {
		phi.event.setProxy('change', new EventProxy('change', /input|select|textarea/i, 'mousedown'));
	}

	phi.event.mouse = {
		isChildOf: function (p, c) {
			while (c && c !== p) { c = c.parentNode; }
			return c === p;
		},
		leave: function (o, e) {
			e = e || window.event;
			return !this.isChildOf(o, e.relatedTarget || e.toElement);
		},
		enter: function (o, e) {
			e = e || window.event;
			return !this.isChildOf(o, e.relatedTarget || e.fromElement);
		}
	}


})();
