@page "/can-monitor"
@rendermode InteractiveServer
@implements IDisposable

@using Peak.Can.Basic

@inject ICanService CanService

<PageTitle>CAN Monitor</PageTitle>

<h1>CAN Monitor</h1>

@* ── Connection ──────────────────────────────────────────────────────────────── *@
<div class="card mb-3">
    <div class="card-body py-2">
        @if (CanService.IsConnected && !showConnectionForm)
        {
            <div class="d-flex align-items-center gap-3">
                <span class="badge bg-success">● Connected</span>
                <span class="font-monospace fw-semibold">@CanService.ChannelName</span>
                <span class="text-muted">@BitrateLabel(CanService.CurrentBitrate)</span>
                <button class="btn btn-sm btn-outline-secondary ms-auto"
                        @onclick="() => { showConnectionForm = true; ScanChannels(); }">
                    Change
                </button>
            </div>
        }
        else
        {
            <div class="row g-2 align-items-end">
                <div class="col-auto">
                    <label class="form-label small mb-0">Channel</label>
                    <select class="form-select form-select-sm font-monospace" @bind="selectedChannel"
                            style="min-width:100px">
                        @if (availableChannels.Count == 0)
                        {
                            <option value="">— none found —</option>
                        }
                        @foreach (var ch in availableChannels)
                        {
                            <option value="@ch.ToString()">@ch.ToString()</option>
                        }
                    </select>
                </div>
                <div class="col-auto">
                    <label class="form-label small mb-0">Bitrate</label>
                    <select class="form-select form-select-sm" @bind="selectedBitrate"
                            style="min-width:120px">
                        @foreach (var (val, label) in BitrateOptions)
                        {
                            <option value="@val.ToString()">@label</option>
                        }
                    </select>
                </div>
                <div class="col-auto d-flex gap-2">
                    <button class="btn btn-sm btn-outline-secondary" @onclick="ScanChannels">Scan</button>
                    <button class="btn btn-sm btn-primary" @onclick="Connect"
                            disabled="@(availableChannels.Count == 0 || isConnecting)">
                        @(isConnecting ? "Connecting…" : CanService.IsConnected ? "Reconnect" : "Connect")
                    </button>
                    @if (CanService.IsConnected)
                    {
                        <button class="btn btn-sm btn-outline-secondary"
                                @onclick="() => showConnectionForm = false">Cancel</button>
                    }
                </div>
            </div>
            @if (availableChannels.Count == 0)
            {
                <div class="text-warning small mt-1">
                    No PCAN USB channels detected. Plug in your adapter and click Scan.
                </div>
            }
            @if (connectError != null)
            {
                <div class="text-danger small mt-1">@connectError</div>
            }
        }
    </div>
</div>

@* ── Live messages ──────────────────────────────────────────────────────────── *@
<div class="card mb-3">
    <div class="card-header d-flex justify-content-between align-items-center py-0 ps-0">
        <ul class="nav nav-tabs border-0 mb-0">
            <li class="nav-item">
                <button class="nav-link rounded-0 @(msgTab == "stream" ? "active" : "")"
                        @onclick='() => msgTab = "stream"'>
                    Stream
                    <span class="badge bg-secondary ms-1">@messages.Count</span>
                </button>
            </li>
            <li class="nav-item">
                <button class="nav-link rounded-0 @(msgTab == "latest" ? "active" : "")"
                        @onclick='() => msgTab = "latest"'>
                    Latest
                    <span class="badge bg-secondary ms-1">@latestMessages.Count</span>
                </button>
            </li>
        </ul>
        <button class="btn btn-sm btn-outline-secondary me-2" @onclick="ClearMessages">Clear</button>
    </div>

    @if (msgTab == "stream")
    {
        <div style="max-height:320px; overflow-y:auto;">
            <table class="table table-sm table-hover font-monospace mb-0">
                <thead class="table-light sticky-top">
                    <tr>
                        <th>Timestamp (µs)</th>
                        <th>ID</th>
                        <th>DLC</th>
                        <th>Data</th>
                        <th>Signals</th>
                    </tr>
                </thead>
                <tbody>
                    @lock (messages)
                    {
                        foreach (var m in messages)
                        {
                            <tr>
                                <td>@m.TimestampUs</td>
                                <td>@($"0x{m.Id:X3}")</td>
                                <td>@m.Dlc</td>
                                <td>@(m.Data.Length > 0 ? BitConverter.ToString(m.Data).Replace("-", " ") : "–")</td>
                                <td>@SignalsText(m)</td>
                            </tr>
                        }
                    }
                </tbody>
            </table>
        </div>
    }
    else
    {
        <div style="max-height:320px; overflow-y:auto;">
            <table class="table table-sm table-hover font-monospace mb-0">
                <thead class="table-light sticky-top">
                    <tr>
                        <th>ID</th>
                        <th>DLC</th>
                        <th>Data</th>
                        <th>Signals</th>
                        <th>Timestamp (µs)</th>
                        <th class="text-end">Count</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var m in GetLatestRows())
                    {
                        <tr>
                            <td>@($"0x{m.Id:X3}")</td>
                            <td>@m.Dlc</td>
                            <td>@(m.Data.Length > 0 ? BitConverter.ToString(m.Data).Replace("-", " ") : "–")</td>
                            <td>@SignalsText(m)</td>
                            <td>@m.TimestampUs</td>
                            <td class="text-end">@updateCounts.GetValueOrDefault(m.Id)</td>
                        </tr>
                    }
                </tbody>
            </table>
        </div>
    }
</div>

@* ── Filters + Send (two columns) ──────────────────────────────────────────── *@
<div class="row g-3 mb-3">

    @* Filters *@
    <div class="col-lg-7">
        <div class="card h-100">
            <div class="card-header">
                Filters
                <span class="text-muted small ms-2">
                    @(filters.Count == 0 ? "(no filters — all messages pass)" : $"({filters.Count} active)")
                </span>
            </div>
            <div class="card-body">
                @if (filters.Count > 0)
                {
                    <table class="table table-sm mb-2">
                        <thead><tr><th>ID</th><th>Mask</th><th>Description</th><th></th></tr></thead>
                        <tbody>
                            @foreach (var f in filters)
                            {
                                <tr>
                                    <td class="font-monospace">@($"0x{f.MessageId:X3}")</td>
                                    <td class="font-monospace">@($"0x{f.Mask:X3}")</td>
                                    <td>@f.Description</td>
                                    <td>
                                        <button class="btn btn-sm btn-outline-danger py-0 px-2"
                                                @onclick="() => RemoveFilter(f.Id)">×</button>
                                    </td>
                                </tr>
                            }
                        </tbody>
                    </table>
                }

                <div class="row g-1 align-items-end">
                    <div class="col-3">
                        <label class="form-label small mb-0">ID (hex)</label>
                        <input class="form-control form-control-sm font-monospace"
                               placeholder="e.g. 100" @bind="newFilterId" />
                    </div>
                    <div class="col-3">
                        <label class="form-label small mb-0">Mask (hex)</label>
                        <input class="form-control form-control-sm font-monospace"
                               placeholder="e.g. 7FF" @bind="newFilterMask" />
                    </div>
                    <div class="col-4">
                        <label class="form-label small mb-0">Description</label>
                        <input class="form-control form-control-sm"
                               placeholder="optional" @bind="newFilterDesc" />
                    </div>
                    <div class="col-2">
                        <button class="btn btn-sm btn-primary w-100" @onclick="AddFilter">Add</button>
                    </div>
                </div>

                @if (filterError != null)
                {
                    <div class="text-danger small mt-1">@filterError</div>
                }
                @if (filters.Count > 0)
                {
                    <button class="btn btn-sm btn-outline-danger mt-2" @onclick="ClearFilters">Clear all</button>
                }
            </div>
        </div>
    </div>

    @* Send *@
    <div class="col-lg-5">
        <div class="card h-100">
            <div class="card-header">Send Message</div>
            <div class="card-body">
                <div class="mb-2">
                    <label class="form-label small mb-0">CAN ID (hex)</label>
                    <input class="form-control form-control-sm font-monospace"
                           placeholder="e.g. 7FF" @bind="sendIdHex" />
                </div>
                <div class="mb-2">
                    <label class="form-label small mb-0">Data bytes (hex, space-separated)</label>
                    <input class="form-control form-control-sm font-monospace"
                           placeholder="e.g. DE AD BE EF" @bind="sendDataHex" />
                </div>
                <div class="form-check mb-2">
                    <input class="form-check-input" type="checkbox" id="extFrame" @bind="sendExtended" />
                    <label class="form-check-label small" for="extFrame">Extended frame (29-bit ID)</label>
                </div>
                <div class="d-flex align-items-center gap-2">
                    <button class="btn btn-sm btn-success" @onclick="SendMessage">Send</button>
                    @if (sendFeedback != null)
                    {
                        <span class="small @(sendOk ? "text-success" : "text-danger")">@sendFeedback</span>
                    }
                </div>
            </div>
        </div>
    </div>
</div>

@* ── Bitmasks ───────────────────────────────────────────────────────────────── *@
<div class="card">
    <div class="card-header">Signal Bitmasks</div>
    <div class="card-body">
        @if (bitmasks.Count == 0)
        {
            <p class="text-muted small mb-2">
                No bitmasks configured. Add one to extract named signal values from a message's data bytes.<br />
                <em>Formula: physicalValue = ((rawUint64 &amp; DataMask) &gt;&gt; RightShift) × Scale + Offset</em>
            </p>
        }
        else
        {
            <table class="table table-sm mb-2">
                <thead>
                    <tr>
                        <th>Msg ID</th><th>Signal</th><th>DataMask (64-bit)</th>
                        <th>Shift</th><th>Scale</th><th>Offset</th><th>Unit</th><th></th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var b in bitmasks)
                    {
                        <tr>
                            <td class="font-monospace">@($"0x{b.MessageId:X3}")</td>
                            <td>@b.SignalName</td>
                            <td class="font-monospace small">@($"0x{b.DataMask:X16}")</td>
                            <td>@b.RightShift</td>
                            <td>@b.Scale</td>
                            <td>@b.Offset</td>
                            <td>@b.Unit</td>
                            <td>
                                <button class="btn btn-sm btn-outline-danger py-0 px-2"
                                        @onclick="() => RemoveBitmask(b.Id)">×</button>
                            </td>
                        </tr>
                    }
                </tbody>
            </table>
        }

        <div class="row g-1 align-items-end">
            <div class="col-sm-2">
                <label class="form-label small mb-0">Msg ID (hex)</label>
                <input class="form-control form-control-sm font-monospace" placeholder="e.g. 100" @bind="newBmMsgId" />
            </div>
            <div class="col-sm-2">
                <label class="form-label small mb-0">Signal name</label>
                <input class="form-control form-control-sm" placeholder="e.g. EngineRPM" @bind="newBmName" />
            </div>
            <div class="col-sm-3">
                <label class="form-label small mb-0">DataMask (64-bit hex)</label>
                <input class="form-control form-control-sm font-monospace" placeholder="e.g. 00FF000000000000" @bind="newBmMask" />
            </div>
            <div class="col-sm-1">
                <label class="form-label small mb-0">Shift</label>
                <input class="form-control form-control-sm" @bind="newBmShift" />
            </div>
            <div class="col-sm-1">
                <label class="form-label small mb-0">Scale</label>
                <input class="form-control form-control-sm" @bind="newBmScale" />
            </div>
            <div class="col-sm-1">
                <label class="form-label small mb-0">Offset</label>
                <input class="form-control form-control-sm" @bind="newBmOffset" />
            </div>
            <div class="col-sm-1">
                <label class="form-label small mb-0">Unit</label>
                <input class="form-control form-control-sm" placeholder="rpm" @bind="newBmUnit" />
            </div>
            <div class="col-sm-1">
                <button class="btn btn-sm btn-primary w-100" @onclick="AddBitmask">Add</button>
            </div>
        </div>

        @if (bitmaskError != null)
        {
            <div class="text-danger small mt-1">@bitmaskError</div>
        }
        @if (bitmasks.Count > 0)
        {
            <button class="btn btn-sm btn-outline-danger mt-2" @onclick="ClearBitmasks">Clear all</button>
        }
    </div>
</div>

@code {
    // ── Connection form ───────────────────────────────────────────────────────
    private List<PcanChannel> availableChannels = [];
    private string selectedChannel = "";
    private string selectedBitrate = Bitrate.Pcan500.ToString();
    private bool showConnectionForm;
    private bool isConnecting;
    private string? connectError;

    private static readonly (Bitrate Value, string Label)[] BitrateOptions =
    [
        (Bitrate.Pcan1000, "1000 kbps"),
        (Bitrate.Pcan800,  "800 kbps"),
        (Bitrate.Pcan500,  "500 kbps"),
        (Bitrate.Pcan250,  "250 kbps"),
        (Bitrate.Pcan125,  "125 kbps"),
        (Bitrate.Pcan100,  "100 kbps"),
        (Bitrate.Pcan50,   "50 kbps"),
        (Bitrate.Pcan20,   "20 kbps"),
        (Bitrate.Pcan10,   "10 kbps"),
        (Bitrate.Pcan5,    "5 kbps"),
    ];

    // ── Data ──────────────────────────────────────────────────────────────────
    private readonly List<CanMessageDto> messages = [];
    private readonly Dictionary<uint, CanMessageDto> latestMessages = new();
    private readonly Dictionary<uint, int> updateCounts = new();
    private string msgTab = "stream";
    private const int MaxMessages = 200;
    private List<CanFilter> filters = [];
    private List<CanBitmask> bitmasks = [];

    // ── Filter form ───────────────────────────────────────────────────────────
    private string newFilterId = "";
    private string newFilterMask = "7FF";
    private string newFilterDesc = "";
    private string? filterError;

    // ── Bitmask form ──────────────────────────────────────────────────────────
    private string newBmMsgId = "";
    private string newBmName = "";
    private string newBmMask = "";
    private int newBmShift = 0;
    private double newBmScale = 1.0;
    private double newBmOffset = 0.0;
    private string newBmUnit = "";
    private string? bitmaskError;

    // ── Send form ─────────────────────────────────────────────────────────────
    private string sendIdHex = "";
    private string sendDataHex = "";
    private bool sendExtended;
    private string? sendFeedback;
    private bool sendOk;

    // ── Lifecycle ─────────────────────────────────────────────────────────────

    private Timer? _updateTimer;

    protected override void OnInitialized()
    {
        filters = CanService.Filters.ToList();
        bitmasks = CanService.Bitmasks.ToList();
        
        // Start timer for polling CAN messages
        _updateTimer = new Timer(UpdateMessages, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(100));

        ScanChannels();
        if (CanService.IsConnected)
        {
            selectedChannel = CanService.ChannelName;
            selectedBitrate = CanService.CurrentBitrate.ToString();
        }
        else
        {
            showConnectionForm = true;
        }
    }

    private void UpdateMessages(object? state)
    {
        InvokeAsync(() =>
        {
            var newMessages = CanService.GetLatestMessages();
            bool hasChanges = false;
            
            lock (latestMessages)
            {
                foreach (var (id, msg) in newMessages)
                {
                    if (!latestMessages.TryGetValue(id, out var existing) ||
                        msg.TimestampUs > existing.TimestampUs)
                    {
                        latestMessages[id] = msg;
                        updateCounts[id] = updateCounts.GetValueOrDefault(id) + 1;
                        
                        // Add to stream messages
                        lock (messages)
                        {
                            messages.Insert(0, msg);
                            if (messages.Count > MaxMessages)
                                messages.RemoveAt(messages.Count - 1);
                        }
                        
                        hasChanges = true;
                    }
                }
            }
            
            if (hasChanges)
            {
                StateHasChanged();
            }
        });
    }

    public void Dispose() => _updateTimer?.Dispose();

    // ── Connection ────────────────────────────────────────────────────────────

    private void ScanChannels()
    {
        availableChannels = CanService.GetAvailableChannels().ToList();
        if (availableChannels.Count > 0 &&
            !availableChannels.Any(c => c.ToString() == selectedChannel))
            selectedChannel = availableChannels[0].ToString();
    }

    private async Task Connect()
    {
        connectError = null;
        if (!Enum.TryParse<PcanChannel>(selectedChannel, out var channel))
        { connectError = "Please select a channel."; return; }
        if (!Enum.TryParse<Bitrate>(selectedBitrate, out var bitrate))
        { connectError = "Please select a bitrate."; return; }

        isConnecting = true;
        CanService.RequestReinitialize(channel, bitrate);

        // Poll until the worker completes the reinit (typically < 50 ms)
        for (int i = 0; i < 20; i++)
        {
            await Task.Delay(100);
            if (CanService.IsConnected) break;
        }

        isConnecting = false;
        if (CanService.IsConnected)
            showConnectionForm = false;
        else
            connectError = "Connection failed. Check the adapter is plugged in and try again.";

        StateHasChanged();
    }

    private static string BitrateLabel(Bitrate b) => b switch
    {
        Bitrate.Pcan1000 => "1000 kbps",
        Bitrate.Pcan800  => "800 kbps",
        Bitrate.Pcan500  => "500 kbps",
        Bitrate.Pcan250  => "250 kbps",
        Bitrate.Pcan125  => "125 kbps",
        Bitrate.Pcan100  => "100 kbps",
        Bitrate.Pcan50   => "50 kbps",
        Bitrate.Pcan20   => "20 kbps",
        Bitrate.Pcan10   => "10 kbps",
        Bitrate.Pcan5    => "5 kbps",
        _                => b.ToString()
    };

    // ── Messages ──────────────────────────────────────────────────────────────

    private void ClearMessages()
    {
        lock (messages) messages.Clear();
        lock (latestMessages) { latestMessages.Clear(); updateCounts.Clear(); }
    }

    // ── Filters ───────────────────────────────────────────────────────────────

    private void AddFilter()
    {
        filterError = null;
        if (!TryParseHex32(newFilterId, out uint msgId))
        { filterError = "Invalid ID — enter a hex value e.g. 100"; return; }
        if (!TryParseHex32(newFilterMask, out uint mask))
        { filterError = "Invalid mask — enter a hex value e.g. 7FF"; return; }

        var filter = new CanFilter { MessageId = msgId, Mask = mask, Description = newFilterDesc };
        CanService.AddFilter(filter);
        filters = CanService.Filters.ToList();
        newFilterId = ""; newFilterMask = "7FF"; newFilterDesc = "";
    }

    private void RemoveFilter(Guid id)
    {
        CanService.RemoveFilter(id);
        filters = CanService.Filters.ToList();
    }

    private void ClearFilters()
    {
        CanService.ClearFilters();
        filters = [];
    }

    // ── Bitmasks ──────────────────────────────────────────────────────────────

    private void AddBitmask()
    {
        bitmaskError = null;
        if (!TryParseHex32(newBmMsgId, out uint msgId))
        { bitmaskError = "Invalid Msg ID"; return; }
        if (string.IsNullOrWhiteSpace(newBmName))
        { bitmaskError = "Signal name is required"; return; }
        if (!TryParseHex64(newBmMask, out ulong mask))
        { bitmaskError = "Invalid 64-bit mask — enter hex e.g. 00FF000000000000"; return; }

        var bitmask = new CanBitmask
        {
            MessageId = msgId,
            SignalName = newBmName.Trim(),
            DataMask = mask,
            RightShift = newBmShift,
            Scale = newBmScale,
            Offset = newBmOffset,
            Unit = string.IsNullOrWhiteSpace(newBmUnit) ? null : newBmUnit
        };
        CanService.AddBitmask(bitmask);
        bitmasks = CanService.Bitmasks.ToList();
        newBmMsgId = ""; newBmName = ""; newBmMask = "";
        newBmShift = 0; newBmScale = 1.0; newBmOffset = 0.0; newBmUnit = "";
    }

    private void RemoveBitmask(Guid id)
    {
        CanService.RemoveBitmask(id);
        bitmasks = CanService.Bitmasks.ToList();
    }

    private void ClearBitmasks()
    {
        CanService.ClearBitmasks();
        bitmasks = [];
    }

    // ── Send ──────────────────────────────────────────────────────────────────

    private void SendMessage()
    {
        sendFeedback = null;
        if (!TryParseHex32(sendIdHex, out uint id))
        { sendFeedback = "Invalid ID"; sendOk = false; return; }

        byte[] data;
        try
        {
            var hex = (sendDataHex ?? "").Replace(" ", "");
            data = hex.Length > 0 ? Convert.FromHexString(hex) : [];
        }
        catch { sendFeedback = "Invalid hex data"; sendOk = false; return; }

        if (data.Length > 8)
        { sendFeedback = "Max 8 bytes"; sendOk = false; return; }

        var msg = new PcanMessage
        {
            ID = id,
            MsgType = sendExtended ? MessageType.Extended : MessageType.Standard,
            DLC = (byte)data.Length,
            Data = new byte[8]
        };
        data.CopyTo(msg.Data, 0);

        var result = CanService.Write(msg);
        sendOk = result == PcanStatus.OK;
        sendFeedback = sendOk ? "Sent!" : $"Error: {result}";
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    private List<CanMessageDto> GetLatestRows()
    {
        lock (latestMessages)
            return latestMessages.Values.OrderBy(m => m.Id).ToList();
    }

    private static string SignalsText(CanMessageDto m)
    {
        if (m.Signals is null || m.Signals.Count == 0) return "–";
        return string.Join(", ", m.Signals.Select(kv => $"{kv.Key}={kv.Value:G4}"));
    }

    private static bool TryParseHex32(string? s, out uint value)
    {
        value = 0;
        if (string.IsNullOrWhiteSpace(s)) return false;
        var hex = s.Trim();
        if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) hex = hex[2..];
        return uint.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out value);
    }

    private static bool TryParseHex64(string? s, out ulong value)
    {
        value = 0;
        if (string.IsNullOrWhiteSpace(s)) return false;
        var hex = s.Trim();
        if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) hex = hex[2..];
        return ulong.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out value);
    }
}
