/* global hasOwn, slice */

( function () {

	/**
	 * @class
	 */
	OO.EventEmitter = function OoEventEmitter() {
		// Properties

		/**
		 * Storage of bound event handlers by event name.
		 *
		 * @private
		 * @property {Object} bindings
		 */
		this.bindings = {};
	};

	OO.initClass( OO.EventEmitter );

	/* Private helper functions */

	/**
	 * Validate a function or method call in a context
	 *
	 * For a method name, check that it names a function in the context object
	 *
	 * @private
	 * @param {Function|string} method Function or method name
	 * @param {any} context The context of the call
	 * @throws {Error} A method name is given but there is no context
	 * @throws {Error} In the context object, no property exists with the given name
	 * @throws {Error} In the context object, the named property is not a function
	 */
	function validateMethod( method, context ) {
		// Validate method and context
		if ( typeof method === 'string' ) {
			// Validate method
			if ( context === undefined || context === null ) {
				throw new Error( 'Method name "' + method + '" has no context.' );
			}
			if ( typeof context[ method ] !== 'function' ) {
				// Technically the property could be replaced by a function before
				// call time. But this probably signals a typo.
				throw new Error( 'Property "' + method + '" is not a function' );
			}
		} else if ( typeof method !== 'function' ) {
			throw new Error( 'Invalid callback. Function or method name expected.' );
		}
	}

	/**
	 * @private
	 * @param {OO.EventEmitter} eventEmitter Event emitter
	 * @param {string} event Event name
	 * @param {Object} binding
	 */
	function addBinding( eventEmitter, event, binding ) {
		var bindings;
		// Auto-initialize bindings list
		if ( hasOwn.call( eventEmitter.bindings, event ) ) {
			bindings = eventEmitter.bindings[ event ];
		} else {
			bindings = eventEmitter.bindings[ event ] = [];
		}
		// Add binding
		bindings.push( binding );
	}

	/* Methods */

	/**
	 * Add a listener to events of a specific event.
	 *
	 * The listener can be a function or the string name of a method; if the latter, then the
	 * name lookup happens at the time the listener is called.
	 *
	 * @param {string} event Type of event to listen to
	 * @param {Function|string} method Function or method name to call when event occurs
	 * @param {Array} [args] Arguments to pass to listener, will be prepended to emitted arguments
	 * @param {Object} [context=null] Context object for function or method call
	 * @return {OO.EventEmitter}
	 * @throws {Error} Listener argument is not a function or a valid method name
	 */
	OO.EventEmitter.prototype.on = function ( event, method, args, context ) {
		validateMethod( method, context );

		// Ensure consistent object shape (optimisation)
		addBinding( this, event, {
			method: method,
			args: args,
			context: ( arguments.length < 4 ) ? null : context,
			once: false
		} );
		return this;
	};

	/**
	 * Add a one-time listener to a specific event.
	 *
	 * @param {string} event Type of event to listen to
	 * @param {Function} listener Listener to call when event occurs
	 * @return {OO.EventEmitter}
	 */
	OO.EventEmitter.prototype.once = function ( event, listener ) {
		validateMethod( listener );

		// Ensure consistent object shape (optimisation)
		addBinding( this, event, {
			method: listener,
			args: undefined,
			context: null,
			once: true
		} );
		return this;
	};

	/**
	 * Remove a specific listener from a specific event.
	 *
	 * @param {string} event Type of event to remove listener from
	 * @param {Function|string} [method] Listener to remove. Must be in the same form as was passed
	 * to "on". Omit to remove all listeners.
	 * @param {Object} [context=null] Context object function or method call
	 * @return {OO.EventEmitter}
	 * @throws {Error} Listener argument is not a function or a valid method name
	 */
	OO.EventEmitter.prototype.off = function ( event, method, context ) {
		if ( arguments.length === 1 ) {
			// Remove all bindings for event
			delete this.bindings[ event ];
			return this;
		}

		validateMethod( method, context );

		if ( !hasOwn.call( this.bindings, event ) || !this.bindings[ event ].length ) {
			// No matching bindings
			return this;
		}

		// Default to null context
		if ( arguments.length < 3 ) {
			context = null;
		}

		// Remove matching handlers
		var bindings = this.bindings[ event ];
		var i = bindings.length;
		while ( i-- ) {
			if ( bindings[ i ].method === method && bindings[ i ].context === context ) {
				bindings.splice( i, 1 );
			}
		}

		// Cleanup if now empty
		if ( bindings.length === 0 ) {
			delete this.bindings[ event ];
		}
		return this;
	};

	/**
	 * Emit an event.
	 *
	 * All listeners for the event will be called synchronously, in an
	 * unspecified order. If any listeners throw an exception, this won't
	 * disrupt the calls to the remaining listeners; however, the exception
	 * won't be thrown until the next tick.
	 *
	 * Listeners should avoid mutating the emitting object, as this is
	 * something of an anti-pattern which can easily result in
	 * hard-to-understand code with hidden side-effects and dependencies.
	 *
	 * @param {string} event Type of event
	 * @param {...any} [args] Arguments passed to the event handler
	 * @return {boolean} Whether the event was handled by at least one listener
	 */
	OO.EventEmitter.prototype.emit = function ( event ) {
		if ( !hasOwn.call( this.bindings, event ) ) {
			return false;
		}

		// Slicing ensures that we don't get tripped up by event
		// handlers that add/remove bindings
		var bindings = this.bindings[ event ].slice();
		var args = slice.call( arguments, 1 );
		for ( var i = 0; i < bindings.length; i++ ) {
			var binding = bindings[ i ];
			var method;
			if ( typeof binding.method === 'string' ) {
				// Lookup method by name (late binding)
				method = binding.context[ binding.method ];
			} else {
				method = binding.method;
			}
			if ( binding.once ) {
				// Unbind before calling, to avoid any nested triggers.
				this.off( event, method );
			}
			try {
				method.apply(
					binding.context,
					binding.args ? binding.args.concat( args ) : args
				);
			} catch ( e ) {
				// If one listener has an unhandled error, don't have it
				// take down the emitter. But rethrow asynchronously so
				// debuggers can break with a full async stack trace.
				setTimeout( ( function ( error ) {
					throw error;
				} ).bind( null, e ) );
			}

		}
		return true;
	};

	/**
	 * Emit an event, propagating the first exception some listener throws
	 *
	 * All listeners for the event will be called synchronously, in an
	 * unspecified order. If any listener throws an exception, this won't
	 * disrupt the calls to the remaining listeners. The first exception
	 * thrown will be propagated back to the caller; any others won't be
	 * thrown until the next tick.
	 *
	 * Listeners should avoid mutating the emitting object, as this is
	 * something of an anti-pattern which can easily result in
	 * hard-to-understand code with hidden side-effects and dependencies.
	 *
	 * @param {string} event Type of event
	 * @param {...any} [args] Arguments passed to the event handler
	 * @return {boolean} Whether the event was handled by at least one listener
	 */
	OO.EventEmitter.prototype.emitThrow = function ( event ) {
		// We tolerate code duplication with #emit, because the
		// alternative is an extra level of indirection which will
		// appear in very many stack traces.
		if ( !hasOwn.call( this.bindings, event ) ) {
			return false;
		}

		var firstError;
		// Slicing ensures that we don't get tripped up by event
		// handlers that add/remove bindings
		var bindings = this.bindings[ event ].slice();
		var args = slice.call( arguments, 1 );
		for ( var i = 0; i < bindings.length; i++ ) {
			var binding = bindings[ i ];
			var method;
			if ( typeof binding.method === 'string' ) {
				// Lookup method by name (late binding)
				method = binding.context[ binding.method ];
			} else {
				method = binding.method;
			}
			if ( binding.once ) {
				// Unbind before calling, to avoid any nested triggers.
				this.off( event, method );
			}
			try {
				method.apply(
					binding.context,
					binding.args ? binding.args.concat( args ) : args
				);
			} catch ( e ) {
				if ( firstError === undefined ) {
					firstError = e;
				} else {
					// If one listener has an unhandled error, don't have it
					// take down the emitter. But rethrow asynchronously so
					// debuggers can break with a full async stack trace.
					setTimeout( ( function ( error ) {
						throw error;
					} ).bind( null, e ) );
				}
			}

		}
		if ( firstError !== undefined ) {
			throw firstError;
		}
		return true;
	};

	/**
	 * Connect event handlers to an object.
	 *
	 * @param {Object} context Object to call methods on when events occur
	 * @param {Object.<string,string>|Object.<string,Function>|Object.<string,Array>} methods
	 *  List of event bindings keyed by event name containing either method names, functions or
	 *  arrays containing method name or function followed by a list of arguments to be passed to
	 *  callback before emitted arguments.
	 * @return {OO.EventEmitter}
	 */
	OO.EventEmitter.prototype.connect = function ( context, methods ) {
		for ( var event in methods ) {
			var method = methods[ event ];
			var args;
			// Allow providing additional args
			if ( Array.isArray( method ) ) {
				args = method.slice( 1 );
				method = method[ 0 ];
			} else {
				args = [];
			}
			// Add binding
			this.on( event, method, args, context );
		}
		return this;
	};

	/**
	 * Disconnect event handlers from an object.
	 *
	 * @param {Object} context Object to disconnect methods from
	 * @param {Object.<string,string>|Object.<string,Function>|Object.<string,Array>} [methods]
	 *  List of event bindings keyed by event name. Values can be either method names, functions or
	 *  arrays containing a method name.
	 *  NOTE: To allow matching call sites with connect(), array values are allowed to contain the
	 *  parameters as well, but only the method name is used to find bindings. It is discouraged to
	 *  have multiple bindings for the same event to the same listener, but if used (and only the
	 *  parameters vary), disconnecting one variation of (event name, event listener, parameters)
	 *  will disconnect other variations as well.
	 * @return {OO.EventEmitter}
	 */
	OO.EventEmitter.prototype.disconnect = function ( context, methods ) {
		var event;
		if ( methods ) {
			// Remove specific connections to the context
			for ( event in methods ) {
				var method = methods[ event ];
				if ( Array.isArray( method ) ) {
					method = method[ 0 ];
				}
				this.off( event, method, context );
			}
		} else {
			// Remove all connections to the context
			for ( event in this.bindings ) {
				var bindings = this.bindings[ event ];
				var i = bindings.length;
				while ( i-- ) {
					// bindings[i] may have been removed by the previous step's
					// this.off so check it still exists
					if ( bindings[ i ] && bindings[ i ].context === context ) {
						this.off( event, bindings[ i ].method, context );
					}
				}
			}
		}

		return this;
	};

}() );