using Cryville.Audio;
using Cryville.Audio.Source;
using Cryville.Common.Font;
using Cryville.Common.Logging;
using Cryville.Common.Unity;
using Cryville.Common.Unity.UI;
using Cryville.Crtr.UI;
using Cryville.Culture;
using Cryville.Input;
using Cryville.Input.Unity;
#if UNITY_ANDROID && !UNITY_EDITOR
using Cryville.Input.Unity.Android;
using Cryville.Interop.Java;
using Cryville.Interop.Java.Unity;
#endif
using FFmpeg.AutoGen;
using Ionic.Zip;
using Newtonsoft.Json;
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using UnityEngine;
using Logger = Cryville.Common.Logging.Logger;
using unity = UnityEngine;

namespace Cryville.Crtr {
	public static class Game {
		public static string GameDataPath { get; private set; }
		public static string UnityDataPath { get; private set; }
		public static IAudioDeviceManager AudioManager;
		public static IAudioDevice AudioDevice;
		public static AudioClient AudioClient;
		public static SimpleSequencerSource AudioSequencer;
		public static SimpleSequencerSession AudioSession;
		public static InputManager InputManager;
		public static readonly NetworkTaskWorker NetworkTaskWorker = new NetworkTaskWorker();

		public static readonly JsonSerializerSettings GlobalJsonSerializerSettings
			= new JsonSerializerSettings() {
				DefaultValueHandling = DefaultValueHandling.Ignore,
			};

		public static BufferedLogger MainLogger { get; private set; }

		static bool _init;
		public static void Init() {
			if (_init) return;
			_init = true;

			bool _bcflag = new Version(Settings.Default.LastRunVersion) < new Version("0.4");
			if (_bcflag) Settings.Default.Reset();

			Logger.SetLogPath(Settings.Default.GameDataPath + "/logs");
			MainLogger = new BufferedLogger();
			Application.logMessageReceivedThreaded += OnLog;
			Logger.Create("main", MainLogger);

			Logger.Log("main", 1, "Game", "Game Version: {0}", Application.version);
			Logger.Log("main", 1, "Game", "Unity Version: {0}", Application.unityVersion);
			Logger.Log("main", 1, "Game", "Operating System: {0}", Environment.OSVersion);
			Logger.Log("main", 1, "Game", "Platform: Build = {0}, Unity = {1}", PlatformConfig.Name, Application.platform);
			Logger.Log("main", 1, "Game", "Culture: {0}, UI = {1}, System = {2}, Unity = {3}", CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture, CultureInfo.InstalledUICulture, Application.systemLanguage);

			if (_bcflag) Logger.Log("main", 2, "Game", "Reset all settings");

			GameDataPath = Settings.Default.GameDataPath;
			UnityDataPath = Application.dataPath;

#if UNITY_ANDROID && !UNITY_EDITOR
			JavaVMManager.Register(JniInvoke.Instance);
#endif

			unity::Input.simulateMouseWithTouches = false;
			var emptyObjectArray = new object[0];
#if UNITY_ANDROID && !UNITY_EDITOR
			InputManager.HandlerRegistries.Add(typeof(AndroidAccelerometerHandler), emptyObjectArray);
			InputManager.HandlerRegistries.Add(typeof(AndroidAccelerometerUncalibratedHandler), emptyObjectArray);
			InputManager.HandlerRegistries.Add(typeof(AndroidGameRotationVectorHandler), emptyObjectArray);
			InputManager.HandlerRegistries.Add(typeof(AndroidGravityHandler), emptyObjectArray);
			InputManager.HandlerRegistries.Add(typeof(AndroidGyroscopeHandler), emptyObjectArray);
			InputManager.HandlerRegistries.Add(typeof(AndroidLinearAccelerationHandler), emptyObjectArray);
			InputManager.HandlerRegistries.Add(typeof(AndroidMagneticFieldHandler), emptyObjectArray);
			InputManager.HandlerRegistries.Add(typeof(AndroidMagneticFieldUncalibratedHandler), emptyObjectArray);
			InputManager.HandlerRegistries.Add(typeof(AndroidRotationVectorHandler), emptyObjectArray);
			InputManager.HandlerRegistries.Add(typeof(AndroidTouchHandler), emptyObjectArray);
#endif
			InputManager.HandlerRegistries.Add(typeof(UnityGuiInputHandler<UnityKeyReceiver>), emptyObjectArray);
			InputManager.HandlerRegistries.Add(typeof(UnityGuiInputHandler<UnityMouseReceiver>), emptyObjectArray);
			InputManager.HandlerRegistries.Add(typeof(UnityMouseHandler), emptyObjectArray);
			InputManager.HandlerRegistries.Add(typeof(UnityTouchHandler), emptyObjectArray);
			InputManager = new InputManager();

#if UNITY_EDITOR_WIN
			ffmpeg.RootPath = Application.dataPath + "/Plugins/Windows";
#elif UNITY_STANDALONE_WIN
			ffmpeg.RootPath = Application.dataPath + "/Plugins/x86_64";
#elif UNITY_ANDROID
			ffmpeg.RootPath = "";
#else
#error No FFmpeg search path.
#endif

#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
			EngineBuilder.Engines.Add(typeof(Audio.Wasapi.MMDeviceEnumeratorWrapper));
			EngineBuilder.Engines.Add(typeof(Audio.WaveformAudio.WaveDeviceManager));
#elif UNITY_ANDROID
			EngineBuilder.Engines.Add(typeof(Audio.AAudio.AAudioManager));
			EngineBuilder.Engines.Add(typeof(Audio.OpenSLES.Engine));
#else
#error No audio engine defined.
#endif
			while (true) {
				try {
					AudioManager = EngineBuilder.Create();
					if (AudioManager == null) {
						Dialog.Show(null, "Fatal error: Cannot initialize audio engine");
						Logger.Log("main", 5, "Audio", "Cannot initialize audio engine");
					}
					else {
						Logger.Log("main", 1, "Audio", "Using audio API: {0}", AudioManager.GetType().Namespace);
						AudioDevice = AudioManager.GetDefaultDevice(DataFlow.Out);
						AudioClient = AudioDevice.Connect(AudioDevice.DefaultFormat, AudioDevice.MinimumBufferSize + AudioDevice.BurstSize);
						Logger.Log(
							"main", 1, "Audio",
							"Audio Output = {{ Name = \"{0}\", BurstSize = {1}, Format = {2}, BufferSize = {3} }}",
							AudioDevice.Name, AudioDevice.BurstSize, AudioClient.Format, AudioClient.BufferSize
						);
						AudioClient.Source = AudioSequencer = new SimpleSequencerSource();
						AudioSession = AudioSequencer.NewSession();
						AudioSequencer.Playing = true;
						AudioClient.Start();
					}
					break;
				}
				catch (Exception ex) {
					Dialog.Show(null, "An error occurred while trying to initialize the recommended audio engine\nTrying to use fallback audio engines");
					Logger.Log("main", 4, "Audio", "An error occurred when initializing the audio engine: {0}", ex);
					Logger.Log("main", 2, "Audio", "Trying to use fallback audio engines");
					EngineBuilder.Engines.Remove(AudioManager.GetType());
				}
			}

			var dir = new DirectoryInfo(Settings.Default.GameDataPath + "/charts");
			if (!dir.Exists || Settings.Default.LastRunVersion != Application.version) {
				Directory.CreateDirectory(dir.FullName);
				var defaultData = Resources.Load<TextAsset>("default");
				using (var zip = ZipFile.Read(defaultData.bytes)) {
					zip.ExtractExistingFile = ExtractExistingFileAction.OverwriteSilently;
					zip.ExtractAll(Settings.Default.GameDataPath);
				}
			}

			Settings.Default.LastRunVersion = Application.version;
			Settings.Default.Save();

			Logger.Log("main", 1, "UI", "Initializing font manager");
			foreach (var res in Resources.LoadAll<TextAsset>("cldr/common/validity")) {
				IdValidity.Load(LoadXmlDocument(res));
			}
			var metadata = new SupplementalMetadata(LoadXmlDocument("cldr/common/supplemental/supplementalMetadata"));
			var subtags = new LikelySubtags(LoadXmlDocument("cldr/common/supplemental/likelySubtags"), metadata);
			var matcher = new LanguageMatching(LoadXmlDocument("cldr/common/supplemental/languageInfo"), subtags);
			TMPAutoFont.FontMatcher = new FallbackListFontMatcher(matcher, PlatformConfig.FontManager) {
				MapScriptToTypefaces = PlatformConfig.ScriptFontMap
			};
			TMPAutoFont.DefaultShader = Resources.Load<Shader>(PlatformConfig.TextShader);

			Logger.Log("main", 1, "Game", "Initialized");
		}

		static readonly Encoding _encoding = new UTF8Encoding(false, true);
		static readonly XmlReaderSettings _xmlSettings = new XmlReaderSettings {
			DtdProcessing = DtdProcessing.Ignore,
		};
		static XDocument LoadXmlDocument(string path) {
			return LoadXmlDocument(Resources.Load<TextAsset>(path));
		}
		static XDocument LoadXmlDocument(TextAsset asset) {
			using (var stream = new MemoryStream(_encoding.GetBytes(asset.text))) {
				using (var reader = XmlReader.Create(stream, _xmlSettings)) {
					return XDocument.Load(reader);
				}
			}
		}

		static bool _shutdown;
		public static void Shutdown() {
			if (_shutdown) return;
			_shutdown = true;

			Logger.Log("main", 1, "Game", "Shutting down");
			try {
				AudioClient.Dispose();
				AudioSequencer.Dispose();
				AudioDevice.Dispose();
				AudioManager.Dispose();
			}
			catch (Exception ex) {
				LogException("Game", "An error occurred while shutting down", ex);
			}
			finally {
				Logger.Close();
			}
		}

		public static void LogException(string module, string prefix, Exception ex) {
			Logger.Log("main", 4, module, "{0}: {1}", prefix, ex);
		}

		static void OnLog(string condition, string stackTrace, LogType type) {
			int l;
			switch (type) {
				case LogType.Log: l = 1; break;
				case LogType.Assert: l = 2; break;
				case LogType.Warning: l = 3; break;
				case LogType.Error:
				case LogType.Exception: l = 4; break;
				default: l = 1; break;
			}
			Logger.Log("main", l, "Internal", "{0}\n{1}", condition, stackTrace);
		}

		public static void SuspendBackgroundTasks() {
			NetworkTaskWorker.SuspendBackgroundTasks();
			Dialog.Suppress();
		}
		public static void ResumeBackgroundTasks() {
			Dialog.Release();
			NetworkTaskWorker.ResumeBackgroundTasks();
		}
	}
}
