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

namespace skyscraper8.Experimentals.OtvSsu
{
	internal class OtvSsuParser : PrivateSectionParser
	{
		private readonly OtvSsuHandler _handler;

		public OtvSsuParser(OtvSsuHandler handler)
		{
			_handler = handler;
		}

		private int errors;
		protected override void HandleTable(byte tableId, int sourcePid, byte[] payload)
		{
			errors++;
		}

		private Dictionary<Coordinate, DataMap> _dataMaps;
		protected override void HandleTable(byte tableId, int sourcePid, byte sectionNumber, byte lastSectionNumber, ushort tableIdExtension, byte[] payload)
		{
			MemoryStream ms = new MemoryStream(payload, false);
			uint fileId = ms.ReadUInt32BE();
			uint unknown1 = ms.ReadUInt32BE();
			uint offset = ms.ReadUInt32BE();
			uint length = ms.ReadUInt32BE();
			if (offset > length)
			{
				_handler.OnOtvSsuError(sourcePid, OtvSsuError.OffsetOutOfRange);
				return;
			}

			if (length > Int32.MaxValue)
			{
				_handler.OnOtvSsuError(sourcePid, OtvSsuError.FileTooLarge);
				return;
			}
			if (tableIdExtension < 10 && (fileId & 0x00ff0000) == 0x00ff0000)
			{
				_handler.OnOtvSsuError(sourcePid, OtvSsuError.NdsOverlap);
				return;
			}

			Coordinate coords = new Coordinate(tableIdExtension, fileId, unknown1, length);
			if (_dataMaps == null)
				_dataMaps = new Dictionary<Coordinate, DataMap>();
			DataMap map;
			if (_dataMaps.ContainsKey(coords))
			{
				map = _dataMaps[coords];
				_handler.OnOtvSsuBlock(sourcePid, tableIdExtension, fileId, unknown1, length);
			}
			else
			{
				map = new DataMap(length);
				if (_handler.OnOtvCheckFileAlreadyKnown(sourcePid, tableIdExtension, fileId, unknown1, length))
				{
					map.Dispose();
					_dataMaps.Add(coords, map);
					return;
				}
				else
				{
					_handler.OnOtvSsuFileAnnouncement(sourcePid, tableIdExtension, fileId, unknown1, length);
					_dataMaps.Add(coords, map);
				}
			}

			if (map.IsComplete)
			{
				return;
			}
			if (map.WasWrittenBefore(offset))
			{
				return;
			}
			map.PushPacket(payload,offset);
			if (map.IsErronous)
			{
				_handler.OnOtvSsuError(sourcePid, OtvSsuError.PacketDeliveredTooMuchData);
				_dataMaps.Remove(coords);
				map.Dispose();
				return;
			}
			if (map.IsComplete)
			{
				_handler.OnOtvSsuComplete(sourcePid, map.GetStream(), tableIdExtension, fileId, unknown1, length);
				map.Dispose();
			}
		}

		class Coordinate
		{
			private readonly ushort _tableIdExtension;
			private readonly uint _fileId;
			private readonly uint _unknown1;
			private readonly uint _length;

			public Coordinate(ushort tableIdExtension, uint fileId, uint unknown1, uint length)
			{
				_tableIdExtension = tableIdExtension;
				_fileId = fileId;
				_unknown1 = unknown1;
				_length = length;
			}

			protected bool Equals(Coordinate other)
			{
				return _tableIdExtension == other._tableIdExtension && _fileId == other._fileId && _unknown1 == other._unknown1 && _length == other._length;
			}

			public override bool Equals(object? obj)
			{
				if (ReferenceEquals(null, obj)) 
					return false;
				if (ReferenceEquals(this, obj)) 
					return true;
				if (obj.GetType() != this.GetType()) 
					return false;
				return Equals((Coordinate)obj);
			}

			public override int GetHashCode()
			{
				return HashCode.Combine(_tableIdExtension, _fileId, _unknown1, _length);
			}
		}

		class DataMap : IDisposable
		{
			private MemoryStream ms;
			private uint length;
			private uint needed;
			private List<uint> knownOffsets;
			private bool disposed;

			public DataMap(uint length)
			{
				ms = new MemoryStream((int)length);
				this.length = length;
				this.needed = length;
				this.knownOffsets = new List<uint>();
			}

			public void PushPacket(byte[] payload, uint offset)
			{
				if (disposed)
					throw new ObjectDisposedException(this.ToString());
				ms.Position = offset;
				ms.Write(payload, 16, payload.Length - 16);
				needed -= (uint)(payload.Length - 16);
				knownOffsets.Add(offset);
			}

			public bool WasWrittenBefore(uint offset)
			{
				if (disposed)
					return true;
				if (knownOffsets == null)
					return false;

				return knownOffsets.Contains(offset);
			}

			public bool IsComplete
			{
				get
				{
					if (disposed)
						return false;
					return needed == 0;
				}
			}

			public bool IsErronous
			{
				get
				{
					if (disposed)
						throw new ObjectDisposedException(this.ToString());
					return length < needed;
				}
			}

			public Stream GetStream()
			{
				if (disposed)
					throw new ObjectDisposedException(this.ToString());
				return ms;
			}

			public void Dispose()
			{
				ms = null;
				needed = 0;
				knownOffsets = null;
				disposed = true;
			}
		}
	}
}
