using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using VRio.Core.Device;
using VRio.Core.Input;
using VRio.Core.Panel;
using VRio.Core.Protocol;
using Windows.Devices.Enumeration;
using Windows.Devices.Lights;
using Windows.System;
using Windows.UI;
using Color = Windows.UI.Color;

namespace VRio.App;

/// <summary>
/// Mirrors the host-commanded lamp states onto per-key RGB keyboards through
/// Windows Dynamic Lighting (<see cref="LampArray"/>). Keys bound to
/// lamp-capable button addresses in the active profile glow with the panel's
/// palette — the red family for most groups, yellow for Secondary/Screen —
/// and the flash bits blink at the panel's rates; every other key is blacked
/// out so the keyboard reads as the button field. Zone-lit keyboards (no
/// per-key addressing, common on laptops) fall back to a whole-board mirror
/// of the strongest current lamp state.
///
/// <para>Opt-in: enabling claims keyboard-kind lamp arrays (hot-plug aware),
/// disabling releases them so Windows hands the LEDs back to the ambient
/// scene — no explicit restore needed. With several keyboards attached,
/// <see cref="SetTarget"/> narrows the mirror to one of them
/// (<see cref="KeyboardsChanged"/> feeds the picker). Dynamic Lighting gives
/// LED control to the <em>foreground</em> app by default; for the keys to
/// stay lit while the game has focus, vRIO needs package identity and a pick
/// under Settings → Personalization → Dynamic Lighting → "Background light
/// control".</para>
/// </summary>
public sealed class KeyboardLampMirror : IDisposable
{
    // Refresh cadence and flash phases match PanelCanvas, so the keyboard and
    // the on-screen panel blink in step.
    private const int RefreshMs = 100;

    private static readonly Stopwatch Clock = Stopwatch.StartNew();

    // Address → panel colouring, from the cockpit layout (yellow = the
    // Secondary/Screen columns, same test as PanelCanvas).
    private static readonly Dictionary<int, bool> LampAddressIsYellow = BuildAddressInfo();

    private readonly VRioDevice _device;
    private readonly object _gate = new();
    private readonly Dictionary<string, (LampArray Array, bool PerKey)> _claimed = new();
    private readonly Dictionary<string, string> _known = new(); // every keyboard array seen, id → name

    // One entry per lamp-capable bound key: which lamp drives it, how to paint it.
    private (int Address, VirtualKey Key, bool Yellow)[] _map = Array.Empty<(int, VirtualKey, bool)>();

    private DeviceWatcher? _watcher;
    private System.Threading.Timer? _timer;
    private Color[]? _lastPushed; // parallel to _map; null forces a full repaint
    private Color _lastAggregate; // last whole-board color for zone keyboards
    private string? _target; // device id to mirror to; null = all keyboards
    private bool _enabled;
    private bool _unsupported; // WinRT/Dynamic Lighting missing — don't retry
    private bool _anyArraySeen; // a lamp-array device event arrived this session

    public KeyboardLampMirror(VRioDevice device) =>
        _device = device ?? throw new ArgumentNullException(nameof(device));

    /// <summary>Status/log lines (attach, detach, availability hints).</summary>
    public event Action<string>? Logged;

    /// <summary>The set of detected keyboards changed (id, name) — for a picker.</summary>
    public event Action<IReadOnlyList<(string Id, string Name)>>? KeyboardsChanged;

    /// <summary>Mirror on/off. Off releases the LEDs back to Windows.</summary>
    public bool Enabled
    {
        get { lock (_gate) return _enabled; }
        set
        {
            lock (_gate)
            {
                if (_enabled == value || _unsupported)
                    return;
                _enabled = value;
            }
            if (value) Start();
            else Stop();
        }
    }

    /// <summary>Mirror to one keyboard by device id, or to all (null).</summary>
    public void SetTarget(string? deviceId)
    {
        LampArray[] release;
        List<(string Id, string Name)> reclaim = new();
        lock (_gate)
        {
            if (_target == deviceId)
                return;
            _target = deviceId;
            release = _claimed.Where(kv => !Matches(kv.Key))
                              .Select(kv => kv.Value.Array).ToArray();
            foreach (string id in _claimed.Keys.Where(id => !Matches(id)).ToArray())
                _claimed.Remove(id);
            foreach (KeyValuePair<string, string> kv in _known)
                if (Matches(kv.Key) && !_claimed.ContainsKey(kv.Key))
                    reclaim.Add((kv.Key, kv.Value));
            _lastPushed = null;
        }
        foreach (LampArray array in release)
            Release(array);
        foreach ((string id, string name) in reclaim)
            ReclaimAsync(id, name);
    }

    /// <summary>Rebuild the key map from a loaded profile (lamp-capable buttons only).</summary>
    public void SetProfile(BindingProfile profile)
    {
        var map = new List<(int, VirtualKey, bool)>();
        var seen = new HashSet<VirtualKey>();
        foreach (KeyButtonBinding b in profile.KeyButtons)
        {
            if (!LampAddressIsYellow.TryGetValue(b.Address, out bool yellow))
                continue; // keypads and unknown addresses have no lamp
            if (!Enum.TryParse(b.Key, ignoreCase: true, out Keys key))
                continue;
            // WinForms Keys and WinRT VirtualKey share the Win32 VK number space.
            var vk = (VirtualKey)(int)(key & Keys.KeyCode);
            if (seen.Add(vk)) // first binding wins, like the router's key lookup
                map.Add((b.Address, vk, yellow));
        }
        lock (_gate)
        {
            _map = map.ToArray();
            _lastPushed = null; // repaint under the new map
        }
    }

    public void Dispose()
    {
        Enabled = false;
        Logged = null;
        KeyboardsChanged = null;
    }

    // ---- Device lifecycle ---------------------------------------------------

    private void Start()
    {
        try
        {
            var watcher = DeviceInformation.CreateWatcher(LampArray.GetDeviceSelector());
            watcher.Added += OnArrayAdded;
            watcher.Removed += OnArrayRemoved;
            watcher.Updated += (_, _) => { }; // required for the watcher to progress
            watcher.EnumerationCompleted += (_, _) =>
            {
                // Attaches log themselves (and may still be in flight); only the
                // empty case needs a line here.
                bool any;
                lock (_gate) any = _anyArraySeen;
                if (!any)
                    Logged?.Invoke("Keyboard lighting: no Dynamic Lighting keyboard found (check Settings → Personalization → Dynamic Lighting)");
            };
            lock (_gate) _watcher = watcher;
            watcher.Start();
            // A leaked exception in a Timer callback kills the process — never
            // let a lighting hiccup take vRIO down mid-flight.
            _timer = new System.Threading.Timer(_ =>
            {
                try { Tick(); }
                catch (Exception ex) { Logged?.Invoke($"Keyboard lighting: paint failed: {ex.Message}"); }
            }, null, RefreshMs, RefreshMs);
        }
        catch (Exception ex)
        {
            lock (_gate)
            {
                _unsupported = true;
                _enabled = false;
            }
            Logged?.Invoke($"Keyboard lighting unavailable (Dynamic Lighting needs Windows 11): {ex.Message}");
        }
    }

    private void Stop()
    {
        DeviceWatcher? watcher;
        LampArray[] arrays;
        lock (_gate)
        {
            watcher = _watcher;
            _watcher = null;
            arrays = _claimed.Values.Select(a => a.Array).ToArray();
            _claimed.Clear();
            _known.Clear();
            _lastPushed = null;
        }
        _timer?.Dispose();
        _timer = null;
        try { watcher?.Stop(); }
        catch (Exception ex) when (ex is InvalidOperationException or COMException) { }
        // Releasing the arrays hands the LEDs back to the system ambient scene.
        foreach (LampArray array in arrays)
            Release(array);
        RaiseKeyboardsChanged();
    }

    private async void OnArrayAdded(DeviceWatcher sender, DeviceInformation info)
    {
        lock (_gate) _anyArraySeen = true;
        try
        {
            LampArray array = await LampArray.FromIdAsync(info.Id);
            if (array is null || array.LampArrayKind != LampArrayKind.Keyboard)
                return; // don't paint mice/strips/cases

            lock (_gate) _known[info.Id] = info.Name;
            if (!TryClaim(info.Id, info.Name, array))
                Release(array);
            RaiseKeyboardsChanged();
        }
        catch (Exception ex)
        {
            Logged?.Invoke($"Keyboard lighting: could not open {info.Name}: {ex.Message}");
        }
    }

    private void OnArrayRemoved(DeviceWatcher sender, DeviceInformationUpdate update)
    {
        string? name;
        LampArray? gone = null;
        lock (_gate)
        {
            if (!_known.TryGetValue(update.Id, out name))
                return;
            _known.Remove(update.Id);
            if (_claimed.TryGetValue(update.Id, out (LampArray Array, bool PerKey) entry))
            {
                gone = entry.Array;
                _claimed.Remove(update.Id);
            }
        }
        if (gone is not null)
            Release(gone);
        Logged?.Invoke($"Keyboard lighting: {name} disconnected");
        RaiseKeyboardsChanged();
    }

    private async void ReclaimAsync(string id, string name)
    {
        try
        {
            LampArray array = await LampArray.FromIdAsync(id);
            if (array is null)
                return;
            if (!TryClaim(id, name, array))
                Release(array);
        }
        catch (Exception ex)
        {
            Logged?.Invoke($"Keyboard lighting: could not reopen {name}: {ex.Message}");
        }
    }

    /// <summary>Store the array if it is wanted right now; false = caller releases.</summary>
    private bool TryClaim(string id, string name, LampArray array)
    {
        bool perKey = array.SupportsVirtualKeys;
        lock (_gate)
        {
            if (!_enabled || !Matches(id) || _claimed.ContainsKey(id))
                return false;
            _claimed[id] = (array, perKey);
            _lastPushed = null; // full repaint including the base coat
        }
        Logged?.Invoke(perKey
            ? $"Keyboard lighting: + {name} ({array.LampCount} LEDs, per-key)"
            : $"Keyboard lighting: + {name} ({array.LampCount} zones — no per-key map, mirroring the strongest lamp board-wide)");
        HookAvailability(array, name);
        return true;
    }

    private bool Matches(string id) => _target is null || _target == id;

    private static void Release(LampArray array)
    {
        // No IDisposable projection on .NET Framework — drop the COM wrapper.
        try { Marshal.ReleaseComObject(array); } catch (ArgumentException) { }
    }

    private void RaiseKeyboardsChanged()
    {
        (string, string)[] known;
        lock (_gate) known = _known.Select(kv => (kv.Key, kv.Value)).ToArray();
        KeyboardsChanged?.Invoke(known);
    }

    private void HookAvailability(LampArray array, string name)
    {
        // IsAvailable/AvailabilityChanged need Windows 11 22H2+; older builds
        // just never get the background-control hint.
        try
        {
            bool lastAvailable = array.IsAvailable;
            if (!lastAvailable)
                Logged?.Invoke($"Keyboard lighting: Windows is withholding {name} — enable vRIO under " +
                               "Settings → Personalization → Dynamic Lighting → Background light control");
            array.AvailabilityChanged += (a, _) =>
            {
                bool available = a.IsAvailable;
                if (available == lastAvailable)
                    return; // the event also fires without a state change
                lastAvailable = available;
                Logged?.Invoke(available
                    ? $"Keyboard lighting: {name} available"
                    : $"Keyboard lighting: {name} withheld (foreground app owns it — see Dynamic Lighting settings)");
                lock (_gate) _lastPushed = null; // repaint when control returns
            };
        }
        catch (Exception ex) when (ex is COMException or InvalidCastException or MissingMethodException) { }
    }

    // ---- Rendering ----------------------------------------------------------

    private void Tick()
    {
        (int Address, VirtualKey Key, bool Yellow)[] map;
        (LampArray Array, bool PerKey)[] arrays;
        Color[]? last;
        Color lastAggregate;
        lock (_gate)
        {
            if (!_enabled || _claimed.Count == 0)
                return;
            map = _map;
            arrays = _claimed.Values.ToArray();
            last = _lastPushed;
            lastAggregate = _lastAggregate;
        }

        long tick = Clock.ElapsedMilliseconds;
        var colors = new Color[map.Length];
        var keys = new VirtualKey[map.Length];
        var bestShade = LampBrightness.Off;
        bool bestYellow = false;
        bool changed = last is null || last.Length != map.Length;
        for (int i = 0; i < map.Length; i++)
        {
            byte state = _device.GetLamp(map[i].Address);
            LampBrightness shade = RioLampState.Brightness(state);
            if (shade != LampBrightness.Off && !FlashPhaseOn(RioLampState.Flash(state), tick))
                shade = LampBrightness.Off;
            if (shade > bestShade)
                (bestShade, bestYellow) = (shade, map[i].Yellow);

            colors[i] = Shade(shade, map[i].Yellow);
            keys[i] = map[i].Key;
            if (last is not null)
                changed |= !colors[i].Equals(last[i]);
        }
        Color aggregate = Shade(bestShade, bestYellow);
        changed |= !aggregate.Equals(lastAggregate);
        if (!changed)
            return;

        foreach ((LampArray array, bool perKey) in arrays)
        {
            try
            {
                if (perKey)
                {
                    if (last is null) // new claim/map: black out the whole board first
                        array.SetColor(ColorHelper.FromArgb(255, 0, 0, 0));
                    if (map.Length > 0)
                        array.SetColorsForKeys(colors, keys);
                }
                else if (map.Length > 0)
                {
                    // Zone keyboard: the whole board is one big lamp showing
                    // the strongest current state (idle dim, alerts bright/flash).
                    array.SetColor(aggregate);
                }
            }
            catch (COMException) { } // device wobble; the watcher handles removal
        }
        lock (_gate)
        {
            _lastPushed = colors;
            _lastAggregate = aggregate;
        }
    }

    /// <summary>The panel's lamp palette (PanelCanvas fills), as LED colors.</summary>
    private static Color Shade(LampBrightness shade, bool yellow) =>
        yellow
            ? shade switch
            {
                LampBrightness.Bright => ColorHelper.FromArgb(255, 245, 210, 60),
                LampBrightness.Dim => ColorHelper.FromArgb(255, 140, 118, 38),
                _ => ColorHelper.FromArgb(255, 70, 60, 24),
            }
            : shade switch
            {
                LampBrightness.Bright => ColorHelper.FromArgb(255, 230, 70, 70),
                LampBrightness.Dim => ColorHelper.FromArgb(255, 120, 50, 50),
                _ => ColorHelper.FromArgb(255, 64, 40, 40),
            };

    // Same half-periods as PanelCanvas, so panel and keyboard blink together.
    private static bool FlashPhaseOn(LampFlash flash, long tick)
    {
        long halfPeriod = flash switch
        {
            LampFlash.FlashSlow => 500,
            LampFlash.FlashMed => 250,
            LampFlash.FlashFast => 125,
            _ => 0,
        };
        return halfPeriod == 0 || tick / halfPeriod % 2 == 0;
    }

    private static Dictionary<int, bool> BuildAddressInfo()
    {
        var info = new Dictionary<int, bool>();
        foreach (PanelButton b in CockpitLayout.Buttons())
            if (b.LampCapable)
                info[b.Address] = b.Group.Title is "Secondary" or "Screen";
        return info;
    }
}
