using System;
using System.Threading;
using System.Threading.Tasks;
using HidSharp;

namespace MuzikaGromche.Via;

enum ViaCommandId : byte
{
    GetProtocolVersion = 0x01,
    GetKeyboardValue = 0x02,
    SetKeyboardValue = 0x03,
    DynamicKeymapGetKeycode = 0x04,
    DynamicKeymapSetKeycode = 0x05,
    DynamicKeymapReset = 0x06,
    CustomSetValue = 0x07,
    CustomGetValue = 0x08,
    CustomSave = 0x09,
    EepromReset = 0x0A,
    BootloaderJump = 0x0B,
    DynamicKeymapMacroGetCount = 0x0C,
    DynamicKeymapMacroGetBufferSize = 0x0D,
    DynamicKeymapMacroGetBuffer = 0x0E,
    DynamicKeymapMacroSetBuffer = 0x0F,
    DynamicKeymapMacroReset = 0x10,
    DynamicKeymapGetLayerCount = 0x11,
    DynamicKeymapGetBuffer = 0x12,
    DynamicKeymapSetBuffer = 0x13,
    DynamicKeymapGetEncoder = 0x14,
    DynamicKeymapSetEncoder = 0x15,
    Unhandled = 0xFF,
}

enum ViaChannelId : byte
{
    CustomChannel = 0,
    QmkBacklightChannel = 1,
    QmkRgblightChannel = 2,
    QmkRgbMatrixChannel = 3,
    QmkAudioChannel = 4,
    QmkLedMatrixChannel = 5,
}

enum ViaQmkRgbMatrixValue : byte
{
    Brightness = 1,
    Effect = 2,
    EffectSpeed = 3,
    Color = 4,
};

enum ViaEffectMode : byte
{
    Off = 0x00,
    Static = 0x01,
    Breathing = 0x02,
}

class ViaKeyboardApi
{
    private const uint RAW_EPSIZE = 32;
    private const byte COMMAND_START = 0x00;

    private readonly HidStream stream;

    public ViaKeyboardApi(HidStream stream)
    {
        this.stream = stream;
    }

    /// <summary>
    /// Sends a raw HID command prefixed with the command byte and returns the response if successful.
    /// </summary>
    public byte[]? HidCommand(ViaCommandId command, byte[] input)
    {
        byte[] commandBytes = [(byte)command, .. input];

        if (!HidSend(commandBytes))
        {
            // Console.WriteLine($"no send");
            return null;
        }

        var buffer = HidRead();
        if (buffer == null)
        {
            // Console.WriteLine($"no read");
            return null;
        }

        // Console.WriteLine($"write command {commandBytes.BytesToHex()}");
        // Console.WriteLine($"read buffer   {buffer.BytesToHex()}");
        if (!buffer.AsSpan(1).StartsWith(commandBytes))
        {
            return null;
        }
        return buffer.AsSpan(1).ToArray();
    }

    /// <summary>
    /// Sends a raw HID command prefixed with the command byte and returns the response if successful.
    /// </summary>
    public async ValueTask<byte[]?> HidCommandAsync(ViaCommandId command, byte[] input, CancellationToken cancellationToken)
    {
        byte[] commandBytes = [(byte)command, .. input];

        if (!await HidSendAsync(commandBytes, cancellationToken))
        {
            return null;
            // Console.WriteLine($"no send");
        }

        var buffer = await HidReadAsync(cancellationToken);
        if (buffer == null)
        {
            // Console.WriteLine($"no read");
            return null;
        }

        // Console.WriteLine($"write command {commandBytes.BytesToHex()}");
        // Console.WriteLine($"read buffer   {buffer.BytesToHex()}");
        if (!buffer.AsSpan(1).StartsWith(commandBytes))
        {
            return null;
        }
        return buffer.AsSpan(1).ToArray();
    }

    /// <summary>
    /// Reads from the HID device. Returns null if the read fails.
    /// </summary>
    public byte[]? HidRead()
    {
        byte[] buffer = new byte[RAW_EPSIZE + 1];
        // Console.WriteLine($"{buffer.BytesToHex()}");
        int count = stream.Read(buffer);
        if (count != RAW_EPSIZE + 1)
        {
            return null;
        }
        return buffer;
    }

    /// <summary>
    /// Reads from the HID device. Returns null if the read fails.
    /// </summary>
    public async ValueTask<byte[]?> HidReadAsync(CancellationToken cancellationToken)
    {
        byte[] buffer = new byte[RAW_EPSIZE + 1];
        // Console.WriteLine($"{buffer.BytesToHex()}");
        int count = await stream.ReadAsync(buffer, cancellationToken);
        if (count != RAW_EPSIZE + 1)
        {
            return null;
        }
        return buffer;
    }

    /// <summary>
    /// Sends a raw HID command prefixed with the command byte. Returns false if the send fails.
    /// </summary>
    public bool HidSend(byte[] bytes)
    {
        if (bytes.Length > RAW_EPSIZE)
        {
            return false;
        }
        byte[] commandBytes = [COMMAND_START, .. bytes];
        byte[] paddedArray = new byte[RAW_EPSIZE + 1];
        commandBytes.AsSpan().CopyTo(paddedArray);
        // Console.WriteLine($"Send {paddedArray.BytesToHex()}");
        stream.Write(paddedArray);
        return true;
    }

    /// <summary>
    /// Sends a raw HID command prefixed with the command byte. Returns false if the send fails.
    /// </summary>
    public async ValueTask<bool> HidSendAsync(byte[] bytes, CancellationToken cancellationToken)
    {
        if (bytes.Length > RAW_EPSIZE)
        {
            return false;
        }
        byte[] commandBytes = [COMMAND_START, .. bytes];
        byte[] paddedArray = new byte[RAW_EPSIZE + 1];
        commandBytes.AsSpan().CopyTo(paddedArray);
        // Console.WriteLine($"Send {paddedArray.BytesToHex()}");
        await stream.WriteAsync(paddedArray, cancellationToken);
        return true;
    }

    /// <summary>
    /// Returns the protocol version of the keyboard.
    /// </summary>
    public ushort GetProtocolVersion()
    {
        var output = HidCommand(ViaCommandId.GetProtocolVersion, []);
        if (output == null)
        {
            return 0;
        }
        return (ushort)((output[1] << 8) | output[2]);
    }

    /// <summary>
    /// Returns the protocol version of the keyboard.
    /// </summary>
    public async ValueTask<ushort> GetProtocolVersionAsync(CancellationToken cancellationToken)
    {
        var output = await HidCommandAsync(ViaCommandId.GetProtocolVersion, [], cancellationToken);
        if (output == null)
        {
            return 0;
        }
        return (ushort)((output[1] << 8) | output[2]);
    }

    public byte? GetRgbMatrixBrightness()
    {
        var output = HidCommand(ViaCommandId.CustomGetValue,
        [
            (byte)ViaChannelId.QmkRgbMatrixChannel,
            (byte)ViaQmkRgbMatrixValue.Brightness,
        ]);
        if (output == null)
        {
            return null;
        }
        return output[3];
    }

    public async ValueTask<byte?> GetRgbMatrixBrightnessAsync(CancellationToken cancellationToken)
    {
        var output = await HidCommandAsync(ViaCommandId.CustomGetValue,
        [
            (byte)ViaChannelId.QmkRgbMatrixChannel,
            (byte)ViaQmkRgbMatrixValue.Brightness,
        ], cancellationToken);
        if (output == null)
        {
            return null;
        }
        return output[3];
    }

    public bool SetRgbMatrixBrightness(byte brightness)
    {
        return HidCommand(ViaCommandId.CustomSetValue,
        [
            (byte)ViaChannelId.QmkRgbMatrixChannel,
            (byte)ViaQmkRgbMatrixValue.Brightness,
            brightness,
        ]) != null;
    }

    public async ValueTask<bool> SetRgbMatrixBrightnessAsync(byte brightness, CancellationToken cancellationToken)
    {
        return await HidCommandAsync(ViaCommandId.CustomSetValue,
        [
            (byte)ViaChannelId.QmkRgbMatrixChannel,
            (byte)ViaQmkRgbMatrixValue.Brightness,
            brightness,
        ], cancellationToken) != null;
    }

    public byte? GetRgbMatrixEffect()
    {
        var output = HidCommand(ViaCommandId.CustomGetValue,
        [
            (byte)ViaChannelId.QmkRgbMatrixChannel,
            (byte)ViaQmkRgbMatrixValue.Effect,
        ]);
        if (output == null)
        {
            return null;
        }
        return output[3];
    }

    public async ValueTask<byte?> GetRgbMatrixEffectAsync(CancellationToken cancellationToken)
    {
        var output = await HidCommandAsync(ViaCommandId.CustomGetValue,
        [
            (byte)ViaChannelId.QmkRgbMatrixChannel,
            (byte)ViaQmkRgbMatrixValue.Effect,
        ], cancellationToken);
        if (output == null)
        {
            return null;
        }
        return output[3];
    }

    public bool SetRgbMatrixEffect(ViaEffectMode effect)
    {
        return SetRgbMatrixEffect((byte)effect);
    }

    public bool SetRgbMatrixEffect(byte effect)
    {
        return HidCommand(ViaCommandId.CustomSetValue, [
            (byte)ViaChannelId.QmkRgbMatrixChannel,
            (byte)ViaQmkRgbMatrixValue.Effect,
            effect,
        ]) != null;
    }

    public ValueTask<bool> SetRgbMatrixEffectAsync(ViaEffectMode effect, CancellationToken cancellationToken)
    {
        return SetRgbMatrixEffectAsync((byte)effect, cancellationToken);
    }

    public async ValueTask<bool> SetRgbMatrixEffectAsync(byte effect, CancellationToken cancellationToken)
    {
        return await HidCommandAsync(ViaCommandId.CustomSetValue, [
            (byte)ViaChannelId.QmkRgbMatrixChannel,
            (byte)ViaQmkRgbMatrixValue.Effect,
            effect,
        ], cancellationToken) != null;
    }

    public (byte Hue, byte Saturation)? GetRgbMatrixColor()
    {
        var output = HidCommand(ViaCommandId.CustomGetValue,
        [
            (byte)ViaChannelId.QmkRgbMatrixChannel,
            (byte)ViaQmkRgbMatrixValue.Color,
        ]);
        if (output == null)
        {
            return null;
        }
        byte hue = output[3];
        byte saturation = output[4];
        return (hue, saturation);
    }

    public async ValueTask<(byte Hue, byte Saturation)?> GetRgbMatrixColorAsync(CancellationToken cancellationToken)
    {
        var output = await HidCommandAsync(ViaCommandId.CustomGetValue,
        [
            (byte)ViaChannelId.QmkRgbMatrixChannel,
            (byte)ViaQmkRgbMatrixValue.Color,
        ], cancellationToken);
        if (output == null)
        {
            return null;
        }
        byte hue = output[3];
        byte saturation = output[4];
        return (hue, saturation);
    }

    public bool SetRgbMatrixColor(byte hue, byte saturation)
    {
        return HidCommand(ViaCommandId.CustomSetValue, [
            (byte)ViaChannelId.QmkRgbMatrixChannel,
            (byte)ViaQmkRgbMatrixValue.Color,
            hue, saturation,
        ]) != null;
    }

    public async ValueTask<bool> SetRgbMatrixColorAsync(byte hue, byte saturation, CancellationToken cancellationToken)
    {
        return await HidCommandAsync(ViaCommandId.CustomSetValue, [
            (byte)ViaChannelId.QmkRgbMatrixChannel,
            (byte)ViaQmkRgbMatrixValue.Color,
            hue, saturation,
        ], cancellationToken) != null;
    }
}

public record struct HueSaturationColor(byte Hue, byte Saturation);
