using System.Diagnostics;
using RioJoy.Core.Editing;
using RioJoy.Core.Mapping;
using RioJoy.Core.Protocol;
using RioJoy.Core.Serial;

// Hardware bringup monitor: open the RIO COM port, run the real serial link, and
// log every event (buttons, keypad, axis, version/check, control bytes, framing).
// It flashes all lamps once to prove the PC -> RIO output path, then echoes a lamp
// on each button press so a physical press lights up.
//     dotnet run --project tools/RioSerialMonitor -- [port] [seconds] [--baud rate]
// Firmware wedge-patch validation (RIO_TAP mash test, see MashTest.cs):
//     dotnet run --project tools/RioSerialMonitor -- --mash [port] [seconds]
//         [--label baseline|patched] [--no-lamps] [--wedge seconds] [--baud rate]
// --baud: 31250 for the retuned firmware variant (rio-firmware --baud31250).
// Exit: 0 = ran, 2 = could not open the port (mash: 1 = wedge detected).

// E0-threshold firmware verification (rio-firmware --e0thresh images, see E0Test.cs):
//     dotnet run --project tools/RioSerialMonitor -- --e0test [port] [--baud rate] [--events n]
if (args.Contains("--e0test"))
    return await RioSerialMonitor.E0Test.RunAsync(args);

if (args.Contains("--mash"))
    return await RioSerialMonitor.MashTest.RunAsync(args);

int baud = 9600;
var positional = new List<string>();
for (int i = 0; i < args.Length; i++)
{
    if (args[i] == "--baud" && i + 1 < args.Length && int.TryParse(args[i + 1], out int b)) { baud = b; i++; }
    else positional.Add(args[i]);
}
string port = positional.Count > 0 ? positional[0] : "COM1";
int seconds = positional.Count > 1 && int.TryParse(positional[1], out int s) ? s : 30;

Dictionary<int, string> groupOf = CockpitPanel.Buttons().ToDictionary(b => b.Address, b => b.Group.Title);
string Name(int addr) => groupOf.TryGetValue(addr, out string? g) ? $"0x{addr:X2} ({g})" : $"0x{addr:X2}";

var gate = new object();
var sw = Stopwatch.StartNew();
void Log(string msg)
{
    lock (gate) Console.WriteLine($"[{sw.Elapsed.TotalSeconds,6:F2}s] {msg}");
}

Console.WriteLine($"== RIO serial monitor :: {port} @ {baud} 8N1 for {seconds}s ==");

SerialPortTransport transport;
try
{
    transport = new SerialPortTransport(port, baud);
}
catch (Exception ex)
{
    Console.WriteLine($"  FAILED to open {port}: {ex.GetType().Name}: {ex.Message}");
    Console.WriteLine("  (Is the tray app or another program holding the port? Is the cable on the right COM?)");
    return 2;
}

Console.WriteLine($"  opened {transport.Description}");
var link = new RioSerialLink(transport);

int packets = 0, presses = 0, releases = 0, analog = 0, controls = 0, framing = 0;
var seen = new SortedSet<int>();
VersionInfo? version = null;
var checks = new List<CheckStatus>();

// Analog: log on meaningful change only (it polls ~18x/sec). Track per-field range.
const int Deadband = 30;
AnalogReport lastLogged = default;
bool haveAnalog = false;
TimeSpan lastAnalogLog = TimeSpan.Zero;
var min = new short[5]; var max = new short[5];
for (int i = 0; i < min.Length; i++) { min[i] = short.MaxValue; max[i] = short.MinValue; }

void Track(AnalogReport r)
{
    Span<short> v = stackalloc short[] { r.Throttle, r.LeftPedal, r.RightPedal, r.JoystickY, r.JoystickX };
    for (int i = 0; i < 5; i++) { if (v[i] < min[i]) min[i] = v[i]; if (v[i] > max[i]) max[i] = v[i]; }
}

bool Changed(AnalogReport a, AnalogReport b) =>
    Math.Abs(a.Throttle - b.Throttle) > Deadband || Math.Abs(a.LeftPedal - b.LeftPedal) > Deadband ||
    Math.Abs(a.RightPedal - b.RightPedal) > Deadband || Math.Abs(a.JoystickY - b.JoystickY) > Deadband ||
    Math.Abs(a.JoystickX - b.JoystickX) > Deadband;

link.PacketReceived += p =>
{
    packets++;
    byte[] pl = p.Payload;
    switch (p.Command)
    {
        case RioCommand.ButtonPressed when pl[0] < RioAddress.ButtonCount:
        {
            int a = RioAddress.FromButton(pl[0]); presses++; seen.Add(a);
            Log($"BUTTON  DOWN  {Name(a)}");
            _ = link.SetLampAsync((byte)a, RioLampState.SolidBright); // light it
            break;
        }
        case RioCommand.ButtonReleased when pl[0] < RioAddress.ButtonCount:
        {
            int a = RioAddress.FromButton(pl[0]); releases++; seen.Add(a);
            Log($"button up    {Name(a)}");
            _ = link.SetLampAsync((byte)a, RioLampState.SolidDim);
            break;
        }
        case RioCommand.KeyPressed when pl[0] is 0 or 1 && pl[1] <= 0x0F:
        {
            int a = RioAddress.FromKeypad(pl[0], pl[1]); presses++; seen.Add(a);
            Log($"KEYPAD  DOWN  pad{pl[0]} key 0x{pl[1]:X} -> {Name(a)}");
            break;
        }
        case RioCommand.KeyReleased when pl[0] is 0 or 1 && pl[1] <= 0x0F:
        {
            int a = RioAddress.FromKeypad(pl[0], pl[1]); releases++; seen.Add(a);
            Log($"keypad up    pad{pl[0]} key 0x{pl[1]:X} -> {Name(a)}");
            break;
        }
    }
};

link.AnalogReceived += r =>
{
    analog++;
    Track(r);
    if (!haveAnalog || (Changed(r, lastLogged) && sw.Elapsed - lastAnalogLog > TimeSpan.FromMilliseconds(200)))
    {
        Log($"analog  {r}");
        lastLogged = r; haveAnalog = true; lastAnalogLog = sw.Elapsed;
    }
};

link.VersionReceived += v => { version = v; Log($"VERSION reply: firmware {v}"); };
link.CheckReceived += c => { checks.Add(c); Log($"CHECK   reply: {c}"); };
link.ControlReceived += b => { controls++; Log($"control byte 0x{b:X2} ({(Enum.IsDefined(typeof(RioControl), (RioControl)b) ? (RioControl)b : (object)"?")})"); };
link.FramingError += () => { framing++; Log("FRAMING ERROR (resync)"); };

using var cts = new CancellationTokenSource();
Task run = link.RunAsync(cts.Token);

await Task.Delay(400);
Log("requesting firmware version + board/lamp check...");
await link.RequestVersionAsync();
await link.RequestCheckAsync();
await Task.Delay(900);

Log("lamp test: all lamps BRIGHT for 1.5s (watch the MFD)...");
for (int a = 0; a <= RioAddress.MaxAddress; a++) _ = link.SetLampAsync((byte)a, RioLampState.SolidBright);
await Task.Delay(1500);
for (int a = 0; a <= RioAddress.MaxAddress; a++) _ = link.SetLampAsync((byte)a, RioLampState.SolidOff);

Log(">>> NOW: press the MFD buttons, press keypad keys, and move the axis <<<");
Log(">>> a pressed button should light up <<<");

await Task.Delay(TimeSpan.FromSeconds(seconds));
// Transport first: a silent board leaves the receive loop in a pending read
// that only the port close unblocks (net48 ReadAsync ignores cancellation).
cts.Cancel();
transport.Dispose();
try { await run; } catch { /* shutdown */ }

Console.WriteLine();
Console.WriteLine("== summary ==");
Console.WriteLine($"  packets={packets}  presses={presses}  releases={releases}  analog={analog}  control={controls}  framing={framing}");
Console.WriteLine($"  firmware version : {(version is null ? "(no reply)" : version.ToString())}");
Console.WriteLine($"  check items      : {(checks.Count == 0 ? "(none)" : string.Join(", ", checks))}");
Console.WriteLine($"  addresses seen   : {(seen.Count == 0 ? "(none)" : string.Join(", ", seen.Select(Name)))}");
if (haveAnalog)
{
    string[] axes = { "Throttle", "LeftPedal", "RightPedal", "JoyY", "JoyX" };
    Console.WriteLine("  analog raw range (min..max), the live axis is the one that moved:");
    for (int i = 0; i < 5; i++)
        Console.WriteLine($"    {axes[i],-10}: {min[i],6} .. {max[i],-6}  (span {max[i] - min[i]})");
}
else
{
    Console.WriteLine("  analog: (no analog replies received)");
}
return 0;
