using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using TweetLib.Utils.Serialization.Converters;
using TweetLib.Utils.Static;

namespace TweetLib.Utils.Serialization {
	public sealed class SimpleObjectSerializer<T> {
		private const string NewLineReal = "\r\n";
		private const string NewLineCustom = "\r~\n";

		private static string EscapeLine(string input) => input.Replace("\\", "\\\\").Replace(Environment.NewLine, "\\\r\n");
		private static string UnescapeLine(string input) => input.Replace(NewLineCustom, Environment.NewLine);

		private static string UnescapeStream(StreamReader reader) {
			string data = reader.ReadToEnd();

			StringBuilder build = new StringBuilder(data.Length);
			int index = 0;

			while (true) {
				int nextIndex = data.IndexOf('\\', index);

				if (nextIndex == -1 || nextIndex + 1 >= data.Length) {
					break;
				}
				else {
					build.Append(data[index..nextIndex]);

					char next = data[nextIndex + 1];

					if (next == '\\') { // convert double backslash to single backslash
						build.Append('\\');
						index = nextIndex + 2;
					}
					else if (next == '\r' && nextIndex + 2 < data.Length && data[nextIndex + 2] == '\n') { // convert backslash followed by CRLF to custom new line
						build.Append(NewLineCustom);
						index = nextIndex + 3;
					}
					else { // single backslash
						build.Append('\\');
						index = nextIndex + 1;
					}
				}
			}

			return build.Append(data[index..]).ToString();
		}

		private readonly TypeConverterRegistry converterRegistry;
		private readonly Dictionary<string, PropertyInfo> props;

		public SimpleObjectSerializer(TypeConverterRegistry converterRegistry) {
			this.converterRegistry = converterRegistry;
			this.props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(static prop => prop.CanWrite).ToDictionary(static prop => prop.Name);
		}

		public void Write(string file, T obj) {
			if (props.Count == 0) {
				File.Delete(file);
				return;
			}

			var errors = new LinkedList<string>();

			FileUtils.CreateDirectoryForFile(file);

			using (StreamWriter writer = new StreamWriter(new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))) {
				foreach (KeyValuePair<string, PropertyInfo> prop in props) {
					var type = prop.Value.PropertyType;
					var converter = converterRegistry.TryGet(type) ?? ClrTypeConverter.Instance;

					if (converter.TryWriteType(type, prop.Value.GetValue(obj), out string? converted)) {
						if (converted != null) {
							writer.Write(prop.Key);
							writer.Write(' ');
							writer.Write(EscapeLine(converted));
							writer.Write(NewLineReal);
						}
					}
					else {
						errors.AddLast($"Missing converter for type: {type}");
					}
				}
			}

			if (errors.First != null) {
				throw new SerializationSoftException(errors.ToArray());
			}
		}

		public void Read(string file, T obj) {
			string contents;

			using (StreamReader reader = new StreamReader(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))) {
				contents = UnescapeStream(reader);
			}

			if (string.IsNullOrWhiteSpace(contents)) {
				throw new FormatException("File is empty.");
			}
			else if (contents[0] <= (char) 1) {
				throw new FormatException("Input appears to be a binary file.");
			}

			var errors = new LinkedList<string>();
			int currentPos = 0;

			do {
				string line;
				int nextPos = contents.IndexOf(NewLineReal, currentPos);

				if (nextPos == -1) {
					line = contents[currentPos..];
					currentPos = -1;

					if (string.IsNullOrEmpty(line)) {
						break;
					}
				}
				else {
					line = contents.Substring(currentPos, nextPos - currentPos);
					currentPos = nextPos + NewLineReal.Length;
				}

				int space = line.IndexOf(' ');

				if (space == -1) {
					errors.AddLast($"Missing separator on line: {line}");
					continue;
				}

				string property = line[..space];
				string value = UnescapeLine(line[(space + 1)..]);

				if (props.TryGetValue(property, out var info)) {
					var type = info.PropertyType;
					var converter = converterRegistry.TryGet(type) ?? ClrTypeConverter.Instance;

					if (converter.TryReadType(type, value, out object? converted)) {
						info.SetValue(obj, converted);
					}
					else {
						errors.AddLast($"Failed reading property {property} with value: {value}");
					}
				}
			} while (currentPos != -1);

			if (errors.First != null) {
				throw new SerializationSoftException(errors.ToArray());
			}
		}

		public void ReadIfExists(string file, T obj) {
			try {
				Read(file, obj);
			} catch (FileNotFoundException) {
				// ignore
			} catch (DirectoryNotFoundException) {
				// ignore
			}
		}
	}
}