using System.Linq;
using System;
using System.Net;
using System.Net.Sockets;
using NLog;

namespace NzbDrone.Common
{
    public class UdpProvider
    {
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

        public UdpProvider()
        {
            
        }

        private const int StandardPort = 9777;
        private const int MaxPacketSize = 1024;
        private const int HeaderSize = 32;
        private const int MaxPayloadSize = MaxPacketSize - HeaderSize;
        private const byte MajorVersion = 2;
        private const byte MinorVersion = 0;

        public enum PacketType
        {
            Helo = 0x01,
            Bye = 0x02,
            Button = 0x03,
            Mouse = 0x04,
            Ping = 0x05,
            Broadcast = 0x06,  //Currently not implemented
            Notification = 0x07,
            Blob = 0x08,
            Log = 0x09,
            Action = 0x0A,
            Debug = 0xFF //Currently not implemented
        }

        private byte[] Header(PacketType packetType, int numberOfPackets, int currentPacket, int payloadSize, uint uniqueToken)
        {
            byte[] header = new byte[HeaderSize];

            header[0] = (byte)'X';
            header[1] = (byte)'B';
            header[2] = (byte)'M';
            header[3] = (byte)'C';

            header[4] = MajorVersion;
            header[5] = MinorVersion;

            if (currentPacket == 1)
            {
                header[6] = (byte)(((ushort)packetType & 0xff00) >> 8);
                header[7] = (byte)((ushort)packetType & 0x00ff);
            }
            else
            {
                header[6] = ((ushort)PacketType.Blob & 0xff00) >> 8;
                header[7] = (ushort)PacketType.Blob & 0x00ff;
            }

            header[8] = (byte)((currentPacket & 0xff000000) >> 24);
            header[9] = (byte)((currentPacket & 0x00ff0000) >> 16);
            header[10] = (byte)((currentPacket & 0x0000ff00) >> 8);
            header[11] = (byte)(currentPacket & 0x000000ff);

            header[12] = (byte)((numberOfPackets & 0xff000000) >> 24);
            header[13] = (byte)((numberOfPackets & 0x00ff0000) >> 16);
            header[14] = (byte)((numberOfPackets & 0x0000ff00) >> 8);
            header[15] = (byte)(numberOfPackets & 0x000000ff);

            header[16] = (byte)((payloadSize & 0xff00) >> 8);
            header[17] = (byte)(payloadSize & 0x00ff);

            header[18] = (byte)((uniqueToken & 0xff000000) >> 24);
            header[19] = (byte)((uniqueToken & 0x00ff0000) >> 16);
            header[20] = (byte)((uniqueToken & 0x0000ff00) >> 8);
            header[21] = (byte)(uniqueToken & 0x000000ff);

            return header;

        }

        public virtual bool Send(string address, PacketType packetType, byte[] payload)
        {
            var uniqueToken = (uint)DateTime.Now.TimeOfDay.Milliseconds;

            var socket = Connect(address, StandardPort);

            if (socket == null || !socket.Connected)
            {
                return false;
            }

            try
            {
                bool successfull = true;
                int packetCount = (payload.Length / MaxPayloadSize) + 1;
                int bytesToSend = 0;
                int bytesSent = 0;
                int bytesLeft = payload.Length;

                for (int Package = 1; Package <= packetCount; Package++)
                {

                    if (bytesLeft > MaxPayloadSize)
                    {
                        bytesToSend = MaxPayloadSize;
                        bytesLeft -= bytesToSend;
                    }
                    else
                    {
                        bytesToSend = bytesLeft;
                        bytesLeft = 0;
                    }

                    byte[] header = Header(packetType, packetCount, Package, bytesToSend, uniqueToken);
                    byte[] packet = new byte[MaxPacketSize];

                    Array.Copy(header, 0, packet, 0, header.Length);
                    Array.Copy(payload, bytesSent, packet, header.Length, bytesToSend);

                    int sendSize = socket.Send(packet, header.Length + bytesToSend, SocketFlags.None);

                    if (sendSize != (header.Length + bytesToSend))
                    {
                        successfull = false;
                        break;
                    }

                    bytesSent += bytesToSend;
                }
                Disconnect(socket);
                return successfull;
            }

            catch
            {
                Disconnect(socket);
                return false;
            }
        }

        private Socket Connect(string address, int port)
        {
            try
            {
                var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

                IPAddress ip;
                if (!IPAddress.TryParse(address, out ip))
                {
                    IPHostEntry ipHostEntry = Dns.GetHostEntry(address);
                    foreach (IPAddress ipAddress in ipHostEntry.AddressList)
                    {
                        if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
                        {
                            ip = ipAddress;
                            break;
                        }
                    }
                }

                socket.Connect(new IPEndPoint(ip, port));
                return socket;
            }

            catch (Exception exc)
            {
                Logger.TraceException(exc.Message, exc);
                return null;
            }
        }

        private void Disconnect(Socket socket)
        {
            try
            {
                if (socket != null)
                {
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();
                }
            }
            catch
            {
            }
        }
    }
}
