/* Copyright (c) 2008-2011 - Eric P. Mangold
 * Released under the terms of the MIT/X11 license - see LICENSE.txt */
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;

namespace AMP
{
    // TODO implement AMPList

    public interface IAmpType
    {
        byte[] ToAmpBytes(Object obj);
        Object FromAmpBytes(byte[] bytes);
    }

    namespace Type
    {
        /// <summary>
        /// Encoder/Decoder for .NET `string' type
        /// </summary>
        public class String : IAmpType
        {
            public byte[] ToAmpBytes(Object obj)
            {
                return Encoding.UTF8.GetBytes((string)obj);
            }
            public Object FromAmpBytes(byte[] bytes)
            {
                return Encoding.UTF8.GetString(bytes);
            }
        }

        /// <summary>
        /// Encoder/Decoder for .NET `byte[]' type
        /// </summary>
        public class Bytes : IAmpType
        {
            public byte[] ToAmpBytes(Object obj)
            {
                return (byte[])obj;
            }
            public Object FromAmpBytes(byte[] bytes)
            {
                return bytes;
            }
        }

        /// <summary>
        /// Encoder/Decoder for .NET `bool' type
        /// </summary>
        public class Bool : IAmpType
        {
            private byte[] TRUE = Encoding.UTF8.GetBytes("True");
            private byte[] FALSE = Encoding.UTF8.GetBytes("False");

            public byte[] ToAmpBytes(Object obj)
            {
                return (bool)obj ? TRUE : FALSE;
            }
            public Object FromAmpBytes(byte[] bytes)
            {
                if (BytesUtil.AreEqual(TRUE, bytes))
                {
                    return true;
                }
                if (BytesUtil.AreEqual(FALSE, bytes))
                {
                    return false;
                }
                /// XXX If this throws it blows up when passing this error across thread boundaries
                /// with SerializationException.  test_AMP_error will demonstrate if you
                /// change this method to always throw
                throw new Error.TypeDecodeError();
            }
        }

        /// <summary>
        /// Encoder/Decoder for .NET `int' type
        /// </summary>
        public class Int32 : IAmpType
        {
            public byte[] ToAmpBytes(Object obj)
            {
                return Encoding.UTF8.GetBytes(((int)obj).ToString());
            }
            public Object FromAmpBytes(byte[] bytes)
            {
                return System.Int32.Parse(Encoding.UTF8.GetString(bytes));
            }
        }

        /// <summary>
        /// Encoder/Decoder for .NET `uint' type
        /// </summary>
        public class UInt32 : IAmpType
        {
            public byte[] ToAmpBytes(Object obj)
            {
                return Encoding.UTF8.GetBytes(((uint)obj).ToString());
            }
            public Object FromAmpBytes(byte[] bytes)
            {
                return System.UInt32.Parse(Encoding.UTF8.GetString(bytes));
            }
        }

        /// <summary>
        /// Encoder/Decoder for .NET `long' type
        /// </summary>
        public class Int64 : IAmpType
        {
            public byte[] ToAmpBytes(Object obj)
            {
                return Encoding.UTF8.GetBytes(((long)obj).ToString());
            }
            public Object FromAmpBytes(byte[] bytes)
            {
                return System.Int64.Parse(Encoding.UTF8.GetString(bytes));
            }
        }

        /// <summary>
        /// Encoder/Decoder for .NET `ulong' type
        /// </summary>
        public class UInt64 : IAmpType
        {
            public byte[] ToAmpBytes(Object obj)
            {
                return Encoding.UTF8.GetBytes(((ulong)obj).ToString());
            }

            public Object FromAmpBytes(byte[] bytes)
            {
                return System.UInt64.Parse(Encoding.UTF8.GetString(bytes));
            }
        }

        /// <summary>
        /// Encoder/Decoder for .NET `decimal' type
        /// </summary>
        public class Decimal : IAmpType
        {
            public byte[] ToAmpBytes(Object obj)
            {
                return Encoding.UTF8.GetBytes(((decimal)obj).ToString());
            }

            public Object FromAmpBytes(byte[] bytes)
            {
                string s = Encoding.UTF8.GetString(bytes);
                string[] parts = s.Split('E'); // might use Engineering notation
                if (parts.Length == 1)
                {
                    return decimal.Parse(s);
                }
                else if (parts.Length == 2)
                {
                    decimal d = decimal.Parse(parts[0]);
                    int i = int.Parse(parts[1]);

                    if (i != 0)
                    {
                        decimal factor = (i > 0) ? 10m : 0.1m;
                        for (int j = Math.Abs(i); j > 0; j--)
                        {
                            d *= factor;
                        }
                    }
                    return d;
                }
                else
                {
                    throw new Error.TypeDecodeError();
                }
            }
        }

        /// <summary>
        /// Encoder/Decoder for .NET `double' type
        /// </summary>
        public class Double : IAmpType
        {
	        public static byte[] NAN    = Encoding.UTF8.GetBytes("nan");
	        public static byte[] POSINF = Encoding.UTF8.GetBytes("inf");
	        public static byte[] NEGINF = Encoding.UTF8.GetBytes("-inf");
	        
        	static bool arraysEqual(byte[] a, byte[] b)
			{
				if (a.Length != b.Length)
					return false;
				
				int i;
				for (i = 0; i < a.Length; i++) {
					if (a[i] != b[i])
						return false;
				}
				return true;
			}
			
            public byte[] ToAmpBytes(Object obj)
            {
            	double d = (double)obj;
            	
				if (double.IsNaN(d)) {
					return NAN;
				} else if (double.IsPositiveInfinity(d)) {
					return POSINF;
				} else if (double.IsNegativeInfinity(d)) {
					return NEGINF;
				} else {
	                return Encoding.UTF8.GetBytes(d.ToString());
				}
            }
            public Object FromAmpBytes(byte[] bytes)
            {
            	if (arraysEqual(bytes, NAN)) {
					return double.NaN;
				} else if (arraysEqual(bytes, POSINF)) {
					return double.PositiveInfinity;
				} else if (arraysEqual(bytes, NEGINF)) {
					return double.NegativeInfinity;
				} else {
	                return double.Parse(Encoding.UTF8.GetString(bytes));
				}
            }
        }

        /// <summary>
        /// Encoder/Decoder for .NET `DateTimeOffset' type, using the standard AMP `DateTime' format.
        /// If you wish to send .NET `DateTime' instances over the wire, convert them to a `DateTimeOffset' first.
        /// </summary>
        public class DateTimeOffset : IAmpType
        {
            public byte[] ToAmpBytes(Object obj)
            {
                var dt = (System.DateTimeOffset)obj;
                var fmt = new System.Globalization.DateTimeFormatInfo();
                fmt.TimeSeparator = ":";

                string s = dt.ToString("yyyy-MM-ddTHH:mm:ss.ffffffzzz", fmt);
                System.Diagnostics.Debug.Assert(s.Length == 32);
                return Encoding.UTF8.GetBytes(s);
            }

            public Object FromAmpBytes(byte[] bytes)
            {
                // "2012-01-23T12:34:56.054321+01:23"
                string s = Encoding.UTF8.GetString(bytes);
                System.Diagnostics.Debug.Assert(s.Length == 32);
                int year = int.Parse(s.Substring(0, 4));
                int month = int.Parse(s.Substring(5, 2));
                int day = int.Parse(s.Substring(8, 2));

                int hour = int.Parse(s.Substring(11, 2));
                int min = int.Parse(s.Substring(14, 2));
                int sec = int.Parse(s.Substring(17, 2));

                // The AMP spec specifies 6 decimal places for the fractional portion of the seconds.
                // But a DateTimeOffset can only hold miliseconds (3 decimal places), so we just
                // take the first 3 and ignore the rest.
                int miliseconds = int.Parse(s.Substring(20, 3));
                string offsetDirection = s.Substring(26, 1);
                int offsetHour = int.Parse(s.Substring(27, 2));
                int offsetMinute = int.Parse(s.Substring(30, 2));

                TimeSpan offset;
                if (offsetDirection == "+")
                {
                    offset = new TimeSpan(offsetHour, offsetMinute, 0);
                }
                else if (offsetDirection == "-")
                {
                    offset = new TimeSpan(offsetHour, offsetMinute, 0).Negate();
                }
                else
                {
                    throw new AMP.Error.TypeDecodeError();
                }

                return new System.DateTimeOffset(year, month, day, hour, min, sec, miliseconds, offset);
            }
        }

        /// <summary>
        /// Encoder/Decoder for .NET `DateTime' type. Encodes and decodes as UTC - timezone information is not retained.
        /// **DEPRECATED** - use Amp.Type.DateTimeOffset
        /// </summary>
        public class TimeArgument : IAmpType
        {
            public byte[] ToAmpBytes(Object obj)
            {
                DateTime dt = ((DateTime)obj).ToUniversalTime();
                string s = dt.ToString("yyyy/M/d HH:mm:ss");
                return Encoding.UTF8.GetBytes(s);
            }
            public Object FromAmpBytes(byte[] bytes)
            {
                string s = Encoding.UTF8.GetString(bytes);
                string[] parts = s.Split(' ');
                string[] date_parts = parts[0].Split('/');
                string[] time_parts = parts[1].Split(':');

                int year = int.Parse(date_parts[0]);
                int month = int.Parse(date_parts[1]);
                int day = int.Parse(date_parts[2]);

                int hour = int.Parse(time_parts[0]);
                int min = int.Parse(time_parts[1]);
                int sec = int.Parse(time_parts[2]);

                var dt = new DateTime(year, month, day, hour, min, sec, DateTimeKind.Utc);
                return dt;
            }
        }

        /// <summary>
        /// Encoder/Decoder for a List of one of the other IAmpType classes
        /// e.g. new ListOf(new AMP.Type.String())
        /// </summary>
        public class ListOf : IAmpType
        {
            private IAmpType listType;

            public ListOf(IAmpType type)
            {
                listType = type;
            }

            public byte[] ToAmpBytes(Object obj)
            {
                var chunks = new List<byte[]>();
                byte[] chunkLen;
                byte[] chunk;
                byte[] result;

                int totalLen = 0;
                if (BitConverter.IsLittleEndian)
                {
                    foreach (Object item in (IEnumerable)obj)
                    {
                        chunk = listType.ToAmpBytes(item);
                        totalLen += chunk.Length + 2;
                        chunkLen = BitConverter.GetBytes((UInt16)chunk.Length);
                        // XOR SWAP the bytes because we're on little-endian but the value
                        // needs to be in big-endian order.
                        chunkLen[0] ^= chunkLen[1];
                        chunkLen[1] ^= chunkLen[0];
                        chunkLen[0] ^= chunkLen[1];

                        chunks.Add(chunkLen);
                        chunks.Add(chunk);
                    }
                }
                else
                {
                    foreach (Object item in (IEnumerable)obj)
                    {
                        chunk = listType.ToAmpBytes(item);
                        totalLen += chunk.Length + 2;
                        chunkLen = BitConverter.GetBytes((UInt16)chunk.Length);

                        chunks.Add(chunkLen);
                        chunks.Add(chunk);
                    }
                }

                result = new byte[totalLen];

                // blit each sub-array to the result array
                int i = 0;
                foreach (byte[] c in chunks)
                {
                    c.CopyTo(result, i);
                    i += c.Length;
                }
                return result;
            }

            public Object FromAmpBytes(byte[] bytes)
            {
                int idx = 0;
                byte[] chunkLen = new byte[2];
                int chunkLenInt;
                var result = new List<Object>();

                while (idx < bytes.Length)
                {
                    // got 2 bytes remaining at least?
                    if (bytes.Length - idx < 2)
                    {
                        throw new Error.TypeDecodeError();
                    }

                    // read the length prefix
                    chunkLen[0] = bytes[idx];
                    chunkLen[1] = bytes[idx + 1];
                    idx += 2;

                    if (BitConverter.IsLittleEndian)
                    {
                        // on a little-endian machine, but value is in big-endian, so
                        // XOR SWAP bytes before decoding integer
                        chunkLen[0] ^= chunkLen[1];
                        chunkLen[1] ^= chunkLen[0];
                        chunkLen[0] ^= chunkLen[1];
                    }

                    chunkLenInt = BitConverter.ToUInt16(chunkLen, 0);

                    // We *should* have at least chunkLenInt bytes remaining in the array...
                    if (bytes.Length - idx < chunkLenInt)
                    {
                        throw new Error.TypeDecodeError();
                    }
                    var tmp = new byte[chunkLenInt];
                    Array.Copy(bytes, idx, tmp, 0, chunkLenInt);
                    idx += chunkLenInt;

                    result.Add(listType.FromAmpBytes(tmp));
                }
                return result;
            }
        }
        
        /// <summary>
        /// Encoder/Decoder for a List of AMP messages. E.g.
        /// <code>
        /// var aList = new AMP.Type.AmpList();
        /// aList.AddParameter("full_name", new AMP.Type.String());
        /// aList.AddParameter("age", new AMP.Type.Int32());
        /// </code>
        /// Then each item of the List that you send or receive using aList
        /// will be an AMP.Msg containing a "full_name" and an "age" key
        /// with values of the appropriate type (though casted to Object for
        /// storage in an AMP.Msg)
        /// </summary>
        public class AmpList : IAmpType
        {
            // A customized Protocol for parsing and accumulating AMP boxes
            private class SubParser : Protocol
            {
                private List<Msg_Raw> msgs;
                public SubParser(List<Msg_Raw> msgs)
                    : base()
                {
                    this.msgs = msgs;
                }
                internal override void ProcessFullMessage(Msg_Raw raw_msg)
                {
                    msgs.Add(raw_msg);
                }
            }
          
            private Command cmd;

            public AmpList()
            {
                // The Command class wasn't really meant to help us parse nested AMP messages
                // but it's flexible enough to do so without being refactored, so who am I to argue?
                cmd = new Command();
            }

            public void AddParameter(string keyName, IAmpType type)
            {
                cmd.AddArgument(keyName, type);
            }

            public byte[] ToAmpBytes(Object obj)
            {
                var chunks = new List<byte[]>();
                byte[] chunk;
                Msg_Raw raw_msg;
                int totalLen = 0;

                var msgs = (IEnumerable<Msg>)obj;

                foreach (Msg msg in msgs)
                {
                    raw_msg = cmd.ToRawMessage(msg, MsgType.REQUEST);
                    chunk = Protocol.BuildAMPWireCommand(raw_msg);
                    totalLen += chunk.Length;
                    chunks.Add(chunk);
                }

                var result = new byte[totalLen];
                // blit each sub-array to the result array
                int i = 0;
                foreach (byte[] c in chunks)
                {
                    c.CopyTo(result, i);
                    i += c.Length;
                }
                return result;
            }

            public Object FromAmpBytes(byte[] bytes)
            {
                var msgs = new List<Msg_Raw>();
                var subParser = new SubParser(msgs);
                var result = new List<Msg>();

                subParser.DataReceived(bytes);
                Msg typedMsg;
                foreach (Msg_Raw raw in msgs)
                {
                    typedMsg = cmd.ToTypedMessage(raw, MsgType.REQUEST);
                    result.Add(typedMsg);
                }
                return result;
            }
        }
    }
}
