package com.foxx.androidcast.receiver;

/*********************************************************************
 * StreamMetrics.java
 * Created at: Fri 15 May 2026 18:17:39 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class StreamMetrics
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (6 commits, 265 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	d8a6a1d9253073899f2be582d7334435d8b5e69563656098aef7749f038234ab
**********************************************************************/
import com.foxx.androidcast.network.control.NetworkStatsSnapshot;
import com.foxx.androidcast.network.diag.DiagPingMetrics;

/** Receiver-side counters for diagnostics overlay and bandwidth estimates. */
public final class StreamMetrics {
    private long videoPackets;
    private long audioPackets;
    private long audioRenderedWrites;
    private long audioPcmSilentBlocks;
    private long audioPcmAudibleBlocks;
    private float audioVuSmoothed;
    private float audioLastPeakDb;
    private float audioLastRmsDb;
    private long keyFrames;
    private long deltaFrames;
    private long videoBytes;
    private long audioBytes;
    private long renderedFrames;
    private long windowVideoBytes;
    private long windowVideoPackets;
    private long windowStartMs;
    private float lastSecondInFps;
    private long lastVideoPacketMs;
    private long lastRenderedFrameMs;
    private long firstVideoPacketMs;
    private int peerRttMs;
    private int peerRecvKbps;
    private int peerSendKbps;
    private NetworkStatsSnapshot lastPeer;
    private DiagPingMetrics.Snapshot diagPing = DiagPingMetrics.Snapshot.EMPTY;

    public synchronized void reset() {
        videoPackets = 0;
        audioPackets = 0;
        audioRenderedWrites = 0;
        audioPcmSilentBlocks = 0;
        audioPcmAudibleBlocks = 0;
        audioVuSmoothed = 0f;
        audioLastPeakDb = -96f;
        audioLastRmsDb = -96f;
        keyFrames = 0;
        deltaFrames = 0;
        videoBytes = 0;
        audioBytes = 0;
        renderedFrames = 0;
        windowVideoBytes = 0;
        windowVideoPackets = 0;
        windowStartMs = 0;
        lastSecondInFps = 0f;
        lastVideoPacketMs = 0;
        lastRenderedFrameMs = 0;
        firstVideoPacketMs = 0;
        peerRttMs = 0;
        peerRecvKbps = 0;
        peerSendKbps = 0;
        lastPeer = null;
        diagPing = DiagPingMetrics.Snapshot.EMPTY;
    }

    public synchronized void onDiagPingSample(DiagPingMetrics.Snapshot sample) {
        diagPing = sample != null ? sample : DiagPingMetrics.Snapshot.EMPTY;
        if (diagPing.avgRttMs > 0) {
            peerRttMs = diagPing.avgRttMs;
        } else if (diagPing.lastRttMs > 0) {
            peerRttMs = diagPing.lastRttMs;
        }
    }

    public synchronized DiagPingMetrics.Snapshot getDiagPing() {
        return diagPing;
    }

    public synchronized void onVideoPacket(boolean keyFrame, int bytes) {
        long now = System.currentTimeMillis();
        videoPackets++;
        if (keyFrame) {
            keyFrames++;
        } else {
            deltaFrames++;
        }
        videoBytes += bytes;
        if (windowStartMs == 0) {
            windowStartMs = now;
        }
        long windowElapsed = now - windowStartMs;
        if (windowElapsed >= 1000) {
            lastSecondInFps = windowVideoPackets;
            windowVideoPackets = 0;
            windowVideoBytes = 0;
            windowStartMs = now;
        }
        windowVideoPackets++;
        windowVideoBytes += bytes;
        if (firstVideoPacketMs == 0) {
            firstVideoPacketMs = now;
        }
        lastVideoPacketMs = now;
    }

    public synchronized void onAudioPacket(int bytes) {
        audioPackets++;
        audioBytes += bytes;
    }

    public synchronized void onAudioRendered(int pcmBytes, float peak, float rms, boolean silent) {
        if (pcmBytes > 0) {
            audioRenderedWrites++;
        }
        if (silent) {
            audioPcmSilentBlocks++;
        } else {
            audioPcmAudibleBlocks++;
        }
        float instant = Math.max(peak, rms);
        audioVuSmoothed = instant > audioVuSmoothed
                ? instant * 0.35f + audioVuSmoothed * 0.65f
                : instant * 0.12f + audioVuSmoothed * 0.88f;
        if (peak > 1e-9f) {
            audioLastPeakDb = (float) (20.0 * Math.log10(peak));
        }
        if (rms > 1e-9f) {
            audioLastRmsDb = (float) (20.0 * Math.log10(rms));
        }
    }

    public synchronized long getAudioRenderedWrites() {
        return audioRenderedWrites;
    }

    public synchronized long getAudioPcmSilentBlocks() {
        return audioPcmSilentBlocks;
    }

    public synchronized long getAudioPcmAudibleBlocks() {
        return audioPcmAudibleBlocks;
    }

    public synchronized int getAudioVuPercent() {
        return com.foxx.androidcast.media.PcmLevelAnalyzer.vuPercent(audioVuSmoothed);
    }

    public synchronized float getAudioLastPeakDb() {
        return audioLastPeakDb;
    }

    public synchronized float getAudioLastRmsDb() {
        return audioLastRmsDb;
    }

    public synchronized void onRenderedFrame() {
        renderedFrames++;
        lastRenderedFrameMs = System.currentTimeMillis();
    }

    public synchronized void onPeerStats(NetworkStatsSnapshot peer) {
        if (peer == null) {
            return;
        }
        lastPeer = peer;
        peerRttMs = peer.rttMs;
        peerRecvKbps = peer.recvBitrateKbps;
        peerSendKbps = peer.sendBitrateKbps;
    }

    public synchronized NetworkStatsSnapshot getLastPeer() {
        return lastPeer;
    }

    public synchronized long getLastVideoPacketMs() {
        return lastVideoPacketMs;
    }

    public synchronized float getInFps() {
        long now = System.currentTimeMillis();
        float inFps = lastSecondInFps > 0 ? lastSecondInFps : computeFps(videoPackets, firstVideoPacketMs, now);
        return inFps;
    }

    public synchronized float getRenderFps() {
        long now = System.currentTimeMillis();
        return computeFps(renderedFrames, firstVideoPacketMs, lastRenderedFrameMs > 0 ? lastRenderedFrameMs : now);
    }

    public synchronized int getBwInKbps() {
        return computeLocalKbps(System.currentTimeMillis());
    }

    public synchronized long getVideoPackets() {
        return videoPackets;
    }

    public synchronized long getAudioPackets() {
        return audioPackets;
    }

    public synchronized long getKeyFrames() {
        return keyFrames;
    }

    public synchronized long getDeltaFrames() {
        return deltaFrames;
    }

    public synchronized int getAvgKbPerFrame() {
        if (videoPackets <= 0) {
            return 0;
        }
        return (int) (videoBytes / videoPackets / 1024);
    }

    public synchronized long getLastFrameIdleMs() {
        if (lastVideoPacketMs <= 0) {
            return -1;
        }
        return System.currentTimeMillis() - lastVideoPacketMs;
    }

    public synchronized int getPeerRttMs() {
        return peerRttMs;
    }

    public synchronized int getPeerSendKbps() {
        return peerSendKbps;
    }

    public synchronized int getPeerRecvKbps() {
        return peerRecvKbps;
    }

    public synchronized float getDecodeFps() {
        return getRenderFps();
    }

    /** Plain-text fallback (tests / logging). */
    public synchronized String format(String transport, String videoCodec, int streamW, int streamH, int renderW,
            int renderH, boolean idle) {
        StringBuilder sb = new StringBuilder();
        sb.append("In FPS: ").append(String.format("%.1f", getInFps()));
        sb.append(" Render: ").append(String.format("%.1f", getRenderFps()));
        sb.append(" BW: ").append(getBwInKbps()).append(" kbps");
        sb.append(idle ? " idle" : " live");
        return sb.toString();
    }

    private int computeLocalKbps(long now) {
        if (windowStartMs == 0 || windowVideoBytes == 0) {
            return peerRecvKbps;
        }
        long elapsed = now - windowStartMs;
        if (elapsed < 200) {
            return peerRecvKbps;
        }
        int kbps = (int) (windowVideoBytes * 8L / elapsed);
        if (elapsed > 4000) {
            windowStartMs = now;
            windowVideoBytes = 0;
        }
        return kbps;
    }

    private static float computeFps(long count, long startMs, long endMs) {
        if (count <= 1 || startMs <= 0) {
            return 0f;
        }
        long end = endMs > startMs ? endMs : System.currentTimeMillis();
        float sec = (end - startMs) / 1000f;
        if (sec < 0.05f) {
            return 0f;
        }
        return count / sec;
    }
}
