using Ionic.Zlib;
using log4net;
using MimeKit;
using moe.yo3explorer.skyscraper8.DVBI.Model;
using skyscraper5.Abertis;
using skyscraper5.Docsis;
using skyscraper5.Docsis.AnnexC;
using skyscraper5.Docsis.MacManagement;
using skyscraper5.DsmCc.Descriptors;
using skyscraper5.Dvb.DataBroadcasting;
using skyscraper5.Dvb.DataBroadcasting.Biop;
using skyscraper5.Dvb.DataBroadcasting.Dsm.Event;
using skyscraper5.Dvb.DataBroadcasting.Dsm.Stream;
using skyscraper5.Dvb.DataBroadcasting.IntModel;
using skyscraper5.Dvb.DataBroadcasting.SkyscraperVfs;
using skyscraper5.Dvb.Descriptors;
using skyscraper5.Dvb.Psi;
using skyscraper5.Dvb.Psi.Model;
using skyscraper5.Dvb.Subtitling;
using skyscraper5.Dvb.Subtitling.Model;
using skyscraper5.Dvb.SystemSoftwareUpdate;
using skyscraper5.Dvb.SystemSoftwareUpdate.Model;
using skyscraper5.Dvb.TvAnytime;
using skyscraper5.Ietf.Rfc768;
using skyscraper5.Ietf.Rfc971;
using skyscraper5.Mheg5;
using skyscraper5.Mhp.Descriptors;
using skyscraper5.Mhp.Descriptors.InteractionTransportSelectors;
using skyscraper5.Mhp.Si;
using skyscraper5.Mhp.Si.Model;
using skyscraper5.Mpeg2;
using skyscraper5.Mpeg2.Descriptors;
using skyscraper5.Mpeg2.Psi;
using skyscraper5.Mpeg2.Psi.Model;
using skyscraper5.Rds;
using skyscraper5.Rds.Messages;
using skyscraper5.Scte35;
using skyscraper5.Skyscraper.IO;
using skyscraper5.Skyscraper.Net;
using skyscraper5.Skyscraper.Plugins;
using skyscraper5.Skyscraper.Scraper.FrameGrabber;
using skyscraper5.Skyscraper.Scraper.Storage.Utilities;
using skyscraper5.Skyscraper.Scraper.StreamAutodetection;
using skyscraper5.Skyscraper.Scraper.Utils;
using skyscraper5.src.Id3;
using skyscraper5.src.InteractionChannel;
using skyscraper5.src.InteractionChannel.Model;
using skyscraper5.src.InteractionChannel.Model.Descriptors;
using skyscraper5.src.InteractionChannel.Model2;
using skyscraper5.src.Mpeg2;
using skyscraper5.src.Mpeg2.PacketFilter;
using skyscraper5.src.Skyscraper.Scraper.Dns;
using skyscraper5.src.Teletext;
using skyscraper5.T2MI;
using skyscraper5.T2MI.Packets;
using skyscraper5.T2MI.Packets.AdressingFunctions;
using skyscraper5.Teletext;
using skyscraper5.Teletext.Vps;
using skyscraper5.Teletext.Wss;
using skyscraper8.Abertis;
using skyscraper8.DvbI;
using skyscraper8.DvbNip;
using skyscraper8.DvbNip.UiIntegration;
using skyscraper8.DvbSis;
using skyscraper8.Experimentals.NdsSsu;
using skyscraper8.Experimentals.OtvSsu;
using skyscraper8.GS;
using skyscraper8.GSE;
using skyscraper8.Ieee802_1AB;
using skyscraper8.Ietf.FLUTE;
using skyscraper8.Ietf.Rfc4236_ULE;
using skyscraper8.InteractionChannel.Model;
using skyscraper8.InteractionChannel.Model2;
using skyscraper8.InteractionChannel.Model2.Descriptors;
using skyscraper8.ReutersWne;
using skyscraper8.Ses;
using skyscraper8.Skyscraper.Net;
using skyscraper8.Skyscraper.Scraper;
using skyscraper8.Skyscraper.Scraper.Storage;
using skyscraper8.T2MI;
using skyscraper8.T2MI.Packets;
using skyscraper8.yo3explorer;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Text;
using System.Threading;
using Tsubasa.IO;
using Platform = skyscraper5.Dvb.SystemSoftwareUpdate.Model.Platform;
using RntParser = skyscraper5.Dvb.TvAnytime.RntParser;

namespace skyscraper5.Skyscraper.Scraper
{
    public class SkyscraperContext : IPatEventHandler, IPmtEventHandler, INitEventHandler, ITeletextPageHandler,
        IBatEventHandler, ISdtEventHandler, ICatEventHandler, ITdtEventHandler, ITotEventHandler, ITsdtEventHandler,
        IEitEventHandler, IAitEventHandler, ISubtitleEventHandler,
        UpdateNotificationEventHandler, DataCarouselEventHandler, RdsEventHandler, IScte35EventHandler,
        IAutodetectionEventHandler, IRstEventHandler, IRntEventHandler, IMultiprotocolEncapsulationEventHandler, ObjectCarouselEventHandler, T2MIEventHandler,
		IDisposable, IFrameGrabberEventHandler, IntEventHandler, IRctEventHandler, ISkyscraperContext, IDocsisEventHandler, AbertisDecoderEventHandler, Id3Handler,
        InteractionChannelHandler, SgtEventHandler, IDvbNipEventHandler, UleEventHandler, OtvSsuHandler, NdsSsuHandler, ISubTsHandler, ILldpFrameHandler, SisHandler, IWneHandler
    {
        public const bool ALLOW_STREAM_TYPE_AUTODETECTION = true;
        public const bool ALLOW_FFMPEG_FRAMEGRABBER = true;

		private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.Name);

		public TsContext DvbContext { get; }
        public DataStorage DataStorage { get; }
        public ObjectStorage ObjectStorage { get; }


        public bool SourceIsDisk { get; set; }

        public PsiDecoder PatDecoder { get; private set; }
        public PsiDecoder CatDecoder { get; private set; }
        public PsiDecoder NitDecoder { get; private set; }
        public PsiDecoder Pid0x11Decoder { get; private set; }
        public PsiDecoder Pid0x12Decoder { get; private set; }
        public IPsiDecoderTransformer PmtDecoderTransformer { get; set; }
        public SkyscraperContext(TsContext dvbContext, DataStorage dataStorage = null, ObjectStorage objectStorage = null, int skipAtStart = 0, int? currentNetworkId = null, int? currentTransportStreamId = null)
        {
            TsDescriptorUnpacker descriptorUnpacker = TsDescriptorUnpacker.GetInstance();
            for (byte i = 0x80; i < 0xfe; i++)
            {
                descriptorUnpacker.SetUserDefined(i, true);
            }

            descriptorUnpacker.SetUserDefined(0xfe, true);

            DvbContext = dvbContext;


            //PAT
            PatDecoder = new PsiDecoder(0, new PatParser(this));
			dvbContext.RegisterPacketProcessor(0, PatDecoder);
            //PMT autodetect from PAT

            //CAT
            CatDecoder = new PsiDecoder(1, new CatParser(this));
			dvbContext.RegisterPacketProcessor(1, CatDecoder);

            //TSDT (experimental)
            dvbContext.RegisterPacketProcessor(2, new PsiDecoder(2, new TsdtParser(this)));

            //IPMP (experimental)
            dvbContext.RegisterPacketProcessor(3, new PsiDecoder(3, new IpmpParser(this)));

            //NIT
            NitDecoder = new PsiDecoder(0x10, new NitParser(this));
			dvbContext.RegisterPacketProcessor(0x10, NitDecoder);

            //SDT / BAT
            Pid0x11Decoder = new PsiDecoder(0x11, new Pid0x11Decoder(this, this));
			dvbContext.RegisterPacketProcessor(0x11, Pid0x11Decoder);

            //EIT
            //dvbContext.RegisterPacketProcessor(0x12, new PsiDecoder(0x12, new EitParser(this)));
            Pid0x12Decoder = new PsiDecoder(0x12, new Pid0x12Decoder(this));
			dvbContext.RegisterPacketProcessor(0x12, Pid0x12Decoder);

            //RST
            DvbContext.RegisterPacketProcessor(0x13, new PsiDecoder(0x13, new RstParser(this)));

            //TDT
            //TOT handled by TDT processor
            dvbContext.RegisterPacketProcessor(0x14, new PsiDecoder(0x14, new TimetableParser(this, this)));

            //RNT
            dvbContext.RegisterPacketProcessor(0x16, new PsiDecoder(0x16, new RntParser(this)));

            //DIT
            //PID 0x1E
            //won't need these. We operate on broadcasted stuff, so we shouldn't encounter partial TS

            //DIT
            //PID 0x1F
            //won't need these. We operate on broadcasted stuff, so we shouldn't encounter partial TS

            if (dataStorage == null)
            {
	            throw new ArgumentNullException(nameof(dataStorage));
            }
            DataStorage = dataStorage;

			if (objectStorage == null)
            {
	            throw new ArgumentNullException(nameof(objectStorage));
            }
			ObjectStorage = objectStorage;
            
            CurrentNetworkId = currentNetworkId;
            CurrentTransportStreamId = currentTransportStreamId;
            DsmCcsToLookFor = new Dictionary<ushort, byte>();
            SsusToLookFor = new List<KeyValuePair<ushort, byte>>();

            this.skipBufferRemain = skipAtStart;
        }

        public int? CurrentNetworkId { get; private set; }
        public int? CurrentTransportStreamId { get; private set; }

        public TeiFilter TeiFilter { get; private set; }
        public ScrambleFilter ScrambleFilter { get; private set; }
        public TeiOnOffFilter TeiOnOffFilter { get; private set; }
        public void InitalizeFilterChain(params IPacketFilter[] args)
        {
            if (DvbContext.FilterChain != null)
                throw new InvalidOperationException("The filter chain was already initalized.");
            if (TeiFilter == null)
                TeiFilter = new TeiFilter();
            if (ScrambleFilter == null)
                ScrambleFilter = new ScrambleFilter();
            if (TeiOnOffFilter == null)
                TeiOnOffFilter = new TeiOnOffFilter();

            List<IPacketFilter> result = new List<IPacketFilter>();
            result.AddRange(args);
            result.Add(TeiFilter);
            result.Add(ScrambleFilter);
            result.Add(TeiOnOffFilter);
            result.Sort(new PluginPrioritySorter());
            DvbContext.FilterChain = result;

            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.Append("IN");
            for (int i = 0; i < result.Count; i++)
            {
                stringBuilder.Append(" -> ");
                if (result[i] != null)
                {
                    stringBuilder.Append(result[i].GetType().Name);
                }
                else
                {
                    stringBuilder.Append("(null)");
                }
            }
            stringBuilder.Append(" -> OUT");
            logger.DebugFormat("Built filter chain: {0}", stringBuilder.ToString());
        }

        public int GetIpCommunicationParties()
        {
	        if (trafficInfos == null)
		        return 0;
	        else
		        return trafficInfos.Count;
        }

        public void IngestFromStream(Stream stream)
        {
            IngestFromStream(stream, true);
        }

        public void IngestFromStream(Stream stream, bool closeWhenDone = true)
        {
            byte[] buffer = new byte[188];
            int read = 0;
            while (true)
            {
                if (IsAbortConditionMet())
                    break;

                read = stream.Read(buffer, 0, 188);

				if (read != 188)
                {
                    logger.Warn(String.Format("Reading {0} bytes failed. Got {1} instead.", 188, read));
                    break;
                }

                try
                {
                    IngestSinglePacket(buffer);
                }
                catch (InvalidTsPacketException)
                {
                    break;
                }
            }

            if (closeWhenDone)
            {
                stream.Close();
            }
            runningEvents = null;
        }

        private bool skipBufferStarted;
        private int skipBufferRemain;
        private bool firstPacketDone = false;
        private ulong totalPacketsPushed;

        public void IngestSinglePacket(byte[] buffer)
        {
            if (buffer.Length != 188)
                throw new ArgumentException(String.Format("{0}.{1} != {2}", nameof(buffer), nameof(buffer.Length),
                    188));

            if (skipBufferRemain > 0)
            {
                if (!skipBufferStarted)
                {
                    LogEvent(SkyscraperContextEvent.StartPacketProcessing, String.Format("Skipping {0} packets before begin.", skipBufferRemain));
                    skipBufferStarted = true;
                }
                skipBufferRemain--;
            }

            if (!firstPacketDone)
            {
                LogEvent(SkyscraperContextEvent.StartPacketProcessing);
                if (buffer[0] == 0x47 && buffer[1] == 0x41 && buffer[2] == 0x18 && (buffer[3] & 0x10) != 0 && buffer[4] == 0x00 && buffer[5] == 0x80 && buffer[6] == 0x00)
                {
                    DvbContext.RegisterPacketProcessor(0x0118, new OldStreamReaderDetector());
                    SpecialTsType = 2;
                }
                if (buffer[0] == 0x47 && buffer[1] == 0x41 && buffer[2] == 0x0e && (buffer[3] & 0x10) != 0 && buffer[4] == 0x00 && buffer[5] == 0x80 && buffer[6] == 0x00)
                {
                    if (!DvbContext.IsPidProcessorPresent(0x010e))
                    {
                        DvbContext.RegisterPacketProcessor(0x010e, new Stid135BbFrameReader(CreateGsContext()));
                        UiJunction?.EnableUiFeature(SkyscraperUiFeature.BbframeAnalysis);
                        LogEvent(SkyscraperContextEvent.SpecialTsMode, "STiD135 encapsulated GS detected.");
                        SpecialTsType = 3;
                    }
                }
                firstPacketDone = true;
            }
            
            DvbContext.PushPacket(buffer);
            if (totalPacketsPushed++ == 1000)
            {
                CheckSpecialTs();
            }
        }

        private GsContextDto CreateGsContext()
        {
            //Build the object
	        GsContextDto child = new GsContextDto();
	        child.IpOutput = this;
	        child.TsOutput = this;
	        child.Rcs2Output = this;
            child.UiJunction = this.UiJunction;

            //for futureproofing
            PropertyInfo[] properties = typeof(GsContextDto).GetProperties();
            foreach (PropertyInfo propertyInfo in properties)
            {
                if (!propertyInfo.Name.Equals(nameof(child.UiJunction)))
                {
                    object? value = propertyInfo.GetValue(child);
                    if (value == null)
                    {
                        throw new SkyscraperException(String.Format("While creating the {0}, the {1} was not properly set. This is a bug, tell Fey.", nameof(GsContextDto), propertyInfo.Name));
                    }
                }
            }

            //actually return the object
	        return child;
        }

        private bool TestNullAllowed(PropertyInfo prop)
        {
			//AllowNull
			Attribute? attribute = prop.GetCustomAttribute(typeof(AllowNullAttribute));
            if (attribute != null)
                return true;
            else
                return false;
		}

		private DocsisPacketProcessor docsisPacketProcessor;
        public int SpecialTsType { get; private set; }
        private void CheckSpecialTs()
        {
			ulong[] pidStatistics = DvbContext.GetPidStatistics();
            if (pidStatistics == null)
            {
                totalPacketsPushed = 0;
                return;
            }
            int occupiedPids = pidStatistics.Where(x => x > 0).Count();
            int numTotal = (int)totalPacketsPushed;
            int num1fff = (int)pidStatistics[0x1fff];
            double percentage1fff = (double)num1fff * 100.0 / (double)numTotal;
            if (true)
            {
                if (pidStatistics[0x0118] > 0)
                {
                    int num118 = (int)pidStatistics[0x0118];
                    double p = (double)num118 * 100.0 / numTotal;
                    if (p > 58)
                    {
                        if (!DvbContext.IsPidProcessorPresent(0x0118))
                        {
                            DvbContext.RegisterPacketProcessor(0x0118, new OldStreamReaderDetector());
                            return;
                        }
                    }
                }
                if (pidStatistics[0x010e] > 0)
                {
                    int num10e = (int)pidStatistics[0x010e];
                    double p = (double)num10e * 100.0 / numTotal;
                    if (occupiedPids == 1)
                    {
                        if (p > 20)
                        {
                            if (!DvbContext.IsPidProcessorPresent(0x010e))
                            {
                                DvbContext.RegisterPacketProcessor(0x010e, new Stid135BbFrameReader(CreateGsContext()));
                                UiJunction?.EnableUiFeature(SkyscraperUiFeature.BbframeAnalysis);
                                LogEvent(SkyscraperContextEvent.SpecialTsMode, "STiD135 encapsulated GS detected.");
                                SpecialTsType = 3;
                                return;
                            }
                        }
                    }
                }
            }

			if (occupiedPids <= 2)
            {
                if (pidStatistics[0x1ffe] > 10 && num1fff > 10)
                {
                    docsisPacketProcessor = new DocsisPacketProcessor(this);
                    DvbContext.RegisterPacketProcessor(0x1ffe, docsisPacketProcessor);
                    UiJunction?.EnableUiFeature(SkyscraperUiFeature.DocsisAnalysis);
                    LogEvent(SkyscraperContextEvent.SpecialTsMode, "DOCSIS Carrier TS detected.");
					SpecialTsType = 4;
					return;
                }

                int num20 = (int)pidStatistics[0x0020];
                double percentage20 = (double)num20 * 100.0 / (double)numTotal;
                if (percentage20 > 99.0 && percentage1fff < 1.0)
                {
                    MultiprotocolEncapsulationDecoder blockstreamDecoder = new MultiprotocolEncapsulationDecoder(this);
                    DvbContext.RegisterPacketProcessor(0x0020, new PsiDecoder(0x0020,blockstreamDecoder));
                    UiJunction?.EnableUiFeature(SkyscraperUiFeature.BlockstreamAnalysis);
                    LogEvent(SkyscraperContextEvent.SpecialTsMode, "Blockstream Carrier TS detected.");
					SpecialTsType = 5;
					return;
                }
            }
        }


        [DebuggerStepThrough]
        private void LogEvent(SkyscraperContextEvent eventType, string name = null)
        {
            string loggerName = String.Format("{0}{1}", IsChild ? ChildName + ", " : "", eventType.ToString());
            LogManager.GetLogger(loggerName).Info(name);
            if (eventType != SkyscraperContextEvent.TdtTime && eventType != SkyscraperContextEvent.TotTime && eventType != SkyscraperContextEvent.Scte35TimeSignal && eventType != SkyscraperContextEvent.Rcs2TdtTime)
            {
				lastEventTimestamp = DateTime.Now;
			}
            EventsLogged++;
        }

        public ulong EventsLogged { get; private set; }
        private DateTime lastEventTimestamp;

        public void NetworkPidFromPat(int networkPid)
        {
            if (DvbContext.IsPidProcessorPresent(networkPid))
                return;

            DvbContext.RegisterPacketProcessor(networkPid, new PsiDecoder(networkPid, new NitParser(this)));
            LogEvent(SkyscraperContextEvent.NetworkPidFromPat);
        }

        private PmtTracker pmtTracker;

        public void ProgramMapPidFromPat(int pmtPid, ushort programId)
        {
            SpecialTsType = 1;
            if (CurrentNetworkId.HasValue && CurrentTransportStreamId.HasValue)
            {
                DataStorage.StorePatEntry(CurrentNetworkId.Value, CurrentTransportStreamId.Value, pmtPid, programId);
            }

            if (DvbContext.IsPidProcessorPresent(pmtPid))
                return;

            PsiDecoder pmtParser = new PsiDecoder(pmtPid, new PmtParser(this));
            if (PmtDecoderTransformer != null)
            {
                PmtDecoderTransformer.Transform(pmtParser);
            }

			DvbContext.RegisterPacketProcessor(pmtPid, pmtParser);
            LogEvent(SkyscraperContextEvent.ProgramMapPidFromPat, String.Format("PMT for program #{0} is on PID 0x{1:X4}", programId, pmtPid));
            UiJunction?.NotifyPatProgram(pmtPid, programId);

            if (pmtTracker == null)
	            pmtTracker = new PmtTracker();
            pmtTracker.AddPmtPid(pmtPid);
        }

        public void SetTransportStreamId(ushort transportStreamId)
        {
            if (!CurrentTransportStreamId.HasValue)
            {
                CurrentTransportStreamId = transportStreamId;
                return;
            }
        }

        private List<KeyValuePair<ushort, byte>> SsusToLookFor;
        private Dictionary<ushort, byte> DsmCcsToLookFor;

        public void PmtEvent(ProgramMapping result, int pmtPid)
        {
	        bool logworthy = false;
            if (CurrentNetworkId.HasValue && CurrentTransportStreamId.HasValue)
            {

                if (!DataStorage.TestForPmtEvent(CurrentNetworkId.Value, CurrentTransportStreamId.Value, result))
                {
                    if (DataStorage.StorePmtEvent(CurrentNetworkId.Value, CurrentTransportStreamId.Value, result))
                        logworthy = true;
                }

                if (result.CaSystemId.HasValue && result.CaPid.HasValue)
                {
                    CaDescriptor caDescriptor = new CaDescriptor(result.CaSystemId.Value, result.CaPid.Value, result.CaPrivateData);
                    NotifyOfCaSystem(caDescriptor, true);
                }
            }
            
            bool allPrivateStreams = true;
            foreach (ProgramMappingStream mappingStream in result.Streams)
            {
                if (!mappingStream.IsUserPrivateStream())
                {
                    allPrivateStreams = false;
                    break;
                }
            }

            byte madeUpComponentTags = 1;
            foreach (ProgramMappingStream mappingStream in result.Streams)
            {
                if (mappingStream.CaPid.HasValue)
                {
                    if (!DvbContext.IsPidProcessorPresent(mappingStream.CaPid.Value))
                    {
                        NotifyOfCaSystem(new CaDescriptor(mappingStream.CaSystemId.Value, mappingStream.CaPid.Value, mappingStream.CaPrivateData),true);
                    }
                }

                if (!DvbContext.IsPidProcessorPresent(mappingStream.ElementaryPid))
                {
                    List<KeyValuePair<ushort, byte>> ssuKvs = SsusToLookFor.FindAll(x => x.Key == result.ProgramNumber);
                    bool foundSsu = false;
                    foreach (KeyValuePair<ushort, byte> keyValuePair in ssuKvs)
                    {
                        byte ssuComponentTag = keyValuePair.Value;
                        if (ssuComponentTag == mappingStream.ComponentTag)
                        {
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PsiDecoder(mappingStream.ElementaryPid, new DataCarouselDecoder(mappingStream, result, DataCarouselIntention.SoftwareUpdate, this)));
                            SsusToLookFor.Remove(keyValuePair);
                            foundSsu = true;
                            break;
                        }
                    }

                    if (foundSsu)
                        continue;


                    StreamType guessedStreamType = GuessStreamType(mappingStream, result.Streams.Count, result.RegistrationFormatIdentifier, allPrivateStreams);
                    switch (guessedStreamType)
                    {
                        case StreamType.Video:
                            ITsPacketProcessor videoPacketProcessor = null;
                            if (ALLOW_FFMPEG_FRAMEGRABBER)
                            {
                                if (TcpProxyEnabled)
                                {
                                    if (!result.CaPid.HasValue)
                                    {
                                        if (!mappingStream.CaPid.HasValue)
                                        {
                                            if (CurrentNetworkId.HasValue && CurrentTransportStreamId.HasValue)
                                            {
                                                if (!ObjectStorage.TestForFramegrab(CurrentNetworkId.Value, CurrentTransportStreamId.Value, result.ProgramNumber, mappingStream.ElementaryPid))
                                                {
                                                    ffmpegFrameGrabber ffmfg = new ffmpegFrameGrabber(CurrentNetworkId.Value, CurrentTransportStreamId.Value, result, mappingStream, this);
                                                    ffmfg.TsProxyEndPoint = DvbContext.GetTcpProxyEndPoint();
                                                    videoPacketProcessor = ffmfg;
                                                }
                                                else
                                                {
                                                    videoPacketProcessor = new PacketDiscarder();
                                                }
                                            }
                                        }
                                    }
                                }
                            }

                            if (videoPacketProcessor != null)
                            {
                                DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, videoPacketProcessor);
                            }

                            if (!DvbContext.IsPidProcessorPresent(mappingStream.ElementaryPid))
                            {
                                DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PacketDiscarder());
                            }

                            break;
                        case StreamType.Teletext:
                            if (!CurrentNetworkId.HasValue)
                                break;
                            if (!CurrentTransportStreamId.HasValue)
                                break;
                            TeletextPesProcessor ttp = new TeletextPesProcessor(this, CurrentNetworkId.Value, CurrentTransportStreamId.Value, result.ProgramNumber, mappingStream.ElementaryPid);
                            if (result.PrivateDataSpecifier.HasValue)
                                ttp.PrivateDataSpecifier = result.PrivateDataSpecifier;
                            if (mappingStream.PrivateDataSpecifier.HasValue)
                                ttp.PrivateDataSpecifier = mappingStream.PrivateDataSpecifier;
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PesDecoder(ttp));
                            break;
                        case StreamType.Audio:
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PacketDiscarder());
                            break;
                        case StreamType.Application:
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid,
                                new PsiDecoder(mappingStream.ElementaryPid, new AitParser(this, result.ProgramNumber)));
                            break;
                        case StreamType.Ignorable:
                            break;
                        case StreamType.HbbTv:
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid,
                                new PsiDecoder(mappingStream.ElementaryPid,
                                    new DataCarouselDecoder(mappingStream, result, DataCarouselIntention.NoIntention, this)));
                            break;
                        case StreamType.SystemSoftwareUpdate:
                            SystemSoftwareUpdateInfo ssuInfo =
                                new SystemSoftwareUpdateInfo(mappingStream.DataBroadcastSelector);
                            if (ssuInfo.Valid)
                                TryRegisterSystemSoftwareUpdateInfo(ssuInfo, mappingStream, result);
                            break;
                        case StreamType.Subtitles:
                            PesDecoder pesDecoder = new PesDecoder(new SubtitleDecoder(this));
                            pesDecoder.Tag = "subs";
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, pesDecoder);
                            //DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PacketDumper(new FileInfo("subtitle.ts")));
                            break;
                        case StreamType.DvbMhp:
							//Test and make up component tags.
							if (!mappingStream.ComponentTag.HasValue)
								MakeUpComponentTags(result);
							DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid,
                                new PsiDecoder(mappingStream.ElementaryPid,
                                    new DataCarouselDecoder(mappingStream, result, DataCarouselIntention.NoIntention, this)));
                            break;
                        case StreamType.Scte35:
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid,
                                new PsiDecoder(mappingStream.ElementaryPid,
                                    new Scte35SiDecoder(result.ProgramNumber, this)));
                            break;
                        case StreamType.RDS:
                            RdsPesProcessor rdsPesProcessor = new RdsPesProcessor(mappingStream.ElementaryPid,
                                result.ProgramNumber, this);
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid,
                                new PesDecoder(rdsPesProcessor));
                            break;
                        case StreamType.MultiprotocolEncapsulation:
                            MultiprotocolEncapsulationDecoder mpeDec = new MultiprotocolEncapsulationDecoder(this);
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid,
                                new PsiDecoder(mappingStream.ElementaryPid, mpeDec));
                            break;
                        case StreamType.Mheg5:
                            Mheg5DataBroadcastIdSelector selector =
                                new Mheg5DataBroadcastIdSelector(mappingStream.DataBroadcastSelector);
                            //These looked like Object Carousels to me
                            if (!mappingStream.ComponentTag.HasValue)
                                MakeUpComponentTags(result);
                            //ObjectCarouselDecoder ocd = new ObjectCarouselDecoder(mappingStream.ElementaryPid, this, result, mappingStream.ComponentTag.Value);
                            DataCarouselDecoder dcd = new DataCarouselDecoder(mappingStream, result, DataCarouselIntention.NoIntention, this);
                            //ocd.Tag = StreamType.Mheg5;
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PsiDecoder(mappingStream.ElementaryPid, dcd));
                            break;
                        case StreamType.TryAutodetect:
                            StreamTypeAutodetection sta = new StreamTypeAutodetection(mappingStream.ElementaryPid, this);
                            sta.ProgramContext.Program = result;
                            sta.ProgramContext.Stream = mappingStream;
                            sta.IntroduceStreamToContestants();
                            if (!sta.IsWinnerDetermined)
                            {
	                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, sta);
                            }
                            break;
                        case StreamType.IpMacNotification:
                            IntParser intParser = new IntParser(this);
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PsiDecoder(mappingStream.ElementaryPid, intParser));
                            break;
                        case StreamType.RelatedContentTable:
                            RctParser rctParser = new RctParser(this, result.ProgramNumber);
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PsiDecoder(mappingStream.ElementaryPid, rctParser));
                            break;
                        case StreamType.T2Mi:
                            T2MIDecoder t2miDecoder = new T2MIDecoder(mappingStream.ElementaryPid, this);
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, t2miDecoder);
                            break;
                        case StreamType.InteractionChannelForSatellite:
                            InteractionChannelPsiGatherer interactionChannelDecoder = new InteractionChannelPsiGatherer();
                            interactionChannelDecoder.ExpectedTables = mappingStream.UnknownUserDefines[0];
                            interactionChannelDecoder.Handler = this;
							DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PsiDecoder(mappingStream.ElementaryPid, interactionChannelDecoder));
                            break;

                        case StreamType.Id3:
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PesDecoder(new Id3PesProcessor(this)));
                            break;
                        case StreamType.SisFramingAndTiming:
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new FtiHandler(mappingStream.ElementaryPid, this, this));
                            break;
                        case StreamType.SisDaughterSiteAdapterConfiguration:
                            DsAciHandler dsaciHandler = new DsAciHandler(this);
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PsiDecoder(mappingStream.ElementaryPid, dsaciHandler));
                            break;
                        case StreamType.SisPsiTables:
                            SisPsiHandler sisPsiHandler = new SisPsiHandler(this, result.ProgramNumber);
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PsiDecoder(mappingStream.ElementaryPid, sisPsiHandler));
                            break;
                        default:
                            throw new NotImplementedException(String.Format(
                                "Detection of Stream type for PID 0x{0:X4} in Service {1} - was detected as {2}, but handling for {2} is not implemented yet.", pmtPid,
                                result.ProgramNumber, guessedStreamType));
                    }
                }
            }

            if (logworthy)
                LogEvent(SkyscraperContextEvent.PmtEvent, String.Format("#{0}", result.ProgramNumber));

            UiJunction?.NotifyPmtProgram(result, pmtPid);

            if (pmtTracker != null)
            {
                pmtTracker.MarkAsProcessed(pmtPid);
                if (pmtTracker.ShouldFireAutodetection())
                {
                    CheckForHiddenMpes();
                }
            }
        }

        public void MakeUpComponentTags(ProgramMapping target)
        {
            bool[] usedTags = new bool[byte.MaxValue];
            foreach (ProgramMappingStream programMappingStream in target.Streams)
            {
                if (programMappingStream.ComponentTag.HasValue)
                {
                    usedTags[programMappingStream.ComponentTag.Value] = true;
                }
            }

            usedTags[0] = true;

            foreach (ProgramMappingStream mappingStream in target.Streams)
            {
                if (!mappingStream.ComponentTag.HasValue)
                {
                    for (byte b = 0; b < Byte.MaxValue; b++)
                    {
                        if (!usedTags[b])
                        {
                            usedTags[b] = true;
                            mappingStream.ComponentTag = b;
                            break;
                        }
                    }
                }
            }
        }

        private void TryRegisterSystemSoftwareUpdateInfo(SystemSoftwareUpdateInfo info,
            ProgramMappingStream mappingStream, ProgramMapping mapping)
        {
            if (DvbContext.IsPidProcessorPresent(mappingStream.ElementaryPid))
                return;

            //TODO: Scrape this once we have an SSU Event handler.
            foreach (SystemSoftwareUpdateInfo.Oui infoOui in info.Ouis)
            {
                switch (infoOui.UpdateType)
                {
                    //Found on ETSI TS 102 006, Page 13
                    case 0: //proprietary update solution
                        if (ALLOW_STREAM_TYPE_AUTODETECTION)
                        {
                            StreamTypeAutodetection sta = new StreamTypeAutodetection(mappingStream.ElementaryPid, this);
                            sta.ProgramContext.Stream = mappingStream;
                            sta.ProgramContext.Program = mapping;
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, sta);
                        }
                        else
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PacketDiscarder());
                        break;
                    case 1: //standard update carousel (i.e. without notification table) via broadcast
                        if (!mappingStream.ComponentTag.HasValue)
                        {
                            MakeUpComponentTags(mapping);
                        }

                        DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PsiDecoder(mappingStream.ElementaryPid, new DataCarouselDecoder(mappingStream, mapping, DataCarouselIntention.SoftwareUpdate, this)));
                        break;
                    case 2: //system software update carousel with notification table (UNT) both available via broadcast
                    case 3: //system software update signalled via broadcast UNT, update available from the return channel
                    case 4: //system software update signalled via broadcast UNT, update available from the internet
                        DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PsiDecoder(mappingStream.ElementaryPid, new UntDecoder(this, mapping)));
                        break;
                    case 5:
                    case 6:
                    case 7:
                    case 8:
                    case 9:
                    case 10:
                    case 11:
                    case 12:
                    case 13:
                    case 14:
                    case 15:
                        //reserved for future use as of 2015-06
                        //ignore it.
                        break;
                    default:
                        throw new NotImplementedException(String.Format("{0} = {1:X2}", nameof(infoOui.UpdateType),
                            infoOui.UpdateType));
                }
            }
        }
        
        private StreamType GuessStreamType(ProgramMappingStream stream, int totalStreams,
            uint? parentRegistrationFormatIdentifier, bool allPrivateStreams)
        {
            if (stream.StreamType == PmtStreamType.H262)
                return StreamType.Video;
            if (stream.AvcStillPresent.HasValue)
                return StreamType.Video;
            if (stream.AudioType.HasValue)
                return StreamType.Audio;
            if (stream.StreamType == PmtStreamType.AvcVideoStream)
                return StreamType.Video;
            if (stream.StreamType == PmtStreamType.Iso11172Audio)
                return StreamType.Audio;
            if (stream.StreamType == PmtStreamType.Iso13818_3Audio)
                return StreamType.Audio;
            if (stream.StreamType == PmtStreamType.Iso13818_7AudioADTS)
                return StreamType.Audio;
            if (stream.StreamType == PmtStreamType.HevcVideoStream)
                return StreamType.Video;
            if (stream.StreamType == PmtStreamType.Iso13818_1PesPackets && stream.Ac4ChannelMode.HasValue)
                return StreamType.Audio; //AC-4 Audio
            if (stream.StreamType == PmtStreamType.Iso13818_1PesPackets && stream.BSID.HasValue)
                return StreamType.Audio;
            if (stream.StreamType == PmtStreamType.Iso14496_3Audio && stream.AacProfileAndLevel.HasValue)
                return StreamType.Audio;
            if ((int)stream.StreamType == 0x81 && stream.ComponentType.HasValue)
                return StreamType.Audio; //AC-3 Audio, but weird.
            if (stream.Teletexts != null)
            {
                for (int i = 0; i < stream.Teletexts.Length; i++)
                {
                    if (stream.Teletexts[i] != null)
                    {
                        return StreamType.Teletext;
                    }
                }
            }

            if (stream.Applications != null)
            {
                return StreamType.Application;
            }

            if (stream.DataBroadcastId == 0x0123)
            {
                return StreamType.HbbTv;
            }

            if (stream.DataBroadcastId == 0x000a)
            {
                return StreamType.SystemSoftwareUpdate;
            }

            if (!ALLOW_STREAM_TYPE_AUTODETECTION)
            {
                if ((int)stream.StreamType == 0xc0 || (int)stream.StreamType == 0xc1)
                {
                    if (stream.PrivateDataSpecifier.HasValue)
                        throw new NotImplementedException(String.Format("{0}", nameof(stream.PrivateDataSpecifier)));
                    else
                        return StreamType.Ignorable;
                }
            }

            if (stream.StreamType == PmtStreamType.Iso13818_1PesPackets && stream.Subtitlings != null &&
                stream.Subtitlings.Length > 0)
            {
                return StreamType.Subtitles;
            }

            if (stream.DataBroadcastId == 0xf0)
            {
                return StreamType.DvbMhp;
            }

            if (!ALLOW_STREAM_TYPE_AUTODETECTION)
            {
                if (!stream.DataBroadcastId.HasValue && stream.StreamType == PmtStreamType.Iso13818_1PrivateSections &&
                    totalStreams == 1)
                {
                    return StreamType.Ignorable;
                }
            }

            if (stream.FormatIdentifier.HasValue && stream.FormatIdentifier == 0x43554549 &&
                (int)stream.StreamType == 0x86)
            {
                return StreamType.Scte35;
            }

            if ((int)stream.StreamType == 0x86 && parentRegistrationFormatIdentifier.HasValue &&
                parentRegistrationFormatIdentifier.Value == 0x43554549)
            {
                return StreamType.Scte35;
            }

            if (stream.DataBroadcastId == 0x0007) //According to dvbservices.com, this is an Object Carousel
            {
                return StreamType.DvbMhp;
            }

            if (stream.StreamType == PmtStreamType.Iso13818_1PesPackets && stream.VbiData != null)
            {
                return StreamType.Teletext;
            }

            if ((byte)stream.StreamType == 0x89 && stream.AncillaryDataDescriptor != null &&
                stream.AncillaryDataDescriptor.RdsOnly)
                return StreamType.RDS;

            if (!ALLOW_STREAM_TYPE_AUTODETECTION)
            {
                if (stream.StreamType == PmtStreamType.Iso13818_6TypeB && !stream.DataBroadcastId.HasValue)
                    return StreamType.Ignorable;

                if (stream.StreamType == PmtStreamType.Iso13818_6TypeC && !stream.DataBroadcastId.HasValue)
                    return StreamType.Ignorable;

                if (stream.DataBroadcastId.HasValue && stream.DataBroadcastId.Value == 0x0140)
                {
                    //Proprietary by CANAL+, does not seem to be documented.
                    return StreamType.Ignorable;
                }
            }

            if (stream.DataBroadcastId.HasValue && stream.DataBroadcastId.Value == 0x0005 /*&& stream.StreamType == PmtStreamType.Iso13818_6TypeD*/)
            {
                return StreamType.MultiprotocolEncapsulation;
            }

            if (!ALLOW_STREAM_TYPE_AUTODETECTION)
            {
                if (allPrivateStreams && !stream.DataBroadcastId.HasValue && !stream.PrivateDataSpecifier.HasValue &&
                    !parentRegistrationFormatIdentifier.HasValue)
                    return StreamType.Ignorable;

                if (stream.DataBroadcastId == 0x010b)
                {
                    //NDS France Technologies system software download
                    //sadly undocumented.
                    return StreamType.Ignorable;
                }

                if (stream.StreamType == PmtStreamType.Iso13818_1PesPackets && totalStreams == 1 &&
                    !stream.DataBroadcastId.HasValue && !stream.PrivateDataSpecifier.HasValue)
                {
                    //A single private stream. We have no clue how to deal with it, at all.
                    return StreamType.Ignorable;
                }
            }

            if (stream.DataBroadcastId == 0x0106)
            {
                //Defined in ETSI ES 202 184 V2.4.1
                return StreamType.Mheg5;
            }

            if (stream.DataBroadcastId == 0x000b)
            {
                return StreamType.IpMacNotification;
            }

            if (stream.RelatedContentDescriptorPresent.HasValue)
            {
                if (stream.RelatedContentDescriptorPresent.Value && stream.StreamType == PmtStreamType.Iso13818_1PrivateSections)
                {
                    return StreamType.RelatedContentTable;
                }
            }

            if (stream.NumT2MiStreams.HasValue && stream.StreamType == PmtStreamType.Iso13818_1PesPackets)
            {
                return StreamType.T2Mi;
            }

            if (stream.UnknownUserDefines != null)
            {
                if (stream.UnknownUserDefines.Count == 1)
                {
                    if (stream.UnknownUserDefines[0].DescriptorTag == 0xa7)
                    {
                        bool interactionChannelPossible = stream.UnknownUserDefines[0].Data.All(x => x == 0x40 || x == 0x41 || x == 0x4d || x == 0x70 || x >= 0xA0);
                        if (interactionChannelPossible)
                        {
                            
                            return StreamType.InteractionChannelForSatellite;
                        }
                    }
                }
            }
            

            if (stream.MetadataApplicationFormat == 0xffff && stream.MetadataApplicationFormatIdentifier == 0x49443320 && stream.MetadataFormat == 0xff && stream.MetadataFormatIdentifier == 0x49443320)
            {
				return StreamType.Id3;
            }

            if (stream.StreamType == PmtStreamType.Iso13818_1PesPackets && stream.DataBroadcastId == 0x000e && stream.DataBroadcastSelector != null)
            {
                if (stream.DataBroadcastSelector[0] == 0x01)
                {
                    return StreamType.SisFramingAndTiming;
                }
                if (stream.DataBroadcastSelector[0] == 0x02)
                {
                    return StreamType.SisDaughterSiteAdapterConfiguration;
                }
                if (stream.DataBroadcastSelector[0] == 0x03)
                {
                    return StreamType.SisPsiTables;
                }
            }

            if (ALLOW_STREAM_TYPE_AUTODETECTION)
            {
                //Abandon all hope, ye who enter here...
                return StreamType.TryAutodetect;
            }

            return StreamType.Unknown;
        }

        private int onidHits, onidMisses;

        public void SetNetworkId(ushort networkId)
        {
            SetNetworkId(networkId, false);
        }

        public void SetNetworkId(ushort networkId, bool forceOverwrite = false)
        {
            if (CurrentNetworkId == null)
            {
                CurrentNetworkId = networkId;
                return;
            }

            if (CurrentNetworkId != networkId)
            {
                onidMisses++;
                if (forceOverwrite)
                {
                    CurrentNetworkId = networkId;
                }
            }
            else
                onidHits++;
        }

        private EitEvent[] runningEvents;
        public void OnEitEvent(EitEvent eitEvent)
        {
            if (DataStorage.StoreEitEvent(eitEvent))
                LogEvent(SkyscraperContextEvent.EitEvent, eitEvent.EventName);

            UiJunction?.NotifyEvent(eitEvent);

            if (eitEvent.RunningStatus == RunningStatus.Running)
            {
                if (runningEvents == null)
                    runningEvents = new EitEvent[UInt16.MaxValue];
                runningEvents[eitEvent.ServiceId] = eitEvent;
            }
        }

        public void OnNitTransportStream(ushort networkId, NitTransportStream transportStream)
        {
            string name;
            switch (transportStream.DeliveryMethod)
            {
                case NitTransportStream.TransportMedium.DVB_S:
                case NitTransportStream.TransportMedium.DVB_S2:
                    name = String.Format("{3}°{4}/{0}/{1}/{2}", transportStream.Frequency / 100,
                        transportStream.Polarization.ToString().Substring(0, 1),
                        (float)transportStream.SymbolRate / 10.0f, transportStream.OrbitalPosition,
                        transportStream.East.Value ? "E" : "W");
                    break;
                case NitTransportStream.TransportMedium.DVB_T2:
                    if (transportStream.Frequency != null)
                    {
                        throw new NotImplementedException();
                    }
                    if (transportStream.CellInfos == null)
                        return;
                    if (transportStream.CellInfos.Count == 0)
                        return;
                    name = String.Format("Center Frequency: {0}", transportStream.GetT2Frequency());
                    break;
                case NitTransportStream.TransportMedium.Unknown:
                    return;
                case NitTransportStream.TransportMedium.DVB_C:
                    name = String.Format("{0}/{1}/{2}", transportStream.Frequency / 100000, transportStream.SymbolRate,
                        transportStream.RenderModulation());
                    break;
                default:
                    throw new NotImplementedException(transportStream.DeliveryMethod.ToString());
            }

            if (!DataStorage.TestForNitTransportStream(networkId, transportStream))
            {
                DataStorage.StoreNitTransportStream(networkId, transportStream);
                LogEvent(SkyscraperContextEvent.OnNitTransportStream, name);
                return;
            }

            if (DataStorage.UpdateNitTransportStream(networkId, transportStream))
            {
                LogEvent(SkyscraperContextEvent.OnNitTransportStreamUpdate, name);
                return;
            }
        }

        public void OnNitNetwork(NitNetwork nitNetwork)
        {
            UiJunction?.NotifyNit(nitNetwork);
            if (nitNetwork.XaitPid.HasValue && CurrentNetworkId.HasValue)
            {
                ushort nitNetworkXaitPid = nitNetwork.XaitPid.Value;
                if (nitNetwork.NetworkId == CurrentNetworkId.Value)
                {
                    if (!DvbContext.IsPidProcessorPresent(nitNetworkXaitPid))
                    {
                        AitParser aitParser = new AitParser(this, 0x00);
                        aitParser.Tag = "XAIT";
                        DvbContext.RegisterPacketProcessor(nitNetworkXaitPid,
                            new PsiDecoder(nitNetworkXaitPid, aitParser));
                        LogEvent(SkyscraperContextEvent.XaitDetected, String.Format("PID {0}", nitNetworkXaitPid));
                    }
                }
            }

            if (!DataStorage.TestForNitNetwork(nitNetwork))
            {
                DataStorage.StoreNitNetwork(nitNetwork);
                LogEvent(SkyscraperContextEvent.OnNitNetwork, nitNetwork.Name);
                return;
            }

            if (DataStorage.UpdateNitNetwork(nitNetwork))
            {
                LogEvent(SkyscraperContextEvent.OnNitNetworkUpdate, nitNetwork.Name);
                return;
            }
        }


        private HashSet<VpsCoordinate> _vpsCoordinates;

        public void OnVpsData(int networkId, int transportStreamId, ushort programNumber, VpsDataBlock vpsDataField, int elementaryPid)
        {
            //We don't really need VPS data, but we'll use it as another way of making sure we
            //got everything.
            if (_vpsCoordinates == null)
            {
                _vpsCoordinates = new HashSet<VpsCoordinate>();
            }

            VpsCoordinate vpsCoordinate = new VpsCoordinate(programNumber, vpsDataField.Month, vpsDataField.Day,
                vpsDataField.Hour, vpsDataField.Minute);
            if (_vpsCoordinates.Add(vpsCoordinate))
            {
                LogEvent(SkyscraperContextEvent.VpsData,
                    String.Format("{0:D2}.{1:D2} {2:D2}:{3:D2} {4}", vpsCoordinate.Day, vpsCoordinate.Month,
                        vpsCoordinate.Hour, vpsCoordinate.Minute, vpsCoordinate.ProgramNumber));
            }
        }


        private Dictionary<ushort, WssDataBlock> _wssDataBlocks;

        public void OnWssData(int networkId, int transportStreamId, ushort programNumber, WssDataBlock wssDataBlock, int elementaryPid)
        {
            if (_wssDataBlocks == null)
            {
                _wssDataBlocks = new Dictionary<ushort, WssDataBlock>();
            }

            if (!_wssDataBlocks.ContainsKey(programNumber))
            {
                _wssDataBlocks.Add(programNumber, wssDataBlock);
                LogEvent(SkyscraperContextEvent.WssData, programNumber.ToString());
                UiJunction?.NotifyWss(programNumber, wssDataBlock, elementaryPid);
            }
        }

        public void OnTeletextPage(int networkId, int transportStreamId, ushort programNumber,
            TeletextMagazine magazine)
        {
            if (!currentTime.HasValue)
                return;

            DataStorage.StoreTeletextPage(networkId, transportStreamId, programNumber, magazine, currentTime.Value);
        }

        public void OnBatBouquet(BatBouquet batBouquet)
        {
            UiJunction?.NotifyBat(batBouquet);
            if (DataStorage.TestForBatBouquet(batBouquet))
            {
                if (DataStorage.UpdateBatBouquet(batBouquet))
                {
                    LogEvent(SkyscraperContextEvent.BatBouquetUpdate, batBouquet.BouquetName);
                }
            }
            else
            {
	            DataStorage.StoreBatBouquet(batBouquet);
                LogEvent(SkyscraperContextEvent.BatBouquet, batBouquet.BouquetName);
            }
        }

        public void OnBatTransportStream(BatBouquet batBouquet, BatTransportStream child)
        {
            string name = String.Format("{0},{1}", batBouquet.BouquetId, child.OriginalNetworkId);
            if (DataStorage.TestForBatTransportStream(batBouquet.BouquetId, child))
            {
                if (DataStorage.UpdateBatTransportStream(batBouquet.BouquetId, child))
                {
                    LogEvent(SkyscraperContextEvent.BatTransportStreamUpdate, name);
                }
            }
            else
            {
	            DataStorage.StoreBatTransportStream(batBouquet.BouquetId, child);
                LogEvent(SkyscraperContextEvent.BatTransportStream, name);
            }
        }

        public void OnSdtService(ushort transportStreamId, ushort originalNetworkId, SdtService sdtService)
        {
            if (originalNetworkId == 1)
            {
                if (transportStreamId == 1)
                {
                    if (!DvbContext.IsPidProcessorPresent(0x0dde))
                    {
                        if (sdtService.ServiceName.Equals("REUTERS WNE"))
                        {
                            logger.InfoFormat("Found REUTERS WNE, registering PID 0x0dde");
                            DvbContext.RegisterPacketProcessor(0x0dde, new PsiDecoder(0xdde,new MultiprotocolEncapsulationDecoder(this)));
                        }
                    }
                }
            }
            if (DataStorage.TestForSdtService(transportStreamId, originalNetworkId, sdtService))
            {
                if (!IsChild)
                {
                    if (DataStorage.UpdateSdtService(transportStreamId, originalNetworkId, sdtService))
                    {
                        LogEvent(SkyscraperContextEvent.OnSdtServiceUpdate, sdtService.ServiceName);
                    }
                }
            }
            else
            {
	            DataStorage.StoreSdtService(transportStreamId, originalNetworkId, sdtService);
                LogEvent(SkyscraperContextEvent.OnSdtService, sdtService.ServiceName);
            }

            UiJunction?.NotifySdtService(sdtService);
        }

        private DateTime? currentTime;
        private DateTime? currentTimeForTim;

        public void SetCurrentTime(DateTime currentTime)
        {
            if (!this.currentTime.HasValue)
            {
                this.currentTime = currentTime;
            }
            currentTimeForTim = currentTime;
        }

        public void SetCurrentProgrammeType(int programNumber, PTY.ProgrammeTypeCodes pty)
        {
            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (DataStorage.UpdateRdsPty(CurrentNetworkId.Value, CurrentTransportStreamId.Value, programNumber, pty))
            {
                LogEvent(SkyscraperContextEvent.RdsPty, String.Format("{0} -> {1}", programNumber, pty.ToString()));
            }
        }

        public void MarkAsRdsTrafficInformationProgramme(int programNumber)
        {
            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (DataStorage.MarkAsRdsTrafficInformationProgramme(CurrentNetworkId.Value, CurrentTransportStreamId.Value, programNumber))
            {
                LogEvent(SkyscraperContextEvent.RdsTrafficInfoDetected, String.Format("{0}", programNumber));
            }
        }

        public void OnTotTime(DateTime utcTime, LocalTimeOffsetDescriptor ltod)
        {
            UiJunction?.NotifyTot(utcTime, ltod);
            SetCurrentTime(utcTime);

            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (DataStorage.UpdateTimeOffsetTable(CurrentNetworkId.Value, CurrentTransportStreamId.Value, utcTime,
                    ltod))
                LogEvent(SkyscraperContextEvent.TotTime, utcTime.ToString());
        }

        public void OnTdtTime(DateTime utcTime)
        {
            UiJunction?.NotifyTdt(utcTime);
            SetCurrentTime(utcTime);

            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (DataStorage.UpdateTimeAndDate(CurrentNetworkId.Value, CurrentTransportStreamId.Value, utcTime))
                LogEvent(SkyscraperContextEvent.TdtTime, utcTime.ToString());
        }
        
        public void OnAitApplication(AitApplication aitApplication, ushort programNumber)
        {
            UiJunction?.NotifyAit(aitApplication);
            foreach (TransportProtocolDescriptor transportProtocol in aitApplication.TransportProtocols)
            {
                switch (transportProtocol.ProtocolId)
                {
                    case 0x01:
                        ObjectCarouselTransportSelector octs =
                            (ObjectCarouselTransportSelector)transportProtocol.Selector;
                        DsmCcsToLookFor[programNumber] = octs.ComponentTag;
                        break;
                    case 0x03:
                        //The interwebs!
                        break;
                    default:
                        if (transportProtocol.Selector == null)
                            continue;
                        throw new NotImplementedException(String.Format("{0} {1:x2}",
                            nameof(transportProtocol.ProtocolId), transportProtocol.ProtocolId));
                }
            }

            if (!DataStorage.TestForAitApplication(aitApplication.ApplicationIdentifier))
            {
	            DataStorage.StoreAitApplication(aitApplication);
                LogEvent(SkyscraperContextEvent.AitApplication, aitApplication.TryGetName());
            }
        }

        public void NotifyFileArrival(VfsFile vfsFile)
        {
            if (!CurrentTransportStreamId.HasValue)
                return;
            if (!CurrentNetworkId.HasValue)
                return;

            UiJunction?.NotifyTransportStreamId(CurrentTransportStreamId.Value, CurrentNetworkId.Value);
			UiJunction?.DsmCcVfs(vfsFile);

			if (ObjectStorage.ObjectCarouselFileArrival(vfsFile, CurrentTransportStreamId.Value, CurrentNetworkId.Value))
            {
                LogEvent(SkyscraperContextEvent.FileArrival, String.Format("PID {0:X4}, Path {1}", vfsFile.SourcePid, vfsFile.ToString()));
            }

        }

        public void NotifySubStream(ProgramMapping programMapping, Tap tap, ushort[] eventIds, Dictionary<uint, string> context, EventList_T eventListT, Info_T infoT)
        {
            foreach (ProgramMappingStream mappingStream in programMapping.Streams)
            {
                if (mappingStream.ComponentTag.HasValue)
                {
                    ushort l = mappingStream.ComponentTag.Value;
                    ushort r = tap.AssociationTag;
                    if (l == r)
                    {
                        if (!DvbContext.IsPidProcessorPresent(mappingStream.ElementaryPid))
                        {
                            //DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PsiDecoder(mappingStream.ElementaryPid, new ObjectCarouselDecoder(mappingStream.ElementaryPid, this, programMapping, mappingStream.ComponentTag.Value)));
                            DvbContext.RegisterPacketProcessor(mappingStream.ElementaryPid, new PsiDecoder(mappingStream.ElementaryPid, new DataCarouselDecoder(mappingStream, programMapping, DataCarouselIntention.NoIntention, this)));
                            LogEvent(SkyscraperContextEvent.SubStreamFromObjectCarousel, String.Format("PID {0:X4}", mappingStream.ElementaryPid));
                        }
                    }
                }
            }
        }

        public void NotifyOfCaSystem(CaDescriptor caDescriptor, bool fromPmt = false)
        {
            if (caDescriptor.CaSystemId == 0x0000)
                return; //Invalid CAID

            if (!CurrentNetworkId.HasValue)
                return;
            if (!CurrentTransportStreamId.HasValue)
                return;

            if (!fromPmt)
            {
                UiJunction?.NotifyCat(caDescriptor);
                if (!DataStorage.TestForCaSystem(CurrentNetworkId.Value, CurrentTransportStreamId.Value, caDescriptor.CaPid))
                {
	                DataStorage.StoreCaSystem(CurrentNetworkId.Value, CurrentTransportStreamId.Value, caDescriptor);
                    LogEvent(SkyscraperContextEvent.CaSystem, String.Format("{0} on PID {1:X4}", CaSystemNames.GetHumanReadableName(caDescriptor.CaSystemId), caDescriptor.CaPid));
                }
            }

            if (!DvbContext.IsPidProcessorPresent(caDescriptor.CaPid))
            {
                //These PIDs for CA Systems contain ECM and EMM messages.
                //They are specific for *every* CA System.
                //It is probably not worth reverse-engineering every single one of them.
                //So we just discard these packages.
                DvbContext.RegisterPacketProcessor(caDescriptor.CaPid, new PacketDiscarder());
            }
        }

        public void NotifyOfSubtitleLine(Page page)
        {

        }

        public void UpdateNotification(UpdateNotificationGroup common, UpdateNotificationTarget target, ushort ProgramNumber)
        {
            UiJunction?.OnSsuNotification(common, target, ProgramNumber);
            DataStorage.StoreUpdateNotification2(common, target);

            if (common.AssociationTag.HasValue)
            {
                Platform platform = new Platform();
                platform.DataBroadcastId = common.DataBroadcastId;
                platform.PrivateData = common.PrivateData;
                platform.AssociationTag = common.AssociationTag;
                platform.UpdateFlag = common.UpdateFlag;
                platform.UpdateMethod = common.UpdateMethod;
                platform.UpdatePriority = common.UpdatePriority;
                target.Platforms.Add(platform);
            }

            foreach (Platform targetPlatform in target.Platforms)
            {
                if (!targetPlatform.AssociationTag.HasValue)
                    continue;
                if (!SsusToLookFor.Any(x => x.Key == ProgramNumber && x.Value == (byte)targetPlatform.AssociationTag))
                {
                    SsusToLookFor.Add(new KeyValuePair<ushort, byte>(ProgramNumber,
                        (byte)targetPlatform.AssociationTag.Value));
                }
            }
        }

        private Dictionary<DatabaseKeyDsmCcModule, double> _dataCarouselProgresses;

        public bool IsModuleWanted(int elementaryPid, ushort moduleId, byte moduleVersion)
        {
            if (!CurrentNetworkId.HasValue)
                return false;

            if (!CurrentTransportStreamId.HasValue)
                return false;

            if (DataStorage.IsDsmCcModuleBlacklisted(CurrentNetworkId.Value, CurrentTransportStreamId.Value, elementaryPid, moduleId, moduleVersion))
                return false;

            if (ObjectStorage.IsDsmCcModuleWanted(CurrentNetworkId.Value, CurrentTransportStreamId.Value, elementaryPid, moduleId, moduleVersion))
                return true;
            
            return false;
        }

        public void NotifyOfNewModule(int elementaryPid, ushort moduleInfoModuleId, byte moduleInfoModuleVersion)
        {
            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (_dataCarouselProgresses == null)
                _dataCarouselProgresses = new Dictionary<DatabaseKeyDsmCcModule, double>();

            DatabaseKeyDsmCcModule key = new DatabaseKeyDsmCcModule(CurrentNetworkId.Value, CurrentTransportStreamId.Value, elementaryPid, moduleInfoModuleId, moduleInfoModuleVersion);

            if (dedupModules.Contains(key))
                return;

            if (ObjectStorage.IsDsmCcModuleWanted(CurrentNetworkId.Value, CurrentTransportStreamId.Value, elementaryPid, moduleInfoModuleId, moduleInfoModuleVersion))
            {
                if (!_dataCarouselProgresses.ContainsKey(key))
                {
                    _dataCarouselProgresses.Add(key, 0.0);
                }
                LogEvent(SkyscraperContextEvent.NotifyOfNewModule, String.Format("PID {2:X4}, Module #{0:X4}, Version {1}", moduleInfoModuleId, moduleInfoModuleVersion, elementaryPid));
                UiJunction?.DsmCcModuleAdd(elementaryPid, moduleInfoModuleId, moduleInfoModuleVersion);
            }
        }

        public void NotifyModuleDownloadProgress(int elementaryPid, ushort moduleInfoModuleId, byte moduleInfoModuleVersion, double moduleInfoDownloadProgress)
        {
            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (_dataCarouselProgresses == null)
                return;

			DatabaseKeyDsmCcModule key = new DatabaseKeyDsmCcModule(CurrentNetworkId.Value, CurrentTransportStreamId.Value, elementaryPid, moduleInfoModuleId, moduleInfoModuleVersion);
			if (_dataCarouselProgresses.ContainsKey(key))
            {
                _dataCarouselProgresses[key] = moduleInfoDownloadProgress;
                LogEvent(SkyscraperContextEvent.ModuleDownloadProgress, String.Format("PID {3:X4}, Module #{0}, Version {1}, {2}% done", moduleInfoModuleId, moduleInfoModuleVersion, moduleInfoDownloadProgress, elementaryPid));
                UiJunction?.DsmCcModuleProgress(elementaryPid, moduleInfoModuleId, moduleInfoModuleVersion, moduleInfoDownloadProgress);
            }
        }

        public ObjectCarouselEventHandler GetObjectCarouselEventHandler()
        {
            return this;
        }

        public void NotifyDownloadComplete(int elementaryPid, ushort moduleModuleId, byte moduleModuleVersion, Stream result, bool isObjectCarousel)
        {
            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (_dataCarouselProgresses == null)
                return;

            if (isObjectCarousel)
                throw new NotImplementedException();

            ObjectStorage.DataCarouselModuleArrival(CurrentNetworkId.Value, CurrentTransportStreamId.Value, elementaryPid, moduleModuleId, moduleModuleVersion, result);

            LogEvent(SkyscraperContextEvent.ModuleDownloadComplete, String.Format("Module {0}, Version {1}", moduleModuleId, moduleModuleVersion));

			DatabaseKeyDsmCcModule key = new DatabaseKeyDsmCcModule(CurrentNetworkId.Value, CurrentTransportStreamId.Value, elementaryPid, moduleModuleId, moduleModuleVersion);
            _dataCarouselProgresses.Remove(key);

            UiJunction?.DsmCcModuleComplete(elementaryPid, moduleModuleId, moduleModuleVersion);
        }

        public void DsmCcDoItNowEvent(ProgramMapping programMapping, StreamEventDescriptor descriptorListStreamEventDescriptor, int pid)
        {
            if (!CurrentTransportStreamId.HasValue)
                return;
            if (!CurrentNetworkId.HasValue)
                return;
            if (!currentTime.HasValue)
                return;
            DataStorage.StoreDsmCcDoItNowEvent(currentTime.Value, CurrentNetworkId.Value, CurrentNetworkId.Value, programMapping.ProgramNumber, descriptorListStreamEventDescriptor, pid);

            //These "do-it-now" Events are specific for _EVERY_ application and probably not
            //worth extending the scraping time. They are most likely subtitles or commands
            //for game-show play-along apps. Therefore we won't create a log entry for these, as it
            //would increase scraping duration.
        }

        private List<DatabaseKeyDsmCcModule> dedupModules = new List<DatabaseKeyDsmCcModule>();
        public void PreventDsmCcModuleRepetition(int elementaryPid, ushort moduleModuleId, byte moduleModuleVersion)
        {
            dedupModules.Add(new DatabaseKeyDsmCcModule(CurrentNetworkId.Value, CurrentTransportStreamId.Value, elementaryPid, moduleModuleId, moduleModuleVersion));
        }

        public void SetRdsProgrammeServiceName(int programNumber, string programmeService2)
        {
            if (DataStorage.UpdateRdsProgrammeServiceName(CurrentNetworkId.Value, CurrentTransportStreamId.Value,
                    programNumber, programmeService2))
            {
                LogEvent(SkyscraperContextEvent.RdsProgrammeServiceName,
                    String.Format("{0:X4}, {1:X4}", programNumber, programmeService2));
            }
        }

        public bool TestCanHandleRdsMessages(int programNumber)
        {
            if (!CurrentNetworkId.HasValue)
                return false;

            if (!CurrentTransportStreamId.HasValue)
                return false;

            if (!DataStorage.TestForKnownRdsData(CurrentNetworkId.Value, CurrentTransportStreamId.Value,
                    programNumber))
            {
	            DataStorage.EnableRdsCollection(CurrentNetworkId.Value, CurrentTransportStreamId.Value,
                    programNumber);
                LogEvent(SkyscraperContextEvent.BeginRdsRecording,
                    String.Format("{0:X4},{1:X4},{2:X4}", CurrentNetworkId.Value, CurrentTransportStreamId.Value,
                        programNumber));
            }

            return true;
        }

        public void SetRdsRadioText(int programNumber, string text)
        {
            if (DataStorage.UpdateRdsRadioText(CurrentNetworkId.Value, CurrentTransportStreamId.Value, programNumber,
                    text))
            {
                LogEvent(SkyscraperContextEvent.RdsText, String.Format("{0:X4} [{1}]", programNumber, text));
            }
        }

        public void NotifySpliceInsert(ushort ProgramNumber, SpliceInsert spliceInsert)
        {
            UiJunction?.NotifyScte35(ProgramNumber, spliceInsert);
            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (!DataStorage.TestForScte35SpliceInsert(CurrentNetworkId.Value, CurrentTransportStreamId.Value,
                    ProgramNumber, spliceInsert))
            {
	            DataStorage.StoreScte35SpliceInsert(CurrentNetworkId.Value, CurrentTransportStreamId.Value, ProgramNumber, spliceInsert);
                LogEvent(SkyscraperContextEvent.Scte35Splice,
                    String.Format("Program {0:X4}, Splice at {1:X16}", ProgramNumber, spliceInsert.SpliceTime));
            }
        }

        public void NotifyTimeSignal(ushort programNumber, TimeSignal timeSignal)
        {
            UiJunction?.NotifyScte35(programNumber, timeSignal);
            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (!currentTime.HasValue)
                return;

            DataStorage.SetScte35TimeSignal(CurrentNetworkId.Value, CurrentTransportStreamId.Value, currentTime.Value, programNumber, timeSignal);
            LogEvent(SkyscraperContextEvent.Scte35TimeSignal, String.Format("Program {0:X4}, Time Signal {1:X16}", programNumber, timeSignal.Value));
        }

        public void NotifyOfCompliance(string compliance)
        {
            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (!DataStorage.IsCompliant(CurrentNetworkId.Value, CurrentTransportStreamId.Value, compliance))
            {
	            DataStorage.MarkAsCompliant(CurrentNetworkId.Value, CurrentTransportStreamId.Value, compliance);
                LogEvent(SkyscraperContextEvent.Compliance, compliance);
            }
        }

        public void NotifiyStationIdentification(string stationIdentification)
        {
            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (DataStorage.SetStationIdentification(CurrentNetworkId.Value, CurrentTransportStreamId.Value,
                    stationIdentification))
            {
                LogEvent(SkyscraperContextEvent.StationIdentification, stationIdentification);
            }
        }
        
        public void AutodetectionSucessful(int pid, Contestant contestant, ProgramContext programContext)
        {
            UiJunction?.NotifyStreamTypeDetection(contestant.Tag, pid);
            LogEvent(SkyscraperContextEvent.StreamTypeAutodetection, String.Format("{0} on PID {1:X4}", contestant.Tag, pid));
            contestant.DeclareWinner(this, pid, programContext);
            return;
        }


        private IpTrafficHandler ipTrafficHandler;
        public void OnIpDatagram(int pid, byte[] payload)
        {
            UiJunction?.EnableUiFeature(SkyscraperUiFeature.IpTrafficAnalysis);
	        if (ipTrafficHandler == null)
	        {
		        StorageConnectionManager storageConnectionManager = StorageConnectionManager.GetInstance();
		        ipTrafficHandler = storageConnectionManager.GetDefaultIpTrafficHandler();
	        }
            if (payload.Length == 0)
            {
                return;
            }

	        ipTrafficHandler?.HandlePacket(pid, payload);

            int ipVersion = (payload[0] & 0xf0) >> 4;
            if (ipVersion == 4)
            {
                if (Array.TrueForAll(payload, x => x == 0))
                    return;
                int ihl = (payload[0] & 0x0f);
                ihl *= 32;
                ihl /= 8;
                byte[] ipv4Header = new byte[ihl];
                if (payload.Length < ihl)
                    return;
                Array.Copy(payload, 0, ipv4Header, 0, ihl);
                InternetHeader internetHeader = new InternetHeader(ipv4Header);
                if (!internetHeader.ChecksumValid)
                    return;
                byte[] ipv4Packet = new byte[payload.Length - ihl];
                Array.Copy(payload, ihl, ipv4Packet, 0, payload.Length - ihl);
                OnIpv4PacketArrival(internetHeader, ipv4Packet);
            }
            else if (ipVersion == 6)
            {
	            if (payload.Length < 40)
	            {
		            return;
	            }
	            MemoryStream ipv6Stream = new MemoryStream(payload, false);
	            byte byteA = ipv6Stream.ReadUInt8();
	            byte byteB = ipv6Stream.ReadUInt8();

	            int trafficClass = (byteA & 0x0f) << 4;
	            trafficClass = ((byteB & 0xf0) >> 4);

	            int flowLabel = (byteB & 0x0f) << 16;
	            flowLabel += ipv6Stream.ReadUInt16BE();

	            ushort payloadLength = ipv6Stream.ReadUInt16BE();
	            byte nextHeader = ipv6Stream.ReadUInt8();
	            byte hopLimit = ipv6Stream.ReadUInt8();

	            IPAddress sourceAddress = new IPAddress(ipv6Stream.ReadBytes(16));
	            IPAddress destinationAddress = new IPAddress(ipv6Stream.ReadBytes(16));
	            InternetHeader ipv6Header = new InternetHeader(trafficClass, flowLabel, payloadLength, nextHeader, hopLimit, sourceAddress, destinationAddress);
	            if (ipv6Stream.GetAvailableBytes() >= payloadLength)
	            {
		            OnIpv4PacketArrival(ipv6Header, ipv6Stream.ReadBytes(payloadLength));
	            }
	            return;
            }
            else
            {
                //throw new Exception("invalid ip packet");
            }
        }

        public void GsIpTrafficDetected()
        {
            LogEvent(SkyscraperContextEvent.SpecialTsMode, "Valid IP Traffic detected");
        }
        
        private HashSet<IpTrafficInfo> trafficInfos;
        private ReadOnlyCollection<ISkyscraperMpePlugin> mpePlugins;
        public SkyscraperDnsCache DnsCache { get; private set; }

        private bool ProcessDns(IpTrafficInfo trafficInfo, byte[] ipv4Packet)
        {
            if (DnsCache == null)
                DnsCache = new SkyscraperDnsCache(DataStorage);
            //Handle DNS
            bool result = false;
			if (trafficInfo.Protocol == 0x11)
			{
				UserDatagram udpPacket = new UserDatagram(ipv4Packet);
                if (udpPacket.Valid)
                {
                    if (udpPacket.SourcePort == 53)
                    {
						result = DnsCache.ProcessDnsPacket(udpPacket.Payload);
					}
                }
			}
            trafficInfo.SourceName = DnsCache.GetDnsName(trafficInfo.Source);
            trafficInfo.TargetName = DnsCache.GetDnsName(trafficInfo.Target);
            return result;
		}
        
		public void OnIpv4PacketArrival(InternetHeader internetHeader, byte[] ipv4Packet)
        {
            //There are as many use cases as there are internet applications in these.
            //Not really required to decode all of them, if you ask me.
            /*
             * One known use is LX9SES on Astra 23.5, with the following arguments:
             * -> Destination Address always 228.64.0.56
             * -> Source Address always 10.225.49.1
             * -> Protocol always 0x11 (UDP)
             */
            if (trafficInfos == null)
                trafficInfos = new HashSet<IpTrafficInfo>();

            IpTrafficInfo iti = IpTrafficInfo.FromInternetHeader(internetHeader);
            if (ProcessDns(iti, ipv4Packet))
            {
                LogEvent(SkyscraperContextEvent.LearnDns, String.Format("{0} = {1}", DnsCache.LastLearned.Value, DnsCache.LastLearned.Key));
            }
			UiJunction?.NotifyMpeTraffic(iti, ipv4Packet);

			if (trafficInfos.Add(iti))
            {
	            LogEvent(SkyscraperContextEvent.IpTraffic, iti.ToString());
            }

            if (mpePlugins == null)
            {
	            mpePlugins = PluginManager.GetInstance().GetMpePlugins();
	            object[] connector = StorageConnectionManager.GetPluginConnectors(DataStorage, ObjectStorage);
                foreach (ISkyscraperMpePlugin plugin in mpePlugins)
                {
                    try
                    {
                        plugin.ConnectToStorage(connector);
                    }
                    catch (Exception e)
                    {

                    }
                }
            }


            foreach (ISkyscraperMpePlugin plugin in mpePlugins)
            {
	            if (plugin.CanHandlePacket(internetHeader, ipv4Packet))
	            {
		            plugin.SetContext(currentTime,this);
		            plugin.HandlePacket(internetHeader, ipv4Packet);
		            if (plugin.StopProcessingAfterThis())
			            return;
	            }
            }
            /*
            if (internetHeader.Protocol == 0x11)
            {
                UserDatagram userDatagram = new UserDatagram(ipv4Packet);
                if (!userDatagram.Valid)
                    return;

                if (internetHeader.SourceAddress.Equals(_fpUrmetSrc) && internetHeader.DestinationAddress.Equals(_fpUrmetDst))
                {
                    //Found on Astra 19.2 12604/H - not the slightest idea what this does.
                    //Service name is FP URMET.
                    return;
                }
                else
                {
                    ipPackets++;
                }
            }
            else if (internetHeader.Protocol == 0x02)
            {
                IgmpMessage igmpMessage = new IgmpMessage(ipv4Packet);
                //Also Found on Astra 19.2 12604/H, I don't think we'll need these.
                return;
            }
            else
            {
                ipPackets++;
            }*/
        }
        
        public void NotifyRunningStatus(uint transportStreamId, uint originalNetworkId, uint serviceId, uint eventId,
            RunningStatus runningStatus)
        {
            if (!currentTime.HasValue)
                return;

            if (DataStorage.StoreRunningStatus(transportStreamId, originalNetworkId, serviceId, eventId, runningStatus, currentTime.Value))
            {
                LogEvent(SkyscraperContextEvent.RunningStatusChange, String.Format("({0:X4},{1:X4},{2:X4},{3:X4}) -> {4}", originalNetworkId, transportStreamId, serviceId, eventId, runningStatus));
            }
        }

        public bool EnableTimeout { get; set; }

        public int TimeoutSeconds { get; set; }

        public bool CancelOnNextPacket { get; set; }
        public ISkyscraperUiJunction UiJunction { get; set; }

        public bool IsAbortConditionMet()
        {
            if (ffmpegFrameGrabber.BusyFrameGrabbers > 0)
                return false;

            if (EnableTimeout && TimeoutSeconds == 0)
                TimeoutSeconds = 10;

            if (!firstPacketDone)
                return false;

            if (CancelOnNextPacket)
            {
                LogEvent(SkyscraperContextEvent.AbortConditionMet, "Aborted from another thread.");
                CancelOnNextPacket = false;
                return true;
            }

            if (EnableTimeout)
            {
                TimeSpan sinceLastEvent = DateTime.Now - lastEventTimestamp;
                if (sinceLastEvent.TotalSeconds > TimeoutSeconds)
                {
                    LogEvent(SkyscraperContextEvent.AbortConditionMet, String.Format("Event Timeout after {0} seconds", TimeoutSeconds));
                    return true;
                }
            }

            return false;
        }

        public void OnT2MiPacketLoss(int pid, byte expectedPacket, T2MIHeader header)
        {
        }
        
        public void OnT2MiPacket(int pid, byte basebandFramePlpId, byte[] basebandPacket)
        {
	        if (subSkyscrapers == null)
		        subSkyscrapers = new Dictionary<object, SkyscraperContext>();

	        T2MiSubSkyscraperIdentifier subSkyscraperIdentifier = new T2MiSubSkyscraperIdentifier(pid, basebandFramePlpId);
	        if (!subSkyscrapers.ContainsKey(subSkyscraperIdentifier))
	        {
		        LogEvent(SkyscraperContextEvent.T2MiPlpDetected, String.Format("PID {0:X4}, PLP {1}", pid, basebandFramePlpId));
	        }

	        OnSubTsPacket(subSkyscraperIdentifier, basebandPacket);
        }

        private Dictionary<object, SkyscraperContext> subSkyscrapers;
        public void OnSubTsPacket(object identifier, byte[] packet)
        {
	        if (subSkyscrapers == null)
		        subSkyscrapers = new Dictionary<object, SkyscraperContext>();

	        if (!subSkyscrapers.ContainsKey(identifier))
	        {
		        SkyscraperContext child = new SkyscraperContext(new TsContext(), DataStorage, ObjectStorage);
		        child.IsChild = true;
		        child.ChildName = identifier.ToString();
		        child.ChildTimestamp = DateTime.Now.Ticks;

		        List<IPacketFilter> packetFilters = new List<IPacketFilter>();
		        StorageConnectionManager storageConnectionManager = StorageConnectionManager.GetInstance();
		        if (storageConnectionManager.IsSubTsDumpingEnabled())
		        {
			        string filename = String.Format("{0}_{1}.ts", child.ChildName.SanitizeFileName(), child.ChildTimestamp);

                    string outputPath = storageConnectionManager.GetSubTsOutputPath();
                    if (!string.IsNullOrEmpty(outputPath))
                    {
                        DirectoryInfo outDir = new DirectoryInfo(outputPath);
                        outDir.EnsureExists();
                        filename = Path.Combine(outDir.FullName, filename);
                    }

			        FileInfo fi = new FileInfo(filename);
			        TsRecorder packetDumper = new TsRecorder();
			        packetDumper.RecordingOutputDirectory = new DirectoryInfo(".");
			        packetDumper.SetNextFilename(filename);
                    packetDumper.CreateBufferedStream();
                    packetFilters.Add(packetDumper);
		        }

		        child.InitalizeFilterChain(packetFilters.ToArray());
		        subSkyscrapers.Add(identifier, child);
                UiJunction?.OnDetectionOfInnerTs(child, identifier);
	        }

	        SkyscraperContext context = subSkyscrapers[identifier];
	        context.IngestSinglePacket(packet);
        }
        public long ChildTimestamp { get; private set; }
        public string ChildName { get; private set; }
        public bool IsChild { get; private set; }

        public void OnT2MiTimestamp(int pid, _0x20_DvbT2Timestamp t2Timestamp)
        {
            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (subSkyscrapers == null)
                return;

            List<SkyscraperContext> contexts = new List<SkyscraperContext>();
            foreach (KeyValuePair<object, SkyscraperContext> subSkyscraper in subSkyscrapers)
            {
	            object subSkyscraperKey = subSkyscraper.Key;
	            if (!(subSkyscraperKey is T2MiSubSkyscraperIdentifier))
		            continue;

                T2MiSubSkyscraperIdentifier identifier = subSkyscraperKey as T2MiSubSkyscraperIdentifier;
                if (identifier.Pid != pid)
	                continue;

                contexts.Add(subSkyscraper.Value);
            }

            if (contexts.Count == 0)
                return;
            
            long sum = contexts.Select(x => (long)x.EventsLogged).Sum();
            if (sum < (contexts.Count * 2))
                return;

            DateTime resolveTime = t2Timestamp.ResolveTime();
            DateTime knownTimestamp = DataStorage.T2MiGetTimestamp(CurrentNetworkId.Value, CurrentTransportStreamId.Value, pid);
            if (resolveTime > knownTimestamp)
            {
	            DataStorage.T2MiSetTimestamp(CurrentNetworkId.Value, CurrentTransportStreamId.Value, pid, resolveTime);
            }
        }

        public void OnT2MiIqData(int relatedPid, _0x31_IqData iqData)
        {

        }

        public void OnT2MiIqData(int relatedPid, _0x01_IqData iqData2)
        {

        }

        public void OnT2MiError(int relatedPid, int skyscraperErrorCode)
        {
            
        }

        public void OnT2MiL1Current(int relatedPid, _0x10_L1Current l1Current)
        {
            //No clue about this data. It's not really interesting.
        }

        public void OnT2MiArbitraryCellInsertion(int relatedPid, _0x02_ArbitraryCellInsertion arbitraryCellInsertion)
        {
            
        }

        public void OnT2MiBalancingCells(int relatedPid, byte frameIndex, uint numActiveBiasCellsPerP2)
        {
            
        }

        public void OnT2MiL1Future(int relatedPid, _0x11_L1Future l1Future)
        {
            if (l1Future.FutureData.L1DynNext.Length == 0)
                return;

            throw new NotImplementedException();
        }

        public void OnT2MiIndividualAddressing(int relatedPid, _0x21_IndividualAddressing individualAddressing)
        {
            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (subSkyscrapers == null)
                return;

            bool any = subSkyscrapers.Where(x => x.Key is T2MiSubSkyscraperIdentifier)
	            .Select(x => (T2MiSubSkyscraperIdentifier)x.Key)
	            .Any();
            if (!any)
                return;

            foreach (AddressingFunction function in individualAddressing.Commands)
            {
                if (!DataStorage.T2MiTestForTransmitter(CurrentNetworkId, CurrentTransportStreamId, relatedPid, function.TxIdentifier))
	                DataStorage.T2MiRememberTransmitter(CurrentNetworkId, CurrentTransportStreamId, relatedPid, function.TxIdentifier);

                switch (function.Tag)
                {
                    case 0x00:
                        _0x00_TransmitterTimeOffset tto = (_0x00_TransmitterTimeOffset)function;
                        DataStorage.T2MiSetTransmitterTimeOffset(CurrentNetworkId, CurrentTransportStreamId, relatedPid, function.TxIdentifier, tto.TimeOffset);
                        break;
                    default:
                        throw new NotImplementedException(String.Format("Individual Addressing function 0x{0:X2}", function.Tag));
                }
            }
        }

        public bool TcpProxyEnabled
        {
            get => DvbContext.TcpProxyEnabled;
            set => DvbContext.TcpProxyEnabled = value;
        }

        public void Dispose()
        {
            if (TcpProxyEnabled)
                TcpProxyEnabled = false;


            if (ipTrafficHandler != null)
            {
                ipTrafficHandler.Dispose();
            }
            

            if (DvbContext.TcpProxyEnabled)
            {
                DvbContext.TcpProxyEnabled = false;
            }

            if (docsisPacketProcessor != null)
            {
                docsisPacketProcessor.Dispose();
            }

            if (!SourceIsDisk)
            {
                if (_dataCarouselProgresses != null)
                {
                    foreach (KeyValuePair<DatabaseKeyDsmCcModule, double> item in _dataCarouselProgresses)
                    {
                        if (item.Value == 0.0)
                            continue;

                        DataStorage.FailDsmCcDownload(item.Key, item.Value);
                    }
                }
            }

            if (_pluginContexts != null)
            {
	            foreach (object plugin in _pluginContexts)
	            {
                    IDisposable disposable = plugin as IDisposable;
                    if (disposable != null)
                        disposable.Dispose();
	            }
                _pluginContexts.Clear();
            }

            DataStorage.WaitForCompletion();
        }

        public bool TestForFrame(int currentNetworkId, int transportStreamId, ushort mappingProgramNumber, int mappingStreamElementaryPid)
        {
            return ObjectStorage.TestForFramegrab(currentNetworkId, transportStreamId, mappingProgramNumber, mappingStreamElementaryPid);
        }

        public void NotifyCompletedFrame(int currentNetworkId, int transportStreamId, ushort mappingProgramNumber,
            int mappingStreamElementaryPid, byte[] imageData)
        {
            ObjectStorage.StoreFramegrab(currentNetworkId, transportStreamId, mappingProgramNumber, (ushort)mappingStreamElementaryPid, imageData);
            LogEvent(SkyscraperContextEvent.GotFramegrab, String.Format("Program {0:X4}, PID {1:X4}", mappingProgramNumber, mappingStreamElementaryPid));
            UiJunction?.ShowFramegrab(currentNetworkId, transportStreamId, mappingProgramNumber, mappingStreamElementaryPid, imageData);
        }

        public void OnIpMacNotification(int sourcePid, Dvb.DataBroadcasting.IntModel.Platform platform, Target target, Operational operational)
        {
            UiJunction?.OnIpMacNotification(sourcePid, platform, target, operational);

            IEnumerable<IpMacNotification> ipMacNotifications = IpMacNotification.FlatMap(platform, target, operational);
            foreach (IpMacNotification notification in ipMacNotifications)
            {
                if (!DataStorage.TestForIpMacNotification(notification))
                {
	                DataStorage.StoreIpMacNotification(notification);
                    LogEvent(SkyscraperContextEvent.IpMacNotification, String.Format("{0} -> {1}", platform.Name, notification.Target.ToString()));
                }
            }
        }

        public void OnRelatedContent(Rct result, ushort resultProgramNumber)
        {
            if (result.LinkInfo.Length == 0)
                return;
            if (runningEvents == null)
                return;
            if (runningEvents[resultProgramNumber] == null)
                return;

            EitEvent lEvent = runningEvents[resultProgramNumber];

            foreach (RctLinkInfo rctLinkInfo in result.LinkInfo)
            {
                if (!DataStorage.TestForRelatedContent(lEvent, rctLinkInfo))
                {
	                DataStorage.SetRelatedContent(lEvent, rctLinkInfo);
                    LogEvent(SkyscraperContextEvent.RelatedContent, String.Format("Program {0}, Event {1} -> {2}", resultProgramNumber, lEvent.EventId, rctLinkInfo.GetIdentifierString()));
                }
            }
        }

        private bool warnedMissingLocation;

        private void WarnMissingLocation()
        {
            if (!warnedMissingLocation)
            {
                LogEvent(SkyscraperContextEvent.LocationMissing);
                warnedMissingLocation = true;
            }
        }

        public void OnParticipantDetected(PhysicalAddress pa)
        {
            int? currentLocation = DataStorage.GetCurrentLocationId();
            if (currentLocation.HasValue)
            {
                LogEvent(SkyscraperContextEvent.DocsisParticipant, BitConverter.ToString(pa.GetAddressBytes()));
                DataStorage.StoreDocsisParticipant(pa, currentLocation.Value);
            }
            else
            {
                WarnMissingLocation();
            }
        }

        public void OnCmtsTimestamp(PhysicalAddress source, uint timing)
        {
            //DOCSIS Timestamps are not interesting to us.
        }

        public void OnUpstreamChannel(UpstreamChannelDescriptor mmm)
        {
            UiJunction?.NotifyDocsisFrequency(mmm.Frequency, true, mmm);
            if (!mmm.Frequency.HasValue)
                return;

            int? currentLocation = DataStorage.GetCurrentLocationId();
            if (!currentLocation.HasValue)
            {
                WarnMissingLocation();
                return;
            }

            if (!DataStorage.TestForDocsisUpstreamChannel(mmm.Source, mmm.Frequency.Value,currentLocation.Value))
            {
	            DataStorage.StoreDocsisUpstreamChannel(mmm, currentLocation.Value);
                LogEvent(SkyscraperContextEvent.DocsisUpstreamChannel, String.Format("{0} -> {1}", mmm.Source.ToString(), mmm.Frequency.Value));
            }
        }

        public void OnDownstreamChannel(PhysicalAddress physicalAddress, MacDomainDescriptor.DownstreamActiveChannel downstreamActiveChannel)
        {
            UiJunction?.NotifyDocsisFrequency(downstreamActiveChannel.Frequency, false, downstreamActiveChannel);

            if (!downstreamActiveChannel.Frequency.HasValue)
                return;

            int? currentLocation = DataStorage.GetCurrentLocationId();
            if (!currentLocation.HasValue)
            {
                WarnMissingLocation();
                return;
            }

            if (!DataStorage.TestForDocsisDownstreamChannel(physicalAddress, downstreamActiveChannel,currentLocation.Value))
            {
	            DataStorage.StoreDocsisDownstreamChannel(physicalAddress, downstreamActiveChannel,currentLocation.Value);
                LogEvent(SkyscraperContextEvent.DocsisDownstreamChannel, String.Format("{0} -> {1}", physicalAddress.ToString(), downstreamActiveChannel.Frequency.Value));
            }
        }

        public void OnLearnedIpFromMac(PhysicalAddress arpHeaderSenderHardwareAddress, IPAddress arpHeaderSenderProtocolAddress)
        {
            int? currentLocation = DataStorage.GetCurrentLocationId();
            if (!currentLocation.HasValue)
            {
                WarnMissingLocation();
                return;
            }

            if (DataStorage.SetCmtsIp(arpHeaderSenderHardwareAddress, arpHeaderSenderProtocolAddress))
            {
                LogEvent(SkyscraperContextEvent.LearnedCmtsIp,String.Format("{0} -> {1}",arpHeaderSenderHardwareAddress.ToString(),arpHeaderSenderProtocolAddress.ToString()));
            }
        }

        public void OnAbertisPacket(int pid, byte[] outBuffer)
        {
	        AbertisSubSkyscraperKey key = new AbertisSubSkyscraperKey(pid);
	        OnSubTsPacket(key, outBuffer);
		}

        public void OnAbertisSyncLoss(int pid, int oldPosition = -1, long newPosition = -1)
        {
	        if (subSkyscrapers == null)
                DvbContext.RegisterPacketProcessor(pid, new PacketDiscarder());

	        AbertisSubSkyscraperKey key = new AbertisSubSkyscraperKey(pid);
			if (!subSkyscrapers.ContainsKey(key) == null)
                DvbContext.RegisterPacketProcessor(pid, new PacketDiscarder());

			if (!subSkyscrapers[key].firstPacketDone)
				DvbContext.RegisterPacketProcessor(pid, new PacketDiscarder());
			else
				throw new NotImplementedException(
					"I couldn't sync this Abertis-styled stream, and I couldn't figure out why. Could you please share a sample of this stream?");
        }

		public void OnMonochromeData(int networkId, int transportStreamId, ushort programNumber, MonochromeDataField result)
		{
			throw new NotImplementedException();
		}

		void Id3Handler.OnId3Error(Id3ErrorState state)
		{
			throw new NotImplementedException();
		}

		void Id3Handler.OnId3Tag(Id3Tag tag)
		{
            if (tag.IsAllPrivate())
                return;
			throw new NotImplementedException();
		}

		void InteractionChannelHandler.OnCorrectionMessage(ushort interactiveNetworkId, Cmt cmt)
		{
			SetLikelyNetwork(interactiveNetworkId);

			foreach (Cmt.CmtEntry entry in cmt.Entries)
            {
                if (!DataStorage.TestForCmtEntry(interactiveNetworkId,entry))
                {
                    LogEvent(SkyscraperContextEvent.CorrectionMessage, String.Format("Network ID = {0}, Group ID = {1}, Logon ID = {2}, Slot Type = {3}", interactiveNetworkId, entry.GroupId, entry.LoginId,entry.SlotType.ToString()));
                    DataStorage.InsertCmtEntry(interactiveNetworkId, entry);
                }
            }
		}

		void InteractionChannelHandler.OnFrameComposition(ushort interactiveNetworkId, Fct fct)
		{
			SetLikelyNetwork(interactiveNetworkId);

			foreach (Fct.Frame frame in fct.Frames)
            {
                if (!DataStorage.TestForFrameComposition(interactiveNetworkId,frame))
                {
                    LogEvent(SkyscraperContextEvent.FrameComposition, String.Format("Network ID = {0}, Frame = {1}", interactiveNetworkId, frame.FrameId));
                    DataStorage.InsertFctFrame(interactiveNetworkId, frame);
                }
            }
		}

        private bool[] interactionChannelErrors;
		void InteractionChannelHandler.OnInteractionChannelError(InteractionChannelErrorState unexpectedTable)
		{
            if (interactionChannelErrors == null)
            {
                interactionChannelErrors = new bool[Enum.GetNames(typeof(InteractionChannelErrorState)).Length];
            }
            int offset = (int)unexpectedTable;
            if (!interactionChannelErrors[offset])
            {
                LogEvent(SkyscraperContextEvent.InteractionChannelError, unexpectedTable.ToString());
                interactionChannelErrors[offset] = true;
            }
		}

		public void OnRcsMap(Rmt rmt)
		{
            if (rmt.Linkages != null)
            {
                if (rmt.Linkages.Count > 0)
                {
                    SetLikelyNetwork(rmt.Linkages[0].InteractiveNetworkId);
				}
            }
			UiJunction?.EnableUiFeature(SkyscraperUiFeature.RcsAnalysis);
            UiJunction?.OnRcs2Rmt(rmt);

            if (!DataStorage.TestForRmt(rmt))
            {
                DataStorage.InsertRmt(rmt);
				foreach (_0x4a_LinkageDescriptor linkage in rmt.Linkages)
				{
					LogEvent(SkyscraperContextEvent.RmtLinkage, String.Format("Interactive Network #{0} -> ONID {1}, TSID {2}", linkage.InteractiveNetworkId, linkage.OriginalNetworkId, linkage.TransportStreamId));
				}
				foreach (Rmt.TransportStream transportStream in rmt.TransportStreams)
				{
					LogEvent(SkyscraperContextEvent.RmtTransportStream, String.Format("{0} -> {1},{2}", rmt.NetworkId, transportStream.OriginalNetworkId, transportStream.TransportStreamId));
				}
			}
		}
        
		private void CheckForHiddenMpes()
		{
			uint threshold = 1000;
			if (pmtTracker != null)
			{
				if (!pmtTracker.AllPmtsProcessed)
				{
					return;
				}

				threshold = 1;
			}
			int[] occupiedPids = DvbContext.GetOccupiedPids();
			ulong[] pidStatistics = DvbContext.GetPidStatistics();
			for (int i = 0; i < (pidStatistics.Length - 1); i++)
			{
				if (pidStatistics[i] > threshold)
				{
					if (!occupiedPids.Contains(i))
					{
						LogEvent(SkyscraperContextEvent.SpecialTsMode, String.Format("Attaching stream-type autodetector to PID 0x{0:X4}", i));
						DvbContext.RegisterPacketProcessor(i, new StreamTypeAutodetection(i, this));
					}
				}
			}
		}

		public void OnSatellitePosition(ushort interactiveNetworkId, Spt spt)
		{
			SetLikelyNetwork(interactiveNetworkId);
            UiJunction?.OnRcs2Spt(interactiveNetworkId, spt);

			foreach (Spt.Satellite satellite in spt.Satellites)
            {
                if (!DataStorage.TestForSatellitePosition(interactiveNetworkId,satellite))
                {
	                LogEvent(SkyscraperContextEvent.SatellitePosition, String.Format("NID = {0}, Satellite ID = {1} (X = {2}, Y = {3}, Z = {4})", interactiveNetworkId, satellite.Id, satellite.X, satellite.Y, satellite.Z));
                    DataStorage.StoreSatellitePosition(interactiveNetworkId, satellite);
                }
            }
		}

		void InteractionChannelHandler.OnSuperframeComposition(ushort interactiveNetworkId, Sct sct)
		{
			SetLikelyNetwork(interactiveNetworkId);
            UiJunction?.OnRcs2Sct(interactiveNetworkId, sct);

			foreach (Sct.Superframe superframe in sct.Superframes)
            {
                if (!DataStorage.TestForSuperframeComposition(interactiveNetworkId,superframe))
                {
                    LogEvent(SkyscraperContextEvent.SuperframeComposition, String.Format("Network ID = {0}, Superframe ID = {1}, Frequency = {2}/{3}", interactiveNetworkId, superframe.SuperframeId, superframe.SuperframeCentreFrequency,superframe.UplinkPolarization));
                    DataStorage.StoreSuperframeComposition(interactiveNetworkId, superframe);
                }
            }
		}

		void InteractionChannelHandler.OnTerminalBurstTimePlan(ushort interactiveNetworkId, Tbtp tbtp)
		{
			SetLikelyNetwork(interactiveNetworkId);
            UiJunction?.OnRcs2Tbtp(interactiveNetworkId, tbtp);

			foreach (Tbtp.TbtpFrame frame in tbtp.Frames)
            {
                foreach (Tbtp.TbtpFrame.BtpEntity btp in frame.BTP)
                {
                    if (DataStorage.TestForTerminalBurstTimePlan(interactiveNetworkId,tbtp.GroupId,btp.LogonId))
                    {
                        LogEvent(SkyscraperContextEvent.TerminalBurstTimePlan, String.Format("Network ID = {0}, Group ID = {1}, Logon ID = {2}", interactiveNetworkId, tbtp.GroupId, btp.LogonId));
                        DataStorage.StoreTerminalBurstTimePlan(interactiveNetworkId, tbtp.GroupId, tbtp.SuperframeCount, frame.FrameNumber, btp);
                    }
                }
            }
		}

        public void OnTimeslotComposition(ushort interactiveNetworkId, Tct tct)
        {
			SetLikelyNetwork(interactiveNetworkId);
            UiJunction?.OnRcs2Tct(interactiveNetworkId, tct);

			throw new NotImplementedException();
        }

        public void OnTerminalBurstTimePlan2(ushort interactiveNetworkId, Tbtp2 tbtp2)
		{
			SetLikelyNetwork(interactiveNetworkId);
            UiJunction?.OnRcs2Tbtp2(interactiveNetworkId, tbtp2);

			foreach (Tbtp2.Frame frame in tbtp2.Frames)
			{
				for (int assignmentOrdinal = 0; assignmentOrdinal < frame.Assignments.Length; assignmentOrdinal++)
				{
					Tbtp2.Assignment assignment = frame.Assignments[assignmentOrdinal];
					if (!DataStorage.TestForTerminalBurstTimePlan2(interactiveNetworkId, tbtp2.GroupId, frame.FrameNumber))
					{
                        LogEvent(SkyscraperContextEvent.TerminalBurstTimePlan2, String.Format("Network ID = {0}, Group ID = {1}, Frame Number = {2}",interactiveNetworkId,tbtp2.GroupId,frame.FrameNumber));
                        DataStorage.StoreTerminalBurstTimePlan2(interactiveNetworkId, tbtp2.GroupId, frame);
					}
				}
			}
		}

		public void OnFrameComposition2(ushort networkId, Fct2 fct2)
		{
			SetLikelyNetwork(networkId);
            UiJunction?.OnRcs2Fct2(networkId, fct2);

			foreach (Fct2.Frame frame in fct2.FrameTypes)
			{
				if (!DataStorage.TestForFrameComposition2(networkId, frame))
				{
                    LogEvent(SkyscraperContextEvent.FrameComposition2,String.Format("Network ID = {0}, Frame Type = {1}",networkId,frame.FrameType));
                    DataStorage.InsertFct2Frame(networkId, frame);
				}
			}
		}

		public void OnBroadcastConfiguration(ushort networkId, Bct bct)
		{
			SetLikelyNetwork(networkId);
            UiJunction?.OnRcs2Bct(networkId, bct);

			foreach (Bct.BroadcastConfiguration txType in bct.TxTypes)
			{
				if (!DataStorage.TestForBroadcastConfiguration(networkId, txType.TxType))
				{
					LogEvent(SkyscraperContextEvent.BroadcastConfiguration, String.Format("Network ID = {0}, Broadcast Configuration #{1}", networkId, txType.TxType));
					DataStorage.InsertBroadcastConfiguration(networkId, txType);
				}
			}
		}

		public void OnRcs2Nit(ushort interactiveNetworkId, RcsNit nit)
		{
			SetLikelyNetwork(interactiveNetworkId);
            UiJunction?.OnRcs2Nit(interactiveNetworkId, nit);

			if (!DataStorage.TestForRcs2Nit(nit))
			{
				LogEvent(SkyscraperContextEvent.Rcs2Network, String.Format("Network ID #{0}, ({1}) on {2}", nit.OriginalNetworkId, nit.NetworkName, nit.SatelliteDeliverySystem.ToString()));
				DataStorage.InsertRcs2Nit(nit);
			}
		}

		public void OnRcs2Tdt(ushort interactiveNetworkId, Rcs2Tdt tdt)
		{
			LogEvent(SkyscraperContextEvent.Rcs2TdtTime, String.Format("Network ID #{0}, {1}", interactiveNetworkId, tdt.Timestamp));
			DataStorage.UpdateRcs2Tdt(interactiveNetworkId, tdt.Timestamp);
			SetLikelyNetwork(interactiveNetworkId);
            UiJunction?.OnRcs2Tdt(interactiveNetworkId, tdt);
		}

		public void OnTransmissionModeSupport2(ushort interactiveNetworkId, Tmst2 tmst2)
		{
			SetLikelyNetwork(interactiveNetworkId);
            UiJunction?.OnRcs2Tmst2(interactiveNetworkId, tmst2);

			foreach (Tmst2.TransmissionMode mode in tmst2.TransmissionModes)
			{
				if (!DataStorage.TestForTmst2(interactiveNetworkId, mode))
				{
					LogEvent(SkyscraperContextEvent.TransmissionModeSupport2, String.Format("Network ID #{0}, MODCOD {1}", interactiveNetworkId, mode.Modcod));
					DataStorage.InsertTmst2(interactiveNetworkId, mode);
				}
			}
		}

		void InteractionChannelHandler.OnTransmissionModeSupport(ushort interactiveNetworkId, Tmst tmst)
		{
            SetLikelyNetwork(interactiveNetworkId);
            UiJunction?.OnRcs2Tmst(interactiveNetworkId, tmst);

            byte[] knownTmst = DataStorage.GetTmst(interactiveNetworkId);
            if (knownTmst == null)
            {
                LogEvent(SkyscraperContextEvent.TransmissionModeSupport, String.Format("Network ID = {0}, Modes = {1}", interactiveNetworkId, BitConverter.ToString(tmst.Modes)));
                DataStorage.InsertTmst(interactiveNetworkId, tmst.Modes);
            }
            else if (Array.Equals(knownTmst,tmst.Modes))
            {
                LogEvent(SkyscraperContextEvent.TransmissionModeUpdate,String.Format("Network ID = {0}, Old Modes = {2}, New Modes = {1}", interactiveNetworkId, BitConverter.ToString(tmst.Modes), BitConverter.ToString(knownTmst)));
                DataStorage.UpdateTmst(interactiveNetworkId, tmst.Modes);
			}
		}

		public int GetRmtTransmissionStandard(ushort networkId)
		{
            return DataStorage.GetRmtTransmissionStandard(networkId);
		}

        public void AutodetectionRuleOut(int pid, Contestant contestant, ProgramContext programContext)
        {
            //LogEvent(SkyscraperContextEvent.StreamTypeAutodetection, String.Format("PID 0x{0:X4} probably isn't a {1}", pid, contestant.Tag));
        }

		void SgtEventHandler.AnnounceSgtList(SgtList list)
		{
            UiJunction?.OnAstraSgtList(list);

            if (!DataStorage.TestForSgtList(list))
            {
                LogEvent(SkyscraperContextEvent.SgtList, String.Format("List #{0} ({1})", list.ServiceListId, list.GetName()));
                DataStorage.InsertSgtList(list);
            }
		}

		void SgtEventHandler.OnSgtError(SgtError invalidTableId)
		{
			throw new NotImplementedException();
		}

		void SgtEventHandler.OnSgtService(SgtService child)
		{
            UiJunction?.OnAstraSgtService(child);
            if (!DataStorage.TestForSgtService(child))
            {
                LogEvent(SkyscraperContextEvent.SgtService, String.Format("LCN #{0} in List #{1} ({2})", child.Lcn, child.ServiceListId, child.GetName()));
                DataStorage.InsertSgtService(child);
            }
		}

		public void ProcessVirtualFilesystem(IFilesystem vfs, ProgramMappingStream programMappingStream)
		{
            yo3explorerToSkyscraperContextBridge context = new yo3explorerToSkyscraperContextBridge();
            context.DataStorage = DataStorage;
            if (!currentTime.HasValue)
                return;
            context.CurrentTime = currentTime.Value;
            context.Source = ServiceListEntryPointSource.TransportStream;
            context.OriginalNetworkId = CurrentNetworkId;
            context.TransportStreamId = CurrentTransportStreamId;
            context.PID = programMappingStream.ElementaryPid;

			yo3explorerState state = yo3explorerState.GetInstance();

			PluginManager pluginManager = PluginManager.GetInstance();
			IReadOnlyList<skyscraper8.yo3explorer.FilesystemProcessorPlugin> processors = pluginManager.GetFilesystemProcessors();
            
            foreach(FilesystemProcessorPlugin processor in processors)
            {
                processor.ProcessFilesystem(vfs, context, state);
            }
		}

        public void FluteFileArrival(NipActualCarrierInformation carrier, FluteListener listener)
        {
            

	        Stream stream = listener.ToStream();
	        bool isMime = DvbNipUtilities.IsMime(stream);
	        if (isMime)
	        {
		        MimeMessage message = MimeMessage.Load(stream);
		        foreach (MimeEntity entity in message.BodyParts)
		        {
                    MimePart bodyPart = entity as MimePart;
                    if (bodyPart.ContentLocation == null)
	                    continue;

                    FluteListenerMime fluteListener = new FluteListenerMime(listener.DestinationAddress, listener.DestinationPort, listener.DestinationTsi, listener.DestinationToi);
                    fluteListener.FileAssociation = new FileType();
                    fluteListener.FileAssociation.ContentLocation = bodyPart.ContentLocation.ToString();
                    fluteListener.FileAssociation.ContentType = bodyPart.ContentType.MimeType;
                    fluteListener.FileAssociation.ContentEncoding = null;
                    fluteListener.WrappedStream = new MemoryStream();
                    bodyPart.Content.DecodeTo(fluteListener.WrappedStream);
                    fluteListener.FileAssociation.ContentLength = (ulong)fluteListener.WrappedStream.Position;
                    fluteListener.FileAssociation.TransferLength = (ulong)fluteListener.WrappedStream.Position;
                    fluteListener.FileAssociation.TransferLengthSpecified = true;
                    fluteListener.WrappedStream.Position = 0;

                    FluteFileArrival(carrier, fluteListener);
		        }
                return;
	        }

	        string extension = Path.GetExtension(listener.FileAssociation.ContentLocation).ToLowerInvariant();
			if (!DvbNipUtilities.IsContinuousFileType(extension))
		        LogEvent(SkyscraperContextEvent.FluteFileArrival, listener.FileAssociation.ContentLocation);
			else
			{
				IPEndPoint ep = new IPEndPoint(listener.DestinationAddress, listener.DestinationPort);
				if (!knownNipSegments.ContainsKey(ep))
					knownNipSegments[ep] = new List<string>();
				knownNipSegments[ep].Add(listener.FileAssociation.ContentLocation);
			}

			if (extension.Equals(".m4s"))
			{
				listener = DvbNipUtilities.PatchMpegDashSegment(listener);
			}

	        ObjectStorage.DvbNipFileArrival(carrier, listener);
        }

        public void OnMulticastGatewayConfiguration(NipActualCarrierInformation carrier, MulticastGatewayConfigurationType multicastGatewayConfiguration)
        {
            UiJunction?.OnDvbNipMulticastGatewayConfiguration(carrier, multicastGatewayConfiguration);

	        if (multicastGatewayConfiguration.MulticastSession != null)
	        {
		        foreach (MulticastSessionType multicastSession in multicastGatewayConfiguration.MulticastSession)
		        {
			        if (!DataStorage.DvbNipTestForMulticastSession(multicastSession))
			        {
				        LogEvent(SkyscraperContextEvent.DvbNipMulticastSession, multicastSession.serviceIdentifier);
				        DataStorage.DvbNipInsertMulticastSession(multicastSession);
			        }
		        }
	        }

	        if (multicastGatewayConfiguration.MulticastGatewayConfigurationTransportSession != null)
	        {
		        foreach (MulticastGatewayConfigurationTransportSessionType multicastGatewayConfigurationTransportSession in multicastGatewayConfiguration.MulticastGatewayConfigurationTransportSession)
		        {
			        if (multicastGatewayConfigurationTransportSession.EndpointAddress == null)
				        continue;

			        MulticastEndpointAddressType endpointAddress = multicastGatewayConfigurationTransportSession.EndpointAddress[0];
			        if (!DataStorage.DvbNipTestForMulticastGatewayConfigurationTransportSession(carrier, endpointAddress))
			        {
				        string name = String.Format("{0} -> {1}:{2}, TSI = {3}", endpointAddress.NetworkSourceAddress,
					        endpointAddress.NetworkDestinationGroupAddress, endpointAddress.TransportDestinationPort,
					        endpointAddress.MediaTransportSessionIdentifier);
				        LogEvent(SkyscraperContextEvent.DvbNipMulticastGatewayConfigurationTransportSession, name);
				        DataStorage.DvbNipInsertMulticastGatewayConfigurationTransportSession(carrier, endpointAddress);
			        }
		        }
	        }

	        if (multicastGatewayConfiguration.MulticastGatewaySessionReporting != null)
	        {
		        throw new NotImplementedException(nameof(multicastGatewayConfiguration.MulticastGatewaySessionReporting));
	        }
        }

		public void OnNipCarrierDetected(NipActualCarrierInformation currentCarrierInformation)
		{
            UiJunction?.EnableUiFeature(SkyscraperUiFeature.DvbNipAnalyis);
            UiJunction?.OnDvbNipCarrierDetected(currentCarrierInformation);
            LogEvent(SkyscraperContextEvent.NipCarrierDetected, String.Format("{0}", currentCarrierInformation.NipStreamProviderName));
            if (!DataStorage.DvbNipTestForCarrier(currentCarrierInformation))
            {
	            DataStorage.DvbNipInsertCarrier(currentCarrierInformation);
            }
		}

		public bool IsFileNeeded(string announcedFileContentLocation)
		{
			if (announcedFileContentLocation.StartsWith("urn:dvb"))
				return true;

			return !ObjectStorage.DvbNipTestForFile(announcedFileContentLocation);
		}

		public void OnPrivateDataSignallingManifest(NipActualCarrierInformation currentCarrierInformation, PrivateDataSignallingManifestType privateDataSignallingManifest)
		{
            UiJunction?.OnDvbNipPrivateDataSignallingManifest(privateDataSignallingManifest);
			foreach (PrivateDataProviderType privateDataProvider in privateDataSignallingManifest.PrivateDataProvider)
			{
                privateDataProvider.privateDataProviderID.FlipEndian();
				uint privateDataSpecifier = BitConverter.ToUInt32(privateDataProvider.privateDataProviderID);
				List<string> privateDataSessions = privateDataProvider.PrivateDataSession.SelectMany(x => x.TagRef).ToList();
				bool added = DataStorage.DvbNipPrivateDataSpecifier(currentCarrierInformation, privateDataSignallingManifest.VersionUpdate, privateDataSpecifier, privateDataSessions);
				if (added)
				{
					LogEvent(SkyscraperContextEvent.NipPrivateDataSpecifier, String.Format("{0} -> {1}", currentCarrierInformation.NipStreamProviderName, privateDataSpecifier));
				}
			}
		}

		public void OnServiceListEntryPoints(NipActualCarrierInformation currentCarrierInformation, ServiceListEntryPoints serviceListEntryPoints, DateTime dvbNipTime, DvbNipServiceListNotifier serviceListNotifier)
		{
            UiJunction?.OnDvbNipServiceListEntryPoints(currentCarrierInformation, serviceListEntryPoints, dvbNipTime);

			long sourceHash = DvbIUtils.GetSourceHash(ServiceListEntryPointSource.DvbNip, currentCarrierInformation.NipNetworkId, currentCarrierInformation.NipCarrierId, currentCarrierInformation.NipLinkId);
			serviceListNotifier.SetSourceHash(sourceHash);
			DvbIDataStorage dataStorage = DataStorage;

			if (dataStorage.TestForDvbiServiceListEntryPoints(sourceHash))
			{
				DateTime lastChecked = dataStorage.GetLastDvbiServiceListEntryPointUpdateDate(sourceHash);
				TimeSpan sinceLastChecked = dvbNipTime - lastChecked;
				if (sinceLastChecked.TotalDays < 1.0)
					return;
			}
			else
			{
				logger.InfoFormat("New DVB-I Service List Entry Point: {0}", currentCarrierInformation.NipStreamProviderName);
                dataStorage.InsertDvbiServiceListEntryPoint(sourceHash);
			}
            
			IEnumerable<DvbiServiceList> enumerable = DvbIUtils.FlattenServiceListEntryPoints(serviceListEntryPoints);
			foreach (DvbiServiceList serviceList in enumerable)
			{
				DateTime serviceListLastChecked;
				if (dataStorage.TestForDvbiServiceList(serviceList.Id))
				{
					serviceListLastChecked = dataStorage.GetDvbiServiceListLastUpdateDate(serviceList.Id);
				}
				else
				{
					logger.InfoFormat("New DVB-I Service List: {0}", serviceList.Name);
					dataStorage.InsertDvbiServiceList(serviceList);
					dataStorage.AddDvbiServiceListToServiceListEntryPoint(serviceList, sourceHash);
					serviceListLastChecked = new DateTime(1970, 1, 1);
				}

				TimeSpan sinceLastServiceListCheck = dvbNipTime - serviceListLastChecked;
				if (sinceLastServiceListCheck.TotalDays < 1.0)
					continue;

				serviceListNotifier.AddServiceListUrl(serviceList.URI, serviceList.Id);
				/*if (vfs.FileExists(serviceList.URI))
				{
					byte[] serviceListBytes = vfs.GetFile(serviceList.URI);
					ServiceListType serviceListData = DvbIUtils.UnpackServiceList(serviceListBytes);
					HandleServiceList(serviceListData, dataStorage, serviceList.Id);
					dataStorage.UpdateDvbiServiceListLastCheckedDate(serviceList.Id, context.CurrentTime);
				}*/
			}

			dataStorage.UpdateDvbiServiceListEntryPointUpdateDate(sourceHash, dvbNipTime);
		}

		public void OnServiceList(NipActualCarrierInformation currentCarrierInformation, string serviceListId,
			ServiceListType serviceList)
		{
            UiJunction?.OnDvbNipServiceList(currentCarrierInformation, serviceListId, serviceList);

			List<DvbIService> services = DvbIUtils.FlattenServiceList(serviceList).ToList();
			DvbIDataStorage dataStorage = DataStorage;
			foreach (DvbIService service in services)
			{
				if (dataStorage.TestForDvbiService(service.Id))
				{
					int versionInDb = dataStorage.GetDvbiServiceVersion(service.Id);
					if (service.Version > versionInDb)
					{
						dataStorage.UpdateDvbiService(service);
					}
				}
				else
				{
					logger.InfoFormat("New DVB-I Service: {0}", service.ServiceName);
					dataStorage.InsertDvbiService(service);
					dataStorage.AddDvbiServiceToServiceList(service.Id, serviceListId);
				}
			}
		}

		public void OnTimeOffsetFile(NipActualCarrierInformation currentCarrierInformation, TimeOffsetFileType timeOffsetFile)
		{
            UiJunction?.OnDvbNipTimeOffsetFile(currentCarrierInformation, timeOffsetFile);
			if (timeOffsetFile.time_of_change != DateTime.MinValue)
			{
				throw new NotImplementedException();
			}
		}

		public void OnNetworkInformationFile(NipActualCarrierInformation currentCarrierInformation, NetworkInformationFileType networkInformationFile)
		{
            UiJunction?.OnDvbNipNetworkInformationFile(currentCarrierInformation, networkInformationFile);
			List<BroadcastNetworkType> broadcastNetworks = new List<BroadcastNetworkType>();
			if (networkInformationFile.ActualBroadcastNetwork != null)
				broadcastNetworks.Add(networkInformationFile.ActualBroadcastNetwork);
			if (networkInformationFile.OtherBroadcastNetwork != null)
				broadcastNetworks.AddRange(networkInformationFile.OtherBroadcastNetwork);

			foreach (BroadcastNetworkType network in broadcastNetworks)
			{
				if (!DataStorage.DvbNipTestForNetwork(network))
				{
					LogEvent(SkyscraperContextEvent.DvbNipNetwork, network.NetworkName);
					DataStorage.DvbNipInsertNetwork(network);
				}
			}
		}

		public void OnServiceInformationFile(NipActualCarrierInformation currentCarrierInformation, ServiceInformationFileType serviceInformationFile)
		{
            UiJunction?.DvbNipServiceInformation(currentCarrierInformation, serviceInformationFile);
			foreach (BroadcastMediaStreamType broadcastMediaStreamType in serviceInformationFile.BroadcastMediaStream)
			{
				if (!DataStorage.DvbNipTestForService(broadcastMediaStreamType))
				{
					string id = String.Format("{0},{1},{2},{3}", broadcastMediaStreamType.NIPNetworkID,
						broadcastMediaStreamType.NIPCarrierID, broadcastMediaStreamType.NIPLinkID,
						broadcastMediaStreamType.NIPServiceID);
					LogEvent(SkyscraperContextEvent.DvbNipService, id);
                    DataStorage.DvbNipInsertService(broadcastMediaStreamType);
				}
			}
		}

		private Dictionary<IPEndPoint, List<string>> knownNipSegments;
		private HashSet<string> knownUrls;
		public void FluteFileAnnouncement(IPAddress ip, ushort port, FDTInstanceType flute)
		{
            UiJunction?.OnDvbNipFileAnnouncement(flute);

			if (knownUrls == null)
			{
				knownUrls = new HashSet<string>();
				knownNipSegments = new Dictionary<IPEndPoint, List<string>>();
			}

			string url = flute.File[0].ContentLocation;
			if (knownUrls.Contains(url))
				return;

			LogEvent(SkyscraperContextEvent.FluteFileAnnouncement, String.Format("{0}:{1} -> {2}", ip.ToString(), port, url));
			knownUrls.Add(url);

			IPEndPoint ep = new IPEndPoint(ip, port);
			if (!knownNipSegments.ContainsKey(ep))
				knownNipSegments[ep] = new List<string>();
			knownNipSegments[ep].Add(url);
		}

		public void FluteFileDownloadProgress(NipActualCarrierInformation currentCarrierInformation, ulong destinationTsi, ulong destinationToi, double progress, FileType filetype)
		{
            if (filetype != null)
            {
	            if (!filetype.ContentLocation.Equals("urn:dvb:metadata:cs:NativeIPMulticastTransportObjectTypeCS:2023:bootstrap"))
	            {
		            string extension = Path.GetExtension(filetype.ContentLocation).ToLowerInvariant();
		            if (!DvbNipUtilities.IsContinuousFileType(extension))
		            {
			            LogEvent(SkyscraperContextEvent.FluteFileProgress,
				            String.Format("Filename={0}, {1}%", filetype.ContentLocation, progress));
		            }
	            }
            }
		}

		private List<object> _pluginContexts;

		public List<object> PluginContext
		{
			get
			{
				if (_pluginContexts == null)
					_pluginContexts = new List<object>();

				return _pluginContexts;
			}
		}

		public void OnPluginEvent()
		{
			lastEventTimestamp = DateTime.Now;
		}

		public void OnEthernetFrame(int pid, PhysicalAddress destination, PhysicalAddress source, ushort etherType, byte[] contents)
		{
			if (Array.TrueForAll(contents, x => x == 0))
				return;
			if (etherType <= 1500)
			{
				OnLlcFrame(etherType, contents);
				return;
			}
			switch (etherType)
			{
                case 0x0800:
	                OnIpDatagram(pid, contents);
	                return;
                case 0x22e3:
					//This is related to G.9961, a PowerLine standard specified by ITU-T T-REC-G.9961
					//I don't think we need this.
					return;
                case 0x2f00:
                    //This is something proprietary.
					//Quite possibly related to Extron hardware?
                    return;
				case 0x0806:
                    //This is an ARP Frame. We don't need it.
                    return;
                case 0x8137:
	                //This is an IPX Frame. We don't need it.
	                return;
                case 0x86dd:
	                OnIpDatagram(pid, contents);
	                return;
                case 0x88e1:
					//This is related to FRITZ!Powerline.
					//Probably very proprietary and undocumented, so I guess we'll discard it.
					return;
				case 0x890d:
					//IEEE Std 802.11 - Fast Roaming Remote Request (802.11r)
					//These seem to happen when one moves from one Wi-Fi Access Point to another. We probably don't need that one.
					return;
                case 0x8912:
					//Related to mediaxtream, see https://de.wikipedia.org/wiki/Mediaxtream
					//Probably not very interesting.
					return;
				case 0x9001:
					//3Com(Bridge) XNS Sys Mgmt
					//Can't say anything about these. I don't have any 3Com equipment.
					return;
                case 0x94ce:
                    //Something proprietary. No idea.
                    return;
                case 0xb69d:
	                //Something proprietary. No idea.
	                return;
				case 0xf67f:
	                if (contents[20] == 0xaa && contents[21] == 0xaa && contents[22] == 0x03 && contents[23] == 0x00 && contents[24] == 0x00 && contents[25] == 0x00)
	                {
		                (contents[26], contents[27]) = (contents[27], contents[26]);
		                ushort newEtherType = BitConverter.ToUInt16(contents, 26);
		                byte[] newPacket = new byte[contents.Length - 28];
		                Array.Copy(contents, 28, newPacket, 0, newPacket.Length);
		                OnEthernetFrame(pid, destination, source, newEtherType, newPacket);
	                }
	                return;
                case 0x94ad:
                    //Something proprietary. No idea.
                    return;
                case 0x4957:
                    //Something proprietary. No idea.
                    return;
                case 0x3e30:
                    //Something proprietary. No idea.
                    return;
                case 0x88cc:
                    //Link Layer Discovery Protocol
                    LldpFrame lldpFrame = new LldpFrame(contents);
                    OnLldpFrame(lldpFrame);
                    return;
                case 0x9417:
                case 0xf95c:
                    //Unknown proprietary.
                    return;
				default:
	                throw new NotImplementedException(String.Format("EtherType: 0x{0:X4}", etherType));
			}
		}

		public void OnUleError(int pid, int errorNo)
		{
			throw new NotImplementedException();
		}

		public void OnLlcFrame(ushort etherType, byte[] contents)
		{
			MemoryStream llcStream = new MemoryStream(contents, true);
			byte dsap = llcStream.ReadUInt8();
			byte ssap = llcStream.ReadUInt8();
			if (dsap == 0x00 || ssap == 0x00)
			{
                //This is a NULL frame. It can be discarded either way.
				return;
			}
			if (dsap == 0x42 || ssap == 0x42)
			{
				//This is a Spanning Tree Frame. We probably don't need it.
				return;
			}
			if (dsap == 0xe0 || ssap == 0xe0)
			{
				//This is a Novell Netware Frame. We don't need to bother with it.
				return;
			}

			if (dsap == 0xaa || ssap == 0xaa)
			{
				byte snapType = llcStream.ReadUInt8();
				if (snapType == 0x03)
				{
					byte[] oui = llcStream.ReadBytes(3);
					ushort snapProtocolId = llcStream.ReadUInt16BE();
					if (snapProtocolId == 0x809b)
					{
                        //This is an AppleTalk Frame. We probably don't need it.
                        return;
					}
                    else if (snapProtocolId == 0x8137)
					{
                        //This is an IPX Frame. We probably don't need it.
                        return;
					}
                    else if (snapProtocolId == 0x2000)
					{
                        //This is from Cisco's Discovery Protocol (CDP). Probably not interesting.
                        return;
					}
                    else
                    {
                        throw new NotImplementedException(String.Format("SNAP Frame, OUI = {0:X2}-{1:X2}-{2:X2}, Protocol ID = 0x{3:X4}", oui[0], oui[1], oui[2], snapProtocolId));
                    }
				}
				else
				{
					throw new NotImplementedException(String.Format("LLC/SNAP Type 0x{0:X2}", snapType));
				}
			}

			if (dsap == 0xff || ssap == 0xff)
			{
				ushort llcLength = llcStream.ReadUInt16BE();
				if (llcLength == etherType)
				{
					//TEST or XID PDU, likely unneeded.
					return;
				}
                else if (llcLength == 40)
				{
					//TEST or XID PDU, likely unneeded.
					return;
				}
                else
                {
                    throw new NotImplementedException("LLC/SNAP (2)");
                }
			}

            if (dsap == 0x09 && ssap == 0x01)
            {
                //Likely an LLC management packet (TEST or XID)
                return;
            }
			throw new NotImplementedException("LLC/SNAP");
		}

		public void OnOtvSsuError(int pid, OtvSsuError offsetOutOfRange)
		{
			DvbContext.RegisterPacketProcessor(pid, new PacketDiscarder());
		}

		public void OnOtvSsuFileAnnouncement(int pid, ushort tableIdExtension, uint fileId, uint unknown1, uint length)
		{
			LogEvent(SkyscraperContextEvent.OtvSsuFileDetected, String.Format("TID = {0}, FID = {1:X8}, Length = {2}", tableIdExtension, fileId, length));
		}

		public void OnOtvSsuBlock(int pid, ushort tableIdExtension, uint fileId, uint unknown1, uint length)
		{
		}

		public void OnOtvSsuComplete(int sourcePid, Stream getStream, ushort tableIdExtension, uint fileId, uint unknown1,
			uint length)
		{
            LogEvent(SkyscraperContextEvent.OtvSsuComplete, String.Format("TID = {0}, FID = {1:X8}, Length = {2}", tableIdExtension, fileId, length));
			ObjectStorage.OnOtvSsuComplete(CurrentNetworkId, CurrentTransportStreamId, sourcePid, getStream, tableIdExtension, fileId, unknown1, length);
		}

		public bool OnOtvCheckFileAlreadyKnown(int sourcePid, ushort tableIdExtension, uint fileId, uint unknown1, uint length)
		{
			return ObjectStorage.OtvSsuTestFile(CurrentNetworkId, CurrentTransportStreamId, sourcePid, tableIdExtension, fileId, unknown1, length);
		}

		public void OnNdsSsuError(int pid, NdsSsuError error)
		{
		}
        
		public void OnNdsSsuFileAnnouncement(int pid, ushort tableIdExtension)
		{
			LogEvent(SkyscraperContextEvent.NdsSsuFileAnnounced, String.Format("PID = 0x{1:X4}, Table ID Extension = {0}", tableIdExtension, pid));
		}

		public void OnNdsSsuFileComplete(int pid, ushort tableIdExtension, NdsSsuDataMap dataMap)
		{
			ObjectStorage.OnNdsSsuComplete(CurrentNetworkId, CurrentTransportStreamId, pid, tableIdExtension, dataMap);
		}

		public void OnNdsSsuProgress(int pid, ushort tableIdExtension, int blocksDone, int blocksTotal, bool theresMore)
		{
			LogEvent(SkyscraperContextEvent.NdsSsuProgress, String.Format("PID 0x{0:X4}, File ID {1}, Blocks: {2}/{3}{4}", pid, tableIdExtension, blocksDone, blocksTotal, theresMore ? "+" : ""));
		}

		public bool TestNdsSsuFileExists(int pid, ushort tableIdExtension)
		{
			return ObjectStorage.NdsSsuTestFile(CurrentNetworkId, CurrentTransportStreamId, pid, tableIdExtension);
		}

        private HashSet<LldpFrame> lldpFrames;
        public void OnLldpFrame(LldpFrame frame)
        {
	        if (lldpFrames == null)
		        lldpFrames = new HashSet<LldpFrame>();

	        if (lldpFrames.Add(frame))
	        {
		        LogEvent(SkyscraperContextEvent.EthernetLinkLayerDiscovery, frame.ToString());
	        }
        }

        void T2MIEventHandler.OnFramingAndTimingInformation(int relatedPid, _0xF0_FramingTimingInformation fti)
        {
            logger.WarnFormat("Found T2MI F&TI Information on PID 0x{0:X4}. This isn't supported yet. It would be great if you could share a sample of this stream, so this can be implemented.", relatedPid);
        }

        public void OnKeyManagementResponse(PrivacyKeyManagementResponse privacyKeyManagementResponse)
        {
            logger.WarnFormat("Found a DOCSIS Key Management Response. Those aren't supported yet. It would be great if you could share a sample of this stream, so those can be implemented.");
        }

        public void OnDynamicServiceAddition(ushort transactionId, byte confirmationCode, CommonTlvEncodingObject tlvEncodedInformation)
        {
            logger.WarnFormat("Found a DOCSIS Dynamic Service Addition. Those aren't supported yet. It would be great if you could share a sample of this stream, so those can be implemented.");
        }

        private SkyscraperContext GetSisContext(ushort sisService)
        {
            if (subSkyscrapers == null)
                subSkyscrapers = new Dictionary<object, SkyscraperContext>();

            SisProgramIdentifier sisContext = new SisProgramIdentifier(sisService);
            if (subSkyscrapers.ContainsKey(sisContext))
            {
                return subSkyscrapers[sisContext];
            }
            else
            {
                LogEvent(SkyscraperContextEvent.DvbSisDaughterSitePsi, String.Format("Additional terrestrial PSI/SI tables for SIS Program {0}", sisService));
                SkyscraperContext child = new SkyscraperContext(new TsContext(), this.DataStorage, this.ObjectStorage);
                child.IsChild = true;
                child.ChildName = sisContext.ToString();
                subSkyscrapers.Add(sisContext, child);
                return child;
            }
        }

        public void OnSisCat(int sourcePid, ushort sourceProgram, SisCatContainer catContainer)
        {
            UiJunction?.OnDvbSisCat(sourcePid, catContainer);
            if (catContainer.CaDescriptors.Count > 0)
            {
                SkyscraperContext skyscraperContext = GetSisContext(sourceProgram);

                foreach(CaDescriptor catEntry in catContainer.CaDescriptors)
                {
                    skyscraperContext.NotifyOfCaSystem(catEntry);
                }
            }
        }

        public void OnSisDsaci(ushort currentDsaGroupId, int versionNumber, byte sectionNumber, byte lastSectionNumber, Stream dsaci)
        {
            UiJunction?.OnDvbSisDsaci(currentDsaGroupId, versionNumber, sectionNumber, lastSectionNumber, dsaci);

            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            if (!ObjectStorage.TestForSisDsaci(CurrentNetworkId.Value,CurrentTransportStreamId.Value, currentDsaGroupId, versionNumber))
            {
                dsaci.Position = 0;
                byte gzMagicA = dsaci.ReadUInt8();
                byte gzMagicB = dsaci.ReadUInt8();
                dsaci.Position -= 2;
                if (gzMagicA == 0x1f &&  gzMagicB == 0x8b)
                {
                    dsaci = new GZipStream(dsaci, CompressionMode.Decompress);
                }

                LogEvent(SkyscraperContextEvent.DvbSisDaughterSiteAdapterConfiguration, String.Format("Group ID = {0}, Version = {1}", currentDsaGroupId, versionNumber));
                ObjectStorage.StoreSisDsaci(CurrentNetworkId.Value, CurrentTransportStreamId.Value, currentDsaGroupId, versionNumber, dsaci);
                dsaci.Dispose();
            }
        }

        public void OnSisEit(int sourcePid, ushort sourceProgram, SisEitContainer eitContainer)
        {
            UiJunction?.OnDvbSisEit(sourcePid, eitContainer);
            SkyscraperContext skyscraperContext = GetSisContext(sourceProgram);
            if (eitContainer.NetworkId.HasValue)
                skyscraperContext.SetNetworkId(eitContainer.NetworkId.Value);
            if (eitContainer.TransportStreamId.HasValue)
                skyscraperContext.SetTransportStreamId(eitContainer.TransportStreamId.Value);

            foreach (EitEvent eitEvent in eitContainer.Events)
            {
                skyscraperContext.OnEitEvent(eitEvent);
            }
        }

        private bool[] _sisFtiFlags;
        private _0xF0_FramingTimingInformation.Plp[][] _sisFtiData;
        public void OnSisFti(int relatedPid, _0xF0_FramingTimingInformation fti)
        {
            UiJunction?.OnDvbSisFti(relatedPid, fti);
            if (_sisFtiFlags == null)
            {
                _sisFtiFlags = new bool[0x1fff];
                _sisFtiData = new _0xF0_FramingTimingInformation.Plp[0x1fff][];
            }

            if (!_sisFtiFlags[relatedPid])
            {
                LogEvent(SkyscraperContextEvent.DvbSisFramingInformation, String.Format("on PID 0x{0:X4}", relatedPid));
                _sisFtiFlags[relatedPid] = true;
            }
            _sisFtiData[relatedPid] = fti.Plps;
        }

        public void OnSisNit(int sourcePid, ushort sourceProgram, SisNitContainer nitContainer)
        {
            UiJunction?.OnDvbSisNit(sourcePid, nitContainer);
            SkyscraperContext skyscraperContext = GetSisContext(sourceProgram);
            if (nitContainer.NetworkId.HasValue)
                skyscraperContext.SetNetworkId(nitContainer.NetworkId.Value);
            if (nitContainer.Network != null)
                skyscraperContext.OnNitNetwork(nitContainer.Network);

            foreach(Tuple<ushort,NitTransportStream> nitEntry in nitContainer.TransportStreams)
            {
                ushort networkId = nitEntry.Item1;
                NitTransportStream ts = nitEntry.Item2;
                skyscraperContext.OnNitTransportStream(networkId, ts);
            }
        }

        public void OnSisPat(int sourcePid, ushort sourceProgram, SisPatContainer patContainer)
        {
            UiJunction?.OnDvbSisPat(sourcePid, patContainer);

            SkyscraperContext skyscraperContext = GetSisContext(sourceProgram);
            if (patContainer.NetworkPid.HasValue)
                skyscraperContext.NetworkPidFromPat(patContainer.NetworkPid.Value);
            if (patContainer.TransportStreamId.HasValue)
                skyscraperContext.SetTransportStreamId(patContainer.TransportStreamId.Value);

            foreach ( KeyValuePair<int, ushort> program in patContainer.ProgramMappings)
            {
                skyscraperContext.ProgramMapPidFromPat(program.Key, program.Value);
            }

        }

        public void OnSisPmt(int sourcePid, ushort sourceProgram, SisPmtContainer pmtContainer)
        {
            UiJunction?.OnDvbSisPmt(sourcePid, pmtContainer);
            SkyscraperContext context = GetSisContext(sourceProgram);
            context.PmtEvent(pmtContainer.ProgramMapping, 0);
        }

        public void OnSisSdt(int sourcePid, ushort sourceProgram, SisSdtContainer sdtContainer)
        {
            UiJunction?.OnDvbSisSdt(sourcePid, sdtContainer);
            SkyscraperContext context = GetSisContext(sourceProgram);

            if (sdtContainer.NetworkId.HasValue)
                context.SetNetworkId(sdtContainer.NetworkId.Value);
            if (sdtContainer.TransportStreamId.HasValue)
                context.SetTransportStreamId(sdtContainer.TransportStreamId.Value);

            foreach(Tuple<ushort, ushort, SdtService> sdtEntry in sdtContainer.Services)
            {
                ushort tsId = sdtEntry.Item1;
                ushort netId = sdtEntry.Item2;
                SdtService service = sdtEntry.Item3;
                context.OnSdtService(tsId, netId, service);
            }
        }

        public void OnSisTdt(int sourcePid, ushort sourceProgram, SisTdtContainer tdtContainer)
        {
            UiJunction?.OnDvbSisTdt(sourcePid, tdtContainer.UtcTime);
            if (tdtContainer.UtcTime.HasValue)
            {
                SkyscraperContext context = GetSisContext(sourceProgram);
                context.OnTdtTime(tdtContainer.UtcTime.Value);
            }
        }

        private bool[] _sisTimestampFlags;
        public void OnSisTimestamp(int pid, _0x20_DvbT2Timestamp t2Timestamp)
        {
            UiJunction?.OnDvbSisTimestamp(pid, t2Timestamp);

            if (!CurrentNetworkId.HasValue)
                return;

            if (!CurrentTransportStreamId.HasValue)
                return;

            DateTime resolveTime = t2Timestamp.ResolveTime();
            DateTime knownTimestamp = DataStorage.T2MiGetTimestamp(CurrentNetworkId.Value, CurrentTransportStreamId.Value, pid);
            if (resolveTime > knownTimestamp)
            {
                if (_sisTimestampFlags == null)
                    _sisTimestampFlags = new bool[0x1fff];

                if (!_sisTimestampFlags[pid])
                {
                    LogEvent(SkyscraperContextEvent.DvbSisTimestamp, t2Timestamp.ResolveTime().ToString());
                    _sisTimestampFlags[pid] = true;
                }
                DataStorage.T2MiSetTimestamp(CurrentNetworkId.Value, CurrentTransportStreamId.Value, pid, resolveTime);
            }
        }

        public void OnSisTot(int sourcePid, ushort sourceProgram, SisTotContainer totContainer)
        {
            if (totContainer.LocalTimeOffset != null)
            {
                SkyscraperContext context = GetSisContext(sourceProgram);
                context.OnTotTime(totContainer.UtcTime, totContainer.LocalTimeOffset);
            }
        }

        private ushort lastKnownInteractiveNetworkId;
        private int networkIdChanges;
        
        private void SetLikelyNetwork(ushort interactiveNetworkId)
        {
            if (lastKnownInteractiveNetworkId != interactiveNetworkId)
            {
                lastKnownInteractiveNetworkId = interactiveNetworkId;
                networkIdChanges++;
            }
        }

		public void OnTerminalInformationMessage(ushort? interactiveNetworkId, PhysicalAddress physicalAddress, Tim currentTim)
		{
            if (!interactiveNetworkId.HasValue)
            {
				if (networkIdChanges != 1)
					return;

                interactiveNetworkId = lastKnownInteractiveNetworkId;
			}
            else
            {
                SetLikelyNetwork(interactiveNetworkId.Value);
            }

            Tim lastTim = DataStorage.GetLastTim(interactiveNetworkId.Value, physicalAddress);

            if (currentTim.Equals(lastTim))
            {
                return;
            }

            DataStorage.UpsertTim(interactiveNetworkId.Value, physicalAddress, currentTim);

            LogEvent(SkyscraperContextEvent.Tim, String.Format("Terminal Information Message for participant {0} on network {1}", physicalAddress, interactiveNetworkId));
            foreach(TsDescriptor ts in currentTim.Descriptors)
            {
				LogEvent(SkyscraperContextEvent.Tim, String.Format(" -> {0}", ts.ToString()));
			}
		}

        void IWneHandler.OnWneStoryComplete(WneStoryProgress wneStoryProgress, Stream value)
        {
            UiJunction?.WneStoryComplete(wneStoryProgress.SessionId, wneStoryProgress.Filename);
            if (!ObjectStorage.TestForWneStory(wneStoryProgress.SessionId,wneStoryProgress.Filename))
            {
                ObjectStorage.StoreWneStory(wneStoryProgress.SessionId, wneStoryProgress.Filename, value);
            }
        }

        public void OnWneStoryDetect(uint embeddedSessionId)
        {
            UiJunction?.WneStoryDetect(embeddedSessionId);
        }

        public void OnWneStoryFail(uint sessionId)
        {
            UiJunction?.WneStoryError(sessionId);
        }

        void IWneHandler.OnWneStoryProgress(WneStoryProgress wneStoryProgress)
        {
            UiJunction?.WneStoryProgress(wneStoryProgress.SessionId, wneStoryProgress.Filename, wneStoryProgress.FileCaught, wneStoryProgress.FileSize);
        }
    }
}
