package com.foxx.androidcast.diagnostics;

/*********************************************************************
 * CastDiagnosticsFormatter.java
 * Created at: Sat 16 May 2026 13:35:48 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class CastDiagnosticsFormatter
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (5 commits, 516 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	97b7c3221465ac0650199c90cb2a6cd2d8b1dfb27bffa97447139936130aad34
**********************************************************************/
import com.foxx.androidcast.CastSettings;
import com.foxx.androidcast.CastTuningEngine;
import com.foxx.androidcast.media.CodecNegotiator;
import com.foxx.androidcast.media.codec.CodecSessionRegistry;
import com.foxx.androidcast.media.codec.PassthroughCodecPolicy;
import com.foxx.androidcast.media.codec.jni.NativeCodecBridge;
import com.foxx.androidcast.network.control.NetworkStatsSnapshot;
import com.foxx.androidcast.network.diag.DiagPingMetrics;
import com.foxx.androidcast.receiver.StreamMetrics;

import java.util.ArrayList;
import java.util.List;

/** Builds HTML diagnostics overlay with red highlights for out-of-range metrics. */
public final class CastDiagnosticsFormatter {
    private static final String COLOR_OK = "#E0FFE0";
    private static final String COLOR_WARN = "#FF6666";
    private static final String COLOR_DIM = "#AAAAAA";

    private CastDiagnosticsFormatter() {}

    public static String formatHtml(StreamMetrics metrics, String transport, String videoCodec,
            int streamW, int streamH, int renderW, int renderH, boolean idle,
            CastSettings localSettings, CastSettings remoteSettings,
            NetworkStatsSnapshot peer, NetworkStatsSnapshot lastLocal, int viewZoomPercent,
            CastMtuSampler.Info mtuInfo) {
        float inFps = metrics.getInFps();
        float renderFps = metrics.getRenderFps();
        int bwInKbps = metrics.getBwInKbps();
        long videoPackets = metrics.getVideoPackets();
        long audioPackets = metrics.getAudioPackets();
        long audioRendered = metrics.getAudioRenderedWrites();
        long audioPcmSilent = metrics.getAudioPcmSilentBlocks();
        long audioPcmAudible = metrics.getAudioPcmAudibleBlocks();
        int audioVu = metrics.getAudioVuPercent();
        float audioPeakDb = metrics.getAudioLastPeakDb();
        float audioRmsDb = metrics.getAudioLastRmsDb();
        long keyFrames = metrics.getKeyFrames();
        long deltaFrames = metrics.getDeltaFrames();
        int avgKb = metrics.getAvgKbPerFrame();
        long idleMs = metrics.getLastFrameIdleMs();
        int rttMs = metrics.getPeerRttMs();

        CastSettings tuneSettings = remoteSettings != null ? remoteSettings : localSettings;
        CastTuningEngine.EffectiveParams eff = CastTuningEngine.resolve(tuneSettings, peer);
        int suggestedBwKbps = eff.videoBitrate / 1000;
        int targetEncKbps = eff.videoBitrate / 1000;
        int congestionLevel = 0;
        float peerEncodeFps = 0f;
        int peerSendKbps = metrics.getPeerSendKbps();
        int peerRecvKbps = metrics.getPeerRecvKbps();
        int peerQueue = 0;
        int peerDroppedP = 0;
        long peerAudioFrames = 0;
        boolean peerAudioActive = false;
        int peerCaptureVu = 0;
        long peerCaptureSilent = 0;
        long peerCaptureAudible = 0;
        float peerCapturePeakDb = -96f;
        float peerCaptureRmsDb = -96f;

        if (peer != null) {
            if (peer.rttMs > 0) {
                rttMs = peer.rttMs;
            }
            peerSendKbps = peer.sendBitrateKbps;
            peerRecvKbps = peer.recvBitrateKbps;
            peerEncodeFps = peer.encodeFps;
            peerQueue = peer.encodeQueueDepth;
            peerDroppedP = peer.droppedPFrames;
            peerAudioFrames = peer.audioFramesSent;
            peerAudioActive = peer.audioActive;
            congestionLevel = peer.congestionLevel;
            if (peer.suggestedBitrateKbps > 0) {
                suggestedBwKbps = peer.suggestedBitrateKbps;
            }
            if (peer.targetBitrateKbps > 0) {
                targetEncKbps = peer.targetBitrateKbps;
            }
            peerCaptureVu = peer.audioCaptureVuPercent;
            peerCaptureSilent = peer.audioCaptureSilentBlocks;
            peerCaptureAudible = peer.audioCaptureAudibleBlocks;
            peerCapturePeakDb = peer.audioCapturePeakDb;
            peerCaptureRmsDb = peer.audioCaptureRmsDb;
        }
        if (lastLocal != null) {
            congestionLevel = Math.max(congestionLevel, lastLocal.congestionLevel);
        }

        boolean audioRequested = remoteSettings != null && remoteSettings.isAudioRequested();
        List<String> hints = buildHints(inFps, renderFps, bwInKbps, rttMs, videoPackets);

        int quality = CastDiagnosticsScorer.compute(inFps, renderFps, bwInKbps, suggestedBwKbps,
                rttMs, congestionLevel, audioPackets, audioRequested);
        String qualityColor = quality >= 70 ? COLOR_OK : (quality >= 45 ? "#FFCC66" : COLOR_WARN);

        String bitrateMode = CastDiagnosticsLabels.bitrateMode(
                localSettings != null ? localSettings.getBitrateMode() : CastSettings.BitrateMode.AUTO);
        String netAdopt = CastDiagnosticsLabels.networkAdoption(
                localSettings != null ? localSettings.getNetworkAdoption() : CastSettings.NetworkAdoption.AUTO);
        String ctrl = CastDiagnosticsLabels.congestionController();

        StringBuilder html = new StringBuilder();
        html.append("<font color='").append(COLOR_DIM).append("'>Transport: ")
                .append(escape(transport != null ? transport.toUpperCase() : "?"))
                .append("</font><br/>");
        appendMtuSection(html, mtuInfo);

        if (videoCodec != null && !videoCodec.isEmpty()) {
            html.append("Codec: <font color='").append(COLOR_OK).append("'>")
                    .append(escape(CodecNegotiator.displayName(videoCodec))).append("</font><br/>");
        }
        appendCodecStackSection(html, localSettings, remoteSettings);

        html.append("Stream: ").append(streamW).append('x').append(streamH);
        if (viewZoomPercent != 0) {
            html.append(" (").append(viewZoomPercent > 0 ? "+" : "").append(viewZoomPercent).append("%)");
        }
        if (renderW > 0 && renderH > 0 && (renderW != streamW || renderH != streamH)) {
            html.append("  render: ").append(renderW).append('x').append(renderH);
        }
        html.append("<br/>");

        html.append("In FPS: ").append(colored(formatFloat(inFps), inFps > 0 && inFps < 15f));
        html.append("  Render FPS: ").append(colored(formatFloat(renderFps),
                inFps > 8f && renderFps > 0 && renderFps < inFps * 0.75f));
        html.append("<br/>");

        html.append("BW in: ").append(colored(bwInKbps > 0 ? bwInKbps + " kbps" : "n/a",
                suggestedBwKbps > 0 && bwInKbps > 0 && bwInKbps < suggestedBwKbps * 0.7f));
        html.append("  RTT: ").append(colored(rttMs > 0 ? rttMs + " ms" : "n/a", rttMs > 350));
        DiagPingMetrics.Snapshot diagPing = metrics.getDiagPing();
        if (diagPing.enabled && diagPing.pongsRx > 0) {
            int pingRtt = diagPing.avgRttMs > 0 ? diagPing.avgRttMs : diagPing.lastRttMs;
            html.append("  Diag ping: ")
                    .append(colored(pingRtt + " ms avg (" + diagPing.pongsRx + ")", pingRtt > 350));
        }
        html.append("<br/>");

        html.append("Suggested BW: ").append(colored(
                suggestedBwKbps > 0 ? suggestedBwKbps + " kbps" : "n/a",
                bwInKbps > 0 && suggestedBwKbps > 0 && bwInKbps < suggestedBwKbps * 0.85f));
        html.append("  Target enc: ").append(colored(
                targetEncKbps > 0 ? targetEncKbps + " kbps" : "n/a", false));
        html.append("<br/>");

        html.append("Congestion: ").append(colored(String.valueOf(congestionLevel), congestionLevel > 0));
        html.append("<br/>");

        appendUdpLossSection(html, peer, lastLocal);
        appendProtectionSection(html, peer, lastLocal);

        html.append("Ctrl: ").append(escape(ctrl));
        html.append("<br/>");
        html.append("Mode: ").append(escape(bitrateMode));
        html.append("<br/>");
        html.append("Net: ").append(escape(netAdopt));
        html.append("<br/>");

        if (peerSendKbps > 0 || peerRecvKbps > 0 || peerEncodeFps > 0) {
            html.append("<font color='").append(COLOR_DIM).append("'>Sender:</font> ");
            if (peerEncodeFps > 0) {
                html.append("enc ").append(colored(formatFloat(peerEncodeFps), peerEncodeFps < 12f));
                html.append(' ');
            }
            html.append("tx ").append(peerSendKbps).append(" / rx ").append(peerRecvKbps).append(" kbps");
            if (peerQueue > 8 || peerDroppedP > 0) {
                html.append("  q").append(peerQueue);
                if (peerDroppedP > 0) {
                    html.append(" dropP=").append(peerDroppedP);
                }
            }
            html.append("<br/>");
            if (audioRequested && (peerCaptureSilent > 0 || peerCaptureAudible > 0 || peerCaptureVu > 0)) {
                html.append("Sender cap VU: ").append(vuBarHtml(peerCaptureVu)).append(' ')
                        .append(colored(peerCaptureVu + "%",
                                peerCaptureVu < 3 && peerCaptureSilent > peerCaptureAudible));
                html.append("  peak ").append(formatDb(peerCapturePeakDb))
                        .append("  rms ").append(formatDb(peerCaptureRmsDb));
                html.append("<br/>Sender PCM: silent ")
                        .append(colored(String.valueOf(peerCaptureSilent),
                                peerCaptureSilent > 20 && peerCaptureAudible == 0))
                        .append("  audible ").append(String.valueOf(peerCaptureAudible));
                if (peerCaptureSilent > 20 && peerCaptureAudible == 0) {
                    html.append(" <font color='").append(COLOR_WARN).append("'>")
                            .append("(sender capture silent)")
                            .append("</font>");
                }
                html.append("<br/>");
            }
        }

        if (remoteSettings != null) {
            html.append("<font color='").append(COLOR_DIM).append("'>Sender prefs:</font><br/>")
                    .append(escape(CastDiagnosticsLabels.adjustmentPreset(remoteSettings.getAdjustmentPreset())))
                    .append("<br/>")
                    .append(escape(CastDiagnosticsLabels.bitrateMode(remoteSettings.getBitrateMode())))
                    .append("<br/>")
                    .append(escape(CastDiagnosticsLabels.resolution(remoteSettings.getResolution())))
                    .append("<br/>")
                    .append(escape(CastDiagnosticsLabels.videoCodec(remoteSettings.getVideoCodec())))
                    .append("<br/>")
                    .append(escape(CastDiagnosticsLabels.audioCodec(remoteSettings.getAudioCodec())))
                    .append("<br/>")
                    .append(escape(CastDiagnosticsLabels.captureMode(remoteSettings.getCaptureMode())));
            if (audioRequested) {
                html.append(" · audio");
                if (peerAudioActive) {
                    html.append(" (").append(peerAudioFrames).append(" frm)");
                }
            }
            html.append("<br/>");
        }

        html.append("Video pkts: ").append(videoPackets);
        html.append(" (IDR ").append(keyFrames).append(" / P ").append(deltaFrames).append(')');
        if (videoPackets > 0) {
            html.append("  ~").append(Math.max(1, avgKb)).append(" KB/frame");
        }
        html.append("<br/>");

        html.append("Audio pkts: ").append(colored(String.valueOf(audioPackets),
                audioRequested && audioPackets == 0 && videoPackets > 20));
        html.append("  rendered: ").append(colored(String.valueOf(audioRendered),
                audioRequested && audioPackets > 5 && audioRendered == 0));
        if (audioRequested && audioPackets > 0 && audioRendered == 0) {
            html.append(" (check receiver volume)");
        }
        if (audioRequested && audioPackets == 0 && videoPackets > 20) {
            html.append(" <font color='").append(COLOR_DIM).append("'>")
                    .append("(play media on sender)")
                    .append("</font>");
        } else if (audioRequested && audioPackets > 10 && audioRendered == 0) {
            html.append(" <font color='").append(COLOR_WARN).append("'>")
                    .append("(decode/render stalled)")
                    .append("</font>");
        } else if (audioRequested && audioRendered > 15 && audioPcmAudible == 0
                && audioPcmSilent > 15) {
            html.append(" <font color='").append(COLOR_WARN).append("'>")
                    .append("(decoded PCM is silence)")
                    .append("</font>");
            if (peerCaptureSilent > 20 && peerCaptureAudible == 0) {
                html.append(" <font color='").append(COLOR_WARN).append("'>")
                        .append("(fix on sender: Choose app, Android 14+)")
                        .append("</font>");
            }
        }
        html.append("<br/>");

        if (audioRequested && audioRendered > 0) {
            html.append("Audio VU: ").append(vuBarHtml(audioVu)).append(' ')
                    .append(colored(audioVu + "%", audioVu < 3 && audioPcmSilent > audioPcmAudible));
            html.append("  peak ").append(formatDb(audioPeakDb))
                    .append("  rms ").append(formatDb(audioRmsDb));
            html.append("<br/>PCM blocks: silent ")
                    .append(colored(String.valueOf(audioPcmSilent),
                            audioPcmSilent > 20 && audioPcmAudible == 0))
                    .append("  audible ").append(String.valueOf(audioPcmAudible));
            html.append("<br/>");
        }

        CpuLoadSampler.Sample cpu = CpuLoadSampler.sample();
        html.append("CPU: ").append(vuBarHtml(cpu.loadPercent)).append(' ')
                .append(colored(cpu.loadPercent + "%", cpu.loadPercent > 85))
                .append(' ').append(CpuLoadSampler.formatCoreSuffix(cpu));
        html.append("<br/>");

        for (String hint : hints) {
            html.append("<font color='").append(COLOR_WARN).append("'>")
                    .append(escape(hint)).append("</font><br/>");
        }

        html.append("Quality: <font color='").append(qualityColor).append("'>")
                .append(quality).append(" — ").append(CastDiagnosticsScorer.label(quality))
                .append("</font><br/>");

        html.append("<font color='").append(COLOR_DIM).append("'>State: ")
                .append(idle ? "idle (awaiting)" : "streaming");
        if (idleMs >= 0) {
            html.append("  last frame: ").append(idleMs).append(" ms ago");
        }
        html.append("</font>");

        return html.toString();
    }

    private static void appendMtuSection(StringBuilder html, CastMtuSampler.Info mtu) {
        if (mtu == null) {
            return;
        }
        html.append("<font color='").append(COLOR_DIM).append("'>MTU:</font> ");
        if (mtu.interfaceName.length() > 0) {
            html.append(escape(mtu.interfaceName)).append(' ');
        }
        if (mtu.linkMtu > 0) {
            html.append("link ").append(mtu.linkMtu);
        } else {
            html.append("link ?");
        }
        html.append(" · dg ").append(mtu.castDatagramBytes);
        html.append(" frag ").append(mtu.castFragPayloadBytes);
        html.append(" · IPv4 ").append(mtu.ipv4WireBytes);
        if (mtu.headroomBytes >= 0) {
            html.append(" · room ").append(colored(String.valueOf(mtu.headroomBytes),
                    mtu.headroomBytes < 100));
        }
        html.append("<br/>");
    }

    private static void appendUdpLossSection(StringBuilder html, NetworkStatsSnapshot peer,
            NetworkStatsSnapshot local) {
        long rx = pick(peer, local, true, false);
        long tx = pick(peer, local, true, true);
        long lost = val(peer, local, p -> p.udpReassemblyLost, l -> l.udpReassemblyLost);
        long invalid = val(peer, local, p -> p.udpDatagramsInvalid, l -> l.udpDatagramsInvalid);
        long dup = val(peer, local, p -> p.udpFragmentsDuplicate, l -> l.udpFragmentsDuplicate);
        long wireDrop = val(peer, local, p -> p.udpWireDropped, l -> l.udpWireDropped);
        long skipKey = val(peer, local, p -> p.recvVideoSkippedKey, l -> l.recvVideoSkippedKey);
        long vDecErr = val(peer, local, p -> p.recvVideoDecodeErrors, l -> l.recvVideoDecodeErrors);
        long aDecErr = val(peer, local, p -> p.recvAudioDecodeErrors, l -> l.recvAudioDecodeErrors);
        long aDrop = val(peer, local, p -> p.recvAudioQueueDrops, l -> l.recvAudioQueueDrops);
        long dropAudio = val(peer, local, p -> (long) p.droppedAudioFrames, l -> (long) l.droppedAudioFrames);

        if (rx == 0 && tx == 0 && lost == 0 && invalid == 0) {
            return;
        }
        html.append("<font color='").append(COLOR_DIM).append("'>UDP:</font> ");
        html.append("rx ").append(rx).append(" / tx ").append(tx).append(" dg");
        if (lost > 0) {
            html.append("  ").append(colored("reasm-lost " + lost, true));
        }
        if (invalid > 0) {
            html.append("  ").append(colored("bad " + invalid, true));
        }
        if (dup > 0) {
            html.append(" dup ").append(dup);
        }
        if (wireDrop > 0) {
            html.append("  ").append(colored("sess-drop " + wireDrop, true));
        }
        html.append("<br/>");
        if (skipKey > 0 || vDecErr > 0 || aDecErr > 0 || aDrop > 0 || dropAudio > 0) {
            html.append("App drops: ");
            if (skipKey > 0) {
                html.append(colored("vid-wait-key " + skipKey, skipKey > 50));
            }
            if (vDecErr > 0) {
                html.append(' ').append(colored("vid-dec " + vDecErr, true));
            }
            if (aDecErr > 0) {
                html.append(' ').append(colored("aud-dec " + aDecErr, true));
            }
            if (aDrop > 0) {
                html.append(' ').append(colored("aud-drop " + aDrop, true));
            }
            if (dropAudio > 0) {
                html.append(' ').append(colored("snd-aud " + dropAudio, true));
            }
            html.append("<br/>");
        }
    }

    private static void appendCodecStackSection(StringBuilder html, CastSettings localSettings,
            CastSettings remoteSettings) {
        CastSettings prefs = remoteSettings != null ? remoteSettings : localSettings;
        html.append("<font color='").append(COLOR_DIM).append("'>Video stack:</font> ");
        html.append(escape(CodecSessionRegistry.getVideoBackend().label));
        html.append(" · pref ");
        html.append(escape(PassthroughCodecPolicy.displayVideoPreference(
                prefs != null ? prefs.getVideoCodec() : CodecSessionRegistry.getVideoPreference())));
        String wire = CodecSessionRegistry.getNegotiatedVideoMime();
        if (wire != null && !wire.isEmpty()) {
            html.append(" · wire ").append(escape(CodecNegotiator.displayName(wire)));
        }
        html.append("<br/>");
        html.append("<font color='").append(COLOR_DIM).append("'>Audio stack:</font> ");
        html.append(escape(CodecSessionRegistry.getAudioBackend().label));
        html.append(" · pref ");
        html.append(escape(PassthroughCodecPolicy.displayAudioPreference(
                prefs != null ? prefs.getAudioCodec() : CodecSessionRegistry.getAudioPreference())));
        if (prefs != null && prefs.isAudioRequested()) {
            CastSettings.AudioCodec ac = prefs.getAudioCodec();
            if (ac == CastSettings.AudioCodec.OPUS) {
                if (NativeCodecBridge.isOpusAvailable()) {
                    html.append(" · <font color='").append(COLOR_OK).append("'>Opus in-band FEC</font>");
                } else {
                    html.append(" · <font color='").append(COLOR_WARN).append("'>Opus fallback AAC</font>");
                }
            } else if (ac == CastSettings.AudioCodec.SPEEX) {
                if (NativeCodecBridge.isSpeexAvailable()) {
                    html.append(" · <font color='").append(COLOR_OK).append("'>Speex FEC</font>");
                } else {
                    html.append(" · <font color='").append(COLOR_WARN).append("'>Speex fallback AAC</font>");
                }
            } else {
                html.append(" · AAC-LC (MediaCodec)");
            }
        }
        html.append("<br/>");
    }

    private static void appendProtectionSection(StringBuilder html, NetworkStatsSnapshot peer,
            NetworkStatsSnapshot local) {
        String mode = str(peer, local, p -> p.protectionMode, l -> l.protectionMode, "NONE");
        String summary = CodecSessionRegistry.getProtectionSummary();
        html.append("<font color='").append(COLOR_DIM).append("'>Protect:</font> ");
        html.append(escape(mode));
        if (summary != null && !summary.isEmpty()) {
            html.append(" · ").append(escape(summary));
        }
        long fecPkts = val(peer, local, p -> p.fecStubPackets, l -> l.fecStubPackets);
        long fecOk = val(peer, local, p -> p.fecPacketsDecoded, l -> l.fecPacketsDecoded);
        long fecFail = val(peer, local, p -> p.fecDecodeFailures, l -> l.fecDecodeFailures);
        long nackReq = val(peer, local, p -> p.nackStubRequests, l -> l.nackStubRequests);
        if (fecPkts > 0 || fecOk > 0 || fecFail > 0) {
            html.append("<br/>FEC enc=").append(fecPkts).append(" dec=").append(fecOk);
            if (fecFail > 0) {
                html.append(" <font color='").append(COLOR_WARN).append("'>fail=").append(fecFail)
                        .append("</font>");
            }
        }
        if (nackReq > 0) {
            html.append("<br/>NACK events=").append(nackReq);
        }
        html.append("<br/>");
    }

    private static long pick(NetworkStatsSnapshot peer, NetworkStatsSnapshot local,
            boolean preferLocal, boolean tx) {
        if (preferLocal && local != null) {
            return tx ? local.udpDatagramsTx : local.udpDatagramsRx;
        }
        if (peer != null) {
            return tx ? peer.udpDatagramsTx : peer.udpDatagramsRx;
        }
        return local != null ? (tx ? local.udpDatagramsTx : local.udpDatagramsRx) : 0;
    }

    private static long val(NetworkStatsSnapshot peer, NetworkStatsSnapshot local,
            java.util.function.Function<NetworkStatsSnapshot, Long> peerFn,
            java.util.function.Function<NetworkStatsSnapshot, Long> localFn) {
        long v = 0;
        if (local != null) {
            v = Math.max(v, localFn.apply(local));
        }
        if (peer != null) {
            v = Math.max(v, peerFn.apply(peer));
        }
        return v;
    }

    private static String str(NetworkStatsSnapshot peer, NetworkStatsSnapshot local,
            java.util.function.Function<NetworkStatsSnapshot, String> peerFn,
            java.util.function.Function<NetworkStatsSnapshot, String> localFn,
            String def) {
        if (local != null && localFn.apply(local) != null && !localFn.apply(local).isEmpty()) {
            return localFn.apply(local);
        }
        if (peer != null && peerFn.apply(peer) != null && !peerFn.apply(peer).isEmpty()) {
            return peerFn.apply(peer);
        }
        return def;
    }

    private static List<String> buildHints(float inFps, float renderFps, int bwInKbps,
            int rttMs, long videoPackets) {
        List<String> hints = new ArrayList<>();
        if (inFps > 0 && inFps < 8f) {
            hints.add("Hint: low In FPS → sender encode stall / TCP backlog");
        }
        if (bwInKbps > 0 && bwInKbps < 350 && inFps < 8f && videoPackets > 30) {
            hints.add("Hint: very low BW — static screen or throttled encoder");
        }
        if (rttMs > 450 && inFps < 15f) {
            hints.add("Hint: high RTT — Wi‑Fi congestion");
        }
        if (inFps > 12f && renderFps > 0 && renderFps < inFps * 0.7f) {
            hints.add("Hint: decode slower than network (try H.264 or lower res)");
        }
        return hints;
    }

    private static String colored(String text, boolean bad) {
        return "<font color='" + (bad ? COLOR_WARN : COLOR_OK) + "'>" + escape(text) + "</font>";
    }

    private static String escape(String s) {
        if (s == null) {
            return "";
        }
        return s.replace("&", "&amp;").replace("<", "&lt;");
    }

    private static String formatFloat(float v) {
        if (v <= 0f) {
            return "n/a";
        }
        return String.format(java.util.Locale.US, "%.1f", v);
    }

    private static String formatDb(float db) {
        if (db <= -90f) {
            return "-∞";
        }
        return String.format(java.util.Locale.US, "%.0f dB", db);
    }

    private static String vuBarHtml(int percent) {
        int width = 16;
        int filled = Math.min(width, (percent * width + 50) / 100);
        StringBuilder b = new StringBuilder("<font color='#555'>[</font>");
        for (int i = 0; i < width; i++) {
            if (i < filled) {
                String c = i < width * 2 / 3 ? "#66FF66" : (i < width * 5 / 6 ? "#FFCC00" : "#FF4444");
                b.append("<font color='").append(c).append("'>█</font>");
            } else {
                b.append("<font color='#333'>░</font>");
            }
        }
        b.append("<font color='#555'>]</font>");
        return b.toString();
    }
}
