using System;
using System.Collections.Generic;
using System.Text;

using BootstrapProto = IGP.UnitySDK.Generated.IGPProtoContracts.ArenaHostBootstrap;
using IGP.UnitySDK.Models;

namespace IGP.UnitySDK
{
    internal static class IGPHostBridgeProtocol
    {
        private const int WireTypeVarint = 0;
        private const int WireTypeLengthDelimited = 2;

        public static byte[] EncodeRedeemLaunchTicketRequest(string ticket, string secret)
        {
            var requestBytes = new List<byte>();
            WriteStringField(requestBytes, BootstrapProto.RedeemLaunchTicketRequest.Ticket, ticket);
            WriteStringField(requestBytes, BootstrapProto.RedeemLaunchTicketRequest.Secret, secret);

            var envelopeBytes = new List<byte>();
            WriteMessageField(envelopeBytes, BootstrapProto.BootstrapRequest.RedeemLaunchTicket, requestBytes.ToArray());
            return envelopeBytes.ToArray();
        }

        public static LaunchTicketRedeemResponse DecodeBootstrapResponse(byte[] payload)
        {
            if (payload == null || payload.Length == 0)
            {
                throw new IGPSDKException("Empty bootstrap response");
            }

            var offset = 0;
            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case BootstrapProto.BootstrapResponse.RedeemLaunchTicket:
                    {
                        var redeemBytes = ReadLengthDelimited(payload, ref offset);
                        var response = DecodeRedeemLaunchTicketResponse(redeemBytes);
                        response.success = true;
                        return response;
                    }
                    case BootstrapProto.BootstrapResponse.Error:
                    {
                        var errorBytes = ReadLengthDelimited(payload, ref offset);
                        var error = DecodeBootstrapError(errorBytes);
                        throw new IGPSDKException($"Host bridge error ({error.code}): {error.message}");
                    }
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            throw new IGPSDKException("Unsupported bootstrap response payload");
        }

        private static LaunchTicketRedeemResponse DecodeRedeemLaunchTicketResponse(byte[] payload)
        {
            var response = new LaunchTicketRedeemResponse();
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case BootstrapProto.RedeemLaunchTicketResponse.RoomId:
                        response.roomId = ReadString(payload, ref offset);
                        break;
                    case BootstrapProto.RedeemLaunchTicketResponse.RoomCode:
                        response.roomCode = ReadString(payload, ref offset);
                        break;
                    case BootstrapProto.RedeemLaunchTicketResponse.PlayerId:
                        response.playerId = ReadString(payload, ref offset);
                        break;
                    case BootstrapProto.RedeemLaunchTicketResponse.Player:
                        response.player = DecodePlayer(ReadLengthDelimited(payload, ref offset));
                        break;
                    case BootstrapProto.RedeemLaunchTicketResponse.Token:
                        response.token = ReadString(payload, ref offset);
                        break;
                    case BootstrapProto.RedeemLaunchTicketResponse.ExpiresAt:
                        response.expiresAt = ReadString(payload, ref offset);
                        break;
                    case BootstrapProto.RedeemLaunchTicketResponse.ServerUrl:
                        response.serverUrl = ReadString(payload, ref offset);
                        break;
                    case BootstrapProto.RedeemLaunchTicketResponse.SessionEndpoint:
                        response.sessionEndpoint = ReadString(payload, ref offset);
                        break;
                    case BootstrapProto.RedeemLaunchTicketResponse.SessionSecret:
                        response.sessionSecret = ReadString(payload, ref offset);
                        break;
                    case BootstrapProto.RedeemLaunchTicketResponse.ReliableMessageMaxBytes:
                        response.reliableMessageMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    case BootstrapProto.RedeemLaunchTicketResponse.ReliableChunkMaxBytes:
                        response.reliableChunkMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    case BootstrapProto.RedeemLaunchTicketResponse.KcpDataPlanePayloadMaxBytes:
                        response.kcpDataPlanePayloadMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    case BootstrapProto.RedeemLaunchTicketResponse.KcpFrameMaxBytes:
                        response.kcpFrameMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return response;
        }

        private static Player DecodePlayer(byte[] payload)
        {
            var player = new Player();
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case BootstrapProto.BootstrapPlayer.Id:
                        player.id = ReadString(payload, ref offset);
                        break;
                    case BootstrapProto.BootstrapPlayer.Name:
                        player.name = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return player;
        }

        private static (string code, string message) DecodeBootstrapError(byte[] payload)
        {
            string code = string.Empty;
            string message = string.Empty;
            var offset = 0;

            while (offset < payload.Length)
            {
                var tag = ReadVarint(payload, ref offset);
                var fieldNumber = tag >> 3;
                var wireType = tag & 0x07;

                switch (fieldNumber)
                {
                    case BootstrapProto.BootstrapError.Code:
                        code = ReadString(payload, ref offset);
                        break;
                    case BootstrapProto.BootstrapError.Message:
                        message = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return (code, message);
        }

        private static void WriteStringField(List<byte> buffer, int fieldNumber, string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                return;
            }

            WriteTag(buffer, fieldNumber, WireTypeLengthDelimited);
            var valueBytes = Encoding.UTF8.GetBytes(value);
            WriteVarint(buffer, (uint)valueBytes.Length);
            buffer.AddRange(valueBytes);
        }

        private static void WriteMessageField(List<byte> buffer, int fieldNumber, byte[] value)
        {
            if (value == null || value.Length == 0)
            {
                return;
            }

            WriteTag(buffer, fieldNumber, WireTypeLengthDelimited);
            WriteVarint(buffer, (uint)value.Length);
            buffer.AddRange(value);
        }

        private static void WriteTag(List<byte> buffer, int fieldNumber, int wireType)
        {
            WriteVarint(buffer, (uint)((fieldNumber << 3) | wireType));
        }

        private static void WriteVarint(List<byte> buffer, uint value)
        {
            var current = value;
            while (current >= 0x80)
            {
                buffer.Add((byte)((current & 0x7F) | 0x80));
                current >>= 7;
            }

            buffer.Add((byte)current);
        }

        private static int ReadVarint(byte[] buffer, ref int offset)
        {
            var value = 0;
            var shift = 0;

            while (offset < buffer.Length)
            {
                var current = buffer[offset++];
                value |= (current & 0x7F) << shift;
                if ((current & 0x80) == 0)
                {
                    return value;
                }

                shift += 7;
            }

            throw new IGPSDKException("Invalid protobuf varint");
        }

        private static byte[] ReadLengthDelimited(byte[] buffer, ref int offset)
        {
            var length = ReadVarint(buffer, ref offset);
            if (length < 0 || offset + length > buffer.Length)
            {
                throw new IGPSDKException("Invalid protobuf length-delimited field");
            }

            var result = new byte[length];
            Buffer.BlockCopy(buffer, offset, result, 0, length);
            offset += length;
            return result;
        }

        private static string ReadString(byte[] buffer, ref int offset)
        {
            var bytes = ReadLengthDelimited(buffer, ref offset);
            return Encoding.UTF8.GetString(bytes);
        }

        private static void SkipField(byte[] buffer, int wireType, ref int offset)
        {
            switch (wireType)
            {
                case WireTypeVarint:
                    ReadVarint(buffer, ref offset);
                    return;
                case WireTypeLengthDelimited:
                    ReadLengthDelimited(buffer, ref offset);
                    return;
                default:
                    throw new IGPSDKException($"Unsupported protobuf wire type: {wireType}");
            }
        }
    }
}
