#nullable enable
using System;
using System.Collections.Generic;
using System.Text;

using SessionProto = IGP.UnitySDK.Generated.IGPProtoContracts.ArenaHostSession;
using IGP.UnitySDK.Models;

namespace IGP.UnitySDK
{
    internal enum IGPHostSessionCommandType
    {
        SetReady = (int)SessionProto.SessionCommandType.SetReady,
        LeaveRoom = (int)SessionProto.SessionCommandType.LeaveRoom,
        StartGame = (int)SessionProto.SessionCommandType.StartGame,
        FinishGame = (int)SessionProto.SessionCommandType.FinishGame,
        RematchGame = (int)SessionProto.SessionCommandType.RematchGame,
        ChangeTeam = (int)SessionProto.SessionCommandType.ChangeTeam,
        RefreshRoom = (int)SessionProto.SessionCommandType.RefreshRoom,
        SendMessage = (int)SessionProto.SessionCommandType.SendArenaMessage,
        RequestDataPlane = (int)SessionProto.SessionCommandType.RequestDataPlane,
        SendPing = (int)SessionProto.SessionCommandType.SendPing,
        SetState = (int)SessionProto.SessionCommandType.SetState,
        GetState = (int)SessionProto.SessionCommandType.GetState,
        ResetState = (int)SessionProto.SessionCommandType.ResetState,
        RegisterRpc = (int)SessionProto.SessionCommandType.RegisterRpc,
        UnregisterRpc = (int)SessionProto.SessionCommandType.UnregisterRpc,
        CallRpc = (int)SessionProto.SessionCommandType.CallRpc,
        UnlockAchievement = (int)SessionProto.SessionCommandType.UnlockAchievement,
        ReportAchievementProgress = (int)SessionProto.SessionCommandType.ReportAchievementProgress,
        ClearAchievements = (int)SessionProto.SessionCommandType.ClearAchievements,
    }

    internal enum IGPHostSessionServerMessageType
    {
        Attached,
        RoomSnapshot,
        CommandResult,
        Message,
        RoomEvent,
        Detached,
        Error,
        DataPlane,
    }

    public enum IGPHostedDataPlaneMode
    {
        Unspecified = (int)SessionProto.SessionDataPlaneMode.Unspecified,
        DirectKcp = (int)SessionProto.SessionDataPlaneMode.DirectKcp,
    }

    public enum IGPHostSessionRoomEventType
    {
        PlayerJoined = (int)SessionProto.SessionRoomEventType.PlayerJoined,
        PlayerLeft = (int)SessionProto.SessionRoomEventType.PlayerLeft,
        HostTransferred = (int)SessionProto.SessionRoomEventType.HostTransferred,
        GameStarted = (int)SessionProto.SessionRoomEventType.GameStarted,
        GameEnded = (int)SessionProto.SessionRoomEventType.GameEnded,
    }

    internal sealed class IGPHostSessionAttachedResponse
    {
        public string RoomId { get; set; } = string.Empty;
        public string PlayerId { get; set; } = string.Empty;
    }

    public sealed class IGPHostSessionCommandResult
    {
        public string RequestId { get; set; } = string.Empty;
        public bool Success { get; set; }
        public string Message { get; set; } = string.Empty;
        public string ContentJson { get; set; } = string.Empty;
    }

    internal sealed class IGPHostSessionMessageEvent
    {
        public string MessageType { get; set; } = string.Empty;
        public string RoomId { get; set; } = string.Empty;
        public string PlayerId { get; set; } = string.Empty;
        public string TargetPlayerId { get; set; } = string.Empty;
        public string ContentJson { get; set; } = string.Empty;
        public bool? Reliable { get; set; }
        public string Timestamp { get; set; } = string.Empty;
    }

    public sealed class IGPHostSessionDataPlaneResponse
    {
        public string RequestId { get; set; } = string.Empty;
        public bool Success { get; set; }
        public string Message { get; set; } = string.Empty;
        public IGPHostSessionDataPlaneDescriptor? DataPlane { get; set; }
    }

    public sealed class IGPHostSessionDataPlaneDescriptor
    {
        public IGPHostedDataPlaneMode Mode { get; set; } = IGPHostedDataPlaneMode.Unspecified;
        public string Token { get; set; } = string.Empty;
        public string Host { get; set; } = string.Empty;
        public uint Port { get; set; }
        public ulong ExpiresAtUnixMs { get; set; }
        public int? reliableMessageMaxBytes { get; set; }
        public int? reliableChunkMaxBytes { get; set; }
        public int? kcpDataPlanePayloadMaxBytes { get; set; }
        public int? kcpFrameMaxBytes { get; set; }
    }

    public sealed class IGPHostSessionRoomEvent
    {
        public IGPHostSessionRoomEventType EventType { get; set; }
        public string RoomId { get; set; } = string.Empty;
        public Player? Player { get; set; }
        public string PreviousHostId { get; set; } = string.Empty;
        public string CurrentHostId { get; set; } = string.Empty;
        public string PreviousStatus { get; set; } = string.Empty;
        public string CurrentStatus { get; set; } = string.Empty;
    }

    internal sealed class IGPHostSessionDetachedEvent
    {
        public string Reason { get; set; } = string.Empty;
    }

    internal sealed class IGPHostSessionErrorResponse
    {
        public string Code { get; set; } = string.Empty;
        public string Message { get; set; } = string.Empty;
    }

    public sealed class IGPHostSessionSnapshotEvent
    {
        public string ConnectionStatus { get; set; } = "disconnected";
        public Room? Room { get; set; }
        public Player? CurrentPlayer { get; set; }
    }

    internal sealed class IGPHostSessionServerMessage
    {
        public IGPHostSessionServerMessageType Type { get; set; }
        public IGPHostSessionAttachedResponse? Attached { get; set; }
        public IGPHostSessionSnapshotEvent? RoomSnapshot { get; set; }
        public IGPHostSessionCommandResult? CommandResult { get; set; }
        public IGPHostSessionMessageEvent? Message { get; set; }
        public IGPHostSessionDataPlaneResponse? DataPlane { get; set; }
        public IGPHostSessionRoomEvent? RoomEvent { get; set; }
        public IGPHostSessionDetachedEvent? Detached { get; set; }
        public IGPHostSessionErrorResponse? Error { get; set; }
    }

    internal static class IGPHostSessionProtocol
    {
        private const int WireTypeVarint = 0;
        private const int WireTypeLengthDelimited = 2;

        public static byte[] EncodeAttachRequest(string secret, string roomId, string playerId)
        {
            var attachBytes = new List<byte>();
            WriteStringField(attachBytes, SessionProto.SessionAttachRequest.Secret, secret);
            WriteStringField(attachBytes, SessionProto.SessionAttachRequest.RoomId, roomId);
            WriteStringField(attachBytes, SessionProto.SessionAttachRequest.PlayerId, playerId);

            var envelope = new List<byte>();
            WriteMessageField(envelope, SessionProto.SessionClientMessage.Attach, attachBytes.ToArray());
            return envelope.ToArray();
        }

        public static byte[] EncodeCommandRequest(
            string requestId,
            IGPHostSessionCommandType command,
            bool? boolValue = null,
            string? stringValue = null,
            string? messageType = null,
            string? targetPlayerId = null,
            string? contentJson = null,
            bool? reliable = null,
            string? timestamp = null,
            string? scope = null,
            string? key = null,
            string? rpcName = null,
            string? mode = null)
        {
            var commandBytes = new List<byte>();
            WriteStringField(commandBytes, SessionProto.SessionCommandRequest.RequestId, requestId);
            WriteVarintField(commandBytes, SessionProto.SessionCommandRequest.Command, (uint)command);
            if (boolValue.HasValue)
            {
                WriteVarintField(commandBytes, SessionProto.SessionCommandRequest.BoolValue, boolValue.Value ? 1u : 0u);
            }

            WriteStringField(commandBytes, SessionProto.SessionCommandRequest.StringValue, stringValue);
            WriteStringField(commandBytes, SessionProto.SessionCommandRequest.MessageType, messageType);
            WriteStringField(commandBytes, SessionProto.SessionCommandRequest.TargetPlayerId, targetPlayerId);
            WriteStringField(commandBytes, SessionProto.SessionCommandRequest.ContentJson, contentJson);
            if (reliable.HasValue)
            {
                WriteVarintField(commandBytes, SessionProto.SessionCommandRequest.Reliable, reliable.Value ? 1u : 0u);
            }
            WriteStringField(commandBytes, SessionProto.SessionCommandRequest.Timestamp, timestamp);
            WriteStringField(commandBytes, SessionProto.SessionCommandRequest.Scope, scope);
            WriteStringField(commandBytes, SessionProto.SessionCommandRequest.Key, key);
            WriteStringField(commandBytes, SessionProto.SessionCommandRequest.RpcName, rpcName);
            WriteStringField(commandBytes, SessionProto.SessionCommandRequest.Mode, mode);

            var envelope = new List<byte>();
            WriteMessageField(envelope, SessionProto.SessionClientMessage.Command, commandBytes.ToArray());
            return envelope.ToArray();
        }

        public static IGPHostSessionServerMessage DecodeServerMessage(byte[] payload)
        {
            if (payload == null || payload.Length == 0)
            {
                throw new IGPSDKException("Empty host session 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 SessionProto.SessionServerMessage.Attached:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.Attached,
                            Attached = DecodeAttached(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.RoomSnapshot:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.RoomSnapshot,
                            RoomSnapshot = DecodeRoomSnapshot(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.CommandResult:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.CommandResult,
                            CommandResult = DecodeCommandResult(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.ArenaMessage:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.Message,
                            Message = DecodeMessage(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.RoomEvent:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.RoomEvent,
                            RoomEvent = DecodeRoomEvent(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.Detached:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.Detached,
                            Detached = DecodeDetached(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.Error:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.Error,
                            Error = DecodeError(ReadLengthDelimited(payload, ref offset)),
                        };
                    case SessionProto.SessionServerMessage.DataPlane:
                        return new IGPHostSessionServerMessage
                        {
                            Type = IGPHostSessionServerMessageType.DataPlane,
                            DataPlane = DecodeDataPlaneResponse(ReadLengthDelimited(payload, ref offset)),
                        };
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            throw new IGPSDKException("Unsupported host session response payload");
        }

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

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

                switch (fieldNumber)
                {
                    case SessionProto.SessionAttachedResponse.RoomId:
                        response.RoomId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionAttachedResponse.PlayerId:
                        response.PlayerId = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return response;
        }

        private static IGPHostSessionSnapshotEvent DecodeRoomSnapshot(byte[] payload)
        {
            var snapshot = new IGPHostSessionSnapshotEvent();
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case SessionProto.SessionRoomSnapshotEvent.ConnectionStatus:
                        snapshot.ConnectionStatus = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionRoomSnapshotEvent.Room:
                        snapshot.Room = DecodeRoom(ReadLengthDelimited(payload, ref offset));
                        break;
                    case SessionProto.SessionRoomSnapshotEvent.CurrentPlayer:
                        snapshot.CurrentPlayer = DecodePlayer(ReadLengthDelimited(payload, ref offset));
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return snapshot;
        }

        private static IGPHostSessionCommandResult DecodeCommandResult(byte[] payload)
        {
            var result = new IGPHostSessionCommandResult();
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case SessionProto.SessionCommandResponse.RequestId:
                        result.RequestId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionCommandResponse.Success:
                        result.Success = ReadVarint(payload, ref offset) == 1;
                        break;
                    case SessionProto.SessionCommandResponse.Message:
                        result.Message = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionCommandResponse.ContentJson:
                        result.ContentJson = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static IGPHostSessionDetachedEvent DecodeDetached(byte[] payload)
        {
            var result = new IGPHostSessionDetachedEvent();
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case SessionProto.SessionDetachedEvent.Reason:
                        result.Reason = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static IGPHostSessionRoomEvent DecodeRoomEvent(byte[] payload)
        {
            var result = new IGPHostSessionRoomEvent();
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case SessionProto.SessionRoomEvent.EventType:
                        result.EventType = (IGPHostSessionRoomEventType)ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionRoomEvent.RoomId:
                        result.RoomId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionRoomEvent.Player:
                        result.Player = DecodePlayer(ReadLengthDelimited(payload, ref offset));
                        break;
                    case SessionProto.SessionRoomEvent.PreviousHostId:
                        result.PreviousHostId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionRoomEvent.CurrentHostId:
                        result.CurrentHostId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionRoomEvent.PreviousStatus:
                        result.PreviousStatus = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionRoomEvent.CurrentStatus:
                        result.CurrentStatus = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static IGPHostSessionMessageEvent DecodeMessage(byte[] payload)
        {
            var result = new IGPHostSessionMessageEvent();
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case SessionProto.SessionArenaMessageEvent.MessageType:
                        result.MessageType = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionArenaMessageEvent.RoomId:
                        result.RoomId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionArenaMessageEvent.PlayerId:
                        result.PlayerId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionArenaMessageEvent.TargetPlayerId:
                        result.TargetPlayerId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionArenaMessageEvent.ContentJson:
                        result.ContentJson = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionArenaMessageEvent.Reliable:
                        result.Reliable = ReadVarint(payload, ref offset) == 1;
                        break;
                    case SessionProto.SessionArenaMessageEvent.Timestamp:
                        result.Timestamp = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static IGPHostSessionDataPlaneResponse DecodeDataPlaneResponse(byte[] payload)
        {
            var result = new IGPHostSessionDataPlaneResponse();
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case SessionProto.SessionDataPlaneResponse.RequestId:
                        result.RequestId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneResponse.Success:
                        result.Success = ReadVarint(payload, ref offset) == 1;
                        break;
                    case SessionProto.SessionDataPlaneResponse.Message:
                        result.Message = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneResponse.Descriptor:
                        result.DataPlane = DecodeDataPlaneDescriptor(ReadLengthDelimited(payload, ref offset));
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static IGPHostSessionDataPlaneDescriptor DecodeDataPlaneDescriptor(byte[] payload)
        {
            var result = new IGPHostSessionDataPlaneDescriptor();
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case SessionProto.SessionDataPlaneDescriptor.Mode:
                        result.Mode = (IGPHostedDataPlaneMode)ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.Host:
                        result.Host = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.Port:
                        result.Port = (uint)ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.Token:
                        result.Token = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.ExpiresAtUnixMs:
                        result.ExpiresAtUnixMs = (ulong)ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.ReliableMessageMaxBytes:
                        result.reliableMessageMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.ReliableChunkMaxBytes:
                        result.reliableChunkMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.KcpDataPlanePayloadMaxBytes:
                        result.kcpDataPlanePayloadMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.SessionDataPlaneDescriptor.KcpFrameMaxBytes:
                        result.kcpFrameMaxBytes = ReadVarint(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static IGPHostSessionErrorResponse DecodeError(byte[] payload)
        {
            var result = new IGPHostSessionErrorResponse();
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case SessionProto.SessionError.Code:
                        result.Code = ReadString(payload, ref offset);
                        break;
                    case SessionProto.SessionError.Message:
                        result.Message = ReadString(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return result;
        }

        private static Room DecodeRoom(byte[] payload)
        {
            var room = new Room
            {
                players = new Dictionary<string, Player>(),
            };
            var teams = new List<Team>();
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case SessionProto.RoomSnapshot.Id:
                        room.id = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.Code:
                        room.code = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.Name:
                        room.name = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.GameId:
                        room.gameId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.Status:
                        room.status = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.HostId:
                        room.hostId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.MaxPlayers:
                        room.maxPlayers = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.MapPublicId:
                        room.mapPublicId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.MapVersionId:
                        room.mapVersionId = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.RoomSnapshot.Players:
                    {
                        var player = DecodePlayer(ReadLengthDelimited(payload, ref offset));
                        if (!string.IsNullOrEmpty(player.id))
                        {
                            room.players[player.id] = player;
                        }
                        break;
                    }
                    case SessionProto.RoomSnapshot.Teams:
                        teams.Add(DecodeTeam(ReadLengthDelimited(payload, ref offset)));
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            room.teams = teams.ToArray();
            return room;
        }

        private static Player DecodePlayer(byte[] payload)
        {
            var player = new Player
            {
                state = new Dictionary<string, object>(),
            };
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case SessionProto.PlayerSnapshot.Id:
                        player.id = ReadString(payload, ref offset);
                        break;
                    case SessionProto.PlayerSnapshot.Name:
                        player.name = ReadString(payload, ref offset);
                        break;
                    case SessionProto.PlayerSnapshot.IsReady:
                        player.state["isReady"] = ReadVarint(payload, ref offset) == 1;
                        break;
                    case SessionProto.PlayerSnapshot.TeamId:
                        player.teamId = ReadString(payload, ref offset);
                        break;
                    case SessionProto.PlayerSnapshot.IsBot:
                        player.isBot = ReadVarint(payload, ref offset) == 1;
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return player;
        }

        private static Team DecodeTeam(byte[] payload)
        {
            var team = new Team();
            var offset = 0;

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

                switch (fieldNumber)
                {
                    case SessionProto.TeamSnapshot.Id:
                        team.id = ReadString(payload, ref offset);
                        break;
                    case SessionProto.TeamSnapshot.Name:
                        team.name = ReadString(payload, ref offset);
                        break;
                    case SessionProto.TeamSnapshot.Color:
                        team.color = ReadString(payload, ref offset);
                        break;
                    case SessionProto.TeamSnapshot.MaxPlayers:
                        team.maxPlayers = ReadVarint(payload, ref offset);
                        break;
                    case SessionProto.TeamSnapshot.PlayerCount:
                        team.playerCount = ReadVarint(payload, ref offset);
                        break;
                    default:
                        SkipField(payload, wireType, ref offset);
                        break;
                }
            }

            return team;
        }

        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 WriteVarintField(List<byte> buffer, int fieldNumber, uint value)
        {
            WriteTag(buffer, fieldNumber, WireTypeVarint);
            WriteVarint(buffer, value);
        }

        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}");
            }
        }
    }
}
