using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using skyscraper5.Mpeg2;
using skyscraper5.Skyscraper.IO.CrazycatStreamReader;
using skyscraper5.Skyscraper.Scraper;
using skyscraper5.Skyscraper.Scraper.Storage;

namespace skyscraper5.Skyscraper.IO
{
    internal class StreamReaderScraperController
    {
        private readonly IStreamReader streamReader;

        public StreamReaderScraperController(IStreamReader streamReader)
        {
            this.streamReader = streamReader;
        }


        private const string RECORDING_FILENAME_MASK = "skyscraper_{0}.ts";

        public IScraperStroage ScraperStroage { get; set; }

        public bool Recording { get; set; }

        public DirectoryInfo RecordingOutputDirectory { get; set; }

        private bool PrepareRecording()
        {
            if (!Recording)
                return false;

            if (RecordingOutputDirectory == null)
                return false;

            DirectoryInfo rootDirectory = RecordingOutputDirectory.Root;
            DriveInfo driveInfo = new DriveInfo(rootDirectory.FullName);
            long driveFreeSpace = driveInfo.TotalFreeSpace;
            long driveTotalSize = driveInfo.TotalSize;
            long tenPercentOfDriveTotal = driveTotalSize / 10;
            if (driveFreeSpace > tenPercentOfDriveTotal)
                return true;

            FileInfo[] fileInfos = RecordingOutputDirectory.GetFiles(string.Format(RECORDING_FILENAME_MASK, "*"));
            Array.Sort(fileInfos, (x, y) => DateTime.Compare(x.CreationTimeUtc, y.CreationTimeUtc));
            long sizeOfAllKnown = fileInfos.Select(x => x.Length).Sum();
            long driveFreeSpaceAfterDeleting = driveFreeSpace + sizeOfAllKnown;
            if (driveFreeSpaceAfterDeleting < tenPercentOfDriveTotal)
                return false;

            for (int i = 0; i < fileInfos.Length; i++)
            {
                fileInfos[i].Delete();
                Console.WriteLine("Deleted for rotation: {0}", fileInfos[i].Name);
                fileInfos[i] = null;
                driveInfo = new DriveInfo(rootDirectory.FullName);
                driveFreeSpace = driveInfo.TotalFreeSpace;
                if (driveFreeSpace > tenPercentOfDriveTotal)
                    return true;
            }
            driveInfo = new DriveInfo(rootDirectory.FullName);
            driveFreeSpace = driveInfo.TotalFreeSpace;
            if (driveFreeSpace > tenPercentOfDriveTotal)
                return true;
            else
                return false;
        }

        private bool HasLock(bool cableTv)
        {
            if (cableTv)
            {
                SearchResult2 sr2 = new SearchResult2();
                if (!streamReader.SignalInfo2(ref sr2))
                    return false;
                return sr2.Lock;
            }
            else
            {
                SearchResult sr = new SearchResult();
                if (!streamReader.SignalInfo(ref sr))
                    return false;
                return sr.Lock;
            }
        }

        private ulong packetsReceivedInTotal;
        private Queue<byte[]> packetsQueue;
        private void DvbCallback(IntPtr data, int length)
        {
            if (length % 188 == 0)
            {
                byte[] buffer = new byte[length];
                Marshal.Copy(data, buffer, 0, length);
                lock (packetsQueue)
                {
                    packetsQueue.Enqueue(buffer);
                }
                packetsReceivedInTotal++;
            }
            else
            {
                Console.WriteLine("odd packet size!");
            }
        }

        private bool HasPackets()
        {
            lock (packetsQueue)
            {
                return packetsQueue.Count > 0;
            }
        }

        private DateTime startedAt;
        private bool StopConditionMet()
        {
            if (startedAt == DateTime.MinValue)
                startedAt = DateTime.Now;

            if (packetsReceivedInTotal == 0 && (DateTime.Now - startedAt).TotalSeconds > 5)
                return true;

            if (!skyscraperContext.EnableTimeout)
            {
                skyscraperContext.TimeoutSeconds = 10;
                skyscraperContext.EnableTimeout = true;
            }

            if (packetsReceivedInTotal > 0 && skyscraperContext.IsAbortConditionMet())
                return true;

            return false;
        }

        private void DrainPackets()
        {
            if (packetsQueue.Count == 0)
                return;
            
            DateTime startedAt = DateTime.Now;
            byte[] singlePacketBuffer = new byte[188];
            byte[] packetBuffer = null;
            while (packetsQueue.Count > 0)
            {
                packetBuffer = packetsQueue.Dequeue();
                if (recordingBufferedStream != null)
                    recordingBufferedStream.Write(packetBuffer, 0, packetBuffer.Length);
                for (int i = 0; i < packetBuffer.Length; i += 188)
                {
                    Array.Copy(packetBuffer, i, singlePacketBuffer, 0, 188);
                    skyscraperContext.IngestSinglePacket(singlePacketBuffer);

                }
            }

        }

        private BufferedStream recordingBufferedStream;
        private FileStream recordingFileStream;
        private SkyscraperContext skyscraperContext;
        public void Run()
        {
            bool cableTvMode = false;
            Caps caps = streamReader.GetCaps();
            if (caps.HasFlag(Caps.SR_SIGINFO))
            {
                cableTvMode = false;
            }
            else if (caps.HasFlag(Caps.SR_SIGINFO2))
            {
                cableTvMode = true;
            }
            else
            {
                throw new NotImplementedException("Couldn't figure out what signal info to use.");
            }

            if (!HasLock(cableTvMode))
            {
                throw new NotImplementedException("No lock.");
            }

            int misCounter = 1;
            int misMode = 0;
            byte[] s2_is = null;
            if (!cableTvMode)
            {
                SearchResult sr = default;
                if (!streamReader.SignalInfo(ref sr))
                {
                    throw new StreamReaderException(string.Format("{0} failed", nameof(streamReader.SignalInfo)));
                }

                if (!sr.Lock)
                    return;

                misMode = sr.MIS;
                s2_is = sr.IS;
            }

            for (int mis = 0; mis < misCounter; mis++)
            {
                if (misMode != 0)
                {
                    Console.WriteLine("Selecting MIS IS {0}", s2_is[mis]);
                    bool misSel = streamReader.MISSel(misMode != 0, s2_is[mis], 0xff);
                    if (!misSel)
                    {
                        throw new StreamReaderException(string.Format("{0} failed", nameof(streamReader.MISSel)));
                    }
                }

                //Start Filter
                if (PrepareRecording())
                {
                    string recordingFileName = GenerateRecordingFilename(mis, misMode);
                    FileInfo recordingFileInfo = new FileInfo(recordingFileName);
                    recordingFileInfo.Directory.EnsureExists();
                    recordingFileStream = recordingFileInfo.OpenWrite();
                    recordingBufferedStream = new BufferedStream(recordingFileStream);
                }

                IntPtr filterReference = IntPtr.MaxValue;
                packetsQueue = new Queue<byte[]>();
                bool filter = streamReader.SetFilter(8192, DvbCallback, 0x02, 2, ref filterReference);
                if (!filter)
                {
                    throw new Exception("failed to set filter");
                }

                //Use the Filter
                skyscraperContext = new SkyscraperContext(new TsContext(), ScraperStroage);
                skyscraperContext.TcpProxyEnabled = true;
                byte[] singlePacketBuffer = new byte[188];
                while (!StopConditionMet())
                {
                    if (!HasPackets())
                    {
                        Thread.Sleep(100);
                        continue;
                    }

                    byte[] packetBuffer = null;
                    lock (packetsQueue)
                    {
                        packetBuffer = packetsQueue.Dequeue();
                    }

                    if (recordingBufferedStream != null)
                        recordingBufferedStream.Write(packetBuffer, 0, packetBuffer.Length);


                    for (int i = 0; i < packetBuffer.Length; i += 188)
                    {
                        Array.Copy(packetBuffer, i, singlePacketBuffer, 0, 188);
                        skyscraperContext.IngestSinglePacket(singlePacketBuffer);
                    }
                }

                //Stop Filter
                bool stopped = streamReader.DelFilter(filterReference);
                if (!stopped)
                {
                    throw new Exception("failed to stop filter");
                }

                DrainPackets();
                if (recordingBufferedStream != null)
                {
                    recordingBufferedStream.Flush();
                    recordingBufferedStream.Close();
                    recordingBufferedStream.Dispose();
                    recordingBufferedStream = null;
                    recordingFileStream.Close();
                    recordingFileStream.Dispose();
                    recordingFileStream = null;
                }
            }
        }

        private string GenerateRecordingFilename(int mis, int misMode)
        {
            DateTime dt = DateTime.Now;
            string dateFmt = string.Format("{0:D4}{1:D2}{2:D2}_{3:D2}{4:D2}", dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute);
            if (namingHint != null)
            {
                switch (namingHint.System)
                {
                    case STD_TYPE.STD_DVBS:
                        string resultS = null;
                        if (misMode == 0)
                        {
                            resultS = string.Format("{0}_{1}_{2}_{3}.ts", dateFmt, namingHint.SearchResult1.Freq / 1000, namingHint.SearchResult1.Pol == 0 ? "H" : "V", namingHint.SearchResult1.SR / 1000);
                        }
                        else
                        {
                            resultS = string.Format("{0}_{1}_{2}_{3}_MIS{4}.ts", dateFmt, namingHint.SearchResult1.Freq / 1000, namingHint.SearchResult1.Pol == 0 ? "H" : "V", namingHint.SearchResult1.SR / 1000, mis);
                        }

                        return Path.Combine(RecordingOutputDirectory.FullName, resultS);
                    default:
                        throw new NotImplementedException(namingHint.System.ToString());
                }
            }
            else
            {
                return Path.Combine(RecordingOutputDirectory.FullName, string.Format(RECORDING_FILENAME_MASK, dateFmt));
            }
        }

        private BlindScanResult namingHint;
        public void SetNamingHint(BlindScanResult blindScanResult)
        {
            namingHint = blindScanResult;
        }
    }
}
