#define BUILD

using Cryville.Common;
using Cryville.Common.Plist;
using Cryville.Crtr.Config;
using Cryville.Crtr.Event;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
using UnityEngine.Scripting;
using UnityEngine.UI;
using diag = System.Diagnostics;
using Logger = Cryville.Common.Logger;

namespace Cryville.Crtr {
	public class ChartPlayer : MonoBehaviour {
		Chart chart;
		Skin skin;
		PdtSkin pskin;
		Ruleset ruleset;
		PdtRuleset pruleset;
		Dictionary<string, Texture2D> texs;
		public static Dictionary<string, Cocos2dFrames.Frame> frames;
		List<Cocos2dFrames> plists;

		readonly Queue<string> texLoadQueue = new Queue<string>();
#if UNITY_5_4_OR_NEWER
		DownloadHandlerTexture texHandler;
		UnityWebRequest texLoader = null;
#else
		WWW texLoader = null;
#endif

		EventBus cbus;
		EventBus bbus;
		EventBus tbus;
		EventBus nbus;
		Judge judge;
		bool started = false;

		static bool initialized;
		static Text logs;
		Text status;

		static Vector2 screenSize;
		public static Rect hitRect;
		public static Plane[] frustumPlanes;

		static bool disableGC = true;
		static float clippingDist = 1f;
		static float renderDist = 6f;
		static float renderStep = 0.05f;
		public static float actualRenderStep = 0f;
		static bool autoRenderStep = false;
		public static float soundOffset = 0;
		static float startOffset = 0;
		public static float sv = 16f;

		public static Dictionary<Identifier, MotionRegistry> motionRegistry = new Dictionary<Identifier, MotionRegistry>();

		public static PdtEvaluator etor;
		
		InputProxy inputProxy;

		~ChartPlayer() {
			Dispose();
		}

		public void Dispose() {
			if (loadThread != null) loadThread.Abort();
			if (texLoader != null) texLoader.Dispose();
		}

		#region MonoBehaviour
		void Start() {
			var logobj = GameObject.Find("Logs");
			if (logobj != null)
				logs = logobj.GetComponent<Text>();
			if (!initialized) {
				Game.Init();
				GenericResources.LoadDefault();
				initialized = true;
			}
			OnSettingsUpdate();

			status = GameObject.Find("Status").GetComponent<Text>();

			texHandler = new DownloadHandlerTexture();
#if BUILD
			Play();
#endif

			// Camera.main.RenderToCubemap();
		}

		bool texloaddone;
		diag::Stopwatch texloadtimer = new diag::Stopwatch();
		bool firstFrame;
		double atime0;
		void Update() {
			if (started) GameUpdate();
			else if (loadThread != null) LoadUpdate();
			if (logEnabled) LogUpdate();
			else Game.MainLogger.Enumerate((level, module, msg) => { });
		}
		void GameUpdate() {
				try {
					if (Screen.width != screenSize.x || Screen.height != screenSize.y)
						throw new InvalidOperationException("Window resized while playing");
					float dt = firstFrame
						? 1f / Application.targetFrameRate
						: Time.deltaTime;
					firstFrame = false;
					inputProxy.ForceTick();
					cbus.ForwardByTime(dt);
					bbus.ForwardByTime(dt);
					UnityEngine.Profiling.Profiler.BeginSample("ChartPlayer.Forward");
					UnityEngine.Profiling.Profiler.BeginSample("EventBus.Copy");
					bbus.CopyTo(2, tbus);
					bbus.CopyTo(3, nbus);
					UnityEngine.Profiling.Profiler.EndSample();
					float step = autoRenderStep ? ( firstFrame
						? 1f / Application.targetFrameRate
						: Time.smoothDeltaTime
					) : renderStep;
					actualRenderStep = step;

					nbus.ForwardStepByTime(clippingDist, step);
					nbus.BroadcastEndUpdate();
					nbus.Anchor();

					tbus.ForwardStepByTime(clippingDist, step);
					tbus.ForwardStepByTime(renderDist, step);
					tbus.BroadcastEndUpdate();
					UnityEngine.Profiling.Profiler.EndSample();
				}
				catch (Exception ex) {
					Game.LogException("Game", "An error occured while playing", ex);
				Popup.CreateException(ex);
					Stop();
				}
			}
		void LoadUpdate() {
				if (texLoader != null) {
					string url = texLoader.url;
					string name = StringUtils.TrimExt(url.Substring(url.LastIndexOfAny(new char[] {'/', '\\'}) + 1));
#if UNITY_5_4_OR_NEWER
					if (texHandler.isDone) {
						var tex = texHandler.texture;
						texs.Add(name, tex);
						Logger.Log("main", 0, "Load/MainThread", "Loaded texture {0} ({1} bytes)", name, texLoader.downloadedBytes);
						texLoader.Dispose();
						texHandler.Dispose();
						texLoader = null;
					}
					else if (texLoader.downloadProgress != 0) {
						Logger.Log("main", 0, "Load/MainThread", "Loading texture {0} {1:P0}", name, texLoader.downloadProgress);
					}
#else
					if (texLoader.isDone) {
						var tex = texLoader.texture;
						texs.Add(name, tex);
						Logger.Log("main", 0, "Load/MainThread", "Loaded texture {0} ({1} bytes)", name, texLoader.bytesDownloaded);
						texLoader.Dispose();
						texLoader = null;
					}
					else if (texLoader.progress != 0) {
						Logger.Log("main", 0, "Load/MainThread", "Loading texture {0} {1:P0}", name, texLoader.progress);
					}
#endif
				}
				if (texLoader == null)
					if (texLoadQueue.Count > 0) {
#if UNITY_5_4_OR_NEWER
						texHandler = new DownloadHandlerTexture();
						texLoader = new UnityWebRequest(Game.FileProtocolPrefix + texLoadQueue.Dequeue(), "GET", texHandler, null);
						texLoader.SendWebRequest();
#else
						texLoader = new WWW(Game.FileProtocolPrefix + texLoadQueue.Dequeue());
#endif
					}
					else if (!texloaddone) {
						texloaddone = true;
						texloadtimer.Stop();
						Logger.Log("main", 1, "Load/MainThread", "Main thread done ({0}ms)", texloadtimer.Elapsed.TotalMilliseconds);
					}
				if (!loadThread.IsAlive) {
				if (threadException != null) {
						Logger.Log("main", 4, "Load/MainThread", "Load failed");
						loadThread = null;
					Popup.CreateException(threadException);
#if BUILD
						ReturnToMenu();
#endif
					}
					else if (texLoader == null) {
						Prehandle();
						loadThread = null;
					}
				}
			}
		string timetext = string.Empty;
		void LogUpdate() {
				string _logs = logs.text;
				Game.MainLogger.Enumerate((level, module, msg) => {
					string color;
					switch (level) {
						case 0: color = "#888888"; break;
						case 1: color = "#bbbbbb"; break;
						case 2: color = "#0088ff"; break;
						case 3: color = "#ffff00"; break;
						case 4: color = "#ff0000"; break;
						case 5: color = "#bb0000"; break;
						default: color = "#ff00ff"; break;
					}
					_logs += string.Format(
						"\r\n<color={1}bb><{2}> {3}</color>",
						DateTime.UtcNow.ToString("s"), color, module, msg
					);
				});
				logs.text = _logs.Substring(Mathf.Max(0, _logs.IndexOf('\n', Mathf.Max(0, _logs.Length - 4096))));
				var sttext = string.Format(
					"FPS: i{0:0} / s{1:0}\nSMem: {2:N0} / {3:N0}\nIMem: {4:N0} / {5:N0}",
					1 / Time.deltaTime,
					1 / Time.smoothDeltaTime,
#if UNITY_5_6_OR_NEWER
					UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong(),
					UnityEngine.Profiling.Profiler.GetMonoHeapSizeLong(),
					UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong(),
					UnityEngine.Profiling.Profiler.GetTotalReservedMemoryLong()
#else
					UnityEngine.Profiling.Profiler.GetMonoUsedSize(),
					UnityEngine.Profiling.Profiler.GetMonoHeapSize(),
					UnityEngine.Profiling.Profiler.GetTotalAllocatedMemory(),
					UnityEngine.Profiling.Profiler.GetTotalReservedMemory()
#endif
				);
			sttext += timetext;
			if (judge != null) sttext += "\n== Scores ==\n" + judge.GetFullFormattedScoreString();
			status.text = sttext;
		}
		void OnCameraPostRender(Camera cam) {
			if (started) timetext = string.Format(
					"\nSTime: {0:R}\nATime: {1:R}\nITime: {2:R}",
					cbus.Time,
					Game.AudioClient.Position - atime0,
					inputProxy.GetTimestampAverage()
				);
			else timetext = string.Empty;
			}
		#endregion

		#region Triggers
		private void ReturnToMenu() {
#if UNITY_EDITOR
			Invoke(nameof(_returnToMenu), 4);
#else
			_returnToMenu();
#endif
		}

#pragma warning disable IDE1006
		private void _returnToMenu() {
			GameObject.Find("Master").GetComponent<Master>().ShowMenu();
			GameObject.Destroy(gameObject);
#if UNITY_5_5_OR_NEWER
			SceneManager.UnloadSceneAsync("Play");
#elif UNITY_5_3_OR_NEWER
			SceneManager.UnloadScene("Play");
#endif
		}
#pragma warning restore IDE1006

		bool logEnabled = true;
		public void ToggleLogs() {
			logs.text = "";
			status.text = "";
			logEnabled = !logEnabled;
		}

		public void TogglePlay() {
			if (started) Stop();
			else {
				if (loadThread == null) Play();
				else Logger.Log("main", 2, "Load/MainThread", "The chart is currently loading");
			}
		}
		#endregion

		#region Load
		void Play() {
			disableGC = Settings.Default.DisableGC;
			clippingDist = Settings.Default.BackwardClippingDistance;
			renderDist = Settings.Default.RenderDistance;
			renderStep = Settings.Default.RenderStep;
			actualRenderStep = renderStep;
			autoRenderStep = renderStep == 0;
			soundOffset = Settings.Default.SoundOffset;
			startOffset = Settings.Default.StartOffset;
			sv = Settings.Default.ScrollVelocity;
			firstFrame = true;
			texloaddone = false;
			Game.NetworkTaskWorker.SuspendBackgroundTasks();
			Game.AudioSession = Game.AudioSequencer.NewSession();

			Camera.onPostRender += OnCameraPostRender;

			var hitPlane = new Plane(Vector3.forward, Vector3.zero);
			var r0 = Camera.main.ViewportPointToRay(new Vector3(0, 0, 1));
			float dist;
			hitPlane.Raycast(r0, out dist);
			var p0 = r0.GetPoint(dist);
			var r1 = Camera.main.ViewportPointToRay(new Vector3(1, 1, 1));
			hitPlane.Raycast(r1, out dist);
			var p1 = r1.GetPoint(dist);
			hitRect = new Rect(p0, p1 - p0);

			screenSize = new Vector2(Screen.width, Screen.height);
			frustumPlanes = GeometryUtility.CalculateFrustumPlanes(Camera.main);

			FileInfo chartFile = new FileInfo(
				Game.GameDataPath + "/charts/" + Settings.Default.LoadChart
			);
			FileInfo rulesetFile = new FileInfo(
				Game.GameDataPath + "/rulesets/" + Settings.Default.LoadRuleset
			);
			FileInfo rulesetConfigFile = new FileInfo(
				Game.GameDataPath + "/config/rulesets/" + Settings.Default.LoadRulesetConfig
			);
			FileInfo skinFile = new FileInfo(
				Game.GameDataPath + "/skins/" + Settings.Default.LoadSkin
			);
			loadThread = new Thread(new ParameterizedThreadStart(Load));
			loadThread.Start(new LoadInfo() {
				chartFile = chartFile,
				rulesetFile = rulesetFile,
				rulesetConfigFile = rulesetConfigFile,
				skinFile = skinFile,
			});

			Logger.Log("main", 0, "Load/MainThread", "Loading textures...");
			texloadtimer = new diag::Stopwatch();
			texloadtimer.Start();
			texs = new Dictionary<string, Texture2D>();
			var flist = skinFile.Directory.GetFiles("*.png");
			foreach (FileInfo f in flist) {
				texLoadQueue.Enqueue(f.FullName);
			}
		}

		void Prehandle() {
			try {
				diag::Stopwatch timer = new diag::Stopwatch();
				timer.Reset(); timer.Start();
				Logger.Log("main", 0, "Load/Prehandle", "Prehandling (iteration 3)");
				foreach (var i in plists) i.Init(texs);
				foreach (var t in texs) {
					if (frames.ContainsKey(t.Key)) {
						Logger.Log("main", 3, "Load/Prehandle", "Duplicated texture name: {0}", t.Key);
						continue;
					}
					var f = new Cocos2dFrames.Frame(t.Value);
					f.Init();
					frames.Add(t.Key, f);
				}
				Logger.Log("main", 0, "Load/Prehandle", "Initializing states");
				cbus.BroadcastInit();
				inputProxy.Activate();
				if (logEnabled) ToggleLogs();
				Logger.Log("main", 0, "Load/Prehandle", "Cleaning up");
				GC.Collect();
				if (disableGC) GarbageCollector.GCMode = GarbageCollector.Mode.Disabled;
				timer.Stop();
				Logger.Log("main", 1, "Load/Prehandle", "Prehandling done ({0}ms)", timer.Elapsed.TotalMilliseconds);
				Game.AudioSequencer.Playing = true;
				atime0 = Game.AudioClient.BufferPosition;
				Thread.Sleep((int)((atime0 - Game.AudioClient.Position) * 1000));
				inputProxy.SyncTime(cbus.Time);
				started = true;
			}
			catch (Exception ex) {
				Game.LogException("Load/Prehandle", "An error occured while prehandling the data", ex);
				Popup.CreateException(ex);
				Stop();
			}
		}

		public void Stop() {
			try {
				Logger.Log("main", 1, "Game", "Stopping");
				chart = null;
				Game.AudioSession = Game.AudioSequencer.NewSession();
				if (cbus != null) cbus.Dispose();
				if (bbus != null) bbus.Dispose();
				if (tbus != null) tbus.Dispose();
				if (nbus != null) nbus.Dispose();
				inputProxy.Deactivate();
				foreach (var t in texs) Texture.Destroy(t.Value);
				Camera.onPostRender -= OnCameraPostRender;
				Logger.Log("main", 1, "Game", "Stopped");
			}
			catch (Exception ex) {
				if (!logEnabled) ToggleLogs();
				Game.LogException("Game", "An error occured while stopping", ex);
				Popup.CreateException(ex);
			}
			finally {
				if (started) {
					if (disableGC) GarbageCollector.GCMode = GarbageCollector.Mode.Enabled;
					GC.Collect();
					started = false;
				}
			}
			Game.NetworkTaskWorker.ResumeBackgroundTasks();
#if BUILD
			ReturnToMenu();
#endif
		}

		void OnSettingsUpdate() {
			Application.targetFrameRate = Settings.Default.TargetFrameRate;
			QualitySettings.vSyncCount = Settings.Default.VSync ? 1 : 0;
		}
		#endregion

		#region Threaded
		struct LoadInfo {
			public FileInfo chartFile;
			public FileInfo rulesetFile;
			public FileInfo rulesetConfigFile;
			public FileInfo skinFile;
		}

		Exception threadException;
#if !NO_THREAD
		Thread loadThread = null;
		diag::Stopwatch workerTimer;
#endif
		void Load(object _info) {
			var info = (LoadInfo)_info;
			try {
				workerTimer = new diag::Stopwatch();
				workerTimer.Start();
				RMVPool.Prepare();
				LoadChart(info);
				workerTimer.Stop();
				Logger.Log("main", 1, "Load/WorkerThread", "Worker thread done ({0}ms)", workerTimer.Elapsed.TotalMilliseconds);
			}
			catch (Exception ex) {
				Game.LogException("Load/WorkerThread", "An error occured while loading the data", ex);
				threadException = ex;
			}
		}

		void LoadChart(LoadInfo info) {
			DirectoryInfo dir = info.chartFile.Directory;
			Logger.Log("main", 0, "Load/WorkerThread", "Loading chart: {0}", info.chartFile);

			using (StreamReader reader = new StreamReader(info.chartFile.FullName, Encoding.UTF8)) {
				chart = JsonConvert.DeserializeObject<Chart>(reader.ReadToEnd(), new JsonSerializerSettings() {
					MissingMemberHandling = MissingMemberHandling.Error
				});

				if (chart.format != 2) throw new FormatException("Invalid chart file format version");

				etor = new PdtEvaluator();

				LoadRuleset(info.rulesetFile, info.rulesetConfigFile);
				Logger.Log("main", 0, "Load/WorkerThread", "Applying ruleset (iteration 1)");
				pruleset.PrePatch(chart);

				Logger.Log("main", 0, "Load/WorkerThread", "Batching events");
				var batcher = new EventBatcher(chart);
				batcher.Forward();
				cbus = batcher.Batch();
				Logger.Log("main", 0, "Load/WorkerThread", "Batched {0} event batches", cbus.events.Count);

				LoadSkin(info.skinFile);

				judge = new Judge(pruleset);
				etor.ContextJudge = judge;

				inputProxy = new InputProxy(pruleset, judge);
				inputProxy.LoadFrom(_rscfg.inputs);
				if (!inputProxy.IsCompleted) {
					throw new ArgumentException("Input config not completed\nPlease complete the input settings");
				}

				cbus.AttachSystems(pskin, judge);
				Logger.Log("main", 0, "Load/WorkerThread", "Attaching handlers");
				var ch = new ChartHandler(chart, dir);
				cbus.RootState.AttachHandler(ch);
				foreach (var gs in cbus.RootState.Children) {
					var gh = new GroupHandler((Chart.Group)gs.Key, ch);
					gs.Value.AttachHandler(gh);
					foreach (var ts in gs.Value.Children) {
						ContainerHandler th;
						if (ts.Key is Chart.Note) {
							th = new NoteHandler(gh, (Chart.Note)ts.Key, pruleset, judge);
						}
						else {
							th = new TrackHandler(gh, (Chart.Track)ts.Key);
						}
						ts.Value.AttachHandler(th);
					}
				}
				Logger.Log("main", 0, "Load/WorkerThread", "Prehandling (iteration 1)");
				cbus.Clone(16).Forward();
				Logger.Log("main", 0, "Load/WorkerThread", "Patching events");
				cbus.DoPatch();
				Logger.Log("main", 0, "Load/WorkerThread", "Prehandling (iteration 2)");
				cbus.Clone(17).Forward();

				Logger.Log("main", 0, "Load/WorkerThread", "Cloning states (type 1)");
				bbus = cbus.Clone(1, -clippingDist);
				Logger.Log("main", 0, "Load/WorkerThread", "Cloning states (type 2)");
				tbus = bbus.Clone(2);
				Logger.Log("main", 0, "Load/WorkerThread", "Cloning states (type 3)");
				nbus = bbus.Clone(3);
			}
		}

		RulesetConfig _rscfg;
		void LoadRuleset(FileInfo file, FileInfo cfgfile) {
			DirectoryInfo dir = file.Directory;
			Logger.Log("main", 0, "Load/WorkerThread", "Loading ruleset: {0}", file);
			using (StreamReader reader = new StreamReader(file.FullName, Encoding.UTF8)) {
				ruleset = JsonConvert.DeserializeObject<Ruleset>(reader.ReadToEnd(), new JsonSerializerSettings() {
					MissingMemberHandling = MissingMemberHandling.Error
				});
				if (ruleset.format != 1) throw new FormatException("Invalid ruleset file version");
				ruleset.LoadPdt(dir);
				pruleset = ruleset.Root;
				if (!cfgfile.Exists) throw new FileNotFoundException("Ruleset config not found\nPlease open the config to generate");
				using (StreamReader cfgreader = new StreamReader(cfgfile.FullName, Encoding.UTF8)) {
					_rscfg = JsonConvert.DeserializeObject<RulesetConfig>(cfgreader.ReadToEnd(), new JsonSerializerSettings() {
						MissingMemberHandling = MissingMemberHandling.Error
					});
				}
				pruleset.Optimize(etor);
			}
		}

		void LoadSkin(FileInfo file) {
			DirectoryInfo dir = file.Directory;
			Logger.Log("main", 0, "Load/WorkerThread", "Loading skin: {0}", file);
			using (StreamReader reader = new StreamReader(file.FullName, Encoding.UTF8)) {
				skin = JsonConvert.DeserializeObject<Skin>(reader.ReadToEnd(), new JsonSerializerSettings() {
					MissingMemberHandling = MissingMemberHandling.Error
				});
				if (skin.format != 1) throw new FormatException("Invalid skin file version");
				skin.LoadPdt(dir);
				pskin = skin.Root;
				pskin.Optimize(etor);
			}
			plists = new List<Cocos2dFrames>();
			frames = new Dictionary<string, Cocos2dFrames.Frame>();
			foreach (FileInfo f in file.Directory.GetFiles("*.plist")) {
				var pobj = PlistConvert.Deserialize<Cocos2dFrames>(f.FullName);
				plists.Add(pobj);
				foreach (var i in pobj.frames)
					frames.Add(StringUtils.TrimExt(i.Key), i.Value);
			}
		}
		#endregion
	}
}
