using System;
using System.Reflection;
using System.Collections.Generic;

namespace MessagePack.Formatters
{
    public sealed class PrimitiveObjectFormatter : IMessagePackFormatter<object>
    {
        public static readonly IMessagePackFormatter<object> Instance = new PrimitiveObjectFormatter();

        static readonly Dictionary<Type, int> typeToJumpCode = new Dictionary<Type, int>()
        {
            { typeof(Boolean), 0 },
            { typeof(Char), 1 },
            { typeof(SByte), 2 },
            { typeof(Byte), 3 },
            { typeof(Int16), 4 },
            { typeof(UInt16), 5 },
            { typeof(Int32), 6 },
            { typeof(UInt32), 7 },
            { typeof(Int64), 8 },
            { typeof(UInt64),9  },
            { typeof(Single), 10 },
            { typeof(Double), 11 },
            { typeof(DateTime), 12 },
            { typeof(string), 13 },
            { typeof(byte[]), 14 }
        };

        PrimitiveObjectFormatter()
        {

        }

#if !UNITY_WSA

        public static bool IsSupportedType(Type type, TypeInfo typeInfo, object value)
        {
            if (value == null) return true;
            if (typeToJumpCode.ContainsKey(type)) return true;
            if (typeInfo.IsEnum) return true;

            if (value is System.Collections.IDictionary) return true;
            if (value is System.Collections.ICollection) return true;

            return false;
        }

#endif

        public int Serialize(ref byte[] bytes, int offset, object value, IFormatterResolver formatterResolver)
        {
            if (value == null)
            {
                return MessagePackBinary.WriteNil(ref bytes, offset);
            }

            var t = value.GetType();

            int code;
            if (typeToJumpCode.TryGetValue(t, out code))
            {
                switch (code)
                {
                    case 0:
                        return MessagePackBinary.WriteBoolean(ref bytes, offset, (bool)value);
                    case 1:
                        return MessagePackBinary.WriteChar(ref bytes, offset, (char)value);
                    case 2:
                        return MessagePackBinary.WriteSByteForceSByteBlock(ref bytes, offset, (sbyte)value);
                    case 3:
                        return MessagePackBinary.WriteByteForceByteBlock(ref bytes, offset, (byte)value);
                    case 4:
                        return MessagePackBinary.WriteInt16ForceInt16Block(ref bytes, offset, (Int16)value);
                    case 5:
                        return MessagePackBinary.WriteUInt16ForceUInt16Block(ref bytes, offset, (UInt16)value);
                    case 6:
                        return MessagePackBinary.WriteInt32ForceInt32Block(ref bytes, offset, (Int32)value);
                    case 7:
                        return MessagePackBinary.WriteUInt32ForceUInt32Block(ref bytes, offset, (UInt32)value);
                    case 8:
                        return MessagePackBinary.WriteInt64ForceInt64Block(ref bytes, offset, (Int64)value);
                    case 9:
                        return MessagePackBinary.WriteUInt64ForceUInt64Block(ref bytes, offset, (UInt64)value);
                    case 10:
                        return MessagePackBinary.WriteSingle(ref bytes, offset, (Single)value);
                    case 11:
                        return MessagePackBinary.WriteDouble(ref bytes, offset, (double)value);
                    case 12:
                        return MessagePackBinary.WriteDateTime(ref bytes, offset, (DateTime)value);
                    case 13:
                        return MessagePackBinary.WriteString(ref bytes, offset, (string)value);
                    case 14:
                        return MessagePackBinary.WriteBytes(ref bytes, offset, (byte[])value);
                    default:
                        throw new InvalidOperationException("Not supported primitive object resolver. type:" + t.Name);
                }
            }
            else
            {
#if UNITY_WSA && !NETFX_CORE
                if (t.IsEnum)
#else
                if (t.GetTypeInfo().IsEnum)
#endif
                {
                    var underlyingType = Enum.GetUnderlyingType(t);
                    var code2 = typeToJumpCode[underlyingType];
                    switch (code2)
                    {
                        case 2:
                            return MessagePackBinary.WriteSByteForceSByteBlock(ref bytes, offset, (sbyte)value);
                        case 3:
                            return MessagePackBinary.WriteByteForceByteBlock(ref bytes, offset, (byte)value);
                        case 4:
                            return MessagePackBinary.WriteInt16ForceInt16Block(ref bytes, offset, (Int16)value);
                        case 5:
                            return MessagePackBinary.WriteUInt16ForceUInt16Block(ref bytes, offset, (UInt16)value);
                        case 6:
                            return MessagePackBinary.WriteInt32ForceInt32Block(ref bytes, offset, (Int32)value);
                        case 7:
                            return MessagePackBinary.WriteUInt32ForceUInt32Block(ref bytes, offset, (UInt32)value);
                        case 8:
                            return MessagePackBinary.WriteInt64ForceInt64Block(ref bytes, offset, (Int64)value);
                        case 9:
                            return MessagePackBinary.WriteUInt64ForceUInt64Block(ref bytes, offset, (UInt64)value);
                        default:
                            break;
                    }
                }
                else if (value is System.Collections.IDictionary) // check IDictionary first
                {
                    var d = value as System.Collections.IDictionary;
                    var startOffset = offset;
                    offset += MessagePackBinary.WriteMapHeader(ref bytes, offset, d.Count);
                    foreach (System.Collections.DictionaryEntry item in d)
                    {
                        offset += Serialize(ref bytes, offset, item.Key, formatterResolver);
                        offset += Serialize(ref bytes, offset, item.Value, formatterResolver);
                    }
                    return offset - startOffset;
                }
                else if (value is System.Collections.ICollection)
                {
                    var c = value as System.Collections.ICollection;
                    var startOffset = offset;
                    offset += MessagePackBinary.WriteArrayHeader(ref bytes, offset, c.Count);
                    foreach (var item in c)
                    {
                        offset += Serialize(ref bytes, offset, item, formatterResolver);
                    }
                    return offset - startOffset;
                }
            }

            throw new InvalidOperationException("Not supported primitive object resolver. type:" + t.Name);
        }

        public object Deserialize(byte[] bytes, int offset, IFormatterResolver formatterResolver, out int readSize)
        {
            var type = MessagePackBinary.GetMessagePackType(bytes, offset);
            switch (type)
            {
                case MessagePackType.Integer:
                    var code = bytes[offset];
                    if (MessagePackCode.MinNegativeFixInt <= code && code <= MessagePackCode.MaxNegativeFixInt) return MessagePackBinary.ReadSByte(bytes, offset, out readSize);
                    else if (MessagePackCode.MinFixInt <= code && code <= MessagePackCode.MaxFixInt) return MessagePackBinary.ReadByte(bytes, offset, out readSize);
                    else if (code == MessagePackCode.Int8) return MessagePackBinary.ReadSByte(bytes, offset, out readSize);
                    else if (code == MessagePackCode.Int16) return MessagePackBinary.ReadInt16(bytes, offset, out readSize);
                    else if (code == MessagePackCode.Int32) return MessagePackBinary.ReadInt32(bytes, offset, out readSize);
                    else if (code == MessagePackCode.Int64) return MessagePackBinary.ReadInt64(bytes, offset, out readSize);
                    else if (code == MessagePackCode.UInt8) return MessagePackBinary.ReadByte(bytes, offset, out readSize);
                    else if (code == MessagePackCode.UInt16) return MessagePackBinary.ReadUInt16(bytes, offset, out readSize);
                    else if (code == MessagePackCode.UInt32) return MessagePackBinary.ReadUInt32(bytes, offset, out readSize);
                    else if (code == MessagePackCode.UInt64) return MessagePackBinary.ReadUInt64(bytes, offset, out readSize);
                    throw new InvalidOperationException("Invalid primitive bytes.");
                case MessagePackType.Boolean:
                    return MessagePackBinary.ReadBoolean(bytes, offset, out readSize);
                case MessagePackType.Float:
                    if (MessagePackCode.Float32 == bytes[offset])
                    {
                        return MessagePackBinary.ReadSingle(bytes, offset, out readSize);
                    }
                    else
                    {
                        return MessagePackBinary.ReadDouble(bytes, offset, out readSize);
                    }
                case MessagePackType.String:
                    return MessagePackBinary.ReadString(bytes, offset, out readSize);
                case MessagePackType.Binary:
                    return MessagePackBinary.ReadBytes(bytes, offset, out readSize);
                case MessagePackType.Extension:
                    var ext = MessagePackBinary.ReadExtensionFormatHeader(bytes, offset, out readSize);
                    if (ext.TypeCode == ReservedMessagePackExtensionTypeCode.DateTime)
                    {
                        return MessagePackBinary.ReadDateTime(bytes, offset, out readSize);
                    }
                    throw new InvalidOperationException("Invalid primitive bytes.");
                case MessagePackType.Array:
                    {
                        var length = MessagePackBinary.ReadArrayHeader(bytes, offset, out readSize);
                        var startOffset = offset;
                        offset += readSize;

                        var objectFormatter = formatterResolver.GetFormatter<object>();
                        var array = new object[length];
                        for (int i = 0; i < length; i++)
                        {
                            array[i] = objectFormatter.Deserialize(bytes, offset, formatterResolver, out readSize);
                            offset += readSize;
                        }

                        readSize = offset - startOffset;
                        return array;
                    }
                case MessagePackType.Map:
                    {
                        var length = MessagePackBinary.ReadMapHeader(bytes, offset, out readSize);
                        var startOffset = offset;
                        offset += readSize;

                        var objectFormatter = formatterResolver.GetFormatter<object>();
                        var hash = new Dictionary<object, object>(length);
                        for (int i = 0; i < length; i++)
                        {
                            var key = objectFormatter.Deserialize(bytes, offset, formatterResolver, out readSize);
                            offset += readSize;

                            var value = objectFormatter.Deserialize(bytes, offset, formatterResolver, out readSize);
                            offset += readSize;

                            hash.Add(key, value);
                        }

                        readSize = offset - startOffset;
                        return hash;
                    }
                case MessagePackType.Nil:
                    readSize = 1;
                    return null;
                default:
                    throw new InvalidOperationException("Invalid primitive bytes.");
            }
        }
    }
}
