namespace TLVProtocal.SubLayers;

internal class TlvAckLayer(bool ConvertToBigEnd, int timeoutMillionSeconds) : BaseFilter
{
    private const byte STX = 0x02;//Send
    private const byte ENQ = 0x05;//Request
    private const byte ACK = 0x06;//Response ACK

    private readonly object _idLock = new();
    private byte _messageID = 0;
    private readonly int _timeoutMillionSeconds = timeoutMillionSeconds;
    private readonly ConcurrentDictionary<byte, AutoResetEvent> _ackReceivedEvent = [];

    private byte GetNextID()
    {
        lock (_idLock)
            _messageID = _messageID == byte.MaxValue ? (byte)0 : ++_messageID;
        return _messageID;
    }

    public override bool Send(Data data)
    {
        if (data is not TlvData tlv)
            return false;

        byte messageID = GetNextID();
        AutoResetEvent resetEvent = new(false);
        for (; !this._ackReceivedEvent.TryAdd(messageID, resetEvent); messageID = GetNextID()) ;

        List<byte> sendBytes = [];
        switch (tlv.RequestID.HasValue)
        {
            case true:
                sendBytes.Add(ENQ);
                byte[] requestID = BitConverter.GetBytes(tlv.RequestID.Value);
                if (ConvertToBigEnd)
                    Array.Reverse(requestID);
                sendBytes.AddRange(requestID);
                break;

            case false:
                sendBytes.Add(STX);
                break;
        }

        sendBytes.Add(this._messageID);
        sendBytes.AddRange(data.RawData);
        Data sendData = new([.. sendBytes], Connection, data.DateTime);

        try
        {
            if (!base.Send(sendData))
                return false;

            return resetEvent.WaitOne(this._timeoutMillionSeconds);
        }
        finally
        {
            this._ackReceivedEvent.TryRemove(messageID, out _);
        }
    }

    public override bool Receive(Data data)
    {
        byte[] content;
        switch (data.RawData[0])
        {
            case ENQ:
                this.AckReply(data);
                content = new byte[data.RawData.Length - 4];
                ushort requestID;

                if (ConvertToBigEnd)
                {
                    byte[] requestIDBytes = new byte[2];
                    Array.Copy(data.RawData, 1, requestIDBytes, 0, 2);
                    Array.Reverse(requestIDBytes);
                    requestID = BitConverter.ToUInt16(requestIDBytes, 0);
                }
                else
                    requestID = BitConverter.ToUInt16(data.RawData, 0);

                Array.Copy(data.RawData, 4, content, 0, content.Length);
                TlvData tlvData = new(0, content, data.Connection, data.DateTime)
                {
                    RequestID = requestID
                };
                return base.Receive(tlvData);
            case STX:
                this.AckReply(data);
                content = new byte[data.RawData.Length - 2];

                Array.Copy(data.RawData, 2, content, 0, content.Length);
                return base.Receive(new TlvData(0, content, data.Connection, data.DateTime));

            case ACK:
                if (!this._ackReceivedEvent.TryGetValue(data.RawData[1], out AutoResetEvent resetEvent) || resetEvent is null)
                    return false;

                resetEvent.Set();
                return true;
            default:
                return false;
        }
    }

    private bool AckReply(Data data)
    {
        Data ackReply = new([ACK, data.RawData[1]], Connection, DateTime.Now);
        return base.Send(ackReply);
    }

    public override void Connected(Connection connection)
    {
        this.Connection = connection;
        base.Connected(connection);
    }

    private Connection Connection;
}
