using skyscraper5.Skyscraper.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace skyscraper5.src.Skyscraper.IO.PortableExecutables
{
	public class VsVersionInfo
	{
		public VsVersionInfo(byte[] languageDataBuffer)
		{
			MemoryStream ms = new MemoryStream(languageDataBuffer, false);
			ushort wLength = ms.ReadUInt16LE();
			ushort wValueLength = ms.ReadUInt16LE();
			this.Type = ms.ReadUInt16LE();
			string szKey = ms.ReadUTF16NullTerminated();
			ushort Padding1 = ms.ReadUInt16LE();
			if (wValueLength != 0)
			{
				byte[] fixedFileInfoBuffer = ms.ReadBytes(wValueLength);
				FixedFileInfo = new VsFixedFileInfo(fixedFileInfoBuffer);
			}
			while (ms.Position % 4 != 0)
				ms.ReadUInt16LE();

			while (ms.GetAvailableBytes() > 0)
			{
				uint var_wLength = ms.ReadUInt16LE();
				if (var_wLength == 0)
					continue;
				byte[] varBuffer = ms.ReadBytes(var_wLength - 2);
				Var currentChild = new Var(var_wLength, varBuffer);
				if (Vars == null)
					Vars = new List<Var>();
				Vars.Add(currentChild);
			}
		}

		public VsFixedFileInfo FixedFileInfo { get; }
		public ushort Type { get; }

		public List<Var> Vars { get; }

		const string DICTIONARY_ROOT = "StringFileInfo";
		public Dictionary<string,string> ToDictionary()
		{
			if (Vars == null)
				throw new VarNotFoundException("The root VS_VERSION_INFO element does not contain any children.");

			Var stringFileInfoVar = Vars.FirstOrDefault(x => x.Key.Equals(DICTIONARY_ROOT));
			if (stringFileInfoVar == null)
				throw new VarNotFoundException(String.Format("Could not find the {0} {1}", DICTIONARY_ROOT, nameof(Var)));
			if (stringFileInfoVar.Vars == null)
				throw new VarNotFoundException(String.Format("The {0} does not contain any sub-{1}s", DICTIONARY_ROOT, nameof(Var)));

			Var languageVar = stringFileInfoVar.Vars.FirstOrDefault();
			if (languageVar == null)
				throw new VarNotFoundException(String.Format("Could not find any language related {1}s in {0}", DICTIONARY_ROOT,nameof(Var)));
			if (languageVar.Vars == null)
				throw new VarNotFoundException(String.Format("Did not find any {0}s in the first language related {0}.", nameof(Var)));

			Dictionary<string, string> result = new Dictionary<string, string>();
			foreach(Var var in languageVar.Vars)
			{
				if (var.Type == 1)
				{
					result.Add(var.Key, var.TextValue);
				}
			}
			return result;
		}
	}

	public class VsFixedFileInfo
	{
		public VsFixedFileInfo(byte[] value)
		{
			MemoryStream ms = new MemoryStream(value, false);
			uint dwSignature = ms.ReadUInt32LE();
			uint dwStructVersion = ms.ReadUInt32LE();
			uint dwFileVersionMS = ms.ReadUInt32LE();
			uint dwFileVersionLS = ms.ReadUInt32LE();
			uint dwProductVersionMS = ms.ReadUInt32LE();
			uint dwProductVersionLS = ms.ReadUInt32LE();
			uint dwFileFlagsMask = ms.ReadUInt32LE();
			FileFlags = (FileFlagsEnum)ms.ReadUInt32LE();
			FileOs = (FileOsEnum)ms.ReadUInt32LE();
			FileType = (FileTypeEnum)ms.ReadUInt32LE();
			FileSubtype = ms.ReadUInt32LE();
			uint dwFileDateMS = ms.ReadUInt32LE();
			uint dwFileDateLS = ms.ReadUInt32LE();
		}

		public FileFlagsEnum FileFlags { get; }
		public FileOsEnum FileOs { get; }
		public FileTypeEnum FileType { get; }
		public uint FileSubtype { get; }

		[Flags]
		public enum FileFlagsEnum : uint
		{
			/// <summary>
			/// Die Datei enthält Debuginformationen oder wird mit aktivierten Debugfeatures kompiliert.
			/// </summary>
			Debug = 0x01,
			/// <summary>
			/// Die Versionsstruktur der Datei wurde dynamisch erstellt. daher können einige Elemente in dieser Struktur leer oder falsch sein. Dieses Flag sollte niemals in den VS_VERSIONINFO Daten einer Datei festgelegt werden. 
			/// </summary>
			InfoInferred = 0x10,
			/// <summary>
			/// Die Datei wurde geändert und ist nicht mit der ursprünglichen Versanddatei mit derselben Versionsnummer identisch.
			/// </summary>
			Patched = 0x04,
			/// <summary>
			/// Die Datei ist eine Entwicklungsversion, kein kommerziell freigegebenes Produkt. 
			/// </summary>
			Prerelease = 0x02,
			/// <summary>
			/// Die Datei wurde nicht mithilfe von Standardfreigabeprozeduren erstellt. Wenn dieses Flag festgelegt ist, sollte die StringFileInfo-Struktur einen PrivateBuild-Eintrag enthalten. 
			/// </summary>
			PrivateBuild = 0x08,
			/// <summary>
			/// Die Datei wurde vom ursprünglichen Unternehmen mithilfe von Standard-Releaseprozeduren erstellt, ist aber eine Variante der normalen Datei mit derselben Versionsnummer. Wenn dieses Flag festgelegt ist, sollte die StringFileInfo-Struktur einen SpecialBuild-Eintrag enthalten.
			/// </summary>
			SpecialBuild = 0x20
		}

		[Flags]
		public enum FileOsEnum : uint
		{
			Dos = 0x00010000,
			Nt = 0x00040000,
			Windows16 = 0x00000001,
			Windows32 = 0x00000004,
			Os216 = 0x00020000,
			Os232 = 0x00030000,
			Pm16 = 0x00000002,
			Pm32 = 0x00000003,
			Unknown = 0x00000000,
		}

		public enum FileTypeEnum : uint
		{
			App = 0x00000001,
			Dll = 0x00000002,
			Drv = 0x00000003,
			Font = 0x00000004,
			StaticLib = 0x000000007,
			Unknown = 0x00000000,
			Vxd = 0x00000005,
		}

		public enum FileSubtypeDriverEnum : uint
		{
			Comm = 0x00000000A,
			Display = 0x00000004,
			Installable = 0x00000008,
			Keyboard = 0x00000002,
			Language = 0x00000003,
			Mouse = 0x00000005,
			Network = 0x00000006,
			Printer = 0x00000001,
			Sound = 0x000000009,
			System = 0x000000007,
			VersionedPrinter = 0x0000000C,
			Unknown = 0x00000000
		}

		public enum FileSubtypeFontEnum : uint
		{
			Raster = 0x00000001,
			TrueType = 0x00000003,
			Vector = 0x00000002,
			Unknown = 0x00000000
		}
	}

	public class Var
	{
		public Var(uint wLength, byte[] buffer)
		{
			MemoryStream ms = new MemoryStream(buffer, false);
			ushort wValueLength = ms.ReadUInt16LE();
			this.Type = ms.ReadUInt16LE();
			Key = ms.ReadUTF16NullTerminated();
			while (((ms.Position + 2) % 4) != 0)
				ms.ReadUInt16LE();
			if (this.Type == 1)
			{
				byte[] textBuffer = ms.ReadBytes(wValueLength * 2);
				TextValue = Encoding.Unicode.GetString(textBuffer).Trim('\0');
			}
			else
			{
				BinaryValue = ms.ReadBytes(wValueLength);
			}

			while (ms.GetAvailableBytes() > 0)
			{
				while (ms.GetAvailableBytes() > 0)
				{
					uint var_wLength = ms.ReadUInt16LE();
					if (var_wLength == 0)
						continue;
					byte[] varBuffer = ms.ReadBytes(var_wLength - 2);
					Var currentChild = new Var(var_wLength, varBuffer);
					if (Vars == null)
						Vars = new List<Var>();
					Vars.Add(currentChild);
				}
			}

		}

		public ushort Type { get; }
		public string Key { get; }

		public List<Var> Vars { get; }
		public string TextValue { get; }
		public byte[] BinaryValue { get; }

		public override string ToString()
		{
			StringBuilder sb = new StringBuilder();
			sb.Append(Key);
			sb.Append(" = ");
			if (Type == 1)
			{
				if (string.IsNullOrEmpty(TextValue))
				{
					sb.Append("<EMPTY>");
				}
				sb.Append(TextValue);
			}
			else
			{
				sb.Append("0x");
				sb.Append(BitConverter.ToString(BinaryValue).Replace("-",""));
			}

			if (Vars != null)
			{
				sb.AppendFormat(" ({0} children)", Vars.Count);
			}
			return sb.ToString();
		}
	}
}
