#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)
        {
            Version = version;
            Flags = flags;
            MessageType = messageType;
            TargetKind = targetKind;
            SenderPlayerId = senderPlayerId ?? string.Empty;
            TargetPlayerId = targetPlayerId ?? string.Empty;
            Payload = payload ?? throw new ArgumentNullException(nameof(payload));
        }

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

    internal static class IGPKcpBinaryEnvelopeCodec
    {
        private const int HeaderLength = 1 + 1 + 4 + 1 + 2 + 2 + 4;

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

            var resolvedOptions = options ?? IGPReliableTransportOptions.Default;
            if (envelope.Payload.Length > resolvedOptions.KcpDataPlanePayloadMaxBytes)
            {
                throw new ArgumentOutOfRangeException(nameof(envelope),
                    $"KCP payload exceeds KcpDataPlanePayloadMaxBytes={resolvedOptions.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[HeaderLength + 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 = HeaderLength;
            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;
        }

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

            if (buffer.Length < HeaderLength)
            {
                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));

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

            int cursor = HeaderLength;
            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);
        }
    }

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