using Cryville.Common;
using Cryville.Common.Network.Http11;
using Cryville.Common.Unity;
using Cryville.Crtr.UI;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
using System.Runtime.InteropServices;
#endif
using System.Text;
using System.Threading;
using UnityEngine;
using Logger = Cryville.Common.Logging.Logger;
using ThreadPriority = System.Threading.ThreadPriority;

namespace Cryville.Crtr.Network {
	public class UpdateChecker : MonoBehaviour {
		string _currentVersion;
		Thread _thread;
#pragma warning disable IDE0044
		bool _shutdown;
#pragma warning restore IDE0044
		void Start() {
			_currentVersion = Application.version;
			_thread = new Thread(ThreadLogic) { IsBackground = true, Priority = ThreadPriority.BelowNormal };
			_thread.Start();
		}
		void Update() {
			if (!_thread.IsAlive) {
				enabled = false;
				if (_shutdown) Application.Quit();
			}
		}
		static readonly Uri BaseUri = new Uri("https://www.cryville.world/api/crtr/index");
		List<VersionInfo> _versions;
		void ThreadLogic() {
			try {
				CheckVersion();
				Logger.Log("main", 0, "Network", "Update checker exited normally");
			}
			catch (Exception ex) {
				Logger.Log("main", 4, "Network", "An error occurred while checking for update: {0}", ex);
				Dialog.Show(null, "Failed to check for update.");
			}
		}
		void CheckVersion() {
			using (var client = new Https11Client(BaseUri)) {
				client.Connect();
				using (var response = client.Request("GET", new Uri(BaseUri, "versions"))) {
					var data = Encoding.UTF8.GetString(response.MessageBody.ReadToEnd());
					_versions = JsonConvert.DeserializeObject<List<VersionInfo>>(data, Game.GlobalJsonSerializerSettings);
				}
			}
			var availableVersions = _versions.Where(v => v.platforms.ContainsKey(PlatformConfig.Name)).ToArray();
			var versionIndex = new Dictionary<string, int>(availableVersions.Length);
			for (int i = 0; i < availableVersions.Length; i++) versionIndex.Add(availableVersions[i].name, i);
			var currentVersion = availableVersions.Where(v => v.name == _currentVersion).SingleOrDefault();
			var latestVersion = availableVersions.Last();
			if (currentVersion == null) {
				Dialog.Show(null, string.Format("You are playing an unknown version of Cosmo Resona: {0}\nThe latest version is: {1}", _currentVersion, latestVersion.name));
				return;
			}
			if (latestVersion.name == _currentVersion) return;
			var latestResources = latestVersion.platforms[PlatformConfig.Name].resources;
			VersionResourceInfo fullPackage = null;
			if (latestResources != null) {
				fullPackage = (from r in latestResources where r.upstream == null select r).SingleOrDefault();
			}
			if (fullPackage == null) {
				Dialog.Show(null, string.Format("A new version is present: {0}\nUpdate is not available.", latestVersion.name));
				return;
			}
			long totalDiffSize = 0;
			int searchIndex = versionIndex[latestVersion.name], targetIndex = versionIndex[currentVersion.name];
			var stream = new List<VersionResourceInfo>();
			while (searchIndex != targetIndex) {
				var searchVersion = availableVersions[searchIndex];
				VersionResourceInfo matchedUpstream = null;
				var resources = searchVersion.platforms[PlatformConfig.Name].resources;
				if (resources == null) {
					totalDiffSize = 0;
					break;
				}
				foreach (var r in resources) {
					if (r.upstream == null) continue;
					var upstreamIndex = versionIndex[r.upstream];
					if (upstreamIndex >= targetIndex && upstreamIndex < searchIndex) {
						matchedUpstream = r;
						searchIndex = upstreamIndex;
					}
				}
				if (matchedUpstream != null) {
					if (matchedUpstream.external) {
						totalDiffSize = 0;
						Dialog.ShowAndWait("An error occurred while checking for update.\nPlease report this to the developers.");
						throw new InvalidOperationException("Diff package is external, which is not expected");
					}
					stream.Add(matchedUpstream);
					totalDiffSize += matchedUpstream.size;
					if (totalDiffSize >= fullPackage.size) {
						totalDiffSize = 0;
						break;
					}
				}
				else {
					totalDiffSize = 0;
					break;
				}
			}
			if (totalDiffSize == 0) {
				// TODO Check if external
				if (Dialog.ShowAndWait(string.Format("A new version is available: {0}\nYou have to download the full package.\nOpen the download link now?", latestVersion.name), "Yes", "No") == 0) {
					UrlOpener.OpenThreaded(fullPackage.url);
				}
			}
			else {
				if (Dialog.ShowAndWait(string.Format("A new version is available: {0}\nDo you want to update?", latestVersion.name), "Yes", "No") == 0) {
					var diffPaths = new List<string>();
					var updateDir = Directory.CreateDirectory(Path.Combine(Game.GameDataPath, "update")).FullName;
					foreach (var diff in stream) {
						string path = Path.Combine(updateDir, StringUtils.EscapeFileName(diff.upstream));
						diffPaths.Add(path);
						Download(diff, path);
					}
					while (Dialog.ShowAndWait("The new version has been downloaded.\nUpdate now?\n(The game will be shut down)", "OK", "Later") == 1)
						Thread.Sleep(60000);
					ExecuteUpdate(diffPaths);
				}
			}
		}
		void Download(VersionResourceInfo diff, string path) {
			var uri = new Uri(diff.url);
			using (var client = new Https11Client(uri)) {
				client.Connect();
				using (var response = client.Request("GET", uri)) {
					var data = response.MessageBody.ReadToEnd();
					using (var file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) {
						file.Write(data);
					}
				}
			}
		}
		void ExecuteUpdate(List<string> diffPaths) {
#if UNITY_EDITOR
			Dialog.Show(null, "Could not update the game in editor");
#elif UNITY_STANDALONE_WIN
			var gameDir = new DirectoryInfo(Game.UnityDataPath).Parent;
			var exe = new FileInfo(Path.Combine(gameDir.FullName, System.Diagnostics.Process.GetCurrentProcess().ProcessName + ".exe"));
			var updateDir = Path.Combine(Game.GameDataPath, "update");
			File.Copy(Path.Combine(Game.UnityDataPath, "Plugins", "x86_64", "hpatchz.dll"), Path.Combine(updateDir, "hpatchz.exe"), true);
			var batchPath = Path.Combine(updateDir, "update.bat");
			var oldPath = Path.Combine(updateDir, "old");
			var newPath = Path.Combine(updateDir, "new");
			using (var batch = new StreamWriter(batchPath, false, new UTF8Encoding(false))) {
				batch.WriteLine("@echo off\r\ntitle [Updater] Cosmo Resona\r\necho Waiting for the game to shut down...\r\n:pending\r\ntasklist /fi \"ImageName eq {0}\" | find /i \"{0}\" >nul\r\nif \"%ERRORLEVEL%\" == \"0\" goto pending\r\necho Copying old data...\r\nrmdir /s /q {2}\r\nxcopy \"{1}\" \"{2}\" /s /e /h /i /q /y\r\nif not \"%ERRORLEVEL%\" == \"0\" goto error", exe.Name, gameDir, oldPath);
				for (int i = 0; i < diffPaths.Count; i++) {
					string path = diffPaths[i];
					batch.WriteLine("echo Applying patch... ({0}/{1})\r\nhpatchz -f \"{2}\" \"{3}\" \"{4}\"\r\nif not \"%ERRORLEVEL%\" == \"0\" goto error\r\nrmdir /s /q \"{2}\"\r\nmove /y \"{4}\" \"{2}\"\r\nif not \"%ERRORLEVEL%\" == \"0\" goto error", i + 1, diffPaths.Count, oldPath, path, newPath);
				}
				batch.WriteLine("echo Copying new data...\r\nrmdir /s /q {1}\r\nmove /y \"{0}\" \"{1}\"\r\nif not \"%ERRORLEVEL%\" == \"0\" goto error\r\nrmdir /s /q \"{2}\"\r\necho Update succeeded\r\npause\r\nexit\r\n:error\r\necho Update failed\r\npause\r\nexit", oldPath, gameDir, updateDir);
			}
			ShellExecute(IntPtr.Zero, "open", batchPath, null, Path.Combine(updateDir), 1);
			_shutdown = true;
#elif UNITY_ANDROID
			// TODO
#else
#error No update logic for the selected platform.
#endif
		}
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
		[DllImport("shell32.dll")]
		static extern int ShellExecute(IntPtr hwnd, [MarshalAs(UnmanagedType.LPStr)] string lpOperation, [MarshalAs(UnmanagedType.LPStr)] string lpFile, [MarshalAs(UnmanagedType.LPStr)] string lpParameters, [MarshalAs(UnmanagedType.LPStr)] string lpDirectory, int nShowCmd);
#endif
		class VersionInfo {
			[JsonRequired]
			public string name;
			public string type;
			public Dictionary<string, PlatformVersionInfo> platforms;
		}
		class PlatformVersionInfo {
			public List<VersionResourceInfo> resources;
		}
		class VersionResourceInfo {
			public bool external;
			public string upstream;
			[JsonRequired]
			public string url;
			[JsonRequired]
			public long size;
		}
	}
}
