using VRio.Core.Device;

namespace VRio.Core.Input;

/// <summary>
/// Routes keyboard and gamepad input into a <see cref="VRioDevice"/> through
/// a <see cref="BindingProfile"/>. The host feeds key edges
/// (<see cref="KeyDown"/>/<see cref="KeyUp"/> — auto-repeat is suppressed
/// here), gamepad snapshots (<see cref="SetPadState"/>), and a steady
/// <see cref="Tick"/> for the time-based axis modes.
///
/// <para>Axes are composed in normalized units (1 = full realistic travel,
/// <see cref="RioAxisRange.Full"/> supplies the wire sign): rate integrators
/// (throttle-style, position holds) + deflect keys currently held + direct
/// pad positions, clamped to the axis' travel window. The device is only
/// written when the composed value changes, so mouse drags on the panel keep
/// working while a source is idle.</para>
///
/// <para>Buttons keep a hold count per RIO address, so a key and a pad button
/// bound to the same control overlap cleanly: the press goes on the wire at
/// 0→1 and the release at 1→0. <see cref="AddressHeldChanged"/> mirrors those
/// edges for the UI. Not thread-safe — call from one thread (the UI's).</para>
/// </summary>
public sealed class InputRouter
{
    private static readonly RioAxis[] Axes = (RioAxis[])Enum.GetValues(typeof(RioAxis));

    private readonly VRioDevice _device;

    private BindingProfile _profile = BindingProfile.Empty;
    private readonly Dictionary<string, List<KeyButtonBinding>> _keyButtons = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, List<KeyAxisBinding>> _keyAxes = new(StringComparer.OrdinalIgnoreCase);
    private readonly List<PadAxisBinding>[] _padAxesByAxis;
    private readonly List<KeyAxisBinding>[] _deflectByAxis;

    private readonly HashSet<string> _heldKeys = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<int, int> _holdCounts = new();
    private readonly HashSet<int> _toggled = new();

    private readonly float[] _rate = new float[Axes.Length];      // normalized integrators
    private readonly short?[] _lastSent = new short?[Axes.Length];
    private PadState _pad;
    private PadButtons _prevPadButtons;

    public InputRouter(VRioDevice device)
    {
        _device = device ?? throw new ArgumentNullException(nameof(device));
        _padAxesByAxis = new List<PadAxisBinding>[Axes.Length];
        _deflectByAxis = new List<KeyAxisBinding>[Axes.Length];
        for (int i = 0; i < Axes.Length; i++)
        {
            _padAxesByAxis[i] = new List<PadAxisBinding>();
            _deflectByAxis[i] = new List<KeyAxisBinding>();
        }
    }

    /// <summary>A routed address went down (true) or came back up (false) — for panel highlighting.</summary>
    public event Action<int, bool>? AddressHeldChanged;

    /// <summary>
    /// The active bindings. Assigning releases everything currently held
    /// (keys, toggles, pad buttons) so a reload never strands a pressed input.
    /// </summary>
    public BindingProfile Profile
    {
        get => _profile;
        set
        {
            ReleaseEverything();
            _profile = value ?? BindingProfile.Empty;

            _keyButtons.Clear();
            _keyAxes.Clear();
            foreach (KeyButtonBinding b in _profile.KeyButtons)
                Bucket(_keyButtons, b.Key).Add(b);
            foreach (KeyAxisBinding b in _profile.KeyAxes)
                Bucket(_keyAxes, b.Key).Add(b);

            for (int i = 0; i < Axes.Length; i++)
            {
                _padAxesByAxis[i].Clear();
                _deflectByAxis[i].Clear();
            }
            foreach (PadAxisBinding b in _profile.PadAxes)
                _padAxesByAxis[(int)b.Axis].Add(b);
            foreach (KeyAxisBinding b in _profile.KeyAxes)
                if (b.Mode == KeyAxisMode.Deflect)
                    _deflectByAxis[(int)b.Axis].Add(b);

            ResetAxisState();
        }
    }

    private static List<T> Bucket<T>(Dictionary<string, List<T>> map, string key)
    {
        if (!map.TryGetValue(key, out List<T>? list))
            map[key] = list = new List<T>();
        return list;
    }

    /// <summary>True if any binding uses this key (the host swallows only bound keys).</summary>
    public bool HasKeyBinding(string key) => _keyButtons.ContainsKey(key) || _keyAxes.ContainsKey(key);

    // ---- Keyboard -----------------------------------------------------------

    /// <summary>A key went down. Repeats while held are ignored.</summary>
    public void KeyDown(string key)
    {
        if (!_heldKeys.Add(key))
            return;
        if (!_keyButtons.TryGetValue(key, out List<KeyButtonBinding>? bindings))
            return;
        foreach (KeyButtonBinding b in bindings)
        {
            if (b.Toggle)
                ToggleAddress(b.Address);
            else
                IncHold(b.Address);
        }
    }

    /// <summary>A key came back up.</summary>
    public void KeyUp(string key)
    {
        if (!_heldKeys.Remove(key))
            return;
        if (!_keyButtons.TryGetValue(key, out List<KeyButtonBinding>? bindings))
            return;
        foreach (KeyButtonBinding b in bindings)
        {
            if (!b.Toggle)
                DecHold(b.Address);
        }
    }

    /// <summary>
    /// Release every held key (focus loss, keyboard input turned off).
    /// Toggle latches survive, like the panel's right-click latches.
    /// </summary>
    public void ReleaseAllKeys()
    {
        foreach (string key in _heldKeys.ToArray())
            KeyUp(key);
    }

    // ---- Gamepad ------------------------------------------------------------

    /// <summary>
    /// Feed the latest gamepad snapshot; button edges fire immediately, axes
    /// are picked up by the next <see cref="Tick"/>. Feed <c>default</c> when
    /// the pad disconnects or is disabled so everything it held releases.
    /// </summary>
    public void SetPadState(PadState state)
    {
        PadButtons changed = state.Buttons ^ _prevPadButtons;
        if (changed != PadButtons.None)
        {
            foreach (PadButtonBinding b in _profile.PadButtons)
            {
                if ((changed & b.Button) == 0)
                    continue;
                bool down = (state.Buttons & b.Button) != 0;
                if (b.Toggle)
                {
                    if (down)
                        ToggleAddress(b.Address);
                }
                else if (down)
                {
                    IncHold(b.Address);
                }
                else
                {
                    DecHold(b.Address);
                }
            }
        }
        _prevPadButtons = state.Buttons;
        _pad = state;
    }

    // ---- Time base ----------------------------------------------------------

    /// <summary>
    /// Advance rate integrators by <paramref name="dtSeconds"/> and push the
    /// composed axis values to the device (only where they changed).
    /// </summary>
    public void Tick(double dtSeconds)
    {
        // Pass 1: advance the rate integrators (held rate keys, rate-mode pad axes).
        foreach (KeyAxisBinding b in _profile.KeyAxes)
        {
            if (b.Mode == KeyAxisMode.Rate && _heldKeys.Contains(b.Key))
                AddRate(b.Axis, (float)(b.Value * dtSeconds));
        }
        foreach (PadAxisBinding b in _profile.PadAxes)
        {
            if (b.Rate > 0f)
                AddRate(b.Axis, (float)(Shape(_pad.Axis(b.Source), b) * b.Rate * dtSeconds));
        }

        // Pass 2: compose each axis and write it if it moved.
        for (int i = 0; i < Axes.Length; i++)
        {
            RioAxis axis = Axes[i];
            float total = _rate[i];

            foreach (KeyAxisBinding b in _deflectByAxis[i])
            {
                if (_heldKeys.Contains(b.Key))
                    total += b.Value;
            }

            foreach (PadAxisBinding b in _padAxesByAxis[i])
            {
                if (b.Rate <= 0f)
                    total += Shape(_pad.Axis(b.Source), b);
            }
            total = ClampNorm(axis, total);

            short raw = (short)Math.Round(total * RioAxisRange.Full(axis));
            short prev = _lastSent[i] ?? _device.GetAxis(axis);
            if (raw != prev)
                _device.SetAxis(axis, raw);
            _lastSent[i] = raw;
        }
    }

    /// <summary>
    /// Forget integrated/last-sent axis state — call after the axes were
    /// re-zeroed behind the router's back (center button, host ResetRequest)
    /// so a rate-driven throttle doesn't resume from its old position.
    /// </summary>
    public void ResetAxisState()
    {
        Array.Clear(_rate, 0, _rate.Length);
        for (int i = 0; i < _lastSent.Length; i++)
            _lastSent[i] = null;
    }

    // ---- Internals ----------------------------------------------------------

    private void AddRate(RioAxis axis, float delta) =>
        _rate[(int)axis] = ClampNorm(axis, _rate[(int)axis] + delta);

    /// <summary>Deadzone (rescaled so travel stays continuous) + inversion.</summary>
    private static float Shape(float value, PadAxisBinding b)
    {
        float v = value;
        if (b.Deadzone > 0f)
        {
            float mag = Math.Abs(v);
            v = mag <= b.Deadzone ? 0f : Math.Sign(v) * (mag - b.Deadzone) / (1f - b.Deadzone);
        }
        return b.Invert ? -v : v;
    }

    /// <summary>Stick axes are bipolar (±1), throttle/pedals unipolar (0..1 of travel).</summary>
    private static float ClampNorm(RioAxis axis, float value)
    {
        float min = axis is RioAxis.JoystickX or RioAxis.JoystickY ? -1f : 0f;
        return Math.Max(min, Math.Min(1f, value));
    }

    private void ToggleAddress(int address)
    {
        if (_toggled.Remove(address))
            DecHold(address);
        else
        {
            _toggled.Add(address);
            IncHold(address);
        }
    }

    private void IncHold(int address)
    {
        _holdCounts.TryGetValue(address, out int count);
        _holdCounts[address] = count + 1;
        if (count == 0)
        {
            _device.PressAddress(address);
            AddressHeldChanged?.Invoke(address, true);
        }
    }

    private void DecHold(int address)
    {
        if (!_holdCounts.TryGetValue(address, out int count))
            return;
        if (count <= 1)
        {
            _holdCounts.Remove(address);
            _device.ReleaseAddress(address);
            AddressHeldChanged?.Invoke(address, false);
        }
        else
        {
            _holdCounts[address] = count - 1;
        }
    }

    private void ReleaseEverything()
    {
        _heldKeys.Clear();
        _toggled.Clear();
        _prevPadButtons = PadButtons.None;
        _pad = default;
        foreach (int address in _holdCounts.Keys.ToArray())
        {
            _holdCounts.Remove(address);
            _device.ReleaseAddress(address);
            AddressHeldChanged?.Invoke(address, false);
        }
    }
}
