using System;
using System.Collections.Generic;
using System.Text;
using System.Net;

namespace Orbiter.Multiplayer
{
    public class STUNChangeRequest
    {
        public STUNChangeRequest()
        {
            ChangePort = true;
            ChangeIP = true;
        }
        public bool ChangeIP { get; set; }
        public bool ChangePort { get; set; }
    }

    public enum STUNMessageType
    {
        BindingRequest = 0x0001,
        BindingResponse = 0x0101,
        BindingErrorResponse = 0x0111,
        SharedSecretRequest = 0x0002,
        SharedSecretResponse = 0x0102,
        SharedSecretErrorResponse = 0x0112,
    }

    public class STUNMessage
    {
        private enum Attribute
        {
            MappedAddress    = 0x0001,
            ResponseAddress  = 0x0002,
            ChangeRequest    = 0x0003,
            SourceAddress    = 0x0004,
            ChangedAddress   = 0x0005,
            Username         = 0x0006,
            Password         = 0x0007,
            MessageIntegrity = 0x0008,
            ErrorCode        = 0x0009,
            UnknownAttribute = 0x000A,
            ReflectedFrom    = 0x000B,
            ServerName       = 0x8022,
        }

        private enum IPFamily
        {
            IPv4 = 0x01,
            IPv6 = 0x02,
        }

        public STUNMessage()
        {
            byte[] bytes = Guid.NewGuid().ToByteArray();
            bytes[0] = 0x21;
            bytes[1] = 0x12;
            bytes[2] = 0xA4;
            bytes[3] = 0x42;
            TransactionID = new Guid(bytes);
            Type = STUNMessageType.BindingRequest;
        }

        public void Deserialize(byte[] data)
        {
            var i = 0;
            var messageType = (data[i++] << 8 | data[i++]);
            var messageLength = (data[i++] << 8 | data[i++])+20;

            try{Type = (STUNMessageType) messageType;}
            catch{throw new InvalidCastException("Invalid STUN message type "+ messageType +"!");}

            var guid = new byte[16];
            Array.Copy(data,i,guid,0,16);
            TransactionID = new Guid(guid);
            i += 16;

            while(i < messageLength)
            {
                var type = (Attribute)(data[i++] << 8 | data[i++]);
                var length = (data[i++] << 8 | data[i++]);
                var bytes=new byte[length];
                Array.Copy(data, i, bytes, 0, length);
                switch (type)
                {
                    case Attribute.MappedAddress: MappedAddress = (IPEndPoint) DeserializeAttribute(type, bytes); break;
                    case Attribute.ResponseAddress: ResponseAddress = (IPEndPoint)DeserializeAttribute(type, bytes); break;
                    case Attribute.SourceAddress: SourceAddress = (IPEndPoint)DeserializeAttribute(type, bytes); break;
                    case Attribute.ChangedAddress: ChangedAddress = (IPEndPoint)DeserializeAttribute(type, bytes); break;
                    case Attribute.ReflectedFrom: ReflectedFrom = (IPEndPoint)DeserializeAttribute(type, bytes); break;
                    case Attribute.Username: User = (string) DeserializeAttribute(type, bytes); break;
                    case Attribute.Password: Password = (string)DeserializeAttribute(type, bytes); break;
                    case Attribute.ServerName: Server = (string)DeserializeAttribute(type, bytes); break;
                    case Attribute.ChangeRequest: ChangeRequest = (STUNChangeRequest) DeserializeAttribute(type, bytes); break;
                    case Attribute.ErrorCode: Error = (STUNErrorCode) DeserializeAttribute(type, bytes); break;
                }
                i += length;
            }
        }

        public byte[] Serialize()
        {
            var buf = new List<byte> { (byte)((int)Type >> 8), (byte)((int)Type & 0xFF), 0, 0};
            buf.AddRange(TransactionID.ToByteArray());
            buf.AddRange(SerializeAttribute(Attribute.MappedAddress, MappedAddress));
            buf.AddRange(SerializeAttribute(Attribute.ResponseAddress, ResponseAddress));
            buf.AddRange(SerializeAttribute(Attribute.ChangeRequest, ChangeRequest));
            buf.AddRange(SerializeAttribute(Attribute.SourceAddress, SourceAddress));
            buf.AddRange(SerializeAttribute(Attribute.ChangedAddress, ChangedAddress));
            buf.AddRange(SerializeAttribute(Attribute.Username, User));
            buf.AddRange(SerializeAttribute(Attribute.Password, Password));
            buf.AddRange(SerializeAttribute(Attribute.ErrorCode, Error));
            buf.AddRange(SerializeAttribute(Attribute.ReflectedFrom, ReflectedFrom));

            buf[2] = (byte)((buf.Count - 20) >> 8);
            buf[3] = (byte)((buf.Count - 20) & 0xFF);
       
            return buf.ToArray();
        }

        private static object DeserializeAttribute(Attribute type, byte[] data)
        {
            switch (type)
            {
                case Attribute.MappedAddress:
                case Attribute.ResponseAddress:
                case Attribute.SourceAddress:
                case Attribute.ChangedAddress:
                case Attribute.ReflectedFrom: return DeserializeEndPoint(data);
                case Attribute.Username:
                case Attribute.Password:
                case Attribute.ServerName: return Encoding.Default.GetString(data);
                case Attribute.ChangeRequest:
                    return new STUNChangeRequest
                               {
                                   ChangeIP = (data[3] & 4) != 0, ChangePort = (data[3] & 2) != 0
                               };
                case Attribute.ErrorCode:
                    return new STUNErrorCode
                                {
                                    Code = (data[2] & 0x7)*100 + (data[3] & 0xFF),
                                    ReasonText = Encoding.Default.GetString(data, 4, data.Length - 4)
                                };
            }
            return null;
        }

        private static IPEndPoint DeserializeEndPoint(byte[] data)
        {
            var port = (data[2] << 8 | data[3]);
            var ip = new byte[4];
            Array.Copy(data, 4, ip, 0, 4);
            return new IPEndPoint(new IPAddress(ip),port);
        }

        private static IEnumerable<byte> SerializeAttribute(Attribute type, object value)
        {
            if (value==null) return new byte[0];
            var buf=new List<byte> {(byte) ((int) type >> 8), (byte) ((int) type & 0xFF)};
            byte[] bytes;
            switch(type)
            {
                case Attribute.MappedAddress:
                case Attribute.ResponseAddress:
                case Attribute.SourceAddress:
                case Attribute.ChangedAddress:
                case Attribute.ReflectedFrom:
                    buf.AddRange(new byte[]{0, 8});
                    buf.AddRange(SerializeEndPoint((IPEndPoint) value));
                    break;
                case Attribute.ChangeRequest:
                    var creq = (STUNChangeRequest)value;
                    buf.AddRange(new byte[]{0, 4, 0, 0, 0,(byte) ((creq.ChangeIP ? 4 : 0) | (creq.ChangePort ? 2 : 0))});
                    break;
                case Attribute.Username:
                case Attribute.Password:
                    bytes = Encoding.ASCII.GetBytes((string)value);
                    buf.AddRange(new byte[] { (byte)(bytes.Length >> 8), (byte)(bytes.Length & 0xFF) });
                    buf.AddRange(bytes);
                    break;
                case Attribute.ErrorCode:
                    var err = (STUNErrorCode) value;
                    bytes = Encoding.ASCII.GetBytes(err.ReasonText);
                    buf.AddRange(new byte[]
                                     {
                                         0, (byte) bytes.Length, 0, 0,
                                         (byte) Math.Floor((double) (err.Code/100)),
                                         (byte) (err.Code & 0xFF)
                                     });
                    buf.AddRange(bytes);
                    break;
            }
            return buf.ToArray();
        }

        private static IEnumerable<byte> SerializeEndPoint(IPEndPoint endPoint)
        {
            var bytes = endPoint.Address.GetAddressBytes();
            return new byte[]
                       {
                           0, 8, 0, (byte) IPFamily.IPv4, (byte) (endPoint.Port >> 8), (byte) (endPoint.Port & 0xFF),
                           bytes[0], bytes[1], bytes[2], bytes[3]
                       };
        }

        public STUNMessageType Type { get; set; }
        public Guid TransactionID { get; private set; }
        public IPEndPoint MappedAddress { get; set; }
        public IPEndPoint ResponseAddress { get; set; }
        public STUNChangeRequest ChangeRequest { get; set; }
        public IPEndPoint SourceAddress { get; set; }
        public IPEndPoint ChangedAddress { get; set; }
        public string User { get; set; }
        public string Password { get; set; }
        public STUNErrorCode Error { get; set; }
        public IPEndPoint ReflectedFrom { get; set; }
        public string Server { get; set; }
    }
}
