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

namespace IGP.UnitySDK.Protocol
{
    internal enum IGPKcpTargetKind : byte
    {
        Broadcast = 0,
        Player = 1,
    }

    internal sealed class IGPKcpBinaryEnvelope
    {
        public IGPKcpBinaryEnvelope(
            byte version,
            byte flags,
            uint messageType,
            IGPKcpTargetKind targetKind,
            string senderPlayerId,
            string targetPlayerId,
            byte[] payload,
            string? reliableMessageId = null,
            int? reliableChunkIndex = null,
            int? reliableChunkCount = null,
            int? reliableTotalBytes = null,
            uint? reliableMessageType = null)
        {
            Version = version;
            Flags = flags;
            MessageType = messageType;
            TargetKind = targetKind;
            SenderPlayerId = senderPlayerId ?? string.Empty;
            TargetPlayerId = targetPlayerId ?? string.Empty;
            Payload = payload ?? throw new ArgumentNullException(nameof(payload));
            ReliableMessageId = reliableMessageId ?? string.Empty;
            ReliableChunkIndex = reliableChunkIndex;
            ReliableChunkCount = reliableChunkCount;
            ReliableTotalBytes = reliableTotalBytes;
            ReliableMessageType = reliableMessageType;
        }

        public byte Version { get; }
        public byte Flags { get; }
        public uint MessageType { get; }
        public IGPKcpTargetKind TargetKind { get; }
        public string SenderPlayerId { get; }
        public string TargetPlayerId { get; }
        public byte[] Payload { get; }
        public string ReliableMessageId { get; }
        public int? ReliableChunkIndex { get; }
        public int? ReliableChunkCount { get; }
        public int? ReliableTotalBytes { get; }
        public uint? ReliableMessageType { get; }
        public bool IsReliableChunk =>
            !string.IsNullOrWhiteSpace(ReliableMessageId) &&
            ReliableChunkIndex.HasValue &&
            ReliableChunkCount.HasValue &&
            ReliableTotalBytes.HasValue &&
            ReliableMessageType.HasValue;
    }

    internal static class IGPKcpBinaryEnvelopeCodec
    {
        public const byte Version1 = 1;
        public const byte Version2 = 2;
        public const byte ReliableChunkFlag = 1;
        private const int Version1HeaderLength = 1 + 1 + 4 + 1 + 2 + 2 + 4;
        private const int Version2HeaderLength = 1 + 1 + 4 + 1 + 2 + 2 + 2 + 4 + 4 + 4 + 4 + 4;

        public static byte[] Encode(IGPKcpBinaryEnvelope envelope, IGPReliableTransportOptions? options = null)
        {
            if (envelope == null)
            {
                throw new ArgumentNullException(nameof(envelope));
            }

            var resolvedOptions = options ?? IGPReliableTransportOptions.Default;
            switch (envelope.Version)
            {
                case Version1:
                    return EncodeVersion1(envelope, resolvedOptions);
                case Version2:
                    return EncodeVersion2(envelope, resolvedOptions);
                default:
                    throw new InvalidOperationException($"KCP binary envelope version {envelope.Version} is not supported.");
            }
        }

        private static byte[] EncodeVersion1(IGPKcpBinaryEnvelope envelope, IGPReliableTransportOptions options)
        {
            if (envelope.IsReliableChunk)
            {
                throw new InvalidOperationException("KCP binary envelope v1 cannot carry reliable chunk metadata.");
            }

            if (envelope.Payload.Length > options.KcpDataPlanePayloadMaxBytes)
            {
                throw new ArgumentOutOfRangeException(nameof(envelope),
                    $"KCP payload exceeds KcpDataPlanePayloadMaxBytes={options.KcpDataPlanePayloadMaxBytes}.");
            }

            byte[] senderBytes = Encoding.UTF8.GetBytes(envelope.SenderPlayerId);
            byte[] targetBytes = Encoding.UTF8.GetBytes(envelope.TargetPlayerId);
            if (senderBytes.Length > ushort.MaxValue)
            {
                throw new ArgumentOutOfRangeException(nameof(envelope), "KCP sender player id is too large.");
            }

            if (targetBytes.Length > ushort.MaxValue)
            {
                throw new ArgumentOutOfRangeException(nameof(envelope), "KCP target player id is too large.");
            }

            var buffer = new byte[Version1HeaderLength + senderBytes.Length + targetBytes.Length + envelope.Payload.Length];
            buffer[0] = envelope.Version;
            buffer[1] = envelope.Flags;
            BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(2, 4), envelope.MessageType);
            buffer[6] = (byte)envelope.TargetKind;
            BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(7, 2), (ushort)senderBytes.Length);
            BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(9, 2), (ushort)targetBytes.Length);
            BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(11, 4), (uint)envelope.Payload.Length);

            int cursor = Version1HeaderLength;
            if (senderBytes.Length > 0)
            {
                Buffer.BlockCopy(senderBytes, 0, buffer, cursor, senderBytes.Length);
                cursor += senderBytes.Length;
            }

            if (targetBytes.Length > 0)
            {
                Buffer.BlockCopy(targetBytes, 0, buffer, cursor, targetBytes.Length);
                cursor += targetBytes.Length;
            }

            if (envelope.Payload.Length > 0)
            {
                Buffer.BlockCopy(envelope.Payload, 0, buffer, cursor, envelope.Payload.Length);
            }

            return buffer;
        }

        private static byte[] EncodeVersion2(IGPKcpBinaryEnvelope envelope, IGPReliableTransportOptions options)
        {
            if (!envelope.IsReliableChunk)
            {
                throw new InvalidOperationException("KCP binary envelope v2 requires reliable chunk metadata.");
            }

            if (envelope.Payload.Length <= 0 || envelope.Payload.Length > options.ReliableChunkMaxBytes)
            {
                throw new ArgumentOutOfRangeException(nameof(envelope),
                    $"KCP reliable chunk payload exceeds ReliableChunkMaxBytes={options.ReliableChunkMaxBytes}.");
            }

            int chunkIndex = envelope.ReliableChunkIndex!.Value;
            int chunkCount = envelope.ReliableChunkCount!.Value;
            int totalBytes = envelope.ReliableTotalBytes!.Value;
            if (chunkIndex < 0 || chunkCount <= 0 || chunkIndex >= chunkCount || totalBytes <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(envelope), "Reliable chunk metadata is invalid.");
            }

            byte[] senderBytes = Encoding.UTF8.GetBytes(envelope.SenderPlayerId);
            byte[] targetBytes = Encoding.UTF8.GetBytes(envelope.TargetPlayerId);
            byte[] messageIdBytes = Encoding.UTF8.GetBytes(envelope.ReliableMessageId);
            if (senderBytes.Length > ushort.MaxValue)
            {
                throw new ArgumentOutOfRangeException(nameof(envelope), "KCP sender player id is too large.");
            }

            if (targetBytes.Length > ushort.MaxValue)
            {
                throw new ArgumentOutOfRangeException(nameof(envelope), "KCP target player id is too large.");
            }

            if (messageIdBytes.Length == 0 || messageIdBytes.Length > ushort.MaxValue)
            {
                throw new ArgumentOutOfRangeException(nameof(envelope), "KCP reliable message id is invalid.");
            }

            int totalLength = Version2HeaderLength +
                              senderBytes.Length +
                              targetBytes.Length +
                              messageIdBytes.Length +
                              envelope.Payload.Length;
            if (totalLength > options.KcpFrameMaxBytes)
            {
                throw new ArgumentOutOfRangeException(nameof(envelope),
                    $"KCP binary reliable chunk exceeds KcpFrameMaxBytes={options.KcpFrameMaxBytes}.");
            }

            var buffer = new byte[totalLength];
            buffer[0] = Version2;
            buffer[1] = (byte)(envelope.Flags | ReliableChunkFlag);
            BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(2, 4), envelope.MessageType);
            buffer[6] = (byte)envelope.TargetKind;
            BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(7, 2), (ushort)senderBytes.Length);
            BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(9, 2), (ushort)targetBytes.Length);
            BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(11, 2), (ushort)messageIdBytes.Length);
            BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(13, 4), (uint)chunkIndex);
            BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(17, 4), (uint)chunkCount);
            BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(21, 4), (uint)totalBytes);
            BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(25, 4), envelope.ReliableMessageType!.Value);
            BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(29, 4), (uint)envelope.Payload.Length);

            int cursor = Version2HeaderLength;
            if (senderBytes.Length > 0)
            {
                Buffer.BlockCopy(senderBytes, 0, buffer, cursor, senderBytes.Length);
                cursor += senderBytes.Length;
            }

            if (targetBytes.Length > 0)
            {
                Buffer.BlockCopy(targetBytes, 0, buffer, cursor, targetBytes.Length);
                cursor += targetBytes.Length;
            }

            Buffer.BlockCopy(messageIdBytes, 0, buffer, cursor, messageIdBytes.Length);
            cursor += messageIdBytes.Length;
            Buffer.BlockCopy(envelope.Payload, 0, buffer, cursor, envelope.Payload.Length);
            return buffer;
        }

        public static IGPKcpBinaryEnvelope Decode(byte[] buffer)
        {
            if (buffer == null)
            {
                throw new ArgumentNullException(nameof(buffer));
            }

            if (buffer.Length == 0)
            {
                throw new InvalidOperationException("KCP binary envelope is truncated.");
            }

            return buffer[0] switch
            {
                Version1 => DecodeVersion1(buffer),
                Version2 => DecodeVersion2(buffer),
                _ => throw new InvalidOperationException($"KCP binary envelope version {buffer[0]} is not supported.")
            };
        }

        private static IGPKcpBinaryEnvelope DecodeVersion1(byte[] buffer)
        {
            if (buffer.Length < Version1HeaderLength)
            {
                throw new InvalidOperationException("KCP binary envelope is truncated.");
            }

            byte version = buffer[0];
            byte flags = buffer[1];
            uint messageType = BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(2, 4));
            var targetKind = (IGPKcpTargetKind)buffer[6];
            ushort senderLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(7, 2));
            ushort targetLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(9, 2));
            uint payloadLength = BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(11, 4));

            long totalLength = Version1HeaderLength + senderLength + targetLength + payloadLength;
            if (totalLength > int.MaxValue || buffer.Length != totalLength)
            {
                throw new InvalidOperationException("KCP binary envelope length is invalid.");
            }

            int cursor = Version1HeaderLength;
            string senderPlayerId = senderLength == 0
                ? string.Empty
                : Encoding.UTF8.GetString(buffer, cursor, senderLength);
            cursor += senderLength;

            string targetPlayerId = targetLength == 0
                ? string.Empty
                : Encoding.UTF8.GetString(buffer, cursor, targetLength);
            cursor += targetLength;

            var payload = new byte[payloadLength];
            if (payloadLength > 0)
            {
                Buffer.BlockCopy(buffer, cursor, payload, 0, (int)payloadLength);
            }

            return new IGPKcpBinaryEnvelope(version, flags, messageType, targetKind, senderPlayerId, targetPlayerId, payload);
        }

        private static IGPKcpBinaryEnvelope DecodeVersion2(byte[] buffer)
        {
            if (buffer.Length < Version2HeaderLength)
            {
                throw new InvalidOperationException("KCP binary envelope v2 is truncated.");
            }

            byte flags = buffer[1];
            if ((flags & ReliableChunkFlag) == 0)
            {
                throw new InvalidOperationException("KCP binary envelope v2 missing reliable chunk flag.");
            }

            uint messageType = BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(2, 4));
            var targetKind = (IGPKcpTargetKind)buffer[6];
            ushort senderLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(7, 2));
            ushort targetLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(9, 2));
            ushort messageIdLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(11, 2));
            uint chunkIndexRaw = BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(13, 4));
            uint chunkCountRaw = BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(17, 4));
            uint totalBytesRaw = BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(21, 4));
            uint reliableMessageType = BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(25, 4));
            uint payloadLength = BinaryPrimitives.ReadUInt32BigEndian(buffer.AsSpan(29, 4));

            if (chunkIndexRaw > int.MaxValue ||
                chunkCountRaw > int.MaxValue ||
                totalBytesRaw > int.MaxValue ||
                payloadLength > int.MaxValue)
            {
                throw new InvalidOperationException("KCP binary reliable chunk metadata is too large.");
            }

            if (messageIdLength == 0 ||
                payloadLength == 0 ||
                chunkCountRaw == 0 ||
                chunkIndexRaw >= chunkCountRaw ||
                totalBytesRaw == 0)
            {
                throw new InvalidOperationException("KCP binary reliable chunk metadata is invalid.");
            }

            long totalLength = Version2HeaderLength +
                               senderLength +
                               targetLength +
                               messageIdLength +
                               payloadLength;
            if (totalLength > int.MaxValue || buffer.Length != totalLength)
            {
                throw new InvalidOperationException("KCP binary envelope v2 length is invalid.");
            }

            int cursor = Version2HeaderLength;
            string senderPlayerId = senderLength == 0
                ? string.Empty
                : Encoding.UTF8.GetString(buffer, cursor, senderLength);
            cursor += senderLength;

            string targetPlayerId = targetLength == 0
                ? string.Empty
                : Encoding.UTF8.GetString(buffer, cursor, targetLength);
            cursor += targetLength;

            string reliableMessageId = messageIdLength == 0
                ? string.Empty
                : Encoding.UTF8.GetString(buffer, cursor, messageIdLength);
            cursor += messageIdLength;

            var payload = new byte[payloadLength];
            if (payloadLength > 0)
            {
                Buffer.BlockCopy(buffer, cursor, payload, 0, (int)payloadLength);
            }

            return new IGPKcpBinaryEnvelope(
                Version2,
                flags,
                messageType,
                targetKind,
                senderPlayerId,
                targetPlayerId,
                payload,
                reliableMessageId,
                (int)chunkIndexRaw,
                (int)chunkCountRaw,
                (int)totalBytesRaw,
                reliableMessageType);
        }
    }

    internal static class IGPKcpFrameCodec
    {
        private const int HeaderLength = 4;

        public static byte[] EncodeFrame(byte[] payload, IGPReliableTransportOptions? options = null)
        {
            if (payload == null)
            {
                throw new ArgumentNullException(nameof(payload));
            }

            var resolvedOptions = options ?? IGPReliableTransportOptions.Default;
            if (payload.Length <= 0 || payload.Length > resolvedOptions.KcpFrameMaxBytes)
            {
                throw new ArgumentOutOfRangeException(nameof(payload),
                    $"KCP frame payload exceeds KcpFrameMaxBytes={resolvedOptions.KcpFrameMaxBytes}.");
            }

            var frame = new byte[HeaderLength + payload.Length];
            BinaryPrimitives.WriteUInt32BigEndian(frame.AsSpan(0, HeaderLength), (uint)payload.Length);
            Buffer.BlockCopy(payload, 0, frame, HeaderLength, payload.Length);
            return frame;
        }
    }

    internal sealed class IGPKcpFrameDecoder
    {
        private const int HeaderLength = 4;
        private readonly IGPReliableTransportOptions options;
        private readonly List<byte> buffer = new List<byte>();

        public IGPKcpFrameDecoder(IGPReliableTransportOptions? options = null)
        {
            this.options = options ?? IGPReliableTransportOptions.Default;
        }

        public IReadOnlyList<byte[]> Append(byte[] bytes, int offset, int count)
        {
            if (bytes == null)
            {
                throw new ArgumentNullException(nameof(bytes));
            }

            if (offset < 0 || count < 0 || offset + count > bytes.Length)
            {
                throw new ArgumentOutOfRangeException(nameof(offset));
            }

            for (int i = 0; i < count; i++)
            {
                buffer.Add(bytes[offset + i]);
            }

            var frames = new List<byte[]>();
            while (buffer.Count >= HeaderLength)
            {
                byte[] header = new byte[HeaderLength];
                header[0] = buffer[0];
                header[1] = buffer[1];
                header[2] = buffer[2];
                header[3] = buffer[3];
                int payloadLength = (int)BinaryPrimitives.ReadUInt32BigEndian(header);

                if (payloadLength <= 0 || payloadLength > options.KcpFrameMaxBytes)
                {
                    buffer.Clear();
                    throw new InvalidOperationException(
                        $"KCP frame payload exceeds KcpFrameMaxBytes={options.KcpFrameMaxBytes}.");
                }

                int totalLength = HeaderLength + payloadLength;
                if (buffer.Count < totalLength)
                {
                    break;
                }

                var payload = buffer.GetRange(HeaderLength, payloadLength).ToArray();
                buffer.RemoveRange(0, totalLength);
                frames.Add(payload);
            }

            return frames;
        }

        public void Reset()
        {
            buffer.Clear();
        }
    }
}
