/* Minification failed. Returning unminified contents.
(1772,23-24): run-time error JS1195: Expected expression: )
(1772,26-27): run-time error JS1195: Expected expression: >
(1772,44-45): run-time error JS1195: Expected expression: )
(1772,75-76): run-time error JS1195: Expected expression: )
(1773,2-3): run-time error JS1002: Syntax error: }
(1774,33-34): run-time error JS1195: Expected expression: )
(1774,35-36): run-time error JS1004: Expected ';': {
(1782,3-4): run-time error JS1195: Expected expression: ,
(1783,32-33): run-time error JS1195: Expected expression: )
(1783,34-35): run-time error JS1004: Expected ';': {
(1788,6-7): run-time error JS1195: Expected expression: ,
(1804,3-4): run-time error JS1195: Expected expression: ,
(1815,3-4): run-time error JS1195: Expected expression: ,
(1816,37-38): run-time error JS1195: Expected expression: )
(1820,3-4): run-time error JS1195: Expected expression: ,
(1821,28-29): run-time error JS1195: Expected expression: )
(1828,3-4): run-time error JS1195: Expected expression: ,
(1829,37-38): run-time error JS1195: Expected expression: )
(1832,6-7): run-time error JS1195: Expected expression: ,
(1833,27-28): run-time error JS1195: Expected expression: )
(1854,3-4): run-time error JS1195: Expected expression: ,
(1858,11-12): run-time error JS1004: Expected ';': :
(1861,16-25): run-time error JS1197: Too many errors. The file might not be a JavaScript file: undefined
(1837,4-34): run-time error JS1018: 'return' statement outside of function: return this.deviceTypes.tablet
(1843,5-35): run-time error JS1018: 'return' statement outside of function: return this.deviceTypes.tablet
(1847,5-35): run-time error JS1018: 'return' statement outside of function: return this.deviceTypes.mobile
(1852,4-34): run-time error JS1018: 'return' statement outside of function: return this.deviceTypes.mobile
(1831,3-136): run-time error JS1018: 'return' statement outside of function: return GameLoader.configuration.swipeToHideIosBlacklist.slice(1, -1).split(',').map(Number).includes(PlatformManager.getIOSVersion())
(1827,3-15): run-time error JS1018: 'return' statement outside of function: return false
(1825,4-15): run-time error JS1018: 'return' statement outside of function: return true
(1819,3-16): run-time error JS1018: 'return' statement outside of function: return "True"
(1809,4-39): run-time error JS1018: 'return' statement outside of function: return config.disableSpacebarToSpin
(1813,4-17): run-time error JS1018: 'return' statement outside of function: return "True"
(1802,3-17): run-time error JS1018: 'return' statement outside of function: return "False"
(1800,4-27): run-time error JS1018: 'return' statement outside of function: return fullscreenConfig
(1796,4-18): run-time error JS1018: 'return' statement outside of function: return "False"
(1787,3-15): run-time error JS1018: 'return' statement outside of function: return false
(1785,4-15): run-time error JS1018: 'return' statement outside of function: return true
(1781,3-15): run-time error JS1018: 'return' statement outside of function: return false
(1779,4-15): run-time error JS1018: 'return' statement outside of function: return true
(1776,4-15): run-time error JS1018: 'return' statement outside of function: return true
 */

/**
 * Module used for localization (to translate messages)
 */
var PNGLocalizationManager = PNGLocalizationManager || (function () {
	var _values = {};

	function init(values) {
		_values = values;
	}

	function translate(key, defaultValue) {
		if ((_values || {}).hasOwnProperty(key))
			return _values[key];

		return defaultValue;
	}

	return {
		init: init,
		get: translate,
	};
})();
;

/**
 * Helper methods for megaton modules to subscribe themself and make sure the engine can instanciate them. 
 */
var PNGModules = PNGModules || (function () {
	var external = {};
	var internal = {};

    /**
     * Called from the gameloader as soon as the config is returned from the server.
     * @param {any} availableModules
     */
	function setAvailableModules(availableModules)
	{
		for (var i = 0; i < availableModules.length; ++i)
		{
			external[availableModules[i].name] = availableModules[i];
		}
	}

	/**
	 * Use this function to include your module without adding it to the grid.
	 * @param {string} name The name of the module. For example "honeycomb". This should match the name property in the modules JSON config.
	 * @param {Function} factoryMethod The initiation method for the module. This will be called after game start.
	 * @returns {boolean} If the registration was successful or not.
	 */
	function addExternalModule(name, factoryMethod)
	{
		if (!hasExternalModule(name))
		{
			console.warn("Module with name " + name + " tried to register itself but it's not configured correctly.");
			return false;
		}

		external[name].initiate = factoryMethod;
		return true;
	}

	/**
	* Use this function to include your module and adding it to the grid.
	* @param {string} name The name of the module. For example "honeycomb". This should match the name property in the modules JSON config.
	* @param {string[]} positions An array with elements in the grid that the module want to utilize.
	* @param {string[]} priorities Unused argument that specified where the parent should position its child.
	* @param {Function} factoryMethod The initiation method for the module. This will be called after game start.
    * @param {Function} positionsMethod Optional method that returns an array of positions for elements within the module.
	* @returns {boolean} If the registration was successful or not.
	*/
	function addExternalModuleInGrid(name, positions, priorities, factoryMethod)
	{
		var order = external[name].order;
		if (!validateInput(name, positions, order))
		{
			console.warn("Failed to insert module: " + name + " into grid. It will not be initialized.");
			return false;
		}

		var gridCells = order.map(function(item, index)
		{
			return createElement(order[index]);
		});

		external[name].initiate = factoryMethod;
		external[name].gridCells = gridCells;
		external[name].position = {
			current: [],
		};

		/*
			* Calling reevaluateLayout for first time during module
			* initialization to append it inside the grid.
		*/
		reevaluateLayout(external[name], positions);
		return true;
	}

	/**
	 * This function takes care of correct positioning for module elements within the grid.
	 **/
	function reevaluateLayout(module, newPositions)
	{
		// ensures that basic initialization is complete
		if (module.gridCells && module.position)
		{
			if (newPositions.length !== module.gridCells.length)
			{
				return;
			}

			for (var j = 0; j < newPositions.length; ++j)
			{
				if (module.position.current[j] !== newPositions[j])
				{
					// remove element from its parent if it has one
					if (module.gridCells[j].parentElement)
					{
						module.gridCells[j].parentElement.removeChild(module.gridCells[j]);
					}
					// append to the new container element
					insertElement(newPositions[j], module.gridCells[j]);
					module.position.current[j] = newPositions[j];
				}
			}
		}
	}

	/**
	 * The idea is that internal modules are passed down to the engine and initialized there.
	 * The only module handled like that atm is the viewfactory.
	 * @param {string} name The name of the module. For example "honeycomb". This should match the name property in the modules JSON config.
	 * @param {Function} factoryMethod The initiation method for the module. This will be called inside Megaton GameBase class.
	 * @returns {boolean} If the registration was successful or not.
	 */
	function addInternalModule(name, factoryMethod)
	{
		if (hasInternalModule(name))
		{
			console.warn("module with name " + name + " already defined as internal module.");
			return false;
		}

		internal[name] = factoryMethod;
		return true;
	}

	function hasExternalModule(name)
	{
		return external.hasOwnProperty(name);
	}

	function hasInternalModule(name)
	{
		return internal.hasOwnProperty(name);
	}

	function getExternalModules()
	{
		return Object.freeze(external);
	}

	function getInternalModules()
	{
		return Object.freeze(internal);
	}

	var gridPosition = {
		LEFT: "left-column", // To the left of the game, covering full window in height.
		TOP: "game-column-top", // Above the game. Same width as game.
		BOTTOM: "game-column-bot", // Below the game. Same with as game.
		RIGHT: "right-column" // To the right of the game, covering full window in height.
	};

	var order = {
		OUTER: "outer", // As far from the game as possible
		INNER: "inner"
	};

	function validateInput(name, positions, priorities)
	{
		if (!hasExternalModule(name))
		{
			console.warn("module with name " + name + " hasn't been registered correctly.");
			return false;
		}

		if (!Array.isArray(positions) || !Array.isArray(priorities))
		{
			console.warn("module with name " + name + " should provide position and order in arrays.");
			return false;
		}

		if (positions.length !== priorities.length)
		{
			console.warn("module with name " + name + " should provide order for each position.");
			return false;
		}

		return true;
	}

    function createElement(order)
	{
		var element = document.createElement("div");
		element.className = "grid-cell";
		element.style.order = order.toString();
		return element;
	}

	/**
	 * This will insert a new cell in the grid next to some other cell
	 * @param {string} position RIGHT/LEFT/TOP/BOTTOM of the game.
	 * @param {HTMLDivElement} element The element to insert.
	 * @param {string} moduleOrder INNER or OUTER (inner is closest to the game).
	 */
	function insertElement(position, element)
	{
        var parentElement = "";

		switch (position)
		{
			case gridPosition.RIGHT:
                parentElement = "#right-column";
				break;

			case gridPosition.LEFT:
                parentElement = "#left-column";
				break;

			case gridPosition.TOP:
                parentElement = "#game-row-top";
				break;

			case gridPosition.BOTTOM:
                parentElement = "#game-row-bottom";
				break;

			default:
				console.warn("Failed to position element.", position);
				return;
		}

		document.querySelector(parentElement).appendChild(element);
	}

    return {
		setAvailableModules: setAvailableModules,
		addExternalModule: addExternalModule,
		addExternalModuleInGrid: addExternalModuleInGrid,
		addInternalModule: addInternalModule,
		hasExternalModule: hasExternalModule,
		hasInternalModule: hasInternalModule,
		gridPosition: gridPosition,
		order: order,
		getExternalModules: getExternalModules,
		getInternalModules: getInternalModules,
		reevaluateLayout: reevaluateLayout,
	};

})();;
/**
 * Constructor for the StatsHandler. This class is used by all three platforms.
 * For Megaton most of the communication goes through extcom.
 * Mobile initiates most events by calling window.StatsHandler.HandleEvent in the game.
 * Flash initiates all events in js.cshtml.
 * If the endPoint is not configured we will not load the required piwik.js file. The class
 * should still work (so we don't have to check if it's there everytime we call it) but it
 * won't try to send any events to Matomo.
 */
function StatsHandler(gId, channel, endPoint, platform, language, practice, matomoSiteId)
{
	window._paq = window._paq || [];

	this.gId = gId;
	this.channel = channel;
	this.endPoint = endPoint;
	this.platform = platform;
	this.language = language;
	this.practice = practice;
	this.matomoSiteId = matomoSiteId;

	// Some events are dispatched multiple times during startup.Use this bool to prevent sending duplicated data to Matomo
	this.gameLoaded = false;

	// In Megaton we identify the initial sound with SOUND_ON/SOUND_OFF events but after that we use the SETTINGS event instead.
	this.initialSoundReceived = false;

	this.oldOrientation = "";
	this.currentOrientation = "";
	this.loadStartTime = 0;
	this.loadEndTime = 0;
	this.connectStartTime = new Date().getTime();
	this.connectEndTime = 0;
	this.isHyperSpinActive = false;
	this.sentSpinEventsList = [];  // keeps track of which spin events were sent since last orientation change

	this.debug = false;
	this.platform = "";
	this.gameSpecificVariables = {
		tutorial: {
			autoInit: false,
			startTimeStampMS: -1
		}
	};

	if (endPoint)
	{
		this.addMatomoScript();
	}

	this.loadingStart();
}

StatsHandler.prototype.Megaton = 1;
StatsHandler.prototype.Flash = 2;
StatsHandler.prototype.LegacyMobile = 3;

/**
 * Load piwik.js. A script required to communicate with the server correctly.
 */
StatsHandler.prototype.addMatomoScript = function ()
{
	window._paq.push(["setDocumentTitle", "Megaton " + this.channel]);
	window._paq.push(["setTrackerUrl", this.endPoint + "piwik.php"]);
	window._paq.push(["setSiteId", this.matomoSiteId]);

	var script = document.createElement("script");
	var scripts = document.getElementsByTagName("script")[0];

	script.type = "text/javascript";
	script.async = true;
	script.defer = true;

	script.src = this.endPoint + "piwik.js";
	scripts.parentNode.insertBefore(script, scripts);
};

/**
 * Add a callback for all events that we want to listen to in Megaton.
 */
StatsHandler.prototype.addGameListeners = function ()
{
	var HandleEventBind = this.HandleEvent.bind(this);

	for (var i in this.gameEvents)
	{
		if (this.gameEvents.hasOwnProperty(i))
		{
			window.extcom.addEventListener(this.gameEvents[i], HandleEventBind);
		}
	}
	var calls = window.extcom.gameCalls();
	calls.GET_SOUND();
};

StatsHandler.prototype.setDebug = function (debug)
{
	this.debug = debug;
};

// These are events we listen to from the game.
StatsHandler.prototype.gameEvents =
{
	Loader: "LOADER",
	LoginAnswer: "LOGINANSWER",
	Settings: "SETTINGS",
	RoundStart: "round_start",
	OnRoundStart: "onroundstart",
	SoundOn: "SOUND_ON",
	SoundOff: "SOUND_OFF",
	OnHelp: "UIHELP",
	OnShowPaytable: "UISHOWPAYTABLE",
	OnHistory: "UIHISTORY",
	AutoplayStarted: "AUTOPLAY_STARTED",
	Message: "MESSAGE",
	ExternalTutorialAutoInit: "EXTERNAL_TUTORIAL_AUTOINIT",
	ExternalTutorialInit: "EXTERNAL_TUTORIAL_INIT",
	ExternalTutorialStarted: "EXTERNAL_TUTORIAL_STARTED",
	ExternalTutorialExit: "EXTERNAL_TUTORIAL_EXIT",
	ExternalTutorialFinish: "EXTERNAL_TUTORIAL_FINISH",
	HyperSpinActivated: "HYPERSPIN_ACTIVATE",
	HyperSpinDeactivated: "HYPERSPIN_DEACTIVATE"
};

// StatsHandler has some internal events to communicate the result of some state changes.
// These are usually followed by other analyticEvents.
StatsHandler.prototype.internalEvents =
{
	Load: "load",
	Loaded: "loaded",
	Connected: "connected",
	LoadTime: "loadtime",
	LoginTime: "logintime",
	Language: "language"
};

StatsHandler.prototype.legacyEvents =
{
	AutoplayStarted: "autoplay",
	SoundOn: "soundon",
	SoundOff: "soundoff",
	InitialSoundOn: "initialsoundon",
	InitialSoundOff: "initialsoundoff",
	ShowPaytable: "showpaytable",
	ShowHelp: "showhelp",
	ConnectionLost: "connectionlost"
};

// These are events we send to Matomo.
StatsHandler.prototype.analyticEvents =
{
	Loaded: "Loaded",
	Load: "Load",
	LoadTime: "Load Time",
	LoginTime: "Login time",
	Connected: "Connected",
	InitialSoundOn: "Initial Sound On",
	InitialSoundOff: "Initial Sound Off",
	SoundOn: "Sound On",
	SoundOff: "Sound Off",
	ShowHelp: "Show Help",
	ShowPaytable: "Show Paytable",
	ShowHistory: "Show History",
	AutoplayStarted: "AutoplayStarted",
	Language: "Language",
	ConnectionFailure: "Connection Failure",
	ConnectionLost: "Connection Lost",
	TutorialStartedInPractice: "Tutorial Started In Practice",
	TutorialStarted: "Tutorial Started",
	TutorialRevisit: "Tutorial Revisit",
	TutorialAborted: "Tutorial Aborted",
	TutorialTimeAborted: "Tutorial Time Aborted",
	TutorialSkipped: "Tutorial Skipped",
	TutorialTimeCompleted: "Tutorial Time Completed",
	TutorialCompleted: "Tutorial Completed",
	SpinLandscape: "Spin /Landscape",
	SpinPortrait: "Spin /Portrait",
	HyperSpinLandscape: "HyperSpin /Landscape",
	HyperSpinPortrait: "HyperSpin /Portrait"
};

// Send events with the following parameters:
// Category: for example Game or Help.
// Action: for example "Load Time".
// Name: name of the game.
// Value: any numeric value that the action might need. For example the actual time for the "Load Time" action.
// PascalCase to fit with the legacy mobile code that calls this function directly.
// e can be an object (megaton, flash) or a string (legacymobile). In legacymobile any properties are
// passed in an object as a second argument to this function.
StatsHandler.prototype.HandleEvent = function (e)
{
	this.log(e.type);

	var eventString = typeof e === "object" ? e.type : e;
	var eventData = arguments[1];

	switch (eventString)
	{
		case this.gameEvents.Loader:
			if (e.id === 3)
			{
				this.LoadingComplete();
			}
			break;

		case this.gameEvents.LoginAnswer:
			if (e.userId > 0)
			{
				this.connectingComplete();
			}
			break;

		case this.gameEvents.Settings:
			this.paseSettings(e);
			break;

		case this.internalEvents.Load:
			this.addStatEvent(this.analyticEvents.Load);
			break;

		case this.internalEvents.Loaded:
			this.gameLoaded = true;
			this.addStatEvent(this.analyticEvents.Loaded);
			break;

		case this.internalEvents.Connected:
			this.addStatEvent(this.analyticEvents.Connected);
			break;

		case this.gameEvents.RoundStart:
		case this.gameEvents.OnRoundStart:
			if (this.channel !== "mobile")
				break;

			this.oldOrientation = this.currentOrientation;
			this.currentOrientation = window.innerWidth > window.innerHeight ? "ls" : "pt";

			// clear the spin events array on orientation change
			if (this.oldOrientation !== this.currentOrientation)
			{
				this.sentSpinEventsList = [];
			}

			var spinEventName = "";
			if (this.currentOrientation === "pt")
			{
				spinEventName = this.isHyperSpinActive ? this.analyticEvents.HyperSpinPortrait : this.analyticEvents.SpinPortrait;
			}
			else
			{
				spinEventName = this.isHyperSpinActive ? this.analyticEvents.HyperSpinLandscape : this.analyticEvents.SpinLandscape;
			}

			// send the spin event if it hasn't already been sent
			if (this.sentSpinEventsList.indexOf(spinEventName) === -1)
			{
				this.sentSpinEventsList.push(spinEventName);
				this.addStatEvent(spinEventName);
			}

			break;

		case this.legacyEvents.InitialSoundOn:
			if (this.gameLoaded)
			{
				this.addStatEvent(this.analyticEvents.InitialSoundOn);
			}
			break;

		case this.legacyEvents.InitialSoundOff:
			if (this.gameLoaded)
			{
				this.addStatEvent(this.analyticEvents.InitialSoundOff);
			}
			break;

		case this.gameEvents.SoundOn:
			if (!this.initialSoundReceived)
			{
				this.addStatEvent(this.analyticEvents.InitialSoundOn);
				this.initialSoundReceived = true;
			}
			break;

		case this.gameEvents.SoundOff:
			if (!this.initialSoundReceived)
			{
				this.addStatEvent(this.analyticEvents.InitialSoundOff);
				this.initialSoundReceived = true;
			}
			break;

		case this.legacyEvents.SoundOn:
			if (this.gameLoaded)
			{
				this.addStatEvent(this.analyticEvents.SoundOn);
			}
			break;

		case this.legacyEvents.SoundOff:
			if (this.gameLoaded)
			{
				this.addStatEvent(this.analyticEvents.SoundOff);
			}
			break;

		case this.legacyEvents.ShowHelp:
		case this.gameEvents.OnHelp:
			this.addStatEvent(this.analyticEvents.ShowHelp);
			break;

		case this.legacyEvents.ShowPaytable:
		case this.gameEvents.OnShowPaytable:
			this.addStatEvent(this.analyticEvents.ShowPaytable);
			break;

		case this.gameEvents.OnHistory:
			this.addStatEvent(this.analyticEvents.ShowHistory);
			break;

		case this.gameEvents.AutoplayStarted:
			this.addStatEvent(this.analyticEvents.AutoplayStarted, e.playsLeft);
			break;

		case this.legacyEvents.AutoplayStarted:
			this.addStatEvent(this.analyticEvents.AutoplayStarted, eventData.numautoplays);
			break;

		case this.internalEvents.LoadTime:
			this.addStatEvent(this.analyticEvents.LoadTime, e.time);
			break;

		case this.internalEvents.LoginTime:
			this.addStatEvent(this.analyticEvents.LoginTime, e.time);
			break;

		case this.legacyEvents.ConnectionLost:
			this.addStatEvent(this.analyticEvents.ConnectionLost);
			break;

		case this.gameEvents.Message:
			if (e.flags === 4)
			{
				this.addStatEvent(this.analyticEvents.ConnectionLost);
			}
			else if (e.flags === 3)
			{
				this.addStatEvent(this.analyticEvents.ConnectionFailure);
			}
			break;

		case this.gameEvents.ExternalTutorialAutoInit:
			this.gameSpecificVariables.tutorial.autoInit = true;
			break;

		case this.gameEvents.ExternalTutorialInit:
			this.gameSpecificVariables.tutorial.autoInit = false;
			break;

		case this.gameEvents.ExternalTutorialStarted:
			var tutorialStartEvent = this.practice === "1" ? this.analyticEvents.TutorialStartedInPractice : this.analyticEvents.TutorialStarted;
			this.addStatEvent(tutorialStartEvent);
			this.gameSpecificVariables.tutorial.startTimeStampMS = new Date().getTime();
			if (!this.gameSpecificVariables.tutorial.autoInit)
			{
				this.addStatEvent(this.analyticEvents.TutorialRevisit);
			}
			break;

		case this.gameEvents.ExternalTutorialExit:
			//if it was aborted mid tutorial
			if (this.gameSpecificVariables.tutorial.startTimeStampMS !== -1)
			{
				var exitDiffMs = Date.now() - this.gameSpecificVariables.tutorial.startTimeStampMS;
				this.gameSpecificVariables.tutorial.startTimeStampMS = -1;
				this.addStatEvent(this.analyticEvents.TutorialAborted);
				this.addStatEvent(this.analyticEvents.TutorialTimeAborted, exitDiffMs);
			}
			// it was skipped upon start
			else
			{
				this.addStatEvent(this.analyticEvents.TutorialSkipped);
			}
			break;

		case this.gameEvents.ExternalTutorialFinish:
			if (this.gameSpecificVariables.tutorial.startTimeStampMS !== -1)
			{
				var endDiffMs = Date.now() - this.gameSpecificVariables.tutorial.startTimeStampMS;
				this.addStatEvent(this.analyticEvents.TutorialTimeCompleted, endDiffMs);
			}
			this.gameSpecificVariables.tutorial.startTimeStampMS = -1;
			this.gameSpecificVariables.tutorial.autoInit = false;
			this.addStatEvent(this.analyticEvents.TutorialCompleted);
			break;
		case this.gameEvents.HyperSpinActivated:
			this.isHyperSpinActive = true;
			break;
		case this.gameEvents.HyperSpinDeactivated:
			this.isHyperSpinActive = false;
			break;

		default:
			break;
	}
};

// Use this function instead of pushing to the queue directly in case we want to re-arrange the order
// or initiate some further action for each event.
StatsHandler.prototype.addStatEvent = function (event, value)
{
	if (typeof value !== "undefined")
	{
		window._paq.push(["trackEvent", "Game", event, this.gId, value]);
	}
	else
	{
		window._paq.push(["trackEvent", "Game", event, this.gId]);
	}

	this.log("Sent event to matomo: " + event, " with value: " + value);
};

StatsHandler.prototype.loadingStart = function ()
{
	this.log("LoadingStart");
	this.loadStartTime = new Date().getTime();
	this.HandleEvent({ type: "load" });
};

StatsHandler.prototype.LoadingComplete = function ()
{
	this.loadEndTime = new Date().getTime();
	var time = this.loadEndTime - this.loadStartTime;
	this.HandleEvent({ type: "loaded" });
	this.HandleEvent({ type: "loadtime", time: time });
};

StatsHandler.prototype.connectingComplete = function ()
{
	this.connectEndTime = new Date().getTime();
	var time = this.connectEndTime - this.connectStartTime;
	this.HandleEvent({ type: "connected" });
	this.HandleEvent({ type: "logintime", time: time });
};

StatsHandler.prototype.lastSettings = undefined;

StatsHandler.prototype.copyObject = function (obj)
{
	var newObj = {};
	for (prop in obj)
	{
		newObj[prop] = obj[prop];
	}

	return newObj;
};

StatsHandler.prototype.paseSettings = function (o)
{
	if (this.lastSettings === undefined)
	{
		this.lastSettings = {};
		for (key in o.settings)
		{
			if (key != "children")
				this.lastSettings[key] = o.settings[key];
		}
		return;
	}
	var settingsChanged = [];
	for (key in o.settings)
	{
		if ((key !== "children" || key !== "_children") && (key[0] === "_"))
		{
			if (this.lastSettings[key] != o.settings[key])
			{
				settingsChanged.push({ id: key, newVal: o.settings[key] });
			}
		}
	}
	if (this.lastSettings === undefined)
	{
		this.lastSettings = o;
		return;
	}

	for (var i = 0; i < settingsChanged.length; i++)
	{
		this.onSettingsChange(settingsChanged[i]);
	}

	for (key in o.settings)
	{
		if (key != "children")
			this.lastSettings[key] = o.settings[key];
	}
};

StatsHandler.prototype.onSettingsChange = function (setting)
{
	this.log("Settings changed: " + setting.id);
	switch (setting.id)
	{
		case "_sound":
			if (setting.newVal === true)
			{
				this.addStatEvent(this.analyticEvents.SoundOn);
			}
			else if (setting.newVal === false)
			{
				this.addStatEvent(this.analyticEvents.SoundOff);
			}
			break;

		case "_fastPlay":
			break; // //Not yet ín use. Remove break to activate
			if (setting.newVal === true)
				ga_playngo("send", "event", "Megaton " + this.channel, "FastPlay On", this.gId);
			else if (setting.newVal === false)
				ga_playngo("send", "event", "Megaton " + this.channel, "FastPlay Off", this.gId);
			break
		case "_leftHandMode":
			break; // //Not yet ín use. Remove break to activate
			if (setting.newVal === true)
				ga_playngo("send", "event", "Megaton " + this.channel, "LeftHandMode On", this.gId);
			else if (setting.newVal === false)
				ga_playngo("send", "event", "Megaton " + this.channel, "LeftHandMode Off", this.gId);
			break
	}
};

StatsHandler.prototype.log = function (msg)
{
	if (this.debug)
	{
		var s = "", css = "";
		for (var i = 0; i < arguments.length; i++)
		{
			if (typeof arguments[i] == "string" && arguments[i].match("background"))
				css = arguments[i];
			else
				s += " " + arguments[i];
		}
		console.log(s, css);
	}
};

// This is the worst fix mankind has ever seen. This method is being called in practice mode in the mobile client, and we cannot change them at this time. So we need a empty function to avoid crashes.
StatsHandler.prototype.ConnectingStart = function ()
{

};
;


/**
 * Some functions for loading and importing scripts. Other resources are handled in getBundleObject.
 * 3rd party dependencies like pixi are now included in common.bundle.js (bundlemode 0)
 *
 * We currently have multiple configs for various resources:
 *		game.modules which is used almost only for json resource bundles except for the viewfactory.
 *		game.url which is used to extract the game name here.
 *		game.additionalModules is now used for modules like soundhelper and debug.
 *		Other modules implementing IModules in Megaton be declared through game.modules.
 *
 */
var PNGResources = PNGResources || (function () {

	/**
	 * Stores the bundle modes in an enum type object.
	 */
	var bundleModes = {
		GameSeparation: "0",
		Old: "1"
	};

	/**
	 * Load JS bundles by appending them to document.head.
	 * @param dependencies An array with .js files we want to load.
	 * @param errorCallback Fired if at least one file fails to load.
	 * @param successCallback Fired if ALL dependencies are loaded correctly.
	 */
	function loadScripts(dependencies, errorCallback, successCallback)
	{
		/*
			TODO: load all files async at the same time.
			Do one promise using Promise.all([...promises]) that will load all files and either call
			success or error callback. This will make loading faster since we load multiple assets at the same time.
		*/
		var index = 0;
		var ie = navigator.userAgent.indexOf('MSIE') !== -1;
		var head = document.getElementsByTagName('head')[0];

		function loadScript()
		{
			var script = document.createElement('script');
			script.type = 'text/javascript';
			script.src = dependencies[index].src;
			script.crossOrigin = "anonymous";

			if (ie)
			{
				script.onreadystatechange = function ()
				{
					if (this.readyState === 'loaded' || this.readyState === 'complete')
					{
						onLoaded();
					}
				};
				script.onerror = onError;
			}
			else
			{
				script.addEventListener('load', onLoaded, false);
				script.addEventListener('error', onError, false);
			}

			head.appendChild(script);
		}

		function loadCss()
		{
			var head = document.getElementsByTagName('head')[0];
			var cssNode = document.createElement('link');
			cssNode.type = 'text/css';
			cssNode.rel = 'stylesheet';
			cssNode.href = dependencies[index].src;

			// Only interrupt game launch if file is required.
			if (dependencies[index].required)
			{
				cssNode.addEventListener('error', errorCallback, false);
				cssNode.addEventListener('load', onLoaded, false);
			}
			else
			{
				onLoaded();
			}

			head.appendChild(cssNode);
		}

		function loadFile()
		{
			if (/.js$/g.test(dependencies[index].src))
			{
				loadScript();
			}
			else if (/.css$/g.test(dependencies[index].src))
			{
				loadCss();
			}
			else
			{
				console.warn("Tried to load unsupported file type.", dependencies[index].src);

				if (dependencies[index].required)
				{
					return errorCallback();
				}
				
				onLoaded();
			}
		}

		/* Called for each script that fails to load.*/
		function onError(e)
		{
			if (!--dependencies[index].retries)
			{
				// Only interrupt game launch if file is required.
				if (dependencies[index].required)
				{
					return errorCallback();
				}

				onLoaded();
			}
			else
			{
				loadFile();
			}
		}

		function onLoaded(e)
		{
			index++;

			if (index === dependencies.length)
			{
				successCallback();
			}
			else
			{
				loadFile();
			}
		}

		loadFile();
	}

	/**
	 * Declare which dependencies should be passed to the function loadScripts.
	 * @param {any} config
	 * @param {any} channel
	 * @param {any} successCallback
	 * @param {any} additionalModules
	 * @param {any} corewebUrl
	 */
	function initiateScriptLoad(config, channel, successCallback, additionalModules, corewebUrl)
	{
		var splitted = config.gameURL.split("/");
		var gameName = splitted[splitted.length - 2];
		channel = getValidResourceChannel(channel);
		var onLoadError = function () 
		{
			new LauncherMessage({ msg: PNGLocalizationManager.get("COREWEB.RESOURCES.LOADFAILMESSAGE", "Failed to load resources."), channel: channel, lobbyUrl: config.lobbyUrl });

			EngageProxy.postMessage({
				type: "COREWEB_ERROR",
				data: {
					_flags: 4,
					_title: PNGLocalizationManager.get("COREWEB.RESOURCES.ENGAGE.ERRORTITLE", "Gameloader Error"),
					_message: PNGLocalizationManager.get("COREWEB.RESOURCES.ENGAGE.ERRORMESSAGE", "Error loading script")
				}
			});
		};
		var dependencies = [];

		switch (config.bundleMode) 
		{
			case bundleModes.Old:
				dependencies = [
					{ src: "${RESOURCEROOT}games/common.bundle.js", retries: 10, required: true },
					{ src: "${RESOURCEROOT}games/${GAMENAME}/${GAMENAME}_main.bundle.js", retries: 10, required: true },
					{ src: "${RESOURCEROOT}games/${GAMENAME}/${GAMENAME}_${CHANNEL}${UIVERSION}.bundle.js", retries: 10, required: true }
				];
				break;
			case bundleModes.GameSeparation:
				dependencies = [
					{ src: "${RESOURCEROOT}games/${GAMENAME}/${GAMENAME}_${CHANNEL}${UIVERSION}.bundle.js", retries: 10, required: true }
				];
				break;
			default:
				return;
		}

		dependencies = dependencies
			.concat(getPolyfills())
			.concat(getAdditionalModules(additionalModules))
			.map(function (dep) {
				dep.src = dep.src
					.replace(/\$\{RESOURCEROOT\}/g, config.resourceRoot)
					.replace(/\$\{GAMENAME\}/g, gameName)
					.replace(/\$\{CHANNEL\}/g, channel)
                    .replace(/\$\{COREWEBURL\}/g, corewebUrl)
                    .replace(/\$\{BRAND\}/g, config.brand)
					.replace(/\$\{UIVERSION\}/g, config.uiVersion || "");
				return dep;
			});

		loadScripts(dependencies, onLoadError, successCallback);
	}

	/**
	 *	Scripts are now loaded by appending them to head (bundlemode 0). Each module should have a unique entry in the global scope (bundlemode 0).
	 *	Internal modules like the UI are located in the megaton-games/packages repos.
	 *	Some of the external modules are in the megaton-modules repo (soundhelper and debug).
	 */
	function getEntries(config, channel) 
	{
		channel = getValidResourceChannel(channel);
		var entries = {
			internalModules: {},
			externalModules: {},
			start: undefined
		};
    var uiVersion = config.uiVersion || "";
    
		return new Promise(function (resolve, reject)
		{
			switch (config.bundleMode)
			{
				case bundleModes.Old:
					PNGModules.addInternalModule("ui", window[channel + uiVersion + "_Entry"]);
					entries.internalModules = PNGModules.getInternalModules() || {};
					entries.externalModules = PNGModules.getExternalModules() || {};
					//The module should be able to do PNGModules.addInternalModule("ui", create); instead of doing this hack.
					
					entries.internalModules["ui"] = window[channel + uiVersion + "_Entry"];
					entries.start = window.main_Entry.start;
					resolve(entries);
					break;
				case bundleModes.GameSeparation:
					PNGModules.addInternalModule("ui", window[channel + uiVersion + "_Entry"]);
					entries.internalModules = PNGModules.getInternalModules() || {};
					entries.externalModules = PNGModules.getExternalModules() || {};
					// The module should be able to do PNGModules.addInternalModule("ui", create); instead of doing this hack.
					entries.internalModules["ui"] = window[channel + uiVersion + "_Entry"];
					entries.start = window[channel + uiVersion + "_Entry"].start;
					resolve(entries);
					break;
			}
		});
	}

	/**
	 * Polyfills. Bluebird required for promises in IE.
	 */
	function getPolyfills()
	{
		var conditionalModules = [
			{ src: "${COREWEBURL}Content/javascript/3rdparty/bluebird.core.min.js", name: "bluebird.core", condition: typeof (window.Promise) === "undefined" }
		];

		return conditionalModules.filter(function (m)
		{
			return m.condition;
		});

	}


	/**
	 * Modules such as honeycomb and playerprotectionsweden.
	 * module.src should be an absolute path since we're not using game.resourceRoot
	 * @param {any} additionalModules
	 */
	function getAdditionalModules(additionalModules)
	{
		var modulesToLoad = [];
		Object.keys(additionalModules).forEach(function (key)
		{
			var module = additionalModules[key];
			/* Dependencies should be loaded before the module to prevent undefined references */
			if (module.dependencies)
			{
				module.dependencies.forEach(function (d)
				{
					modulesToLoad.push({
						src: d,
						retries: 2,
						required: module.required
					});
				});
			}
			if (module.resources)
			{
				module.resources.forEach(function(r) 
				{
					modulesToLoad.push({
						src: r,
						retries: 2,
						required: module.required
					});
				});
			}
			if (module.script)
			{
				modulesToLoad.push({
					src: module.script,
					name: module.name,
					retries: 2,
					required: module.required
				});
			}
		});
		return modulesToLoad;
	}

	/**
	 * Here we wrap some resources into an object which will be passed down to the game and loaded there. 
	 * These are configured with the game.modules config.
	 * @param {any} channel
	 * @param {any} configuration
	 */
	function getBundleObject(channel, configuration)
	{
		channel = getValidResourceChannel(channel);
		var gameResources = JSON.parse(configuration.gameModules);
		addHardCodedResources(gameResources, channel, configuration);

		var data = {
			bundle: []
		};

		var bundle = data.bundle;

		for (var k in gameResources)
		{
			if (!gameResources.hasOwnProperty(k))
			{
				continue;
			}
			if (gameResources[k].resource && shouldLoadResource(configuration, k))
			{
				// To avoid adding placeholder for uiversion in all of game_set we simply append it together with channel since it comes directly afterwards.
				var uiVersion = configuration.uiVersion || "";
				var url = gameResources[k].resource
					.replace("${CHANNEL}", channel + uiVersion);

				bundle.push({ name: k, url: url });
			}
		}

		return data;
	}

	/**
	 * Available channels:
	 * desktop
	 * mobile
	 * mini
	 * cabinet
	 * 
	 * Resources for cabinet are the same as desktop.
	 * @param {any} channel
	 */
	function getValidResourceChannel(channel) {
		switch (channel) {
			case "cabinet":
				return "desktop";
			default:
				return channel;
		}
	}

	/**
	 * Some resources always have the same path.They can be added here.
	 * @param {any} modules
	 * @param {any} channel
	 * @param {any} configuration
	 */
	function addHardCodedResources(modules, channel, configuration)
	{
		modules["language"] = {
			script: "",
			resource: "resources/lang/${language}/locale.json"
		};

		if(channel === "desktop" && (configuration.showHelpInPaytable  === "True" || configuration.showMobilePaytableInDesktop  === "True") && modules.hasOwnProperty("bundleconfig") && modules["bundleconfig"].resource !== undefined)
		{
			modules["germanydesktopbundle"] = {
				script: "",
				resource: modules["bundleconfig"].resource.replace("config_${CHANNEL}", "germany_desktop_additional_resources")
			};
		}

	}

	function shouldLoadResource(configuration, resourceName)
	{
		switch (resourceName)
		{
			case "featurepreview":
				return configuration.showSplash === "True";
			case "mysteryjackpot":
				return configuration.hasMysteryJackpot === "True";
			case "guaranteedjackpot":
				return configuration.hasGuaranteedJackpot === "True";
			default:
				return true;
		}
	}

	return {
		getBundleObject: getBundleObject,
		getEntries: getEntries,
		initiateScriptLoad: initiateScriptLoad
	};

})();















;

function LoginRequest(serverUrl, pid, ticket, username, password, contextId, language, gameId, gameName, channel, onLoginCallback, onErrorCallback, localizedFFErrors, country, region)
{
	var packetIndex = 1;
	var responses = {};
	var sessionId = "0";
	var authenticationString = ticket ? ("\"" + ticket + "\" \"\" \"" + contextId + "\" \"\" \"" + country + "\" \"" + region + "\"") : ("\"" + username + "\" \"" + password + "\" \"" + contextId + "\" \"\" \"" + country + "\" \"" + region + "\"");
	var done = false;
	var loginRequestSent = false;

	var sessionRequest = [LoginRequest.COMMAND_SESSION, pid, '"' + language + '"', gameId, '"' + encodeURIComponent(navigator.userAgent) + '"', '"' + gameName + '"', '"' + channel + '"'].join(" ");
	makeRequest(sessionRequest, onResponse);

	function onResponse(response)
	{
		/* Split the response at new line and filter out any empty values. */
		var commands = response.split("\r\n").filter(function (value)
		{
			return value !== "";
		});

		commands.forEach(function (command)
		{
			/* Split the command at every space. */
			var splittedCommand = command.split(/(?: ([^"\s]+|(?:"(?:[^"\\]|\\.)*")))/g).filter(function (value)
			{
				return value !== "";
			}).map(function (value)
			{
				/* Strip off the UNESCAPED inverted commas at the beginning and at the end of the string. */
				return value.toString().replace(/(^""$)|(^")|(((?:[^\\]))"$)/g, '$4');
			});

			var commandType = splittedCommand[0].replace("d=", "");
			responses[commandType] = splittedCommand;

			switch (commandType)
			{
				case LoginRequest.COMMAND_SESSION:
					sessionId = splittedCommand[1];
					if (!loginRequestSent)
					{
						loginRequestSent = true;
						var loginRequest = LoginRequest.COMMAND_LOGIN + " " + authenticationString;
						makeRequest(loginRequest, onResponse);
					}
					break;
				case LoginRequest.COMMAND_LOGIN:
					done = true;
					break;
			}
		});

		if (done)
		{
			parseLoginResponse();
		}

	}

	function parseLoginResponse()
	{
		var custId = responses[LoginRequest.COMMAND_LOGIN][1];
		if (custId > 0)
		{
			var config = {};
			config.sessionId = sessionId;
			config.cId = custId;
			config.currency = responses[LoginRequest.COMMAND_LOGIN][2];

			if (responses[LoginRequest.COMMAND_LOGIN][3] && responses[LoginRequest.COMMAND_LOGIN][3] !== '""')
			{
				config.aamsSessionString = responses[LoginRequest.COMMAND_LOGIN][3].trim().replace(/"/g, "");
			}

			if (responses[LoginRequest.COMMAND_LOGIN][4] !== undefined)
			{
				config.jurisdiction = responses[LoginRequest.COMMAND_LOGIN][4].trim().replace(/"/g, "");
			}
			if (responses[LoginRequest.COMMAND_LOGIN][5] !== undefined)
			{
				config.externalId = responses[LoginRequest.COMMAND_LOGIN][5].trim().replace(/"/g, "");
			}
			if (responses[LoginRequest.COMMAND_LOGIN][6] !== undefined) {
				config.identityToken = responses[LoginRequest.COMMAND_LOGIN][6].trim().replace(/"/g, "");
			}

			if (responses[LoginRequest.COMMAND_SERVERTIME] !== undefined)
			{
				config.serverTime = responses[LoginRequest.COMMAND_SERVERTIME][1].replace(/"/g, "").trim();
				config.serverTimeDelta = new Date(config.serverTime) - Date.now();
			}

			onLoginCallback(config, packetIndex);
		}
		else
		{
			if (!checkIfRelaunchUrl(responses[LoginRequest.COMMAND_LOGIN][2]))
			{

				/* LoginRequest example: d = 101 0 "Unable to login, product not supported".Slice to remove d = 101 and 0. */
				var message = responses[LoginRequest.COMMAND_LOGIN].slice(2).join(" ").replace(/"/g, "").trim();
				/* If the error is a identifier and matches any of the translated errors, use that. */

				var pngLocalizedErrors = localizedFFErrors;
				if (pngLocalizedErrors.hasOwnProperty(message))
				{
					message = pngLocalizedErrors[message];
				}
				onErrorCallback(message, "CONNECTION_ERROR");
			}
			else
			{
				return window.location = responses[LoginRequest.COMMAND_LOGIN][2];
			}

		}
	}

	function checkIfRelaunchUrl(message)
	{
		var regex = new RegExp("(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@@?^=%&:/~+#-]*[\w@@?^=%&/~+#-])?");
		if (regex.test(message))
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	function makeRequest(message, callback)
	{
		var xhr = new XMLHttpRequest();
		xhr.open("POST", serverUrl);
		xhr.setRequestHeader("Content-type", "text/plain");
		xhr.onerror = onXhrError;
		xhr.onabort = onXhrAbort;
		xhr.ontimeout = onXhrTimeout;
		if (callback)
		{
			xhr.onreadystatechange = function ()
			{
				if (xhr.status === 200 && xhr.readyState === 4)
				{
					callback(xhr.responseText);
				}
			}
		}

		if (packetIndex < LoginRequest.PACKET_INDEX_LIMIT)
		{
			xhr.send("d=" + packetIndex++ + "\r\n" + sessionId + "\r\n" + message + "\r\n");
		}
	}


	function onXhrError()
	{
		onErrorCallback(PNGLocalizationManager.get("COREWEB.LOGIN.ERRORMESSAGE", "An error occured while connecting to the server"));
	}
	function onXhrAbort()
	{
		onErrorCallback(PNGLocalizationManager.get("COREWEB.LOGIN.ABORTMESSAGE", "The connection to the server was aborted"));
	}
	function onXhrTimeout()
	{
		onErrorCallback(PNGLocalizationManager.get("COREWEB.LOGIN.TIMEOUTMESSAGE", "Connection timeout"));
	}
}
LoginRequest.COMMAND_SESSION = "103";
LoginRequest.COMMAND_LOGIN = "101";
LoginRequest.COMMAND_SERVERTIME = "127";
/* Safety hack to prevent the login from sending requests forever */
LoginRequest.PACKET_INDEX_LIMIT = 3;;


PlatformManager = {
	init: function (coreWebUrl, embedMode, channel)
	{
		this.coreWebUrl = coreWebUrl;
		this.embedMode = embedMode;
		this.channel = channel;
	},
	devices: {
		iPhone4: { type: "iPhone", version: "4" },
		iPhone5: { type: "iPhone", version: "5" },
		iPhone6: { type: "iPhone", version: "6" },
		iPhone6p: { type: "iPhone", version: "6+" }
	},
	deviceTypes: {
		mobile: "mobile",
		tablet: "tablet"
	},
	debug: false,
	errwebglrenderingcontextErrorCode: "errwebglrenderingcontext",
	errstencilErrorCode: "errstencil",
	checkStencilBuffer: function ()
	{
		var contextOptions = { stencil: true };
		try
		{
			if (!window.WebGLRenderingContext)
			{
				return this.errwebglrenderingcontextErrorCode;
			}

			var canvas = document.createElement('canvas'),
				gl = canvas.getContext('webgl', contextOptions) || canvas.getContext('experimental-webgl', contextOptions);

			return !!(gl && gl.getContextAttributes().stencil) ? "" : this.errstencilErrorCode;
		}
		catch (e)
		{
			return this.errstencilErrorCode;
		}
	},
	checkHardwareAcceleration: function ()
	{
		var props = PlatformManager.collectWebGlInfo();
		if (props.majorPerformanceCaveat === "Yes")
		{
			return "errhardwareaccelerationdisabled";
		}
	},
	isHighResolutionDisplay: function ()
	{
		if (window.matchMedia)
		{
			var mq = window.matchMedia("only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen  and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)");
			return (mq && mq.matches || (window.devicePixelRatio > 1));
		}
		else
		{
			return window.devicePixelRatio > 1;
		}
	},
	checkFeatureSupport: function (config)
	{
		var requiredFeatures = {};
		requiredFeatures["enforcehardwareacceleration"] = this.checkHardwareAcceleration;
		requiredFeatures["stencilbuffer"] = this.checkStencilBuffer;

		var configuredRequiredFeatures = config.requiredPlatformFeatureSupport;

		if (configuredRequiredFeatures === "")
			return false;

		configuredRequiredFeatures = configuredRequiredFeatures.split(",");
		for (var i = 0; i < configuredRequiredFeatures.length; i++)
		{
			var feature = configuredRequiredFeatures[i].toLowerCase();
			if (typeof requiredFeatures[feature] === "function")
			{
				var value = requiredFeatures[feature]();
				if (value)
					return value;
			}
		}
		return "";
	},
	getIOSVersion: function ()
	{
		return Number((navigator.userAgent.match(/\b[1-9]?[0-9]+_[0-9]+(?:_[0-9]+)?\b/) || [''])[0].split('_', 1));
	},
	checkIOSSupport: function ()
	{
		if (this.isIOs() && this.getIOSVersion() >= 8)
		{
			return "";
		}
		else
		{
			return "errios";
		}
	},
	checkPlusDevice: function ()
	{
		if (this.checkIOSSupport())
		{
			if (this.getIPhoneVersion() === this.devices.iPhone6p.version)
			{
				return true;
			}
		}
		return false;
	},
	isDevice: function (device)
	{
		switch (device.type)
		{
			case "iPhone":
				return this.isIOs() && this.getIPhoneVersion() === device.version;
			case "iPad":
				return this.isIOs() && (this.isIpad());
			default:
				return false;
		}
	},
	isIpad: function ()
	{
		return /iPad/i.test(navigator.userAgent);
	},
	isIphone: function ()
	{
		return /iPhone/i.test(navigator.userAgent);
	},
	isIOs: function ()
	{
		return /iPhone|iPod|iPad/.test(navigator.userAgent);
	},
	isIE11: function ()
	{
		return /Trident/.test(navigator.userAgent);
	},
	isEdge: function ()
	{
		return /Edge/.test(navigator.userAgent);
	},
	isFirefox: function ()
	{
		return /Firefox/.test(navigator.userAgent);
	},
	isFirefoxWebpSupportedVersion: function ()
	{
		//Webp support detection. For firefox should be active from version 65+ 
		if(this.isFirefox())
		{
			const match = window.navigator.userAgent.match(/Firefox\/([0-9]+)\./);
			const ver = match ? parseInt(match[1],10) : 0;
			if(ver > 64) return true;
		}
		return false
	},
	isUnsupportedEdgeWebp: function ()
	{	
		//heck if the current version of Edge support the Webp version 
		if(isEdge())
		{
			var edge = navigator.userAgent.indexOf('Edge/');
			try 
			{
				var edgeversion = parseInt(navigator.userAgent.substring(edge + 5, navigator.userAgent.indexOf('.', edge)), 10);
				if (edgeversion <= 18)
				{
					return true
				}
			} 
			catch (e) 
			{
				return false;
			}
		}
		return false;
	},
	isOldIpad: function ()
	{
		/* This will return true for first, second gen iPad, and first generation iPad mini */
		var iHeight = Math.max(window.screen.height, window.screen.width);
		var iWidth = Math.min(window.screen.height, window.screen.width);
		if (iWidth === 768 && iHeight === 1024 && window.devicePixelRatio === 1)
		{
			return true;
		}
		return false;
	},
	getIPhoneVersion: function ()
	{
		var iHeight = Math.max(window.screen.height, window.screen.width);
		var iWidth = Math.min(window.screen.height, window.screen.width);
		if (iWidth === 320 && iHeight === 480)
		{
			return "4";
		}
		else if (iWidth === 320 && iHeight === 568)
		{
			return "5";
		}
		else if (iWidth === 375 && iHeight === 667)
		{
			return "6";
		}
		else if (iWidth === 414 && iHeight === 736)
		{
			return "6+";
		}
		return 'none';
	},
	checkAndroidSupport: function ()
	{
		var androidVersion = navigator.userAgent.match(/Android[\/\s](\d+\.?\d?)?/);
		if (androidVersion)
		{
			// extract the android version number from matched "Android x.y". Since they are in groups androidVersion[0] will be "Android" and androidVersion[1] will be "X.Y"
			if ((new Number(androidVersion[1])) >= 4.4 && !(/firefox/gi.test(navigator.userAgent) && !(/opera/gi.test(navigator.userAgent))))
			{
				return "";
			}
			else
			{
				return "errandroid";
			}
		}
		else
		{
			return "errandroid";
		}
	},
	/* Since we are not supporting windows phone in megaton right now just return errwindows always */
	checkWindowsSupport: function ()
	{
		return "errwindows";
	},

	checkDesktopChromeSupport: function ()
	{
		if (!/Android|iPhone|iPod|iPad/.test(navigator.userAgent) && (navigator.userAgent.match("Chrome")))
		{
			return "";
		}
		return "errdesktopchrome";
	},
	checkConfiguredBlockedDevice: function ()
	{
		var configuredList = this.configuration.customDeviceBlockRegex;
		if (!configuredList)
			return false;

		configuredList = configuredList.split(",");
		for (var i = 0; i < configuredList.length; i++)
		{
			var regex = new RegExp(configuredList[i], 'gi');
			if (regex.test(navigator.userAgent))
				return true;
		}
		return false;
	},
	checkSupportedDevice: function (config)
	{
		this.configuration = config;
		if (this.checkConfiguredBlockedDevice() === true)
		{
			return "errconfigblock";
		}

		var featureNotSupported = this.checkFeatureSupport(config);
		if (featureNotSupported)
		{
			return featureNotSupported;
		}

		if (navigator.userAgent.match("Windows Phone"))
		{
			return this.checkWindowsSupport();
		}
		else if (navigator.userAgent.match("Android"))
		{
			return this.checkAndroidSupport();
		}
		else if (navigator.userAgent.match(/iPhone|iPod|iPad/))
		{
			return this.checkIOSSupport();
		}

		return "";
	},
	logDeviceError: function (logText)
	{
		var url = this.coreWebUrl + "casino/LogDeviceError";
		var xhttp = new XMLHttpRequest();
		xhttp.open("POST", url, true);
		xhttp.setRequestHeader("Content-Type", "application/json");
		xhttp.send(JSON.stringify({ errorInfo: logText }));
	},
	checkWebAudioSupport: function ()
	{
		return typeof (window.AudioContext) == 'function' ||
			typeof (window.AudioContext) === 'object' ||
			typeof (window.webkitAudioContext) === "function" ||
			typeof (window.webkitAudioContext) === "object";
	},
	checkSoundSupport: function ()
	{
		if (navigator.userAgent.match("Windows Phone"))
		{
			return this.checkWebAudioSupport();
		}
		if (this.isIpad() && this.isOldIpad())
		{
			return false;
		}
		return true;
	},
	checkVideoSupport: function ()
	{
		if (PlatformManager.isIOs() && PlatformManager.getIOSVersion() < 10)
		{
			return false;
		}
		return true;
	},
	checkWebpActiveSupport : function () {
		let webptest = document.createElement('canvas');
		if (!!(webptest.getContext && webptest.getContext('2d'))) {
			return webptest.toDataURL('image/webp').indexOf('data:image/webp') == 0;
		}
		return false;
	},
	checkJp2ActiveSupport : function () {
		let jp2test = document.createElement('canvas');
		if (!!(jp2test.getContext && jp2test.getContext('2d'))) {
			return jp2test.toDataURL('image/jp2').indexOf('data:image/jp2') == 0;
		}
		return false;
	},
	checkAvifSupport : function (callback ) {
		var avif = new Image();
		avif.src = "data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A=";
		avif.decode().then(() => {callback(true)}).catch(() => {callback(false)});
	},
    checkWebpSupport: function () {
		if(this.checkWebpActiveSupport()){
			return true;
		}
		if(this.isFirefoxWebpSupportedVersion()){
			return true; 
		}
		return false;
	},
    checkJP2Support: function () {
		if(this.checkJp2ActiveSupport()){
			return true;
		}
		return false;
    },
	
	/* If we're running Android Chrome, use the configured value. Default true.*/
	/* If we're running in embedded mode, return false. Wrapper should handle the fullscreen.*/
	checkFullScreenSupport: function (fullscreenConfig)
	{
		if (this.embedMode != "")
		{
			return "False";
		}
		if ((navigator.userAgent.match("Android") && navigator.userAgent.match("Chrome")) !== null)
		{
			return fullscreenConfig;
		}
		return "False";

	},
	checkDisabledSpacebarToSpinSupport: function (config)
	{
		if (this.channel === "desktop" || this.channel === "cabinet")
		{
			return config.disableSpacebarToSpin;
		}
		else
		{
			return "True";
		}
	},
	checkAutoPreventDefault: function ()
	{
		/* return true for both mobile and desktop */
		return "True";
	},
	checkIosChrome: function ()
	{
		if (navigator.userAgent.match("CriOS"))
		{
			return true;
		}
		return false;
	},
	isIosVersionInBlackList: function ()
	{
		return GameLoader.configuration.swipeToHideIosBlacklist.slice(1, -1).split(',').map(Number).includes(PlatformManager.getIOSVersion());
    },
	getDeviceType: function ()
	{
		if (/iPad/.test(navigator.userAgent))
		{
			return this.deviceTypes.tablet;
		}
		else if (/Android/.test(navigator.userAgent))
		{
			if (Math.min(window.screen.width, window.screen.height) > 600)
			{
				return this.deviceTypes.tablet;
			}
			else
			{
				return this.deviceTypes.mobile;
			}
		}
		else
		{
			return this.deviceTypes.mobile;
		}
	},
	/* Swipe to fullscreen UI handling.Displays an overlay that makes the game scrollable. */
	addSwipeToFullscreen: {
		background: undefined,
		arrowDot: undefined,
		hand: undefined,
		sizeInterval: undefined,
		gamewrapper: undefined,
		allowScrollToFullscreen: undefined,
		playerProtectionSweden: undefined,
		playerProtectionUK: undefined,
		isPreventingPinchZoom: true,
		isShowing: true,
		init: function (gameWrapper)
		{
			this.gamewrapper = gameWrapper;
			this.background = document.createElement("div");
			this.background.id = "pngSwipebackground";

			document.getElementById("mask").style.display = "block"; // In some games the div element is inline - block which breaks swipe to fullscreen

			this.background.addEventListener("touchstart", this.onTouchStart);
			this.background.addEventListener("touchmove", this.onTouchMove);

			window.addEventListener("resize", this.onResize);

			this.arrowDot = document.createElement("div");
			this.arrowDot.id = "pngArrowDot";

			this.hand = document.createElement("div");
			this.hand.id = "pngSwipeHand";

			this.background.appendChild(this.arrowDot);
			this.background.appendChild(this.hand);

			document.body.appendChild(this.background);
			if (PlatformManager.checkIosChrome())
			{
				document.body.style.height = "2000px";
			}

			// In iOS 13 they fixed so overflow:hidden on body works (at least on portrait) https://bugs.webkit.org/show_bug.cgi?id=153852. 
			// We use this property on the body and it prevents swipetofullscreen functionality in portrait. 
			// Here we enable scrolling so the user can enter fullscreen and play the game.
			if (PlatformManager.getIOSVersion() >= 13)
			{
				document.body.style.overflowY = "scroll";
			}
			
			//The interval needs to be long enough for the innerHeight to stabalize in IOS Safari.
			this.sizeInterval = setInterval(this.pollSize.bind(this), 1000);
		},
		pollSize: function ()
		{
			//if landscape landscape else portrait *
			var shouldShowModal;
			if (window.innerHeight < window.innerWidth)
			{
				//Specific special handling for Chrome on IOS 
				if (PlatformManager.checkIosChrome())
				{
					var iWidth = Math.min(window.screen.height, window.screen.width);
					shouldShowModal = (iWidth - 20) > window.innerHeight;
				}
				else if (GameLoader.configuration.disableSwipeToFullscreenLandscapeIos === "False")
				{
					var screenHeight = window.outerHeight || document.body.clientHeight;
					shouldShowModal = !(window.innerHeight + window.pageYOffset >= document.body.offsetHeight && screenHeight === window.innerHeight)
				}
				else
				{
					shouldShowModal = false;
				}
				this.show(shouldShowModal);
			}
			else if (GameLoader.configuration.disableSwipeToFullscreenPortraitIos === "False")
			{
				shouldShowModal = document.documentElement.clientHeight >= window.innerHeight;
				this.show(shouldShowModal);
			}
			else
			{
				this.show(false);
			}
			if (typeof document.getElementById("gameWrapper") !== "undefined" && document.getElementById("gameWrapper").clientWidth > window.innerWidth)
			{
				// Is the window zoomed ? Then show swipe animation to allow user zoom out again.This forces the user to zoom out
				this.show(true);
				this.background.removeEventListener("touchmove", this.onTouchMove); // Removes event.preventdefault to enable normal pinch zoom when zoomed in (which can only be done when scrolling and pinching)
				this.background.removeEventListener("touchstart", this.onTouchStart);
				this.isPreventingPinchZoom = false;
			} else if (!this.isPreventingPinchZoom)
			{
				this.background.addEventListener("touchmove", this.onTouchMove); // Adds event.preventdefault to prohibit zoom when zoomed out 
				this.background.addEventListener("touchstart", this.onTouchStart);
				this.isPreventingPinchZoom = true;
			}
			if (GameLoader.configuration.disableSwipeToFullscreenIos === "True" && !PlatformManager.checkIosChrome() || PlatformManager.isIosVersionInBlackList())
			{
				this.show(false);
			}
		},
		show: function (b)
		{
			if (b && GameLoader.loaderComplete)
			{
				this.updateBackgroundPositionForSwipeToHide();
				this.background.classList.add("pngSwipeBgShow");
				this.background.classList.remove("pngFadeOutAnim");
				this.gamewrapper.style.pointerEvents = "none";

				// If this is the show call where it goes from hidden => shown 
				// then scroll to top so the user can complete a long enough scroll motion.
				if (!this.isShowing)
				{
					window.scroll(0, 0);
				}

				this.isShowing = true;  //Bool which gives the state of the fullscreen animation
			}
			else
			{
				this.background.classList.remove("pngSwipeBgShow");
				this.background.classList.add("pngFadeOutAnim");
				this.gamewrapper.style.pointerEvents = "";
				this.isShowing = false;
			}
		},

		/**
		 * If we have playerProtectionSweden we don't want the swipeToFullscreen background to cover it since it should always be clickable.
		 * Previously this was solved by utilizing z-index but it's not applicable anymore since playerProtection is rendered within the grid (which should be below swipeToFullscreen).
		 * To work around this we simply push down the swipeToFullscreen background so it doesn't cover playerprotection.
		 * This only works if playerProtection is the topmost module but this should always the case since it's a requirement for compliance.
		 */
		updateBackgroundPositionForSwipeToHide: function() {

			if (PNGModules.hasExternalModule("playerProtectionSweden"))
			{
				if (!this.playerProtectionSweden)
				{
					this.playerProtectionSweden = document.querySelector(".pps-top-bar-mobile");
				}

				if (this.playerProtectionSweden)
				{
					var ppHeight = this.playerProtectionSweden.clientHeight;
					this.background.style.top = ppHeight ? ppHeight + "px" : 0;
				}
			}
			if (PNGModules.hasExternalModule("playerProtectionUK"))
			{
				if (!this.playerProtectionUK)
				{
					this.playerProtectionUK = document.querySelector(".pps-top-bar-mobile");
				}

				if (this.playerProtectionUK)
				{
					var ppHeight = this.playerProtectionUK.clientHeight;
					this.background.style.top = ppHeight ? ppHeight + "px" : 0;
				}
			}
		},
		onResize: function ()
		{
			// make the mask element big enough to scroll
			var maskElement = document.getElementById("mask");
			if (maskElement)
			{
				if (PlatformManager.isIosVersionInBlackList() && !PlatformManager.checkIosChrome() && window.innerHeight >= window.innerWidth) {
					maskElement.style.removeProperty('height');
					return;
				}
				maskElement.style.height = window.innerHeight * 3 + "px";
			}
			// TODO get rid of magic number
			// 39 seems to be correct for iOS 8-10 but this should be proved
			// user agent string could be used to determine iOS version and use that info
			var narrowAddressHeight = 39;

			var minimalUIState;
			if (window.orientation === 90 || window.orientation === 270 || window.orientation === -90)
			{
				minimalUIState = window.screen.width - window.innerHeight <= narrowAddressHeight;
			}
			else
			{
				minimalUIState = window.screen.height - window.innerHeight <= narrowAddressHeight;
			}
			if (minimalUIState)
			{
				// disables user to exit fullscreen with scrolling
				window.scroll(0, window.innerHeight);
				window.addEventListener("scroll", this.onScroll);
			}
			else
			{
				this.allowScrollToFullscreen = true;
				window.removeEventListener("scroll", this.onScroll);
			}
		},
		onScroll: function ()
		{
			var doc = document.documentElement;
			var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
			if (top !== window.innerHeight && this.allowScrollToFullscreen === true)
			{
				window.scroll(0, window.innerHeight);
				this.allowScrollToFullscreen = true;
			}
			else
			{
				this.allowScrollToFullscreen = false;
				window.removeEventListener("scroll", this.onScroll);
			}
		},
		onTouchStart: function (e)
		{
			if (e.touches.length > 1)
			{
				e.preventDefault();
			}
		},
		onTouchMove: function (e)
		{
			if (e.touches.length > 1)
			{
				e.preventDefault();
			}
		}
	},
	isStandalone: function ()
	{
		if (this.embedMode === "iframe")
			return true;

		return window.navigator.standalone;
	},
	collectWebGlInfo: function ()
	{

		try
		{

			var glProperties = {
				platform: navigator.platform,
				userAgent: navigator.userAgent
			};

			var contextName;
			var testCanvas = document.createElement("canvas");
			var wgl;

			function tryGetContext(canvasTag, name, requireStencil)
			{
				wgl = canvasTag.getContext(name, { stencil: requireStencil });
				contextName = name;
				return !!wgl;
			}

			/* Pick the first one that is working.First with and then without stencil. */
			if (!tryGetContext(testCanvas, "webgl", true))
			{
				if (!tryGetContext(testCanvas, "experimental-webgl", true))
				{
					if (!tryGetContext(testCanvas, "webgl", false))
					{
						tryGetContext(testCanvas, "experimental-webgl", false);
					}
				}
			}

			testCanvas.remove();

			if (!wgl)
			{
				return "Browser supports WebGL but initialization failed";
			}

			function describeRange(value)
			{
				return "[" + value[0] + ", " + value[1] + "]";
			}

			function getMaxAnisotropy()
			{
				var e = wgl.getExtension("EXT_texture_filter_anisotropic")
					|| wgl.getExtension("WEBKIT_EXT_texture_filter_anisotropic")
					|| wgl.getExtension("MOZ_EXT_texture_filter_anisotropic");

				if (e)
				{
					var max = wgl.getParameter(e.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
					/* Canary bug */
					if (max === 0)
					{
						max = 2;
					}
					return max;
				}
				return "n/a";
			}

			function formatPower(exponent, verbose)
			{
				if (verbose)
				{
					return "" + Math.pow(2, exponent);
				} else
				{
					return "2^" + exponent;
				}
			}

			function getPrecisionDescription(precision, verbose)
			{
				var verbosePart = verbose ? " bit mantissa" : "";
				return "[-" +
					formatPower(precision.rangeMin, verbose) +
					", " +
					formatPower(precision.rangeMax, verbose) +
					"] (" +
					precision.precision +
					verbosePart +
					")";
			}

			function getBestFloatPrecision(shaderType)
			{
				var high = wgl.getShaderPrecisionFormat(shaderType, wgl.HIGH_FLOAT);
				var medium = wgl.getShaderPrecisionFormat(shaderType, wgl.MEDIUM_FLOAT);
				var low = wgl.getShaderPrecisionFormat(shaderType, wgl.LOW_FLOAT);

				var best = high;
				if (high.precision === 0)
				{
					best = medium;
				}

				return "High: " + getPrecisionDescription(high, true) + " Medium: " + getPrecisionDescription(medium, true) + " Low: " + getPrecisionDescription(low, true) +
					getPrecisionDescription(best, false);
			}

			function getFloatIntPrecision(gl)
			{
				var high = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT);
				var s = (high.precision !== 0) ? "highp/" : "mediump/";

				high = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_INT);
				s += (high.rangeMax !== 0) ? "highp" : "lowp";

				return s;
			}

			function isPowerOfTwo(n)
			{
				return (n !== 0) && ((n & (n - 1)) === 0);
			}

			function getAngle(gl)
			{
				var lineWidthRange = describeRange(gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE));

				/* Heuristic: ANGLE is only on Windows, not in IE, and not in Edge, and does not implement line width greater than one. */
				var angle = ((navigator.platform === "Win32") || (navigator.platform === "Win64")) &&
					(gl.getParameter(gl.RENDERER) !== "Internet Explorer") &&
					(gl.getParameter(gl.RENDERER) !== "Microsoft Edge") &&
					(lineWidthRange === describeRange([1, 1]));

				if (angle)
				{
					/* Heuristic: D3D11 backend does not appear to reserve uniforms like the D3D9 backend, e.g.,
						D3D11 may have 1024 uniforms per stage, but D3D9 has 254 and 221.
		
					We could also test for WEBGL_draw_buffers, but many systems do not have it yet
					due to driver bugs, etc. */
					if (isPowerOfTwo(gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS)) && isPowerOfTwo(gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS)))
					{
						return "Yes, D3D11";
					} else
					{
						return "Yes, D3D9";
					}
				}

				return "No";
			}

			function getMajorPerformanceCaveat()
			{
				/* Does context creation fail to do a major performance caveat ? */
				var canvas = document.createElement("canvas");
				var gl = canvas.getContext(contextName, { failIfMajorPerformanceCaveat: true });
				canvas.remove();

				if (!gl)
				{
					/* The original context creation passed.This did not. */
					return "Yes";
				}

				if (typeof gl.getContextAttributes().failIfMajorPerformanceCaveat === "undefined")
				{
					/* If getContextAttributes() doesn"t include the failIfMajorPerformanceCaveat
					property, assume the browser doesn"t implement it yet. */
					return "Not implemented";
				}

				return "No";
			}

			function getMaxColorBuffers(gl)
			{
				var maxColorBuffers = 1;
				var ext = gl.getExtension("WEBGL_draw_buffers");
				if (ext != null)
					maxColorBuffers = gl.getParameter(ext.MAX_DRAW_BUFFERS_WEBGL);

				return maxColorBuffers;
			}

			function getUnmaskedInfo(gl)
			{
				var unMaskedInfo = {
					renderer: "",
					vendor: ""
				};

				var dbgRenderInfo = gl.getExtension("WEBGL_debug_renderer_info");
				if (dbgRenderInfo != null)
				{
					unMaskedInfo.renderer = gl.getParameter(dbgRenderInfo.UNMASKED_RENDERER_WEBGL);
					unMaskedInfo.vendor = gl.getParameter(dbgRenderInfo.UNMASKED_VENDOR_WEBGL);
				}

				return unMaskedInfo;
			}

			glProperties.contextName = contextName;
			glProperties.glVersion = wgl.getParameter(wgl.VERSION);
			glProperties.shadingLanguageVersion = wgl.getParameter(wgl.SHADING_LANGUAGE_VERSION);
			glProperties.vendor = wgl.getParameter(wgl.VENDOR);
			glProperties.renderer = wgl.getParameter(wgl.RENDERER);
			glProperties.unMaskedVendor = getUnmaskedInfo(wgl).vendor;
			glProperties.unMaskedRenderer = getUnmaskedInfo(wgl).renderer;
			glProperties.antialias = wgl.getContextAttributes().antialias ? "Available" : "Not available";
			glProperties.angle = getAngle(wgl);
			glProperties.majorPerformanceCaveat = getMajorPerformanceCaveat();
			glProperties.maxColorBuffers = getMaxColorBuffers(wgl);
			glProperties.redBits = wgl.getParameter(wgl.RED_BITS);
			glProperties.greenBits = wgl.getParameter(wgl.GREEN_BITS);
			glProperties.blueBits = wgl.getParameter(wgl.BLUE_BITS);
			glProperties.alphaBits = wgl.getParameter(wgl.ALPHA_BITS);
			glProperties.depthBits = wgl.getParameter(wgl.DEPTH_BITS);
			glProperties.stencilBits = wgl.getParameter(wgl.STENCIL_BITS);
			glProperties.maxRenderBufferSize = wgl.getParameter(wgl.MAX_RENDERBUFFER_SIZE);
			glProperties.maxCombinedTextureImageUnits = wgl.getParameter(wgl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
			glProperties.maxCubeMapTextureSize = wgl.getParameter(wgl.MAX_CUBE_MAP_TEXTURE_SIZE);
			glProperties.maxFragmentUniformVectors = wgl.getParameter(wgl.MAX_FRAGMENT_UNIFORM_VECTORS);
			glProperties.maxTextureImageUnits = wgl.getParameter(wgl.MAX_TEXTURE_IMAGE_UNITS);
			glProperties.maxTextureSize = wgl.getParameter(wgl.MAX_TEXTURE_SIZE);
			glProperties.maxVaryingVectors = wgl.getParameter(wgl.MAX_VARYING_VECTORS);
			glProperties.maxVertexAttributes = wgl.getParameter(wgl.MAX_VERTEX_ATTRIBS);
			glProperties.maxVertexTextureImageUnits = wgl.getParameter(wgl.MAX_VERTEX_TEXTURE_IMAGE_UNITS);
			glProperties.maxVertexUniformVectors = wgl.getParameter(wgl.MAX_VERTEX_UNIFORM_VECTORS);
			glProperties.aliasedLineWidthRange = describeRange(wgl.getParameter(wgl.ALIASED_LINE_WIDTH_RANGE));
			glProperties.aliasedPointSizeRange = describeRange(wgl.getParameter(wgl.ALIASED_POINT_SIZE_RANGE));
			glProperties.maxViewportDimensions = describeRange(wgl.getParameter(wgl.MAX_VIEWPORT_DIMS));
			glProperties.maxAnisotropy = getMaxAnisotropy();
			glProperties.vertexShaderBestPrecision = getBestFloatPrecision(wgl.VERTEX_SHADER);
			glProperties.fragmentShaderBestPrecision = getBestFloatPrecision(wgl.FRAGMENT_SHADER);
			glProperties.fragmentShaderFloatIntPrecision = getFloatIntPrecision(wgl);
			glProperties.extensions = wgl.getSupportedExtensions();

			return glProperties;
		}
		catch (e)
		{
			return { unexpectedError: e };
		}
	},
	log: function (msg)
	{
		if (this.configuration.debug)
			console.log(msg);
	}
};
/**
 * Creates a modal window with some information. This window contains one
 * button by default which redirects the user to the previous page.
 * You can customize the following properties with a config object:
 * msg - the only required configuration
 * title
 * cancelCB - Callback for default button.
 * confirmCB - Callback for additional button.
 * cancelText - Text for default button.
 * confirmText - Text for additional button.
 * lobbyUrl - Path to lobby
 * channel - mobile or desktop
 * @param {any} config
 */
function LauncherMessage(config)
{
	this.msgBox = document.createElement("div");
	this.msgBox.id = "pngLauncherMessage";

	this.title = document.createElement("h2");
	this.title.textContent = config.title || PNGLocalizationManager.get("COREWEB.LAUNCHERMESSAGE.TITLE", "Something went wrong");
	this.msgBox.appendChild(this.title);

	this.msgText = document.createElement("p");
	this.msgText.id = "launcherMessageText";
	this.msgText.textContent = config.msg;
	this.msgBox.appendChild(this.msgText);

	this.channel = config.channel;

	this.lobbyUrl = config.lobbyUrl ? decodeURIComponent(config.lobbyUrl) : "";
	

	this.cancelbutton = document.createElement("button");

	if (this.channel === "desktop" || this.channel === "mini" || this.channel === "cabinet")
	{
		this.cancelbutton.innerHTML = config.cancelText || PNGLocalizationManager.get("COREWEB.LAUNCHERMESSAGE.OK", "OK");
	} else
	{
		this.cancelbutton.innerHTML = config.cancelText || PNGLocalizationManager.get("COREWEB.LAUNCHERMESSAGE.PREVIOUSPAGE", "Previous Page");
	}

	this.cancelbutton.id = "cancelButton";
	this.cancelbutton.className = "pngMsgBoxButton";
	this.cancelbutton.onclick = config.cancelCB || this.onExit.bind(this);
	this.msgBox.appendChild(this.cancelbutton);

	if (config.confirmCB)
	{
		this.confirmbutton = document.createElement("button");
		this.confirmbutton.innerHTML = config.confirmText;
		this.confirmbutton.className = "pngMsgBoxButton";
		this.confirmbutton.id = "confirmbutton";
		this.confirmbutton.onclick = config.confirmCB;
		this.msgBox.appendChild(this.confirmbutton);
	}

	if (typeof PngPreloader !== "undefined")
	{
		PngPreloader.onLauncherMessage(true);
	}

	document.body.insertAdjacentElement("afterbegin", this.msgBox);
}

LauncherMessage.prototype.onExit = function ()
{
	if (this.channel === "mobile")
	{
		var hreflobbyurl = this.lobbyUrl;
		if (hreflobbyurl !== "")
		{
			location.href = hreflobbyurl;
		} else
		{
			history.back();
		}
	}
	else
	{
		this.msgBox.parentElement.removeChild(this.msgBox);
	}
};

LauncherMessage.prototype.remove = function ()
{
	if (typeof PngPreloader !== "undefined")
	{
		PngPreloader.onLauncherMessage(false);
	}

	if (this.msgBox && this.msgBox.parentElement)
	{
		this.msgBox.parentElement.removeChild(this.msgBox);
	}
};;

/**
* Engage - API for communicating with a PNG Flash game.
*
* Accepts external requests via Engage.request({req:REQUESTSTRING, data:OPTIONAL});
* Accepted requests:
* "gameDisable"		- Disable all user interaction in game and stops Autoplay. Replies with "gameDisabled"
* "gameEnable"		- Enables the game. Replies with "gameEnabled"
* "gameEnd"			- Sends logout command to game. Replies with "logout" and redirects if redirect url is set.
* "logout"          - Sends logout command to game. Replies with "logout".
* "refreshBalance"	- Request GAME client to refresh balance. Replies with "balanceUpdate" along with raw balance, formatted balance and currency.
* "inGameMessage"   - Request the game to show a modal window. { req: "inGameMessage", data: {id:"", title: "", message: "", okBtn: "", exitBtn: "", actionBtn: ""} }
* "stopAutoplay"    - Stops Autoplay
* "soundOn"         - Enables in-game sound
* "soundOff"        - Disables in-game sound
* "hideUI"          - Hides game UI elements
*
*/
var Engage = {
	debug: false, //set to false in production
	state: -1, //Internal state
	gameStateVariables: { // object for storing game state variables we might need to check for some messages
		freeGameStarted: false
	},
	mainLoopInterval: undefined,
	redirectUrl: undefined,
	modules: [],
	initialized: false,
	inGameMessageQueue: [],
	requestQueue: [],
	gameHostInterface: undefined,
	gameCalls: undefined,
	iframeCom: undefined,
	iframeOverlayUrl: undefined,
	cleanup: function ()
	{
		this.dispatchEvent({ type: "cleanup" });
		if (this.initialized)
		{
			this.gameCalls = undefined;
			clearInterval(this.mainLoopInterval);
			this.mainLoopInterval = undefined;
			this.requestQueue.length = 0;
			this.inGameMessageQueue.length = 0;
			this.initialized = false;
			this.removeAllEventListeners();
		}
	},
	init: function (gameExtcom, iframeUrl)
	{
		this.gameHostInterface = gameExtcom || extcom;
		this.registerGameListeners();

		if (iframeUrl && iframeUrl !== "")
		{
			this.iframeOverlayUrl = iframeUrl;
			this.iframeCom.init(this.request.bind(this));
		}


		this.initialized = true;
		this.mainLoopInterval = setInterval(this.mainLoop.bind(this), 100);
		this.state = Engage.CANSHOWMESSAGE;
		this.dispatchEvent({ type: "initialized" });
		return this.initialized;
	},
	registerGameListeners: function ()
	{
		if (typeof this.gameHostInterface === "object" || typeof this.gameHostInterface === "function")
		{
			this.gameCalls = this.gameHostInterface.gameCalls();

			for (var i = 0; i < this.internalGenericGameEvents.length; i++)
			{
				//this.gameHostInterface.addEventListener(this.internalGenericGameEvents[i], this.parseGameMessage.bind(this))
				this.log(this.gameHostInterface.addEventListener(this.internalGenericGameEvents[i], this.parseGameMessage.bind(this)));
			}
			for (var i = 0; i < this.internalVideoSlotGameEvents.length; i++)
			{
				this.log(this.gameHostInterface.addEventListener(this.internalVideoSlotGameEvents[i], this.parseGameMessage.bind(this)));
			}
		}
		else
		{
			this.log("extcom object not defined. External communictation in game not accessible");
		}
	},
	//Internal event types in the client
	internalGenericGameEvents:
		[
			"LOADER",
			"GAME_READY",
			"GAME_IDLE",
			"LOGOUT",
			"LOGINANSWER",
			"LOGOUTANSWER",
			"round_start",
			"round_end",
			"round_balance",
			"round_win",
			"SESSIONRESPONSE",
			//"SETTINGS",
			"UIHELP",
			"UIPLAYFORREAL",
			"PAYTABLE_CHANGED",
			"UIHISTORY",
			"CURRENCY",
			"PLAYFORREAL",
			"RECONNECTEND",
			"MESSAGE",
			//"resize",
			"BET",
			"BALANCE",
			"AVAILABLE_COINS",
			"SELECTED_COIN",
			"SELECTED_COIN_VALUE",
			//"SELECTED_COIN_VALUE_UNFORMATTED",
			"BET_UPDATE",
			"AVAILABLE_COIN_VALUES",
			"WIN",
			"WIN_SHOW",
			"WIN_PAYLINE",
			"ACTIVE_CARDS",
			"GAME_SCREEN_ENABLED",
			"GAME_SCREEN_DISABLED",
			"RELOAD_GAME",
			"EXTERNAL_MESSAGE_OK",
			"EXTERNAL_MESSAGE_EXIT",
			"EXTERNAL_MESSAGE_ACTION",
			"SERVERMESSAGE",
			"REALITYCHECKEVENT",
			"SPLASH_SHOW",
			"SPLASH_HIDE",
			"SOUND_ON",
			"SOUND_OFF",
			"FASTPLAY_ON",
			"FASTPLAY_OFF",
			"LEFTHANDMODE_ON",
			"LEFTHANDMODE_OFF",
			"MODAL_SHOWING",
			"MODAL_HIDING",
			"JP_POPUP_SHOWING",
			"JP_POPUP_HIDING",
			"LOADER_CRITICAL_ERROR",
			"JACKPOT_WIN",
			"SESSIONTOKEN",
			"HIDE_UI",
		],
	internalVideoSlotGameEvents:
		[
			"UISPIN",
			"AUTOPLAY_STARTED",
			"AUTOPLAY_TRIGGER",
			"AUTOPLAY_ENDED",
			"AUTOPLAYS_LEFT",
			"FREESPIN_START",
			"FREESPIN_END",
			"BONUS_STARTED",
			"BONUS_ENDED",
			"SELECTED_LINES",
			"AVAILABLE_LINES",
			"GAMBLE_ANSWER",
			"UIGAMBLE",
			"UICOLLECT",
			"FREEGAMEMESSAGE",
			"SPIN_START",
			"REEL_ALL_STOPPED",
			"GAMBLE_AVAILABLE",
			"UIEXTRABALL_AVAILABLE",
			"extraball_on_select_event",
			"FREEGAME_START",
			"FREEGAME_END",
			"FREEGAME_LOGOUT",
			"WIN_START"
		],

	allowedRequests: [
		"gameDisable",
		"gameEnable",
		"gameEnd",
		"refreshBalance",
		"externalBalance",
		"inGameMessage",
		"inGameMessageOk",
		"inGameMessageAction",
		"inGameMessageExit",
		"modalOk",
		"modalCancel",
		"modalExit",
		"stopAutoplay",
		"logout",
		"soundOn",
		"soundOff",
		"showHelp",
		"openGameHelp",

		"setCoins",
		"setCoinValue",
		"setLines",
		"incCoins",
		"decCoins",
		"incCoinValue",
		"decCoinValue",
		"incLines",
		"decLines",

		"togglePaytable",
		"betMax",
		"spin",
		"gamble",
		"collect",
		"autoPlay",
		"setFastplay",
		"getFastplay",
		"setLeftHandMode",
		"getLeftHandMode",
		"pause",

		"getBet",
		"getBalance",
		"getWin",
		"getAvailableCoins",
		"getAvailableCoinValues",
		"getSelectedLines",
		"getAvailableLines",
		"getSelectedCoin",
		"getSelectedCoinValue",
		"getActiveCards",
		"getAutoplaysLeft",
		"closeMysteryJackpotPopup",
		"buyExtraBall",
		"externalMessageHandled",
		"openSettings",
		"hideUI",
	],
	//Validate request
	allowedRequest: function (req)
	{
		var i, allowed;
		for (i = 0; i < this.allowedRequests.length; i++)
		{
			if (this.allowedRequests[i] === req)
				return true;
		}
		return false;
	},
	//Handle external requests
	request: function (e)
	{
		if (this.initialized === false)
		{
			this.log("Engage is not yet initialized! Waiting for game to be ready");
			return;
		}
		//Catch addEvent requests from Iframe implementation
		if (this.iframeCom && e.addEvent)
		{
			this.log("Engage: Processing Received addEvent: " + e.addEvent);

			//     if (this.listeners[e.addEvent] != e.addEvent) {
			//          this.listeners[e.addEvent] = e.addEvent;
			//   }

			this.addEventListener(e.addEvent);
		}
		else if (e.req)
		{
			this.log("Engage: Processing external request", "%c" + e.req, 'background: #222; color: yellow');
			if (this.allowedRequest(e.req))
			{

				//Queue the request. Processed in mainloop
				this.requestQueue.push(e.req);

				switch (e.req)
				{
					case "gameDisable":
						//Request is processed in mainloop
						//Stop autoplay if any
						this.stopAutoPlay();
						break;
					case "gameEnable":
						//Request is processed in mainloop
						break;
					case "gameEnd":
						//store redirect url and wait for game to logout
						if (e.data)
						{
							this.redirectUrl = e.data.redirectUrl;
						}
						this.logout();
						break;
					case "logout":
						//Simple logout, no redirect
						this.logout();
						break;
					case "refreshBalance":
						this.refreshInGameBalance();
						break;
					case "externalBalance":
						this.externalInGameBalance(e.balance);
						break;
					case "inGameMessage":
						//Postpone message until game is idle
						this.inGameMessageQueue.push(e);
						break;
					case "inGameMessageOk":
						this.inGameMessageOk(e.data.id);
						break;
					case "inGameMessageAction":
						this.inGameMessageAction(e.data.id);
						break;
					case "inGameMessageExit":
						this.inGameMessageExit(e.data.id);
						break;
					case "modalOk":
						this.modalOk();
						break;
					case "modalCancel":
						this.modalCancel();
						break;
					case "modalExit":
						this.modalExit();
						break;
					case "stopAutoplay":
						//stop autoplay
						this.stopAutoPlay();
						break;
					case "soundOn":
						//stop autoplay
						this.soundOn();
						break;
					case "soundOff":
						//stop autoplay
						this.soundOff();
						break;
					case "setCoins":
						this.setCoins(e);
						break;
					case "setCoinValue":
						this.setCoinValue(e);
						break;
					case "setLines":
						this.setLines(e);
						break;
					case "togglePaytable":
						this.togglePaytable(e.data.pageNum);
						break;
					case "betMax":
						this.betMax();
						break;
					case "spin":
						this.spin();
						break;
					case "gamble":
						this.gamble(e);
						break;
					case "collect":
						this.collect();
						break;
					case "pause":
						this.pause(e.data.pause);
						break;
					case "autoPlay":
						this.autoPlay(e);
						break;
					case "getBet":
						this.getBet();
						break;
					case "getBalance":
						this.getBalance();
						break;
					case "getWin":
						this.getWin();
						break;
					case "getAvailableCoins":
						this.getAvailableCoins();
						break;
					case "getAvailableCoinValues":
						this.getAvailableCoinValues();
						break;
					case "getSelectedLines":
						this.getSelectedLines();
						break;
					case "getAvailableLines":
						this.getAvailableLines();
						break;
					case "getActiveCards":
						this.getActiveCards();
						break;
					case "getSelectedCoin":
						this.getSelectedCoin();
						break;
					case "getSelectedCoinValue":
						this.getSelectedCoinValue();
						break;
					case "getAutoplaysLeft":
						this.getAutoplaysLeft();
						break;
					case "incCoins":
						this.incCoins();
						break;
					case "decCoins":
						this.decCoins();
						break;
					case "incCoinValue":
						this.incCoinValue();
						break;
					case "decCoinValue":
						this.decCoinValue();
						break;
					case "incLines":
						this.incLines();
						break;
					case "decLines":
						this.decLines();
						break;
					case "showHelp":
					case "openGameHelp":
						this.showHelp();
						break;
					case "setFastplay":
						this.setFastPlay(e);
						break;
					case "getFastplay":
						this.getFastPlay();
						break;
					case "setLeftHandMode":
						this.setLeftHandMode(e.data.value);
						break;
					case "getLeftHandMode":
						this.getLeftHandMode();
						break;
					case "closeMysteryJackpotPopup":
						this.closeMysteryJackpotPopup();
						break;
					case "buyExtraBall":
						this.buyExtraBall();
						break;
					case "externalMessageHandled":
						this.externalMessageHandled();
						break;
					case "openSettings":
						this.openSettings();
						break;
					case "hideUI":
						this.hideUI();
						break;
				}
			}
			else
			{
				this.log("ERROR! " + e.req + " is not a supported request");
				this.dispatchEvent({ type: "error", msg: e.req + " is not a supported request" });
			}
		}

		//Pass request down to all modules
		var i, moduleEvent;
		for (i = 0; i < this.modules.length; i++)
		{
			moduleEvent = this.modules[i].request(e)
			if (moduleEvent != undefined)
			{
				this.dispatchEvent(moduleEvent);
			}
		}
	},
	mainLoop: function ()
	{
		//Process requests in order
		switch (this.requestQueue[0])
		{

			case "gameDisable":
				if (this.state === Engage.CANSHOWMESSAGE)
				{
					//Make sure autoplay isn't restarted
					this.stopAutoPlay();
					this.disableGame();
					if (this.iframeCom.initialized)
					{
						this.iframeCom.setIframeInteractive(true);
						this.iframeCom.frame.style.display = "block";

						this.iframeCom.dispatchMessage({
							type: "gameDisabled"
						});
					}
					this.requestQueue.shift();
				}
				break;
			case "gameEnable":
				this.enableGame();

				if (this.iframeCom.initialized)
				{
					this.iframeCom.setIframeInteractive(false);
					this.iframeCom.frame.style.display = "none";

					this.iframeCom.dispatchMessage({
						type: "gameEnabled"
					});
					window.focus();
				}
				this.requestQueue.shift();

				break
			case "inGameMessage":
				if (this.state === Engage.CANSHOWMESSAGE)
				{
					// The autoplayhandler in the client handles autoplay states for ingamemessages. This is in order to be able to pause autoplay on reality check message
					// this.stopAutoPlay();
					var message = this.inGameMessageQueue[0];
					this.inGameMessage(message.data);
					this.dispatchEvent({ type: "inGameMessageShowing" });

					this.inGameMessageQueue.shift();
					this.requestQueue.shift();
				}
				break;
			default:
				//No delayed action for request? discard it.
				this.requestQueue.shift();
				break;
		}
	},
	//parseGameMessage can accept multiple paramenters. See arguments array if expecting more data.
	parseGameMessage: function (event)
	{
		if (!this.initialized)
			this.init();

		switch (event.type)
		{
			case "GAME_SCREEN_ENABLED":
				this.processGameMessage("gameEnabled");
				break;
			case "GAME_SCREEN_DISABLED":
				this.processGameMessage("gameDisabled");
				break;
			case "round_start":
				if (event._inGame)
				{
					this.processGameMessage("running");
					this.processGameMessage("roundStarted");
				}
				break;
			case "round_end":
				this.processGameMessage("gameIdle");
				this.processGameMessage("roundEnded");
				break;
			case "GAME_READY":
				//No autoplay in game? Disable feature.
				if (!this.gameCalls.STOP_AUTOPLAY)
				{
					this.stopAutoPlay = function () { };
				}
				this.processGameMessage("gameReady");
				//this.processGameMessage("gameIdle");
				break;
			case "GAME_IDLE":
				this.processGameMessage("gameIdle");
				break;
			case "LOGOUTANSWER":
			case "LOGOUT":
				this.processGameMessage("logout");

				switch (event.reason)
				{
					case "Exit":
					case "Script":
					case "ServerMessage":
						this.processGameMessage("backToLobby");
						break;
				}

				break;
			case "round_balance":
				this.processGameMessage("balanceUpdate", event);
				break;
			case "SPIN_START":
				this.processGameMessage("spinStarted");
				break;
			case "REEL_ALL_STOPPED":
				this.processGameMessage("spinEnded");
				break;
			case "WIN_PAYLINE":
				this.processGameMessage("paylineWin", event);
				break;
			case "round_win":
				if (event._amount > 0)
				{
					this.processGameMessage("roundWin", event);
				}
				break;
			case "AUTOPLAY_STARTED":
				this.processGameMessage("autoplayStarted", event);
				break;
			case "AUTOPLAY_TRIGGER":
				this.processGameMessage("autoplayNextRound", event);
				break;
			case "AUTOPLAY_ENDED":
				this.processGameMessage("autoplayEnded", event);
				break;
			case "AUTOPLAYS_LEFT":
				if (event._amount >= 0)
				{
					this.processGameMessage("autoplaysLeft", { autoplaysLeft: event._amount });
				}
				break;
			case "FREESPIN_START":
				this.processGameMessage("freespinStarted", event);
				break;
			case "FREESPIN_END":
				this.processGameMessage("freespinEnded", event);
				break;
			case "BONUS_STARTED":
				this.processGameMessage("bonusGameStarted", event);
				break;
			case "BONUS_ENDED":
				this.processGameMessage("bonusGameEnded", event);
				break;
			case "EXTERNAL_MESSAGE_OK":
				this.processGameMessage("externalMessageOk", event);
				break;
			case "EXTERNAL_MESSAGE_EXIT":
				this.processGameMessage("externalMessageExit", event);
				break;
			case "EXTERNAL_MESSAGE_ACTION":
				this.processGameMessage("externalMessageAction", event);
				break;
			case "UIGAMBLE":
				this.processGameMessage("gambleStarted");
				break;
			case "GAMBLE_AVAILABLE":
				this.processGameMessage("gambleAvailable");
				break;
			case "UICOLLECT":
				this.processGameMessage("gambleEnded");
				break;
			case "GAMBLE_ANSWER":
				if (event._winCode > 0)
				{
					this.processGameMessage("gambleWon");
				}
				else
				{
					this.processGameMessage("gambleLost");
					this.processGameMessage("gambleEnded");
				}

				break;
			case "RELOAD_GAME":
				this.processGameMessage("reloadGame");
				break;
			case "SESSIONRESPONSE":
				this.processGameMessage("sessionID", event);
				break;
			case "SETTINGS":
				this.processGameMessage("settings", event);
				break;
			case "SOUND_ON":
				this.processGameMessage("soundOn");
				break;
			case "SOUND_OFF":
				this.processGameMessage("soundOff");
				break;
			case "FASTPLAY_ON":
				this.processGameMessage("fastPlayOn");
				break;
			case "FASTPLAY_OFF":
				this.processGameMessage("fastPlayOff");
				break;
			case "LEFTHANDMODE_ON":
				this.processGameMessage("leftHandModeOn");
				break;
			case "LEFTHANDMODE_OFF":
				this.processGameMessage("leftHandModeOff");
				break;
			case "PAYTABLE_CHANGED":
				this.processGameMessage("paytableChanged", event);
				break;
			case "UIPLAYFORREAL":
			case "PLAYFORREAL":
				this.processGameMessage("playForReal");
				break;
			case "LOADER":
				//TODO: handle loader events
				if (event._id === 6)
				{
					this.processGameMessage("gameError",
						{
							_titleText: "Loader error",
							_messageText: "An error occured while loading a resource. Retrying...",
							id: 9
						});
				}
				else
				{
					this.processGameMessage("loader", event);
				}
				break;
			case "LOADER_CRITICAL_ERROR":
				this.processGameMessage("gameError",
					{
						_titleText: "Critical loader error",
						_messageText: "An error occured while loading a resource. code: " + event._data.code,
						id: 10
					});
				break;
			case "RECONNECTEND":
				this.processGameMessage("reconnectend");
				break;
			case "CURRENCY":
				//TODO: handle CURRENCY event
				//this.processGameMessage("loader");
				break;
			case "HANDLE_GAME_ERROR":
				if (arguments[1][1] > 0)
				{
					this.processGameMessage("gameError", event);
				}
				break;
			case "REALITYCHECKEVENT":
				this.processGameMessage("realitycheckevent", event);
				break;
			case "FREEGAMEMESSAGE":
			case "SERVERMESSAGE":
			case "MESSAGE":
				if (arguments[0]._flags === 4)
				{
					this.processGameMessage("gameError", event);
				}
				else
				{
					this.processGameMessage("message", event);
				}
				break;
			case "BET":
				this.processGameMessage("bet", event);
				break;
			case "BALANCE":
				this.processGameMessage("balance", event);
				break;
			case "WIN_START":
				this.processGameMessage("winStart");
				break;
			case "WIN_SHOW":
				this.processGameMessage("winShow", event);
				break;
			case "WIN":
				this.processGameMessage("win", event);
				break;
			case "UIHELP":
				this.processGameMessage("gameHelpOpened", event);
				break;
			case "UIHISTORY":
				this.processGameMessage("gameHistoryOpened", event);
				break;
			case "AVAILABLE_COINS":
				this.processGameMessage("availableCoins", event);
				break;
			case "SPLASH_SHOW":
				this.processGameMessage("splashShow", event);
				break;
			case "SPLASH_HIDE":
				this.processGameMessage("splashHide", event);
				break;
			case "AVAILABLE_COIN_VALUES":
				this.processGameMessage("availableCoinValues", event);
				break;
			case "SELECTED_LINES":
				this.processGameMessage("selectedLines", event);
				break;
			case "AVAILABLE_LINES":
				this.processGameMessage("availableLines", event);
				break;
			case "ACTIVE_CARDS":
				this.processGameMessage("activeCards", event);
				break;
			case "SELECTED_COIN":
				this.processGameMessage("selectedCoin", event);
				break;
			case "SELECTED_COIN_VALUE":
				this.processGameMessage("selectedCoinValue", event);
				break;
			case "JACKPOT_WIN":
				this.processGameMessage("jackpotWin", event);
				break;
            /*case "GET_SELECTED_COIN_VALUE_UNFORMATTED":
                this.processGameMessage("selectedCoinValueUnformatted", event);
                break;*/
			case "BET_UPDATE":
				this.processGameMessage("betUpdated", event);
				break;
			case "MODAL_SHOWING":
				this.processGameMessage("modalShowing", event);
				break;
			case "MODAL_HIDING":
				this.processGameMessage("modalHiding", event);
				break;
			case "JP_POPUP_SHOWING":
				this.processGameMessage("jackpotPopupShowing", event);
				break;
			case "JP_POPUP_HIDING":
				this.processGameMessage("jackpotPopupHiding", event);
				break;
			case "UIEXTRABALL_AVAILABLE":
				this.processGameMessage("extraballAvailable");
				break;
			case "extraball_on_select_event":
				this.processGameMessage("extraballBought");
				break;
			case "FREEGAME_START":
				if (event._resumed === false)
				{
					this.gameStateVariables.freeGameStarted = true;
					this.processGameMessage("freeGameStarted");
				}
				if (this.gameStateVariables.freeGameStarted === false && event._resumed === true)
				{
					this.gameStateVariables.freeGameStarted = true;
					this.processGameMessage("freeGameResumed");
				}
				break;
			case "FREEGAME_END":
			case "FREEGAME_LOGOUT":
				this.processGameMessage("freeGameEnded");
				break;
			case "HIDE_UI":
				this.processGameMessage("hideUI");
				break;
			default:
				this.log("%cEngage: game event", event.type, "is not defined and will not be passed to Engage", 'background: #222; color: white')
				break;
		}
	},
	gambleModeStarted: false,
	processGameMessage: function (state, e)
	{
		this.log("Processing game message: " + state);
		var gameMessage = { type: state };
		switch (state)
		{
			case "jackpotWin":
				gameMessage.jackpotWin = e._amountInMoney;
				gameMessage.currency = e._currency;
				break;
			case "loader":
				gameMessage.loader = e;
				break;
			case "gameIdle":
				//Game is in idle/waitingspin state. Now we allow message to be shown.
				this.state = Engage.CANSHOWMESSAGE;
				break;
			case "gambleStarted":
				this.state = Engage.GAMEROUNDACTIVE;
				this.gambleModeStarted = true;
				break;
			case "gambleEnded":
				if (this.gambleModeStarted !== true)
				{
					gameMessage.type = "";
				}
				this.gambleModeStarted = false;
				break;
			case "running":
				this.state = Engage.GAMEROUNDACTIVE;
				break;
			case "logout":
				//Redirect when logged out
				if (this.redirectUrl != undefined)
				{
					location.href = this.redirectUrl;
				}
				break;
			case "balanceUpdate":
				gameMessage.rawBalance = e._amount;
				gameMessage.currency = e._currency;

				//Add data related to balance
				gameMessage.data = {
					rawBalance: e._amount,
					currency: e._currency
				};
				break;
			case "balance":
				gameMessage.data = {};
				if (e._amount >= 0)
					gameMessage.data.BalanceInCoins = e._amount;

				gameMessage.data.BalanceInMoney = e._amountInMoney;
				gameMessage.data.Currency = e._currency;
				break;
			case "bet":
			case "betUpdated":
				gameMessage.data = {};
				if (e._amount >= 0)
					gameMessage.data.BetInCoins = e._amount;

				gameMessage.data.BetInMoney = e._amountInMoney;
				gameMessage.data.Currency = e._currency;

				if (e._lines >= 0)
					gameMessage.data.Lines = e._lines;
				if (e._coins >= 0)
					gameMessage.data.Coins = e._coins;
				if (e._denomination >= 0)
					gameMessage.data.CoinValue = e._denomination;

				break;
			case "win":
				gameMessage.data = {};
				if (e._amount >= 0)
					gameMessage.data.WinInCoins = e._amount;

				gameMessage.data.WinInMoney = e._amountInMoney;
				gameMessage.data.Currency = e._currency;
				break;
			case "paytableChanged":
				gameMessage.data = {
					PageNum: e._pageNum,
					Visible: e._visible
				};
				break;
			case "roundWin":
				gameMessage.winAmount = e._amount;
				gameMessage.data = e._amount;
				break;
			case "winShow":
				gameMessage.data = {};
				if (e._winData._line >= 0)
				{
					gameMessage.data.type = "lineWin";
					gameMessage.data.winData = e._winData;
				}
				else
				{
					gameMessage.data.type = "scatterWin";
					gameMessage.data.winData = e._winData;
				}
				break;
			case "availableLines":
			case "availableCoins":
			case "selectedLines":
			case "activeCards":
			case "selectedCoin":
				gameMessage.data = e._amount;
				break;
			case "selectedCoinValue":
				gameMessage.data = {
					amount: e._amount,
					amountInMoney: e._amountInMoney
				};
				break;
			case "availableCoinValues":
				gameMessage.data = e._denom;
				break;
			case "externalMessageOk":
			case "externalMessageExit":
			case "externalMessageAction":
				//Pass id of message
				gameMessage.id = e._id;
				break;
			case "autoplayStarted":
				//Pass id of message
				gameMessage.data = {
					numAutoplay: e.playsLeft
				}
				break;
			case "autoplayNextRound":
				//Pass id of message
				gameMessage.data = {
					numAutoplayLeft: e.playsLeft
				};
				break;
			case "autoplaysLeft":
				gameMessage.data = {
					numAutoplayLeft: e.autoplaysLeft
				}
				break;
			case "realitycheckevent":
				gameMessage.data = {
					type: e.type,
					bet: e._bet,
					minutes: e._minutes,
					win: e._win
				}
				break;
			case "message":
			case "gameError":
				gameMessage.data = {
					title: e._titleText,
					message: e._messageText,
					internalId: e.id
				};
				break;
			case "settings":
				//gameMessage.data = this.parseSettings(e);
				return;
			case "modalShowing":
				gameMessage.buttons = {
					ok: e.ok,
					cancel: e.cancel,
					exit: e.exit
				};
				break;
			case "bonusGameStarted":
				gameMessage.data = {
					bonusSymbol: e.bonusSymbol
				};
				break;
			default:
				//no special internal action for state?
				//this.state = -1;
				break;
		}
		this.dispatchEvent(gameMessage);
	},
    /**
    * inGameMessage
    * Displays a in game message which player must act upon before allowed to continue to play.
    * @@param id String Used as identifier on response events
    * @@param title String
    * @@param message String
    * @@param okBtn String Closes message without any action. Respons: EXTERNAL_MESSAGE_OK
    * @@param exitBtn String sends logout request to server. Respons:EXTERNAL_MESSAGE_EXIT
    * @@param actionBtn String. Respons:EXTERNAL_MESSAGE_ACTION
    */
	//TODO: Expose and map functions in client.
	inGameMessage: function (data)
	{
		if (this.gameCalls.EXTERNAL_MESSAGE)
		{
			if (data.exitBtn == undefined)
				data.exitBtn = "";
			if (data.actionBtn == undefined)
				data.actionBtn = "";
			this.gameCalls.EXTERNAL_MESSAGE(data);

		} else
		{
			this.log("Warning! ExternalMessage is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: "ExternalMessage is not a registred in GAME" });
		}
	},
	inGameMessageOk: function (id)
	{
		this.gameCalls.EX_MESSAGE_OK(id);
	},
	inGameMessageAction: function (id)
	{
		this.gameCalls.EX_MESSAGE_ACTION(id);
	},
	inGameMessageExit: function (id)
	{
		this.gameCalls.EX_MESSAGE_EXIT(id);
	},
	modalOk: function ()
	{
		this.gameCalls.MODAL_OK();
	},
	modalCancel: function ()
	{
		this.gameCalls.MODAL_CANCEL();
	},
	modalExit: function ()
	{
		this.gameCalls.MODAL_EXIT();
	},
	externalMessageHandled: function ()
	{
		this.gameCalls.EXTERNAL_MESSAGE_CLOSE();
	},
	openSettings: function ()
	{
		this.gameCalls.OPEN_SETTINGS();
	},
	disableGame: function ()
	{
		if (this.gameCalls.DISABLE_SCREEN)
		{
			this.gameCalls.DISABLE_SCREEN(true);
		} else
		{
			this.log("Warning! DisableGame is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: "DisableGame is not a registred in GAME" });
		}
	},
	enableGame: function ()
	{
		if (this.gameCalls.ENABLE_SCREEN)
		{
			this.gameCalls.ENABLE_SCREEN(false);
		} else
		{
			this.log("Warning! EnableGame is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: "EnableGame is not a registred in GAME" });
		}
	},
	/**
	 * A temporal implementation of a novel Extcom feature with a fallback to disableGame
	 * @return {boolean} true if the feature available and false if fallback applied
	 */
	disableUI: function ()
	{
		if (this.gameCalls.DISABLE_BUTTONS)
		{
			this.gameCalls.DISABLE_BUTTONS();
			return true;
		} else
		{
			this.disableGame();
			return false;
		}
	},
	/**
	 * A temporal implementation of a novel Extcom feature with a fallback to enableGame
	 * @return {boolean} true if the feature available and false if fallback applied
	 */
	enableUI: function ()
	{
		if (this.gameCalls.ENABLE_BUTTONS)
		{
			this.gameCalls.ENABLE_BUTTONS();
			return true;
		} else
		{
			this.enableGame();
			return false;
		}
	},
	logout: function ()
	{
		if (this.gameCalls.LOGOUT)
		{
			this.gameCalls.LOGOUT();
		} else
		{
			this.log("Warning! Logout is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: "Logout is not a registred in GAME" });
		}
	},
	stopAutoPlay: function ()
	{
		if (this.gameCalls.STOP_AUTOPLAY)
		{
			this.gameCalls.STOP_AUTOPLAY();
		} else
		{
			this.log("Warning! StopAutoplay is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: "StopAutoplay is not a registred in GAME" });
		}
	},
	refreshInGameBalance: function ()
	{
		if (this.gameCalls.BALANCE_UPDATE)
		{
			this.gameCalls.BALANCE_UPDATE();
		} else
		{
			this.log("Warning! RefreshBalance is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " RefreshBalance is not a registred in GAME" });
		}
	},
	externalInGameBalance: function (balance)
	{
		if (this.gameCalls.EXTERNAL_BALANCE_UPDATE)
		{
			this.gameCalls.EXTERNAL_BALANCE_UPDATE({ balance: balance, inGame: true });
		} else
		{
			this.log("Warning! externalInGameBalance is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " externalInGameBalance is not a registred in GAME" });
		}
	},
	getBalance: function ()
	{
		if (this.gameCalls.GET_BALANCE)
		{
			this.gameCalls.GET_BALANCE();
		} else
		{
			this.log("Warning! GetBalance is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GetBalance is not a registred in GAME" });
		}
	},
	getBet: function ()
	{
		if (this.gameCalls.GET_BET)
		{
			this.gameCalls.GET_BET();
		} else
		{
			this.log("Warning! GetBet is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GetBet is not a registred in GAME" });
		}
	},
	getWin: function ()
	{
		if (this.gameCalls.GET_WIN)
		{
			this.gameCalls.GET_WIN();
		} else
		{
			this.log("Warning! GetWin is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GetWin is not a registred in GAME" });
		}
	},
	getSelectedCoin: function ()
	{
		if (this.gameCalls.GET_SELECTED_COIN)
		{
			this.gameCalls.GET_SELECTED_COIN();
		} else
		{
			this.log("Warning! GetSelectedCoin is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GetSelectedCoin is not a registred in GAME" });
		}
	},
	getSelectedCoinValue: function ()
	{
		if (this.gameCalls.GET_SELECTED_COIN_VALUE)
		{
			this.gameCalls.GET_SELECTED_COIN_VALUE();
		} else
		{
			this.log("Warning! GetSelectedCoinValue is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GetSelectedCoinValue is not a registred in GAME" });
		}
	},
	getSelectedLines: function ()
	{
		if (this.gameCalls.GET_SELECTED_LINES)
		{
			this.gameCalls.GET_SELECTED_LINES();
		} else
		{
			this.log("Warning! GetSelectedLines is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GetSelectedLines is not a registred in GAME" });
		}
	},
	getAvailableLines: function ()
	{
		if (this.gameCalls.GET_AVAILABLE_LINES)
		{
			this.gameCalls.GET_AVAILABLE_LINES();
		} else
		{
			this.log("Warning! GET_AVAILABLE_LINES is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GET_AVAILABLE_LINES is not a registred in GAME" });
		}
	},
	getAvailableCoins: function ()
	{
		if (this.gameCalls.GET_AVAILABLE_COINS)
		{
			this.gameCalls.GET_AVAILABLE_COINS();
		} else
		{
			this.log("Warning! GetAvailableCoins is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GetAvailableCoins is not a registred in GAME" });
		}
	},
	getAvailableCoinValues: function ()
	{
		if (this.gameCalls.GET_AVAILABLE_COIN_VALUES)
		{
			this.gameCalls.GET_AVAILABLE_COIN_VALUES();
		} else
		{
			this.log("Warning! GetAvailableCoinValues is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GetAvailableCoinValues is not a registred in GAME" });
		}
	},
	getAutoplaysLeft: function ()
	{
		if (this.gameCalls.GET_AP_LEFT)
		{
			this.gameCalls.GET_AP_LEFT();
		} else
		{
			this.log("Warning! getAutoplaysLeft is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " getAutoplaysLeft is not a registred in GAME" });
		}
	},
	getActiveCards: function ()
	{
		if (this.gameCalls.GET_ACTIVE_CARDS)
		{
			this.gameCalls.GET_ACTIVE_CARDS();
		} else
		{
			this.log("Warning! GetActiveCards is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GetActiveCards is not a registred in GAME" });
		}
	},
	//Returns a localized string
	getLocalizedString: function (string)
	{
		if (this.gameCalls.GET_LOCALIZED_STRING)
		{
			return this.gameCalls.GET_LOCALIZED_STRING(string);
		} else
		{
			this.log("Warning! GetLocalizedString is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GetLocalizedString is not a registred in GAME" });
		}
	},
	soundOn: function ()
	{
		if (this.gameCalls.SET_SOUND)
		{
			this.gameCalls.SET_SOUND(1);
		} else
		{
			this.log("Warning! SetSound is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " SetSound is not a registred in GAME" });
		}
	},
	soundOff: function ()
	{
		if (this.gameCalls.SET_SOUND)
		{
			this.gameCalls.SET_SOUND(0);
		} else
		{
			this.log("Warning! SetSound is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " SetSound is not a registred in GAME" });
		}
	},
	setCoins: function (e)
	{
		if (this.gameCalls.SET_COINS)
		{
			this.gameCalls.SET_COINS(e.data);
		} else
		{
			this.log("Warning! SetCoin is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " SetCoin is not a registred in GAME" });
		}
	},
	setCoinValue: function (e)
	{
		if (this.gameCalls.SET_COIN_VALUE)
		{
			this.gameCalls.SET_COIN_VALUE(e.data);
		} else
		{
			this.log("Warning! SetCoinValue is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " SetCoinValue is not a registred in GAME" });
		}
	},
	setLines: function (e)
	{
		if (this.gameCalls.SET_LINES)
		{
			this.gameCalls.SET_LINES(e.data);
		} else
		{
			this.log("Warning! SetLines is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " SetLines is not a registred in GAME" });
		}
	},
	incCoins: function ()
	{
		if (this.gameCalls.INC_COINS)
		{
			this.gameCalls.INC_COINS();
		} else
		{
			this.log("Warning! INC_COINS is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " INC_COINS is not a registred in GAME" });
		}
	},
	decCoins: function ()
	{
		if (this.gameCalls.DEC_COINS)
		{
			this.gameCalls.DEC_COINS();
		} else
		{
			this.log("Warning! DEC_COINS is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " DEC_COINS is not a registred in GAME" });
		}
	},
	incCoinValue: function ()
	{
		if (this.gameCalls.INC_COIN_VALUE)
		{
			this.gameCalls.INC_COIN_VALUE();
		} else
		{
			this.log("Warning! INC_COIN_VALUE is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " INC_COIN_VALUE is not a registred in GAME" });
		}
	},
	decCoinValue: function ()
	{
		if (this.gameCalls.DEC_COIN_VALUE)
		{
			this.gameCalls.DEC_COIN_VALUE();
		} else
		{
			this.log("Warning! DEC_COIN_VALUE is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " DEC_COIN_VALUE is not a registred in GAME" });
		}
	},
	incLines: function ()
	{
		if (this.gameCalls.INC_LINES)
		{
			this.gameCalls.INC_LINES();
		} else
		{
			this.log("Warning! INC_LINES is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " INC_LINES is not a registred in GAME" });
		}
	},
	decLines: function ()
	{
		if (this.gameCalls.DEC_LINES)
		{
			this.gameCalls.DEC_LINES();
		} else
		{
			this.log("Warning! DEC_LINES is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " DEC_LINES is not a registred in GAME" });
		}
	},
	togglePaytable: function (pageNum)
	{
		if (this.gameCalls.TOGGLE_PAYTABLE)
		{
			this.gameCalls.TOGGLE_PAYTABLE(pageNum);
		} else
		{
			this.log("Warning! TogglePaytable is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " TOGGLE_PAYTABLE is not a registred in GAME" });
		}
	},
	showHelp: function ()
	{
		if (this.gameCalls.OPEN_GAMEHELP)
		{
			this.gameCalls.OPEN_GAMEHELP();
		} else
		{
			this.log("Warning! OPEN_GAMEHELP is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " OPEN_GAMEHELP is not a registred in GAME" });
		}
	},
	betMax: function ()
	{
		if (this.gameCalls.BET_MAX)
		{
			this.gameCalls.BET_MAX();
		} else
		{
			this.log("Warning! BetMax is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " BetMax is not a registred in GAME" });
		}
	},
	spin: function ()
	{
		if (this.gameCalls.SPIN)
		{
			this.gameCalls.SPIN();
		} else
		{
			this.log("Warning! Spin is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " Spin is not a registred in GAME" });
		}
	},
	gamble: function (e)
	{
		if (this.gameCalls.GAMBLE)
		{
			if (e.data)
			{
				this.gameCalls.GAMBLE(e.data);
			}
			else
			{
				this.gameCalls.GAMBLE();
			}
		} else
		{
			this.log("Warning! Gamble is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " Gamble is not a registred in GAME" });
		}
	},
	collect: function ()
	{
		if (this.gameCalls.COLLECT)
		{
			this.gameCalls.COLLECT();
		} else
		{
			this.log("Warning! Collect is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " Collect is not a registred in GAME" });
		}
	},
	pause: function (b)
	{
		if (this.gameCalls.PAUSE)
		{
			this.gameCalls.PAUSE(b);
		} else
		{
			this.log("Warning! Pause is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " Pause is not a registred in GAME" });
		}
	},
	autoPlay: function (e)
	{
		if (this.gameCalls.AUTOPLAY)
		{
			if (e.data)
			{
				this.gameCalls.AUTOPLAY(e.data);
			}
			else
			{
				this.gameCalls.AUTOPLAY();
			}
		} else
		{
			this.log("Warning! AutoPlay is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " AutoPlay is not a registred in GAME" });
		}
	},
	setFastPlay: function (e)
	{
		if (this.gameCalls.SET_FASTPLAY)
		{
			this.gameCalls.SET_FASTPLAY(e.data.value); // 0 or 1
		} else
		{
			this.log("Warning! SET_FASTPLAY is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " SET_FASTPLAY is not a registred in GAME" });
		}
	},
	getFastPlay: function ()
	{
		if (this.gameCalls.GET_FASTPLAY)
		{
			this.gameCalls.GET_FASTPLAY();
		} else
		{
			this.log("Warning! GET_FASTPLAY is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GET_FASTPLAY is not a registred in GAME" });
		}
	},
	setLeftHandMode: function (value)
	{
		if (this.gameCalls.SET_LEFTHAND_MODE)
		{
			this.gameCalls.SET_LEFTHAND_MODE(value);
		} else
		{
			this.log("Warning! SET_LEFTHAND_MODE is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " SET_LEFTHAND_MODE is not a registred in GAME" });
		}
	},
	getLeftHandMode: function ()
	{
		if (this.gameCalls.GET_LEFTHAND_MODE)
		{
			this.gameCalls.GET_LEFTHAND_MODE();
		} else
		{
			this.log("Warning! GET_LEFTHAND_MODE is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " GET_LEFTHAND_MODE is not a registred in GAME" });
		}
	},
	closeMysteryJackpotPopup: function ()
	{
		if (this.gameCalls.CLOSE_MJP_POPUP)
		{
			this.gameCalls.CLOSE_MJP_POPUP();
		} else
		{
			this.log("Warning! CLOSE_MJP_POPUP is not a registred in GAME");
			this.dispatchEvent({ type: "error", msg: " CLOSE_MJP_POPUP is not a registred in GAME" });
		}
	},
	buyExtraBall: function ()
	{
		if (this.gameCalls.BUY_EXTRA_BALL)
		{
			this.gameCalls.BUY_EXTRA_BALL();
		} else
		{
			this.log("Warning! BUY_EXTRA_BALL is not registered in GAME");
			this.dispatchEvent({ type: "error", msg: "BUY_EXTRA_BALL is not registered in GAME" });
		}
	},
	hideUI: function ()
	{
		if (this.gameCalls.HIDE_UI)
		{
			this.gameCalls.HIDE_UI();
		} else
		{
			this.log("Warning! HIDE_UI is not registered in GAME");
			this.dispatchEvent({ type: "error", msg: "HIDE_UI is not registered in GAME" });
		}
	},
	parseSettings: function (o)
	{
		if (this.lastSettings === undefined)
		{
			this.lastSettings = {};
			for (key in o.settings)
			{
				if (key != "children")
					this.lastSettings[key] = o.settings[key];
			}
			return;
		}
		var settingsChanged = [];
		for (key in o.settings)
		{
			if ((key !== "children" || key !== "_children") && (key[0] === "_"))
			{
				if (this.lastSettings[key] != o.settings[key])
				{
					settingsChanged.push({ id: key, newVal: o.settings[key] });
				}
			}
		}
		if (this.lastSettings === undefined)
		{
			this.lastSettings = o;
			return;
		}

		for (var i = 0; i < settingsChanged.length; i++)
		{
			this.onSettingsChange(settingsChanged[i]);
		}

		for (key in o.settings)
		{
			if (key != "children")
				this.lastSettings[key] = o.settings[key];
		}

	},
	onSettingsChange: function (setting)
	{
		this.log("Settings changed: " + setting.id);

		this.dispatchEvent({ type: setting.id, data: setting.newVal })
	},
	listeners: {},
	addEventListener: function (type, f)
	{
		var e = { type: type }
		if (!this.listeners[e.type])
		{
			this.listeners[e.type] = [];
		}
		var listeners = this.listeners[e.type];
		if (listeners.indexOf(f) === -1)
		{
			listeners.push(f);
		}
	},
	removeEventListener: function (type, f)
	{
		var e = { type: type }
		var listeners = this.listeners[e.type];
		if (listeners)
		{
			var index = listeners.indexOf(f);
			if (index !== -1)
			{
				listeners.splice(index, 1);
			}
		}
	},
	removeAllEventListeners: function ()
	{
		;
		for (var k in this.listeners)
		{
			if (!this.listeners.hasOwnProperty(k))
			{
				continue;
			}
			for (var i = 0; i < this.listeners[k].length; i++)
			{
				this.listeners[k][i] = undefined;
			}
			this.listeners[k].length = 0;
			delete this.listeners[k];
		}
	},
    /**
    * dispatchEvent
    * Dispatches events to external implementation
    */
	dispatchEvent: function (event)
	{
		if (this.iframeCom.initialized)
		{
			var listeners = this.listeners[event.type];

			if (listeners)
			{
				this.iframeCom.dispatchMessage(event);
			}

		}
		// else
		//{
		var listeners = this.listeners[event.type];
		if (listeners)
		{
			for (var i = 0; i < listeners.length; i++)
			{
				if (typeof listeners[i] == "function")
				{
					listeners[i](event);
				}
			}
		}
		//}
	},
	//Allow custom modules to be added. For example see Engage.RealityCheck
	registerModule: function (module)
	{
		module.parent = this;
		this.modules.push(module);
	},
	enableDebug: function ()
	{
		Engage.debug = true;
	},
	log: function (msg)
	{
		if (this.debug)
		{
			var s = "", css = "";
			for (var i = 0; i < arguments.length; i++)
			{
				if (typeof arguments[i] == "string" && arguments[i].match("background"))
					css = arguments[i];
				else
					s += " " + arguments[i];
			}
			console.log(s, css);
		}
	},
}

/**
* iframeCom
*
*/
Engage.iframeCom =
	{
		debug: false,
		domain: undefined,
		source: undefined,
		targetOrigin: undefined,
		initialized: false,
		frame: undefined,
		boundProcessMessage: undefined,
		cleanup: function ()
		{
			if (this.boundProcessMessage)
			{
				window.removeEventListener("message", this.boundProcessMessage, false);
				this.boundProcessMessage = undefined;
			}
			this.initialized = false;
		},
		init: function (reqCallback)
		{
			this.cleanup();
			this.targetOrigin = Engage.iframeOverlayUrl;
			this.requestCallback = reqCallback;
			this.createIframe("msgIframe", this.parseTargetOriginUrl(this.targetOrigin));
			this.boundProcessMessage = this.processMessage.bind(this);
			window.addEventListener("message", this.boundProcessMessage, false);
			this.initialized = true;

		},
		processMessage: function (e)
		{
			this.requestCallback(e.data);
		},
		parseTargetOriginUrl: function (targetOrigin)
		{
			//Fix url. If targetOrigin path is relative. Assume it's hosted locally and append window.location.origin.
			var res = targetOrigin.match(/http/g);
			if (res == null)
			{
				this.targetOrigin = window.location.origin + "/" + this.targetOrigin;

				var url = this.targetOrigin + "?x=";
				//Get all module specific parameters
				for (var i = 0; i < Engage.modules.length; i++)
				{
					if (Engage.modules[i].resolveParameters)
					{
						url += Engage.modules[i].resolveParameters();
					}
				}
				return url;
			}
			return targetOrigin;
		},
		createIframe: function (id, src)
		{
			this.frame = document.createElement("iframe");
			this.frame.id = id;
			this.frame.src = src;
			this.frame.scrolling = "no";
			this.frame.style.position = "fixed";
			this.frame.style.top = "0px";
			this.frame.style.left = "0px";
			this.frame.style.pointerEvents = "none";
			this.frame.style.width = "100%";
			this.frame.style.height = "100%";
			this.frame.allowtransparency = "true";
			this.frame.style.zIndex = "999999999";
			this.frame.style.border = "none";
			this.frame.style.display = "none";

			document.body.appendChild(this.frame);
			this.source = this.frame.contentWindow;

			//When iframe done loading, establish connection with component within frame.
			this.frame.onload = function ()
			{
				this.dispatchMessage({
					type: "initialized",
				})
			}.bind(this);
		},
		setIframeInteractive: function (b)
		{
			if (b)
			{
				this.frame.style.pointerEvents = "all";
			}
			else
			{
				this.frame.style.pointerEvents = "none";
			}
		},
		//Dispatches message to iframe
		dispatchMessage: function (message)
		{
			if (this.source != undefined)
			{
				this.source.postMessage(message, this.targetOrigin);
			}
			else
			{
				this.log("%cERROR! Iframe source is not defined! No message posted.", 'background: red; color: white')
			}
		},
		log: function (msg)
		{
			if (this.debug)
			{
				var s = "", css = "";
				for (var i = 0; i < arguments.length; i++)
				{
					if (typeof arguments[i] == "string" && arguments[i].match("background"))
						css = arguments[i];
					else
						s += " " + arguments[i];
				}
				console.log(s, css);
			}
		},
		setDebug: function (b)
		{
			this.debug = b;
			this.testModule = this.frame.contentWindow.EngageTestModule;
			this.dispatchMessage({ type: "debug" });
		},
		//Event should only have one listener.
		dispatchGameEvent: function (event)
		{
			var listeners = this.listeners[event.type];
			if (listeners)
			{
				this.dispatchMessage(event);
			}
			else
			{
				this.log("Engage: No external listener registred for event", "%c" + event.type, 'background: yellow; color: black');
			}
		},
	}

Engage.CANSHOWMESSAGE = 0;
Engage.GAMEROUNDACTIVE = 1;

//Map PNGExternal & Engage name to Engage for compatibility
PNGExternal = Engage;

PNGHostInterface = Engage;;
/**
* Reality Check helper module for Engage. Handles Reality Check specific logic for PNGs RealityCheckMessage.
*
* Accepted requets:
* "getRCStrings"    - Returns localized strings used in RealityCheck message
* "onGameEnd"       - redirects page to passed url
* "onAccountHistory"- redirects page to passed url
*/
    Engage.RealityCheck =
    {
        parent: undefined,
        request: function(e) {
            switch (e.rcreq) {
            case "getRCStrings":
                //Returns strings for PNG version of message. Operator can use this or handle strings on their side.
                return { type: "rcStrings", data: { strings: this.getRCStrings() } };
                break;
            case "onGameEnd":
                if (e.data.redirectUrl != "")
                    window.location = e.data.redirectUrl;
                break;
            case "onAccountHistory":
                if (e.data.redirectUrl != "") {
                    if (e.data.urlMode == "samepage") {
                        window.location = e.data.redirectUrl;
                    } else {
                        var win = window.open(e.data.redirectUrl, '_blank');
                        win.focus();
                    }
                }
                break;
            default:
                return undefined;
                break;
            }
        },

        getRCStrings: function() {

            if (!this.parent.gameHostInterface || !this.parent.gameHostInterface.GetLocalizedString) {
                // this.log("Flash game no initialized or GetLocalizedString not available. No strings returned")
                // return;
            }
            //TODO: fix Megaton implementation GetLocalizedString
            for (key in RCComponent.localizedStrings) {
                var ret = this.parent.getLocalizedString(RCComponent.localizedStrings[key]);
                if (ret) {
                    RCComponent.localizedStrings[key] = ret;
                }
            }
            return RCComponent.localizedStrings;
        },
    }
    Engage.registerModule(Engage.RealityCheck);
/**
 * Example of a Reality Check component.
 * Handles timer logic and presentation
 *
 * */

    var RCComponent =
    {
        debug: false,
        proceedUrl: undefined,

        exitUrl: undefined,
        historyUrl: undefined,
        continueUrl: undefined,
        lobbyUrl: undefined,
        platform: undefined,
        historyUrlMode: undefined,
        brand: undefined,
        redirectUrl: undefined,

        startTime: 0,
        initialSessionTime: undefined,
        rcIntervalTime: undefined,
    totalTime:0,
    showing: false,

    messageData: { id: "", title: "", message: "", okBtn: "", exitBtn: "", actionBtn: "", redirect: false },

    rcInterval:undefined,
    localizedStrings: {
        title: "IDS_RC_HEADING",
        playtime: "IDS_RC_PLAYTIME",
        minutes: "IDS_RC_MINUTES",
        continueorend: "IDS_RC_CONTINUE",
        btncontinue: "IDS_BTN_CONTINUE",
        btnexit: "IDS_BTN_EXIT",
        btnaccounthistory: "IDS_BTN_ACCOUNTHISTORY"
    },
    init: function(config) {
        this.exitUrl = config.exitUrl;
        this.historyUrl = config.historyUrl;
        this.continueUrl = config.continueUrl;
        this.lobbyUrl = config.lobbyUrl;
        this.platform = config.platform;
        this.historyUrlMode = config.historyUrlMode;
        this.initialSessionTime = config.initialSessionTime;
        this.rcIntervalTime = config.rcIntervalTime;
        this.brand = config.brand;

        //Add listeners to game events.
        Engage.addEventListener(
            "rcStrings",
            RCComponent.onEngageEvent.bind(RCComponent)
        );
        Engage.addEventListener(
            "loader",
            RCComponent.onEngageEvent.bind(RCComponent)
        );
        Engage.addEventListener(
            "externalMessageOk",
            RCComponent.onEngageEvent.bind(RCComponent)
        );
        Engage.addEventListener(
            "externalMessageExit",
            RCComponent.onEngageEvent.bind(RCComponent)
        );
        Engage.addEventListener(
            "externalMessageAction",
            RCComponent.onEngageEvent.bind(RCComponent)
        );

        /*Engage.addEventListneer(
            "GET_LOCALIZED_STRING",
            RCComponent.onEngageEvent.bind(RCComponent)
        );*/


        //Request localized strings
        //Engage.request({ rcreq: "getRCStrings" });

    },
    setupTimeLogic: function() {
        this.totalTime = this.initialSessionTime * 1000;
        this.startTime = new Date().getTime();

        //Get time until first message
        var firstRC = this.initialSessionTime % this.rcIntervalTime;
        firstRC = this.rcIntervalTime - firstRC;

        this.rcInterval = setTimeout(function () { RCComponent.onRealityCheck("activate") }.bind(this), firstRC * 1000);
    },
    //Construct reality check message
    buildRealityCheckMessage: function () {
    	var message;
	    var time = this.totalTime / 1000;
	    var timeFormattedString = this.secondsToHHMMSS(time);
	    var playTimeString = this.localizedStrings.playtime.replace("%d", timeFormattedString)
			.replace("%x", "")
			.replace(".", "");

        message = playTimeString + "\n" + this.localizedStrings.continueorend;

        this.messageData.id = "realitycheckmessage";
        this.messageData.title = this.localizedStrings.title;
        this.messageData.message = message;
        this.messageData.okBtn = this.localizedStrings.btncontinue;
        this.messageData.exitBtn = this.localizedStrings.btnexit;
        this.messageData.actionBtn = this.localizedStrings.btnaccounthistory;
        this.messageData.redirect = false;

        this.showing = true;
        return this.messageData;
    },
    //Restart interval timer
    onContinuePlaying: function()
    {
        this.rcInterval = setTimeout(function () { RCComponent.onRealityCheck("activate") }.bind(this), this.rcIntervalTime * 1000);
        this.showing = false;
    },
    //User decided to stop playing, redirect to exiturl.
    onStopPlaying: function()
    {
        if (this.platform === "mobile")
        {
            this.lobbyUrl = "";
        }
        Engage.request(
            { rcreq: "onGameEnd", data: { redirectUrl: this.lobbyUrl == "" ? this.exitUrl : this.lobbyUrl } }
        );
    },
    onAccountHistory: function () {
        Engage.request(
            { rcreq: "onAccountHistory", data: { redirectUrl: this.historyUrl, urlMode: this.historyUrlMode } }
        );
    },
    setLocalizedString: function (e) {
        for (var k in this.localizedStrings) {
            if (this.localizedStrings[k] == e.id) {
                this.localizedStrings[k] = e.string;
            }
        }

    },
    onEngageEvent: function(e)
    {
    	this.log("RCComponent Received message: ", "%c" + e.type, 'background: blue; color: white');
        switch(e.type)
    	{
            case "loader":
                if (e.loader._id === 3)
                {
                    Engage.request({ rcreq: "getRCStrings" });
                }
                break;
        	/*case "initialized":
        		//Request localized strings
        		Engage.request({ rcreq: "getRCStrings" });
                break;*/
            case "rcStrings":
                //Save strings
                //this.localizedStrings = e.data.strings;
                //Initialize time logic
                this.setupTimeLogic();
                break;
            case "externalMessageOk":
                if (e.id == "realitycheckmessage")
                {
                    this.notifyOperator(this.continueUrl);
                    this.onContinuePlaying();
                }

                break;
            case "externalMessageExit":
                if (e.id == "realitycheckmessage")
                {
                    this.notifyOperator(this.exitUrl);
                    this.onStopPlaying();
                }

                break;
            case "externalMessageAction":
                if (e.id == "realitycheckmessage")
                {
                    this.onAccountHistory();
                }

                break;
        }
    },
    onRealityCheck: function(action)
    {
        switch(action)
        {
            //Reality check is needed,
            //Send request that the game should display the message.
            case "activate":
                if (!this.showing) {
                    this.totalTime = (this.initialSessionTime * 1000) + (new Date().getTime() - this.startTime);
                    this.log("RCComponent: Requesting to activate Reality Check message! totalTime:", this.totalTime / 1000);
                    clearTimeout(this.rcInterval);
                    Engage.request({ req: "inGameMessage", data: this.buildRealityCheckMessage() });
                }
                break;
        }
    },
    notifyOperator: function (url) {
        if (url != "")
        {
            var xhttp = new XMLHttpRequest();
            xhttp.onreadystatechange = function () {
                if (xhttp.readyState == 4 && xhttp.status == 200) {
                    //this.log("Got response from url", url, "RESPONSE TEXT", xhttp.responseText);
                }
            };
            xhttp.open("GET", url, true);
            if (this.brand === "bet365")
            {
                xhttp.withCredentials = true;
            }
            xhttp.send();
        }
    },
    log: function (msg) {
    	if (this.debug) {
    		var s = "", css = "";
    		for (var i = 0; i < arguments.length; i++) {
    			if (typeof arguments[i] == "string" && arguments[i].match("background"))
    				css = arguments[i];
    			else
    				s += " " + arguments[i];
    		}
    		console.log(s, css);
    	}
    },
    secondsToHHMMSS: function (sec) {
    	var sec_num = parseInt(sec, 10);
    	var h = Math.floor(sec_num / 3600);
    	var m = Math.floor((sec_num - (h * 3600)) / 60);
    	var s = sec_num - (h * 3600) - (m * 60);
    	if (h < 10) { h = "0" + h; }
    	if (m < 10) { m = "0" + m; }
    	if (s < 10) { s = "0" + s; }
    	var t = h + ':' + m + ':' + s;
    	return t;
    }
};
