#!/usr/bin/env python3
"""Measure glass-to-glass latency of any remote desktop stack.

    ./run.py --window <client window name> [--container <name>] [--samples 30]

Two pieces have to be in place:

1. `g2g_probe` running INSIDE the desktop being streamed (build with `make`,
   copy it in, run it there against that desktop's DISPLAY).
2. The client window showing that desktop, on THIS machine, unobscured.

What you get is draw-to-visible: the probe's draw and commit, the streamed
desktop's compose, capture, encode, transport, jitter buffer, decode, and this
machine's present. It excludes this monitor's scanout, so it is
glass-to-framebuffer, and it excludes any input path.

The subtraction is only arithmetic if both ends read the same clock. With a
container, --container proves it per run. Across two physical machines it does
not hold and this instrument does not apply.
"""
import argparse
import statistics
import sys

import client_probe as cp
import g2g_measure as g


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--window", required=True, help="client window name (exact)")
    ap.add_argument("--container", help="docker container running the streamed desktop; "
                                        "proves the clock is shared before measuring")
    ap.add_argument("--samples", type=int, default=30)
    ap.add_argument("--interval", type=float, default=0.05)
    a = ap.parse_args()

    if a.container:
        import clock_gate
        gate = clock_gate.check(a.container, samples=5)
        print(f"clock: shared={gate['shared_clock']} (by {gate['decided_by']})")
        if not gate["shared_clock"]:
            sys.exit("the container does not share this machine's monotonic clock; "
                     "the subtraction below would not be arithmetic")

    grabber = g.Grabber()
    win = cp.find_window(a.window)
    box = cp.locate_timecode(grabber, win)
    print(f"window {win['w']}x{win['h']}+{win['x']}+{win['y']}, "
          f"timecode {box['w']}x{box['h']} at scale {box['scale']:.3f}")

    rows = cp.sample_latency(grabber, box, a.samples, a.interval)
    vals = [r[0] for r in rows if r[0] is not None]
    grabs = [r[1] for r in rows]
    if not vals:
        sys.exit("no timecode decoded")

    in_range = sum(1 for v in vals if 0 < v < 1000) / len(vals)
    distinct = sum(1 for i in range(1, len(vals)) if vals[i] != vals[i - 1])
    grab_p50 = cp.percentile(grabs, 50)

    print(f"p50 {statistics.median(vals):.1f} ms   "
          f"p90 {cp.percentile(vals, 90):.1f} ms   n={len(vals)}")
    print(f"in range {in_range:.0%}   distinct {distinct / max(1, len(vals) - 1):.0%}   "
          f"observer grab p50 {grab_p50 / 1000:.1f} ms")

    # The same gate the published campaigns used. A measurement that fails it is
    # not a slow stack, it is a reading of something else -- most often a frozen
    # stream, which reports plausible 400-700 ms "latencies".
    bad = []
    if in_range < 0.8:
        bad.append("under 80% of samples in 0<x<1000 ms")
    if len(vals) > 1 and distinct / (len(vals) - 1) < 0.5:
        bad.append("the timecode stopped advancing (frozen stream)")
    if grab_p50 > 5000:
        bad.append("this observer's own median grab cost is above 5 ms")
    print("VALID" if not bad else "INVALID: " + "; ".join(bad))
    return 0 if not bad else 1


if __name__ == "__main__":
    sys.exit(main())
