/*jslint browser: true, nomen: true, plusplus: true */

/// @file coherent.js
/// @namespace engine

/// Coherent UI JavaScript interface.
/// The `engine` module contains all functions for communication between the UI and the game / application.
(function (factory) {
	if (typeof module === 'object' && module.exports) {
		module.exports = factory(global, global.engine, false);
	} else {
		window.engine = factory(window, window.engine, true);
	}
})(function (global, engine, hasOnLoad) {
	'use strict';

	var VERSION = [1, 10, 2, 0];

	/**
	* Event emitter
	*
	* @class Emitter
	*/
	function Emitter() {
		this.events = {};
	}

	function Handler(code, context) {
		this.code = code;
		this.context = context;
	}

	Emitter.prototype._createClear = function (object, name, handler) {
		return function() {
			var handlers = object.events[name];
			if (handlers) {
				var index = -1;
				// this was in native previously
				if(handler === undefined)
				{
					for(var i = 0; i < handlers.length; ++i)
					{
						if(handlers[i].wasInCPP !== undefined)
						{
							index = i;
							break;
						}
					}
				}
				else
				{
					index = handlers.indexOf(handler);
				}
				if (index != -1) {
					handlers.splice(index, 1);
					if (handlers.length === 0) {
						delete object.events[name];
					}
				}
			} else {
				if(engine.RemoveOnHandler !== undefined) {
					engine.RemoveOnHandler(name);
				}
			}
		};
	};

	/// @file coherent.js

	/**
	* Add a handler for an event
	*
	* @method on
	* @param name the event name
	* @param callback function to be called when the event is triggered
	* @param context this binding for executing the handler, defaults to the Emitter
	* @return connection object
	*/
	Emitter.prototype.on = function (name, callback, context) {
		var handlers = this.events[name];
		if (handlers === undefined)
			handlers = this.events[name] = [];

		var handler = new Handler(callback, context || this);
		handlers.push(handler);
		return { clear: this._createClear(this, name, handler) };
	};

	/**
	* Remove a handler from an event
	*
	* @method off
	* @param name the event name
	* @param callback function to be called when the event is triggered
	* @param context this binding for executing the handler, defaults to the Emitter
	* @return connection object
	*/
	Emitter.prototype.off = function (name, handler, context) {
		var handlers = this.events[name];

		if (handlers !== undefined) {
			context = context || this;

			var index;
			var length = handlers.length;
			for (index = 0; index < length; ++index) {
				var reg = handlers[index];
				if (reg.code == handler && reg.context == context) {
					break;
				}
			}
			if (index < length) {
				handlers.splice(index, 1);
				if (handlers.length === 0) {
					delete this.events[name];
				}
			}
		}
		else
		{
			engine.RemoveOnHandler(name);
		}
	};


	var isAttached = engine !== undefined;
	engine = engine || {};
	/// @var engine.isAttached
	/// Indicates whether the script is currently running inside Coherent GT
	engine.isAttached = isAttached;

	/// @var engine.forceEnableMocking
	/// Indicates whether mocking should be enabled despite running inside Coherent GT
	engine.forceEnableMocking = global.engineForceEnableMocking || false;
	
	/// @var engine.IsAttached
	/// [DEPRECATED] Indicates whether the script is currently running inside Coherent GT
	/// @warning This property is deprecated, please use engine.isAttached (with camelCase)
	engine.IsAttached = engine.isAttached;

	engine.onEventsReplayed = null;
	Emitter.prototype.trigger = function(name) {
		var handlers = this.events[name];

		if (handlers !== undefined) {
			var args = Array.prototype.slice.call(arguments, 1);

			handlers.forEach(function (handler) {
				handler.code.apply(handler.context, args);
			});
		}
	};

	Emitter.prototype.merge = function (emitter) {
		var lhs = this.events,
			rhs = emitter.events,
			push = Array.prototype.push,
			events;

		for (var e in rhs) {
			events = lhs[e] = lhs[e] || [];
			push.apply(events, rhs[e]);
		}
	};

	var pending = 'pending';
	var fulfilled = 'fulfilled';
	var broken = 'broken';

	function callAsync(code, context, argument) {
		var async = function () {
			code.call(context, argument);
		};
		setTimeout(async);
	}

	function Promise () {
		this.emitter = new Emitter();
		this.state = pending;
		this.result = null;
	}

	Promise.prototype.resolve = function (result) {
		this.state = fulfilled;
		this.result = result;

		this.emitter.trigger(fulfilled, result);
	};

	Promise.prototype.reject = function (result) {
		this.state = broken;
		this.result = result;

		this.emitter.trigger(broken, result);
	};

	Promise.prototype.success = function (code, context) {
		if (this.state !== fulfilled) {
			this.emitter.on(fulfilled, code, context);
		} else {
			callAsync(code, context || this, this.result);
		}
		return this;
	};

	Promise.prototype.always = function (code, context) {
		this.success(code, context);
		this.otherwise(code, context);
		return this;
	};

	Promise.prototype.otherwise = function (code, context) {
		if (this.state !== broken) {
			this.emitter.on(broken, code, context);
		} else {
			callAsync(code, context || this, this.result);
		}
		return this;
	};

	Promise.prototype.merge = function (other) {
		if (this.state === pending) {
			this.emitter.merge(other.emitter);
		} else {
			var handlers = other.emitter.events[this.state];
			var self = this;
			if (handlers !== undefined) {
				handlers.forEach(function (handler) {
					handler.code.call(handler.context, self.result);
				});
			}
		}
	};

	Promise.prototype.make_chain = function (handler, promise, ok) {
		return function (result) {
			var handlerResult;
			try {
				handlerResult = handler.code.call(handler.context, result);
				if (handlerResult instanceof Promise) {
					handlerResult.merge(promise);
				} else if (this.state === ok) {
					promise.resolve(handlerResult);
				} else {
					promise.reject(handlerResult);
				}
			} catch (error) {
				promise.reject(error);
			}
		};
	};

	function makeDefaultHandler(promise) {
		return function () {
			return promise;
		};
	}

	Promise.prototype.then = function (callback, errback) {
		var promise = new Promise();

		var handler = new Handler(callback || makeDefaultHandler(this), this);

		this.success(this.make_chain(handler, promise, fulfilled), this);

		var errorHandler = new Handler(errback || makeDefaultHandler(this), this);
		this.otherwise(this.make_chain(errorHandler, promise, broken), this);


		return promise;
	};

	if (!engine.isAttached || engine.forceEnableMocking) {
		Emitter.prototype.on = function (name, callback, context) {
			var handlers = this.events[name];
			if (this.browserCallbackOn) {
				this.browserCallbackOn(name, callback, context);
			}

			if (handlers === undefined) {
				handlers = this.events[name] = [];
			}

			var handler = new Handler(callback, context || this);
			handlers.push(handler);
			return { clear: this._createClear(this, name, handler) };
		};
		Emitter.prototype.off = function (name, handler, context) {
			var handlers = this.events[name];

			if (handlers !== undefined) {
				context = context || this;

				var index;
				var length = handlers.length;
				for (index = 0; index < length; ++index) {
					var reg = handlers[index];
					if (reg.code == handler && reg.context == context) {
						break;
					}
				}
				if (index < length) {
					handlers.splice(index, 1);
					if (handlers.length === 0) {
						delete this.events[name];

						if (this.browserCallbackOff) {
							this.browserCallbackOff(name, handler, context);
						}
					}
				}
			}
		};

		engine.SendMessage = function (name, id) {
			var args = Array.prototype.slice.call(arguments, 2);
			var deferred = engine._ActiveRequests[id];

			delete engine._ActiveRequests[id];
			var call = function () {
				var mock = engine._mocks[name];

				if (mock !== undefined) {
					deferred.resolve(mock.apply(engine, args));
				}
			};

			window.setTimeout(call, 16);
		};

		engine.TriggerEvent = function () {
			var args = Array.prototype.slice.call(arguments);

			var trigger = function () {
				var mock = engine._mocks[args[0]];

				if (mock !== undefined) {
					mock.apply(engine, args.slice(1));
				}
			};
			window.setTimeout(trigger, 16);
		};

		engine.BindingsReady = function () {
			engine._OnReady();
		};

		engine.__observeLifetime = function () {
		};

		engine.beginEventRecording =
		engine.endEventRecording =
		engine.saveEventRecord = function() {
			console.warning("Event recording will not work in the browser!");
		};

		engine._mocks = {};
		engine._mockImpl = function (name, mock, isCppCall, isEvent) {
			if (mock && isCppCall) {
				this._mocks[name] = mock;
			}
			// Extract the name of the arguments from Function.prototype.toString
			var functionStripped = mock.toString().replace("function " + mock.name + "(", "");
			var rightParanthesis = functionStripped.indexOf(")");
			var args = functionStripped.substr(0, rightParanthesis);
			if (this.browserCallbackMock) {
				this.browserCallbackMock(name,
										 args,
										 isCppCall,
										 Boolean(isEvent));
			}
		}
		engine.mock = function (name, mock, isEvent) {
			this._mockImpl(name, mock, true, isEvent);
		};
		// Mock the call to translate to always return the key
		engine.translate = function (text) {
			return text;
		};
	}

	engine.events = {};
	for (var property in Emitter.prototype) {
		engine[property] = Emitter.prototype[property];
	}

	if (engine.isAttached && !engine.forceEnableMocking) {
		engine.on = function (name, callback, context) {
			var handlers = this.events[name];

			if (handlers === undefined && engine.AddOrRemoveOnHandler !== undefined) {
				// Check where to cache the handler
				var prevEvent = engine.AddOrRemoveOnHandler(name, callback, context);

				// handler cached in C++
				if(prevEvent === undefined) {
					return { clear: this._createClear(this, name, undefined) };
				}

				handlers = this.events[name] = [];

				// Add the previous handler
				var prevHandler = new Handler(prevEvent[0], prevEvent[1] || this);
				prevHandler.wasInCPP = true;
				handlers.push(prevHandler);
			} else if (handlers === undefined) {
				handlers = this.events[name] = [];
			}

			var handler = new Handler(callback, context || this);
			handlers.push(handler);
			return { clear: this._createClear(this, name, handler) };
		}
	}

	/// @function engine.on
	/// Register handler for and event
	/// @param {String} name name of the event
	/// @param {Function} callback callback function to be executed when the event has been triggered
	/// @param context *this* context for the function, by default the engine object

	/// @function engine.beginEventRecording
	/// Begins recording all events triggered using View::TriggerEvent from the game

	/// @function engine.endEventRecording
	/// Ends event recording

	/// @function engine.saveEventRecord
	/// Saves the events recorded in between the last calls to engine.beginEventRecording and engine.endEventRecording to a file
	/// @param {String} path The path to the file where to save the recorded events. Optional. Defaults to "eventRecord.json"

	/// @function engine.replayEvents
	/// Replays the events previously recorded and stored in path. If you need to be notified when all events
	/// are replayed, assign a callback to engine.onEventsReplayed
	/// @param {Number} timeScale The speed at which to replay the events (e.g. pass 2 to double the speed). Optional. Defaults to 1.
	/// @param {String} path The path to the file the recorded events are stored. Optional. Defaults to "eventRecord.json"

	/// @function engine.translate
	/// Translates the given text by invoking the system's localization manager if one exists.
	/// @param {text} text The text to translate.
	/// @return {String} undefined if no localization manager is set or no translation exists, else returns the translated string

	/// @function engine.reloadLocalization
	/// Updates the text on all elements with the data-l10n-id attribute by calling engine.translate

	/// @function engine.off
	/// Remove handler for an event
	/// @param {String} name name of the event, by default removes all events
	/// @param {Function} callback the callback function to be removed, by default removes all callbacks for a given event
	/// @param context *this* context for the function, by default all removes all callbacks, regardless of context
	/// @warning Removing all handlers for `engine` will remove some *Coherent UI* internal events, breaking some functionality.

	/// @function engine.trigger
	/// Trigger an event
	/// This function will trigger any C++ handler registered for this event with `Coherent::UI::View::RegisterForEvent`
	/// @param {String} name name of the event
	/// @param ... any extra arguments to be passed to the event handlers

	engine._trigger = Emitter.prototype.trigger;
	var concatArguments = Array.prototype.concat;
	engine.trigger = function (name) {
		this._trigger.apply(this, arguments);
		this.TriggerEvent.apply(this, arguments);

		if (this.events['all'] !== undefined) {
			var allArguments = concatArguments.apply(['all'], arguments);
			this._trigger.apply(this, allArguments);
		}
	};
	/// @function engine.showOverlay
	/// Shows the debugging overlay in the browser.
	/// Will also work in Coherent GT only if *engineForceEnableMocking* is set to *true*.
	engine.showOverlay = function () {};


	/// @function engine.hideOverlay
	/// Hides the debugging overlay in the browser.
	/// Will also work in Coherent GT only if *engineForceEnableMocking* is set to *true*.
	engine.hideOverlay = function () {};

	/// @function engine.mock
	/// Mocks a C++ function call with the specified function.
	/// Will also work in Coherent GT only if *engineForceEnableMocking* is set to *true*.
	/// @param {String} name name of the event
	/// @param {Function} mock a function to be called in-place of your native binding
	/// @param {Boolean} isEvent whether you are mocking an event or function call
	if (engine.isAttached && !engine.forceEnableMocking) {
		engine.mock = function (name, mock, isEvent) { };
	}

	engine._BindingsReady = false;
	engine._WindowLoaded = false;
	engine._RequestId = 0;
	engine._ActiveRequests = {};

	/// @function engine.createDeferred
	/// Create a new deferred object.
	/// Use this to create deferred / promises that can be used together with `engine.call`.
	/// @return {Deferred} a new deferred object
	/// @see @ref CustomizingPromises
	engine.createDeferred = (global.engineCreateDeferred === undefined) ?
		function () { return new Promise(); }
		: global.engineCreateDeferred;

	/// @function engine.call
	/// Call asynchronously a C++ handler and retrieve the result
	/// The C++ handler must have been registered with `Coherent::UI::View::BindCall`
	/// @param {String} name name of the C++ handler to be called
	/// @param ... any extra parameters to be passed to the C++ handler
	/// @return {Deferred} deferred object whose promise is resolved with the result of the C++ handler
	engine.call = function () {
		engine._RequestId++;
		var id = engine._RequestId;

		var deferred = engine.createDeferred();
		engine._ActiveRequests[id] = deferred;
		var messageArguments = Array.prototype.slice.call(arguments);
		messageArguments.splice(1, 0, id);
		engine.SendMessage.apply(this, messageArguments);
		return deferred;
	};

	engine._Result = function (requestId) {
		var deferred = engine._ActiveRequests[requestId];
		if (deferred !== undefined)
		{
			delete engine._ActiveRequests[requestId];

			var resultArguments = Array.prototype.slice.call(arguments);
			resultArguments.shift();
			deferred.resolve.apply(deferred, resultArguments);
		}
	};

	engine._Errors = [ 'Success', 'ArgumentType', 'NoSuchMethod', 'NoResult' ];

	engine._ForEachError = function (errors, callback) {
		var length = errors.length;

		for (var i = 0; i < length; ++i) {
			callback(errors[i].first, errors[i].second);
		}
	};

	engine._MapErrors = function (errors) {
		var length = errors.length;

		for (var i = 0; i < length; ++i) {
			errors[i].first = engine._Errors[errors[i].first];
		}
	};

	engine._TriggerError = function (type, message) {
		engine.trigger('Error', type, message);
	};

	engine._OnError = function (requestId, errors) {
		engine._MapErrors(errors);

		if (requestId === null || requestId === 0) {
			engine._ForEachError(errors, engine._TriggerError);
		}
		else {
			var deferred = engine._ActiveRequests[requestId];

			delete engine._ActiveRequests[requestId];

			deferred.reject(errors);
		}
	};

	engine._eventHandles = {};

	engine._Register = function (eventName) {
		var trigger = (function (name, engine) {
			return function () {
				var eventArguments = [name];
				eventArguments.push.apply(eventArguments, arguments);
				engine.TriggerEvent.apply(this, eventArguments);
			};
		}(eventName, engine));

		engine._eventHandles[eventName] = engine.on(eventName, trigger);
	};

	engine._removeEventThunk = function (name) {
		var handle = engine._eventHandles[name];
		handle.clear();
		delete engine._eventHandles[name];
	};

	engine._Unregister = function (name) {
		if (typeof name === 'string') {
			engine._removeEventThunk(name);
		} else {
			name.forEach(engine._removeEventThunk, engine);
		}
	};

	function createMethodStub(name) {
		var stub = function() {
			var args = Array.prototype.slice.call(arguments);
			args.splice(0, 0, name, this._id);
			return engine.call.apply(engine, args);
		};
		return stub;
	}

	engine._boundTypes = {};

	engine._createInstance = function (args) {
		var type = args[0],
			id = args[1],
			methods = args[2],
			constructor = engine._boundTypes[type];

		if (constructor === undefined) {
			constructor = function (id) {
				this._id = id;
			};
			constructor.prototype.__Type = type;
			methods.forEach(function (name) {
				constructor.prototype[name] = createMethodStub(type + '_' + name);
			});
			engine._boundTypes[type] = constructor;
		}

		var instance = new constructor(id);
		engine.__observeLifetime(instance);
		return instance;
	}

	engine._OnReady = function () {
		engine._BindingsReady = true;
		if (engine._WindowLoaded) {
			engine.trigger('Ready');
		}
	};

	engine._OnWindowLoaded = function () {
		engine._WindowLoaded = true;
		if (engine._BindingsReady) {
			engine.trigger('Ready');
		}
	};

	engine._ThrowError = function (error) {
		var prependTab = function (s) { return "\t" + s; };
		var errorString = error.name + ": " + error.message + "\n" +
						  error.stack.split("\n").map(prependTab).join("\n");
		console.error(errorString);
	};

	if (hasOnLoad) {
		global.addEventListener("load", function () {
			engine._OnWindowLoaded();
		});
	} else {
		engine._WindowLoaded = true;
	}

	engine._coherentGlobalCanvas = document.createElement('canvas');
	engine._coherentGlobalCanvas.id     = "coherentGlobalCanvas";
	engine._coherentGlobalCanvas.width  = 1;
	engine._coherentGlobalCanvas.height = 1;
	engine._coherentGlobalCanvas.style.zIndex   = 0;
	engine._coherentGlobalCanvas.style.position = "absolute";
	engine._coherentGlobalCanvas.style.border   = "0px solid";

	engine._coherentLiveImageData = new Array();
	engine._coherentCreateImageData = function(name, guid) {
		var ctx = engine._coherentGlobalCanvas.getContext("2d");

		var coherentImage = ctx.coherentCreateImageData(guid);
		engine._coherentLiveImageData[name] = coherentImage;
	}
	engine._coherentUpdatedImageData = function(name) {
		engine._coherentLiveImageData[name].coherentUpdate();
		var canvases = document.getElementsByTagName('canvas');
		for(var i = 0; i < canvases.length; ++i) {
			if(canvases[i].onEngineImageDataUpdated != null) {
				canvases[i].onEngineImageDataUpdated(name,
					engine._coherentLiveImageData[name]);
			}
		}
	}

	engine.reloadLocalization = function () {
		var localizedElements = document.querySelectorAll('[data-l10n-id]');
		for (var i = 0; i < localizedElements.length; i++) {
			var element = localizedElements.item(i);
			var translated = engine.translate(element.dataset.l10nId);
			if (!translated) {
				var warning = "Failed to find translation for key: " + element.dataset.l10nId;
				console.warn(warning);
			} else {
				element.textContent = translated;
			}
		}
	};

	engine.on("_coherentCreateImageData", engine._coherentCreateImageData);
	engine.on("_coherentUpdatedImageData", engine._coherentUpdatedImageData);

	engine.on('_Result', engine._Result, engine);
	engine.on('_Register', engine._Register, engine);
	engine.on('_Unregister', engine._Unregister, engine);
	engine.on('_OnReady', engine._OnReady, engine);
	engine.on('_OnError', engine._OnError, engine);

	engine.on('__OnReplayRecordCompleted', function(jsonRecords) {
		if (engine.onEventsReplayed) {
			engine.onEventsReplayed();
		}
	});

	engine.BindingsReady(VERSION[0], VERSION[1], VERSION[2], VERSION[3]);

	return engine;
});

