using Cryville.Common.Logging;
using Cryville.Crtr.Extension;
using Mono.Cecil;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace Cryville.Crtr.Browsing {
	internal static class ExtensionManager {
		static readonly Dictionary<string, List<ResourceConverter>> _converters
			= new Dictionary<string, List<ResourceConverter>>();
		public static IEnumerable<string> GetSupportedFormats() {
			return _converters.Keys;
		}
		public static bool TryGetConverters(string extension, out IEnumerable<ResourceConverter> converters) {
			List<ResourceConverter> outResult;
			bool result = _converters.TryGetValue(extension, out outResult);
			converters = outResult;
			return result;
		}
		static readonly Dictionary<string, string> _localRes
			= new Dictionary<string, string>();
		public static IReadOnlyDictionary<string, string> GetLocalResourcePaths() {
			return _localRes;
		}
		public static void Init(string rootPath) {
			LoadExtension(typeof(Extensions.Extension));
			var asms = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToHashSet();
			var modules = new Queue<ModuleItem>();
			var extensionDir = new DirectoryInfo(Path.Combine(rootPath, "extensions"));
			if (extensionDir.Exists) {
				foreach (var extension in extensionDir.EnumerateFiles("*.dll")) {
					try {
						modules.Enqueue(new ModuleItem(extension.OpenRead()));
					}
					catch (Exception ex) {
						Logger.Log("main", 4, "Extension", "Failed to load DLL {0}: {1}", extension, ex);
					}
				}
			}
			int refCounter = 0;
			while (modules.Count > 0 && refCounter < modules.Count) {
				var module = modules.Dequeue();
				bool flag = false;
				foreach (var reference in module.Definition.AssemblyReferences) {
					if (!asms.Contains(reference.Name)) {
						flag = true;
						break;
					}
				}
				if (flag) {
					modules.Enqueue(module);
					refCounter++;
				}
				else {
					try {
						var stream = module.Stream;
						stream.Seek(0, SeekOrigin.Begin);
						var buf = new byte[stream.Length];
						stream.Read(buf, 0, buf.Length);
						var asm = Assembly.Load(buf);
						if (asm == null) throw new TypeLoadException("Failed to load the module");
						asms.Add(asm.GetName().Name);
						foreach (var type in asm.GetTypes()) {
							if (typeof(ExtensionInterface).IsAssignableFrom(type)) {
								LoadExtension(type);
							}
						}
						Logger.Log("main", 1, "Extension", "Loaded module {0}", module.Definition.Name);
					}
					catch (Exception ex) {
						Logger.Log("main", 4, "Extension", "An error occurred while trying to load module {0}: {1}", module.Definition.Name, ex);
					}
					finally {
						module.Definition.Dispose();
						module.Stream.Dispose();
						refCounter = 0;
					}
				}
			}
			var missingList = new List<string>();
			while (modules.Count > 0) {
				missingList.Clear();
				var module = modules.Dequeue();
				foreach (var reference in module.Definition.AssemblyReferences) {
					if (!asms.Contains(reference.Name)) {
						missingList.Add(reference.Name);
					}
				}
				Logger.Log("main", 4, "Extension", "Could not load the module {0} because the following dependencies were missing: {1}", module.Definition.Name, missingList.Aggregate((current, next) => current + ", " + next));
				module.Definition.Dispose();
				module.Stream.Dispose();
			}
		}

		struct ModuleItem {
			public ModuleDefinition Definition { get; set; }
			public FileStream Stream { get; set; }
			public ModuleItem(FileStream stream) {
				Stream = stream;
				Definition = ModuleDefinition.ReadModule(stream);
			}
		}

		static void LoadExtension(Type type) {
			try {
				var extension = (ExtensionInterface)Activator.CreateInstance(type);
				var l1 = extension.GetResourceConverters();
				if (l1 != null) {
					foreach (var c in l1) {
						var fs = c.GetSupportedFormats();
						if (fs == null) continue;
						foreach (var f in fs) {
							if (f == null) continue;
							if (!_converters.ContainsKey(f))
								_converters.Add(f, new List<ResourceConverter> { c });
							else _converters[f].Add(c);
						}
					}
				}
				var l2 = extension.GetResourceFinders();
				if (l2 != null) {
					foreach (var f in l2) {
						var name = f.Name;
						var path = f.GetRootPath();
						if (name != null && path != null) _localRes.Add(name, path);
					}
				}
				Logger.Log("main", 1, "Extension", "Loaded extension {0}", type);
			}
			catch (Exception ex) {
				Logger.Log("main", 4, "Extension", "Failed to load extension {0}: {1}", type, ex);
			}
		}
	}
}
