using skyscraper5.Skyscraper.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.AccessControl;
using System.Text;
using System.Threading.Tasks;

namespace skyscraper8.SophtainerIO
{
	internal class Sophtainer : IDisposable, IDisposer
	{
		private Sophtainer(Stream backingStream) 
		{
			this.backingStream = backingStream;
		}
		private const uint SOPH = 1213222739;
		private const byte METADATA_DATE = 1;
		private const byte METADATA_USER_NAME = 2;
		private const byte METADATA_MACHINE_NAME = 3;
		private const byte METADATA_UUID = 4;
		private const byte METADATA_APPLICATION = 5;
		private const byte METADATA_APPLICATION_VERSION = 6;
		private const byte METADATA_FINAL = 255;
		private Stream backingStream;

		public DateTime CreationDate { get; private set; }
		public string CreationUserName { get; private set; }
		public Guid GUID { get; private set; }
		public string CreationMachineName { get; private set; }
		public uint? Application { get; private set; }
		public Version ApplicationVersion { get; private set; }
		private long StreamOffset { get; set; }

		public bool IsDisposed => disposed;

		public static Sophtainer CreateSophtainer(Stream stream, uint? application = null, Version appVersion = null)
		{
			Sophtainer sophtainer = new Sophtainer(stream);
			stream.WriteUInt8(0xDC);
			stream.WriteUInt8(0xDF);
			stream.WriteUInt32LE(SOPH);
			stream.WriteUInt16LE(1);

			sophtainer.CreationDate = DateTime.Now;
			stream.WriteUInt8(METADATA_DATE);
			stream.WriteInt64LE(sophtainer.CreationDate.Ticks);

			sophtainer.CreationUserName = Environment.UserName;
			stream.WriteUInt8(METADATA_USER_NAME);
			WriteSophtainerString(stream, sophtainer.CreationUserName);

			sophtainer.CreationMachineName = Environment.MachineName;
			stream.WriteUInt8(METADATA_MACHINE_NAME);
			WriteSophtainerString(stream, sophtainer.CreationMachineName);

			sophtainer.GUID = Guid.NewGuid();
			stream.WriteUInt8(METADATA_UUID);
			stream.Write(sophtainer.GUID.ToByteArray(), 0, 16);

			sophtainer.Application = application;
			if (sophtainer.Application.HasValue)
			{
				stream.WriteUInt8(METADATA_APPLICATION);
				stream.WriteUInt32LE(sophtainer.Application.Value);
			}

			sophtainer.ApplicationVersion = appVersion;
			if (sophtainer.ApplicationVersion != null)
			{
				stream.WriteUInt8(METADATA_APPLICATION_VERSION);
				stream.WriteInt32LE(sophtainer.ApplicationVersion.Major);
				stream.WriteInt32LE(sophtainer.ApplicationVersion.Minor);
				stream.WriteInt32LE(sophtainer.ApplicationVersion.Build);
				stream.WriteInt32LE(sophtainer.ApplicationVersion.Revision);
			}

			stream.WriteUInt8(METADATA_FINAL);
			long currentPosition = stream.Position;
			int paddingSize = (int)(stream.Position / 32);
			paddingSize++;
			paddingSize *= 32;
			paddingSize = (int)(paddingSize - currentPosition);
			if (paddingSize != 0)
			{
				byte[] paddingBuffer = new byte[paddingSize];
				stream.Write(paddingBuffer, 0, paddingSize);
			}
			stream.Flush();
			sophtainer.StreamOffset = stream.Position;
			return sophtainer;
		}

		public static Sophtainer LoadSophtainer(Stream stream)
		{
			Sophtainer sophtainer = new Sophtainer(stream);
			if (stream.ReadUInt8() != 0xDC)
				throw new SophtainerException(SophtainerExceptionKind.InvalidMagic);
			if (stream.ReadUInt8() != 0xDF)
				throw new SophtainerException(SophtainerExceptionKind.InvalidMagic);
			if (stream.ReadUInt32LE() != SOPH)
				throw new SophtainerException(SophtainerExceptionKind.InvalidMagic);
			if (stream.ReadUInt16LE() != 1)
				throw new SophtainerException(SophtainerExceptionKind.WrongEndian);

			bool metadataParseComplete = false;
			while (!metadataParseComplete)
			{
				byte metadataOpcode = stream.ReadUInt8();
				switch(metadataOpcode)
				{
					case METADATA_DATE:
						long ticks = stream.ReadInt64LE();
						sophtainer.CreationDate = new DateTime(ticks);
						break;
					case METADATA_USER_NAME:
						sophtainer.CreationUserName = ReadSophtainerString(stream);
						break;
					case METADATA_MACHINE_NAME:
						sophtainer.CreationMachineName = ReadSophtainerString(stream);
						break;
					case METADATA_UUID:
						sophtainer.GUID = new Guid(stream.ReadBytes(16));
						break;
					case METADATA_APPLICATION:
						sophtainer.Application = stream.ReadUInt32LE();
						break;
					case METADATA_APPLICATION_VERSION:
						int major = stream.ReadInt32LE();
						int minor = stream.ReadInt32LE();
						int build = stream.ReadInt32LE();
						int revision = stream.ReadInt32LE();
						sophtainer.ApplicationVersion = new Version(major, minor, build, revision);
						break;
					case METADATA_FINAL:
						long currentPosition = stream.Position;
						int paddingSize = (int)(stream.Position / 32);
						paddingSize++;
						paddingSize *= 32;
						paddingSize = (int)(paddingSize - currentPosition);
						stream.Position += paddingSize;
						sophtainer.StreamOffset = stream.Position;
						metadataParseComplete = true;
						break;
					default:
						throw new SophtainerException(SophtainerExceptionKind.UnknownMetadataOpcode, new NotImplementedException(metadataOpcode.ToString()));
				}
			}

			return sophtainer;
		}

		private static void WriteSophtainerString(Stream stream, string value)
		{
			byte[] bytes = Encoding.UTF8.GetBytes(value);
			stream.WriteVarInt(bytes.Length);
			stream.Write(bytes, 0, bytes.Length);
		}

		private static string ReadSophtainerString(Stream stream)
		{
			long length = stream.ReadVarInt();
			byte[] buffer = stream.ReadBytes(length);
			return Encoding.UTF8.GetString(buffer);
		}

		private OffsetStream data;
		internal OffsetStream GetStream()
		{
			if (disposed)
				throw new ObjectDisposedException(this.ToString());

			if (data == null)
			{
				data = new OffsetStream(backingStream, StreamOffset, this);
			}
			return data;
		}

		private bool disposed;
		public void Dispose()
		{
			if (disposed)
				return;

			backingStream.Dispose();
			disposed = true;
		}
	}
}
