#nullable enable
using System;

namespace IGP.Multiplayer.Mirror
{
    /// <summary>
    /// Mirror logical peer lifecycle carried inside the Game binary lane.
    /// These messages are unrelated to Room Control.
    /// </summary>
    internal static class IGPMirrorPeerProtocol
    {
        internal const uint ConnectRequestMessageType = 41001;
        internal const uint ConnectAcceptMessageType = 41002;
        internal const uint DisconnectMessageType = 41003;
        internal const uint CapabilityPayloadCompression = 1u << 0;
        internal const uint CapabilityApplicationFrameV1 = 1u << 1;

        private const int AttemptIdBytes = 16;
        private const int ConnectionIdBytes = sizeof(int);
        private const int CapabilitiesEnvelopeBytes = 9;
        private const byte CapabilitiesVersion = 1;

        internal readonly struct ConnectRequest
        {
            internal ConnectRequest(Guid? attemptId, uint capabilities)
            {
                AttemptId = attemptId;
                Capabilities = capabilities;
            }

            internal Guid? AttemptId { get; }
            internal uint Capabilities { get; }
        }

        internal readonly struct ConnectAccept
        {
            internal ConnectAccept(int connectionId, Guid? attemptId, uint capabilities)
            {
                ConnectionId = connectionId;
                AttemptId = attemptId;
                Capabilities = capabilities;
            }

            internal int ConnectionId { get; }
            internal Guid? AttemptId { get; }
            internal uint Capabilities { get; }
        }

        internal static byte[] EncodeConnectRequest(Guid attemptId, uint capabilities)
        {
            var payload = new byte[AttemptIdBytes + CapabilitiesEnvelopeBytes];
            Buffer.BlockCopy(attemptId.ToByteArray(), 0, payload, 0, AttemptIdBytes);
            WriteCapabilities(payload, AttemptIdBytes, capabilities);
            return payload;
        }

        internal static byte[] EncodeConnectAccept(int connectionId, Guid? attemptId, uint capabilities)
        {
            int attemptBytes = attemptId.HasValue ? AttemptIdBytes : 0;
            var payload = new byte[ConnectionIdBytes + attemptBytes + CapabilitiesEnvelopeBytes];
            Buffer.BlockCopy(BitConverter.GetBytes(connectionId), 0, payload, 0, ConnectionIdBytes);
            if (attemptId.HasValue)
            {
                Buffer.BlockCopy(attemptId.Value.ToByteArray(), 0, payload, ConnectionIdBytes, AttemptIdBytes);
            }
            WriteCapabilities(payload, ConnectionIdBytes + attemptBytes, capabilities);
            return payload;
        }

        internal static ConnectRequest DecodeConnectRequest(byte[] payload)
        {
            return new ConnectRequest(
                TryReadGuid(payload, 0, out Guid attemptId) ? attemptId : (Guid?)null,
                TryReadCapabilities(payload, AttemptIdBytes, out uint capabilities) ? capabilities : 0u);
        }

        internal static ConnectAccept DecodeConnectAccept(byte[] payload)
        {
            int connectionId = payload.Length >= ConnectionIdBytes
                ? BitConverter.ToInt32(payload, 0)
                : 1;
            Guid? attemptId = TryReadGuid(payload, ConnectionIdBytes, out Guid parsedAttemptId)
                ? parsedAttemptId
                : (Guid?)null;
            uint capabilities = 0;
            if (TryReadCapabilities(payload, ConnectionIdBytes + AttemptIdBytes, out uint parsedCapabilities) ||
                TryReadCapabilities(payload, ConnectionIdBytes, out parsedCapabilities))
            {
                capabilities = parsedCapabilities;
            }
            return new ConnectAccept(connectionId, attemptId, capabilities);
        }

        private static void WriteCapabilities(byte[] payload, int offset, uint capabilities)
        {
            payload[offset] = (byte)'I';
            payload[offset + 1] = (byte)'G';
            payload[offset + 2] = (byte)'P';
            payload[offset + 3] = (byte)'M';
            payload[offset + 4] = CapabilitiesVersion;
            payload[offset + 5] = (byte)(capabilities & 0xff);
            payload[offset + 6] = (byte)((capabilities >> 8) & 0xff);
            payload[offset + 7] = (byte)((capabilities >> 16) & 0xff);
            payload[offset + 8] = (byte)((capabilities >> 24) & 0xff);
        }

        private static bool TryReadCapabilities(byte[] payload, int offset, out uint capabilities)
        {
            capabilities = 0;
            if (payload == null || offset < 0 || payload.Length < offset + CapabilitiesEnvelopeBytes ||
                payload[offset] != (byte)'I' || payload[offset + 1] != (byte)'G' ||
                payload[offset + 2] != (byte)'P' || payload[offset + 3] != (byte)'M' ||
                payload[offset + 4] != CapabilitiesVersion)
            {
                return false;
            }
            capabilities = payload[offset + 5] |
                           ((uint)payload[offset + 6] << 8) |
                           ((uint)payload[offset + 7] << 16) |
                           ((uint)payload[offset + 8] << 24);
            return true;
        }

        private static bool TryReadGuid(byte[] payload, int offset, out Guid value)
        {
            value = Guid.Empty;
            if (payload == null || offset < 0 || payload.Length < offset + AttemptIdBytes) return false;
            var bytes = new byte[AttemptIdBytes];
            Buffer.BlockCopy(payload, offset, bytes, 0, AttemptIdBytes);
            value = new Guid(bytes);
            return true;
        }
    }
}
