/**
 * All intellectual property rights in this Software throughout the world belong to UK Radioplayer, 
 * rights in the Software are licensed (not sold) to subscriber stations, and subscriber stations 
 * have no rights in, or to, the Software other than the right to use it in accordance with the 
 * Terms and Conditions at www.radioplayer.co.uk/terms. You shall not produce any derivate works 
 * based on whole or part of the Software, including the source code, for any other purpose other 
 * than for usage associated with connecting to the UK Radioplayer Service in accordance with these 
 * Terms and Conditions, and you shall not convey nor sublicense the Software, including the source 
 * code, for any other purpose or to any third party, without the prior written consent of UK Radioplayer.
 *
 * provides the bind method for all models and controllers in the radioplayer.models
 * and radioplayer.controllers namespace.
 * Bind will instantiate the event listeners and event delegation
 * unbind will remove all the event listeners & delegation
 * rebind will remove and re-instantiate the event listeners. This is needed if 
 * html elements have been added and no delegation was used.
 * @author Michael Stillwell <michael.stillwell@bbc.co.uk>
 * @author Gilles Ruppert <gilles.ruppert@bbc.co.uk>
 */

glow.lang.apply(radioplayer.utils.events, {
    // Regular expression matching e.g. "div.player a.add_favourite click".  Breaks this
    // into a CSS selector ("div.player a.add_favourite") and an event ("click").
    pattern: /^\s*(.+)\s+(\S+)\s*$/,


    /**
     * Iterates through the keys of "this" (including this.prototype) looking for CSS event
     * selectors. e.g. "a.play click".  If such selectors are found, the corresponding value is
     * bound to the elements returned by the selector using glow.events.addListener().
     *
     * If delegate is provided, then the events that _can_ be delegated, will be delegated
     * on to that element
     *
     * To use this function on object literals, invoke with bind.call(object, delegate);
     *
     * @param {Element | nodespec | glow.dom.NodeList} [delegate] parent container to which the click events will be delegated
     */
    bind: function(delegate) {

		var that = this; // that is a reference to the object we are bound to

		var selector,
		eventName,
		property,
		pattern = radioplayer.utils.events.pattern,
		matches,
		$delegate, // cache of the glow object
		existingEvents = {}, // temporary object that holds the available event types for the current object
		delegatedEvents = ['click', 'dblclick', 'mouseover', 'mouseout', 'focus', 'blur', 'keyup']; // events that can be used for event delegation


		// holds all the eventnames, selectors & callbacks associated with this object
		this._eventRegistry = {};


		// normalise the delegate
		if (delegate) {
			$delegate = glow.dom.get(delegate);
			delegate = $delegate[0];
		}


		// If module has methods/keys matching e.g. "div.player a.add_favourite click",
		// register associated value as callback on CSS selector "div.player a.add_favourite".

		for (property in this) {

			// NOTE: There's no hasOwnProperty() filter because in this case we *do* want to follow
			//       the prototype chain.

			if ((matches = pattern.exec(property))) {

			selector = matches[1];
			eventName = matches[2];

			addToRegistry(selector, eventName, this[property]);

			}
		}

		setupEvents();

		/**
		 * adds the passed selector to the event registry
		 * @param {string} selector css selector of the element to be used
		 * @param {string} eventName
		 * @param {callback} callback callback function to be executed when the
		 *					 event is triggered
		 * @returns {object} returns the updated eventRegistry
		 */
		function addToRegistry(selector, eventName, callback) {

			if (!(eventName in that._eventRegistry)) {
				that._eventRegistry[eventName] = {};
				existingEvents[eventName] = true;
			}

			// if the selector doesn't exist yet...
			if (!(selector in that._eventRegistry[eventName])) {

				that._eventRegistry[eventName][selector] = {};

				// we add it and add the callback
				that._eventRegistry[eventName][selector].callback = callback;
			}
			// else we throw an error
			else {
				throw new Error('the selector' + selector + ' has already been used in this context');
			}

			return that._eventRegistry;
		}


		function setupEvents() {
			var eventName,
			selector,
			customEvent,
			event;

			// if we have a delegate, bind all the delegatedEvents to it

			if (delegate) {

				// currently we only support event delegation for
				// click, mouseover and mouseout events
				// any further allowable events can be added in the delegatedEvents array

				for (var i = 0, len = delegatedEvents.length; i < len; i++) {

					if (delegatedEvents[i] in that._eventRegistry) {
						event = delegatedEvents[i];
						delete existingEvents[event]; // remove the current event from the object

						// we are using a closure here to make sure further iterations of the 
						// loop are not overwriting the vars used
						that._eventRegistry[event].delegate = (function(del, eName, reg) {

							return glow.events.addListener(del, eName, function(e) {
								var source = null,
									isSelectorFound = false,
									$del = glow.dom.get(del);

								// if this is a click event, we loop through all the selectors to 
								// see whether one matches exactly
								if (eName.indexOf('click') !== -1) {

									for (selector in reg[eName]) {
										if (reg[eName].hasOwnProperty(selector) && selector !== 'delegate') {


											// we don't want to make this check if it is a mouse event
											// as it is quite expensive
											if ($del.get(e.source).is(selector)) {
												isSelectorFound = true;

												try {
													reg[eName][selector].callback.call(that, e);
												}
												catch(e) {
													throw new Error('Callback could not be called. ' + e + "\nEvent: " + eName + "\nSelector: " + selector);
												}
											}
										}
									}
								}
								// if the selector was not found yet, move up the tree & rebase to a parent
								if (! isSelectorFound) {

									for (selector in reg[eName]) {
										if (reg[eName].hasOwnProperty(selector) && selector !== 'delegate') {

											// isChild method makes sure that we also listen to children of the target
											// i.e. if we listen to the node spec 'a', we also want to listen to 'a *'
											// as the target would be * in this scenario
											// at the same time as aserting, we assign to avoid doing the same execution twice.
											if ((source = radioplayer.utils.rebase(e.source, selector))) {
												// this makes sure the source is set to what we are actually listening to
												// rather than a child possibly
												e.source = source;

												try {
													reg[eName][selector].callback.call(that, e);
												}
												catch(e) {
													throw new Error('Callback could not be called. ' + e + "\nEvent: " + eName + "\nSelector: " + selector);
												}
											}
										}
									}
								}
							});

						})(delegate, event, that._eventRegistry);
					}
				}
			}


			// bind the remaining events

			for (eventName in existingEvents) {
				if (existingEvents.hasOwnProperty(eventName)) {

					// Special case for "subscribe": in this case we bind to the document object.

					if (eventName === "subscribe") {

						for (customEvent in that._eventRegistry[eventName]) {
							if (that._eventRegistry[eventName].hasOwnProperty(customEvent)) {

								that._eventRegistry[eventName][customEvent].listener = addEventListener(document, customEvent, that._eventRegistry[eventName][customEvent].callback, that);
							}
						}
					}
					else {

						// Bind events indiscriminately
						for (selector in that._eventRegistry[eventName]) {
							if (that._eventRegistry[eventName].hasOwnProperty(selector)) {

								that._eventRegistry[eventName][selector].listener = addEventListener(selector, eventName, that._eventRegistry[eventName][selector].callback, that);
							}
						}

					}
				}

				delete existingEvents[eventName];
			}
		}


		//  little helper function to avoid closure hell, else further listeners
		//	will overwrite the different vars

		function addEventListener(item, event, callback, context) {
			return glow.events.addListener(item, event, function(e) {
				callback.call(context, e);
			}, context);
		}

    },


    /**
     * unbinds the events of the current object
     *
     * if called without any parameters, all the events of the current object are removed
     * if called with an event string in the format '#container a click', only that event is
     * removed.
     *
     * N.B: the eventstring has NOTHING to do with delegate you might have used in the 1st place.
     * 	The eventstring has to be exactly the same as the one used during the initial binding,
     * 	@example: To unbind a specific callback, whether it was delegated or not:
     * 	original binding:
     * 		".container a": function(e) { ... } // original binding in the object
     * 		"radioplayer:testCustom subscribe": function(e) { ... }
     * 		...
     * 		this.bind(delegateNodespec); // binding the events of the objects with a delegate
     *
     * 	To unbind a specific event:
     * 		this.unbind('.container a');
     * 
     *  To unbind all events from the object:
     *      this.unbind();
     *
     * @param {String} [eventstring] same string used to bind the event in the controller
     */
    unbind: function(eventstring) {
		var that = this,
		matches,
		selector,
		eventName,
		reg = that._eventRegistry,
		listeners = [];

		// if no event string was passed, we remove all the events from the object
		if (!eventstring) {

			for (var e in reg) {

				// there is no need for hasOwnProperty since we are checking for the
				// existence of the property in either way

				// removed the delegated events
				if (reg[e].delegate) {
					listeners.push(reg[e].delegate);
					delete reg[e];
				}

				// remove the individual events
				for (var s in reg[e]) {
					if (reg[e][s].listener) {
						listeners.push(reg[e][s].listener);
						delete reg[e][s];
					}
				}

				// remove this event from the registry
				delete reg[e];
			}
		}
		// else we check whether there is an event of this type & we remove the listener
		// or the callback if it is a delegated event
		else {

			if ((matches = radioplayer.utils.events.pattern.exec(eventstring))) {

				selector = matches[1];
				eventName = matches[2];

				// check whether we have a registered listener
				if (!!reg[eventName][selector].listener) {
					listeners.push(reg[eventName][selector].listener);
				}
				// else check whether this selector exists as a delegate
				else if (!!reg[eventName].delegate) {
					delete reg[eventName][selector];
				}

			}
		}

		// delete the listeners
		glow.events.removeListener(listeners);
    },

    /**
     * rebinds all the events of the current object
     * 
     * this can be necessary when items are added to the DOM that have not been 
     * delegated, i.e. CTAs which use mouseenter, mouseleave. Call this on the 
     * object you want to rebind.
     * 
     * @param {Element | glow.dom.NodeList | NodeSpec} [delegate] the parent container. 
     *			Should only be passed if it was used for the inital bind()
     */
    rebind: function(delegate) {
		this.unbind();
		this.bind(delegate);
    }

});

