using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Globalization;
using System.Diagnostics;



namespace SFI.COMM.LIB.Redis
{
    public class RedisClient : IDisposable
    {
        System.Net.Sockets.Socket socket;
        BufferedStream bstream;

        public enum KeyType
        {
            None, String, List, Set
        }

        public class ResponseException : Exception
        {
            public ResponseException(string code) : base("Response error")
            {
                Code = code;
            }

            public string Code { get; private set; }
        }

        public RedisClient(string host, int port, string password)
        {
            try
            {
                if (host == null)
                    throw new ArgumentNullException("host");

                Host = host;
                Port = port;
                Password = password;
                SendTimeout = -1;
            }
            catch (Exception ex)
            {
            }
        }



        public RedisClient(string host, string password) : this(host, 6379, password)
        {
        }

        public RedisClient(string password) : this("localhost", 6379, password)
        {
        }

        public string Host { get; private set; }
        public int Port { get; private set; }
        public int RetryTimeout { get; set; }
        public int RetryCount { get; set; }
        public int SendTimeout { get; set; }
        public string Password { get; set; }

        [Conditional("DEBUG")]
        protected void Log(string id, string message)
        {
            Console.WriteLine(id + ": " + message.Trim().Replace("\r\n", " "));
        }

        public void Connect()
        {
            try
            {
                socket = new System.Net.Sockets.Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                socket.NoDelay = true;
                socket.SendTimeout = SendTimeout;
                socket.Connect(Host, Port);
                if (!socket.Connected)
                {
                    socket.Close();
                    socket = null;
                    return;
                }
                bstream = new BufferedStream(new NetworkStream(socket), 16 * 1024);

                if (Password != null)
                    SendExpectSuccess("AUTH", Password);
            }
            catch (Exception ex)
            {

            }

        }

        protected string ReadLine()
        {
            try
            {
                StringBuilder sb = new StringBuilder();
                int c;

                while ((c = bstream.ReadByte()) != -1)
                {
                    if (c == '\r')
                        continue;
                    if (c == '\n')
                        break;
                    sb.Append((char)c);
                }
                return sb.ToString();
            }
            catch (Exception ex)
            {
                return string.Empty;
            }


        }

        protected byte[] ReadData(params int[] lookahead)
        {
            try
            {
                int c;
                if (lookahead.Length == 1)
                    c = lookahead[0];
                else
                    c = bstream.ReadByte();
                if (c == -1)
                    throw new ResponseException("No more data");

                string s = ReadLine();
                Log("S", (char)c + s);
                if (c == '-')
                    throw new ResponseException(s.StartsWith("ERR ") ? s.Substring(4) : s);
                if (c == '$')
                {
                    if (s == "$-1")
                        return null;
                    int n;

                    if (int.TryParse(s, out n))
                    {
                        byte[] retbuf = new byte[n];

                        int bytesRead = 0;
                        do
                        {
                            int read = bstream.Read(retbuf, bytesRead, n - bytesRead);
                            if (read < 1)
                                throw new ResponseException("Invalid termination mid stream");
                            bytesRead += read;
                        }
                        while (bytesRead < n);
                        if (bstream.ReadByte() != '\r' || bstream.ReadByte() != '\n')
                            throw new ResponseException("Invalid termination");
                        return retbuf;
                    }
                    throw new ResponseException("Invalid length");
                }

                throw new ResponseException("Unexpected reply: " + (char)c + s);
            }
            catch (Exception ex)
            {
                return null;
            }
        }

        // read array of bulk strings
        protected byte[][] ReadDataArray()
        {
            try
            {
                int c = bstream.ReadByte();
                if (c == -1)
                    throw new ResponseException("No more data");

                string s = ReadLine();
                Log("S", (char)c + s);
                if (c == '-')
                    throw new ResponseException(s.StartsWith("ERR ") ? s.Substring(4) : s);
                if (c == '*')
                {
                    int count;
                    if (int.TryParse(s, out count))
                    {
                        byte[][] result = new byte[count][];

                        for (int i = 0; i < count; i++)
                            result[i] = ReadData();

                        return result;
                    }
                }
                throw new ResponseException("Unknown reply on array request: " + c + s);
            }
            catch (Exception ex)
            {
                return null;
            }

        }

        // read array of elements with mixed type (bulk string, integer, nested array)
        protected object[] ReadMixedArray(params int[] lookahead)
        {
            try
            {
                int c;
                if (lookahead.Length == 1)
                    c = lookahead[0];
                else
                    c = bstream.ReadByte();
                if (c == -1)
                    throw new ResponseException("No more data");

                string s = ReadLine();
                Log("S", (char)c + s);
                if (c == '-')
                    throw new ResponseException(s.StartsWith("ERR ") ? s.Substring(4) : s);
                if (c == '*')
                {
                    int count;
                    if (int.TryParse(s, out count))
                    {
                        object[] result = new object[count];

                        for (int i = 0; i < count; i++)
                        {
                            int peek = bstream.ReadByte();
                            if (peek == '$')
                                result[i] = ReadData(peek);
                            else if (peek == ':')
                                result[i] = ReadInt(peek);
                            else if (peek == '*')
                                result[i] = ReadMixedArray(peek);
                            else
                                throw new ResponseException("Unknown array element: " + c + s);
                        }
                        return result;
                    }
                }
                if (s == "OK") return new object[1] { "OK" };
                throw new ResponseException("Unknown reply on array request: " + c + s);
            }
            catch (Exception ex)
            {
                return null;
            }

        }

        byte[] end_line = new byte[] { (byte)'\r', (byte)'\n' };

        protected bool SendDataCommand(byte[] data, string cmd, params object[] args)
        {
            try
            {
                MemoryStream ms = new MemoryStream();
                string resp = "*" + (1 + args.Length + 1) + "\r\n";
                resp += "$" + cmd.Length + "\r\n" + cmd + "\r\n";
                foreach (object arg in args)
                {
                    string argStr = string.Format(CultureInfo.InvariantCulture, "{0}", arg);
                    int argStrLength = Encoding.UTF8.GetByteCount(argStr);
                    resp += "$" + argStrLength + "\r\n" + argStr + "\r\n";
                }
                resp += "$" + data.Length + "\r\n";
                byte[] r = Encoding.UTF8.GetBytes(resp);
                ms.Write(r, 0, r.Length);
                ms.Write(data, 0, data.Length);
                ms.Write(end_line, 0, end_line.Length);

                Log("C", resp);
                return SendRaw(ms.ToArray());
            }
            catch (Exception ex)
            {
                return false;
            }


        }

        protected void SendTuplesCommand(object[] ids, byte[][] values, string cmd, params object[] args)
        {
            try
            {
                if (ids.Length != values.Length)
                    throw new ArgumentException("id's and values must have the same size");

                MemoryStream ms = new MemoryStream();
                string resp = "*" + (1 + 2 * ids.Length + args.Length) + "\r\n";
                resp += "$" + cmd.Length + "\r\n" + cmd + "\r\n";
                foreach (object arg in args)
                {
                    string argStr = string.Format(CultureInfo.InvariantCulture, "{0}", arg);
                    int argStrLength = Encoding.UTF8.GetByteCount(argStr);
                    resp += "$" + argStrLength + "\r\n" + argStr + "\r\n";
                }
                byte[] r = Encoding.UTF8.GetBytes(resp);
                ms.Write(r, 0, r.Length);

                for (int i = 0; i < ids.Length; i++)
                {
                    string idStr = string.Format(CultureInfo.InvariantCulture, "{0}", ids[i]);
                    byte[] id = Encoding.UTF8.GetBytes(idStr);
                    byte[] val = values[i];
                    byte[] idLength = Encoding.UTF8.GetBytes("$" + id.Length + "\r\n");
                    byte[] valLength = Encoding.UTF8.GetBytes("$" + val.Length + "\r\n");
                    ms.Write(idLength, 0, idLength.Length);
                    ms.Write(id, 0, id.Length);
                    ms.Write(end_line, 0, end_line.Length);
                    ms.Write(valLength, 0, valLength.Length);
                    ms.Write(val, 0, val.Length);
                    ms.Write(end_line, 0, end_line.Length);
                }

                Log("C", resp);
                SendRaw(ms.ToArray());
            }
            catch (Exception ex)
            {

            }


        }

        private bool SendRaw(byte[] r)
        {
            try
            {
                if (socket == null)
                    Connect();
                if (socket == null)
                    return false;

                try
                {
                    var result = socket.Send(r);
                }
                catch (SocketException)
                {
                    // timeout;
                    socket.Close();
                    socket = null;

                    return false;
                }
                return true;
            }
            catch (Exception ex)
            {
                return false;
            }
        }

        public bool SendCommand(string cmd, params object[] args)
        {
            try
            {
                string resp = "*" + (1 + args.Length) + "\r\n";
                resp += "$" + cmd.Length + "\r\n" + cmd + "\r\n";
                foreach (object arg in args)
                {
                    string argStr = string.Format(CultureInfo.InvariantCulture, "{0}", arg);
                    int argStrLength = Encoding.UTF8.GetByteCount(argStr);
                    resp += "$" + argStrLength + "\r\n" + argStr + "\r\n";
                }

                Console.WriteLine("Redis-Publish:", resp);
                return SendRaw(Encoding.UTF8.GetBytes(resp));
            }
            catch (Exception ex)
            {
                return false;
            }
        }


        public bool SendCommand2(string cmd, params object[] args)
        {
            try
            {
                string resp = "*" + (1 + args.Length) + " ";
                resp += cmd + " ";
                foreach (object arg in args)
                {
                    string argStr = string.Format(CultureInfo.InvariantCulture, "{0}", arg);
                    int argStrLength = Encoding.UTF8.GetByteCount(argStr);
                    resp +=  argStr + " ";
                }

                Console.WriteLine("Redis-Publish:", resp);
                return SendRaw(Encoding.UTF8.GetBytes(resp));
            }
            catch (Exception ex)
            {
                return false;
            }
        }

        protected void ExpectSuccess()
        {
            try
            {
                int c = bstream.ReadByte();
                if (c == -1)
                    throw new ResponseException("No more data");

                string s = ReadLine();
                Log("S", (char)c + s);
                if (c == '-')
                    throw new ResponseException(s.StartsWith("ERR ") ? s.Substring(4) : s);
            }
            catch (Exception ex)
            {

            }
        }

        protected void SendExpectSuccess(string cmd, params object[] args)
        {
            try
            {
                if (!SendCommand(cmd, args))
                    throw new Exception("Unable to connect");

                ExpectSuccess();
            }
            catch (Exception ex)
            {

            }


        }

        protected int ReadInt(params int[] lookahead)
        {
            try
            {
                int c;
                if (lookahead.Length == 1)
                    c = lookahead[0];
                else
                    c = bstream.ReadByte();
                if (c == -1)
                    throw new ResponseException("No more data");

                string s = ReadLine();
                Log("S", (char)c + s);
                if (c == '-')
                    throw new ResponseException(s.StartsWith("ERR ") ? s.Substring(4) : s);
                if (c == ':')
                {
                    int i;
                    if (int.TryParse(s, out i))
                        return i;
                }
                throw new ResponseException("Unknown reply on integer request: " + c + s);
            }
            catch (Exception ex)
            {
                return 0;
            }
        }

        protected int SendDataExpectInt(byte[] data, string cmd, params object[] args)
        {
            try
            {
                if (!SendDataCommand(data, cmd, args))
                    throw new Exception("Unable to connect");

                return ReadInt();
            }
            catch (Exception ex)
            {
                return 0;
            }


        }

        protected int SendExpectInt(string cmd, params object[] args)
        {
            try
            {
                if (!SendCommand(cmd, args))
                    throw new Exception("Unable to connect");

                return ReadInt();
            }
            catch (Exception ex)
            {
                return 0;
            }


        }

        protected string ReadString()
        {
            try
            {
                int c = bstream.ReadByte();
                if (c == -1)
                    throw new ResponseException("No more data");

                string s = ReadLine();
                Log("S", (char)c + s);
                if (c == '-')
                    throw new ResponseException(s.StartsWith("ERR ") ? s.Substring(4) : s);
                if (c == '+')
                    return s;

                throw new ResponseException("Unknown reply on string request: " + c + s);
            }
            catch (Exception ex)
            {
                return string.Empty;
            }

        }


        protected string ReadString2()
        {
            try
            {
                int c = bstream.ReadByte();
                if (c == -1)
                    throw new ResponseException("No more data");

                string read = ReadLine();
                string result = read;

                while (!string.IsNullOrEmpty(read))
                {
                    read = ReadLine();
                    if (!string.IsNullOrEmpty(read))
                        result += Environment.NewLine + read;
                }

                return result;
            }
            catch (Exception ex)
            {
                return string.Empty;
            }

        }

        public string SendExpectString(string cmd, params object[] args)
        {
            try
            {
                if (!SendCommand(cmd, args))
                    throw new Exception("Unable to connect");

                return ReadString();
            }
            catch (Exception ex)
            {
                return string.Empty;
            }


        }



        public string SendExpectString2(string cmd, params object[] args)
        {
            try
            {
                if (!SendCommand(cmd, args))
                    throw new Exception("Unable to connect");

                return ReadString2();
            }
            catch (Exception ex)
            {
                return string.Empty;
            }


        }


        //
        // This one does not throw errors
        //
        protected string SendGetString(string cmd, params object[] args)
        {
            try
            {
                if (!SendCommand(cmd, args))
                    throw new Exception("Unable to connect");

                return ReadLine();
            }
            catch (Exception ex)
            {
                return string.Empty;
            }


        }

        protected string[] SendExpectStringArray(string cmd, params object[] args)
        {
            byte[][] reply = SendExpectDataArray(cmd, args);
            return ToStringArray(reply);
        }

        protected byte[] SendExpectData(string cmd, params object[] args)
        {
            try
            {
                if (!SendCommand(cmd, args))
                    throw new Exception("Unable to connect");

                return ReadData();
            }
            catch (Exception ex)
            {
                return null;
            }

        }

        protected byte[][] SendExpectDataArray(string cmd, params object[] args)
        {
            try
            {
                if (!SendCommand(cmd, args))
                    throw new Exception("Unable to connect");
                return ReadDataArray();
            }
            catch (Exception ex)
            {
                return null;
            }


        }

        public void Shutdown()
        {
            SendCommand("SHUTDOWN");
            try
            {
                // the server may return an error
                string s = ReadLine();
                Log("S", s);
                if (s.Length == 0)
                    throw new ResponseException("Zero length respose");
                throw new ResponseException(s.StartsWith("-ERR ") ? s.Substring(5) : s.Substring(1));
            }
            catch (IOException)
            {
                // this is the expected good result
                socket.Close();
                socket = null;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        ~RedisClient()
        {
            Dispose(false);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (socket != null)
                {
                    socket.Close();
                    socket = null;
                }

            }
        }

        #region Conversion between text strings and bulk strings
        public static byte[] ToData(string text)
        {
            return Encoding.UTF8.GetBytes(text);
        }

        public static string ToString(byte[] data)
        {
            return Encoding.UTF8.GetString(data);
        }

        public static string[] ToStringArray(byte[][] dataArray)
        {
            string[] result = new string[dataArray.Length];
            for (int i = 0; i < result.Length; i++)
                result[i] = Encoding.UTF8.GetString(dataArray[i]);
            return result;
        }
        #endregion

        // prepend additional arguments to args array
        public static object[] PrependArgs(object[] args, params object[] preArgs)
        {
            if (args.Length == 0)
                return preArgs;
            else
            {
                object[] newArgs = new object[preArgs.Length + args.Length];
                Array.Copy(preArgs, 0, newArgs, 0, preArgs.Length);
                Array.Copy(args, 0, newArgs, preArgs.Length, args.Length);
                return newArgs;
            }
        }
    }
}
