using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using HidSharp;

namespace MuzikaGromche.Via;

class FrameworkDeviceFactory : IDevicePoolFactory<ViaDeviceDelegate>
{
    // Framework Vendor ID
    private const int VID = 0x32AC;
    internal static readonly int[] PIDs =
    [
        0x0012, // Framework Laptop 16 Keyboard Module - ANSI
        0x0013, // Framework Laptop 16 RGB macropad module
    ];
    // VIA/QMK Raw HID identifiers
    private const ushort UsagePage = 0xFF60;
    private const byte Usage = 0x61;

    // In case of Framework Laptop 16 RGB Keyboard and Macropad,
    // Usage Page & Usage are encoded at the start of the report descriptor.
    private static readonly byte[] UsagePageAndUsageEncoded = [0x06, 0x60, 0xFF, 0x09, 0x61];

    private const byte ExpectedVersion = 12;

    private static bool DevicePredicate(HidDevice device)
    {
        if (VID != device.VendorID)
        {
            return false;
        }
        if (!PIDs.Contains(device.ProductID))
        {
            return false;
        }
        var report = device.GetRawReportDescriptor();
        if (report == null)
        {
            return false;
        }
        if (!report.AsSpan().StartsWith(UsagePageAndUsageEncoded))
        {
            return false;
        }
        return true;
    }

    public ViaDeviceDelegate? Create(HidDevice hidDevice)
    {
        if (!DevicePredicate(hidDevice))
        {
            return null;
        }
        if (!hidDevice.TryOpen(out var stream))
        {
            return null;
        }
        var via = new ViaKeyboardApi(stream);
        var version = via.GetProtocolVersion();
        if (version != ExpectedVersion)
        {
            return null;
        }
        var brightness = via.GetRgbMatrixBrightness();
        if (brightness == null)
        {
            return null;
        }
        var effect = via.GetRgbMatrixEffect();
        if (effect == null)
        {
            return null;
        }
        var color = via.GetRgbMatrixColor();
        if (color == null)
        {
            return null;
        }

        return new ViaDeviceDelegate(hidDevice, stream, via, brightness.Value, effect.Value, color.Value.Hue, color.Value.Saturation);
    }
}

class ViaDeviceDelegate : IDevicePoolDelegate, ILightshow, IDisposable
{
    // Objects
    public readonly HidDevice device;
    public readonly HidStream stream;
    public readonly ViaKeyboardApi via;

    private readonly AsyncEventProcessor<ViaDeviceState> asyncEventProcessor;
    private readonly ViaDeviceState initialState;
    private byte? brightnessOverride = null;
    private const ViaEffectMode EFFECT = ViaEffectMode.Static;
    private byte? effectOverride = null;
    private ColorHSV? color = null;

    public ViaDeviceDelegate(HidDevice device, HidStream stream, ViaKeyboardApi via, byte brightness, byte effect, byte hue, byte saturation)
    {
        // Objects
        this.device = device;
        this.stream = stream;
        this.via = via;
        // Initial values
        initialState = new()
        {
            Brightness = brightness,
            Effect = effect,
            Hue = hue,
            Saturation = saturation,
        };
        asyncEventProcessor = new AsyncEventProcessor<ViaDeviceState>(initialState, ProcessDataAsync);
        Notify();
    }

    public void Restore()
    {
        brightnessOverride = null;
        effectOverride = null;
        color = null;
        Notify();
    }

    public void Dispose()
    {
        // Simplified version of Restore()
        brightnessOverride = null;
        effectOverride = null;
        color = null;
        asyncEventProcessor.Notify(initialState, requestShutdown: true);
    }

    public void SetBrightnessOverride(byte? brightness)
    {
        brightnessOverride = brightness;
        effectOverride = (byte)EFFECT;
        Notify();
    }

    public void SetColor(byte hue, byte saturation, byte value)
    {
        color = new ColorHSV(hue, saturation, value);
        effectOverride = (byte)EFFECT;
        Notify();
    }

    private void Notify()
    {
        asyncEventProcessor.Notify(new()
        {
            Brightness = brightnessOverride ?? color?.Value ?? initialState.Brightness,
            Effect = effectOverride ?? initialState.Effect,
            Hue = color?.Hue ?? initialState.Hue,
            Saturation = color?.Saturation ?? initialState.Saturation,
        });
    }

    private async ValueTask ProcessDataAsync(ViaDeviceState oldData, ViaDeviceState newData, bool shutdown)
    {
        var cancellationToken = CancellationToken.None;
        try
        {
            if (oldData.Brightness != newData.Brightness)
            {
                await via.SetRgbMatrixBrightnessAsync(newData.Brightness, cancellationToken);
            }
            if (oldData.Effect != newData.Effect)
            {
                await via.SetRgbMatrixEffectAsync(newData.Effect, cancellationToken);
            }
            if (oldData.Hue != newData.Hue || oldData.Saturation != newData.Saturation)
            {
                await via.SetRgbMatrixColorAsync(newData.Hue, newData.Saturation, cancellationToken);
            }
        }
        catch (IOException)
        {
        }
        catch (TimeoutException)
        {
        }
        finally
        {
            if (shutdown)
            {
                stream.Close();
            }
        }
    }
}

class DevicePoolLightshow<T> : ILightshow
    where T: IDevicePoolDelegate, ILightshow
{
    private readonly DevicePool<T> devicePool;

    public DevicePoolLightshow(DevicePool<T> devicePool)
    {
        this.devicePool = devicePool;
    }

    public void SetBrightnessOverride(byte? brightness)
    {
        devicePool.ForEach(d =>
        {
            d.SetBrightnessOverride(brightness);
        });
    }

    public void SetColor(byte hue, byte saturation, byte value)
    {
        devicePool.ForEach(d => 
        {
            d.SetColor(hue, saturation, value);
        });
    }
}

struct ViaDeviceState
{
    public byte Brightness;
    public byte Effect;
    public byte Hue;
    public byte Saturation;
}

record struct ColorHSV(byte Hue, byte Saturation, byte Value);
