using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace KNLib
{
    public class KNClient
    {
        private static Bridge applet;
        private TcpClient client;
        private byte[] decodeKey;
        private String passwordKey;
        private String nickname, password, channel;
        private String butler;

        static KNClient()
        {
            applet = new Bridge();
        }

        private static String GetHost(RemoteEndpoint endpoint)
        {
            switch (endpoint)
            {
                case RemoteEndpoint.COM:
                    return "knuddels.com";
                default: // DE, AT, CH, TEST, MFC
                    return "knuddels.net";
            }
        }

        private static int GetPort(RemoteEndpoint endpoint)
        {
            switch (endpoint)
            {
                case RemoteEndpoint.AT:
                    return 2711;
                case RemoteEndpoint.CH:
                    return 2712;
                case RemoteEndpoint.COM:
                    return 2713;
                case RemoteEndpoint.TEST:
                    return 2720;
                case RemoteEndpoint.MFC:
                    return 4242;
                default: // DE
                    return 2710;
            }
        }

        public void Connect(RemoteEndpoint endpoint)
        {
            if (!IsConnected())
            {
                client = new TcpClient(GetHost(endpoint), GetPort(endpoint));
                new Thread(new ThreadStart(Run)).Start();
            }
        }

        public void Connect(RemoteEndpoint endpoint, String proxyHost, int proxyPort)
        {
            if (!IsConnected())
            {
                client = new TcpClient(proxyHost, proxyPort);
                Socks5Helper.EstablishConnection(client, GetHost(endpoint), GetPort(endpoint));
                new Thread(new ThreadStart(Run)).Start();
            }
        }

        public void Disconnect()
        {
            if (client != null)
            {
                if (client.Connected)
                {
                    client.Close();
                }

                OnDisconnect();
                client = null;
                decodeKey = null;
                passwordKey = null;
                nickname = null;
            }
        }

        public bool IsConnected()
        {
            return client != null && client.Connected;
        }

        public void Send(String token)
        {
            if (token.StartsWith("n\0"))
            {
                if (passwordKey == null)
                {
                    String[] tokens = token.Split('\0');
                    nickname = tokens[2];
                    password = tokens[3];
                    channel = tokens[1];
                    return;
                }

                token = String.Format("{0}\0{1}", token, passwordKey);
            }

            if (IsConnected())
            {
                byte[] buffer;

                lock (applet)
                {
                    buffer = Encode(applet.Parse(0x01, Encoding.UTF8.GetBytes(token)));
                }

                client.GetStream().Write(buffer, 0, buffer.Length);
            }
        }

        public void Login(String nickname, String password, String channel)
        {
            Send(String.Format("n\0{0}\0{1}\0{2}\0F", channel, nickname, password));
        }

        public void SendMessage(String message, String channel)
        {
            Send(String.Format("e\0{0}\0{1}", channel, message));
        }

        public String GetButler()
        {
            return butler;
        }

        public virtual void OnReceive(String token)
        {
        }

        public virtual void OnDisconnect()
        {
        }

        private void Run()
        {
            client.GetStream().WriteByte(0x00); // 0x00 = Chat Server, 0x02 = Registration Server
            Send("t\0"); // Das Hello-Paket wird vom Applet dynamisch überschrieben.

            while (true)
            {
                try
                {
                    byte[] buffer = Decode(client.GetStream());
                    String token;

                    lock (applet)
                    {
                        token = Encoding.UTF8.GetString(applet.Parse(0x02, buffer));
                    }

                    String[] tokens = token.Split('\0');

                    switch (tokens[0])
                    {
                        case "(":
                            String key = tokens[3].Trim();
                            decodeKey = Encoding.UTF8.GetBytes(key);
                            passwordKey = tokens[1] + key.Substring(1);

                            if (nickname != null)
                            {
                                Login(nickname, password, channel);
                                nickname = null;
                            }

                            break;
                        case ",":
                            if (tokens.Length > 1)
                            {
                                Send("h\0" + tokens[1]);
                            }

                            break;
                        case "5":
                            butler = tokens[1];
                            break;
                    }

                    OnReceive(token);
                }
                catch (IOException)
                {
                    break;
                }
            }

            Disconnect();
        }

        private byte[] Encode(byte[] message)
        {
            int length = message.Length - 1;
            byte[] len;

            if (length < 128)
            {
                len = new byte[] { (byte)length };
            }
            else
            {
                int count = 0;

                while (32 << (count + 1 << 3) <= length)
                {
                    count++;
                }

                count++;
                len = new byte[count + 1];
                len[0] = (byte)(count << 5 | 0x80 | length & 0x1F);

                for (int i = 1; i < len.Length; i++)
                {
                    len[i] = (byte)(length >> 8 * (i - 1) + 5);
                }
            }

            byte[] buffer = new byte[len.Length + message.Length];
            len.CopyTo(buffer, 0);
            message.CopyTo(buffer, len.Length);
            return buffer;
        }

        private byte[] Decode(Stream stream)
        {
            sbyte first = (sbyte)stream.ReadByte();

            if (first == -1)
            {
                throw new IOException("End of stream");
            }

            int length;

            if (first >= 0)
            {
                length = first + 1;
            }
            else
            {
                length = (first & 0x1F) + 1;
                int count = ((first & 0x60) >> 5);

                for (int i = 0; i < count; i++)
                {
                    length += (stream.ReadByte() << (i << 3) + 5);
                }
            }

            byte[] buffer = new byte[length];

            for (int i = 0; i < length; i++)
            {
                buffer[i] = (byte)((byte)stream.ReadByte() ^ (decodeKey != null && i < decodeKey.Length ? decodeKey[i] : 0));
            }

            return buffer;
        }
    }
}
