using log4net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using skyscraper5.Mpeg2;
using skyscraper5.Skyscraper.Scraper.Utils;
using skyscraper5.src.Mpeg2.PacketFilter;

namespace skyscraper8.Skyscraper
{
	internal class CatalogueGenerator : IDisposable
	{
		private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name);
		private readonly DirectoryInfo _inputDir;
		private readonly FileStream _outputCsvStream;
		private readonly StreamWriter _outputCsv;
		private readonly List<IPacketFilter> _nullPacketFilter;
		private readonly PacketDiscarder _packetDiscarder;

		public CatalogueGenerator(DirectoryInfo inputDir, FileInfo outputCsv)
		{
			_inputDir = inputDir;
			if (outputCsv.Exists)
			{
				string originalName = outputCsv.FullName;
				string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputCsv.FullName);
				string newName = Path.Combine(outputCsv.Directory.FullName,String.Format("{0}_{1}.bak", fileNameWithoutExtension, DateTime.Now.Ticks));
				logger.WarnFormat("The file {0} already exists. I don't want to overwrite it, so I'm gonna rename it to {1}:", originalName, newName);
				File.Move(originalName, newName);
				outputCsv.Refresh();
			}

			_outputCsvStream = outputCsv.Open(FileMode.CreateNew, FileAccess.Write, FileShare.Read);
			_outputCsv = new StreamWriter(_outputCsvStream, Encoding.UTF8);
			_outputCsv.WriteLine("FILE_NAME;GUESSED_TYPE;TOTAL_PACKETS;PIDS;PAT_PACKETS;OTHER_PACKETS;NULL_PACKETS;PAT_SCRAMBLED;OTHER_SCRAMBLED;NULL_SCRAMBLED;PAT_TEI;OTHER_TEI;NULL_TEI;PCR_PID;PCR_DURATION;DISCONTINUITIES;READ_ERROR;LOWEST_PID;");
			_nullPacketFilter = new List<IPacketFilter>();
			_nullPacketFilter.Add(new NullPacketFilter());
		}

		public void Dispose()
		{
			_outputCsvStream.Dispose();
		}

		public void Run()
		{
			ScrapeDirectory(_inputDir);
		}

		private void ScrapeDirectory(DirectoryInfo di)
		{
			logger.InfoFormat("Entering directory: {0}", di.FullName);
			foreach (FileSystemInfo fileSystemInfo in di.GetFileSystemInfos())
			{
				switch (fileSystemInfo)
				{
					case DirectoryInfo subdirectory:
						ScrapeDirectory(subdirectory);
						break;
					case FileInfo fi:
						ScrapeFile(fi);
						break;
					default:
						logger.ErrorFormat("{0} is a {1}. Those are not supported yet.", fileSystemInfo.FullName, fileSystemInfo.GetType().Name);
						break;
				}
			}
			logger.InfoFormat("Leaving directory: {0}", di.FullName);
		}

		private void ScrapeFile(FileInfo fi)
		{
			string extension = Path.GetExtension(fi.FullName);
			extension = extension.ToLowerInvariant();
			if (!extension.Equals(".ts"))
				return;

			if (fi.Length == 0)
			{
				_outputCsv.WriteLine("\"{0}\";TOO_SMALL;0;0;0;0;0;0;0;0;0;0;0;0;0;0;", fi.FullName);
				_outputCsv.Flush();
				return;
			}

			TsContext tsContext = new TsContext();
			tsContext.FilterChain = _nullPacketFilter;
			tsContext.RegisterPacketProcessor(0x0000, _packetDiscarder);
			int readError = 0;

			logger.InfoFormat("Reading: {0}", fi.Name);
			FileStream fileStream = fi.OpenRead();
			BufferedStream bufferedStream = new BufferedStream(fileStream);
			byte[] buffer = new byte[188];
			try
			{
				for (long l = 0; l < bufferedStream.Length; l += 188)
				{
					if (bufferedStream.Read(buffer, 0, 188) != 188)
					{
						logger.ErrorFormat("Failed to read 188 bytes from offset {0} of file {1}, aborting reading it.", l, fi.Name);
						readError = 1;
						break;
					}

					if (buffer[0] != 'G')
					{
						logger.ErrorFormat("{0} is not a valid Transport Stream file.", fi.Name);
						readError = 3;
						break;
					}
					tsContext.PushPacket(buffer);
				}
			}
			catch (IOException e)
			{
				readError = 2;
				logger.ErrorFormat("Failed to read from {0}: {1}", fi.Name, e.ToString());
			}

			bufferedStream.Close();
			fileStream.Close();

			string filename = fi.FullName;
			TsType tsType = GuessTsType(tsContext);
			long totalPackets = tsContext.PacketsRead + 1;
			int pids = tsContext.CountPidsWithTraffic();
			ulong patPackets = tsContext.GetNumberOfPacketsOnPid(0);
			ulong otherPackets = CountOtherPackets(tsContext);
			ulong nullPackets = tsContext.GetNumberOfPacketsOnPid(8191);
			ulong patScrambled = tsContext.GetScrambledPacketsOnPid(0);
			ulong otherScrambled = CountOtherScrambledPacket(tsContext);
			ulong nullScrambled = tsContext.GetScrambledPacketsOnPid(8191);
			ulong patTei = tsContext.GetTeiPacketsOnPid(0);
			ulong otherTei = CountOtherTeiPacket(tsContext);
			ulong nullTei = tsContext.GetTeiPacketsOnPid(8191);
			uint pcrPid = GetPcrPid(tsContext);
			TimeSpan pcrDuration = GetPcrDuration(tsContext);
			long discontinuities = tsContext.PacketLossEvents;
			int lowestPid = GetLowestPid(tsContext);

			_outputCsv.WriteLine(
				"\"{0}\";{1};{2};{3};{4};{5};{6};{7};{8};{9};{10};{11};{12};{13};{14};{15};{16};{17}",
				filename, tsType, totalPackets, pids,
				patPackets, otherPackets,
				nullPackets, patScrambled, otherScrambled, nullScrambled, patTei, otherTei, nullTei, pcrPid,
				pcrDuration, discontinuities, readError, lowestPid); 
			_outputCsv.Flush();
		}

		private int GetLowestPid(TsContext tsContext)
		{
			for (int i = 0; i < 8192; i++)
			{
				ulong numberOfPacketsOnPid = tsContext.GetNumberOfPacketsOnPid(i);
				if (numberOfPacketsOnPid > 0)
					return i;
			}

			return -1;
		}

		private TsType GuessTsType(TsContext tsContext)
		{
			if (tsContext.PacketsRead == 0)
				return TsType.TOO_SMALL;

			if (tsContext.LikelyBrokenEncapsulation)
				return TsType.LIKELY_BAD_RECEPTION;

			ulong brokenGsIndicators = tsContext.GetNumberOfPacketsOnPid(0x0118);
			double brokenGsPercentage = (double)brokenGsIndicators / (double)tsContext.PacketsRead;
			if (brokenGsPercentage >= 0.95)
				return TsType.GS_OLD_STREAMREADER;

			ulong goodGsIndicators = tsContext.GetNumberOfPacketsOnPid(0x010e);
			double goodGsPercentage = (double)goodGsIndicators / (double)tsContext.PacketsRead;
			if (goodGsPercentage >= 0.95)
				return TsType.GS_STID135;

			int occupiedPids = tsContext.CountPidsWithTraffic();
			if (occupiedPids == 2)
			{
				ulong blockstreamIndicators = tsContext.GetNumberOfPacketsOnPid(0x0020);
				double blockstreamPercentage = (double)blockstreamIndicators / (double)tsContext.PacketsRead;
				if (blockstreamPercentage >= 0.95 && tsContext.GetNumberOfPacketsOnPid(0x1fff) > 0)
					return TsType.BLOCKSTREAM;

				ulong docsisIndicators = tsContext.GetNumberOfPacketsOnPid(0x1ffe);
				double docsisPercentage = (double)docsisIndicators / (double)tsContext.PacketsRead;
				if (docsisPercentage >= 0.08 && tsContext.GetNumberOfPacketsOnPid(0x1fff) > 0)
					return TsType.DOCSIS;

				ulong nullIndicators = tsContext.GetNumberOfPacketsOnPid(0x1fff);
				double nullPercentage = (double)nullIndicators / (double)tsContext.PacketsRead;
				if (nullPercentage >= 0.95 && tsContext.GetNumberOfPacketsOnPid(0) > 0 && tsContext.GetNumberOfPacketsOnPid(0x1fff) > 0)
					return TsType.BLANK_PAT_AND_NULLS;

				ulong wdrDabIndicators = tsContext.GetNumberOfPacketsOnPid(0x0bb8);
				double wdrDabPercentage = (double)wdrDabIndicators / (double)tsContext.PacketsRead;
				if (wdrDabPercentage >= 0.7 && nullPercentage >= 0.2)
					return TsType.LIKELY_WDR_DAB;
			}
			
			return TsType.NORMAL_TS;
		}

		private uint GetPcrPid(TsContext tsContext)
		{
			if (tsContext.PcrMonitor == null)
				return 8191;

			return tsContext.PcrMonitor.SourcePid;
		}

		private TimeSpan GetPcrDuration(TsContext tsContext)
		{
			if (tsContext.PcrMonitor == null)
				return TimeSpan.Zero;
			
			if (tsContext.PcrMonitor.PcrDuration == null)
				return TimeSpan.Zero;

			return tsContext.PcrMonitor.PcrDuration.Value;
		}
		private ulong CountOtherPackets(TsContext tsContext)
		{
			ulong result = 0;
			for (int i = 1; i < 8191; i++)
			{
				result += tsContext.GetNumberOfPacketsOnPid(i);
			}
			return result;
		}

		private ulong CountOtherScrambledPacket(TsContext tsContext)
		{
			ulong result = 0;
			for (int i = 1; i < 8191; i++)
			{
				result += tsContext.GetScrambledPacketsOnPid(i);
			}
			return result;
		}

		private ulong CountOtherTeiPacket(TsContext tsContext)
		{
			ulong result = 0;
			for (int i = 1; i < 8191; i++)
			{
				result += tsContext.GetTeiPacketsOnPid(i);
			}
			return result;
		}

		private enum TsType
		{
			NORMAL_TS,
			TOO_SMALL,
			GS_STID135,
			GS_OLD_STREAMREADER,
			BLOCKSTREAM,
			DOCSIS,
			LIKELY_BAD_RECEPTION,
			BLANK_PAT_AND_NULLS,
			LIKELY_WDR_DAB
		}
	}
}
