using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.IO;
using System.Drawing;
using System.Text.RegularExpressions;

namespace EduNetworkBuilder
{
    //This is a network packet that can either contain data, or another packet
    [Serializable]
    public class Packet
    {
        public PacketType MyType = PacketType.none;
        public PacketStatus _MyStatus = PacketStatus.processing; //new packets start by being processed by the source device
        public PacketStatus MyStatus
        {
            get { return _MyStatus; }
            set { 
                _MyStatus = value;
                if(_MyStatus == PacketStatus.finished_ok)
                {
                    Tracking.Finished = true;
                }
                if (_MyStatus == PacketStatus.finished_failed || _MyStatus == PacketStatus.finished_ok)
                {
                    string status = _MyStatus.ToString();
                    PacketDump(" " + status, DebugPausePoint.packet_kill);
                }
                if (_MyStatus == PacketStatus.finished_ok)
                    Tracking.Finished = true;
            }
        }
        public int packetID;
        public int VLANID = NB.UntaggedVLAN; //starts on the management vlan
        public int TTL = 20;
        public int OrigTTL = 20; //The original TTL.  We need to know what we started with so we can pass it back on a traceroute
        public int TickTTL = 50;
        public int health = 100;
        public NB_IPAddress sourceIP;
        public NB_IPAddress OriginalDestIP;
        public NB_IPAddress TsourceIP; //This is the local link source ip.  It should go along with the local MAC address
        public NB_IPAddress destIP;
        public string sourceMAC;
        public string destMAC;
        public Packet payloadPacket = null;
        public string payloadData = "";  //Contains the mac-address, or ip-address string, or something else
        public string EncryptionString = "";
        public NB_IPAddress payloadIP = null;
        public PacketMessage Tracking = new PacketMessage();
        public NetworkComponent WhereAmI = null;
        public NetworkLink LastNetworkLink = null;
        public nb_direction myDirection = nb_direction.none;
        public int myLinkPercent = 0; //How far along the path are we.  0%, 50%, 100%.  For moving a dot along the link line
        public bool packet_good = true;
        DateTime StartTime = DateTime.Now;
        public NetworkCard OutboundNic = null;
        public NetworkCard InboundNic = null;
        public NetworkInterface InboundInterface = null;
        public NetworkInterface OutboundIF = null;
        public NB_IPAddress OutboundIP = new NB_IPAddress(NB.ZeroIPString);
        public string OutboundMAC = "";
        public string OutboundDestMAC = "";
        public bool isFresh = false;  //Set to be true if the packet is new.  Set false as soon as it is processed
        public bool DebugOn = false;
        public TraversalClass TraversalInformation = new TraversalClass();

        public  bool ready_to_delete
        {
            get {
                Tracking.duration = DateTime.Now - StartTime;

                if (MyStatus == PacketStatus.finished_failed)
                {
                    packet_good = false;
                    return true;
                }
                if (MyStatus == PacketStatus.finished_ok)
                {
                    packet_good = true; //It should already be good, but we state it just in case
                    return true;
                }
                if (MyStatus == PacketStatus.finished)
                {
                    packet_good = true; //It should already be good, but we state it just in case
                    return true;
                }

                return false;
            }
        }
        public Packet(Packet copyfrom)
        {
            MyType = copyfrom.MyType;
            TTL = copyfrom.TTL;
            OrigTTL = copyfrom.OrigTTL;
            sourceIP = copyfrom.sourceIP;
            TsourceIP = copyfrom.TsourceIP;
            destIP = copyfrom.destIP;
            sourceMAC = copyfrom.sourceMAC;
            destMAC = copyfrom.destMAC;
            payloadData = copyfrom.payloadData;
            payloadPacket = copyfrom.payloadPacket;
            EncryptionString = copyfrom.EncryptionString;
            payloadIP = copyfrom.payloadIP;
            Tracking = copyfrom.Tracking;
            WhereAmI = copyfrom.WhereAmI;
            health = copyfrom.health;
            StartTime = copyfrom.StartTime;
            OriginalDestIP = copyfrom.OriginalDestIP;
            packetID = copyfrom.packetID;
            VLANID = copyfrom.VLANID;
            Tracking.AddMessage(DebugLevel.debug, WhereAmI, NB.Translate("P_PacketDuplicated"));
            TraversalInformation = copyfrom.TraversalInformation.Clone();
        }

        //Generate a packet with the given payload.
        public Packet(NetworkComponent start, string source, string dest, string payload, PacketType theType, int NewPacketID=-1, int startTTL = -1)
        {
            if (NewPacketID == -1)
            {
                packetID = NB.nextPacketID();
            }
            else
            {
                packetID = NewPacketID;
            }
            if (startTTL != -1) this.TTL = startTTL;
            OrigTTL = TTL;
            WhereAmI = start;
            payloadData = payload;
            MyType = theType;
            if(theType == PacketType.arp_answer || theType == PacketType.arp_request || 
                theType == PacketType.dhcp_request || theType == PacketType.dhcp_answer)
            {
                sourceMAC = source;
                destMAC = dest;
                OutboundDestMAC = dest;
                Tracking.AddMessage(DebugLevel.info, start, theType.ToString() + string.Format(NB.Translate("Packet Created")));
                Tracking.AddMessage(DebugLevel.routing, start, string.Format("  MAC: {0} -> {1}", source, dest));
            }
            else
            {
                sourceIP = new NB_IPAddress(source);
                TsourceIP = new NB_IPAddress(source);
                destIP = new NB_IPAddress(dest);
                if (destIP.BroadcastAddress == destIP.GetIP)
                {
                    destMAC = NB.BroadcastMACString;
                    OutboundDestMAC = destMAC;
                }
                Tracking.AddMessage(DebugLevel.info, start, NB.Translate("Packet Created"));
                Tracking.AddMessage(DebugLevel.routing, start, string.Format("  IP: {0} -> {1}", source, dest));
                Tracking.AddMessage(DebugLevel.debug, start, string.Format(NB.Translate("  IPs Translated {0} -> {1}"),sourceIP.GetIP.ToIpString(), destIP.GetIP.ToIpString()));
            }
            isFresh = true;
        }

        public Packet(NetworkComponent start, NB_IPAddress dest, string payload, PacketType theType, int NewPacketID = -1, int startTTL =-1)
        {
            WhereAmI = start;
            payloadData = payload;
            MyType = theType;
            if (NewPacketID == -1)
            {
                packetID = NB.nextPacketID();
            }
            else
            {
                packetID = NewPacketID;
            }

            if (startTTL != -1) this.TTL = startTTL;
            OrigTTL = TTL;

            if (theType != PacketType.arp_answer && theType != PacketType.arp_request)
            {
                sourceIP = new NB_IPAddress(NB.ZeroIPString);
                destIP = dest;
                if (destIP != null && destIP.BroadcastAddress == destIP.GetIP)
                    destMAC = NB.BroadcastMACString;
                Tracking.AddMessage(DebugLevel.info, start, NB.Translate("Packet Created"));
                Tracking.AddMessage(DebugLevel.routing, start, string.Format("  IP:{0} -> {1}", sourceIP.GetIP.ToIpString(), dest.GetIP.ToIpString()));
            }
            else
            {
                sourceIP = new NB_IPAddress(NB.ZeroIPString);
                destMAC = NB.BroadcastMACString;
                destIP = dest;
                Tracking.AddMessage(DebugLevel.info, start, NB.Translate("Packet Created"));
                //Tracking.AddMessage(DebugLevel.routing, start, "  IP:" + sourceIP.GetIP.ToIpString() + " -> " + dest.GetIP.ToIpString());
            }
            isFresh = true;
        }

        public Point PacketLocation()
        {
            if (NB.GetComponentType(WhereAmI) != GeneralComponentType.link)
                return new Point(-1,-1);
            NetworkLink Nl = (NetworkLink)WhereAmI;
            Point mylocation = Nl.PositionOnLine(myDirection, myLinkPercent);
            return mylocation;
        }

        public Rectangle PacketRectangle()
        {
            Point start = PacketLocation();
            int half = NB.PacketPixelSize / 2;
            return new Rectangle(start.X - half, start.Y - half, NB.PacketPixelSize, NB.PacketPixelSize);
        }

        public void Print(Image BaseImage)
        {
            //we draw a packet on the map
            Point thePoint = PacketLocation();
            if (thePoint.X == -1 || thePoint.Y == -1) return; //It is an invalid location
            Color pencolor = Color.Blue;
            switch (MyType)
            {
                case PacketType.arp_answer:
                case PacketType.arp_request:
                    pencolor = Color.Green;
                    break;
                case PacketType.dhcp_answer:
                case PacketType.dhcp_request:
                    pencolor = Color.Red;
                    break;
                case PacketType.ping_answer:
                case PacketType.ping_request:
                    pencolor = Color.Blue;
                    break;
                case PacketType.tracert_reply:
                case PacketType.tracert_request:
                    pencolor = Color.LightBlue;
                    break;
                case PacketType.tun_packet:
                    pencolor = Color.White;
                    break;
                case PacketType.vpn_packet:
                    pencolor = Color.Orange;
                    break;
                case PacketType.bad_packet:
                    pencolor = Color.Black; //bad packets look bad
                    break;
            }
            Network theNet = NB.GetNetwork();
            if (theNet == null)
            {
                Pen myPen = new Pen(pencolor, NB.PacketPixelSize);
                Graphics.FromImage(BaseImage).DrawEllipse(myPen, PacketRectangle());
            }
            else
            {
                Color VLANPacketColor = theNet.ColorFromPacketVLAN(VLANID);
                //if (VLANPacketColor != Color.Blue)
                //    pencolor = VLANPacketColor;
                Image tImage = theNet.GetPacketImage(pencolor, VLANPacketColor);
                //if (VLANPacketColor != Color.Blue && VLANPacketColor != Color.Empty)
                //{
                //    tImage = theNet.ColoredImage(tImage, VLANPacketColor);
                //}
                Graphics.FromImage(BaseImage).DrawImage(tImage, PacketRectangle());                
            }
        }

        /// <summary>
        /// See if we need to do anything for a packet.  If so, do one step in processing it
        /// </summary>
        public void ProcessTick()
        {
            TickTTL--;
            switch(MyStatus)
            {
                case PacketStatus.processing:
                    //The packet is "inside" the device.
                    //  Here we masquerade
                    //  Here we route
                    //  Here the packet is just about to exit.  Maybe we need to set the "source IP"
                    if (NB.PacketVersionNum != 2)
                    {
                        DoProcessing();
                    }
                    else
                    {
                        DoMainProcessing();
                    }
                    break;
                case PacketStatus.finished_ok:
                case PacketStatus.finished_failed:
                    break; //we do not have anything to do.
                case PacketStatus.input: //Just entering a device.
                case PacketStatus.moving: //Traversing a link
                    DoMoving();
                    break;
                case PacketStatus.output: //Just leaving a device
                    break;
                case PacketStatus.waiting_for_arp: //We need to see if the arp information has shown up.  If so, we resume
                    if (NB.GetComponentType(WhereAmI) == GeneralComponentType.device)
                    {
                        NetworkDevice ndWhere = (NetworkDevice)WhereAmI;
                        NB_IPAddress dest = ndWhere.DestinationFromIP(destIP);
                        string dMAC = ndWhere.ArpFromIP(dest.GetIP.ToIpString());
                        if (dMAC != "" || destMAC == NB.BroadcastMACString)
                        {
                            MyStatus = PacketStatus.processing;
                        }
                        else
                        {
                            Network MyNet = NB.GetNetwork();
                            if(MyNet.CountPackets(PacketType.arp_answer) + MyNet.CountPackets(PacketType.arp_request) == 0)
                            {
                                //No more arps going.  We do not have an answer!
                                Tracking.AddMessage(DebugLevel.info, WhereAmI, NB.Translate("P_ProcessTick") + ": "+ dest.GetIP.ToIpString());
                                Tracking.Status = NB.LeftPad(WhereAmI.hostname) + NB.Translate("P_ProcessTick");
                                MyStatus = PacketStatus.finished_failed;
                            }
                            return;
                        }
                    }
                    break;
            }
        }

        private void DoMainProcessing()
        {
            //We pass it to the device
            if (WhereAmI == null)
            {
                AddMessage(DebugLevel.debug, NB.Translate("P_PacketLostLong"));
                Tracking.Status = NB.Translate("P_PacketLostShort");
                MyStatus = PacketStatus.finished_failed;
                return; //We cannot process the packet
            }
            WhereAmI.ProcessPacket(this);//The device knows how to do the processing
        }

        private void DoProcessing()
        {
            //The packet is "inside" the device.
            //  Here we masquerade
            //  Here we route
            //  Here the packet is just about to exit.  Maybe we need to set the "source IP"
            
            //We need to find out which interface we are leaving.
            //Then we figure out what rules we need to follow from there
            //So, we pass the packet to the device to process
            if (WhereAmI == null)
            {
                AddMessage(DebugLevel.debug, NB.Translate("P_PacketLostLong"));
                Tracking.Status = NB.Translate("P_PacketLostShort");
                MyStatus = PacketStatus.finished_failed;
                return; //We cannot process the packet
            }
            WhereAmI.DoProcessing(this);//The device knows how to do the processing
        }

        private void DoMoving()
        {
            if (WhereAmI == null)
            {
                AddMessage(DebugLevel.debug, NB.Translate("P_PacketLostLong"));
                Tracking.Status = NB.Translate("P_PacketLostShort");
                MyStatus = PacketStatus.finished_failed;
                return; //We cannot process the packet
            }
            WhereAmI.DoMoving(this);//The device knows how to do the processing
        }

        public void ReplaceMessage(PacketMessage Tracker)
        {
            Tracking = Tracker; //We do this if we are making a new packet, but tracking it with the old one.
            //We do this for arp requests.
        }

        public void StartOnLink(NetworkLink theLink, NetworkDevice start_device)
        {
            if (theLink == null) return;
            //Console.WriteLine("Starting on link: " + theLink.GetUniqueIdentifier + "  vlanID = " + VLANID.ToString());

            //We are just about to go out.  Verify we are not getting blocked by the firewall.
            if (start_device.FirewallRules.Count > 0)
            {
                if (InboundInterface != null && OutboundIF != null)
                {
                    if (!start_device.FirewallAllows(InboundInterface.nic_name, OutboundIF.nic_name))
                    {
                        ResponseToPacket rtp = start_device.HowToRespondToPacket(this);
                        if (rtp != ResponseToPacket.accept)
                        {
                            //If we are here, the packet is rejected.
                            string message = string.Format(NB.Translate("P_FirewallDropped"), start_device.hostname);
                            AddMessage(DebugLevel.filtering, message);
                            Tracking.Status = message;
                            Tracking.AddMessage(DebugLevel.info, start_device, message);
                            AddMessage(DebugLevel.info, message);
                            MyStatus = PacketStatus.finished_ok;
                            return;
                        }
                    }
                }
            }
            InboundNic = null;
            InboundInterface = null;
            WhereAmI = theLink;
            LastNetworkLink = theLink;
            MyStatus = PacketStatus.moving;
            myLinkPercent = 0;
            TickTTL = 200; //We keep resetting this as we go.
            OutboundIF = null;
            OutboundIP = null;
            OutboundMAC = "";
            OutboundNic = null;
            if (theLink.IsSource(start_device.GetUniqueIdentifier))
                myDirection = nb_direction.to_dst;
            else
                myDirection = nb_direction.to_src;
            //Console.WriteLine(NB.Translate("P_StartingOnLink") + string.Format("  SMAC: {0}  DMAC: {1}",sourceMAC,destMAC));
            AddMessage(DebugLevel.debug, "  " + NB.Translate("P_StartingOnLink"));
            if(theLink.theLinkType == LinkType.wireless)
                TraversalInformation.AddPath(theLink.hostname, TraversalTechnology.wireless,""); //We just went to wireless
            else
                TraversalInformation.AddPath(theLink.hostname, TraversalTechnology.ethernet,""); //We just went to ethernet

        }

        public void StartOnDevice(NetworkDevice theDevice)
        {
            WhereAmI = theDevice;
            MyStatus = PacketStatus.processing;
            myLinkPercent = 0;
            TickTTL = 200;
        }

        public void PrepareToDelete()
        {
            Network myNet = NB.GetNetwork();
            if(myNet != null)
            {
                if (payloadPacket != null)
                {
                    payloadPacket.PrepareToDelete();
                    payloadPacket.Tracking.AddMessage(DebugLevel.info, WhereAmI, Tracking.Status);
                    payloadPacket.Tracking.Status = Tracking.Status;
                    payloadPacket.MyStatus = MyStatus;
                }
                if(MyType == PacketType.arp_request && MyStatus == PacketStatus.finished_failed)
                {
                    Tracking.AddMessage(DebugLevel.info, NB.Translate("P_Packet"), 
                        NB.Translate("P_PacketFailedToReach") + ": " + destIP.GetIPString);
                    Tracking.Status = NB.Translate("P_PacketFailedToReach2") + ": "+ destIP.GetIPString;
                    MyStatus = PacketStatus.finished_failed;
                }
                if (MyStatus != PacketStatus.finished && MyType != PacketType.bad_packet)
                    myNet.AddMessage(Tracking); //We only store finished_failed, and finished_ok
                //if (sourceIP != null && destIP != null)
                //{
                //    if (MyType == PacketType.ping_answer && MyStatus == PacketStatus.finished_failed)
                //    {
                //        myNet.NoteActionDone(NetTestType.FailedPing, destIP.GetIPString, sourceIP.GetIPString);
                //    }
                //    if (MyType == PacketType.ping_request && MyStatus == PacketStatus.finished_failed)
                //    {
                //        myNet.NoteActionDone(NetTestType.FailedPing, sourceIP.GetIPString, destIP.GetIPString);
                //        if(WhereAmI != null)
                //            Console.WriteLine("Dropping at: "+ WhereAmI.hostname);
                //    }
                //}
            }
        }

        public void AddMessage(DebugLevel tLevel, string tMessage)
        {
            Tracking.AddMessage(tLevel, WhereAmI, tMessage);
        }

        public void IncrementDistance()
        {
            myLinkPercent += NB.LinkStep;
        }

        public bool Arrived()
        {
            if (myLinkPercent >= 100)
                return true;
            return false;
        }

        public bool isFinshed()
        {
            if (_MyStatus == PacketStatus.finished_ok) return true;
            if (_MyStatus == PacketStatus.finished_failed) return true;
            if (_MyStatus == PacketStatus.finished_ok) return true;
            return false;
        }

        private bool debugIsSet(DebugPausePoint what)
        {
            DebugPausePoint SetPoint = NB.GetDebugPauseSetting();
            if ((SetPoint & what) == what)
                return true;
            return false;
        }
        public void PacketDump(string hostname, DebugPausePoint WhereWeAre)
        {
            DebugPausePoint WhatIsSet = NB.GetDebugPauseSetting();
            string position = WhereWeAre.ToString();
            string sIP = NB.ZeroIPString;
            string dIP = NB.ZeroIPString;
            if (sourceIP != null) sIP = sourceIP.GetIPString;
            if (destIP != null) dIP = destIP.GetIPString;
            position = Regex.Replace(position, "packet_", "");
            if ((WhereWeAre & WhatIsSet) != WhereWeAre) 
                return; //We are not set to debug here. 
            if ((WhatIsSet & DebugPausePoint.pause) == DebugPausePoint.pause)
            {
                Console.WriteLine("Pausing");
            }
            if ((WhatIsSet & DebugPausePoint.dump) == DebugPausePoint.dump)
            {
                Console.WriteLine(string.Format("{0}: {1}  PACKET: dstIP: {2} destMAC: {3}", hostname, position, dIP, destMAC));
                Console.WriteLine(string.Format("{0}: {1}  PACKET: srcIP: {2} srcMAC: {3}", hostname, position, sIP, sourceMAC));
                Console.WriteLine("---");
            }
            AddMessage(DebugLevel.packet, string.Format("{0}: {1}  PACKET: dstIP: {2} destMAC: {3}", hostname, position, dIP, destMAC));
            AddMessage(DebugLevel.packet, string.Format("{0}: {1}  PACKET: srcIP: {2} srcMAC: {3}", hostname,position,sIP,sourceMAC));
        }

        public static T Clone<T>(T source)
        {
            if (!typeof(T).IsSerializable)
            {
                throw new ArgumentException(NB.Translate("NC_CloneSerialzable"), NB.Translate("NC_source"));
            }

            // Don't serialize a null object, simply return the default for that object
            if (Object.ReferenceEquals(source, null))
            {
                return default(T);
            }

            IFormatter formatter = new BinaryFormatter();
            Stream stream = new MemoryStream();
            using (stream)
            {
                formatter.Serialize(stream, source);
                stream.Seek(0, SeekOrigin.Begin);
                return (T)formatter.Deserialize(stream);
            }
        }
    }
}
