using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Reflection;
using skyscraper5.Dvb.Descriptors;
using skyscraper5.Dvb.Descriptors.Extension;
using skyscraper5.Dvb.Psi.Model;
using skyscraper5.Mpeg2;
using skyscraper5.Mpeg2.Descriptors;
using skyscraper5.Skyscraper.IO;
using skyscraper5.Skyscraper.Plugins;

namespace skyscraper5.Dvb.Psi
{
    class BatParser : IPsiProcessor
    {
        public IBatEventHandler EventHandler { get; }

        public BatParser(IBatEventHandler batEventHandler)
        {
            EventHandler = batEventHandler;
        }

        public void GatherPsi(PsiSection section, int sourcePid)
        {
            MemoryStream ms = new MemoryStream(section.GetDataCopy(), false);

            byte tableId = ms.ReadUInt8();
            if (tableId != 0x4a)
                return;
            
            ushort readUInt16Be = ms.ReadUInt16BE();
            bool sectionSyntaxIndicator = (readUInt16Be & 0x8000) != 0;
            if (!sectionSyntaxIndicator)
                return;
            int sectionLength = (readUInt16Be & 0x0fff);
            
            ushort bouquetId = ms.ReadUInt16BE();

            byte readUInt8 = ms.ReadUInt8();
            int versionNumber = (readUInt8 & 0x3e);
            bool currentNextIndicator = (readUInt8 & 0x01) != 0;

            byte sectionNumber = ms.ReadUInt8();

            byte lastSectionNumber = ms.ReadUInt8();

            readUInt16Be = ms.ReadUInt16BE();
            int bouquetDescriptorsLength = (readUInt16Be & 0x0fff);

            byte[] bouquetDescriptorsData = ms.ReadBytes(bouquetDescriptorsLength);
            IEnumerable<TsDescriptor> outerDescriptors = TsDescriptorUnpacker.GetInstance().UnpackDescriptors(bouquetDescriptorsData, "BAT");
            BatBouquet batBouquet = ParseOuterDescriptors(outerDescriptors,bouquetId);

            readUInt16Be = ms.ReadUInt16BE();
            int transportStreamLoopLength = (readUInt16Be & 0x0fff);
            byte[] transportStreamLoopData = ms.ReadBytes(transportStreamLoopLength);
			ReadOnlyCollection<BatTransportStream> transportStreams = ParseTransportStream(transportStreamLoopData, batBouquet).ToList().AsReadOnly();
            batBouquet.TransportStreams = transportStreams;

            uint crc32 = ms.ReadUInt32BE();

			EventHandler.OnBatBouquet(batBouquet);
            foreach(BatTransportStream ts in transportStreams)
            {
                EventHandler.OnBatTransportStream(batBouquet, ts);
            }
		}

        private IEnumerable<BatTransportStream> ParseTransportStream(byte[] transportStreamLoopData, BatBouquet batBouquet)
        {
            MemoryStream ms = new MemoryStream(transportStreamLoopData, false);
            while (ms.GetAvailableBytes() > 6)
            {
                ushort transportStreamId = ms.ReadUInt16BE();
                ushort originalNetworkId = ms.ReadUInt16BE();

                int transportDescriptorsLength = ms.ReadUInt16BE() & 0x0fff;
                if (ms.GetAvailableBytes() < transportDescriptorsLength)
                    break;
                byte[] transportDescriptorsData = ms.ReadBytes(transportDescriptorsLength);
                IEnumerable<TsDescriptor> descriptors = TsDescriptorUnpacker.GetInstance().UnpackDescriptors(transportDescriptorsData, "BAT");
                BatTransportStream child = ParseInnerDescriptors(transportStreamId, originalNetworkId, descriptors);
                //EventHandler.OnBatTransportStream(batBouquet, child);
                yield return child;
            }
        }

        private BatTransportStream ParseInnerDescriptors(ushort transportStreamId, ushort originalNetworkId, IEnumerable<TsDescriptor> descriptors)
        {
            BatTransportStream result = new BatTransportStream(transportStreamId, originalNetworkId);
            foreach (TsDescriptor dvbDescriptor in descriptors)
            {
                string name = dvbDescriptor.GetType().Name;
                switch (name)
                {
                    case nameof(ServiceListDescriptor):
                        ServiceListDescriptor serviceListDescriptor = (ServiceListDescriptor)dvbDescriptor;
                        if (serviceListDescriptor.Services == null)
                            break;
                        if (result.ServiceList == null)
                            result.ServiceList = new List<ServiceListDescriptor.Service>(serviceListDescriptor.Services);
                        result.ServiceList.AddRange(serviceListDescriptor.Services);
                        break;
                    case nameof(UserDefinedDescriptor):
                        if (!result.PrivateDataSpecifier.HasValue)
                            continue;
                        UserDefinedDescriptor udd = (UserDefinedDescriptor)dvbDescriptor;
                        TsDescriptor tsDescriptor = UserDefinedDescriptorUnpacker.GetInstance().UnpackUserDefinedDescriptor(udd, result.PrivateDataSpecifier.Value, "BAT");
                        if (tsDescriptor == null)
                            break;
                        HandleUserDefinedDescriptor(tsDescriptor, result);
                        break;
                    case nameof(PrivateDataSpecifierDescriptor):
                        result.PrivateDataSpecifier = ((PrivateDataSpecifierDescriptor)dvbDescriptor).PrivateDataSpecifier;
                        break;
                    case nameof(StuffingDescriptor):
                        break;
                    case nameof(CountryAvailabilityDescriptor):
                        CountryAvailabilityDescriptor cad = (CountryAvailabilityDescriptor)dvbDescriptor;
                        if (cad.CountryCodes.Length == 0)
                            break;
                        foreach (string cadCountryCode in cad.CountryCodes)
                        {
                            result.CountryAvailability.Add(cadCountryCode, cad.CountryAvailabilityFlag);
                        }
                        break;
                    case nameof(DefaultAuthorityDescriptor):
                        DefaultAuthorityDescriptor defaultAuthorityDescriptor = (DefaultAuthorityDescriptor)dvbDescriptor;
                        result.DefaultAuthority = defaultAuthorityDescriptor.DefaultAuthority;
                        break;
                    default:
                        throw new NotImplementedException(name);
                }
            }

            return result;
        }

        private void HandleUserDefinedDescriptor(TsDescriptor unpacked, BatTransportStream outputTs)
        {
	        Attribute customAttribute = unpacked.GetType().GetCustomAttribute(typeof(DescriptorPluginBatHandlerAttribute));
	        if (customAttribute == null)
	        {
		        throw new NotImplementedException(String.Format("{0} has no {1} defined.", unpacked.GetType().Name, nameof(DescriptorPluginBatHandlerAttribute)));
	        }

	        DescriptorPluginBatHandlerAttribute handlerAttribute = (DescriptorPluginBatHandlerAttribute)customAttribute;
	        handlerAttribute.Handler.HandleBat(outputTs, unpacked);
		}

        private BatBouquet ParseOuterDescriptors(IEnumerable<TsDescriptor> descriptors, ushort bouquetId)
        {
            BatBouquet result = new BatBouquet(bouquetId);
            foreach (TsDescriptor dvbDescriptor in descriptors)
            {
                string name = dvbDescriptor.GetType().Name;
                switch (name)
                {
                    case nameof(BouquetNameDescriptor):
                        BouquetNameDescriptor bouquetNameDescriptor = (BouquetNameDescriptor)dvbDescriptor;
                        result.BouquetName = bouquetNameDescriptor.BouquetName;
                        break;
                    case nameof(LinkageDescriptor):
                        LinkageDescriptor linkageDescriptor = (LinkageDescriptor)dvbDescriptor;
                        result.Linkages.Add(linkageDescriptor);
                        break;
                    case nameof(CountryAvailabilityDescriptor):
                        CountryAvailabilityDescriptor countryAvailabilityDescriptor = (CountryAvailabilityDescriptor)dvbDescriptor;
                        foreach (string countryCode in countryAvailabilityDescriptor.CountryCodes)
                        {
                            result.CountryAvailabilityDictionary[countryCode] = countryAvailabilityDescriptor.CountryAvailabilityFlag;
                        }
                        break;
                    case nameof(PrivateDataSpecifierDescriptor):
                        result.PrivateDataSpecifier = ((PrivateDataSpecifierDescriptor)dvbDescriptor).PrivateDataSpecifier;
                        break;
                    case nameof(UserDefinedDescriptor):
                        if (!result.PrivateDataSpecifier.HasValue)
                            break;
                        TsDescriptor udd = UserDefinedDescriptorUnpacker.GetInstance().UnpackUserDefinedDescriptor((UserDefinedDescriptor)dvbDescriptor, result.PrivateDataSpecifier.Value, "BAT");
                        if (udd == null)
                            break;
                        throw new NotImplementedException(nameof(UserDefinedDescriptor));
                    case nameof(UriLinkageDescriptor):
                        UriLinkageDescriptor uld = (UriLinkageDescriptor)dvbDescriptor;
                        result.UriLinkageType = uld.UriLinkageType;
                        result.Uri = uld.Uri;
                        result.MinPollingInterval = uld.MinPollingInterval;
                        break;
                    case nameof(MultilingualBouquetNameDescriptor):
                        MultilingualBouquetNameDescriptor mbnd = (MultilingualBouquetNameDescriptor)dvbDescriptor;
                        result.MultilingualBouquetName = mbnd;
                        break;
                    case nameof(FtaContentManagementDescriptor):
                        FtaContentManagementDescriptor ftacmd = (FtaContentManagementDescriptor)dvbDescriptor;
                        result.ControlRemoteAccessOverInternet = ftacmd.ControlRemoteAccessOverInternet;
                        result.DoNotApplyRevocation = ftacmd.DoNotApplyRevocation;
                        result.DoNotScramble = ftacmd.DoNotScramble;
                        break;
                    case nameof(ServiceListDescriptor):
                        Console.WriteLine("Don't forget to implement the service list descriptor in the BatParser!");
                        break;
                    default:
                        throw new NotImplementedException(name);
                }
            }

            return result;
        }
    }
}
