using RioJoy.Core.Protocol;

namespace RioJoy.Core.Mapping;

/// <summary>
/// Decodes RIO button/keypad press &amp; release events through a
/// <see cref="RioInputMap"/> and routes them to the keyboard/mouse, joystick,
/// lamp, and RIO-command sinks. Faithful port of <c>Press_V2</c>/<c>Release_V2</c>
/// (riovjoy2.cpp#L1944); see docs/PROTOCOL.md §5.
///
/// <para>Routing precedence and lamp feedback follow the legacy exactly: a RIO
/// command runs and returns (no lamp); keyboard/joystick/hat/mouse all apply lamp
/// feedback when the entry has the lamp flag (bright on press, dim on release).
/// Modifier keys are pressed before the key and released after it, in reverse.</para>
/// </summary>
public sealed class InputRouter
{
    private readonly RioInputMap _map;
    private readonly IInputSink _input;
    private readonly IJoystickSink _joystick;
    private readonly ILampSink _lamp;
    private readonly IRioCommandSink _commands;
    private readonly int _mouseStep;

    public InputRouter(
        RioInputMap map,
        IInputSink input,
        IJoystickSink joystick,
        ILampSink lamp,
        IRioCommandSink commands,
        int mouseMoveStep = 50)
    {
        _map = map ?? throw new ArgumentNullException(nameof(map));
        _input = input ?? throw new ArgumentNullException(nameof(input));
        _joystick = joystick ?? throw new ArgumentNullException(nameof(joystick));
        _lamp = lamp ?? throw new ArgumentNullException(nameof(lamp));
        _commands = commands ?? throw new ArgumentNullException(nameof(commands));
        _mouseStep = mouseMoveStep;
    }

    /// <summary>
    /// Set every lighted button to its idle (dim) state. The legacy app does this
    /// at startup for all entries with the lamp flag; call it on profile load.
    /// </summary>
    public void InitializeLamps()
    {
        for (int address = 0; address < _map.Count; address++)
        {
            if (_map[address].HasLamp)
                _lamp.SetLamp(address, RioLampState.SolidDim);
        }
    }

    /// <summary>Handle a button/keypad press at the given RIO address.</summary>
    public void Press(int address)
    {
        RioMapEntry entry = _map[address];
        if (entry.IsUnmapped)
            return;

        switch (entry.Kind)
        {
            case RioRouteKind.Keyboard:
                if (entry.Shift) _input.KeyDown(VirtualKey.Shift, extended: false);
                if (entry.Ctrl) _input.KeyDown(VirtualKey.Control, extended: false);
                if (entry.Alt) _input.KeyDown(VirtualKey.Menu, extended: false);
                _input.KeyDown(entry.Value, entry.Extended);
                break;

            case RioRouteKind.RioCommand:
                _commands.Execute((RioCommandCode)entry.Value);
                return; // RIO commands are not outputs and carry no lamp feedback

            case RioRouteKind.Joystick:
                _joystick.SetButton(entry.Value, pressed: true);
                break;

            case RioRouteKind.Hat:
                _joystick.SetHat((RioHat)entry.Value);
                break;

            case RioRouteKind.Mouse:
                MousePress((MouseAction)entry.Value);
                break;
        }

        if (entry.HasLamp)
            _lamp.SetLamp(address, RioLampState.SolidBright);
    }

    /// <summary>Handle a button/keypad release at the given RIO address.</summary>
    public void Release(int address)
    {
        RioMapEntry entry = _map[address];
        if (entry.IsUnmapped)
            return;

        switch (entry.Kind)
        {
            case RioRouteKind.Keyboard:
                _input.KeyUp(entry.Value, entry.Extended);
                // Release modifiers in reverse order.
                if (entry.Alt) _input.KeyUp(VirtualKey.Menu, extended: false);
                if (entry.Ctrl) _input.KeyUp(VirtualKey.Control, extended: false);
                if (entry.Shift) _input.KeyUp(VirtualKey.Shift, extended: false);
                break;

            case RioRouteKind.RioCommand:
                return; // nothing happens on release; no lamp

            case RioRouteKind.Joystick:
                _joystick.SetButton(entry.Value, pressed: false);
                break;

            case RioRouteKind.Hat:
                _joystick.SetHat(RioHat.Centered);
                break;

            case RioRouteKind.Mouse:
                MouseRelease((MouseAction)entry.Value);
                break;
        }

        if (entry.HasLamp)
            _lamp.SetLamp(address, RioLampState.SolidDim);
    }

    // Mouse move deltas corrected vs. the legacy (which mixed dx/dy); the action
    // codes are the contract (docs/PROTOCOL.md §5 ⚠️). Moves act on press only.
    private void MousePress(MouseAction action)
    {
        switch (action)
        {
            case MouseAction.MoveUp: _input.MouseMove(0, -_mouseStep); break;
            case MouseAction.MoveRight: _input.MouseMove(_mouseStep, 0); break;
            case MouseAction.MoveDown: _input.MouseMove(0, _mouseStep); break;
            case MouseAction.MoveLeft: _input.MouseMove(-_mouseStep, 0); break;
            case MouseAction.LeftClick: _input.MouseButton(MouseButton.Left, down: true); break;
            case MouseAction.RightClick: _input.MouseButton(MouseButton.Right, down: true); break;
        }
    }

    private void MouseRelease(MouseAction action)
    {
        switch (action)
        {
            case MouseAction.LeftClick: _input.MouseButton(MouseButton.Left, down: false); break;
            case MouseAction.RightClick: _input.MouseButton(MouseButton.Right, down: false); break;
            // Move actions have no release behavior.
        }
    }
}
