using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Networking;
using VelNet;

namespace VELConnect
{
	public class VELConnectManager : MonoBehaviour
	{
		public string velConnectUrl = "http://localhost";
		public static string VelConnectUrl => _instance.velConnectUrl;
		private static VELConnectManager _instance;

		public class State
		{
			public class User
			{
				public string id;
				public string email;
				public string username;
				public string date_created;
				public string last_modified;
				public Dictionary<string, string> data;
			}

			public class Device
			{
				public string hw_id;
				public string os_info;
				public string friendly_name;
				public string modified_by;
				public string current_app;
				public string current_room;
				public int pairing_code;
				public string date_created;
				public string last_modified;
				public Dictionary<string, string> data;

				/// <summary>
				/// Returns the value if it exists, otherwise null
				/// </summary>
				public string TryGetData(string key)
				{
					string val = null;
					return data?.TryGetValue(key, out val) == true ? val : null;
				}
			}

			public class RoomState
			{
				public string error;
				public string id;
				public string category;
				public string date_created;
				public string modified_by;
				public string last_modified;
				public string last_accessed;
				public Dictionary<string, string> data;

				/// <summary>
				/// Returns the value if it exists, otherwise null
				/// </summary>
				public string TryGetData(string key)
				{
					string val = null;
					return data?.TryGetValue(key, out val) == true ? val : null;
				}
			}

			public User user;
			public Device device;
			public RoomState room;
		}

		public State lastState;

		public static Action<State> OnInitialState;
		public static Action<string, string> OnDeviceFieldChanged;
		public static Action<string, string> OnDeviceDataChanged;
		public static Action<string, string> OnRoomDataChanged;

		private static readonly Dictionary<string, List<CallbackListener>> deviceFieldCallbacks =
			new Dictionary<string, List<CallbackListener>>();

		private static readonly Dictionary<string, List<CallbackListener>> deviceDataCallbacks =
			new Dictionary<string, List<CallbackListener>>();

		private static readonly Dictionary<string, List<CallbackListener>> roomDataCallbacks =
			new Dictionary<string, List<CallbackListener>>();

		private struct CallbackListener
		{
			/// <summary>
			/// Used so that other objects don't have to remove listeners themselves
			/// </summary>
			public MonoBehaviour keepAliveObject;

			public Action<string> callback;

			/// <summary>
			/// Sends the first state received from the network or the state at binding time
			/// </summary>
			public bool sendInitialState;
		}

		public static int PairingCode
		{
			get
			{
				Hash128 hash = new Hash128();
				hash.Append(DeviceId);
				// change once a day
				hash.Append(DateTime.UtcNow.DayOfYear);
				// between 1000 and 9999 inclusive (any 4 digit number)
				return Math.Abs(hash.GetHashCode()) % 9000 + 1000;
			}
		}

		private static string DeviceId
		{
			get
			{
#if UNITY_EDITOR
				// allows running multiple builds on the same computer
				// return SystemInfo.deviceUniqueIdentifier + Hash128.Compute(Application.dataPath);
				return SystemInfo.deviceUniqueIdentifier + "_EDITOR";
#else
				return SystemInfo.deviceUniqueIdentifier;
#endif
			}
		}

		private void Awake()
		{
			if (_instance != null) Debug.LogError("VELConnectManager instance already exists", this);
			_instance = this;
		}

		// Start is called before the first frame update
		private void Start()
		{
			SetDeviceField(new Dictionary<string, object>
			{
				{ "current_app", Application.productName },
				{ "pairing_code", PairingCode },
				{ "friendly_name", SystemInfo.deviceName },
			});

			UpdateUserCount();


			StartCoroutine(SlowLoop());

			VelNetManager.OnJoinedRoom += room =>
			{
				SetDeviceField(new Dictionary<string, object>
				{
					{ "current_app", Application.productName },
					{ "current_room", room },
				});
			};
		}


		private void UpdateUserCount(bool leaving = false)
		{
			if (!VelNetManager.InRoom) return;

			VelNetManager.GetRooms(rooms =>
			{
				Dictionary<string, object> postData = new Dictionary<string, object>
				{
					{ "hw_id", DeviceId },
					{ "app_id", Application.productName },
					{ "room_id", VelNetManager.Room ?? "" },
					{ "total_users", rooms.rooms.Sum(r => r.numUsers) - (leaving ? 1 : 0) },
					{ "room_users", VelNetManager.PlayerCount - (leaving ? 1 : 0) },
					{ "version", Application.version },
					{ "platform", SystemInfo.operatingSystem },
				};
				PostRequestCallback(velConnectUrl + "/api/update_user_count", JsonConvert.SerializeObject(postData));
			});
		}

		private IEnumerator SlowLoop()
		{
			while (true)
			{
				try
				{
					GetRequestCallback(velConnectUrl + "/api/device/get_data/" + DeviceId, json =>
					{
						State state = JsonConvert.DeserializeObject<State>(json);
						if (state == null) return;

						bool isInitialState = false;

						// first load stuff
						if (lastState == null)
						{
							try
							{
								OnInitialState?.Invoke(state);
							}
							catch (Exception e)
							{
								Debug.LogError(e);
							}

							isInitialState = true;
							// lastState = state;
							// return;
						}


						if (state.device.modified_by != DeviceId)
						{
							FieldInfo[] fields = state.device.GetType().GetFields();

							foreach (FieldInfo fieldInfo in fields)
							{
								string newValue = fieldInfo.GetValue(state.device) as string;
								string oldValue = lastState != null
									? fieldInfo.GetValue(lastState.device) as string
									: null;
								if (newValue != oldValue)
								{
									try
									{
										if (!isInitialState) OnDeviceFieldChanged?.Invoke(fieldInfo.Name, newValue);
									}
									catch (Exception e)
									{
										Debug.LogError(e);
									}

									// send specific listeners data
									if (deviceFieldCallbacks.ContainsKey(fieldInfo.Name))
									{
										// clear the list of old listeners
										deviceFieldCallbacks[fieldInfo.Name].RemoveAll(e => e.keepAliveObject == null);

										// send the callbacks
										deviceFieldCallbacks[fieldInfo.Name].ForEach(e =>
										{
											if (!isInitialState || e.sendInitialState)
											{
												try
												{
													e.callback(newValue);
												}
												catch (Exception ex)
												{
													Debug.LogError(ex);
												}
											}
										});
									}
								}
							}

							if (state.device.data != null)
							{
								foreach (KeyValuePair<string, string> elem in state.device.data)
								{
									string oldValue = null;
									lastState?.device?.data?.TryGetValue(elem.Key, out oldValue);
									if (elem.Value != oldValue)
									{
										try
										{
											if (!isInitialState) OnDeviceDataChanged?.Invoke(elem.Key, elem.Value);
										}
										catch (Exception ex)
										{
											Debug.LogError(ex);
										}

										// send specific listeners data
										if (deviceDataCallbacks.ContainsKey(elem.Key))
										{
											// clear the list of old listeners
											deviceDataCallbacks[elem.Key].RemoveAll(e => e.keepAliveObject == null);

											// send the callbacks
											deviceDataCallbacks[elem.Key].ForEach(e =>
											{
												if (!isInitialState || e.sendInitialState)
												{
													try
													{
														e.callback(elem.Value);
													}
													catch (Exception ex)
													{
														Debug.LogError(ex);
													}
												}
											});
										}
									}
								}
							}
						}

						if (state.room.modified_by != DeviceId && state.room.data != null)
						{
							foreach (KeyValuePair<string, string> elem in state.room.data)
							{
								string oldValue = null;
								lastState?.room.data.TryGetValue(elem.Key, out oldValue);
								if (elem.Value != oldValue)
								{
									try
									{
										if (!isInitialState) OnRoomDataChanged?.Invoke(elem.Key, elem.Value);
									}
									catch (Exception e)
									{
										Debug.LogError(e);
									}

									// send specific listeners data
									if (roomDataCallbacks.ContainsKey(elem.Key))
									{
										// clear the list of old listeners
										roomDataCallbacks[elem.Key].RemoveAll(e => e.keepAliveObject == null);

										// send the callbacks
										roomDataCallbacks[elem.Key].ForEach(e =>
										{
											if (!isInitialState || e.sendInitialState)
											{
												try
												{
													e.callback(elem.Value);
												}
												catch (Exception ex)
												{
													Debug.LogError(ex);
												}
											}
										});
									}
								}
							}
						}

						lastState = state;
					});
				}
				catch (Exception e)
				{
					Debug.LogError(e);
					// this make sure the coroutine never quits
				}

				yield return new WaitForSeconds(1);
			}
		}

		/// <summary>
		/// Adds a change listener callback to a particular field name within the Device main fields.
		/// </summary>
		public static void AddDeviceFieldListener(string key, MonoBehaviour keepAliveObject, Action<string> callback,
			bool sendInitialState = false)
		{
			if (!deviceFieldCallbacks.ContainsKey(key))
			{
				deviceFieldCallbacks[key] = new List<CallbackListener>();
			}

			deviceFieldCallbacks[key].Add(new CallbackListener()
			{
				keepAliveObject = keepAliveObject,
				callback = callback,
				sendInitialState = sendInitialState
			});

			if (sendInitialState)
			{
				if (_instance != null && _instance.lastState?.device != null)
				{
					if (_instance.lastState.device.GetType().GetField(key)
						    ?.GetValue(_instance.lastState.device) is string val)
					{
						try
						{
							callback(val);
						}
						catch (Exception e)
						{
							Debug.LogError(e);
						}
					}
				}
			}
		}

		/// <summary>
		/// Adds a change listener callback to a particular field name within the Device data JSON.
		/// </summary>
		public static void AddDeviceDataListener(string key, MonoBehaviour keepAliveObject, Action<string> callback,
			bool sendInitialState = false)
		{
			if (!deviceDataCallbacks.ContainsKey(key))
			{
				deviceDataCallbacks[key] = new List<CallbackListener>();
			}

			deviceDataCallbacks[key].Add(new CallbackListener()
			{
				keepAliveObject = keepAliveObject,
				callback = callback,
				sendInitialState = sendInitialState
			});

			if (sendInitialState)
			{
				string val = GetDeviceData(key);
				if (val != null)
				{
					try
					{
						callback(val);
					}
					catch (Exception e)
					{
						Debug.LogError(e);
					}
				}
			}
		}

		/// <summary>
		/// Adds a change listener callback to a particular field name within the Room data JSON.
		/// </summary>
		public static void AddRoomDataListener(string key, MonoBehaviour keepAliveObject, Action<string> callback,
			bool sendInitialState = false)
		{
			if (!roomDataCallbacks.ContainsKey(key))
			{
				roomDataCallbacks[key] = new List<CallbackListener>();
			}

			roomDataCallbacks[key].Add(new CallbackListener()
			{
				keepAliveObject = keepAliveObject,
				callback = callback,
				sendInitialState = sendInitialState
			});

			if (sendInitialState)
			{
				string val = GetRoomData(key);
				if (val != null)
				{
					try
					{
						callback(val);
					}
					catch (Exception e)
					{
						Debug.LogError(e);
					}
				}
			}
		}

		public static string GetDeviceData(string key)
		{
			return _instance != null ? _instance.lastState?.device?.TryGetData(key) : null;
		}

		public static string GetRoomData(string key)
		{
			return _instance != null ? _instance.lastState?.room?.TryGetData(key) : null;
		}


		/// <summary>
		/// Sets data on the device keys themselves
		/// </summary>
		public static void SetDeviceField(Dictionary<string, object> device)
		{
			PostRequestCallback(
				_instance.velConnectUrl + "/api/device/set_data/" + DeviceId,
				JsonConvert.SerializeObject(device),
				new Dictionary<string, string> { { "modified_by", DeviceId } }
			);
		}

		/// <summary>
		/// Sets the 'data' object of the Device table
		/// </summary>
		public static void SetDeviceData(Dictionary<string, string> data)
		{
			PostRequestCallback(
				_instance.velConnectUrl + "/api/device/set_data/" + DeviceId,
				JsonConvert.SerializeObject(new Dictionary<string, object> { { "data", data } }),
				new Dictionary<string, string> { { "modified_by", DeviceId } }
			);
		}

		public static void SetRoomData(Dictionary<string, string> data)
		{
			if (!VelNetManager.InRoom)
			{
				Debug.LogError("Can't set data for a room if you're not in a room.");
				return;
			}

			PostRequestCallback(
				_instance.velConnectUrl + "/api/set_data/" + Application.productName + "_" + VelNetManager.Room,
				JsonConvert.SerializeObject(data),
				new Dictionary<string, string> { { "modified_by", DeviceId } }
			);
		}

		public static void UploadFile(string fileName, byte[] fileData, Action<string> successCallback = null)
		{
			MultipartFormDataContent requestContent = new MultipartFormDataContent();
			ByteArrayContent fileContent = new ByteArrayContent(fileData);

			requestContent.Add(fileContent, "file", fileName);

			Task.Run(async () =>
			{
				HttpResponseMessage r =
					await new HttpClient().PostAsync(_instance.velConnectUrl + "/api/upload_file", requestContent);
				string resp = await r.Content.ReadAsStringAsync();
				Dictionary<string, string> dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(resp);
				successCallback?.Invoke(dict["key"]);
			});
		}


		public static void DownloadFile(string key, Action<byte[]> successCallback = null)
		{
			_instance.StartCoroutine(_instance.DownloadFileCo(key, successCallback));
		}

		private IEnumerator DownloadFileCo(string key, Action<byte[]> successCallback = null)
		{
			UnityWebRequest www = new UnityWebRequest(velConnectUrl + "/api/download_file/" + key);
			www.downloadHandler = new DownloadHandlerBuffer();
			yield return www.SendWebRequest();

			if (www.result != UnityWebRequest.Result.Success)
			{
				Debug.Log(www.error);
			}
			else
			{
				// Show results as text
				Debug.Log(www.downloadHandler.text);

				// Or retrieve results as binary data
				byte[] results = www.downloadHandler.data;

				successCallback?.Invoke(results);
			}
		}


		public static void GetRequestCallback(string url, Action<string> successCallback = null,
			Action<string> failureCallback = null)
		{
			_instance.StartCoroutine(_instance.GetRequestCallbackCo(url, successCallback, failureCallback));
		}

		private IEnumerator GetRequestCallbackCo(string url, Action<string> successCallback = null,
			Action<string> failureCallback = null)
		{
			using UnityWebRequest webRequest = UnityWebRequest.Get(url);
			// Request and wait for the desired page.
			yield return webRequest.SendWebRequest();

			switch (webRequest.result)
			{
				case UnityWebRequest.Result.ConnectionError:
				case UnityWebRequest.Result.DataProcessingError:
				case UnityWebRequest.Result.ProtocolError:
					Debug.LogError(url + ": Error: " + webRequest.error + "\n" + Environment.StackTrace);
					failureCallback?.Invoke(webRequest.error);
					break;
				case UnityWebRequest.Result.Success:
					successCallback?.Invoke(webRequest.downloadHandler.text);
					break;
			}
		}

		public static void PostRequestCallback(string url, string postData, Dictionary<string, string> headers = null,
			Action<string> successCallback = null,
			Action<string> failureCallback = null)
		{
			_instance.StartCoroutine(PostRequestCallbackCo(url, postData, headers, successCallback, failureCallback));
		}


		private static IEnumerator PostRequestCallbackCo(string url, string postData,
			Dictionary<string, string> headers = null, Action<string> successCallback = null,
			Action<string> failureCallback = null)
		{
			UnityWebRequest webRequest = new UnityWebRequest(url, "POST");
			byte[] bodyRaw = Encoding.UTF8.GetBytes(postData);
			UploadHandlerRaw uploadHandler = new UploadHandlerRaw(bodyRaw);
			webRequest.uploadHandler = uploadHandler;
			webRequest.SetRequestHeader("Content-Type", "application/json");
			if (headers != null)
			{
				foreach (KeyValuePair<string, string> keyValuePair in headers)
				{
					webRequest.SetRequestHeader(keyValuePair.Key, keyValuePair.Value);
				}
			}

			yield return webRequest.SendWebRequest();

			switch (webRequest.result)
			{
				case UnityWebRequest.Result.ConnectionError:
				case UnityWebRequest.Result.DataProcessingError:
				case UnityWebRequest.Result.ProtocolError:
					Debug.LogError(url + ": Error: " + webRequest.error + "\n" + Environment.StackTrace);
					failureCallback?.Invoke(webRequest.error);
					break;
				case UnityWebRequest.Result.Success:
					successCallback?.Invoke(webRequest.downloadHandler.text);
					break;
			}

			uploadHandler.Dispose();
			webRequest.Dispose();
		}

		private void OnApplicationFocus(bool focus)
		{
			UpdateUserCount(!focus);
		}
	}
}