using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using log4net;

namespace Orbiter.Multiplayer
{
    public class User
    {
        private class Job
        {
            public Job(PacketType type, uint[] ids)
            {
                Type = type;
                Ids = ids;
            }

            public PacketType Type { get; private set; }

            public uint[] Ids { get; private set; }
        }

        private readonly int receiverPort;
        private Thread receiver;
        private Thread sender;
        private readonly string password;
        private double timeStamp;
        private Socket socket;
        private readonly Server server;
        
        private readonly Dictionary<uint, Vessel> localsById;
        private readonly Dictionary<uint, double> lockedById;
        private bool receiverActive;
        private bool senderActive;
        private readonly ManualResetEvent sending;
        private readonly Queue<Job> jobs;
        private static readonly ILog log = LogManager.GetLogger(" UDP");
        private bool trackToggle;
        private readonly Queue<uint> tracks;
        private Job ptpJob;
        private Host host;
        private static readonly Type StateInfoPacketType = typeof(StateInfoPacket);
        private static readonly Type GroupInfoPacketType = typeof(GroupInfoPacket);
        private static readonly Type KeepAlivePacketType = typeof(KeepAlivePacket);
        private static readonly Type PingPacketType = typeof(PingPacket);
        private static readonly int StateInfoPacketLength = Marshal.SizeOf(StateInfoPacketType);
        private static readonly int GroupInfoPacketLength = Marshal.SizeOf(GroupInfoPacketType);
        private static readonly int KeepAlivePacketLength = Marshal.SizeOf(KeepAlivePacketType);
        private static readonly int PingPacketLength = Marshal.SizeOf(PingPacketType);

        /// <summary>
        /// Gets the receiver port.
        /// </summary>
        /// <value>The receiver port.</value>
        public int ReceiverPort
        {
            get { return receiverPort; }
        }

        /// <summary>
        /// Gets or sets the nick.
        /// </summary>
        /// <value>The nick.</value>
        public string Nick { get; set; }

        /// <summary>
        /// Gets or sets the name.
        /// </summary>
        /// <value>The name.</value>
        public string Name { get; private set; }

        /// <summary>
        /// Gets or sets the host.
        /// </summary>
        /// <value>The host.</value>
        public Host Host
        {
            get { return host; }
            set
            {
                if (value == null && host != null) Stop();
                host = value;
            }
        }

        /// <summary>
        /// Gets or sets the send to port.
        /// </summary>
        /// <value>The send to port.</value>
        public int SendToPort { get; set; }

        /// <summary>
        /// Gets or sets the received-from port.
        /// </summary>
        /// <value>The received-from port.</value>
        public int ReceivedFromPort { get; set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="User"/> class.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="receiverPort">The receiver port.</param>
        /// <param name="password">The password.</param>
        /// <param name="server">The server.</param>
        public User(string name, int receiverPort, string password, Server server)
        {
            Nick = name;
            Name = name;
            this.receiverPort = receiverPort;
            this.password = password;
            this.server = server;
            localsById = new Dictionary<uint, Vessel>();
            lockedById = new Dictionary<uint, double>();
            jobs = new Queue<Job>();
            tracks = new Queue<uint>();
            sending = new ManualResetEvent(false);
            Time=server.Time;
            LocalTime=new Time(server, this, new StateInfoPacket{MJD = Time.Now, Sent = server.NTP.Now.ToDouble});
        }

        public Vessel GetVessel(uint localId)
        {
            lock(localsById) return !localsById.ContainsKey(localId) ? null : localsById[localId];
        }

        /// <summary>
        /// Starts this instance.
        /// </summary>
        public void Start()
        {
            lock (this)
            {
                if (receiverActive) return;
                if (senderActive) return;
                sending.Reset();
                socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
                {
                    Blocking = true
                };
                socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                socket.Bind(new IPEndPoint(IPAddress.Any, receiverPort));
                receiverActive = true;
                receiver = new Thread(ReceiverThread)
                {
                   IsBackground = true,
                   Priority = ThreadPriority.Normal
                };
                receiver.Start();
                senderActive = true;
                sender = new Thread(SenderThread)
                {
                    IsBackground = true,
                    Priority = ThreadPriority.Normal
                };
                sender.Start();
            }
        }

        /// <summary>
        /// Stops this instance.
        /// </summary>
        public void Stop()
        {
            if (Thread.CurrentThread == receiver) return;
            lock (this)
            {
                if (!receiverActive) return;
                if (!senderActive) return;
                receiverActive = false;
                socket.Close();
                senderActive = false;
                sending.Set();
            }
            receiver.Abort();
            receiver.Join();
            sender.Abort();
            sender.Join();
        }

        /// <summary>
        /// Determines whether [is valid password] [the specified password].
        /// </summary>
        /// <param name="password">The password.</param>
        /// <returns>
        /// 	<c>true</c> if [is valid password] [the specified password]; otherwise, <c>false</c>.
        /// </returns>
        public bool IsValidPassword(string password)
        {
            return password == this.password;
        }

        [StructLayout(LayoutKind.Explicit)]
        private struct IdConverter
        {
            [FieldOffset(0)]
            public double Double;
            [FieldOffset(0)]
            public uint UInt0;
            [FieldOffset(4)]
            public uint UInt1;
        }

        private void ReceiverThread()
        {
            try
            {
                var buffer = new byte[1024];
                EndPoint endPoint=new IPEndPoint(IPAddress.Any, 0);
                while (receiverActive)
                {
                    var bytes = socket.ReceiveFrom(buffer, ref endPoint);
                    if (bytes < 1)
                    {
                        receiverActive = false;
                        break;
                    }
                    if (log.IsDebugEnabled)
                    {
                        var x = new StringBuilder(bytes);
                        x.Append("Rcvd from ");
                        x.Append(((IPEndPoint) endPoint).Address);
                        x.Append(':');
                        x.Append(((IPEndPoint) endPoint).Port);
                        x.Append(' ');
                        for (int i = 0; i < bytes; i++) x.Append(buffer[i].ToString("X2") + " ");
                        log.Debug(x);
                    }
                    var type = (PacketType) buffer[0] & PacketType.Mask;
                    switch(type)
                    {
                        case PacketType.KeepAlive:
                        case PacketType.StateInfo:
                            if (NAT) SendToPort = ((IPEndPoint)endPoint).Port;
                            else ReceivedFromPort = ((IPEndPoint)endPoint).Port;
                            sending.Set();
                            break;
                    }
                    switch(type)
                    {
                        case PacketType.KeepAlive:
                            //TODO: check MJD or space-time bubble?
                            break;
                        case PacketType.StateInfo:
                            {
                                var converter = Marshal.AllocHGlobal(StateInfoPacketLength);
                                Marshal.Copy(buffer, 0, converter, StateInfoPacketLength);
                                var pac = (StateInfoPacket) Marshal.PtrToStructure(converter, StateInfoPacketType);
                                Marshal.FreeHGlobal(converter);
                                if (timeStamp < pac.Sent)
                                {
                                    LocalTime.Update(this, pac);
                                    timeStamp = pac.Sent;
                                    Vessel vessel = null;
                                    var removed = (pac.Flags & 0x40) > 0; //Bit6 in flags indicates delete
                                    lock (localsById)
                                    {
                                        //Really dumb garbage collector, use something else to iterate without blocking the thread
                                        var deletables = new List<uint>();
                                        foreach (var pair in lockedById)
                                        {
                                            if (pair.Value + server.Configuration.Timing.GarbageCollector < timeStamp) deletables.Add(pair.Key);
                                            else if (pair.Key == pac.Id) removed = true;
                                        }
                                        foreach (var v in deletables) lockedById.Remove(v);
                                        deletables = new List<uint>();
                                        foreach (var pair in localsById)
                                        {
                                            if (pair.Key == pac.Id)
                                            {
                                                if (removed)
                                                {
                                                    deletables.Add(pair.Key);
                                                    server.Map.Remove(pair.Value);
                                                }
                                                else (vessel = pair.Value).Update(pac);
                                            }
                                            else if (pair.Value.State.Sent + server.Configuration.Timing.GarbageCollector < timeStamp)
                                            {
                                                deletables.Add(pair.Key);
                                                server.Map.Remove(pair.Value);
                                            }
                                        }
                                        foreach (var v in deletables)
                                        {
                                            localsById.Remove(v);
                                            lockedById[v] = timeStamp;
                                        }
                                    }
                                    if (vessel == null && !removed)
                                    {
                                        vessel = new Vessel(server, this);
                                        switch (server.Map.Add(vessel))
                                        {
                                            case AddObjectResult.OK:
                                                vessel.Update(pac);
                                                lock (localsById) localsById[pac.Id] = vessel;
                                                break;
                                            case AddObjectResult.Collision:
                                                log.Error(receiverPort + "< Local vessel " + pac.Id +
                                                          " already registered. Database inconsistencies!");
                                                break;
                                            case AddObjectResult.TooMuch:
                                                log.Error(receiverPort + "< Local vessel " + pac.Id +
                                                          " can't be registered. No more global IDs available!");
                                                break;
                                        }
                                    }
                                }
                                else
                                {
                                    log.Error(receiverPort + "< Packet " + (timeStamp - pac.Sent) + "s too late");
                                }
                            }
                            break;
                        case PacketType.Audio:
                            //TODO: Broadcast hyperspace frequencies immediately to all tuned-in users (at least with one vessel)
                            //      Put relativistic frequencies into queue for later transmissions.
                            break;
                        case PacketType.GroupInfo:
                        case PacketType.EventInfo:
                        case PacketType.Video:
                            log.Error("Packet type '"+Enum.GetName(typeof(PacketType), buffer[0])+"' is invalid!");
                            break;
                        case PacketType.Ping:
                            {
                                var converter = Marshal.AllocHGlobal(PingPacketLength);
                                Marshal.Copy(buffer, 0, converter, PingPacketLength);
                                var pac = (PingPacket) Marshal.PtrToStructure(converter, PingPacketType);
                                Marshal.FreeHGlobal(converter);
                                if ((pac.Flags & 0x40) > 0) //delete ping flag set
                                {
                                    lock (localsById)
                                    {
                                        //Again a dumb garbage collector, use something else to iterate without blocking the thread
                                        var deletables = new List<uint>();
                                        foreach (var pair in localsById)
                                        {
                                            if (pair.Key == pac.Id)
                                            {
                                                deletables.Add(pair.Key);
                                                server.Map.Remove(pair.Value);
                                            }
                                            else if (pair.Value.State.Sent + server.Configuration.Timing.GarbageCollector < timeStamp)
                                            {
                                                deletables.Add(pair.Key);
                                                server.Map.Remove(pair.Value);
                                            }
                                        }
                                        foreach (var v in deletables)
                                        {
                                            localsById.Remove(v);
                                            lockedById[v] = timeStamp;
                                        }
                                    }
                                }
                                else if ((pac.Flags & 0x80) > 0) //PTP flag set
                                {
                                    var toUInt = new IdConverter {Double = server.NTP.Now.ToDouble};
                                    //Since .NET 2, assign operations are atomic even in the Thread.Abort use-case, therefore locks are not
                                    //used here. Even if a race-condition occurs that mangles struct content while enqueuing the job, it doesn't
                                    //matter much, because it would in the worst case produce a garbage ping result, leading to wrong measurements
                                    //in the NTP algorithm. The later one is fault tolerant, anyway, i.e.: not using measurements outside of the
                                    //95% percentile of the clock offset distribution.
                                    ptpJob = new Job(PacketType.Ping, new[] {pac.Id, pac.Id2, pac.Id3, toUInt.UInt0, toUInt.UInt1});
                                }
                            }
                            break;
                    }
                }
            }
            catch (ThreadAbortException){}
            catch (SocketException ex){log.Debug(ex.Message);}
            finally
            {
                socket.Close();
            }
        }

        private void SenderThread()
        {
            var rawDatas = Marshal.AllocHGlobal(StateInfoPacketLength);
            try
            {
                sending.WaitOne();
                var buffer=new byte[StateInfoPacketLength];

                while (senderActive)
                {
                    
                    var bytes = 0;
                    var host = Host;
                    if (host != null)
                    {
                        var invalid = false;
                        Job job;
                        do if ((job = GetJob()) != null)
                        {
                            invalid = false;
                            switch (job.Type)
                            {
                                case PacketType.KeepAlive:
                                    var ka = new KeepAlivePacket();
                                    if (job.Ids.Length > 0)
                                    {
                                        lock (localsById)
                                        {
                                            if (!localsById.ContainsKey((UInt16) job.Ids[0]))
                                            {
                                                invalid = true;
                                                continue;
                                            }
                                        }
                                        lock (server.Map.GlobalsById)
                                        {
                                            if (!server.Map.GlobalsById.ContainsKey((UInt16) job.Ids[1]))
                                            {
                                                invalid = true;
                                                continue;
                                            }
                                        }
                                        ka.IsGlobalIdForLocalId = 1;
                                        ka.LocalId = (UInt16) job.Ids[0];
                                        ka.GlobalId = (UInt16) job.Ids[1];
                                    }
                                    else
                                    {
                                        ka.IsGlobalIdForLocalId = 0;
                                        ka.LocalId = 0;
                                        ka.GlobalId = 0;
                                    }
                                    ka.Flags = (byte) job.Type;
                                    ka.Sent = server.NTP.Now.ToDouble;
                                    ka.MJD = Time.Now;
                                    Marshal.StructureToPtr(ka, rawDatas, false);
                                    Marshal.Copy(rawDatas, buffer, 0, KeepAlivePacketLength);
                                    bytes = KeepAlivePacketLength;
                                    break;
                                case PacketType.GroupInfo:
                                    var gi = new GroupInfoPacket
                                    {
                                        Source = (UInt16) job.Ids[0],
                                        Ip = job.Ids[1],
                                        Port = (UInt16) job.Ids[2],
                                        Flags = (byte) ((byte) job.Type | 0x40), //TODO: Keep observe flag for client compatibility, i.e. make old clients work with new server. Remove on next compatibility break!
                                        Sent = server.NTP.Now.ToDouble,
                                        MJD = Time.Now
                                    };
                                    Marshal.StructureToPtr(gi, rawDatas, false);
                                    Marshal.Copy(rawDatas, buffer, 0, GroupInfoPacketLength);
                                    bytes = GroupInfoPacketLength;
                                    break;
                                case PacketType.StateInfo:
                                    Vessel vessel = null;
                                    lock (server.Map.GlobalsById)
                                    {
                                        if (server.Map.GlobalsById.ContainsKey((UInt16) job.Ids[0]))
                                            vessel = server.Map.GlobalsById[(UInt16) job.Ids[0]] as Vessel;
                                        if (vessel == null)
                                        {
                                            invalid = true;
                                            continue;
                                        }
                                    }
                                    var si = vessel.State;
                                    si.Flags = (byte) (si.Flags | 0x40); //Set track flag
                                    si.Id = vessel.GlobalId;
                                    Marshal.StructureToPtr(si, rawDatas, false);
                                    Marshal.Copy(rawDatas, buffer, 0, StateInfoPacketLength);
                                    bytes = StateInfoPacketLength;
                                    break;
                                case PacketType.EventInfo:
                                case PacketType.Audio:
                                case PacketType.Video:
                                case PacketType.Ping:
                                    var ping = new PingPacket
                                    {
                                       Flags = (byte) job.Type,
                                       Id = (UInt16) job.Ids[0],
                                       Sent = server.NTP.Now.ToDouble
                                    };
                                    if (job.Ids.Length > 1)
                                    {
                                        ping.Flags |= 0x80; //Set PTP flag
                                        ping.Id2 = (UInt16) job.Ids[1];
                                        ping.Id3 = (UInt16) job.Ids[2];
                                        var converter = new IdConverter {UInt0 = job.Ids[3], UInt1 = job.Ids[4]};
                                        ping.Sent = converter.Double;
                                    }
                                    Marshal.StructureToPtr(ping, rawDatas, false);
                                    Marshal.Copy(rawDatas, buffer, 0, PingPacketLength);
                                    bytes = PingPacketLength;
                                    break;
                            }
                        } while (invalid);
                        
                        if (job == null) sending.Reset();

                        if (bytes > 0)
                        {
                            if (log.IsDebugEnabled)
                            {
                                var x = new StringBuilder(bytes);
                                x.Append("Send ");
                                x.Append(host.HairPinningIP);
                                x.Append(':');
                                x.Append(SendToPort);
                                x.Append(' ');
                                for (var i = 0; i < bytes; i++) x.Append(buffer[i].ToString("X2") + " ");
                                log.Debug(x);
                            }
                            bytes = socket.SendTo(buffer, bytes, SocketFlags.None, new IPEndPoint(host.HairPinningIP, SendToPort));
                            if (bytes < 1)
                            {
                                sending.Reset();
                                break;
                            }
                        }
                    }
                    Thread.Sleep(server.Configuration.Timing.Transmitter);
                    sending.WaitOne();
                }
            }
            catch (ThreadAbortException){}
            catch (SocketException ex) { log.Debug(ex.Message); }
            catch (ObjectDisposedException ex) { log.Debug(ex.Message); }
            finally
            {
                Marshal.FreeHGlobal(rawDatas);
                socket.Close();
            }
        }

        private Job GetJob()
        {
            if (jobs.Count == 0)
            {
                if (ptpJob != null)
                {
                    var result = ptpJob;
                    //Note: no lock here, because even if we have a race-condition that overrides the currently produced ptpJob, it won't
                    //matter much. The worst case is to miss one beat, which gets compensated by the next KA, anyway.
                    ptpJob = null;
                    return result;
                }
                try
                {
                    if (Host.Ping>0) return new Job(PacketType.Ping, new uint[]{Host.Ping});
                }
                catch (NullReferenceException){}
                
                lock (localsById)
                {
                    if (localsById.Count == 0) return new Job(PacketType.KeepAlive, new uint[] {});
                    foreach (var pair in localsById)
                    {
                        if (!TrackToggle) jobs.Enqueue(new Job(PacketType.KeepAlive, new [] { pair.Key, pair.Value.GlobalId }));
                        else
                        {
                            var trackId = GetTrack();
                            if (trackId > 0) jobs.Enqueue(new Job(PacketType.StateInfo, new [] { trackId }));
                        }
                        var userHost = Host;
                        if (userHost == null) continue;
                        var processedNeighbours = new Dictionary<User, UInt16>();
                        var id = pair.Value.GlobalId;
                        lock (server.Map.ObserversById) 
                            if (server.Map.ObserversById.ContainsKey(id))
                                foreach (var user in server.Map.ObserversById[id])
                                    processedNeighbours[user] = id;
                        lock (server.Map.OfferedToById)
                            if (server.Map.OfferedToById.ContainsKey(id))
                                foreach (var user in server.Map.OfferedToById[id])
                                    processedNeighbours[user] = id;
                        lock (server.Map.Cluster) foreach (var neighbour in server.Map.Cluster.ObservingNeighbours(pair.Value))
                        {
                            //Create job only if class is not redirected to ignore pattern "#" or begins with "@"
                            var mapping = neighbour.ClassMapping;
                            if (string.IsNullOrEmpty(mapping) || mapping == "#" || mapping[0] == '@') continue;
                            processedNeighbours[neighbour.User] = id;
                        }
                        foreach (var user in processedNeighbours.Keys)
                        {
                            if (user == this) continue;
                            var observerHost = user.Host;
                            if (observerHost == null) continue;
                            byte[] ip;
                            var convert = new HostVsNetwork();
                            if (userHost.HairPinningIP != userHost.IP && observerHost.HairPinningIP != observerHost.IP) ip = observerHost.HairPinningIP.GetAddressBytes();
                            else ip = observerHost.IP.GetAddressBytes();
                            convert.HostByte0 = ip[0];
                            convert.HostByte1 = ip[1];
                            convert.HostByte2 = ip[2];
                            convert.HostByte3 = ip[3];
                            jobs.Enqueue( new Job(PacketType.GroupInfo, new[]
                            {
                                id,
                                convert.HostUInt32,
                                (uint) user.SendToPort
                            }));
                            if (NAT && !user.NAT && user.ReceivedFromPort > 0) jobs.Enqueue( new Job(PacketType.GroupInfo, new[]
                            {
                                id,
                                convert.HostUInt32,
                                (uint) user.ReceivedFromPort
                            }));
                        }
                    }
                }
            }
            return jobs.Dequeue();
        }
        
        private uint GetTrack()
        {
            if (tracks.Count==0)
            {
                try
                {
                    lock(Host.Tracks) foreach (var track in Host.Tracks) tracks.Enqueue(track);
                }
                catch(NullReferenceException){}
            }
            return tracks.Count > 0 ? tracks.Dequeue() : 0;
        }

        private bool TrackToggle
        {
            get
            {
                if (tracks.Count == 0)
                {
                    var id = GetTrack();
                    if (id==0) return false;
                    tracks.Enqueue(id);
                }
                return trackToggle = !trackToggle;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this <see cref="User"/> is NAT.
        /// </summary>
        /// <value><c>true</c> if NAT; otherwise, <c>false</c>.</value>
        public bool NAT { get; set; }

        /// <summary>
        /// Gets or sets the time.
        /// </summary>
        /// <value>The time.</value>
        public Time Time { get; private set; }

        /// <summary>
        /// Gets or sets the local time.
        /// </summary>
        /// <value>The local time.</value>
        public Time LocalTime { get; private set; }

        public void HandOver(Vessel vessel)
        {
            lock (localsById)
            {
                uint localId = 0;
                var found = false;
                foreach(var pair in localsById)
                    if (pair.Value == vessel)
                    {
                        localId = pair.Key;
                        found = true;
                        break;
                    }
                if (!found) return;
                localsById.Remove(localId);
                lockedById[localId] = timeStamp;
            }
        }

        public void HandOver(Vessel vessel, UInt16 localId)
        {
            lock (localsById)
            {
                vessel.HandOver(this);
                localsById.Add(localId, vessel);
            }
        }
    }
}
