using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml.XPath;
using Ionic.Crc;
using skyscraper5.Docsis.MacManagement;
using skyscraper5.Docsis.MacManagement.BaselinePrivacyKeyManagementMessages;
using skyscraper5.Ietf.Rfc2460;
using skyscraper5.Ietf.Rfc826;
using skyscraper5.Ietf.Rfc971;
using skyscraper5.Mpeg2;
using skyscraper5.Skyscraper;
using skyscraper5.Skyscraper.IO;
using skyscraper5.Skyscraper.Net.Pcap;
using skyscraper5.Skyscraper.Plugins;

namespace skyscraper5.Docsis
{
    public class DocsisEnvironment : IDisposable
    {
        private readonly IDocsisEventHandler eventHandler;

        public DocsisEnvironment(IDocsisEventHandler eventHandler)
        {
            this.eventHandler = eventHandler;
            this._stats = new Statistics();
        }

        public void PushPacket(MemoryStream ms)
        {
            _stats.Throughput += ms.Length;

            byte fc = ms.ReadUInt8();
            int fc_type = (fc & 0xc0) >> 6;
            int fc_parm = (fc & 0x3e) >> 1;
            bool ehdr_on = (fc & 0x01) != 0;

            bool isQueueDepthBasedRequest = false;
            ushort mac_parm;
            if (isQueueDepthBasedRequest)
                mac_parm = ms.ReadUInt16BE();
            else
                mac_parm = ms.ReadUInt8();

            MacExtendedHeader eh = null;
            ushort len = ms.ReadUInt16BE();
            if (ehdr_on)
            {
                len -= mac_parm;
                byte[] ehdr = ms.ReadBytes(mac_parm);
                eh = new MacExtendedHeader(ehdr);
            }

            ushort hcs = ms.ReadUInt16BE();

            switch (fc_type)
            {
                case 0b00:
                    _stats.PacketPduPresent = true;
                    _stats.PacketPdus++;
                    PushPacketPdu(false, len, ms, eh);
                    break;
                case 0b11:
                    _stats.MacSpecificHeadersPresent = true;
                    _stats.MacSpecificHeaders++;
                    PushMacSpecificHeader(fc_parm, ms);
                    break;
                case 0b10:
                    _stats.IsolationPacketPduPresent = true;
                    _stats.IsolationPacketPdus++;
                    PushPacketPdu(true, len, ms, eh);
                    break;
                case 0b01:
                    _stats.SpecialUseMacPresent = true;
                    _stats.SpecialUseMacs++;
                    PushSpecialUseMac(ms);
                    break;

                default:
                    throw new NotImplementedException(String.Format("{0:X}", fc_type));
            }
        }

        private void PushSpecialUseMac(MemoryStream ms)
        {
			if (ms.GetAvailableBytes() < 12)
				return;

			PhysicalAddress da = new PhysicalAddress(ms.ReadBytes(6));
            PhysicalAddress sa = new PhysicalAddress(ms.ReadBytes(6));
        }
        
        private const bool ENABLE_DOCSIS_DUMPER = true;
        private PcapWriter pcapWriter;
        private void PushPacketPdu(bool isolation, ushort len, MemoryStream ms, MacExtendedHeader eh)
        {
            if (len < 14)
                return;

            byte[] buffer = ms.ReadBytes(len);
            ms = new MemoryStream(buffer);

            PhysicalAddress da = new PhysicalAddress(ms.ReadBytes(6));
            PhysicalAddress sa = new PhysicalAddress(ms.ReadBytes(6));
            ushort etherType = ms.ReadUInt16BE();
            byte[] userData = ms.ReadBytes(ms.GetAvailableBytes() - 4);

            AddParticipant(da);
            AddParticipant(sa);

            Statistics.ParticipantStatistic participantStatistic;
            lock (_stats.ParticipantStatistics)
            {
                participantStatistic = _stats.ParticipantStatistics.Find(x => x.Address.Equals(da));
                if (participantStatistic == null)
                {
                    participantStatistic = new Statistics.ParticipantStatistic();
                    participantStatistic.Address = da;
                    _stats.ParticipantStatistics.Add(participantStatistic);
                }

                participantStatistic.Packets++;
                participantStatistic.Throughput += userData.Length;
            }

            if (eh != null && eh.Encrypted)
            {
                _stats.ScrambledPackets++;
                //screw this packet, it's encrypted, we can't deal with it. Danke, Merkel.
                return;
            }
            else
            {
                _stats.UnscrambledPackets++;
            }


            if (ENABLE_DOCSIS_DUMPER)
            {
                if (pcapWriter == null)
                {
                    FileStream fileStream = File.OpenWrite(String.Format("{0}.pcap", DateTime.Now.ToUnixTime()));
                    pcapWriter = new PcapWriter(fileStream, TcpdumpNetworkType.Ethernet);
                }
                pcapWriter.WritePacket(buffer);
            }

            if (etherType == 0x86dd)
            {
                Ipv6Header ipv6Header = new Ipv6Header(userData);
                switch (ipv6Header.NextHeader)
                {
                    case 0x3a:  
                        //ICMP-v6. Boring
                        break;
                    default:
                        throw new NotImplementedException(String.Format("{0}.{1} 0x{2:X4}", nameof(Ipv6Header), nameof(ipv6Header.NextHeader), ipv6Header.NextHeader));
                }
            }
            else if (etherType == 0x0806)
            {
                ArpHeader arpHeader = new ArpHeader(userData);
                eventHandler.OnLearnedIpFromMac(arpHeader.SenderHardwareAddress, arpHeader.SenderProtocolAddress);
                if (arpHeader.Operation == ArpHeader.OperationEnum.Request)
                {
                    participantStatistic.Ip = arpHeader.TargetProtocolAddress;
                }
                else if (arpHeader.Operation == ArpHeader.OperationEnum.Reply)
                {
                    participantStatistic.Ip = arpHeader.SenderProtocolAddress;
                }
                else
                {
                    throw new NotImplementedException();
                }
            }
            else if (etherType == 0x0800)
            {
                InternetHeader ipv4Header = new InternetHeader(userData);
                switch (ipv4Header.Protocol)
                {
                    case 0x02:  //IGMP
                        break;
                    case 0x11:  //UDP
                        break;
                    default:
                        throw new NotImplementedException(String.Format("{0} {1}", nameof(ipv4Header.Protocol), ipv4Header.Protocol));
                }
            }
            else
            {
                //throw new NotImplementedException(String.Format("{0} {1:X4}", nameof(etherType), etherType));
            }
        }
        
        private void PushMacSpecificHeader(int fc_parm, MemoryStream remainingBytes)
        {
            CrcCalculatorStream crcStream = new CrcCalculatorStream(remainingBytes, true);

            PhysicalAddress da = new PhysicalAddress(crcStream.ReadBytes(6));
            PhysicalAddress sa = new PhysicalAddress(crcStream.ReadBytes(6));
            ushort msgLen = crcStream.ReadUInt16BE();
            byte dsap = crcStream.ReadUInt8();
            byte ssap = crcStream.ReadUInt8();
            byte control = crcStream.ReadUInt8();
            byte version = crcStream.ReadUInt8();
            byte type = crcStream.ReadUInt8();
            byte rsvd = crcStream.ReadUInt8();
            msgLen -= 6;

            if (msgLen > remainingBytes.GetAvailableBytes())
                return;

            byte[] packetBytes = crcStream.ReadBytes(msgLen);

            int crc = remainingBytes.ReadInt32LE();
            if (crcStream.Crc != crc)
                return;

            AddParticipant(da);
            AddParticipant(sa);

            switch (fc_parm)
            {
                case 0b00000:
                    PushTimingMessage(sa, packetBytes);
                    break;
                case 0b00001:
                    PushMacManagementMessage(packetBytes, version, type, sa, da, packetBytes);
                    break;
                case 0b00010:
                    //TODO: Request Frame
                    throw new NotImplementedException("Request Frame");
                case 0b00011:
                    throw new NotImplementedException("Fragmentation Header");
                case 0b00100:
                    throw new NotImplementedException("Queue Depth-based Request Frame");
                case 0b11100:
                    throw new NotImplementedException("Concatenation Header");
				default:
                    Debug.WriteLine(String.Format("Got MAC Specific Header with FC_PARM {0} - this wasn't valid in DOCSIS 4.0. Is it time to upgrade?", fc_parm));
                    break;
            }
        }

        private Dictionary<PhysicalAddress, uint> timingMessages;
        private void PushTimingMessage(PhysicalAddress source, byte[] remainingBytes)
        {
            (remainingBytes[3], remainingBytes[2], remainingBytes[1], remainingBytes[0]) = (remainingBytes[0], remainingBytes[1], remainingBytes[2], remainingBytes[3]);
            uint timing = BitConverter.ToUInt32(remainingBytes);

            if (timingMessages == null)
            {
                timingMessages = new Dictionary<PhysicalAddress, uint>();
            }

            if (!timingMessages.ContainsKey(source))
            {
                timingMessages[source] = 0;
            }

            if (timingMessages[source] != timing)
            {
                timingMessages[source] = timing;
                eventHandler.OnCmtsTimestamp(source, timing);
            }
        }

        private ReadOnlyDictionary<MacManagementMessageTypeAttribute, Type> _macManagementMessageTypes;

        public void PushMacManagementMessage(byte[] ms, byte version, byte type, PhysicalAddress sourceAddress, PhysicalAddress destinationAddress, byte[] readBytes)
        {

            if (_macManagementMessageTypes == null)
            {
	            _macManagementMessageTypes = PluginManager.GetInstance().GetDocsisMacManagementMessageTypes();

                if (_macManagementMessageTypes.Count == 0)
                {
	                throw new DocsisException("Didn't find any MAC Management Types.");
                }
            }

            if (_macManagementMessageTypes.Count == 0)
            {
                throw new DocsisException("No MAC Management Message Types");
            }


            MacManagementMessageTypeAttribute neededType = new MacManagementMessageTypeAttribute(version, type);
            if (!_macManagementMessageTypes.ContainsKey(neededType))
            {
                //Found in CM-SP-MULPIv3.0-C0I-171207.pdf  on page 94
                //   or in CM-SP-MULPIv3.1-I03-1406101.pdf on page 100
                //   or in CM-SP-MULPIv4.0-I01-190815.pdf  on page 103
                throw new NotImplementedException(String.Format("{0} {1}", nameof(_macManagementMessageTypes), neededType.ToString()));
            }
            Type macManagementMessageType = _macManagementMessageTypes[neededType];
            MacManagementMessage mmm = (MacManagementMessage)Activator.CreateInstance(macManagementMessageType, new object[] { sourceAddress, destinationAddress, readBytes });
            if (!mmm.Valid)
                return;

            string name = mmm.GetType().Name;
            switch (name)
            {
                case nameof(UpstreamBandwidthAllocation):
                    //not particulary interesting
                    break;
                case nameof(UpstreamBandwidthAllocationV5):
                    //not particulary interesting either
                    break;
                case nameof(UpstreamChannelDescriptor):
                    eventHandler.OnUpstreamChannel((UpstreamChannelDescriptor)mmm);
                    break;
                case nameof(RangingResponse):
                    //not interesting either
                    break;
                case nameof(StatusReportAcknowledge):
                    //take a guess?
                    break;
                case nameof(MacDomainDescriptor):
                    MacDomainDescriptor macDomainDescriptor = (MacDomainDescriptor)mmm;
                    if (macDomainDescriptor.DownstreamActiveChannels == null)
                        break;
                    if (macDomainDescriptor.DownstreamActiveChannels.Count == 0)
                        break;
                    foreach (MacDomainDescriptor.DownstreamActiveChannel downstreamActiveChannel in macDomainDescriptor.DownstreamActiveChannels)
                    {
                        eventHandler.OnDownstreamChannel(macDomainDescriptor.Source, downstreamActiveChannel);
                    }
                    break;
                case nameof(PrivacyKeyManagementResponse):
                    PrivacyKeyManagementResponse privacyKeyManagementResponse = (PrivacyKeyManagementResponse)mmm;
                    eventHandler.OnKeyManagementResponse(privacyKeyManagementResponse);
                    break;
                case nameof(DynamicServiceChangeResponse):
                    //Nothing of interesent in here either.
                    break;
                case nameof(DynamicBondingChangeRequest):
                    //doesn't contain anything of interest.
                    break;
                case nameof(DynamicBondingChangeAcknowledge):
                    //not interesting
                    break;
                case nameof(DynamicServiceAdditionResponse):
                    DynamicServiceAdditionResponse dynamicServiceAdditionResponse = (DynamicServiceAdditionResponse)mmm;
                    eventHandler.OnDynamicServiceAddition(dynamicServiceAdditionResponse.TransactionId, dynamicServiceAdditionResponse.ConfirmationCode, dynamicServiceAdditionResponse.TlvEncodedInformation);
                    break;
                case nameof(DynamicServiceDeletionResponse):
                    //nothing in there
                    break;
                case nameof(T45_V4_MultipartRegistrationResponse):
                    //doesn't look interesting either
                    break;
                default:
                    throw new NotImplementedException();
            }
        }

        private HashSet<PhysicalAddress> participants;
        private void AddParticipant(PhysicalAddress pa)
        {
            if (participants == null)
                participants = new HashSet<PhysicalAddress>();
            if (participants.Add(pa))
            {
                eventHandler.OnParticipantDetected(pa);
            }
        }

        public void Dispose()
        {
            pcapWriter?.Dispose();
        }

        public Statistics DocsisEnvironmentStatistics => _stats;
        private Statistics _stats;
        public class Statistics
        {
            public Statistics()
            {
                ParticipantStatistics = new List<ParticipantStatistic>();
            }

            public bool PacketPduPresent;
            public long PacketPdus { get; internal set; }
            public long PacketPdusPerSecond { get; internal set; }

            
            public bool MacSpecificHeadersPresent;
            public long MacSpecificHeaders { get; internal set; }
            public long MacSpecificHeadersPerSecond { get; internal set; }


            public bool IsolationPacketPduPresent;
            public long IsolationPacketPdus { get; internal set; }
            public long IsolationPacketPdusPerSecond { get; internal set; }


            public bool SpecialUseMacPresent;
            public long SpecialUseMacs { get; internal set; }
            public long SpecialUseMacsPerSecond { get; internal set; }

            public long Throughput { get; internal set; }
            public long ThroughputPerSecond { get; internal set; }
            public long ThroughputTotal { get; internal set; }

            public int ScrambledPackets { get; set; }
            public int ScrambledPacketsPerSecond { get; private set; }
            public long ScrambledPacketsTotal { get; private set; }

            public int UnscrambledPackets { get; set; }
            public int UnscrambledPacketsPerSecond { get; private set; }
            public long UnscrambledPacketsTotal { get; private set; }
            
            public class ParticipantStatistic
            {
                public PhysicalAddress Address { get; internal set; }
                public int Packets { get; set; }
                public int Throughput { get; set; }
                public IPAddress Ip { get; set; }

                public int PacketsPerSecond { get; internal set; }
                public int ThroughputPerSecond { get; internal set; }
                public long ThroughputTotal { get; set; }
            }

            public List<ParticipantStatistic> ParticipantStatistics { get; internal set; }

            public void CaptureSecond()
            {
                PacketPdusPerSecond = PacketPdus;
                PacketPdus = 0;
                MacSpecificHeadersPerSecond = MacSpecificHeaders;
                MacSpecificHeaders = 0;
                IsolationPacketPdusPerSecond = IsolationPacketPdus;
                IsolationPacketPdus = 0;
                SpecialUseMacsPerSecond = SpecialUseMacs;
                SpecialUseMacs = 0;
                ThroughputTotal += Throughput;
                ThroughputPerSecond = Throughput;
                Throughput = 0;
                ScrambledPacketsTotal += ScrambledPackets;
                ScrambledPacketsPerSecond = ScrambledPackets;
                ScrambledPackets = 0;
                UnscrambledPacketsTotal += UnscrambledPackets;
                UnscrambledPacketsPerSecond = UnscrambledPackets;
                UnscrambledPackets = 0;
                lock (ParticipantStatistics)
                {
                    foreach (ParticipantStatistic participantStatistic in ParticipantStatistics)
                    {
                        participantStatistic.ThroughputTotal += participantStatistic.Throughput;
                        participantStatistic.ThroughputPerSecond = participantStatistic.Throughput;
                        participantStatistic.Throughput = 0;
                        participantStatistic.PacketsPerSecond = participantStatistic.Packets;
                        participantStatistic.Packets = 0;
                    }
                }
            }
        }
        
    }
}
