export class EmitterEvent {

	private _type:string;
	private _data:any;

	constructor(eventType:string=null, data:any=null) {
		this._type = eventType;
		this._data = data;
	}

	public getType():string {
		return this._type;
	}

	public getData():any {
		return this._data;
	}
}

export interface ListenerCallback {
	(value:any):void;
}

export interface BulkListenerCallback {
	(value:EmitterEvent[]):void;
}

export interface IEventEmitter extends IDisposable {
	addListener2(eventType:string, listener:ListenerCallback):IDisposable;
	addOneTimeDisposableListener(eventType:string, listener:ListenerCallback):IDisposable;
	addBulkListener2(listener:BulkListenerCallback):IDisposable;
	addEmitter2(eventEmitter:IEventEmitter):IDisposable;
}

export interface IListenersMap {
	[key:string]:ListenerCallback[];
}

export class EventEmitter implements IEventEmitter {

	protected _listeners:IListenersMap;
	protected _bulkListeners:ListenerCallback[];
	private _collectedEvents:EmitterEvent[];
	private _deferredCnt:number;
	private _allowedEventTypes:{[eventType:string]:boolean;};

	constructor(allowedEventTypes:string[] = null) {
		this._listeners = {};
		this._bulkListeners = [];
		this._collectedEvents = [];
		this._deferredCnt = 0;
		if (allowedEventTypes) {
			this._allowedEventTypes = {};
			for (var i = 0; i < allowedEventTypes.length; i++) {
				this._allowedEventTypes[allowedEventTypes[i]] = true;
			}
		} else {
			this._allowedEventTypes = null;
		}
	}

	public dispose(): void {
		this._listeners = {};
		this._bulkListeners = [];
		this._collectedEvents = [];
		this._deferredCnt = 0;
		this._allowedEventTypes = null;
	}

	private addListener(eventType:string, listener:ListenerCallback):IDisposable {
		if (eventType === '*') {
			throw new Error('Use addBulkListener(listener) to register your listener!');
		}

		if (this._allowedEventTypes && !this._allowedEventTypes.hasOwnProperty(eventType)) {
			throw new Error('This object will never emit this event type!');
		}

		if (this._listeners.hasOwnProperty(eventType)) {
			this._listeners[eventType].push(listener);
		} else {
			this._listeners[eventType] = [listener];
		}

		var bound = this;
		return {
			dispose: () => {
				if (!bound) {
					// Already called
					return;
				}

				bound._removeListener(eventType, listener);

				// Prevent leakers from holding on to the event emitter
				bound = null;
				listener = null;
			}
		};
	}

	public addListener2(eventType:string, listener:ListenerCallback):IDisposable {
		return this.addListener(eventType, listener);
	}

	private addOneTimeListener(eventType:string, listener:ListenerCallback):IDisposable {
		var unbind = this.addListener(eventType, (value:any) => {
			unbind.dispose();
			listener(value);
		});
		return unbind;
	}

	public addOneTimeDisposableListener(eventType:string, listener:ListenerCallback):IDisposable {
		return this.addOneTimeListener(eventType, listener);
	}

	protected addBulkListener(listener:BulkListenerCallback):IDisposable {

		this._bulkListeners.push(listener);

		return {
			dispose: () => {
				this._removeBulkListener(listener);
			}
		};
	}

	public addBulkListener2(listener:BulkListenerCallback):IDisposable {
		return this.addBulkListener(listener);
	}

	private addEmitter(eventEmitter:IEventEmitter):IDisposable {
		return eventEmitter.addBulkListener2((events:EmitterEvent[]):void => {
			var newEvents = events;

			if (this._deferredCnt === 0) {
				this._emitEvents(<EmitterEvent[]>newEvents);
			} else {
				// Collect for later
				this._collectedEvents.push.apply(this._collectedEvents, newEvents);
			}
		});
	}

	public addEmitter2(eventEmitter:IEventEmitter):IDisposable {
		return this.addEmitter(eventEmitter);
	}

	private _removeListener(eventType:string, listener:ListenerCallback): void {
		if (this._listeners.hasOwnProperty(eventType)) {
			var listeners = this._listeners[eventType];
			for (var i = 0, len = listeners.length; i < len; i++) {
				if (listeners[i] === listener) {
					listeners.splice(i, 1);
					break;
				}
			}

		}
	}

	private _removeBulkListener(listener:BulkListenerCallback): void {
		for (var i = 0, len = this._bulkListeners.length; i < len; i++) {
			if (this._bulkListeners[i] === listener) {
				this._bulkListeners.splice(i, 1);
				break;
			}
		}
	}

	protected _emitToSpecificTypeListeners(eventType:string, data:any): void {
		if (this._listeners.hasOwnProperty(eventType)) {
			var listeners = this._listeners[eventType].slice(0);
			for (var i = 0, len = listeners.length; i < len; i++) {
				safeInvoke1Arg(listeners[i], data);
			}
		}
	}

	protected _emitToBulkListeners(events:EmitterEvent[]): void {
		var bulkListeners = this._bulkListeners.slice(0);
		for (var i = 0, len = bulkListeners.length; i < len; i++) {
			safeInvoke1Arg(bulkListeners[i], events);
		}
	}

	protected _emitEvents(events:EmitterEvent[]): void {
		if (this._bulkListeners.length > 0) {
			this._emitToBulkListeners(events);
		}
		for (var i = 0, len = events.length; i < len; i++) {
			var e = events[i];

			this._emitToSpecificTypeListeners(e.getType(), e.getData());
		}
	}

	public emit(eventType:string, data:any={}):void {
		if (this._allowedEventTypes && !this._allowedEventTypes.hasOwnProperty(eventType)) {
			throw new Error('Cannot emit this event type because it wasn\'t white-listed!');
		}
		// Early return if no listeners would get this
		if (!this._listeners.hasOwnProperty(eventType) && this._bulkListeners.length === 0) {
			return;
		}
		var emitterEvent = new EmitterEvent(eventType, data);

		if (this._deferredCnt === 0) {
			this._emitEvents([emitterEvent]);
		} else {
			// Collect for later
			this._collectedEvents.push(emitterEvent);
		}
	}

	public deferredEmit(callback:()=>any):any {
		this._deferredCnt = this._deferredCnt + 1;
		var result: any = safeInvokeNoArg(callback);
		this._deferredCnt = this._deferredCnt - 1;

		if (this._deferredCnt === 0) {
			this._emitCollected();
		}
		return result;
	}

	private _emitCollected(): void {
		// Flush collected events
		var events = this._collectedEvents;
		this._collectedEvents = [];

		if (events.length > 0) {
			this._emitEvents(events);
		}
	}
}

class EmitQueueElement {
	public target: Function;
	public arg: any;

	constructor(target: Function, arg: any) {
		this.target = target;
		this.arg = arg;
	}
}

/**
 * Same as EventEmitter, but guarantees events are delivered in order to each listener
 */
export class OrderGuaranteeEventEmitter extends EventEmitter {

	private _emitQueue: EmitQueueElement[];

	constructor(allowedEventTypes:string[] = null) {
		super(allowedEventTypes);
		this._emitQueue = [];
	}

	protected _emitToSpecificTypeListeners(eventType:string, data:any): void {
		if (this._listeners.hasOwnProperty(eventType)) {
			let listeners = this._listeners[eventType];
			for (let i = 0, len = listeners.length; i < len; i++) {
				this._emitQueue.push(new EmitQueueElement(listeners[i], data));
			}
		}
	}

	protected _emitToBulkListeners(events:EmitterEvent[]): void {
		let bulkListeners = this._bulkListeners;
		for (let i = 0, len = bulkListeners.length; i < len; i++) {
			this._emitQueue.push(new EmitQueueElement(bulkListeners[i], events));
		}
	}

	protected _emitEvents(events:EmitterEvent[]): void {
		super._emitEvents(events);

		while (this._emitQueue.length > 0) {
			let queueElement = this._emitQueue.shift();
			safeInvoke1Arg(queueElement.target, queueElement.arg);
		}
	}
}

function safeInvokeNoArg(func:Function): any {
	try {
		return func();
	} catch(e) {
		console.error(e);
	}
}

function safeInvoke1Arg(func:Function, arg1:any): any {
	try {
		return func(arg1);
	} catch(e) {
		console.error(e);
	}
}
