using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using skyscraper5.Skyscraper.IO.CrazycatStreamReader;
using skyscraper5.Skyscraper.Scraper;
using skyscraper5.Skyscraper.Scraper.Storage.Filesystem;
using skyscraper8.Skyscraper.Scraper.Storage;

namespace skyscraper5.Skyscraper.IO
{
    internal class StreamReaderScanController : IDisposable
    {
        private readonly IStreamReader streamReader;

        public static List<StreamReaderTuner> GetTuners(IStreamReader streamReader)
        {
            List<StreamReaderTuner> result = new List<StreamReaderTuner>();
            bool checkForDvbExEx = streamReader.CheckForDVBExEx((index, name, type) =>
            {
                result.Add(new StreamReaderTuner(index, name, type));
            });
            if (!checkForDvbExEx)
            {
                return new List<StreamReaderTuner>();
            }
            return result;
        }

        public List<StreamReaderTuner> GetTunersByStandard(STD_TYPE stdType)
        {
            return GetTuners(streamReader).Where(x => x.Type == stdType).ToList();
        }

        public StreamReaderTuner GetTunerByStandard(STD_TYPE stdType)
        {
            List<StreamReaderTuner> streamReaderTuners = GetTunersByStandard(stdType);
            if (streamReaderTuners.Count == 0)
                return null;
            else
                return streamReaderTuners[0];
        }

        private bool busy;
        private PhysicalAddress physicalAddress;
        private STD_TYPE stdType;
        private Caps caps;
        private StreamReaderTuner tunerMetadata;
        private static int instances = 0;
        private bool disposed;

        public StreamReaderScanController(IStreamReader streamReader, StreamReaderTuner tuner)
        {
            this.streamReader = streamReader;

            if (instances > 0)
            {
                throw new StreamReaderException("Only one StreamReader may be active, dispose the other one first.");
            }

            if (tuner != null)
            {
                tunerMetadata = tuner;
                if (!streamReader.StartDvbEx(tuner.Index))
                {
                    throw new StreamReaderException(string.Format("{0} failed", nameof(streamReader.StartDvbEx)));
                }
            }
            else
            {
                if (!streamReader.StartDVB())
                {
                    throw new StreamReaderException(string.Format("{0} failed", nameof(streamReader.StartDVB)));
                }
            }

            caps = streamReader.GetCaps();
            if (!streamReader.GetTunerType(ref stdType))
            {
                throw new StreamReaderException(string.Format("{0} failed", nameof(streamReader.GetTunerType)));
            }

            if (caps.HasFlag(Caps.SR_GETMAC))
            {
                byte[] macBuffer = new byte[6];
                if (streamReader.GetMAC(macBuffer))
                {
                    physicalAddress = new PhysicalAddress(macBuffer);
                }
            }

            string[] strings = Enum.GetNames(typeof(StreamReaderParameter));
            parameters = new uint[strings.Length];
            paramsSet = new bool[strings.Length];

            if (stdType == STD_TYPE.STD_DVBC)
                SetDefaultDvbCParameters();
        }

        private bool[] paramsSet;
        private uint[] parameters;
        public enum StreamReaderParameter : int
        {
            ScanStartFreq,
            ScanEndFreq,
            ScanStep,
            ScanBandwidth,
            ScanStdType,
            ScanLof1,
            ScanLofSwitch,
            ScanLof2,
            ScanDiseqcType,
            DiseqcSwitchPosition
        }

        public void SetParameter(StreamReaderParameter key, uint value)
        {
            parameters[(int)key] = value;
            paramsSet[(int)key] = true;
        }

        public uint GetParameter(StreamReaderParameter key)
        {
            if (!paramsSet[(int)key])
            {
                throw new StreamReaderException(string.Format("{0} was not set.", key.ToString()));
            }
            return parameters[(int)key];
        }

        public void SetDefaultDvbCParameters()
        {
            SetParameter(StreamReaderParameter.ScanStartFreq, 48000);
            SetParameter(StreamReaderParameter.ScanEndFreq, 1000000);
            SetParameter(StreamReaderParameter.ScanStep, 3000);
            SetParameter(StreamReaderParameter.ScanBandwidth, 8000);
            SetParameter(StreamReaderParameter.ScanStdType, (uint)STD_TYPE.STD_DVBC);
        }

        public void SetDefaultKuBandParameters()
        {
            SetParameter(StreamReaderParameter.ScanLof1, 9750000);
            SetParameter(StreamReaderParameter.ScanLofSwitch, 11700000);
            SetParameter(StreamReaderParameter.ScanLof2, 10600000);
            SetParameter(StreamReaderParameter.ScanStartFreq, 10700000);
            SetParameter(StreamReaderParameter.ScanEndFreq, 12750000);
        }

        public void Dispose()
        {
            if (busy)
            {
                throw new StreamReaderException("TunerMetadata is busy.");
            }

            streamReader.StopDVB();
            instances--;
            disposed = true;
        }

        public class StreamReaderTuner
        {
            public int Index { get; }
            public string Name { get; }
            public STD_TYPE Type { get; }

            public StreamReaderTuner(int index, string name, STD_TYPE type)
            {
                Index = index;
                Name = name;
                Type = type;
            }
        }

        public bool Tune(BlindScanResult blindScanResult)
        {
            switch (stdType)
            {
                case STD_TYPE.STD_DVBC:
                    Console.WriteLine("Tuning to {0} Mhz", blindScanResult.SearchResult2.Freq / 1000.0);
                    bool channel2 = streamReader.SetChannel2((uint)blindScanResult.SearchResult2.Freq, (uint)blindScanResult.SearchResult2.BW);
                    if (!channel2)
                    {
                        throw new StreamReaderException(string.Format("{0} failed", nameof(streamReader.SetChannel2)));
                    }

                    SearchResult2 sr2 = default;
                    bool signalInfo2 = streamReader.SignalInfo2(ref sr2);
                    if (!signalInfo2)
                    {
                        throw new StreamReaderException(string.Format("{0} failed", nameof(streamReader.SetChannel2)));
                    }

                    return sr2.Lock;
                case STD_TYPE.STD_DVBS:
                    Console.WriteLine("Tuning to {0} Mhz", blindScanResult.SearchResult1.Freq / 1000.0);
                    SearchResult searchResult1 = blindScanResult.SearchResult1;
                    uint lof1 = GetParameter(StreamReaderParameter.ScanLof1);
                    uint lof2 = GetParameter(StreamReaderParameter.ScanLof2);
                    uint lofSw = GetParameter(StreamReaderParameter.ScanLofSwitch);
                    if (!streamReader.SetChannel(searchResult1.Freq, searchResult1.SR, searchResult1.Pol, searchResult1.FEC, (int)lof1, (int)lof2, (int)lofSw))
                    {
                        throw new StreamReaderException(string.Format("{0} failed", nameof(streamReader.SetChannel)));
                    }

                    SearchResult cmp = default;
                    bool signalInfo = streamReader.SignalInfo(ref cmp);
                    if (!signalInfo)
                    {
                        throw new StreamReaderException(string.Format("{0} failed", nameof(streamReader.SignalInfo)));
                    }
                    return cmp.Lock;
                default:
                    throw new NotImplementedException();
            }
        }

        private void SearchResult2Callback(BlindScanResults results, SearchResult2 sr2Ptr)
        {
            BlindScanResult bbsr = new BlindScanResult();
            bbsr.System = stdType;
            bbsr.SearchResult2 = sr2Ptr;
            results.Transponders.Add(bbsr);

            Console.WriteLine("{0} Mhz, {1} ksyms, {2}", sr2Ptr.Freq / 1000.0, sr2Ptr.SR / 1000, (MOD_TYPE)sr2Ptr.ModType);
        }

        private void SearchResult2Callback(BlindScanResults results, SearchResult sr2Ptr)
        {
            BlindScanResult bbsr = new BlindScanResult();
            bbsr.SearchResult1 = sr2Ptr;
            bbsr.System = stdType;
            results.Transponders.Add(bbsr);

            Console.WriteLine("{0} Mhz, {1} ksyms, {2}", sr2Ptr.Freq / 1000.0, sr2Ptr.SR / 1000, (MOD_TYPE)sr2Ptr.ModType);
        }

        private void SendDiseqcCommand(int diseqcType, bool highBand, bool horizontal)
        {
            DiSEqC_Opcode myOpcode = DiSEqC_Opcode.DISEQC_HIGH_NIBBLE;
            if (highBand)
                myOpcode |= DiSEqC_Opcode.DISEQC_HIGH_BAND;
            else
                myOpcode |= DiSEqC_Opcode.DISEQC_LOW_BAND;

            if (horizontal)
                myOpcode |= DiSEqC_Opcode.DISEQC_HORIZONTAL;
            else
                myOpcode |= DiSEqC_Opcode.DISEQC_VERTICAL;

            if (diseqcType == 2)
            {
                uint parameter = GetParameter(StreamReaderParameter.DiseqcSwitchPosition);
                switch (parameter)
                {
                    case 1:
                        myOpcode |= DiSEqC_Opcode.DISEQC_OPTION_A;
                        myOpcode |= DiSEqC_Opcode.DISEQC_POSITION_A;
                        break;
                    case 2:
                        myOpcode |= DiSEqC_Opcode.DISEQC_OPTION_A;
                        myOpcode |= DiSEqC_Opcode.DISEQC_POSITION_B;
                        break;
                    case 3:
                        myOpcode |= DiSEqC_Opcode.DISEQC_OPTION_B;
                        myOpcode |= DiSEqC_Opcode.DISEQC_POSITION_A;
                        break;
                    case 4:
                        myOpcode |= DiSEqC_Opcode.DISEQC_OPTION_B;
                        myOpcode |= DiSEqC_Opcode.DISEQC_POSITION_B;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException(nameof(StreamReaderParameter.DiseqcSwitchPosition));
                }
            }

            bool sendDiSEqC = streamReader.SendDiSEqC((uint)diseqcType, myOpcode);
            if (!sendDiSEqC)
            {
                throw new StreamReaderException(string.Format("{0} failed", nameof(streamReader.SendDiSEqC)));
            }
        }

        public void RunBlindscan()
        {
            if (busy)
                throw new StreamReaderException("TunerMetadata is busy");

            int startFreq = (int)GetParameter(StreamReaderParameter.ScanStartFreq);
            int endFreq = (int)GetParameter(StreamReaderParameter.ScanEndFreq);

            BlindScanResults result = new BlindScanResults();
            result.Standard = stdType;
            switch (stdType)
            {
                case STD_TYPE.STD_DVBC:
                    if (caps.HasFlag(Caps.SR_AIRSCAN))
                    {

                        uint step = GetParameter(StreamReaderParameter.ScanStep);
                        uint bandwidth = GetParameter(StreamReaderParameter.ScanBandwidth);
                        STD_TYPE stdType = (STD_TYPE)GetParameter(StreamReaderParameter.ScanStdType);
                        SearchResult2 sr2 = default;
                        int tpNum = default;
                        IntPtr pSearchResult = Marshal.AllocHGlobal(ushort.MaxValue);
                        busy = true;
                        bool airScan = streamReader.AirScan(startFreq, endFreq, step, bandwidth, (int)stdType, pSearchResult, ref tpNum, (ref SearchResult2 searchResult) => SearchResult2Callback(result, searchResult));
                        if (airScan)
                        {
                            Scrape(result);
                        }
                        busy = false;
                        Marshal.FreeHGlobal(pSearchResult);
                        if (!airScan)
                        {
                            throw new StreamReaderException(string.Format("{0} failed", nameof(streamReader.GetTunerType)));
                        }
                    }
                    else
                    {
                        throw new NotImplementedException("Don't know how to blindscan with this tuner.");
                    }

                    break;
                case STD_TYPE.STD_DVBS:
                    if (caps.HasFlag(Caps.SR_BLSCAN2))
                    {
                        SearchResult sr1 = default;
                        int tpNum = default;

                        int lof1 = (int)GetParameter(StreamReaderParameter.ScanLof1);
                        int lof2 = (int)GetParameter(StreamReaderParameter.ScanLof2);
                        int lofSw = (int)GetParameter(StreamReaderParameter.ScanLofSwitch);
                        int diseqc = (int)GetParameter(StreamReaderParameter.ScanDiseqcType);

                        IntPtr allocHGlobal = Marshal.AllocHGlobal(ushort.MaxValue);

                        if (lofSw != 0)
                        {
                            busy = true;

                            result.Transponders.Clear();
                            SendDiseqcCommand(diseqc, false, true);
                            bool hLower = streamReader.BLScan2(startFreq, lofSw, 0, lof1, lof2, lofSw, allocHGlobal, ref tpNum, (ref SearchResult searchResult) => SearchResult2Callback(result, searchResult));
                            if (hLower)
                            {
                                Scrape(result);
                            }

                            result.Transponders.Clear();
                            SendDiseqcCommand(diseqc, true, true);
                            bool hUpper = streamReader.BLScan2(lofSw, endFreq, 0, lof1, lof2, lofSw, allocHGlobal, ref tpNum, (ref SearchResult searchResult) => SearchResult2Callback(result, searchResult));
                            if (hUpper)
                            {
                                Scrape(result);
                            }

                            result.Transponders.Clear();
                            SendDiseqcCommand(diseqc, false, false);
                            bool vLower = streamReader.BLScan2(startFreq, lofSw, 1, lof1, lof2, lofSw, allocHGlobal, ref tpNum, (ref SearchResult searchResult) => SearchResult2Callback(result, searchResult));
                            if (vLower)
                            {
                                Scrape(result);
                            }

                            result.Transponders.Clear();
                            SendDiseqcCommand(diseqc, true, false);
                            bool vUpper = streamReader.BLScan2(lofSw, endFreq, 1, lof1, lof2, lofSw, allocHGlobal, ref tpNum, (ref SearchResult searchResult) => SearchResult2Callback(result, searchResult));
                            if (vUpper)
                            {
                                Scrape(result);
                            }

                            busy = false;
                            if (!hLower && !hUpper && !vLower && !vUpper)
                                throw new StreamReaderException(string.Format("{0} failed", nameof(streamReader.BLScan2)));
                        }
                        else
                        {
                            throw new NotImplementedException("non switching lnb");
                        }

                        Marshal.FreeHGlobal(allocHGlobal);
                    }
                    else
                    {
                        throw new NotImplementedException("Don't know how to blindscan with this tuner.");
                    }
                    break;
                default:
                    throw new NotImplementedException(stdType.ToString());
            }
        }

        private StreamReaderScraperController srsc2;
        private void Scrape(BlindScanResults blindScanResults)
        {
            if (srsc2 == null)
            {
                srsc2 = new StreamReaderScraperController(streamReader);
                srsc2.RecordingOutputDirectory = new DirectoryInfo("recordings");
                srsc2.Recording = true;
                srsc2.DataStorage = new FilesystemStorage(new DirectoryInfo("srtest_results"));
                srsc2.ObjectStorage = (ObjectStorage)srsc2.DataStorage;
            }

            foreach (BlindScanResult blindScanResult in blindScanResults.Transponders)
            {
                if (Tune(blindScanResult))
                {
                    srsc2.SetNamingHint(blindScanResult);
                    srsc2.Run();
                }
                else
                    Console.WriteLine("Lock failed.");
            }
        }
    }

    [Serializable]
    class StreamReaderException : SkyscraperIOException
    {
        //
        // For guidelines regarding the creation of new exception types, see
        //    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
        // and
        //    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
        //

        public StreamReaderException()
        {
        }

        public StreamReaderException(string message) : base(message)
        {
        }

        public StreamReaderException(string message, Exception inner) : base(message, inner)
        {
        }

        protected StreamReaderException(
            SerializationInfo info,
            StreamingContext context) : base(info, context)
        {
        }
    }

    class BlindScanResults
    {
        public BlindScanResults()
        {
            Transponders = new List<BlindScanResult>();
        }

        public List<BlindScanResult> Transponders { get; private set; }
        public STD_TYPE Standard { get; set; }
    }

    class BlindScanResult
    {
        public SearchResult2 SearchResult2 { get; set; }
        public SearchResult SearchResult1 { get; set; }
        public STD_TYPE System { get; set; }
    }
}
