using log4net;
using skyscraper5.Ietf.Rfc768;
using skyscraper5.Ietf.Rfc971;
using skyscraper5.Skyscraper.IO;
using skyscraper5.Skyscraper.Plugins;
using skyscraper8.Ietf.FLUTE;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using moe.yo3explorer.skyscraper8.DVBI.Model;
using skyscraper8.DvbI;
using System.Reflection.Metadata.Ecma335;
using skyscraper8.Ietf.Rfc4566_SDP;

namespace skyscraper8.DvbNip
{
	[SkyscraperPlugin]
	internal class DvbNipReceiver : ISkyscraperMpePlugin, DvbNipServiceListNotifier
	{
		private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name);

		public bool CanHandlePacket(InternetHeader internetHeader, byte[] ipv4Packet)
		{
			if (!internetHeader.IsDestinationMulticast)
				return false;

			if (internetHeader.Protocol != 17)
				return false;

			return true;
		}

		public void ConnectToStorage(object[] connector)
		{
		}

		private bool bootstrapped;
		private uint fluteHits, fluteMisses;

		private Dictionary<Tuple<IPAddress, ushort, ulong, ulong>, FluteListener> flutes;
		private static IPAddress dvbServiceDiscovery = IPAddress.Parse("224.0.23.14");

		public void HandlePacket(InternetHeader internetHeader, byte[] ipv4Packet)
		{
			if (!bootstrapped)
			{
				bool isDvbServiceDiscovery = internetHeader.DestinationAddress.Equals(dvbServiceDiscovery);
				if (isDvbServiceDiscovery)
				{
					UserDatagram discoveryUdpPacket = new UserDatagram(ipv4Packet);
					if (discoveryUdpPacket.DestinationPort == 3937)
					{
						LctFrame discoveryLctFrame = new LctFrame(discoveryUdpPacket.Payload);
						if (discoveryLctFrame == null)
							return;
						LctHeader lctHeader = discoveryLctFrame.LctHeader;
						if (lctHeader == null)
							return;
						if (lctHeader.NipActualCarrierInformation != null)
						{
							CurrentCarrierInformation = discoveryLctFrame.LctHeader.NipActualCarrierInformation;
							EventHandler.OnNipCarrierDetected(CurrentCarrierInformation);
							bootstrapped = true;
						}
						else if (discoveryLctFrame.PayloadContainsNativeIpMulticastTransportObject)
						{
							CurrentCarrierInformation = new NipActualCarrierInformation(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, "(no NIP carrier name provided)");
							EventHandler.OnNipCarrierDetected(CurrentCarrierInformation);
							bootstrapped = true;
						}
					}
				}
				return;
			}
			
			UserDatagram udpPacket = new UserDatagram(ipv4Packet);
			LctFrame lctFrame = new LctFrame(udpPacket.Payload);
			if (lctFrame.LctHeader == null)
			{
				return;
			}
			if (lctFrame.LctHeader.TimeExtenstion != null)
			{
				this.currentTime = lctFrame.LctHeader.TimeExtenstion.Sct;
			}

			if (flutes == null)
				flutes = new Dictionary<Tuple<IPAddress, ushort, ulong, ulong>, FluteListener>();

			Tuple<IPAddress, ushort, ulong, ulong> fluteCoordinate = new Tuple<IPAddress, ushort, ulong, ulong>(internetHeader.DestinationAddress, udpPacket.DestinationPort, lctFrame.LctHeader.TransportSessionIdentifier, lctFrame.LctHeader.TransportObjectIdentifier);
			FluteListener fluteListener;
			if (flutes.ContainsKey(fluteCoordinate))
			{
				fluteListener = flutes[fluteCoordinate];
				fluteHits++;
			}
			else
			{
				fluteListener = new FluteListener(internetHeader.DestinationAddress, udpPacket.DestinationPort, lctFrame.LctHeader.TransportSessionIdentifier, lctFrame.LctHeader.TransportObjectIdentifier);
				flutes.Add(fluteCoordinate, fluteListener);
				fluteMisses++;
				TryPruneCache();
			}
			fluteListener.PushPacket(lctFrame);
			if (fluteListener.IsComplete())
			{
				if (fluteListener.DestinationToi == 0)
				{
					if (fluteListener.CarrierInformation != null)
					{
						CurrentCarrierInformation = fluteListener.CarrierInformation;
						return;
					}

					Stream fluteStream = fluteListener.ToStream();
					bool isValidXml = FluteUtilities.IsXmlWellFormed(fluteStream);
					fluteStream.Position = 0;
					if (isValidXml)
					{
						FDTInstanceType fdtAnnouncement = FluteUtilities.UnpackFluteFdt(fluteStream);
						if (fdtAnnouncement != null)
						{
							EventHandler.FluteFileAnnouncement(fluteCoordinate.Item1, fluteCoordinate.Item2, fdtAnnouncement);
							SetFileAssociations(fluteListener, fdtAnnouncement);
						}
					}
					fluteStream.Close();
					fluteStream.Dispose();
					flutes.Remove(fluteCoordinate);
					return;
				}
				if (fluteListener.FileAssociation == null)
				{
					return;
				}
				if (fluteListener.FileAssociation.ContentLocation.StartsWith("urn:dvb:"))
				{
					EventHandler?.FluteFileArrival(CurrentCarrierInformation, fluteListener);
					HandleMetadata(fluteListener);
					fluteListener.Disabled = true;
					return;
				}

				if (CurrentCarrierInformation != null)
				{
					if (serviceListUrls != null)
					{
						if (serviceListUrls.ContainsKey(fluteListener.FileAssociation.ContentLocation))
						{
							string serviceListId = serviceListUrls[fluteListener.FileAssociation.ContentLocation];
							Stream serviceListStream = fluteListener.ToStream();
							byte[] serviceListByteArray = new byte[serviceListStream.Length];
							serviceListStream.Read(serviceListByteArray, 0, (int)serviceListStream.Length);
							ServiceListType serviceList = DvbIUtils.UnpackServiceList(serviceListByteArray);
							EventHandler.OnServiceList(CurrentCarrierInformation, serviceListId, serviceList);
						}
					}
					EventHandler?.FluteFileArrival(CurrentCarrierInformation, fluteListener);
					fluteListener.Disabled = true;
				}
				return;
			}
			else
			{
				if (fluteListener.DestinationToi != 0)
				{
					double progress = fluteListener.DownloadProgress;
					if (progress > fluteListener.LastReportedProgress)
					{
						EventHandler.FluteFileDownloadProgress(CurrentCarrierInformation, fluteListener.DestinationTsi, fluteListener.DestinationToi, progress, fluteListener.FileAssociation);
						fluteListener.LastReportedProgress = progress;
					}
				}
			}
		}
		

		private DateTime? currentTime;
		private bool HandleMetadata(FluteListener fluteListener)
		{
			switch (fluteListener.FileAssociation.ContentLocation)
			{
				case "urn:dvb:metadata:cs:NativeIPMulticastTransportObjectTypeCS:2023:bootstrap":
					Stream stream = fluteListener.ToStream();
					if (DvbNipUtilities.IsXml(stream))
					{
						MulticastGatewayConfigurationType multicastGatewayConfiguration2023 = DvbNipUtilities.UnpackMulticastGatewayConfiguration(stream);
						EventHandler?.OnMulticastGatewayConfiguration(CurrentCarrierInformation, multicastGatewayConfiguration2023);
						return true;
					}
					else if (DvbNipUtilities.IsMime(stream))
					{
						stream = DvbNipUtilities.GetFirstMimeBodyPart(stream);
						if (SDP.IsSDP(stream))
						{
							SDP loadedSdp = SDP.Load(stream);
							MulticastGatewayConfigurationType convertedSdp = loadedSdp.ToMulticastGatewayConfiguration();
							EventHandler?.OnMulticastGatewayConfiguration(CurrentCarrierInformation, convertedSdp);
							return true;
						}
						else
						{
							stream.DumpToFile("container2.bin");
							logger.WarnFormat("Failed to detect the Metadata format of the Native IP Multicast Transport Object.");
							return false;
						}
					}
					else
					{
						logger.WarnFormat("Failed to detect the Metadata format of the Native IP Multicast Transport Object.");
						return false;
					}
					break;
				case "urn:dvb:metadata:cs:MulticastTransportObjectTypeCS:2021:gateway-configuration":
							MulticastGatewayConfigurationType multicastGatewayConfiguration2021 = DvbNipUtilities.UnpackMulticastGatewayConfiguration(fluteListener.ToStream());
							EventHandler?.OnMulticastGatewayConfiguration(CurrentCarrierInformation, multicastGatewayConfiguration2021);
							return true;
						case "urn:dvb:metadata:nativeip:PrivateDataSignalling":
							PrivateDataSignallingManifestType privateDataSignallingManifest = DvbNipUtilities.UnpackPrivateDataSignallingManifest(fluteListener.ToStream());
							EventHandler?.OnPrivateDataSignallingManifest(CurrentCarrierInformation, privateDataSignallingManifest);
							return true;
						case "urn:dvb:metadata:nativeip:dvb-i-slep":
							if (currentTime.HasValue)
							{
								Stream rawSlepStream = fluteListener.ToStream();
								byte[] rawSlepBytes = new byte[rawSlepStream.Length];
								rawSlepStream.Read(rawSlepBytes, 0, (int)rawSlepStream.Length);
								string rawSlepString = Encoding.UTF8.GetString(rawSlepBytes);
								rawSlepString = rawSlepString.Replace("<dvbi-types:", "<");
								rawSlepString = rawSlepString.Replace("</dvbi-types:", "</");
								rawSlepString = rawSlepString.Replace("<dvbisdt:", "<");
								rawSlepString = rawSlepString.Replace("</dvbisdt:", "</");
								ServiceListEntryPoints serviceListEntryPoints = DvbIUtils.UnpackServiceListEntryPoints(rawSlepString);
								EventHandler?.OnServiceListEntryPoints(CurrentCarrierInformation, serviceListEntryPoints, currentTime.Value, this);
								return true;
							}
							else
							{
								return false;
							}
							break;
						case "urn:dvb:metadata:nativeip:TimeOffsetFile":
							TimeOffsetFileType timeOffsetFile = DvbNipUtilities.UnpackTimeOffsetFile(fluteListener.ToStream());
							EventHandler?.OnTimeOffsetFile(CurrentCarrierInformation, timeOffsetFile);
							return true;
						case "urn:dvb:metadata:nativeip:NetworkInformationFile":
							NetworkInformationFileType networkInformationFile = DvbNipUtilities.UnpackNetworkInformationFile(fluteListener.ToStream());
							EventHandler?.OnNetworkInformationFile(CurrentCarrierInformation, networkInformationFile);
							return true;
						case "urn:dvb:metadata:nativeip:ServiceInformationFile":
							ServiceInformationFileType serviceInformationFile = DvbNipUtilities.UnpackServiceInformationFile(fluteListener.ToStream());
							EventHandler?.OnServiceInformationFile(CurrentCarrierInformation, serviceInformationFile);
							return true;
						case "urn:dvb:metadata:nativeip:ServiceGuide":
							//Unfortunately, the NIPServiceGuideManifest does not contain any useful information at all, which is why we ignore it.
							//There doesn't seem to be a way to get the actual EIT XML from it.
							return true;
						default:
							throw new NotImplementedException(fluteListener.FileAssociation.ContentLocation);
						}
		}
		private void TryPruneCache()
		{
			DateTime now = DateTime.Now;
			List<KeyValuePair<Tuple<IPAddress, ushort, ulong, ulong>, FluteListener>> staleListeners = flutes.Where(x => x.Value.LastTouched != DateTime.MinValue)
				.Where(x => x.Value.FileAssociation != null)
				.Where(x => (now - x.Value.LastTouched).TotalMinutes >= 1.0)
				.Where(x => DvbNipUtilities.IsContinuousFileType(x.Value.FileAssociation))
				.ToList();
			if (staleListeners.Count > 0)
			{
				int prevItems = flutes.Count;
				foreach (KeyValuePair<Tuple<IPAddress, ushort, ulong, ulong>, FluteListener> staleListener in
				         staleListeners)
				{
					staleListener.Value.Disabled = true;
					flutes.Remove(staleListener.Key);
				}
				int nowItems = flutes.Count;
				if (staleListeners.Count > 10)
				{
					logger.DebugFormat(String.Format(
						"Removed {0} stale segments from FLUTE cache. Cache slimmed from {1} to {2} items.",
						staleListeners.Count, prevItems, nowItems));
				}
			}
		}
		private bool SetFileAssociations(FluteListener sourceListener, FDTInstanceType fdtAnnouncement)
		{
			bool result = false;
			foreach(FileType announcedFile in fdtAnnouncement.File)
			{
				if (string.IsNullOrEmpty(announcedFile.TOI))
					continue;

				bool needed = EventHandler.IsFileNeeded(announcedFile.ContentLocation);
				if (serviceListUrls != null)
				{
					if (serviceListUrls.ContainsKey(announcedFile.ContentLocation))
					{
						needed = true;
					}
				}
				ulong targetToi = ulong.Parse(announcedFile.TOI);
				Tuple<IPAddress, ushort, ulong, ulong> fluteCoordinate = new Tuple<IPAddress, ushort, ulong, ulong>(sourceListener.DestinationAddress, sourceListener.DestinationPort, sourceListener.DestinationTsi, targetToi);
				if (flutes.ContainsKey(fluteCoordinate))
				{
					FluteListener listener = flutes[fluteCoordinate];
					if (listener.FileAssociation == null)
					{
						listener.FileAssociation = announcedFile;
						result = true;
					}

					if (!needed)
						listener.Disabled = true;
				}
				else
				{
					FluteListener listener = new FluteListener(sourceListener.DestinationAddress, sourceListener.DestinationPort, sourceListener.DestinationTsi, targetToi);
					listener.FileAssociation = announcedFile;
					flutes.Add(fluteCoordinate, listener);
					result = true;
					if (!needed)
						listener.Disabled = true;
				}
			}

			return result;
		}

		public void SetContext(DateTime? currentTime, object skyscraperContext)
		{
			if (EventHandler == null)
			{
				IDvbNipEventHandler? dvbNipEventHandler = skyscraperContext as IDvbNipEventHandler;
				if (dvbNipEventHandler != null)
					EventHandler = dvbNipEventHandler;
			}
		}

		public bool StopProcessingAfterThis()
		{
			return bootstrapped;
		}

		public NipActualCarrierInformation CurrentCarrierInformation { get; private set; }
		public IDvbNipEventHandler EventHandler { get; private set; }

		private Dictionary<string,string> serviceListUrls;
		public void AddServiceListUrl(string url, string id)
		{
			if (serviceListUrls == null)
				serviceListUrls = new Dictionary<string, string>();
			if (string.IsNullOrEmpty(url))
				return;

			if (!serviceListUrls.ContainsKey(url))
			{
				serviceListUrls.Add(url, id);

				KeyValuePair<Tuple<IPAddress, ushort, ulong, ulong>, FluteListener> keyValuePair = flutes
					.Where(x => x.Value.FileAssociation != null)
					.Where(x => x.Value.FileAssociation.ContentLocation.Equals(url))
					.FirstOrDefault();
				if (keyValuePair.Key != null)
				{
					flutes.Remove(keyValuePair.Key);
				}
			}

		}

		private long sourceHash;
		public void SetSourceHash(long sourceHash)
		{
			this.sourceHash = sourceHash;
		}

		public void Reset()
		{
			bootstrapped = false;
			fluteHits = 0;
			fluteMisses = 0;
			flutes = null;
		}
	}
}
