#if !BESTHTTP_DISABLE_SOCKETIO && BESTHTTP_SOCKETIO_ENABLE_GAMEDEVWARE_MESSAGEPACK

using System;
using System.Linq;

using BestHTTP.PlatformSupport.Memory;
using BestHTTP.SocketIO3.Events;

using GameDevWare.Serialization;
using GameDevWare.Serialization.MessagePack;
using GameDevWare.Serialization.Serializers;

namespace BestHTTP.SocketIO3.Parsers
{
    public sealed class MsgPackParser : IParser
    {
        // Variables to calculate an average buffer size to start with in CreateOutgoing
        ulong sumLength;
        uint count;

        /// <summary>
        /// Custom function instead of char.GetNumericValue, as it throws an error under WebGL using the new 4.x runtime.
        /// It will return the value of the char if it's a numeric one, otherwise -1.
        /// </summary>
        private int ToInt(char ch)
        {
            int charValue = Convert.ToInt32(ch);
            int num = charValue - '0';
            if (num < 0 || num > 9)
                return -1;

            return num;
        }

        public IncomingPacket MergeAttachements(SocketManager manager, IncomingPacket packet) { return packet; }

        public IncomingPacket Parse(SocketManager manager, string from)
        {
            int idx = 0;
            var transportEvent = (TransportEventTypes)ToInt(from[idx++]);
            var socketIOEvent = SocketIOEventTypes.Unknown;
            var nsp = string.Empty;
            var id = -1;
            var payload = string.Empty;
            int attachments = 0;

            if (from.Length > idx && ToInt(from[idx]) >= 0)
                socketIOEvent = (SocketIOEventTypes)ToInt(from[idx++]);
            else
                socketIOEvent = SocketIOEventTypes.Unknown;

            // Parse Attachment
            if (socketIOEvent == SocketIOEventTypes.BinaryEvent || socketIOEvent == SocketIOEventTypes.BinaryAck)
            {
                int endIdx = from.IndexOf('-', idx);
                if (endIdx == -1)
                    endIdx = from.Length;

                int.TryParse(from.Substring(idx, endIdx - idx), out attachments);
                idx = endIdx + 1;
            }

            // Parse Namespace
            if (from.Length > idx && from[idx] == '/')
            {
                int endIdx = from.IndexOf(',', idx);
                if (endIdx == -1)
                    endIdx = from.Length;

                nsp = from.Substring(idx, endIdx - idx);
                idx = endIdx + 1;
            }
            else
                nsp = "/";

            // Parse Id
            if (from.Length > idx && ToInt(from[idx]) >= 0)
            {
                int startIdx = idx++;
                while (from.Length > idx && ToInt(from[idx]) >= 0)
                    idx++;

                int.TryParse(from.Substring(startIdx, idx - startIdx), out id);
            }

            // What left is the payload data
            if (from.Length > idx)
                payload = from.Substring(idx);
            else
                payload = string.Empty;

            return new IncomingPacket(transportEvent, socketIOEvent, nsp, id) { DecodedArg = payload, AttachementCount = attachments };
        }

        public IncomingPacket Parse(SocketManager manager, BufferSegment data, TransportEventTypes transportEvent = TransportEventTypes.Unknown)
        {
            using (var stream = new System.IO.MemoryStream(data.Data, data.Offset, data.Count))
            {
                var buff = BufferPool.Get(MsgPackReader.DEFAULT_BUFFER_SIZE, true);
                try
                {
                    var context = new SerializationContext
                    {
                        Options = SerializationOptions.SuppressTypeInformation/*,
                        ExtensionTypeHandler = CustomMessagePackExtensionTypeHandler.Instance*/
                    };
                    IJsonReader reader = new MsgPackReader(stream, context, Endianness.BigEndian, buff);

                    reader.ReadObjectBegin();

                    int type = -1, id = -1;
                    string nsp = null;

                    bool hasData = false, readData = false;

                    IncomingPacket packet = IncomingPacket.Empty;

                    READ:

                    while (reader.Token != JsonToken.EndOfObject)
                    {
                        string key = reader.ReadMember();

                        switch(key)
                        {
                            case "type":
                                type = reader.ReadByte();
                                break;

                            case "nsp":
                                nsp = reader.ReadString();
                                break;

                            case "id":
                                id = reader.ReadInt32();
                                break;

                            case "data":
                                if (!hasData)
                                {
                                    hasData = true;
                                    SkipData(reader, (SocketIOEventTypes)type);
                                }
                                else
                                {
                                    readData = true;

                                    packet = new IncomingPacket(transportEvent != TransportEventTypes.Unknown ? transportEvent : TransportEventTypes.Message, (SocketIOEventTypes)type, nsp, id);
                                    (string eventName, object[] args) = ReadData(manager, packet, reader);

                                    packet.EventName = eventName;
                                    if (args != null)
                                    {
                                        if (args.Length == 1)
                                            packet.DecodedArg = args[0];
                                        else
                                            packet.DecodedArgs = args;
                                    }
                                }
                                break;
                        }
                    }
                    
                    // type, nsp, id and data can come in any order. To read data strongly typed we need to know all the additional fields before processing the data field.
                    // In order to do it, when we first encounter the data field we skip it than we do a reset and an additional turn but reading the data too now.
                    if (hasData && !readData)
                    {
                        reader.Reset();
                        stream.Position = 0;
                        reader.ReadObjectBegin();

                        goto READ;
                    }

                    reader.ReadObjectEnd();

                    return packet.Equals(IncomingPacket.Empty) ? new IncomingPacket(transportEvent != TransportEventTypes.Unknown ? transportEvent : TransportEventTypes.Message, (SocketIOEventTypes)type, nsp, id) : packet;
                }
                finally
                {
                    BufferPool.Release(buff);
                }
            }
        }

        private void SkipData(IJsonReader reader, SocketIOEventTypes type)
        {
            switch (type)
            {
                case SocketIOEventTypes.Unknown:
                    // TODO: Error?
                    break;

                case SocketIOEventTypes.Connect:
                    // No Data | Object
                    SkipObject(reader);
                    break;

                case SocketIOEventTypes.Disconnect:
                    // No Data
                    break;

                case SocketIOEventTypes.Error:
                    // String | Object
                    switch(reader.Token)
                    {
                        case JsonToken.StringLiteral:
                            reader.ReadString();
                            break;

                        case JsonToken.BeginObject:
                            SkipObject(reader);
                            break;
                    }
                    break;

                default:
                    // Array
                    SkipArray(reader, false, true);
                    break;
            }
        }

        private object[] ReadParameters(Socket socket, Subscription subscription, IJsonReader reader, bool isInArray)
        {
            var desc = subscription != null ? subscription.callbacks.FirstOrDefault() : default(CallbackDescriptor);
            int paramCount = desc.ParamTypes != null ? desc.ParamTypes.Length : 0;
            object[] args = null;

            if (paramCount > 0)
            {
                args = new object[paramCount];

                for (int i = 0; i < desc.ParamTypes.Length; ++i)
                {
                    Type type = desc.ParamTypes[i];

                    if (type == typeof(Socket))
                        args[i] = socket;
                    else if (type == typeof(SocketManager))
                        args[i] = socket.Manager;
                    else
                        args[i] = reader.ReadValue(desc.ParamTypes[i]);
                }
            }
            else
            {
                if (isInArray)
                {
                    if (reader.Token != JsonToken.EndOfArray)
                        SkipArray(reader, true, false);
                }
                else
                {
                    SkipObject(reader);
                }
            }

            return args;
        }

        private (string, object[]) ReadData(SocketManager manager, IncomingPacket packet, IJsonReader reader)
        {
            Socket socket = manager.GetSocket(packet.Namespace);

            string eventName = packet.EventName;
            Subscription subscription = socket.GetSubscription(eventName);

            object[] args = null;

            switch (packet.SocketIOEvent)
            {
                case SocketIOEventTypes.Unknown:
                    // TODO: Error?
                    break;

                case SocketIOEventTypes.Connect:
                    // No Data | Object
                    args = ReadParameters(socket, subscription, reader, false);
                    //SkipObject(reader);
                    break;

                case SocketIOEventTypes.Disconnect:
                    // No Data
                    break;

                case SocketIOEventTypes.Error:
                    // String | Object
                    switch(reader.Token)
                    {
                        case JsonToken.StringLiteral:
                            args = new object[] { new Error(reader.ReadString()) };
                            break;

                        case JsonToken.BeginObject:
                            args = ReadParameters(socket, subscription, reader, false);
                            //if (subscription == null && args == null)
                            //    SkipObject(reader);
                            break;
                    }
                    break;

                case SocketIOEventTypes.Ack:
                    eventName = IncomingPacket.GenerateAcknowledgementNameFromId(packet.Id);
                    subscription = socket.GetSubscription(eventName);

                    reader.ReadArrayBegin();

                    args = ReadParameters(socket, subscription, reader, true);

                    reader.ReadArrayEnd();
                    break;

                default:
                    // Array
                    reader.ReadArrayBegin();
                    eventName = reader.ReadString();

                    subscription = socket.GetSubscription(eventName);

                    args = ReadParameters(socket, subscription, reader, true);

                    reader.ReadArrayEnd();
                    break;
            }

            return (eventName, args);
        }

        private void SkipArray(IJsonReader reader, bool alreadyStarted, bool readFinalArrayToken)
        {
            if (!alreadyStarted)
                reader.ReadArrayBegin();

            int arrayBegins = 1;

            while (arrayBegins > 0)
            {
                switch (reader.Token)
                {
                    case JsonToken.BeginArray: arrayBegins++; break;
                    case JsonToken.EndOfArray: arrayBegins--; break;
                }

                if (readFinalArrayToken || arrayBegins >= 1)
                    reader.NextToken();
            }
        }

        private void SkipObject(IJsonReader reader)
        {
            reader.ReadObjectBegin();
            int objectBegins = 1;

            while (objectBegins > 0)
            {
                switch (reader.Token)
                {
                    case JsonToken.BeginObject: objectBegins++; break;
                    case JsonToken.EndOfObject: objectBegins--; break;
                }
                reader.NextToken();
            }
        }

        public OutgoingPacket CreateOutgoing(TransportEventTypes transportEvent, string payload)
        {
            return new OutgoingPacket { Payload = "" + (char)('0' + (byte)transportEvent) + payload };
        }

        public OutgoingPacket CreateOutgoing(Socket socket, SocketIOEventTypes socketIOEvent, int id, string name, object arg)
        {
            return CreateOutgoing(socket, socketIOEvent, id, name, arg != null ? new object[] { arg } : null);
        }

        public OutgoingPacket CreateOutgoing(Socket socket, SocketIOEventTypes socketIOEvent, int id, string name, object[] args)
        {
            var memBufferTargetLength = count == 0 ? 256 : Math.Max(256, (sumLength / count) / 2);

            var memBuffer = BufferPool.Get((long)memBufferTargetLength, true);
            var stream = new BestHTTP.Extensions.BufferPoolMemoryStream(memBuffer, 0, memBuffer.Length, true, true, false, true);

            var buffer = BufferPool.Get(MsgPackWriter.DEFAULT_BUFFER_SIZE, true);

            var context = new SerializationContext
            {
                Options = SerializationOptions.SuppressTypeInformation,
                EnumSerializerFactory = (enumType) => new EnumNumberSerializer(enumType)/*,
                ExtensionTypeHandler = CustomMessagePackExtensionTypeHandler.Instance*/
            };

            var writer = new MsgPackWriter(stream, context, buffer);

            switch (socketIOEvent)
            {
                case SocketIOEventTypes.Connect:
                    // No Data | Object
                    if (args != null && args.Length > 0 && args[0] != null)
                        writer.WriteObjectBegin(id >= 0 ? 4 : 3);
                    else
                        writer.WriteObjectBegin(id >= 0 ? 3 : 2);
                    break;

                case SocketIOEventTypes.Disconnect: writer.WriteObjectBegin(id > 0 ? 3 : 2); break;
                case SocketIOEventTypes.Error: writer.WriteObjectBegin(id > 0 ? 4 : 3); break;
                default: writer.WriteObjectBegin(id >= 0 ? 4 : 3); break;
            }

            writer.WriteMember("type");
            writer.Write((int)socketIOEvent);

            writer.WriteMember("nsp");
            writer.Write(socket.Namespace);

            if (id >= 0)
            {
                writer.WriteMember("id");
                writer.Write(id);
            }

            switch (socketIOEvent)
            {
                case SocketIOEventTypes.Connect:
                    // No Data | Object
                    if (args != null && args.Length > 0 && args[0] != null)
                    {
                        writer.WriteMember("data");
                        writer.WriteValue(args[0], args[0].GetType());
                    }
                    break;

                case SocketIOEventTypes.Disconnect:
                    // No Data
                    break;

                case SocketIOEventTypes.Error:
                    writer.WriteMember("data");

                    // String | Object
                    if (args != null && args.Length > 0)
                        writer.WriteValue(args[0], args[0].GetType());
                    else
                    {
                        writer.WriteObjectBegin(0);
                        writer.WriteObjectEnd();
                    }
                    break;

                default:
                    writer.WriteMember("data");

                    // Array

                    int argCount = (args != null ? args.Length : 0);
                    writer.WriteArrayBegin(!string.IsNullOrEmpty(name) ? 1 + argCount : argCount);

                    if (!string.IsNullOrEmpty(name))
                        writer.Write(name);

                    if (args != null && args.Length > 0)
                    {
                        foreach (var arg in args)
                        {
                            if (arg != null)
                                writer.WriteValue(arg, arg.GetType());
                            else
                                writer.WriteNull();
                        }
                    }

                    writer.WriteArrayEnd();
                    break;
            }

            writer.WriteObjectEnd();

            writer.Flush();

            BufferPool.Release(buffer);

            // get how much bytes got written to the buffer
            int length = (int)stream.Position;

            buffer = stream.GetBuffer();

            sumLength += (ulong)length;
            count++;

            if(sumLength >= int.MaxValue)
            {
                sumLength /= count;
                count = 1;
            }

            return new OutgoingPacket { PayloadData = new BufferSegment(buffer, 0, length) };
        }
    }
}

#endif
