using Cryville.Common;
using Cryville.Common.Buffers;
using Cryville.Crtr.Config;
using Cryville.Crtr.Event;
using Cryville.Crtr.Ruleset;
using Cryville.Crtr.Skin;
using Cryville.Crtr.UI;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Formatting;
using System.Threading;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
using UnityEngine.Scripting;
using Coroutine = Cryville.Common.Coroutine;
using Logger = Cryville.Common.Logging.Logger;
using Stopwatch = System.Diagnostics.Stopwatch;

namespace Cryville.Crtr {
	public class ChartPlayer : MonoBehaviour {
		#region Fields
		Chart chart;
		SkinDefinition skin;
		public static PdtSkin pskin;
		RulesetDefinition ruleset;
		PdtRuleset pruleset;
		Dictionary<string, Texture2D> texs;
		public static Dictionary<string, SpriteFrame> frames;

		EventBus cbus;
		EventBus bbus;
		EventBus tbus;
		EventBus nbus;
		InputProxy inputProxy;
		Judge judge;
		public static EffectManager effectManager;
		bool started = false;

		static bool initialized;
		TextMeshProUGUI logs;
		TextMeshProUGUI status;

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

		RulesetConfig _rscfg;
		static bool disableGC = true;
		static float clippingDist = 1f;
		static float renderDist = 6f;
		static double renderStep = 0.05;
		public static double actualRenderStep = 0;
		static bool autoRenderStep = false;
		public static float graphicalOffset = 0;
		public static float soundOffset = 0;
		static float startOffset = 0;
		public static int areaJudgePrecision = 16;
		public static float sv = 16f;

		public static Dictionary<Identifier, MotionRegistry> motionRegistry;
		#endregion

		#region MonoBehaviour
		void Start() {
			d_addLogEntry = new Action<int, string, string>(AddLogEntry);

			var logobj = GameObject.Find("Logs");
			if (logobj != null)
				logs = logobj.GetComponent<TextMeshProUGUI>();
			if (!initialized) {
				Game.Init();
				BuiltinResources.LoadDefault();
				initialized = true;
			}
			OnSettingsUpdate();

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

			try {
				Play();
			}
			catch (Exception ex) {
				Game.LogException("Load/WorkerThread", "An error occurred while loading the data", ex);
				Popup.CreateException(ex);
				ReturnToMenu();
			}

			// Camera.main.RenderToCubemap();
		}

		void OnDestroy() {
			if (cbus != null) cbus.Dispose();
			if (bbus != null) bbus.Dispose();
			if (tbus != null) tbus.Dispose();
			if (nbus != null) nbus.Dispose();
			if (loadThread != null) loadThread.Abort();
			if (inputProxy != null) inputProxy.Dispose();
			if (texs != null) foreach (var t in texs) Texture.Destroy(t.Value);
			GC.Collect();
		}

		Coroutine texLoader;
		bool texloaddone;
		Coroutine prehandler;
		int forceSyncFrames;
		double atime0;
		void Update() {
			if (started) GameUpdate();
			else if (prehandler != null) {
				try {
					if (!prehandler.Tick(1.0 / Application.targetFrameRate)) {
						prehandler = null;
						started = true;
					}
				}
				catch (Exception ex) {
					Game.LogException("Load/Prehandle", "An error occurred while prehandling the data", ex);
					Popup.CreateException(ex);
					prehandler = null;
					Stop();
				}
			}
			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");
				double dt, step;
				if (forceSyncFrames != 0) {
					forceSyncFrames--;
					double target = Game.AudioClient.Position - atime0;
					dt = target - cbus.Time - graphicalOffset;
					step = autoRenderStep ? 1f / Application.targetFrameRate : renderStep;
					inputProxy.SyncTime(target);
				}
				else {
					dt = Time.deltaTime;
					step = autoRenderStep ? Time.smoothDeltaTime : renderStep;
				}
				inputProxy.ForceTick();
				if (paused) return;
				cbus.ForwardByTime(dt);
				bbus.ForwardByTime(dt);
				UnityEngine.Profiling.Profiler.BeginSample("ChartPlayer.Forward");
				UnityEngine.Profiling.Profiler.BeginSample("EventBus.Copy");
				bbus.CopyTo(tbus);
				bbus.CopyTo(nbus);
				UnityEngine.Profiling.Profiler.EndSample();
				actualRenderStep = step;

				nbus.PreAnchor();
				nbus.StripTempEvents();
				nbus.ForwardStepByTime(clippingDist, step);
				nbus.EndPreGraphicalUpdate();
				nbus.Anchor();

				tbus.StripTempEvents();
				tbus.ForwardStepByTime(clippingDist, step);
				tbus.ForwardStepByTime(renderDist, step);
				tbus.EndGraphicalUpdate();
				UnityEngine.Profiling.Profiler.EndSample();

				effectManager.Tick(cbus.Time);
			}
			catch (Exception ex) {
				Game.LogException("Game", "An error occurred while playing", ex);
				Popup.CreateException(ex);
				Stop();
			}
		}
		void LoadUpdate() {
			if (!texloaddone) texLoader.Tick(1.0 / Application.targetFrameRate);
			if (!loadThread.IsAlive) {
				if (threadException != null) {
					Logger.Log("main", 4, "Load/MainThread", "Load failed");
					loadThread = null;
					Popup.CreateException(threadException);
					ReturnToMenu();
				}
				else if (texloaddone) {
					if (texLoader == null) Stop();
					else {
						prehandler = new Coroutine(Prehandle());
						texLoader = null;
					}
					loadThread = null;
				}
			}
		}
		readonly TargetString statusstr = new TargetString();
		readonly StringBuffer statusbuf = new StringBuffer();
		readonly TargetString logsstr = new TargetString();
		readonly StringBuffer logsbuf = new StringBuffer();
		readonly List<string> logEntries = new List<string>();
		int logsLength = 0;
		Action<int, string, string> d_addLogEntry;
		void AddLogEntry(int level, string module, string 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;
			}
			var l = string.Format(
				"\n<color={1}bb><{2}> {3}</color>",
				DateTime.UtcNow.ToString("s"), color, module, msg
			);
			logEntries.Add(l);
			logsLength += l.Length;
		}
		void LogUpdate() {
			logsbuf.Clear();
			Game.MainLogger.Enumerate(d_addLogEntry);
			while (logsLength >= 4096) {
				logsLength -= logEntries[0].Length;
				logEntries.RemoveAt(0);
			}
			foreach (var l in logEntries) {
				logsbuf.Append(l);
			}
			logsstr.Length = logsbuf.Count;
			var larr = logsstr.TrustedAsArray();
			logsbuf.CopyTo(0, larr, 0, logsbuf.Count);
			logs.SetText(larr, 0, logsbuf.Count);

			statusbuf.Clear();
			statusbuf.AppendFormat(
				"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
			);
			if (MotionNodePool.Shared != null) {
				statusbuf.AppendFormat(
					"\nPools: RMV {0}, MC {1}, MN {2}",
					RMVPool.Shared.RentedCount,
					MotionCachePool.Shared.RentedCount,
					MotionNodePool.Shared.RentedCount
				);
			}
			if (texLoader != null) statusbuf.AppendFormat("\n(Loading textures) Progress: {0:P}", texLoader.Progress);
			if (loadThread != null) statusbuf.AppendFormat("\n(Loading files) Progress: {0:P}", loadPregress);
			if (prehandler != null) statusbuf.AppendFormat("\n(Prehandling) Progress: {0:P}", prehandler.Progress);
			if (started) {
				statusbuf.AppendFormat(
					"\nStates: c{0} / b{1}",
					cbus.ActiveStateCount, bbus.ActiveStateCount
				);
				var aTime = Game.AudioClient.Position - atime0;
				var iTime = inputProxy.GetTimestampAverage();
				statusbuf.AppendFormat(
					"\nSTime: {0:G9}s {5} {6}\nATime: {1:G9}s ({3:+0.0ms;-0.0ms;0}) {5} {6}\nITime: {2:G9}s ({4:+0.0ms;-0.0ms;0}) {5} {7}",
					cbus.Time, aTime, iTime,
					(aTime - cbus.Time) * 1e3,
					(iTime - cbus.Time) * 1e3,
					forceSyncFrames != 0 ? "(force sync)" : "",
					paused ? "(paused)" : "",
					paused ? "(semi-locked)" : ""
				);
				if (judge != null) {
					statusbuf.Append("\n== Scores ==\n");
					var fullScoreStr = judge.GetFullFormattedScoreString();
					statusbuf.Append(fullScoreStr.TrustedAsArray(), 0, fullScoreStr.Length);
				}
			}
			statusstr.Length = statusbuf.Count;
			var sarr = statusstr.TrustedAsArray();
			statusbuf.CopyTo(0, sarr, 0, statusbuf.Count);
			status.SetText(sarr, 0, statusbuf.Count);
		}
		#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
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
			DiscordController.Instance.SetIdle();
#endif
		}
#pragma warning restore IDE1006

		bool logEnabled = true;
		public void ToggleLogs() {
			logEntries.Clear();
			logsLength = 0;
			logs.text = "";
			status.SetText("");
			logEnabled = !logEnabled;
		}

		public void TogglePlay() {
			if (started) Stop();
			else if (prehandler != null) {
				prehandler = null;
				Stop();
			}
			else if (texLoader != null || loadThread != null) {
				texloaddone = true;
				texLoader = null;
				if (loadThread.IsAlive) {
					Logger.Log("main", 2, "Game", "Stop requested while the chart is loading. Waiting for the loading thread to exit...");
				}
			}
			else {
				Play();
			}
		}

		bool paused = false;
		public void TogglePause() {
			paused = !paused;
			if (!paused) {
				forceSyncFrames = Settings.Default.ForceSyncFrames;
				Game.AudioClient.Start();
				inputProxy.UnlockTime();
#if !UNITY_ANDROID || UNITY_EDITOR
				DiscordController.Instance.SetResume(cbus.Time);
#endif
			}
			else {
				Game.AudioClient.Pause();
				inputProxy.LockTime();
#if !UNITY_ANDROID || UNITY_EDITOR
				DiscordController.Instance.SetPaused();
#endif
			}
		}
#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;
			graphicalOffset = Settings.Default.GraphicalOffset;
			soundOffset = Settings.Default.SoundOffset;
			startOffset = Settings.Default.StartOffset;
			areaJudgePrecision = 1 << Settings.Default.AreaJudgePrecision;
			forceSyncFrames = Settings.Default.ForceSyncFrames;
			texloaddone = false;
			Game.SuspendBackgroundTasks();
			Game.AudioSession = Game.AudioSequencer.NewSession();

			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
			);
			if (!rulesetFile.Exists) throw new FileNotFoundException("Ruleset for the chart not found\nMake sure you have imported the ruleset");

			FileInfo rulesetConfigFile = new FileInfo(
				Game.GameDataPath + "/config/rulesets/" + Settings.Default.LoadRulesetConfig
			);
			if (!rulesetConfigFile.Exists) throw new FileNotFoundException("Ruleset config not found\nPlease open the config to generate");
			using (StreamReader cfgreader = new StreamReader(rulesetConfigFile.FullName, Encoding.UTF8)) {
				_rscfg = JsonConvert.DeserializeObject<RulesetConfig>(cfgreader.ReadToEnd(), new JsonSerializerSettings() {
					MissingMemberHandling = MissingMemberHandling.Error
				});
			}
			sv = _rscfg.generic.ScrollVelocity;
			soundOffset += _rscfg.generic.SoundOffset;

			FileInfo skinFile = new FileInfo(
				string.Format("{0}/skins/{1}/{2}/.umgs", Game.GameDataPath, rulesetFile.Directory.Name, _rscfg.generic.Skin)
			);
			if (!skinFile.Exists) throw new FileNotFoundException("Skin not found\nPlease specify an available skin in the config");
			using (StreamReader reader = new StreamReader(skinFile.FullName, Encoding.UTF8)) {
				skin = JsonConvert.DeserializeObject<SkinDefinition>(reader.ReadToEnd(), new JsonSerializerSettings() {
					MissingMemberHandling = MissingMemberHandling.Error
				});
				if (skin.format != SkinDefinition.CURRENT_FORMAT) throw new FormatException("Invalid skin file version");
			}

			loadThread = new Thread(new ParameterizedThreadStart(Load));
			loadThread.Start(new LoadInfo() {
				chartFile = chartFile,
				rulesetFile = rulesetFile,
				skinFile = skinFile,
			});

			Logger.Log("main", 0, "Load/MainThread", "Loading textures...");
			frames = new Dictionary<string, SpriteFrame>();
			texs = new Dictionary<string, Texture2D>();
			var skinDir = skinFile.Directory.FullName;
			var texLoadQueue = new List<string>();
			foreach (var f in skin.frames) {
				texLoadQueue.Add(Path.Combine(skinDir, f));
			}
			texLoader = new Coroutine(LoadTextures(texLoadQueue));
		}

		IEnumerator<float> LoadTextures(List<string> queue) {
			Stopwatch stopwatch = new Stopwatch();
			stopwatch.Start();
#if UNITY_5_4_OR_NEWER
			DownloadHandlerTexture texHandler = null;
			UnityWebRequest texLoader = null;
#else
			WWW texLoader = null;
#endif
			for (int i = 0; i < queue.Count; i++) {
#if UNITY_5_4_OR_NEWER
				texHandler = new DownloadHandlerTexture();
				texLoader = new UnityWebRequest(PlatformConfig.FileProtocolPrefix + queue[i], "GET", texHandler, null);
				texLoader.SendWebRequest();
#else
				texLoader = new WWW(Game.FileProtocolPrefix + queue[i]);
#endif
				while (!texLoader.isDone) yield return (float)i / queue.Count;
#if UNITY_5_4_OR_NEWER
				if (texHandler.isDone) {
					string url = texLoader.url;
					string name = StringUtils.TrimExt(url.Substring(url.LastIndexOfAny(new char[] {'/', '\\'}) + 1));
					var tex = texHandler.texture;
					tex.wrapMode = TextureWrapMode.Clamp;
					if (frames.ContainsKey(name)) {
						Logger.Log("main", 3, "Load/Prehandle", "Duplicated texture name: {0}", name);
					}
					else {
						frames.Add(name, new SpriteFrame(tex));
					}
					texs.Add(name, tex);
				}
				else {
					Logger.Log("main", 4, "Load/Prehandle", "Unable to load texture: {0}", name);
				}
				texLoader.Dispose();
				texHandler.Dispose();
#else
				string url = texLoader.url;
				string name = StringUtils.TrimExt(url.Substring(url.LastIndexOfAny(new char[] {'/', '\\'}) + 1));
				var tex = texLoader.texture;
				tex.wrapMode = TextureWrapMode.Clamp;
				texs.Add(name, tex);
				texLoader.Dispose();
				texLoader = null;
#endif
			}
			texloaddone = true;
			stopwatch.Stop();
			Logger.Log("main", 1, "Load/MainThread", "Main thread done ({0}ms)", stopwatch.Elapsed.TotalMilliseconds);
			yield return 1;
		}

		IEnumerator<float> Prehandle() {
			Stopwatch timer = new Stopwatch();
			timer.Reset(); timer.Start();
			Logger.Log("main", 0, "Load/Prehandle", "Prehandling (iteration 2)"); yield return 0;
			cbus.BroadcastPreInit();
			Logger.Log("main", 0, "Load/Prehandle", "Prehandling (iteration 3)"); yield return 0;
			using (var pbus = cbus.Clone(17)) {
				while (pbus.Time != double.PositiveInfinity) {
					pbus.ForwardOnce();
					yield return (float)pbus.EventId / pbus.EventCount;
				}
			}
			Logger.Log("main", 0, "Load/Prehandle", "Prehandling (iteration 4)"); yield return 1;
			cbus.BroadcastPostInit();
			Logger.Log("main", 0, "Load/Prehandle", "Seeking to start offset"); yield return 1;
			cbus.ForwardByTime(startOffset);
			bbus.ForwardByTime(startOffset);
			Game.AudioSequencer.SeekTime(startOffset, SeekOrigin.Current);
			Logger.Log("main", 0, "Load/Prehandle", "Cleaning up"); yield return 1;
			if (logEnabled && Settings.Default.HideLogOnPlay) ToggleLogs();
			Camera.main.cullingMask |= 1;
			GC.Collect();
			if (disableGC) GarbageCollector.GCMode = GarbageCollector.Mode.Disabled;
			timer.Stop();
			Logger.Log("main", 1, "Load/Prehandle", "Prehandling done ({0}ms)", timer.Elapsed.TotalMilliseconds); yield return 1;
			if (Settings.Default.ClearLogOnPlay) {
				logEntries.Clear();
				logsLength = 0;
				Game.MainLogger.Enumerate((level, module, msg) => { });
				logs.text = "";
			}
			Game.AudioSequencer.Playing = true;
			atime0 = Game.AudioClient.BufferPosition - startOffset;
			inputProxy.SyncTime(cbus.Time);
			inputProxy.Activate();
		}

		public void Stop() {
			try {
				Logger.Log("main", 1, "Game", "Stopping");
				Game.AudioClient.Start();
				Game.AudioSession = Game.AudioSequencer.NewSession();
				Camera.main.cullingMask &= ~1;
				if (inputProxy != null) {
					inputProxy.Deactivate();
					inputProxy = null;
				}
				judge = null;
				if (nbus != null) { nbus.Dispose(); nbus = null; }
				if (tbus != null) { tbus.Dispose(); tbus = null; }
				if (bbus != null) { bbus.Dispose(); bbus = null; }
				if (cbus != null) { cbus.Dispose(); cbus.DisposeAll(); cbus = null; }
				if (effectManager != null) {
					effectManager.Dispose();
					effectManager = null;
				}
				PdtEvaluator.Instance.Reset();
				motionRegistry = null;
				Logger.Log("main", 1, "Game", "Stopped");
			}
			catch (Exception ex) {
				if (!logEnabled) ToggleLogs();
				Game.LogException("Game", "An error occurred while stopping", ex);
				Popup.CreateException(ex);
			}
			finally {
				if (started) {
					if (disableGC) GarbageCollector.GCMode = GarbageCollector.Mode.Enabled;
					GC.Collect();
					started = false;
				}
			}
			Game.ResumeBackgroundTasks();
			ReturnToMenu();
		}

		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 skinFile;
		}

		Exception threadException;
		Thread loadThread = null;
		volatile float loadPregress;
		Stopwatch workerTimer;
		void Load(object _info) {
			var info = (LoadInfo)_info;
			try {
				workerTimer = new Stopwatch();
				workerTimer.Start();
				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 occurred while loading the data", ex);
				threadException = ex;
			}
		}

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

			motionRegistry = new Dictionary<Identifier, MotionRegistry> {
				{ new Identifier("pt")     , new MotionRegistry(typeof(Vec2)) },
				{ new Identifier("dir")    , new MotionRegistry(typeof(Vec3)) },
				{ new Identifier("normal") , new MotionRegistry(typeof(Vec3)) },
				{ new Identifier("sv")     , new MotionRegistry(new Vec1(0f), new Vec1(hitRect.height)) },
				{ new Identifier("svm")    , new MotionRegistry(new Vec1m(1f)) },
				{ new Identifier("dist")   , new MotionRegistry(new Vec1(0f), new Vec1(float.PositiveInfinity)) },
				{ new Identifier("track")  , new MotionRegistry(typeof(Vec1)) },
			};

			using (StreamReader reader = new StreamReader(info.chartFile.FullName, Encoding.UTF8)) {
				PdtEvaluator.Instance.Reset();

				LoadRuleset(info.rulesetFile); loadPregress = .05f;

				chart = JsonConvert.DeserializeObject<Chart>(reader.ReadToEnd(), new JsonSerializerSettings() {
					MissingMemberHandling = MissingMemberHandling.Error
				});

				Logger.Log("main", 0, "Load/WorkerThread", "Applying ruleset (iteration 1)"); loadPregress = .10f;
				pruleset.PrePatch(chart);

				Logger.Log("main", 0, "Load/WorkerThread", "Batching events"); loadPregress = .20f;
				var batcher = new EventBatcher(chart);
				batcher.Forward();
				cbus = batcher.Batch(); loadPregress = .30f;

				LoadSkin(info.skinFile);

				Logger.Log("main", 0, "Load/WorkerThread", "Initializing judge and input"); loadPregress = .35f;
				judge = new Judge(this, pruleset);
				PdtEvaluator.Instance.ContextJudge = judge;

				inputProxy = new InputProxy(pruleset, judge, screenSize);
				inputProxy.LoadFrom(_rscfg.inputs);
				if (!inputProxy.IsCompleted()) {
					Logger.Log("main", 2, "Game", "Input config not completed. Input disabled");
					inputProxy.Clear();
				}

				Logger.Log("main", 0, "Load/WorkerThread", "Attaching handlers"); loadPregress = .40f;
				var ch = new ChartHandler(chart);
				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((Chart.Note)ts.Key, gh);
						}
						else {
							th = new TrackHandler((Chart.Track)ts.Key, gh);
						}
						ts.Value.AttachHandler(th);
					}
				}
				cbus.AttachSystems(pskin, judge);
				Logger.Log("main", 0, "Load/WorkerThread", "Prehandling (iteration 1)"); loadPregress = .60f;
				using (var pbus = cbus.Clone(16)) {
					pbus.Forward();
				}

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

		void LoadRuleset(FileInfo file) {
			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<RulesetDefinition>(reader.ReadToEnd(), new JsonSerializerSettings() {
					MissingMemberHandling = MissingMemberHandling.Error
				});
				if (ruleset.format != RulesetDefinition.CURRENT_FORMAT) throw new FormatException("Invalid ruleset file version");
				ruleset.LoadPdt(dir);
				pruleset = ruleset.Root;
				pruleset.Optimize(PdtEvaluator.Instance);
			}
			PdtEvaluator.Instance.ContextRulesetConfig = new RulesetConfigStore(pruleset.configs, _rscfg.configs);
			RMVPool.Shared = new RMVPool();
			MotionCachePool.Shared = new MotionCachePool();
			MotionNodePool.Shared = new MotionNodePool();
		}

		void LoadSkin(FileInfo file) {
			DirectoryInfo dir = file.Directory;
			Logger.Log("main", 0, "Load/WorkerThread", "Loading skin: {0}", file);
			skin.LoadPdt(dir);
			pskin = skin.Root;
			pskin.Optimize(PdtEvaluator.Instance);
			effectManager = new EffectManager(pskin);
		}
		#endregion
	}
}
