using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using Microsoft.Win32;
using System.Configuration;
using TCMPortMapper.Helpers;
using Yaulw.Registry;
using Yaulw.File;

namespace TCMPortMapper
{
    /// <summary>
    /// Status of PortMapping object.
    /// </summary>
    public enum PortMappingStatus
    {
        Unmapped = 0,
        Trying = 1,
        Mapped = 2
    }

    /// <summary>
    /// Protocol used for PortMapping object.
    /// </summary>
    public enum PortMappingTransportProtocol
    {
        UDP = 1,
        TCP = 2,
        Both = 3
    }

    public class PortMapping
    {
        private UInt16 localPort;
        private UInt16 externalPort;
        private UInt16 desiredExternalPort;
        private PortMappingStatus mappingStatus;
        private PortMappingTransportProtocol transportProtocol;
        private string description;

        public PortMapping(UInt16 localPort, UInt16 desiredExternalPort, PortMappingTransportProtocol protocol, string description)
        {
            this.localPort = localPort;
            this.desiredExternalPort = desiredExternalPort;
            this.transportProtocol = protocol;
            this.description = description;

            this.mappingStatus = PortMappingStatus.Unmapped;
        }

        public UInt16 LocalPort
        {
            get { return localPort; }
        }

        public UInt16 DesiredExternalPort
        {
            get { return desiredExternalPort; }
        }

        public PortMappingTransportProtocol TransportProtocol
        {
            get { return transportProtocol; }
        }

        public UInt16 ExternalPort
        {
            get { return externalPort; }
        }

        public PortMappingStatus MappingStatus
        {
            get { return mappingStatus; }
        }

        public string Description
        {
            get { return description; }
        }

        internal void SetExternalPort(UInt16 port)
        {
            externalPort = port;
        }

        internal void SetMappingStatus(PortMappingStatus newMappingStatus)
        {
            if (mappingStatus != newMappingStatus)
            {
                mappingStatus = newMappingStatus;
                if (mappingStatus == PortMappingStatus.Unmapped)
                {
                    externalPort = 0;
                }
                PortMapper.SharedInstance.OnDidChangeMappingStatus(this);
            }
        }
    }

    public class PortMapper
    {
        /// <summary>
        /// Singleton instance of class
        /// </summary>
        private static PortMapper sharedInstance;

        /// <summary>
        /// Static constructor.
        /// - executes before any instance of the class is created.
        /// - executes before any of the static members for the class are referenced.
        /// - executes after the static field initializers (if any) for the class.
        /// - executes at most one time during a single program instantiation.
        /// - called automatically to initialize the class before the first instance is created or any static members are referenced.
        /// </summary>
        static PortMapper()
        {
            sharedInstance = new PortMapper();
        }

        /// <summary>
        /// Returns the sharedInstance of the PortMapper.
        /// This is the sole instance that is to be used throughout the library.
        /// </summary>
        public static PortMapper SharedInstance
        {
            get { return sharedInstance; }
        }

        public delegate void PMExternalIPAddressDidChange(PortMapper sender, IPAddress ip);
        public delegate void PMWillStartSearchForRouter(PortMapper sender);
        public delegate void PMDidFinishSearchForRouter(PortMapper sender);
        public delegate void PMDidStartWork(PortMapper sender);
        public delegate void PMDidFinishWork(PortMapper sender);
        public delegate void PMDidReceiveUPNPMappingTable(PortMapper sender, List<ExistingUPnPPortMapping> mappings);
        public delegate void PMDidChangeMappingStatus(PortMapper sender, PortMapping pm);
        public delegate void PMCanAddPortMapping(PortMapper sender);

        public event PMExternalIPAddressDidChange ExternalIPAddressDidChange;
        public event PMWillStartSearchForRouter WillStartSearchForRouter;
        public event PMDidFinishSearchForRouter DidFinishSearchForRouter;
        public event PMDidStartWork DidStartWork;
        public event PMDidFinishWork DidFinishWork;
        public event PMDidReceiveUPNPMappingTable DidReceiveUPNPMappingTable;
        public event PMDidChangeMappingStatus DidChangeMappingStatus;
        public event PMCanAddPortMapping CanAddPortMapping;

        private enum MappingProtocol
        {
            None = 0,
            NATPMP = 1,
            UPnP = 2
        }

        private enum MappingStatus
        {
            Failed = 0,
            Trying = 1,
            Works = 2
        }

        private NATPMPPortMapper natpmpPortMapper;
        private UPnPPortMapper upnpPortMapper;

        private List<PortMapping> portMappings;         // Active mappings, and mappings to add
        private List<PortMapping> portMappingsToRemove; // Active mappings that should be removed

        private List<ExistingUPnPPortMapping> existingUPnPPortMappingsToRemove;

        private bool isRunning;

        private MappingStatus natpmpStatus;
        private MappingStatus upnpStatus;
        private MappingProtocol mappingProtocol;

        private String routerManufacturer;
        private IPAddress routerIPAddress;
        private IPAddress localIPAddress;
        private IPAddress externalIPAddress;
        private IPAddress externalIPAddressFromService;
        private bool externalIPFromRouter;

        private bool localIPOnRouterSubnet;

        private int workCount;

        private Object multiThreadLock = new Object();
        private Object singleThreadLock = new Object();
        private Object workLock = new Object();

        private bool isGoingToSleep;
        private bool isNetworkAvailable;

        private bool requestedUPnPMappingTable;

        private string _externalPortToMap;
        private string _mappingDescription;

        private Logging Logger;

        private bool _isSafeForPortMapping;
        private UInt16 _currentExternalPort;

        private PortMapper()
        {
            natpmpPortMapper = new NATPMPPortMapper();
            upnpPortMapper = new UPnPPortMapper();

            portMappings = new List<PortMapping>();
            portMappingsToRemove = new List<PortMapping>();

            existingUPnPPortMappingsToRemove = new List<ExistingUPnPPortMapping>();
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #region Delegate Methods
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        protected virtual void OnExternalIPAddressDidChange()
        {
            if (ExternalIPAddressDidChange != null)
            {
                ExternalIPAddressDidChange(this, externalIPAddress);
                //Invoke(ExternalIPAddressDidChange, this, externalIPAddress);
            }
        }

        protected virtual void OnWillStartSearchForRouter()
        {
            if (WillStartSearchForRouter != null)
            {
                WillStartSearchForRouter(this);
                //Invoke(WillStartSearchForRouter, this);
            }
        }

        protected virtual void OnDidFinishSearchForRouter()
        {
            if (DidFinishSearchForRouter != null)
            {
                DidFinishSearchForRouter(this);
                //Invoke(DidFinishSearchForRouter, this);
            }
        }

        protected virtual void OnDidStartWork()
        {
            if (DidStartWork != null)
            {
                DidStartWork(this);
                //Invoke(DidStartWork, this);
            }
        }

        protected virtual void OnDidFinishWork()
        {
            if (DidFinishWork != null)
            {
                DidFinishWork(this);
                //Invoke(DidFinishWork, this);
            }
        }

        protected virtual void OnDidReceiveUPNPMappingTable(List<ExistingUPnPPortMapping> mappings)
        {
            if (DidReceiveUPNPMappingTable != null)
            {
                Invoke(DidReceiveUPNPMappingTable, this, mappings);
            }
        }

        internal virtual void OnDidChangeMappingStatus(PortMapping pm)
        {
            // It is vitally important that we Invoke this on a background thread!
            // The mapping status is generally changed within the context of a lock on the mappings list.
            // This lock is also used within public API methods,
            // so if we don't use a background thread, there's a potential for deadlock.
            Thread bgThread = new Thread(new ParameterizedThreadStart(OnDidChangeMappingStatusThread));
            bgThread.IsBackground = true;
            bgThread.Start(pm);
        }

        protected virtual void OnDidChangeMappingStatusThread(Object pm)
        {
            if (DidChangeMappingStatus != null)
            {
                DidChangeMappingStatus(this, (PortMapping)pm);
                //Invoke(DidChangeMappingStatus, this, (PortMapping)pm);
            }
        }

        protected virtual void OnCanAddPortMapping()
        {
            if (CanAddPortMapping != null)
            {
                CanAddPortMapping(this);
            }
        }

        private System.ComponentModel.ISynchronizeInvoke mSynchronizingObject = null;
        /// <summary>
        /// Set the <see cref="System.ComponentModel.ISynchronizeInvoke">ISynchronizeInvoke</see>
        /// object to use as the invoke object. When returning results from asynchronous calls,
        /// the Invoke method on this object will be called to pass the results back
        /// in a thread safe manner.
        /// </summary>
        /// <remarks>
        /// If using in conjunction with a form, it is highly recommended
        /// that you pass your main <see cref="System.Windows.Forms.Form">form</see> (window) in.
        /// </remarks>
        public System.ComponentModel.ISynchronizeInvoke SynchronizingObject
        {
            get { return mSynchronizingObject; }
            set { mSynchronizingObject = value; }
        }

        private bool mAllowApplicationForms = true;
        /// <summary>
        /// Allows the application to attempt to post async replies over the
        /// application "main loop" by using the message queue of the first available
        /// open form (window). This is retrieved through
        /// <see cref="System.Windows.Forms.Application.OpenForms">Application.OpenForms</see>.
        /// 
        /// Note: This is true by default.
        /// </summary>
        public bool AllowApplicationForms
        {
            get { return mAllowApplicationForms; }
            set { mAllowApplicationForms = value; }
        }

        private bool mAllowMultithreadedCallbacks = false;
        /// <summary>
        /// If set to true, <see cref="AllowApplicationForms">AllowApplicationForms</see>
        /// is set to false and <see cref="SynchronizingObject">SynchronizingObject</see> is set
        /// to null. Any time an asynchronous method needs to invoke a delegate method
        /// it will run the method in its own thread.
        /// </summary>
        /// <remarks>
        /// If set to true, you will have to handle any synchronization needed.
        /// If your application uses Windows.Forms or any other non-thread safe
        /// library, then you will have to do your own invoking.
        /// </remarks>
        public bool AllowMultithreadedCallbacks
        {
            get { return mAllowMultithreadedCallbacks; }
            set
            {
                mAllowMultithreadedCallbacks = value;
                if (mAllowMultithreadedCallbacks)
                {
                    mAllowApplicationForms = false;
                    mSynchronizingObject = null;
                }
            }
        }

        /// <summary>
        /// Helper method to obtain a proper invokeable object.
        /// If an invokeable object is set, it's immediately returned.
        /// Otherwise, an open windows form is returned if available.
        /// </summary>
        /// <returns>An invokeable object, or null if none available.</returns>
        private System.ComponentModel.ISynchronizeInvoke GetInvokeObject()
        {
            if (mSynchronizingObject != null) return mSynchronizingObject;

            if (mAllowApplicationForms)
            {
                // Need to post it over control thread
                System.Windows.Forms.FormCollection forms = System.Windows.Forms.Application.OpenForms;

                if (forms != null && forms.Count > 0)
                {
                    System.Windows.Forms.Control control = forms[0];
                    return control;
                }
            }
            return null;
        }

        /// <summary>
        /// Calls a method using the objects invokable object (if provided).
        /// Otherwise, it simply invokes the method normally.
        /// </summary>
        /// <param name="method">
        ///		The method to call.
        /// </param>
        /// <param name="args">
        ///		The arguments to call the method with.
        /// </param>
        /// <returns>
        ///		The result returned from method, or null if the method could not be invoked.
        ///	</returns>
        internal object Invoke(Delegate method, params object[] args)
        {
            System.ComponentModel.ISynchronizeInvoke invokeable = GetInvokeObject();

            try
            {
                if (invokeable != null)
                {
                    return invokeable.Invoke(method, args);
                }

                if (mAllowMultithreadedCallbacks)
                {
                    return method.DynamicInvoke(args);
                }
            }
            catch { }

            return null;
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #endregion
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #region Public Properties
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        public bool IsRunning
        {
            get { return isRunning; }
        }

        public IPAddress LocalIPAddress
        {
            get { return localIPAddress; }
        }

        public IPAddress ExternalIPAddress
        {
            get { return externalIPAddress; }
        }

        public bool ExternalIPFromRouter
        {
            get { return externalIPFromRouter; }
        }

        public IPAddress ExternalIPAddressFromService
        {
            get { return externalIPAddressFromService; }
        }

        public String RouterManufacturer
        {
            get { return routerManufacturer; }
        }

        public IPAddress RouterIPAddress
        {
            get { return routerIPAddress; }
        }

        public String MappingProtocolName
        {
            get
            {
                if (mappingProtocol == MappingProtocol.NATPMP)
                    return "NAT-PMP";
                else if (mappingProtocol == MappingProtocol.UPnP)
                    return "UPnP";
                else
                    return "None";
            }
        }

        public String DefaultLocalBonjourHostName
        {
            get { return System.Net.Dns.GetHostName() + ".local"; }
        }

        public string MappingDescription
        {
            get
            {
                return _mappingDescription;
            }
        }

        /// <summary>
        /// We are safe to attempt port mapping if the server we are currently on
        /// is on the router we are connected to
        /// </summary>
        public bool IsSafeForPortMapping
        {
            get
            {
                return _isSafeForPortMapping;
            }
        }

        public UInt16 CurrentExternalPort
        {
            get { return _currentExternalPort; }
            set
            {
                if (_currentExternalPort != value)
                {
                    _currentExternalPort = value;
                }
            }
        }

        public string ExternalPortToMap
        {
            get
            {
                return _externalPortToMap;
            }
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #endregion
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #region Internal Properties
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal List<PortMapping> PortMappings
        {
            get { return portMappings; }
        }

        internal List<PortMapping> PortMappingsToRemove
        {
            get { return portMappingsToRemove; }
        }

        internal List<ExistingUPnPPortMapping> ExistingUPnPPortMappingsToRemove
        {
            get { return existingUPnPPortMappingsToRemove; }
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #endregion
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #region Public Methods
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        public void Start(IPAddress externalIPFromService, string externalPortToMap, string portMapDescription, Logging Logger)
        {
            if (!isRunning)
            {
                // Initialize winsock
                Win32.WSAData d;
                Win32.WSAStartup(2, out d);

                AddSystemEventDelegates();
                AddNATPMPDelegates();
                AddUPnPDelegates();

                isRunning = true;
            }

            //Set up logger, external ip, & external port to map
            externalIPAddressFromService = externalIPFromService;
            _externalPortToMap = externalPortToMap;
            _mappingDescription = portMapDescription;
            this.Logger = Logger;

            // Start the Thread
            Refresh();
        }

        public void AddPortMapping()
        {
            Logger.Info("AddPortMapping() - Add port mapping is called with Local IP: {0} and Port: {1}", ExternalPortToMap, LocalIPAddress.ToString());
            ReleaseLog.WriteInformation("Getting ready to add port mapping.");
            try
            {
                //Add port mapping
                if (ExternalIPFromRouter && IsSafeForPortMapping)
                {
                    PortMapping pMapping = new PortMapping(Convert.ToUInt16(ExternalPortToMap), Convert.ToUInt16(ExternalPortToMap), PortMappingTransportProtocol.TCP, MappingDescription);
                    AddPortMapping(pMapping);
                    Logger.Info("AddPortMapping() - Added port mapping with Local IP: {0} and Port: {1}", ExternalPortToMap, LocalIPAddress.ToString());
                    ReleaseLog.WriteInformation("Added port mapping with Local IP: {0} and Port: {1}", ExternalPortToMap, LocalIPAddress.ToString());
                }
            }
            catch (Exception ex)
            {
                Logger.Error("AddPortMapping() - Adding port mapping failed.");
                ReleaseLog.WriteError("Adding port mapping failed.");
            }
        }

        /// <summary>
        /// Asynchronously adds the given port mapping.
        /// </summary>
        /// <param name="pm">
        ///		The port mapping to add.
        ///		Note: Many UPnP routers only support port mappings where localPort == externalPort.
        ///	</param>
        private void AddPortMapping(PortMapping pm)
        {
            if (pm == null) return;

            portMappings.Add(pm);
            if (isRunning) UpdatePortMappings();
        }

        /// <summary>
        /// Asynchronously removes the given port mapping.
        /// </summary>
        /// <param name="pm">
        ///		The port mapping to remove.
        /// </param>
        public void RemovePortMapping(PortMapping pm)
        {
            if (pm == null) return;

            portMappings.Remove(pm);
            if (pm.MappingStatus != PortMappingStatus.Unmapped)
            {
                portMappingsToRemove.Add(pm);
            }
            if (isRunning) UpdatePortMappings();
        }

        /// <summary>
        /// Asynchronously removes the given port mapping.
        /// 
        /// This method will also automatically refresh the UPnP mapping table,
        /// and call the DidReceiveUPNPMappingTable delegate.
        /// </summary>
        /// <param name="pm">
        ///		The port mapping to remove.
        ///	</param>
        public void RemovePortMapping(ExistingUPnPPortMapping pm)
        {
            if (pm == null) return;

            if (upnpStatus == MappingStatus.Works)
            {
                existingUPnPPortMappingsToRemove.Add(pm);

                if (isRunning)
                {
                    requestedUPnPMappingTable = true;
                    UpdatePortMappings();
                }
            }
        }

        /// <summary>
        /// Refreshes all port mapping information, and all port mappings.
        /// </summary>
        public void Refresh()
        {
            // All public API methods are wrapped in a single thread lock.
            // This frees users to invoke the public API from multiple threads, but provides us a bit of sanity.
            //lock (singleThreadLock)
            //{
            if (isRunning)
            {
                //Thread bgThread = new Thread(new ThreadStart(RefreshThread));
                //bgThread.Start();
                RefreshThread();
            }
            //}
        }

        public void RequestUPnPMappingTable()
        {
            // All public API methods are wrapped in a single thread lock.
            // This frees users to invoke the public API from multiple threads, but provides us a bit of sanity.
            //lock (singleThreadLock)
            //{
            if (isRunning)
            {
                if (upnpStatus == MappingStatus.Works)
                {
                    requestedUPnPMappingTable = true;
                    upnpPortMapper.UpdateExistingUPnPPortMappings();
                }
            }
            //}
        }

        /// <summary>
        /// Asynchronously stops the port mapper.
        /// All added port mappings will be removed.
        /// </summary>
        public void Stop()
        {
            ReleaseLog.WriteInformation("Port Mapper: Attempting to Stop Port Mapper.");
            if (isRunning)
            {
                RemoveSystemEventDelegates();
                RemoveNATPMPDelegates();
                RemoveUPnPDelegates();
                isRunning = false;

                if (natpmpStatus == MappingStatus.Works)
                {
                    natpmpPortMapper.Stop();
                }
                if (upnpStatus == MappingStatus.Works)
                {
                    upnpPortMapper.Stop();
                }

                Win32.WSACleanup();
                ReleaseLog.WriteInformation("Port Mapper: Stopped Port Mapper.");
            }
            ReleaseLog.WriteInformation("Port Mapper: Attempt to stop port mapper skipped since it's currently not running.");
        }

        /// <summary>
        /// Synchronously stops the port mapper.
        /// All added port mappings will be removed.
        /// </summary>
        public void StopBlocking()
        {
            if (isRunning)
            {
                RemoveSystemEventDelegates();
                RemoveNATPMPDelegates();
                RemoveUPnPDelegates();
                isRunning = false;

                if (natpmpStatus == MappingStatus.Works)
                {
                    natpmpPortMapper.StopBlocking();
                }
                if (upnpStatus == MappingStatus.Works)
                {
                    upnpPortMapper.StopBlocking();
                }

                Win32.WSACleanup();
            }
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #endregion
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #region Refresh Thread
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private void RefreshThread()
        {
            mappingProtocol = MappingProtocol.None;
            externalIPAddress = null;

            for (int i = 0; i < portMappings.Count; i++)
            {
                PortMapping pm = (PortMapping)portMappings[i];
                //if (pm.MappingStatus == PortMappingStatus.Mapped)
                //{
                //    pm.SetMappingStatus(PortMappingStatus.Unmapped);
                //}
            }

            OnWillStartSearchForRouter();

            DebugLog.WriteLine("RefreshThread");

            // Update all the following variables:
            // - routerIPAddress
            // - routerManufacturer
            // - localIPAddress
            // - localIPAddressOnSubnet
            // 
            // Note: The order in which we call the following methods matters.
            // We must update the routerIPAddress before the others.
            UpdateRouterIPAddress();
            UpdateRouterManufacturer();
            UpdateLocalIPAddress();

            DebugLog.WriteLine("routerIPAddress       : {0}", routerIPAddress);
            DebugLog.WriteLine("routerManufacturer    : {0}", routerManufacturer);
            DebugLog.WriteLine("localIPAddress        : {0}", localIPAddress);
            DebugLog.WriteLine("localIPAddressOnSubnet: {0}", localIPOnRouterSubnet);

            if (routerIPAddress != null)
            {
                if ((localIPAddress != null) && localIPOnRouterSubnet)
                {
                    Logger.Info("Port Mapper: Local IP found via router.");
                    externalIPAddress = null;

                    if (IsIPv4AddressInPrivateSubnet(routerIPAddress))
                    {
                        natpmpStatus = MappingStatus.Trying;
                        upnpStatus = MappingStatus.Trying;

                        natpmpPortMapper.Refresh();
                        upnpPortMapper.Refresh();
                    }
                    else
                    {
                        Logger.Info("Port Mapper: External IP found via router.");
                        natpmpStatus = MappingStatus.Failed;
                        upnpStatus = MappingStatus.Failed;
                        externalIPAddress = localIPAddress;
                        mappingProtocol = MappingProtocol.None;
                        externalIPFromRouter = true;

                        // Set all mappings to be mapped with their local port number being the external one
                        lock (portMappings)
                        {
                            for (int i = 0; i < portMappings.Count; i++)
                            {
                                PortMapping pm = (PortMapping)portMappings[i];
                                pm.SetExternalPort(pm.LocalPort);
                                pm.SetMappingStatus(PortMappingStatus.Mapped);
                            }
                        }

                        OnDidFinishSearchForRouter();
                    }
                }
                else
                {
                    //IPAddress.TryParse(ExternalIPAddressFromService, out externalIPAddress);
                    externalIPAddress = ExternalIPAddressFromService;
                    externalIPFromRouter = false;
                    OnDidFinishSearchForRouter();
                }
            }
            else
            {
                //IPAddress.TryParse(ExternalIPAddressFromService, out externalIPAddress);
                externalIPAddress = ExternalIPAddressFromService;
                externalIPFromRouter = false;
                OnDidFinishSearchForRouter();
            }
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #endregion
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #region Private API
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>
        /// Updates the routerIPAddress variable.
        /// </summary>
        private void UpdateRouterIPAddress()
        {
            routerIPAddress = null;

            NetworkInterface[] networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();

            bool found = false;
            for (int i = 0; i < networkInterfaces.Length && !found; i++)
            {
                NetworkInterface networkInterface = networkInterfaces[i];

                if (networkInterface.OperationalStatus == OperationalStatus.Up)
                {
                    GatewayIPAddressInformationCollection gateways;
                    gateways = networkInterface.GetIPProperties().GatewayAddresses;

                    for (int j = 0; j < gateways.Count && !found; j++)
                    {
                        GatewayIPAddressInformation gatewayInfo = gateways[j];

                        if (gatewayInfo.Address.AddressFamily == AddressFamily.InterNetwork)
                        {
                            if (!gatewayInfo.Address.Equals(IPAddress.Any))
                            {
                                routerIPAddress = gatewayInfo.Address;
                                found = true;
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Updates the routerManufacturer variable.
        /// This is done by getting the MAC address of the router,
        /// and then looking up the corresponding manufacturer in the OUI list.
        /// 
        /// The routerIPAddress variable should be set prior to calling this method.
        /// </summary>
        private void UpdateRouterManufacturer()
        {
            DebugLog.WriteLine("UpdateRouterManufacturer()");

            routerManufacturer = "Unknown";

            if (routerIPAddress == null)
            {
                return;
            }

            Exception e;

            PhysicalAddress routerMac = GetHardwareAddressForIPv4Address(routerIPAddress, out e);
            if (routerMac == null)
            {
                DebugLog.WriteLine("PortMapper: Error getting router mac address: {0}", e);
                return;
            }

            String result = GetManufacturerForHardwareAddress(routerMac, out e);
            if (result == null)
            {
                if (e == null)
                    DebugLog.WriteLine("PortMapper: Router MAC address not in OUI list");
                else
                    DebugLog.WriteLine("PortMapper: Error getting router manufacturer: {0}", e);
            }
            else
            {
                routerManufacturer = result;
            }
        }

        /// <summary>
        /// Updates the localIPAddress variable, and the related localIPOnRouterSubnet variable.
        /// 
        /// The routerIPAddress variable should be set prior to calling this method.
        /// </summary>
        private void UpdateLocalIPAddress()
        {
            localIPAddress = null;

            IPHostEntry localhost = Dns.GetHostEntry(Dns.GetHostName());
            foreach (IPAddress ip in localhost.AddressList)
            {
                if (ip.AddressFamily == AddressFamily.InterNetwork)
                {
                    localIPAddress = ip;
                    break;
                }
            }

            if (routerIPAddress != null)
            {
                localIPOnRouterSubnet = IsIPv4AddressInPrivateSubnet(localIPAddress);
            }
            else
            {
                localIPOnRouterSubnet = false;
            }
        }

        /// <summary>
        /// Registers for notifications of power and network events.
        /// </summary>
        private void AddSystemEventDelegates()
        {
            try
            {
                SystemEvents.PowerModeChanged += new PowerModeChangedEventHandler(SystemEvents_PowerModeChanged);
                NetworkChange.NetworkAddressChanged += new NetworkAddressChangedEventHandler(NetworkChange_NetworkAddressChanged);
                NetworkChange.NetworkAvailabilityChanged += new NetworkAvailabilityChangedEventHandler(NetworkChange_NetworkAvailabilityChanged);
            }
            catch (Exception e)
            {
                // I have no idea why this throws exceptions on some computers.
                // As a windows developer, it doesn't really surprise me though.
                DebugLog.WriteLine("PortMapper: AddSystemEventDelegates: {0}", e);
            }
        }

        /// <summary>
        /// Unregisters for notifications of power and network events.
        /// </summary>
        private void RemoveSystemEventDelegates()
        {
            try
            {
                SystemEvents.PowerModeChanged -= new PowerModeChangedEventHandler(SystemEvents_PowerModeChanged);
                NetworkChange.NetworkAddressChanged -= new NetworkAddressChangedEventHandler(NetworkChange_NetworkAddressChanged);
                NetworkChange.NetworkAvailabilityChanged -= new NetworkAvailabilityChangedEventHandler(NetworkChange_NetworkAvailabilityChanged);
            }
            catch (Exception e)
            {
                // I have no idea why this throws exceptions on some computers.
                // As a windows developer, it doesn't really surprise me though.
                DebugLog.WriteLine("PortMapper: RemoveSystemEventDelegates: {0}", e);
            }
        }

        private void AddNATPMPDelegates()
        {
            natpmpPortMapper.DidBeginWorking += new NATPMPPortMapper.PMDidBeginWorking(natpmpPortMapper_DidBeginWorking);
            natpmpPortMapper.DidEndWorking += new NATPMPPortMapper.PMDidEndWorking(natpmpPortMapper_DidEndWorking);
            natpmpPortMapper.DidGetExternalIPAddress += new NATPMPPortMapper.PMDidGetExternalIPAddress(natpmpPortMapper_DidGetExternalIPAddress);
            natpmpPortMapper.DidFail += new NATPMPPortMapper.PMDidFail(natpmpPortMapper_DidFail);
            natpmpPortMapper.DidReceiveBroadcastExternalIPChange += new NATPMPPortMapper.PMDidReceiveBroadcastExternalIPChange(natpmpPortMapper_DidReceiveBroadcastExternalIPChange);
        }

        private void RemoveNATPMPDelegates()
        {
            natpmpPortMapper.DidBeginWorking -= new NATPMPPortMapper.PMDidBeginWorking(natpmpPortMapper_DidBeginWorking);
            natpmpPortMapper.DidEndWorking -= new NATPMPPortMapper.PMDidEndWorking(natpmpPortMapper_DidEndWorking);
            natpmpPortMapper.DidGetExternalIPAddress -= new NATPMPPortMapper.PMDidGetExternalIPAddress(natpmpPortMapper_DidGetExternalIPAddress);
            natpmpPortMapper.DidFail -= new NATPMPPortMapper.PMDidFail(natpmpPortMapper_DidFail);
            natpmpPortMapper.DidReceiveBroadcastExternalIPChange -= new NATPMPPortMapper.PMDidReceiveBroadcastExternalIPChange(natpmpPortMapper_DidReceiveBroadcastExternalIPChange);
        }

        private void AddUPnPDelegates()
        {
            upnpPortMapper.DidBeginWorking += new UPnPPortMapper.PMDidBeginWorking(upnpPortMapper_DidBeginWorking);
            upnpPortMapper.DidEndWorking += new UPnPPortMapper.PMDidEndWorking(upnpPortMapper_DidEndWorking);
            upnpPortMapper.DidGetExternalIPAddress += new UPnPPortMapper.PMDidGetExternalIPAddress(upnpPortMapper_DidGetExternalIPAddress);
            upnpPortMapper.DidFail += new UPnPPortMapper.PMDidFail(upnpPortMapper_DidFail);
        }

        private void RemoveUPnPDelegates()
        {
            upnpPortMapper.DidBeginWorking -= new UPnPPortMapper.PMDidBeginWorking(upnpPortMapper_DidBeginWorking);
            upnpPortMapper.DidEndWorking -= new UPnPPortMapper.PMDidEndWorking(upnpPortMapper_DidEndWorking);
            upnpPortMapper.DidGetExternalIPAddress -= new UPnPPortMapper.PMDidGetExternalIPAddress(upnpPortMapper_DidGetExternalIPAddress);
            upnpPortMapper.DidFail -= new UPnPPortMapper.PMDidFail(upnpPortMapper_DidFail);
        }

        private void UpdatePortMappings()
        {
            // This method is called from either AddPortMapping or RemovePortMapping

            if (mappingProtocol == MappingProtocol.NATPMP)
            {
                natpmpPortMapper.UpdatePortMappings();
            }
            else if (mappingProtocol == MappingProtocol.UPnP)
            {
                upnpPortMapper.UpdatePortMappings();
            }
        }

        /// <summary>
        /// Called from:
        ///  - RefreshThread
        ///  - UpdateLocalIPAddress - RefreshThread
        ///  - GetRouterPhysicalAddress - GetRouterManufacturer - RefreshThread
        /// 
        ///  - RouterIPAddress property
        /// 
        ///  - natpmpPortMapper_DidReceiveBroadcastExternalIPChange
        /// </summary>
        /// <returns></returns>
        //		private IPAddress GetRouterIPAddress()
        //		{
        //			UInt32 routerAddr = 0;
        //			if (NATPMP.getdefaultgateway(ref routerAddr) < 0)
        //			{
        //				DebugLog.WriteLine("PortMapper: Unable to get router ip address");
        //				return null;
        //			}
        //			
        //			try
        //			{
        //				return new IPAddress((long)routerAddr);
        //			}
        //			catch (Exception e)
        //			{
        //				DebugLog.WriteLine("PortMapper: Unable to get router ip address: {0}", e);
        //				return null;
        //			}
        //		}

        private void IncreaseWorkCount()
        {
                if (workCount == 0)
                {
                    OnDidStartWork();
                }
                workCount++;
        }

        private void DecreaseWorkCount()
        {
                workCount--;
                if (workCount == 0)
                {
                    if (upnpStatus == MappingStatus.Works && requestedUPnPMappingTable)
                    {
                        OnDidReceiveUPNPMappingTable(upnpPortMapper.ExistingUPnPPortMappings);
                        requestedUPnPMappingTable = false;
                    }

                    OnDidFinishWork();
                }
        }

        private void DecreaseWorkCount(Object state)
        {
            // Called via timer (on a background thread)
            DecreaseWorkCount();
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #endregion
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #region NATPMPPortMapper Delegate Methods
        /////////////////////////////////////////////////////////////////////////////////////////////////////////////// 

        private void natpmpPortMapper_DidBeginWorking(NATPMPPortMapper sender)
        {
            IncreaseWorkCount();
        }

        private void natpmpPortMapper_DidEndWorking(NATPMPPortMapper sender)
        {
            DecreaseWorkCount();
        }

        private void natpmpPortMapper_DidGetExternalIPAddress(NATPMPPortMapper sender, System.Net.IPAddress ip)
        {
            bool shouldNotify = false;

            if (natpmpStatus == MappingStatus.Trying)
            {
                natpmpStatus = MappingStatus.Works;
                mappingProtocol = MappingProtocol.NATPMP;
                shouldNotify = true;
            }

            externalIPAddress = ip;
            OnExternalIPAddressDidChange();

            //Using Hard external ip (from service) address - this is the external ip address of the actual server
            Logger.Info("Check to see if it's safe to proceed with port mapping. Router External IP: {0}, Server External IP: {1}", externalIPAddress.ToString(), ExternalIPAddressFromService.ToString());
            if (!String.IsNullOrEmpty(ExternalIPAddressFromService.ToString()))
            {
                _isSafeForPortMapping = (externalIPAddress.ToString() == ExternalIPAddressFromService.ToString() ? true : false);
                Logger.Info("Safe to proceed with port mapping.");
            }
            else
            {
                //If external ip is incorrect or can't be retrieved, then we assume port mapping is not safe.
                Logger.Info("Unsafe to proceed with port mapping.");
                _isSafeForPortMapping = false;
            }

            if (shouldNotify)
            {
                OnDidFinishSearchForRouter();
            }

            //OnCanAddPortMapping();
        }

        private void natpmpPortMapper_DidFail(NATPMPPortMapper sender)
        {
            DebugLog.WriteLine("natpmpPortMapper_DidFail");

            if (natpmpStatus == MappingStatus.Trying)
            {
                natpmpStatus = MappingStatus.Failed;
            }
            else if (natpmpStatus == MappingStatus.Works)
            {
                externalIPAddress = null;
            }

            if (upnpStatus == MappingStatus.Failed)
            {
                OnDidFinishSearchForRouter();
            }
        }

        private void natpmpPortMapper_DidReceiveBroadcastExternalIPChange(NATPMPPortMapper sender, IPAddress ip, IPAddress senderIP)
        {
            if (isRunning)
            {
                DebugLog.WriteLine("natpmpPortMapper_DidReceiveBroadcastExternalIPChange");

                if (senderIP == localIPAddress)
                {
                    DebugLog.WriteLine("Refreshing because of NAT-PMP device external IP broadcast");
                    Refresh();
                }
                else
                {
                    DebugLog.WriteLine("Got information from rogue NAT-PMP device");
                }
            }
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #endregion
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #region UPnPPortMapper Delegate Methods
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private void upnpPortMapper_DidBeginWorking(UPnPPortMapper sender)
        {
            IncreaseWorkCount();
        }

        private void upnpPortMapper_DidEndWorking(UPnPPortMapper sender)
        {
            DecreaseWorkCount();
        }

        private void upnpPortMapper_DidGetExternalIPAddress(UPnPPortMapper sender, IPAddress ip, bool isFromRouter)
        {
            DebugLog.WriteLine("upnpPortMapper_DidGetExternalIPAddress: {0}", ip);

            bool shouldNotify = false;

            if (upnpStatus == MappingStatus.Trying)
            {
                upnpStatus = MappingStatus.Works;
                mappingProtocol = MappingProtocol.UPnP;
                shouldNotify = true;
            }

            externalIPAddress = ip;
            externalIPFromRouter = isFromRouter;
            OnExternalIPAddressDidChange();

            //Using Hard external ip (from service) address - this is the external ip address of the actual server
            Logger.Info("Check to see if it's safe to proceed with port mapping. Router External IP: {0}, Server External IP: {1}", externalIPAddress.ToString(), ExternalIPAddressFromService.ToString());
            if (!String.IsNullOrEmpty(ExternalIPAddressFromService.ToString()))
            {
                _isSafeForPortMapping = (externalIPAddress.ToString() == ExternalIPAddressFromService.ToString() ? true : false);
                Logger.Info("Safe to proceed with port mapping.");
            }
            else
            {
                //If external ip is incorrect or can't be retrieved, then we assume port mapping is not safe.
                Logger.Info("Unsafe to proceed with port mapping.");
                _isSafeForPortMapping = false;
            }

            if (upnpStatus == MappingStatus.Works)
            {
                //Get Existing Mappings
                RequestUPnPMappingTable();
            }

            if (shouldNotify)
            {
                OnDidFinishSearchForRouter();
            }

            //OnCanAddPortMapping();
        }

        private void upnpPortMapper_DidFail(UPnPPortMapper sender)
        {
            DebugLog.WriteLine("upnpPortMapper_DidFail");

            if (upnpStatus == MappingStatus.Trying)
            {
                upnpStatus = MappingStatus.Failed;
            }
            else if (upnpStatus == MappingStatus.Works)
            {
                externalIPAddress = null;
            }

            if (natpmpStatus == MappingStatus.Failed)
            {
                OnDidFinishSearchForRouter();
            }
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #endregion
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #region SystemEvent Delegate Methods
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
        {
            if (e.Mode == PowerModes.Suspend)
            {
                // System is going to sleep
                isGoingToSleep = true;

                if (isRunning)
                {
                    if (natpmpStatus == MappingStatus.Works)
                    {
                        natpmpPortMapper.StopBlocking();
                    }
                    if (upnpStatus == MappingStatus.Works)
                    {
                        upnpPortMapper.StopBlocking();
                    }
                }
            }
            else if (e.Mode == PowerModes.Resume)
            {
                // System is waking up (but may not have an internet connection yet)
                // Wait for network information to refresh
                isGoingToSleep = false;
            }
        }

        private void NetworkChange_NetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
        {
            isNetworkAvailable = e.IsAvailable;
        }

        private void NetworkChange_NetworkAddressChanged(object sender, EventArgs e)
        {
            if (!isGoingToSleep)
            {
                if (isNetworkAvailable)
                {
                    DebugLog.WriteLine("Refreshing because of network change");
                    Refresh();
                }
                else
                {
                    // Ignore - System does not yet have network restored
                }
            }
            else
            {
                // Ignore - System is going to sleep
            }
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #endregion
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #region Utility Methods
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private bool IsIPv4AddressInPrivateSubnet(IPAddress myAddr)
        {
            if (myAddr == null) return false;

            // Private subnets as defined in http://tools.ietf.org/html/rfc1918
            // Loopback address 127.0.0.1/8 http://tools.ietf.org/html/rfc3330
            // Zeroconf/bonjour self assigned addresses 169.254.0.0/16 http://tools.ietf.org/html/rfc3927

            String[] netAddrs = { "192.168.0.0", "10.0.0.0", "172.16.0.0", "127.0.0.1", "169.254.0.0" };
            String[] netMasks = { "255.255.0.0", "255.0.0.0", "255.240.0.0", "255.0.0.0", "255.255.0.0" };

            UInt32 myIP = BitConverter.ToUInt32(myAddr.GetAddressBytes(), 0);

            for (int i = 0; i < netMasks.Length; i++)
            {
                IPAddress netAddr = IPAddress.Parse(netAddrs[i]);
                UInt32 netIP = BitConverter.ToUInt32(netAddr.GetAddressBytes(), 0);

                IPAddress maskAddr = IPAddress.Parse(netMasks[i]);
                UInt32 maskIP = BitConverter.ToUInt32(maskAddr.GetAddressBytes(), 0);

                if ((myIP & maskIP) == (netIP & maskIP))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Only called from GetRouterPhysicalAddress()
        /// </summary>
        /// <param name="ip"></param>
        /// <param name="e"></param>
        /// <returns></returns>
        private PhysicalAddress GetHardwareAddressForIPv4Address(IPAddress ip, out Exception e)
        {
            if (ip == null)
            {
                e = new ArgumentNullException();
                return null;
            }

            if (ip.AddressFamily != AddressFamily.InterNetwork)
            {
                e = new ArgumentException("Only supports IPv4 addresses");
                return null;
            }

            UInt32 dstAddrInt = BitConverter.ToUInt32(ip.GetAddressBytes(), 0);
            UInt32 srcAddrInt = 0;

            byte[] mac = new byte[6]; // 48 bit
            int length = mac.Length;
            int reply = Win32.SendARP(dstAddrInt, srcAddrInt, mac, ref length);

            if (reply != 0)
            {
                e = new System.ComponentModel.Win32Exception(reply);
                return null;
            }

            e = null;
            return new PhysicalAddress(mac);
        }

        /// <summary>
        /// Searches for a match for the given mac address in the oui.txt file.
        /// If a match is found, the corresponding company name is returned.
        /// If a match is not found, null is returned.
        /// If an error occurs, null is returned, and the exception is set.
        /// 
        /// Note: The oui list may contain missing names, so an empty string may be returned.
        /// </summary>
        private String GetManufacturerForHardwareAddress(PhysicalAddress macAddr, out Exception e)
        {
            if (macAddr == null)
            {
                e = new ArgumentNullException();
                return null;
            }

            String macAddrPrefix = macAddr.ToString().Substring(0, 6);

            StreamReader streamReader = null;
            String result = null;

            try
            {
                // OUI - Organizationally Unique Identifier
                // 
                // If you wish to update the list of OUI's, you can get the latest version here:
                // http://standards.ieee.org/regauth/oui/index.shtml
                // Then format the list using the ReformatOUI method below.
                // Ensure that the oui file is in UTF-8.

                //streamReader = File.OpenText("oui.txt");
                streamReader = File.OpenText("ouiformatted.txt");
                String line = streamReader.ReadLine();

                while ((line != null) && (result == null))
                {
                    if (line.StartsWith(macAddrPrefix))
                    {
                        result = line.Substring(6).Trim();
                    }
                    line = streamReader.ReadLine();
                }
            }
            catch (Exception ex)
            {
                e = ex;
            }
            finally
            {
                if (streamReader != null) streamReader.Close();
            }

            e = null;
            return result;
        }

        /// <summary>
        /// This method is not used in the framework, but may be used by developers to properly
        /// format the most recently obtained list of OUI's.
        /// The list can be obtained from IEEE:
        /// http://standards.ieee.org/regauth/oui/index.shtml
        /// </summary>
        /// <param name="inFilePath">
        ///		Path to downloaded oui text file.
        ///		This file should be encoded in UTF-8 format.
        /// </param>
        /// <param name="outFilePath">
        ///		Path to output formatted out file.
        ///		This file will be created or overwritten.
        ///	</param>
        private void ReformatOUI(String inFilePath, String outFilePath)
        {
            StreamReader streamReader = File.OpenText(inFilePath);
            String line = streamReader.ReadLine();

            StreamWriter streamWriter = File.CreateText(outFilePath);

            uint lineCount = 0;
            uint badLineCount = 0;

            while (line != null)
            {
                if (line.Contains("(base 16)"))
                {
                    String[] separators = { "(base 16)" };
                    String[] tokens = line.Split(separators, StringSplitOptions.RemoveEmptyEntries);

                    if (tokens.Length == 2)
                    {
                        String mac = tokens[0].Trim();
                        String company = tokens[1].Trim();

                        String outLine = mac + " " + company;

                        lineCount++;
                        streamWriter.WriteLine(outLine);
                    }
                    else
                    {
                        badLineCount++;
                    }
                }

                line = streamReader.ReadLine();
            }

            streamReader.Close();
            streamWriter.Close();

            Console.WriteLine("Number of lines: {0}", lineCount);
            Console.WriteLine("Number of bad lines: {0}", badLineCount);
        }

        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
        #endregion
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
    }
}
