using RioJoy.Core.Calibration;
using RioJoy.Core.Mapping;
using RioJoy.Core.Protocol;
using RioJoy.Core.Serial;

namespace RioJoy.Core;

/// <summary>
/// Ties the live RIO link to the input/output pipeline for one active profile:
/// serial button/keypad packets drive the <see cref="InputRouter"/>; analog
/// replies drive the <see cref="AxisCalibrator"/> and the six joystick axes; RIO
/// commands trigger calibration resets and serial requests. This is the runtime
/// assembled per-profile by the tray app (Phase 5).
/// </summary>
public sealed class RioRuntime : IRioCommandSink, IDisposable
{
    private readonly RioSerialLink _link;
    private readonly IJoystickSink _joystick;
    private readonly AxisCalibrator _calibrator;
    private readonly InputRouter _router;
    private readonly ILampSink _lamp;
    private bool _started;
    private bool _lampsInitialized;

    public RioRuntime(
        RioSerialLink link,
        RioInputMap map,
        IInputSink input,
        IJoystickSink joystick,
        AxisCalibrator? calibrator = null)
    {
        _link = link ?? throw new ArgumentNullException(nameof(link));
        if (map is null) throw new ArgumentNullException(nameof(map));
        if (input is null) throw new ArgumentNullException(nameof(input));
        _joystick = joystick ?? throw new ArgumentNullException(nameof(joystick));
        _calibrator = calibrator ?? new AxisCalibrator();

        _lamp = new SerialLampSink(link);
        _router = new InputRouter(map, input, joystick, _lamp, this);
    }

    /// <summary>
    /// When set, light every pressed button on the RIO (bright on press, dim on
    /// release) regardless of how — or whether — it is mapped. Used by the editor so
    /// a physical press lights the button even with an empty/partial profile.
    /// </summary>
    public bool EchoAllLamps { get; set; }

    /// <summary>Raised when a diagnostic toggle RIO command fires (raw-axes / poll-rate).</summary>
    public event Action<RioCommandCode>? DiagnosticToggle;

    /// <summary>
    /// Raised for every decoded button/keypad event (RIO address, <c>true</c> on press
    /// and <c>false</c> on release) — independent of how the entry is routed, so the
    /// editor can show live button activity even with no-op output sinks.
    /// </summary>
    public event Action<int, bool>? ButtonActivity;

    /// <summary>
    /// Raised with the calibrated readout (virtual axes + pre-mix pedal positions)
    /// after every analog reply — independent of the joystick sink, so the editor
    /// can show live gauges even while input routing is suppressed.
    /// </summary>
    public event Action<AxisReadout>? AxesUpdated;

    /// <summary>Raised with the firmware version when a <see cref="RioCommand.VersionReply"/> arrives.</summary>
    public event Action<VersionInfo>? VersionReceived;

    /// <summary>Raised for each board/lamp status item in a <see cref="RioCommand.CheckReply"/>.</summary>
    public event Action<CheckStatus>? CheckReceived;

    /// <summary>
    /// Subscribe to the link. Lit lamps are dimmed on the first reply from the board
    /// (<see cref="EnsureLampsInitialized"/>) rather than here: lamp commands sent in
    /// the first milliseconds after the DTR reset (on port open) are dropped before
    /// the board finishes booting, so the idle-dim state never takes.
    /// </summary>
    public void Start()
    {
        if (_started)
            return;
        _started = true;
        _lampsInitialized = false;

        _link.PacketReceived += OnPacket;
        _link.AnalogReceived += OnAnalog;
        _link.VersionReceived += OnVersion;
        _link.CheckReceived += OnCheck;
    }

    /// <summary>Unsubscribe from the link.</summary>
    public void Stop()
    {
        if (!_started)
            return;
        _started = false;

        _link.PacketReceived -= OnPacket;
        _link.AnalogReceived -= OnAnalog;
        _link.VersionReceived -= OnVersion;
        _link.CheckReceived -= OnCheck;
    }

    private void OnVersion(VersionInfo version) => VersionReceived?.Invoke(version);

    private void OnCheck(CheckStatus status) => CheckReceived?.Invoke(status);

    public void Dispose() => Stop();

    /// <summary>Invoke a RIO command directly (e.g. from the tray menu).</summary>
    public void Trigger(RioCommandCode command) => ((IRioCommandSink)this).Execute(command);

    // Once the board replies (alive after the reset), set every lit button to dim.
    private void EnsureLampsInitialized()
    {
        if (_lampsInitialized)
            return;
        _lampsInitialized = true;
        _router.InitializeLamps();
    }

    private void OnAnalog(AnalogReport report)
    {
        EnsureLampsInitialized();
        AxisOutputs axes = _calibrator.Update(report);
        try
        {
            _joystick.SetAxis(JoyAxis.X, axes.X);
            _joystick.SetAxis(JoyAxis.Y, axes.Y);
            _joystick.SetAxis(JoyAxis.Z, axes.Z);
            _joystick.SetAxis(JoyAxis.Rx, axes.Rx);
            _joystick.SetAxis(JoyAxis.Ry, axes.Ry);
            _joystick.SetAxis(JoyAxis.Rz, axes.Rz);
        }
        catch
        {
            // Output is best-effort: a faulty joystick sink must not kill the link.
        }

        AxesUpdated?.Invoke(new AxisReadout(
            axes, _calibrator.LeftPedalOutput, _calibrator.RightPedalOutput));
    }

    private void OnPacket(RioPacket packet)
    {
        EnsureLampsInitialized();
        byte[] p = packet.Payload;
        switch (packet.Command)
        {
            case RioCommand.ButtonPressed when p[0] < RioAddress.ButtonCount:
                Activity(RioAddress.FromButton(p[0]), pressed: true);
                break;
            case RioCommand.ButtonReleased when p[0] < RioAddress.ButtonCount:
                Activity(RioAddress.FromButton(p[0]), pressed: false);
                break;
            case RioCommand.KeyPressed when IsKeypad(p):
                Activity(RioAddress.FromKeypad(p[0], p[1]), pressed: true);
                break;
            case RioCommand.KeyReleased when IsKeypad(p):
                Activity(RioAddress.FromKeypad(p[0], p[1]), pressed: false);
                break;
        }
    }

    // Route the event through the input pipeline and notify any live-activity listener.
    private void Activity(int address, bool pressed)
    {
        try
        {
            if (pressed)
                _router.Press(address);
            else
                _router.Release(address);
        }
        catch
        {
            // Output is best-effort: a faulty sink must never kill the serial link.
        }

        // Light the button on the RIO even when it is unmapped (the router only lamps
        // mapped lamp-buttons), so the editor's check-function workflow always responds.
        if (EchoAllLamps)
            _lamp.SetLamp(address, pressed ? RioLampState.SolidBright : RioLampState.SolidDim);

        ButtonActivity?.Invoke(address, pressed);
    }

    private static bool IsKeypad(byte[] p) => p[0] is 0 or 1 && p[1] <= 0x0F;

    // IRioCommandSink: RIO commands routed from a button (RIOcmd port).
    void IRioCommandSink.Execute(RioCommandCode command)
    {
        switch (command)
        {
            case RioCommandCode.ResetAllAxes:
            case RioCommandCode.ResetThrottle:
            case RioCommandCode.ResetLeftPedal:
            case RioCommandCode.ResetRightPedal:
            case RioCommandCode.ResetVerticalJoystick:
            case RioCommandCode.ResetHorizontalJoystick:
                _calibrator.Reset(command);
                FireAndForget(_link.ResetAsync((RioResetTarget)(byte)command));
                break;

            case RioCommandCode.GeneralReset:
                FireAndForget(_link.ResetAsync(RioResetTarget.All));
                _router.InitializeLamps();
                break;

            case RioCommandCode.RequestVersionAndCheck:
                FireAndForget(_link.RequestVersionAsync());
                FireAndForget(_link.RequestCheckAsync());
                break;

            case RioCommandCode.ToggleRawAxesReadout:
            case RioCommandCode.TogglePollRateReadout:
                DiagnosticToggle?.Invoke(command);
                break;
        }
    }

    private static void FireAndForget(Task task) =>
        task.ContinueWith(static t => _ = t.Exception, TaskContinuationOptions.OnlyOnFaulted);
}
