/*
Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/

/**
 * @fileOverview Defines the {@link CKEDITOR.event} class, which serves as the
 *		base for classes and objects that require event handling features.
 */

if ( !CKEDITOR.event )
{
	/**
	 * Creates an event class instance. This constructor is rearely used, being
	 * the {@link #.implementOn} function used in class prototypes directly
	 * instead.
	 * @class This is a base class for classes and objects that require event
	 * handling features.<br />
	 * <br />
	 * Do not confuse this class with {@link CKEDITOR.dom.event} which is
	 * instead used for DOM events. The CKEDITOR.event class implements the
	 * internal event system used by the CKEditor to fire API related events.
	 * @example
	 */
	CKEDITOR.event = function()
	{};

	/**
	 * Implements the {@link CKEDITOR.event} features in an object.
	 * @param {Object} targetObject The object into which implement the features.
	 * @example
	 * var myObject = { message : 'Example' };
	 * <b>CKEDITOR.event.implementOn( myObject }</b>;
	 * myObject.on( 'testEvent', function()
	 *     {
	 *         alert( this.message );  // "Example"
	 *     });
	 * myObject.fire( 'testEvent' );
	 */
	CKEDITOR.event.implementOn = function( targetObject )
	{
		var eventProto = CKEDITOR.event.prototype;

		for ( var prop in eventProto )
		{
			if ( targetObject[ prop ] == undefined )
				targetObject[ prop ] = eventProto[ prop ];
		}
	};

	CKEDITOR.event.prototype = (function()
	{
		// Returns the private events object for a given object.
		var getPrivate = function( obj )
		{
			var _ = ( obj.getPrivate && obj.getPrivate() ) || obj._ || ( obj._ = {} );
			return _.events || ( _.events = {} );
		};

		var eventEntry = function( eventName )
		{
			this.name = eventName;
			this.listeners = [];
		};

		eventEntry.prototype =
		{
			// Get the listener index for a specified function.
			// Returns -1 if not found.
			getListenerIndex : function( listenerFunction )
			{
				for ( var i = 0, listeners = this.listeners ; i < listeners.length ; i++ )
				{
					if ( listeners[i].fn == listenerFunction )
						return i;
				}
				return -1;
			}
		};

		return /** @lends CKEDITOR.event.prototype */ {
			/**
			 * Registers a listener to a specific event in the current object.
			 * @param {String} eventName The event name to which listen.
			 * @param {Function} listenerFunction The function listening to the
			 *		event. A single {@link CKEDITOR.eventInfo} object instanced
			 *		is passed to this function containing all the event data.
			 * @param {Object} [scopeObj] The object used to scope the listener
			 *		call (the this object. If omitted, the current object is used.
			 * @param {Object} [listenerData] Data to be sent as the
			 *		{@link CKEDITOR.eventInfo#listenerData} when calling the
			 *		listener.
			 * @param {Number} [priority] The listener priority. Lower priority
			 *		listeners are called first. Listeners with the same priority
			 *		value are called in registration order. Defaults to 10.
			 * @example
			 * someObject.on( 'someEvent', function()
			 *     {
			 *         alert( this == someObject );  // "true"
			 *     });
			 * @example
			 * someObject.on( 'someEvent', function()
			 *     {
			 *         alert( this == anotherObject );  // "true"
			 *     }
			 *     , anotherObject );
			 * @example
			 * someObject.on( 'someEvent', function( event )
			 *     {
			 *         alert( event.listenerData );  // "Example"
			 *     }
			 *     , null, 'Example' );
			 * @example
			 * someObject.on( 'someEvent', function() { ... } );                   // 2nd called
			 * someObject.on( 'someEvent', function() { ... }, null, null, 100 );  // 3rd called
			 * someObject.on( 'someEvent', function() { ... }, null, null, 1 );    // 1st called
			 */
			on : function( eventName, listenerFunction, scopeObj, listenerData, priority )
			{
				// Get the event entry (create it if needed).
				var events = getPrivate( this ),
					event = events[ eventName ] || ( events[ eventName ] = new eventEntry( eventName ) );

				if ( event.getListenerIndex( listenerFunction ) < 0 )
				{
					// Get the listeners.
					var listeners = event.listeners;

					// Fill the scope.
					if ( !scopeObj )
						scopeObj = this;

					// Default the priority, if needed.
					if ( isNaN( priority ) )
						priority = 10;

					var me = this;

					// Create the function to be fired for this listener.
					var listenerFirer = function( editor, publisherData, stopFn, cancelFn )
					{
						var ev =
						{
							name : eventName,
							sender : this,
							editor : editor,
							data : publisherData,
							listenerData : listenerData,
							stop : stopFn,
							cancel : cancelFn,
							removeListener : function()
							{
								me.removeListener( eventName, listenerFunction );
							}
						};

						listenerFunction.call( scopeObj, ev );

						return ev.data;
					};
					listenerFirer.fn = listenerFunction;
					listenerFirer.priority = priority;

					// Search for the right position for this new listener, based on its
					// priority.
					for ( var i = listeners.length - 1 ; i >= 0 ; i-- )
					{
						// Find the item which should be before the new one.
						if ( listeners[ i ].priority <= priority )
						{
							// Insert the listener in the array.
							listeners.splice( i + 1, 0, listenerFirer );
							return;
						}
					}

					// If no position has been found (or zero length), put it in
					// the front of list.
					listeners.unshift( listenerFirer );
				}
			},

			/**
			 * Fires an specific event in the object. All registered listeners are
			 * called at this point.
			 * @function
			 * @param {String} eventName The event name to fire.
			 * @param {Object} [data] Data to be sent as the
			 *		{@link CKEDITOR.eventInfo#data} when calling the
			 *		listeners.
			 * @param {CKEDITOR.editor} [editor] The editor instance to send as the
			 *		{@link CKEDITOR.eventInfo#editor} when calling the
			 *		listener.
			 * @returns {Boolean|Object} A booloan indicating that the event is to be
			 *		canceled, or data returned by one of the listeners.
			 * @example
			 * someObject.on( 'someEvent', function() { ... } );
			 * someObject.on( 'someEvent', function() { ... } );
			 * <b>someObject.fire( 'someEvent' )</b>;  // both listeners are called
			 * @example
			 * someObject.on( 'someEvent', function( event )
			 *     {
			 *         alert( event.data );  // "Example"
			 *     });
			 * <b>someObject.fire( 'someEvent', 'Example' )</b>;
			 */
			fire : (function()
			{
				// Create the function that marks the event as stopped.
				var stopped = false;
				var stopEvent = function()
				{
					stopped = true;
				};

				// Create the function that marks the event as canceled.
				var canceled = false;
				var cancelEvent = function()
				{
					canceled = true;
				};

				return function( eventName, data, editor )
				{
					// Get the event entry.
					var event = getPrivate( this )[ eventName ];

					// Save the previous stopped and cancelled states. We may
					// be nesting fire() calls.
					var previousStopped = stopped,
						previousCancelled = canceled;

					// Reset the stopped and canceled flags.
					stopped = canceled = false;

					if ( event )
					{
						var listeners = event.listeners;

						if ( listeners.length )
						{
							// As some listeners may remove themselves from the
							// event, the original array length is dinamic. So,
							// let's make a copy of all listeners, so we are
							// sure we'll call all of them.
							listeners = listeners.slice( 0 );

							// Loop through all listeners.
							for ( var i = 0 ; i < listeners.length ; i++ )
							{
								// Call the listener, passing the event data.
								var retData = listeners[i].call( this, editor, data, stopEvent, cancelEvent );

								if ( typeof retData != 'undefined' )
									data = retData;

								// No further calls is stopped or canceled.
								if ( stopped || canceled )
									break;
							}
						}
					}

					var ret = canceled || ( typeof data == 'undefined' ? false : data );

					// Restore the previous stopped and canceled states.
					stopped = previousStopped;
					canceled = previousCancelled;

					return ret;
				};
			})(),

			/**
			 * Fires an specific event in the object, releasing all listeners
			 * registered to that event. The same listeners are not called again on
			 * successive calls of it or of {@link #fire}.
			 * @param {String} eventName The event name to fire.
			 * @param {Object} [data] Data to be sent as the
			 *		{@link CKEDITOR.eventInfo#data} when calling the
			 *		listeners.
			 * @param {CKEDITOR.editor} [editor] The editor instance to send as the
			 *		{@link CKEDITOR.eventInfo#editor} when calling the
			 *		listener.
			 * @returns {Boolean|Object} A booloan indicating that the event is to be
			 *		canceled, or data returned by one of the listeners.
			 * @example
			 * someObject.on( 'someEvent', function() { ... } );
			 * someObject.fire( 'someEvent' );  // above listener called
			 * <b>someObject.fireOnce( 'someEvent' )</b>;  // above listener called
			 * someObject.fire( 'someEvent' );  // no listeners called
			 */
			fireOnce : function( eventName, data, editor )
			{
				var ret = this.fire( eventName, data, editor );
				delete getPrivate( this )[ eventName ];
				return ret;
			},

			/**
			 * Unregisters a listener function from being called at the specified
			 *		event. No errors are thrown if the listener has not been
			 *		registered previously.
			 * @param {String} eventName The event name.
			 * @param {Function} listenerFunction The listener function to unregister.
			 * @example
			 * var myListener = function() { ... };
			 * someObject.on( 'someEvent', myListener );
			 * someObject.fire( 'someEvent' );  // myListener called
			 * <b>someObject.removeListener( 'someEvent', myListener )</b>;
			 * someObject.fire( 'someEvent' );  // myListener not called
			 */
			removeListener : function( eventName, listenerFunction )
			{
				// Get the event entry.
				var event = getPrivate( this )[ eventName ];

				if ( event )
				{
					var index = event.getListenerIndex( listenerFunction );
					if ( index >= 0 )
						event.listeners.splice( index, 1 );
				}
			},

			/**
			 * Checks if there is any listener registered to a given event.
			 * @param {String} eventName The event name.
			 * @example
			 * var myListener = function() { ... };
			 * someObject.on( 'someEvent', myListener );
			 * alert( someObject.<b>hasListeners( 'someEvent' )</b> );  // "true"
			 * alert( someObject.<b>hasListeners( 'noEvent' )</b> );    // "false"
			 */
			hasListeners : function( eventName )
			{
				var event = getPrivate( this )[ eventName ];
				return ( event && event.listeners.length > 0 ) ;
			}
		};
	})();
}
