#nullable enable
using System;
using System.Buffers.Binary;
using System.Text;

namespace IGP.Multiplayer.Protocol
{
    [Flags]
    internal enum IGPDataPlaneEnvelopeFlags : byte
    {
        None = 0,
        Json = 1,
        Fragmented = 2,
    }

    internal enum IGPDataPlaneMessageKind : byte
    {
        Custom = 0,
        P2PData = 1,
        StateSet = 2,
        StateGet = 3,
        StateReset = 4,
        SceneGateSet = 5,
        SceneGateGet = 6,
        RpcCall = 7,
        RpcResponse = 8,
        RpcRegister = 9,
        RpcUnregister = 10,
        Ping = 11,
        Pong = 12,
        Error = 13,
        // Values 14-24 are reserved by an unpublished room-control design.
    }

    internal enum IGPDataPlaneRouteKind : byte
    {
        ToHost = 0,
        Others = 1,
        Direct = 2,
        Mask64 = 3,
    }

    internal sealed class IGPDataPlaneFragment
    {
        public byte[] Id { get; set; } = new byte[16];
        public ushort Index { get; set; }
        public ushort Count { get; set; }
        public uint TotalBytes { get; set; }
    }

    internal sealed class IGPDataPlaneEnvelope
    {
        public IGPDataPlaneEnvelopeFlags Flags { get; set; }
        public IGPDataPlaneMessageKind MessageKind { get; set; }
        public string CustomType { get; set; } = string.Empty;
        public IGPDataPlaneRouteKind RouteKind { get; set; }
        public byte SenderSlot { get; set; }
        public byte TargetSlot { get; set; }
        public ulong TargetMask { get; set; }
        public uint ApplicationMessageType { get; set; }
        public IGPDataPlaneFragment? Fragment { get; set; }
        public byte[] Payload { get; set; } = Array.Empty<byte>();
    }

    internal static class IGPDataPlaneEnvelopeCodec
    {
        public const int Version = 1;
        public const byte MaxPlayerSlot = 64;
        private const int BaseHeaderLength = 4;
        private const IGPDataPlaneEnvelopeFlags KnownFlags =
            IGPDataPlaneEnvelopeFlags.Json | IGPDataPlaneEnvelopeFlags.Fragmented;

        public static IGPDataPlaneMessageKind MessageKindForType(string? type, out string customType)
        {
            customType = string.Empty;
            switch (type)
            {
                case "p2p_data": return IGPDataPlaneMessageKind.P2PData;
                case "state_set": return IGPDataPlaneMessageKind.StateSet;
                case "state_get": return IGPDataPlaneMessageKind.StateGet;
                case "state_reset": return IGPDataPlaneMessageKind.StateReset;
                case "scene_gate_set": return IGPDataPlaneMessageKind.SceneGateSet;
                case "scene_gate_get": return IGPDataPlaneMessageKind.SceneGateGet;
                case "rpc_call": return IGPDataPlaneMessageKind.RpcCall;
                case "rpc_response": return IGPDataPlaneMessageKind.RpcResponse;
                case "rpc_register": return IGPDataPlaneMessageKind.RpcRegister;
                case "rpc_unregister": return IGPDataPlaneMessageKind.RpcUnregister;
                case "ping": return IGPDataPlaneMessageKind.Ping;
                case "pong": return IGPDataPlaneMessageKind.Pong;
                case "error": return IGPDataPlaneMessageKind.Error;
                default:
                    customType = type ?? string.Empty;
                    return IGPDataPlaneMessageKind.Custom;
            }
        }

        public static string TypeForMessageKind(IGPDataPlaneMessageKind kind, string customType)
        {
            if (kind == IGPDataPlaneMessageKind.Custom) return customType;
            string[] types = { "", "p2p_data", "state_set", "state_get", "state_reset", "scene_gate_set", "scene_gate_get", "rpc_call", "rpc_response", "rpc_register", "rpc_unregister", "ping", "pong", "error" };
            int value = (int)kind;
            return value >= 0 && value < types.Length ? types[value] : string.Empty;
        }

        public static byte[] Encode(IGPDataPlaneEnvelope envelope)
        {
            Validate(envelope);
            byte[] customType = envelope.MessageKind == IGPDataPlaneMessageKind.Custom
                ? Encoding.UTF8.GetBytes(envelope.CustomType)
                : Array.Empty<byte>();
            int size = BaseHeaderLength + RouteExtensionLength(envelope.RouteKind);
            if (envelope.MessageKind == IGPDataPlaneMessageKind.Custom)
            {
                size += 1 + customType.Length;
            }
            if (envelope.MessageKind == IGPDataPlaneMessageKind.P2PData)
            {
                size += 4 + (envelope.Fragment == null ? 0 : 24);
            }
            byte[] payload = envelope.Payload ?? Array.Empty<byte>();
            byte[] wire = new byte[size + payload.Length];
            wire[0] = (byte)envelope.Flags;
            wire[1] = (byte)envelope.MessageKind;
            wire[2] = (byte)envelope.RouteKind;
            wire[3] = envelope.SenderSlot;
            int offset = BaseHeaderLength;
            if (envelope.RouteKind == IGPDataPlaneRouteKind.Direct)
            {
                wire[offset++] = envelope.TargetSlot;
            }
            else if (envelope.RouteKind == IGPDataPlaneRouteKind.Mask64)
            {
                BinaryPrimitives.WriteUInt64BigEndian(wire.AsSpan(offset, 8), envelope.TargetMask);
                offset += 8;
            }
            if (envelope.MessageKind == IGPDataPlaneMessageKind.Custom)
            {
                wire[offset++] = (byte)customType.Length;
                Buffer.BlockCopy(customType, 0, wire, offset, customType.Length);
                offset += customType.Length;
            }
            if (envelope.MessageKind == IGPDataPlaneMessageKind.P2PData)
            {
                BinaryPrimitives.WriteUInt32BigEndian(wire.AsSpan(offset, 4), envelope.ApplicationMessageType);
                offset += 4;
                if (envelope.Fragment != null)
                {
                    Buffer.BlockCopy(envelope.Fragment.Id, 0, wire, offset, 16);
                    offset += 16;
                    BinaryPrimitives.WriteUInt16BigEndian(wire.AsSpan(offset, 2), envelope.Fragment.Index);
                    offset += 2;
                    BinaryPrimitives.WriteUInt16BigEndian(wire.AsSpan(offset, 2), envelope.Fragment.Count);
                    offset += 2;
                    BinaryPrimitives.WriteUInt32BigEndian(wire.AsSpan(offset, 4), envelope.Fragment.TotalBytes);
                    offset += 4;
                }
            }
            Buffer.BlockCopy(payload, 0, wire, offset, payload.Length);
            return wire;
        }

        public static IGPDataPlaneEnvelope Decode(byte[] wire)
        {
            if (wire == null || wire.Length < BaseHeaderLength)
            {
                throw new InvalidOperationException("DataPlaneEnvelope V1 base header is truncated.");
            }
            var envelope = new IGPDataPlaneEnvelope
            {
                Flags = (IGPDataPlaneEnvelopeFlags)wire[0],
                MessageKind = (IGPDataPlaneMessageKind)wire[1],
                RouteKind = (IGPDataPlaneRouteKind)wire[2],
                SenderSlot = wire[3],
            };
            int offset = BaseHeaderLength;
            if (envelope.RouteKind == IGPDataPlaneRouteKind.Direct)
            {
                Require(wire, offset, 1, "direct route");
                envelope.TargetSlot = wire[offset++];
            }
            else if (envelope.RouteKind == IGPDataPlaneRouteKind.Mask64)
            {
                Require(wire, offset, 8, "target mask");
                envelope.TargetMask = BinaryPrimitives.ReadUInt64BigEndian(wire.AsSpan(offset, 8));
                offset += 8;
            }
            else if (envelope.RouteKind != IGPDataPlaneRouteKind.ToHost &&
                     envelope.RouteKind != IGPDataPlaneRouteKind.Others)
            {
                throw new InvalidOperationException("DataPlaneEnvelope V1 route kind is invalid.");
            }
            if (envelope.MessageKind == IGPDataPlaneMessageKind.Custom)
            {
                Require(wire, offset, 1, "custom type length");
                int length = wire[offset++];
                Require(wire, offset, length, "custom type");
                envelope.CustomType = Encoding.UTF8.GetString(wire, offset, length);
                offset += length;
            }
            else if ((byte)envelope.MessageKind > (byte)IGPDataPlaneMessageKind.Error)
            {
                throw new InvalidOperationException("DataPlaneEnvelope V1 message kind is invalid.");
            }
            if (envelope.MessageKind == IGPDataPlaneMessageKind.P2PData)
            {
                Require(wire, offset, 4, "P2P metadata");
                envelope.ApplicationMessageType = BinaryPrimitives.ReadUInt32BigEndian(wire.AsSpan(offset, 4));
                offset += 4;
                if ((envelope.Flags & IGPDataPlaneEnvelopeFlags.Fragmented) != 0)
                {
                    Require(wire, offset, 24, "fragment metadata");
                    var fragment = new IGPDataPlaneFragment();
                    Buffer.BlockCopy(wire, offset, fragment.Id, 0, 16);
                    offset += 16;
                    fragment.Index = BinaryPrimitives.ReadUInt16BigEndian(wire.AsSpan(offset, 2));
                    offset += 2;
                    fragment.Count = BinaryPrimitives.ReadUInt16BigEndian(wire.AsSpan(offset, 2));
                    offset += 2;
                    fragment.TotalBytes = BinaryPrimitives.ReadUInt32BigEndian(wire.AsSpan(offset, 4));
                    offset += 4;
                    envelope.Fragment = fragment;
                }
            }
            envelope.Payload = new byte[wire.Length - offset];
            Buffer.BlockCopy(wire, offset, envelope.Payload, 0, envelope.Payload.Length);
            Validate(envelope);
            return envelope;
        }

        private static void Validate(IGPDataPlaneEnvelope envelope)
        {
            if (envelope == null || (envelope.Flags & ~KnownFlags) != 0 || envelope.SenderSlot > MaxPlayerSlot)
            {
                throw new ArgumentException("DataPlaneEnvelope V1 header is invalid.", nameof(envelope));
            }
            if ((envelope.RouteKind == IGPDataPlaneRouteKind.ToHost || envelope.RouteKind == IGPDataPlaneRouteKind.Others) &&
                (envelope.TargetSlot != 0 || envelope.TargetMask != 0) ||
                envelope.RouteKind == IGPDataPlaneRouteKind.Direct &&
                (envelope.TargetSlot == 0 || envelope.TargetSlot > MaxPlayerSlot || envelope.TargetMask != 0) ||
                envelope.RouteKind == IGPDataPlaneRouteKind.Mask64 &&
                (envelope.TargetSlot != 0 || envelope.TargetMask == 0) ||
                !Enum.IsDefined(typeof(IGPDataPlaneRouteKind), envelope.RouteKind))
            {
                throw new ArgumentException("DataPlaneEnvelope V1 route is invalid.", nameof(envelope));
            }
            if (envelope.MessageKind == IGPDataPlaneMessageKind.Custom)
            {
                int length = Encoding.UTF8.GetByteCount(envelope.CustomType ?? string.Empty);
                if (length == 0 || length > byte.MaxValue)
                {
                    throw new ArgumentException("DataPlaneEnvelope V1 custom type is invalid.", nameof(envelope));
                }
            }
            else if ((byte)envelope.MessageKind > (byte)IGPDataPlaneMessageKind.Error)
            {
                throw new ArgumentException("DataPlaneEnvelope V1 message kind is invalid.", nameof(envelope));
            }
            bool fragmented = (envelope.Flags & IGPDataPlaneEnvelopeFlags.Fragmented) != 0;
            if (fragmented != (envelope.Fragment != null) || fragmented && envelope.MessageKind != IGPDataPlaneMessageKind.P2PData)
            {
                throw new ArgumentException("DataPlaneEnvelope V1 fragment metadata is inconsistent.", nameof(envelope));
            }
            if (envelope.Fragment != null &&
                (envelope.Fragment.Id == null || envelope.Fragment.Id.Length != 16 || envelope.Fragment.Count == 0 ||
                 envelope.Fragment.Index >= envelope.Fragment.Count || envelope.Fragment.TotalBytes == 0))
            {
                throw new ArgumentException("DataPlaneEnvelope V1 fragment metadata is invalid.", nameof(envelope));
            }
        }

        private static int RouteExtensionLength(IGPDataPlaneRouteKind route) =>
            route == IGPDataPlaneRouteKind.Direct ? 1 : route == IGPDataPlaneRouteKind.Mask64 ? 8 : 0;

        private static void Require(byte[] wire, int offset, int length, string field)
        {
            if (length <= 0 || offset < 0 || offset > wire.Length - length)
            {
                throw new InvalidOperationException($"DataPlaneEnvelope V1 {field} is truncated.");
            }
        }
    }
}
