using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System;
using WebSocketSharp;
using WebSocketSharp.Server;
using Newtonsoft.Json.Linq;
using UnityEngine.SceneManagement;

namespace NDream.AirConsole {
	public enum StartMode {
		VirtualControllers,
		Normal,
		NoBrowserStart
	}

	public enum AndroidUIResizeMode {
		NoResizing,
		ResizeCamera,
		ResizeCameraAndReferenceResolution
	}

	public delegate void OnReady (string code);

	public delegate void OnMessage (int from, JToken data);

	public delegate void OnDeviceStateChange (int device_id, JToken user_data);

	public delegate void OnConnect (int device_id);

	public delegate void OnDisconnect (int device_id);

	public delegate void OnCustomDeviceStateChange (int device_id, JToken custom_device_data);

	public delegate void OnDeviceProfileChange (int device_id);

	public delegate void OnAdShow ();

	public delegate void OnAdComplete (bool ad_was_shown);

	public delegate void OnGameEnd ();

	public delegate void OnHighScores (JToken highscores);
	
	public delegate void OnHighScoreStored (JToken highscore);

	public delegate void OnPersistentDataStored (string uid);
	
	public delegate void OnPersistentDataLoaded (JToken data);
   
	public delegate void OnPremium (int device_id);

	public class AirConsole : MonoBehaviour {
		#if !DISABLE_AIRCONSOLE
		#region airconsole api

		/// <summary>
		/// AirConsole Singleton Instance.
		/// This is your direct access to the AirConsole API.
		/// </summary>
		/// <value>AirConsole Singleton Instance</value>
		public static AirConsole instance {
			get {
				
				if (_instance == null) {
					_instance = GameObject.FindObjectOfType<AirConsole> ();
					if (_instance != null) {
						DontDestroyOnLoad (_instance.gameObject);
					}
				}
				
				return _instance;
			}
		}
		
		/// <summary>
		/// Gets called when the game console is ready.
		/// This event also also fires onConnect for all devices that already are
		/// connected and have loaded your game.
		/// This event also fires OnCustomDeviceStateChange for all devices that are
		/// connected, have loaded your game and have set a custom Device State.
		/// </summary>
		/// <param name="code">The AirConsole join code.</param> 
		public event OnReady onReady;

		/// <summary>
		/// Gets called when a message is received from another device
		/// that called message() or broadcast().
		/// </summary>
		/// <param name="from">The device ID that sent the message.</param> 
		/// <param name="data">The data that was sent.</param>
		public event OnMessage onMessage;

		/// <summary>
		/// Gets called when a device joins/leaves a game session or updates its DeviceState (custom DeviceState, profile pic, nickname). 
		/// This is function is also called every time OnConnect, OnDisconnect or OnCustomDeviceStateChange is called. It's like their root function.
		/// </summary>
		/// <param name="device_id">the device ID that changed its DeviceState.</param>
		/// <param name="data"> the data of that device. If undefined, the device has left.</param>
		public event OnDeviceStateChange onDeviceStateChange;

		/// <summary>
		/// Gets called when a device has connected and loaded the game.
		/// </summary>
		/// <param name="device_id">the device ID that loaded the game.</param>
		public event OnConnect onConnect;

		/// <summary>
		/// Gets called when a device has left the game.
		/// </summary>
		/// <param name="device_id">the device ID that left the game.</param>
		public event OnDisconnect onDisconnect;

		/// <summary>
		/// Gets called when a device updates it's custom DeviceState by calling SetCustomDeviceState or SetCustomDeviceStateProperty. 
		/// Make sure you understand the power of device states: http://developers.airconsole.com/#/guides/device_ids_and_states
		/// </summary>
		/// <param name="device_id">the device ID that changed its customDeviceState.</param> 
		/// <param name="cutsom_data">The custom DeviceState data value.</param>
		public event OnCustomDeviceStateChange onCustomDeviceStateChange;

		/// <summary>
		/// Gets called when a device updates it's profile pic, nickname or email.
		/// </summary>
		/// <param name="device_id">The device_id that changed its profile.</param>
		public event OnDeviceProfileChange onDeviceProfileChange;

		/// <summary>
		/// Gets called if a fullscreen advertisement is shown on this screen.
		/// In case this event gets called, please mute all sounds.
		/// </summary>
		public event OnAdShow onAdShow;

		/// <summary>
		/// Gets called when an advertisement is finished or no advertisement was shown.
		/// </summary>
		/// <param name="ad_was_shown">True if an ad was shown and onAdShow was called.</param>
		public event OnAdComplete onAdComplete;

		/// <summary>
		/// Gets called when the game should be terminated. 
		/// In case this event gets called, please mute all sounds and stop all animations.
		/// </summary>
		public event OnGameEnd onGameEnd;

		/// <summary> 
		/// Gets called when high scores are returned after calling requestHighScores.
		/// <param name="highscores">The high scores.</param>
		/// </summary>
		public event OnHighScores onHighScores;

		/// <summary>
		/// Gets called when a high score was successfully stored.
		/// </summary>
		/// <param name="highscore">The stored high score if it is a new best for the user or else null.</param>
		public event OnHighScoreStored onHighScoreStored;

		/// <summary>
		/// Gets called when persistent data was stored from StorePersistentData().
		/// </summary>
		/// <param name="uid">The uid for which the data was stored.</param>
		public event OnPersistentDataStored onPersistentDataStored;

		/// <summary>
		/// Gets called when persistent data was loaded from RequestPersistentData().
		/// </summary>
		/// <param name="data">An object mapping uids to all key value pairs.</param>
		public event OnPersistentDataLoaded onPersistentDataLoaded;

		/// <summary> 
		/// Gets called when a device becomes premium or when a premium device connects.
		/// <param name="device_id">The device id of the premium device.</param>
		/// </summary>
		public event OnPremium onPremium;

		/// <summary>
		/// Determines whether the AirConsole Unity Plugin is ready. Use onReady event instead if possible.
		/// </summary>
		/// <returns><c>true</c> if the AirConsole Unity Plugin is ready; otherwise, <c>false</c>.</returns>
		public bool IsAirConsoleUnityPluginReady () {
			return wsListener != null && wsListener.IsReady ();
		}

		/// <summary>
		/// Sends a message to another device.
		/// </summary>
		/// <param name="to">The device ID to send the message to.</param>
		/// <param name="data">The data to send.</param>
		public void Message (int to, object data) {
			
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			
			JObject msg = new JObject ();
			msg.Add ("action", "message");
			msg.Add ("from", to);
			msg.Add ("data", JToken.FromObject (data));
			
			wsListener.Message (msg);
		}

		/// <summary>
		/// Sends a message to all devices.
		/// </summary>
		/// <param name="data">The message to send.</param>
		public void Broadcast (object data) {
			
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			
			JObject msg = new JObject ();
			msg.Add ("action", "broadcast");
			msg.Add ("data", JToken.FromObject (data));
			
			wsListener.Message (msg);
		}

		/// <summary>
		/// Returns the device_id of this device.
		/// Every device in a AirConsole session has a device_id.
		/// The screen always has device_id 0. You can use the AirConsole.SCREEN
		/// constant instead of 0.
		/// All controllers also get a device_id. You can NOT assume that the device_ids
		/// of controllers are consecutive or that they start at 1.
		///
		/// DO NOT HARDCODE CONTROLLER DEVICE IDS!
		///
		/// If you want to have a logic with "players numbers" (Player 0, Player 1,
		/// Player 2, Player 3) use the setActivePlayers helper function! You can
		/// hardcode player numbers, but not device_ids.
		///
		/// Within an AirConsole session, devices keep the same device_id when they
		/// disconnect and reconnect. Different controllers will never get the same
		/// device_id in a session. Every device_id remains reserved for the device that
		/// originally got it.
		///
		/// For more info read
		/// http:// developers.airconsole.com/#/guides/device_ids_and_states
		/// </summary>
		/// <returns>The device identifier.</returns>
		public int GetDeviceId () {
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			return _device_id;
		}

		/// <summary>
		/// Takes all currently connected controllers and assigns them a player number.
		///  Can only be called by the screen. You don't have to use this helper
		/// function, but this mechanism is very convenient if you want to know which
		/// device is the first player, the second player, the third player ...
		/// The assigned player numbers always start with 0 and are consecutive.
		/// You can hardcode player numbers, but not device_ids.
		/// Once the screen has called setActivePlayers you can get the device_id of
		/// the first player by calling convertPlayerNumberToDeviceId(0), the device_id
		/// of the second player by calling convertPlayerNumberToDeviceId(1), ...
		/// You can also convert device_ids to player numbers by calling
		/// convertDeviceIdToPlayerNumber(device_id). You can get all device_ids that
		/// are active players by calling getActivePlayerDeviceIds().
		/// The screen can call this function every time a game round starts.
		/// </summary>
		/// <param name="data">The maximum number of controllers that should 
		/// get a player number assigned.</param>
		public void SetActivePlayers (int max_players=-1) {
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}

			List<int> device_ids = GetControllerDeviceIds ();
			_players.Clear ();
			if (max_players == -1) {
				max_players = device_ids.Count;
			}
			for (int i = 0; i < device_ids.Count && i < max_players; ++i) {
				_players.Add (device_ids [i]);
			}
			JObject msg = new JObject ();
			msg.Add ("action", "setActivePlayers");
			msg.Add ("max_players", max_players);
			
			wsListener.Message (msg);
		}

		/// <summary>
		/// Returns an array of device_ids of the active players previously set by the
		/// screen by calling setActivePlayers. The first device_id in the array is the
		/// first player, the second device_id in the array is the second player, ...
		/// </summary>
		public ReadOnlyCollection<int> GetActivePlayerDeviceIds {
			get { return _players.AsReadOnly (); }
		}

		/// <summary>
		/// Returns the device_id of a player, if the player is part of the active
		/// players previously set by the screen by calling setActivePlayers. If fewer
		/// players are in the game than the passed in player_number or the active
		/// players have not been set by the screen, this function returns undefined.
		/// </summary>
		/// <param name="player_number">Player Number.</param>
		public int ConvertPlayerNumberToDeviceId (int player_number) {
			if (player_number >= 0 && player_number < _players.Count) {
				return _players [player_number];
			}
			return -1;
		}

		/// <summary>
		/// Returns the player number for a device_id, if the device_id is part of the
		/// active players previously set by the screen by calling setActivePlayers.
		/// Player numbers are zero based and are consecutive. If the device_id is not
		/// part of the active players, this function returns -1.
		/// </summary>
		/// <param name="device_id">Device id.</param>
		public int ConvertDeviceIdToPlayerNumber (int device_id) {
			return _players.IndexOf (device_id);
		}
		


		/// <summary>
		/// Returns the globally unique id of a device.
		/// </summary>
		/// <returns>The UID.</returns>
		/// <param name="device_id">The device id for which you want the uid. Default is this device.</param>
		public string GetUID (int device_id = -1) {
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			if (device_id == -1) {
				device_id = GetDeviceId ();
			}
			JToken device = GetDevice (device_id);
			if (device == null) {
				return null;
			}
			return (string)device["uid"];
		}
		
		/// <summary>
		/// Gets the custom DeviceState of a device.
		/// </summary>
		/// <param name="device_id">The device ID of which you want the custom state. Default is this device.</param>
		/// <returns> The custom data previously set by the device.</returns>
		public JToken GetCustomDeviceState (int device_id = -1) {
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			if (device_id == -1) {
				device_id = GetDeviceId ();
			}
			if (GetDevice (device_id) != null) {
				
				try {
					return GetDevice (device_id) ["custom"];
				} catch (Exception e) {
					
					if (Settings.debug.error) {
						Debug.LogError ("AirConsole: " + e.Message);
					}
					return null;
				}
				
			} else {
				
				if (Settings.debug.warning) {
					Debug.LogWarning ("AirConsole: GetCustomDeviceState: device_id " + device_id + " not found");
				}
				return null;
			}
		}
		
		/// <summary>
		/// Returns the nickname of a user.
		/// </summary>
		/// <param name="device_id">The device id for which you want the nickname. Default is this device. Screens don't have nicknames.</param>
		public string GetNickname (int device_id = -1) {
			
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}

			if (device_id == -1) {
				device_id = GetDeviceId ();
			}

			if (GetDevice (device_id) != null) {
				
				try {
					if (GetDevice (device_id) ["nickname"] != null) {
						return (string)GetDevice (device_id) ["nickname"];
					} else {
						return "Guest " + device_id;
					}
				} catch (Exception) { 
					return "Guest " + device_id; 
				}
				
			} else {
				
				if (Settings.debug.warning) {
					Debug.LogWarning ("AirConsole: GetNickname: device_id " + device_id + " not found");
				}
				return null;
			}
			
		}

		/// <summary>
		/// Returns the url to a profile picture of a user.
		/// </summary>
		/// <param name="uid">The uid for which you want a profile picture. Screens don't have profile pictures.</param>
		/// <param name="size">The size of in pixels of the picture. Default is 64.</param>
		public string GetProfilePicture (String uid, int size = 64) {
			return Settings.AIRCONSOLE_PROFILE_PICTURE_URL + uid + "&size=" + size;
		}

		/// <summary>
		/// Returns the url to a profile picture of a user.
		/// </summary>
		/// <param name="device_id">The device id for which you want a profile picture. Defaults to this device. Screens don't have profile pictures.</param>
		/// <param name="size">The size of in pixels of the picture. Default is 64.</param>
		public string GetProfilePicture (int device_id = -1, int size = 64) {
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			if (device_id == -1) {
				device_id = GetDeviceId ();
			}
			if (GetDevice (GetDeviceId ()) != null) {
				
				try {
					return Settings.AIRCONSOLE_PROFILE_PICTURE_URL + (string)GetDevice (device_id) ["uid"] + "&size=" + size;
				} catch (Exception) {
					
					if (Settings.debug.warning) {
						Debug.LogWarning ("AirConsole: GetProfilePicture: can't find profile picture of device_id:" + device_id);
					}
					return null;
				}
				
			} else {
				
				if (Settings.debug.warning) {
					Debug.LogWarning ("AirConsole: GetProfilePicture: " + device_id + " not found");
				}
				return null;
			}
			
		}
		
		/// <summary>
		/// Returns the current time on the game server. 
		/// This allows you to have a synchronized clock: You can send the servertime in a message to know exactly at what point something happened on a device. 
		/// </summary>
		/// <returns> Timestamp in milliseconds.</returns>
		public long GetServerTime () {
			
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			
			return (long)(DateTime.UtcNow.Subtract (new DateTime (1970, 1, 1))).TotalMilliseconds + _server_time_offset;
		}
		
		/// <summary>
		/// Request that all devices return to the AirConsole store.
		/// </summary>
		public void NavigateHome () {
			
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			
			JObject msg = new JObject ();
			msg.Add ("action", "navigateHome");
			
			wsListener.Message (msg);
		}

		/// <summary>
		/// Request that all devices load a game by url. Note that the custom DeviceStates are preserved. 
		/// If you don't want that, override SetCustomDeviceState(null) on every device before calling this function.
		/// </summary>
		public void NavigateTo (string url) {
			
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			
			JObject msg = new JObject ();
			msg.Add ("action", "navigateTo");
			msg.Add ("data", url);
			
			wsListener.Message (msg);
		}
		
		/// <summary>
		/// Sets the custom DeviceState of this device.
		/// </summary>
		/// <param name="data">The custom data to set.</param>
		public void SetCustomDeviceState (object data) {
			
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			JObject msg = new JObject ();
			msg.Add ("action", "setCustomDeviceState");
			msg.Add ("data", JToken.FromObject (data));

			AllocateDeviceSlots (0);
			if (GetDevice (0) == null) {
				_devices [0] = new JObject ();
			}
			_devices [0] ["custom"] = msg ["data"];
			
			wsListener.Message (msg);
		}
		
		/// <summary>
		/// Sets a property in the custom DeviceState of this device.
		/// </summary>
		/// <param name="data">The property name.</param>
		/// <param name="data">The property value.</param>
		public void SetCustomDeviceStateProperty (string key, object value) {
			
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			
			JObject msg = new JObject ();
			msg.Add ("action", "setCustomDeviceStateProperty");
			msg.Add ("key", JToken.FromObject (key));
			msg.Add ("value", JToken.FromObject (value));

			AllocateDeviceSlots (0);
			if (GetDevice (0) == null) {
				_devices [0] = new JObject ();
			}
			
			JToken custom = _devices [0] ["custom"];
			if (custom == null) {
				JObject new_custom = new JObject ();
				_devices [0] ["custom"] = JToken.FromObject (new_custom);
			}
			
			
			_devices [0] ["custom"] [key] = msg ["value"];
			
			wsListener.Message (msg);
		}

		/// <summary>
		/// Requests that AirConsole shows a multiscreen advertisment.
		/// onAdShow is called on all connected devices if an advertisement
		/// is shown (in this event please mute all sounds).
		/// onAdComplete is called on all connected devices when the
		/// advertisement is complete or no advertisement was shown.
		/// </summary>
		public void ShowAd () {
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			
			JObject msg = new JObject ();
			msg.Add ("action", "showAd");
			
			wsListener.Message (msg);
		}

		/// <summary>
		/// Shows or hides the default UI.
		/// </summary>
		/// <param name="visible">Whether to show or hide the default UI.</param>
		public void ShowDefaultUI (bool visible) {
			
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			
			JObject msg = new JObject ();
			msg.Add ("action", "showDefaultUI");
			msg.Add ("data", visible);
			
			wsListener.Message (msg);
		}

		/// <summary>
		/// Returns the device ID of the master controller.
		/// </summary>
		public int GetMasterControllerDeviceId () {
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			List<int> result = GetControllerDeviceIds ();
			if (result.Count > 0) {
				return result [0];
			}
			return 0;
		}
		
		/// <summary>
		/// Returns all controller device ids that have loaded your game.
		/// </summary>
		public List<int> GetControllerDeviceIds () {
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			List<int> result = new List<int> ();
			string game_url = GetGameUrl (_location);
			for (int i = 1; i < _devices.Count; ++i) {
				JToken device = GetDevice (i);
				if (device != null && GetGameUrl ((string)device ["location"]) == game_url) {
					result.Add (i);
				} 
			}
			return result;
		}


		/// <summary>
		/// Returns true if a user is logged in.
		/// </summary>
		public bool IsUserLoggedIn (int device_id = -1) {
				
			if (!IsAirConsoleUnityPluginReady ()) {
						
				throw new NotReadyException ();
						
			}
					
			if (device_id == -1) {
				device_id = GetDeviceId ();
			}
					
			if (GetDevice (device_id) != null) {
						
				try {
					if (GetDevice (device_id) ["auth"] != null) {
						return (bool)GetDevice (device_id) ["auth"];
					}
				} catch (Exception) { 
					return false;
				}
				return false;
			}

			return false;
		}

		/// <summary>
		/// Requests high score data of players (including global high scores and friends). 
		/// Will call onHighScores when data was received.
		/// </summary>
		/// <param name="level_name">The name of the level.</param>
		/// <param name="level_version">The version of the level.</param>
		/// <param name="uids">An array of UIDs of the users should be included in the result. Default is all connected controllers.</param>
		/// <param name="ranks">An array of high score rank types. High score rank types can include data from across the world, only a specific area or a users friends. Valid array entries are "world",  "country",  "region", "city", "friends". Default is ["world"].</param>
		/// <param name="total">Amount of high scores to return per rank type. Default is 8.</param>
		/// <param name="top">Amount of top high scores to return per rank type. top is part of total. Default is 5.</param>
		public void RequestHighScores (string level_name, string level_version, List<string> uids = null, List<string> ranks = null, int total = -1, int top = -1) {
			
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			
			JObject msg = new JObject ();
			msg.Add ("action", "requestHighScores");
			msg.Add ("level_name", level_name);
			msg.Add ("level_version", level_version);

			JArray uidsJArray = null;
			
			if (uids != null) {
				uidsJArray = new JArray();
				foreach (string uid in uids){
					uidsJArray.Add(uid);
				}
				msg.Add ("uids", uidsJArray);
			}

			JArray ranksJArray = null;

			if (ranks != null) {
				ranksJArray = new JArray();
				foreach (string rank in ranks){
					ranksJArray.Add(rank);
				}
				msg.Add ("ranks", ranksJArray);
			}

			if (total != -1) {
				msg.Add ("total", total);
			}

			if (top != -1) {
				msg.Add ("top", top);
			}
			
			wsListener.Message (msg);
		}

		/// <summary>
		/// Stores a high score of the current user on the AirConsole servers. 
		/// High scores may be returned to anyone. Do not include sensitive data. Only updates the high score if it was a higher or same score. 
		/// Calls onHighScoreStored when the request is done.
		/// <param name="level_name">The name of the level the user was playing. This should be a human readable string because it appears in the high score sharing image. You can also just pass an empty string.</param>
		/// <param name="level_version">The version of the level the user was playing. This is for your internal use.</param>
		/// <param name="score">The score the user has achieved</param>
		/// <param name="uid">The UID of the user that achieved the high score.</param>
		/// <param name="data">Custom high score data (e.g. can be used to implement Ghost modes or include data to verify that it is not a fake high score).</param>
		/// <param name="score_string">A short human readable representation of the score. (e.g. "4 points in 3s"). Defaults to "X points" where x is the score converted to an integer.</param>
		/// </summary>
		public void StoreHighScore (string level_name, string level_version, float score, string uid, JObject data = null, string score_string = null) {
			List<String> uids = new List<String> ();
			uids.Add (uid);
			StoreHighScore (level_name, level_version, score, uids, data, score_string);
		}

		/// <summary>
		/// Stores a high score of the current user on the AirConsole servers. 
		/// High scores may be returned to anyone. Do not include sensitive data. Only updates the high score if it was a higher or same score. 
		/// Calls onHighScoreStored when the request is done.
		/// <param name="level_name">The name of the level the user was playing. This should be a human readable string because it appears in the high score sharing image. You can also just pass an empty string.</param>
		/// <param name="level_version">The version of the level the user was playing. This is for your internal use.</param>
		/// <param name="score">The score the user has achieved</param>
		/// <param name="uids">The UIDs of the users that achieved the high score.</param>
		/// <param name="data">Custom high score data (e.g. can be used to implement Ghost modes or include data to verify that it is not a fake high score).</param>
		/// <param name="score_string">A short human readable representation of the score. (e.g. "4 points in 3s"). Defaults to "X points" where x is the score converted to an integer.</param>
		/// </summary>
		public void StoreHighScore (string level_name, string level_version, float score, List<string> uids, JObject data = null, string score_string = null) {

			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			
			JObject msg = new JObject ();
			msg.Add ("action", "storeHighScore");
			msg.Add ("level_name", level_name);
			msg.Add ("level_version", level_version);
			msg.Add ("score", score);


			JArray uidJArray = new JArray();
			foreach (string uid in uids){
				uidJArray.Add(uid);
			}
			msg.Add ("uid", uidJArray);

			if (data != null) {
				msg.Add ("data", data);
			}
			if (score_string != null) {
				msg.Add ("score_string", score_string);
			}
			
			wsListener.Message (msg);
		}

		/// <summary>
		/// Gets thrown when you call an API method before OnReady was called.
		/// </summary>
		public class NotReadyException: SystemException {
			public NotReadyException () : base() {
			}
		}

		/// <summary>
		/// Requests persistent data from the servers.
		/// Will call onPersistentDataLoaded when data was received.
		/// </summary>
		/// <param name="uids">The uids for which you would like to request the persistent data. Default is this device.</param>
		public void RequestPersistentData (List<string> uids = null) { 
			
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}

			JObject msg = new JObject ();
			msg.Add ("action", "requestPersistentData");

			if (uids != null) {
				JArray uidJArray = new JArray();
				foreach (string uid in uids){
					uidJArray.Add(uid);
				}
				
				msg.Add ("uids", uidJArray);
			}

			
			wsListener.Message (msg);
		}

		/// <summary>
		/// Stores a key-value pair persistently on the AirConsole servers.
		/// Storage is per game. Total storage can not exceed 1 MB per game and uid.
		/// Will call onPersistentDataStored when the request is done.
		/// </summary>
		/// <param name="key">The key of the data entry.</param>
		/// <param name="value">The value of the data entry.</param>
		/// <param name="uid">The uid for which the data should be stored. Default is this device.</param>
		public void StorePersistentData (string key, JToken value, string uid = null) {
			
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			
			JObject msg = new JObject ();
			msg.Add ("action", "storePersistentData");
			msg.Add ("key", key);
			msg.Add ("value", value);

			if (uid != null) {
				msg.Add ("uid", uid);
			}
			
			wsListener.Message (msg);
		}

		/// <summary>
		/// Returns true if the device is premium
		/// </summary>
		/// <param name="device_id">The device_id that should be checked. Only controllers can be premium. Default is this device.</param>
		public bool IsPremium(int device_id = -1) {
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}
			
			if (device_id == -1) {
				device_id = GetDeviceId ();
			}
			
			if (GetDevice (device_id) != null) {
				
				try {
					if (GetDevice(device_id)["premium"] != null){
						return (bool)GetDevice (device_id) ["premium"];
					} else {
						return false;
					}
				} catch (Exception) { 
					return false;
				}
				
			} else {
				
				if (Settings.debug.warning) {
					Debug.LogWarning ("AirConsole: IsPremium: device_id " + device_id + " not found");
				}
				return false;
			}
		}

		/// <summary>
		/// Returns all device Ids that are premium.
		/// </summary>
		public List<int> GetPremiumDeviceIds(){
			if (!IsAirConsoleUnityPluginReady ()) {
				
				throw new NotReadyException ();
				
			}

			List<int> result = new List<int>();

			List<int> allControllers = GetControllerDeviceIds ();
			for (int i = 0; i < allControllers.Count; ++i) {
				if (IsPremium(allControllers[i])){
					result.Add(allControllers[i]);
				}
			}

			return result;
		}

		#endregion
		#endif
		
		#region airconsole unity config
		
		public StartMode browserStartMode;
		public UnityEngine.Object controllerHtml;
		public bool autoScaleCanvas = true;
#if UNITY_ANDROID
        public string androidTvGameVersion;
		public AndroidUIResizeMode androidUIResizeMode;
		public Sprite webViewLoadingSprite;
#endif
		
#endregion
#if !DISABLE_AIRCONSOLE
		
#region unity functions

		void Awake () {
			if (instance != this) {
				Destroy (this.gameObject);
			}

			// always set default object name 
			// important for unity webgl communication
			gameObject.name = "AirConsole";
		}

		void Start () {
						
			// application has to run in background
#if UNITY_ANDROID && !UNITY_EDITOR
            Application.runInBackground = false;
#else
			Application.runInBackground = true;
#endif

			// register all incoming events
#if UNITY_ANDROID
            InitWebView();
            wsListener = new WebsocketListener(webViewObject);
            wsListener.onLaunchApp += OnLaunchApp;
            wsListener.onUnityWebviewResize += OnUnityWebviewResize;
			wsListener.onUnityWebviewPlatformReady += OnUnityWebviewPlatformReady;

			SceneManager.sceneLoaded += OnSceneLoaded;
			Screen.sleepTimeout = SleepTimeout.NeverSleep;
#else
			wsListener = new WebsocketListener ();
#endif
			wsListener.onReady += this.OnReady;
			wsListener.onClose += this.OnClose;
			wsListener.onMessage += this.OnMessage;
			wsListener.onDeviceStateChange += OnDeviceStateChange;
			wsListener.onConnect += OnConnect;
			wsListener.onDisconnect += OnDisconnect;
			wsListener.onCustomDeviceStateChange += OnCustomDeviceStateChange;
			wsListener.onDeviceProfileChange += OnDeviceProfileChange;
			wsListener.onAdShow += OnAdShow;
			wsListener.onAdComplete += OnAdComplete;
			wsListener.onGameEnd += OnGameEnd;
			wsListener.onHighScores += OnHighScores;
			wsListener.onHighScoreStored += OnHighScoreStored;
			wsListener.onPersistentDataStored += OnPersistentDataStored;
			wsListener.onPersistentDataLoaded += OnPersistentDataLoaded;
			wsListener.onPremium += OnPremium;


			// check if game is running in webgl build
			if (Application.platform != RuntimePlatform.WebGLPlayer && Application.platform != RuntimePlatform.Android) {

				// start websocket connection
				wsServer = new WebSocketServer (Settings.webSocketPort);
				wsServer.AddWebSocketService<WebsocketListener> (Settings.WEBSOCKET_PATH, () => wsListener);
				wsServer.Start ();

				if (Settings.debug.info) {
					Debug.Log ("AirConsole: Dev-Server started!");
				}

			} else {

				if (Application.platform == RuntimePlatform.WebGLPlayer) {
					// call external javascript init function
					Application.ExternalCall ("onGameReady", this.autoScaleCanvas);
				}
			}
            
		}

		void Update () {

			// dispatch event queue on main unity thread
			while (eventQueue.Count > 0) {
				eventQueue.Dequeue ().Invoke ();
			}
		}

		void OnApplicationQuit () {
			StopWebsocketServer ();
		}

		void OnDisable () {
			StopWebsocketServer ();
		}

#endregion

            #region internal functions

		void OnDeviceStateChange (JObject msg) {
			
			if (msg ["device_id"] == null) {
				return;
			}
			
			try {
				
				int deviceId = (int)msg ["device_id"];
				AllocateDeviceSlots (deviceId);
				JToken deviceData = (JToken)msg ["device_data"];
				if (deviceData != null && deviceData.HasValues) {
					_devices [deviceId] = deviceData;
				} else {
					_devices [deviceId] = null;
				}
				
				if (this.onDeviceStateChange != null) {
					eventQueue.Enqueue (() => this.onDeviceStateChange (deviceId, GetDevice (_device_id)));
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: saved devicestate of " + deviceId);
				}
				
			} catch (Exception e) {
				
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
			
		}
		
		void OnConnect (JObject msg) {
			
			if (msg ["device_id"] == null) {
				return;
			}
			
			try {
				
				int deviceId = (int)msg ["device_id"];
				
				if (this.onConnect != null) {
					eventQueue.Enqueue (() => this.onConnect (deviceId));
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: onConnect " + deviceId);
				}
				
			} catch (Exception e) {
				
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
			
		}
		
		void OnDisconnect (JObject msg) {
			
			if (msg ["device_id"] == null) {
				return;
			}
			
			try {
				
				int deviceId = (int)msg ["device_id"];
				
				if (this.onDisconnect != null) {
					eventQueue.Enqueue (() => this.onDisconnect (deviceId));
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: onDisconnect " + deviceId);
				}
				
			} catch (Exception e) {
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
			
		}
		
		void OnCustomDeviceStateChange (JObject msg) {
			
			if (msg ["device_id"] == null) {
				return;
			}
			
			try {
				
				int deviceId = (int)msg ["device_id"];
				
				if (this.onCustomDeviceStateChange != null) {
					eventQueue.Enqueue (() => this.onCustomDeviceStateChange (deviceId, GetCustomDeviceState (deviceId)));
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: onCustomDeviceStateChange " + deviceId);
				}
				
			} catch (Exception e) {
				
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
			
		}
		
		void OnMessage (JObject msg) {
			
			if (this.onMessage != null) {
				eventQueue.Enqueue (() => this.onMessage ((int)msg ["from"], (JToken)msg ["data"]));
			}
		}
		
		void OnReady (JObject msg) {
			
			// parse server_time_offset
			_server_time_offset = (int)msg ["server_time_offset"];
			
			// parse device_id
			_device_id = (int)msg ["device_id"];
			
			// parse location
			_location = (string)msg ["location"];
			
			// load devices
			_devices.Clear ();
			foreach (JToken data in (JToken)msg["devices"]) {
				JToken assign = data;
				if (data != null && !data.HasValues) {
					assign = null;
				}
				_devices.Add (assign);
			}
			
			if (this.onReady != null) {
				eventQueue.Enqueue (() => this.onReady ((string)msg ["code"]));
			}
		}

		void OnDeviceProfileChange (JObject msg) {
			
			if (msg ["device_id"] == null) {
				return;
			}
			
			try {
				
				int deviceId = (int)msg ["device_id"];
				
				if (this.onDeviceProfileChange != null) {
					eventQueue.Enqueue (() => this.onDeviceProfileChange (deviceId));
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: onDeviceProfileChange " + deviceId);
				}
				
			} catch (Exception e) {
				
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
		}

		void OnAdShow (JObject msg) {
#if UNITY_ANDROID && !UNITY_EDITOR
            webViewObject.SetMargins(0, 0, 0, 0);
#endif
			try {
				
				if (this.onAdShow != null) {
					eventQueue.Enqueue (() => this.onAdShow ());
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: onAdShow");
				}
				
			} catch (Exception e) {
				
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
		}

		void OnAdComplete (JObject msg) {
#if UNITY_ANDROID && !UNITY_EDITOR
            webViewObject.SetMargins(0, 0, 0, Screen.height - webViewHeight);
#endif
			try {
				
				bool adWasShown = (bool)msg ["ad_was_shown"];
				
				if (this.onAdComplete != null) {
					eventQueue.Enqueue (() => this.onAdComplete (adWasShown));
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: onAdComplete");
				}
				
			} catch (Exception e) {
				
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
		}

		void OnGameEnd (JObject msg) {
#if UNITY_ANDROID
			webViewObject.SetMargins(0, 0, 0, 0);
#endif
			try {
				
				if (this.onGameEnd != null) {
					eventQueue.Enqueue (() => this.onGameEnd ());
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: onGameEnd");
				}
				
			} catch (Exception e) {
				
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
		}

		void OnHighScores (JObject msg) {
			try {

				JToken highscores = msg ["highscores"];
				
				if (this.onHighScores != null) {
					eventQueue.Enqueue (() => this.onHighScores (highscores));
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: onHighScores");
				}
				
			} catch (Exception e) {
				
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
		}

		void OnHighScoreStored (JObject msg) {
			try {

				JToken highscore = msg ["highscore"];

				if (highscore != null && !highscore.HasValues) {
					highscore = null;
				} 

				if (this.onHighScoreStored != null) {
					eventQueue.Enqueue (() => this.onHighScoreStored (highscore));
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: onHighScoreStored");
				}
				
			} catch (Exception e) {
				
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
		}

		void OnPersistentDataStored (JObject msg) {
			try {
				
				string uid = (string)msg ["uid"];

				if (this.onPersistentDataStored != null) {
					eventQueue.Enqueue (() => this.onPersistentDataStored (uid));
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: OnPersistentDataStored");
				}
				
			} catch (Exception e) {
				
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
		}

		void OnPersistentDataLoaded (JObject msg) {
			try {
				
				JToken data = msg ["data"];
				
				if (this.onPersistentDataLoaded != null) {
					eventQueue.Enqueue (() => this.onPersistentDataLoaded (data));
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: OnPersistentDataLoaded");
				}
				
			} catch (Exception e) {
				
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
		}

		void OnPremium (JObject msg) {
			try {
				
				int device_id = (int)msg ["device_id"];
				
				if (this.onPremium != null) {
					eventQueue.Enqueue (() => this.onPremium (device_id));
				}
				
				if (Settings.debug.info) {
					Debug.Log ("AirConsole: onPremium");
				}
				
			} catch (Exception e) {
				
				if (Settings.debug.error) {
					Debug.LogError (e.Message);
				}
			}
		}

		[Obsolete("Please use GetServerTime(). This method will be removed in the next version.")]
		public int server_time_offset {
			get { return _server_time_offset; }
		}
		
		[Obsolete("device_id is deprecated, please use GetDeviceId instead. This method will be " +
		          "removed in the next version.")]
		public int device_id {
			get { return GetDeviceId (); }
		}
		
		[Obsolete("Do not use .devices directly. Use the getter and setter functions. Devices in " +
		          "this collection may not have loaded your game yet. This method will be removed in" +
		          "the next version.")]
		public ReadOnlyCollection<JToken> devices {
			get { 
				return _devices.AsReadOnly (); 
			}
		}
		
		// private vars
		private WebSocketServer wsServer;
		private WebsocketListener wsListener;
#if UNITY_ANDROID
        private WebViewObject webViewObject;
		private Canvas webViewLoadingCanvas;
		private UnityEngine.UI.Image webViewLoadingImage;
		private UnityEngine.UI.Image webViewLoadingBG;
		private int webViewHeight;
#endif
		private List<JToken> _devices = new List<JToken> ();
		private int _device_id;
		private int _server_time_offset;
		private string _location;
		private List<int> _players = new List<int> ();
		private readonly Queue<Action> eventQueue = new Queue<Action> ();
		
		// unity singleton handling
		private static AirConsole _instance;

		private void StopWebsocketServer () {
			if (wsServer != null) {
				wsServer.Stop ();
			}
		}

		private void OnClose () {
			_devices.Clear ();
		}

		public static string GetUrl (StartMode mode) {

			switch (mode) {
			case StartMode.VirtualControllers:
				return Settings.AIRCONSOLE_NORMAL_URL;
			case StartMode.Normal:
				return Settings.AIRCONSOLE_URL;
			default:
				return "";
			}
		}

		public void ProcessJS (string data) {
			wsListener.ProcessMessage (data);
		}

		private JToken GetDevice (int deviceId) {
			if (deviceId < _devices.Count && deviceId >= 0) {
				return _devices [deviceId];
			}
			return null;
		}

		private string GetGameUrl (string url) {
			if (url == null) {
				return null;
			}
			url = url.Split ('#') [0];
			url = url.Split ('?') [0];
			if (url.EndsWith ("screen.html")) {
				url = url.Substring (0, url.Length - 11);
			} else if (url.EndsWith ("controller.html")) {
				url = url.Substring (0, url.Length - 15);
			}
			if (url.StartsWith ("https://")) {
				url = "http://" + url.Substring (8);
			}
			return url;
		}

		private void AllocateDeviceSlots (int i) {
			while (i >= _devices.Count) {
				_devices.Add (null);
			}
		}


#if UNITY_ANDROID
        private void InitWebView() {

            if (this.androidTvGameVersion != null && this.androidTvGameVersion != "") {

                if(webViewObject == null) {

                    webViewObject = (new GameObject("WebViewObject")).AddComponent<WebViewObject>();
					GameObject.DontDestroyOnLoad(webViewObject.gameObject);
                    webViewObject.Init((msg) => ProcessJS(msg));

                    string url = Settings.AIRCONSOLE_BASE_URL;
                    url += "client?id=androidunity-" + Settings.VERSION;
                    url += "&game-id=" + Application.bundleIdentifier;
                    url += "&game-version=" + this.androidTvGameVersion;

                    webViewObject.SetMargins(0, Screen.height, 0, -Screen.height);
                    webViewObject.SetVisibility(true);
                    webViewObject.LoadURL(url);

					//Display loading Screen
					webViewLoadingCanvas = (new GameObject("WebViewLoadingCanvas")).AddComponent<Canvas>();
					
					
#if !UNITY_EDITOR
					webViewLoadingCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
					webViewLoadingBG = (new GameObject("WebViewLoadingBG")).AddComponent<UnityEngine.UI.Image>();
					webViewLoadingImage = (new GameObject("WebViewLoadingImage")).AddComponent<UnityEngine.UI.Image>();
					webViewLoadingBG.transform.SetParent(webViewLoadingCanvas.transform, true);
					webViewLoadingImage.transform.SetParent(webViewLoadingCanvas.transform, true);
					webViewLoadingImage.sprite = webViewLoadingSprite;
					webViewLoadingBG.color = Color.black;
					webViewLoadingImage.rectTransform.localPosition = new Vector3 (0, 0, 0);
					webViewLoadingBG.rectTransform.localPosition = new Vector3 (0, 0, 0);
					webViewLoadingImage.rectTransform.sizeDelta = new Vector2 (Screen.width / 2, Screen.height / 2);
					webViewLoadingBG.rectTransform.sizeDelta = new Vector2 (Screen.width, Screen.height);
					webViewLoadingImage.preserveAspect = true;

					if (webViewLoadingSprite == null){
						webViewLoadingImage.sprite = Resources.Load("AirConsoleLogoLoadingScreen", typeof(Sprite)) as Sprite;
					}
#endif
                }

            } else {
                if (Settings.debug.error) {
                    Debug.LogError("AirConsole: for Android builds you need to provide the Game Version Identifier on the AirConsole object in the scene.");
                }
            }
        }

        private void OnLaunchApp(JObject msg) {
            Debug.Log("onLaunchApp");
			string gameId = (string)msg ["game_id"];
			string gameVersion = (string)msg ["game_version"];
			if (gameId != Application.bundleIdentifier || gameVersion != AirConsole.instance.androidTvGameVersion) {
				
				AndroidJavaClass up = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
				AndroidJavaObject ca = up.GetStatic<AndroidJavaObject>("currentActivity");
				AndroidJavaObject packageManager = ca.Call<AndroidJavaObject>("getPackageManager");
				AndroidJavaObject launchIntent = null;
				try {
					launchIntent = packageManager.Call<AndroidJavaObject>("getLeanbackLaunchIntentForPackage", gameId);
				} catch (Exception) {
					Debug.Log("getLeanbackLaunchIntentForPackage for " + gameId + " failed");
				}
				if (launchIntent == null) {
					try {
						launchIntent = packageManager.Call<AndroidJavaObject>("getLaunchIntentForPackage", gameId);
					} catch (Exception) {
						Debug.Log("getLaunchIntentForPackage for " + gameId + " failed");
					}
				}
				if (launchIntent != null && gameId != Application.bundleIdentifier) {
					ca.Call("startActivity", launchIntent);
				} else {
					Application.OpenURL("market://details?id=" + gameId);
				}
				up.Dispose();
				ca.Dispose();
				packageManager.Dispose();
				launchIntent.Dispose();
				System.Diagnostics.Process.GetCurrentProcess().Kill();
			}
        }

        private void OnUnityWebviewResize(JObject msg) {
			Debug.Log("OnUnityWebviewResize");
			if (_devices.Count > 0) {
				Debug.Log("screen device data: " + _devices[0].ToString());
			}
			
            int h = Screen.height;

            if (msg["top_bar_height"] != null) {
                h = (int)msg["top_bar_height"] * 2;
				webViewHeight = h;
			}

            webViewObject.SetMargins(0, 0, 0, Screen.height - h);
			if (androidUIResizeMode == AndroidUIResizeMode.ResizeCamera  || androidUIResizeMode == AndroidUIResizeMode.ResizeCameraAndReferenceResolution) {
				Camera.main.pixelRect = new Rect (0, 0, Screen.width, Screen.height - h);
			}
        }

        private void OnUnityWebviewPlatformReady(JObject msg) {
			GameObject.Destroy (webViewLoadingCanvas.gameObject);
			webViewObject.SetMargins(0, 0, 0, 0);
		}

		private void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) {
			if (instance != this) {
				return;
			}

			if (androidUIResizeMode == AndroidUIResizeMode.ResizeCamera  || androidUIResizeMode == AndroidUIResizeMode.ResizeCameraAndReferenceResolution) {
				Camera.main.pixelRect = new Rect(0, 0, Screen.width, Screen.height - webViewHeight);
			}

			if (androidUIResizeMode == AndroidUIResizeMode.ResizeCameraAndReferenceResolution) {
				UnityEngine.UI.CanvasScaler[] allCanvasScalers = GameObject.FindObjectsOfType<UnityEngine.UI.CanvasScaler> ();
				
				for (int i = 0; i < allCanvasScalers.Length; ++i) {
					allCanvasScalers[i].referenceResolution = new Vector2 (allCanvasScalers[i].referenceResolution.x, allCanvasScalers[i].referenceResolution.y / (allCanvasScalers[i].referenceResolution.y - webViewHeight) * allCanvasScalers[i].referenceResolution.y);
				}
			}
		}
#if !UNITY_EDITOR
		void OnApplicationPause(bool pauseStatus){
			if (pauseStatus) {
				System.Diagnostics.Process.GetCurrentProcess().Kill();
			}
		}
#endif
#endif

#endregion

#endif

	}
}


