using System;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Remoting.Messaging;

namespace Orbiter.Multiplayer
{
    public class STUNClient
    {
        private readonly Socket socket;
        public IPEndPoint EndPoint { get; set; }
        public IPEndPoint AlternateEndPoint { get; set; }

        public STUNClient()
        {
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
            {
                ReceiveTimeout = 3000,
                SendTimeout = 3000
            };
            socket.Bind(new IPEndPoint(IPAddress.Any, 0));
        }

        /// <summary>
        /// Gets NAT info from STUN server at EndPoint
        /// </summary>
        private STUNResponse Query()
        {
            // Test I
            OnProgress(new STUNResponse{ProgressMessage = "STUN-Test I"});
            var test1Response = DoTransaction(new STUNMessage{Type = STUNMessageType.BindingRequest});
    
            // UDP blocked.
            if(test1Response == null) return new STUNResponse{NetworkType = STUNNetworkType.Blocked};

            // Test II
            OnProgress(new STUNResponse { ProgressMessage = "STUN-Test II" });
            var same = socket.LocalEndPoint.Equals(test1Response.MappedAddress);
            if (DoTransaction(
                new STUNMessage
                    {
                        Type = STUNMessageType.BindingRequest,
                        ChangeRequest = new STUNChangeRequest {ChangeIP = true, ChangePort = true}
                    })!=null)
                return same?
                    new STUNResponse { NetworkType = STUNNetworkType.Open, PublicEndPoint = test1Response.MappedAddress } :
                    new STUNResponse{ NetworkType = STUNNetworkType.Cone, PublicEndPoint = test1Response.MappedAddress};

            if (same) // direct internet access
                return new STUNResponse { NetworkType = STUNNetworkType.Firewall, PublicEndPoint = test1Response.MappedAddress };
            
            // Test I(II)
            OnProgress(new STUNResponse { ProgressMessage = "STUN-Test Ia" });
            var test12Response = DoTransaction(new STUNMessage{Type = STUNMessageType.BindingRequest}, true);

            if (test12Response == null) return new STUNResponse { NetworkType = STUNNetworkType.Unknown};
            
            if(!test12Response.MappedAddress.Equals(test1Response.MappedAddress)) // symmetric NAT
                return new STUNResponse{NetworkType = STUNNetworkType.Symmetric, PublicEndPoint = test1Response.MappedAddress};

            // Test III
            OnProgress(new STUNResponse { ProgressMessage = "STUN-Test III" });
            return DoTransaction(
                new STUNMessage
                    {
                        Type = STUNMessageType.BindingRequest,
                        ChangeRequest = new STUNChangeRequest {ChangeIP = false, ChangePort = true}
                    }, true) != null ?
                new STUNResponse{NetworkType = STUNNetworkType.Restricted, PublicEndPoint = test1Response.MappedAddress} : // restricted NAT
                new STUNResponse{NetworkType = STUNNetworkType.PortRestricted, PublicEndPoint = test1Response.MappedAddress}; // port restricted NAT
        }

        private STUNMessage DoTransaction(STUNMessage request) { return DoTransaction(request, false); }
        private STUNMessage DoTransaction(STUNMessage request, bool useAlternate)
        {                        
            var pac = request.Serialize();                              
            var rc = 5;
            var rto = 555000;
            for (; rc>0; rc--, rto*=2)
            {
                try
                {
                    socket.SendTo(pac, useAlternate?AlternateEndPoint:EndPoint);
                    if (!socket.Poll(rto, SelectMode.SelectRead))
                    {
                        OnProgress(new STUNResponse { ProgressMessage = "Poll " + rc });
                        continue;
                    }
                    var buf = new byte[512];
                    var response = new STUNMessage();
                    socket.Receive(buf);
                    response.Deserialize(buf);
                    if (request.TransactionID.Equals(response.TransactionID)) return response;
                }
                catch
                {
                    rc = 0;
                }
            }
            return null;
        }

        /// <summary>
        /// Occurs together with STUN progress].
        /// </summary>
        public event STUNClientProgressDelegate Progress;

        private void OnProgress(STUNResponse response)
        {
            if (Progress != null)
                Progress(this, response);
        }

        private delegate STUNResponse AsyncQuery();

        public void Request()
        {
            new AsyncQuery(Query).BeginInvoke(ar => OnProgress(((AsyncQuery) ((AsyncResult) ar).AsyncDelegate).EndInvoke(ar)), null);
        }
    }
}
