using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using Webserver.UrlHandlers;

namespace Webserver.WebAPI {
	public class OpenApiHelpers {
		private const string masterResourceName = "openapi.master.yaml";
		private const string masterDocName = "openapi.yaml";

		private struct OpenApiSpec {
			public readonly Dictionary<string, string> ExportedPaths;
			public readonly string Spec;

			public OpenApiSpec (string _spec, Dictionary<string, string> _exportedPaths = null) {
				ExportedPaths = _exportedPaths;
				Spec = _spec;
			}
		}

		private readonly Dictionary<string, OpenApiSpec> specs = new CaseInsensitiveStringDictionary<OpenApiSpec> ();

		public OpenApiHelpers () {
			loadMainSpec ();
			Web.ServerInitialized += _ => {
				buildMainSpecRefs ();
			};
		}

		private void loadMainSpec () {
			Assembly apiAssembly = GetType ().Assembly;

			string specText = ResourceHelpers.GetManifestResourceText (apiAssembly, masterResourceName, true);
			if (specText == null) {
				Log.Warning ($"[Web] Failed loading main OpenAPI spec from assembly '{Path.GetFileName (apiAssembly.Location)}'");
				return;
			}

			specs.Add (masterDocName, new OpenApiSpec(specText));
			// Log.Out ($"[Web] Loaded main OpenAPI spec");
		}

		private void buildMainSpecRefs () {
			if (!TryGetOpenApiSpec (null, out string mainSpec)) {
				return;
			}

			StringBuilder sb = new StringBuilder (mainSpec);
			
			foreach ((string apiSpecName, OpenApiSpec spec) in specs) {
				if (apiSpecName.Equals (masterDocName)) {
					continue;
				}

				if (spec.ExportedPaths == null || spec.ExportedPaths.Count < 1) {
					continue;
				}

				foreach ((string exportedPath, string rebasedPath) in spec.ExportedPaths) {
					writePath (sb, apiSpecName, exportedPath, rebasedPath);
				}
			}

			specs[masterDocName] = new OpenApiSpec(sb.ToString ());
			
			Log.Out ("[Web] OpenAPI preparation done");
		}

		private void writePath (StringBuilder _sb, string _apiSpecName, string _exportedPath, string _rebasedPath) {
			_sb.AppendLine ($"  {_rebasedPath ?? _exportedPath}:");
			_sb.Append ($"    $ref: './{_apiSpecName}#/paths/");

			writeJsonPointerEncodedPath (_sb, _exportedPath);
			
			_sb.AppendLine ("'");
		}

		public void LoadOpenApiSpec (AbsWebAPI _api) {
			Assembly apiAssembly = _api.GetType ().Assembly;
			string apiName = _api.Name;
			string apiSpecName = $"{apiName}.openapi.yaml";

			string specText = ResourceHelpers.GetManifestResourceText (apiAssembly, apiSpecName, true);
			if (specText == null) {
				return;
			}

			// Log.Out ($"[Web] Loaded OpenAPI spec for '{apiName}'");
			OpenApiSpec spec = new OpenApiSpec (specText, findExportedPaths (specText));
			specs.Add (apiSpecName, spec);
		}

		public void LoadOpenApiSpec (AbsHandler _pathHandler) {
			Assembly apiAssembly = _pathHandler.GetType ().Assembly;
			string apiName = _pathHandler.GetType ().Name;
			string apiSpecName = $"{apiName}.openapi.yaml";

			string specText = ResourceHelpers.GetManifestResourceText (apiAssembly, apiSpecName, true);
			if (specText == null) {
				return;
			}

			// Log.Out ($"[Web] Loaded OpenAPI spec for '{apiName}'");
			OpenApiSpec spec = new OpenApiSpec (specText, findExportedPaths (specText, _pathHandler.UrlBasePath));
			specs.Add (apiSpecName, spec);
		}

		public void RegisterCustomSpec (Assembly _assembly, string _apiSpecName, string _replaceBasePath = null) {
			string apiSpecName = $"{_apiSpecName}.openapi.yaml";

			string specText = ResourceHelpers.GetManifestResourceText (_assembly, apiSpecName, true);
			if (specText == null) {
				return;
			}

			// Log.Out ($"[Web] Loaded OpenAPI spec for '{_apiSpecName}'");
			OpenApiSpec spec = new OpenApiSpec (specText, findExportedPaths (specText, _replaceBasePath));
			specs.Add (apiSpecName, spec);
		}

		private static readonly Regex pathMatcher = new Regex ("^\\s{1,2}(/\\S+):.*$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
		private Dictionary<string, string> findExportedPaths (string _spec, string _replaceBasePath = null) {
			Dictionary<string, string> result = new Dictionary<string, string> ();

			using TextReader tr = new StringReader (_spec);

			string line;
			bool inPaths = false;
			while ((line = tr.ReadLine ()) != null) {
				if (!inPaths) {
					if (line.StartsWith ("paths:")) {
						inPaths = true;
					}
				} else {
					Match match = pathMatcher.Match (line);
					if (!match.Success) {
						continue;
					}

					string path = match.Groups [1].Value;
					string rebasedPath = null;
					// Log.Out ($"[Web]   Exports: {path}");
					if (_replaceBasePath != null) {
						rebasedPath = path.Replace ("/BASEPATH/", _replaceBasePath);
					}
					result [path] = rebasedPath;
				}
			}

			return result;
		}

		public bool TryGetOpenApiSpec (string _name, out string _specText) {
			if (string.IsNullOrEmpty (_name)) {
				_name = masterDocName;
			}

			if (!specs.TryGetValue (_name, out OpenApiSpec spec)) {
				_specText = null;
				return false;
			}

			_specText = spec.Spec;
			return true;
		}

		private void writeJsonPointerEncodedPath (StringBuilder _targetSb, string _path) {
			for (int i = 0; i < _path.Length; i++) {
				char c = _path[i];

				switch (c) {
					// JSON string escaped characters
					case '"':
						_targetSb.Append ("\\\"");
						break;
					case '\\':
						_targetSb.Append ("\\\\");
						break;
					case '\b':
						_targetSb.Append ("\\b");
						break;
					case '\f':
						_targetSb.Append ("\\f");
						break;
					case '\n':
						_targetSb.Append ("\\n");
						break;
					case '\r':
						_targetSb.Append ("\\r");
						break;
					case '\t':
						_targetSb.Append ("\\t");
						break;
					case (char)0x00:
						_targetSb.Append ("\\u0000");
						break;
					case (char)0x01:
						_targetSb.Append ("\\u0001");
						break;
					case (char)0x02:
						_targetSb.Append ("\\u0002");
						break;
					case (char)0x03:
						_targetSb.Append ("\\u0003");
						break;
					case (char)0x04:
						_targetSb.Append ("\\u0004");
						break;
					case (char)0x05:
						_targetSb.Append ("\\u0005");
						break;
					case (char)0x06:
						_targetSb.Append ("\\u0006");
						break;
					case (char)0x07:
						_targetSb.Append ("\\u0007");
						break;
					case (char)0x0b:
						_targetSb.Append ("\\u000b");
						break;
					case (char)0x0e:
						_targetSb.Append ("\\u000e");
						break;
					case (char)0x0f:
						_targetSb.Append ("\\u000f");
						break;
					case (char)0x10:
						_targetSb.Append ("\\u0010");
						break;
					case (char)0x11:
						_targetSb.Append ("\\u0011");
						break;
					case (char)0x12:
						_targetSb.Append ("\\u0012");
						break;
					case (char)0x13:
						_targetSb.Append ("\\u0013");
						break;
					case (char)0x14:
						_targetSb.Append ("\\u0014");
						break;
					case (char)0x15:
						_targetSb.Append ("\\u0015");
						break;
					case (char)0x16:
						_targetSb.Append ("\\u0016");
						break;
					case (char)0x17:
						_targetSb.Append ("\\u0017");
						break;
					case (char)0x18:
						_targetSb.Append ("\\u0018");
						break;
					case (char)0x19:
						_targetSb.Append ("\\u0019");
						break;
					case (char)0x1a:
						_targetSb.Append ("\\u001a");
						break;
					case (char)0x1b:
						_targetSb.Append ("\\u001b");
						break;
					case (char)0x1c:
						_targetSb.Append ("\\u001c");
						break;
					case (char)0x1d:
						_targetSb.Append ("\\u001d");
						break;
					case (char)0x1e:
						_targetSb.Append ("\\u001e");
						break;
					case (char)0x1f:
						_targetSb.Append ("\\u001f");
						break;
					// JSON Pointer specific
					case '/':
						_targetSb.Append ("~1");
						break;
					case '~':
						_targetSb.Append ("~0");
						break;
					// Non escaped characters
					default:
						_targetSb.Append (c);
						break;
				}
			}
		}

	}
}