/* On-screen timecode source for black-box glass-to-glass latency, meant to run
 * INSIDE the streamed desktop (a container, a VM, a remote box -- anywhere an
 * X11 or Xwayland display exists).
 *
 *   cc -O2 -o g2g_probe g2g_probe.c -lX11
 *   g2g_probe [--motion] [--hz N] [--inset X Y] [--display :1] [--seconds S]
 *
 * Draws the same visual contract as tools/latency/g2g_pattern.py -- a
 * saturated-green hollow frame around 12 blocks encoding
 * `int(CLOCK_MONOTONIC*1000) % 4096`, MSB first, white=1 -- at identical
 * geometry (BITS=12, BLOCK=96, FRAME=16), so tools/latency/g2g_measure.py's
 * green_boxes()/decode() read it with no changes.
 *
 * Why C and Xlib rather than reusing the GTK3 Python source: g2g_pattern.py
 * needs PyGObject + GTK3, which is a bet about what is installed inside a
 * container image. Xlib is present wherever an X11 or Xwayland desktop is.
 * The binary is built on the measuring host and copied in, so the image needs
 * no rebuild and no package install.
 *
 * Two differences from g2g_pattern.py, both deliberate:
 *
 *   - Fixed-rate redraw (clock_nanosleep TIMER_ABSTIME), not a compositor
 *     frame clock. The probe must behave identically on a compositor with a
 *     render loop and on a bare X server without one, since the point is to
 *     measure stacks we did not write. At 240 Hz the probe's own stamp
 *     staleness is bounded by 4.2 ms (~2.1 ms mean) and is disclosed rather
 *     than subtracted.
 *
 *   - Drawn at an inset (default +24+24), not +0+0. The observer asserts the
 *     located strip lies strictly inside the client window; a strip flush
 *     against the desktop edge can be cropped by a client that trims a row of
 *     pixels, and a cropped strip decodes to a wrong value instead of failing.
 *
 * --motion adds a full-screen field that changes every frame (scrolling bars
 * plus a per-frame dither grid), giving a deterministic saturated-damage
 * workload with no ffmpeg, no video file and no toolkit inside the container.
 * Without it the desktop is static apart from the strip, which is the sparse
 * arm: a damage-driven capture path behaves very differently between the two,
 * which is exactly why both are measured.
 */
#define _POSIX_C_SOURCE 200809L
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define BITS 12
#define BLOCK 96
#define FRAME 16
#define PW (BITS * BLOCK + 2 * FRAME) /* 1184 */
#define PH (BLOCK + 2 * FRAME)        /*  128 */

static long long now_us(void) {
  struct timespec ts;
  clock_gettime(CLOCK_MONOTONIC, &ts);
  return (long long)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000;
}

int main(int argc, char **argv) {
  const char *display_name = NULL;
  int hz = 240, motion = 0, inset_x = 24, inset_y = 24, raise_every = 60;
  int managed = 0;
  double remap_every = 0; /* seconds; 0 = never */
  double seconds = 0; /* 0 = run until killed */

  for (int i = 1; i < argc; i++) {
    if (!strcmp(argv[i], "--motion")) motion = 1;
    else if (!strcmp(argv[i], "--managed")) managed = 1;
    else if (!strcmp(argv[i], "--remap-every") && i + 1 < argc) remap_every = atof(argv[++i]);
    else if (!strcmp(argv[i], "--hz") && i + 1 < argc) hz = atoi(argv[++i]);
    else if (!strcmp(argv[i], "--raise-every") && i + 1 < argc) raise_every = atoi(argv[++i]);
    else if (!strcmp(argv[i], "--seconds") && i + 1 < argc) seconds = atof(argv[++i]);
    else if (!strcmp(argv[i], "--display") && i + 1 < argc) display_name = argv[++i];
    else if (!strcmp(argv[i], "--inset") && i + 2 < argc) {
      inset_x = atoi(argv[++i]);
      inset_y = atoi(argv[++i]);
    } else {
      fprintf(stderr,
              "usage: %s [--motion] [--hz N] [--inset X Y] [--display :N] "
              "[--seconds S]\n", argv[0]);
      return 2;
    }
  }
  if (hz < 1 || hz > 1000) { fprintf(stderr, "--hz out of range\n"); return 2; }

  Display *dpy = XOpenDisplay(display_name);
  if (!dpy) {
    fprintf(stderr, "XOpenDisplay(%s) failed -- is DISPLAY set and reachable "
                    "as this user?\n", display_name ? display_name : getenv("DISPLAY"));
    return 1;
  }
  int scr = DefaultScreen(dpy);
  Window root = RootWindow(dpy, scr);
  int sw = DisplayWidth(dpy, scr), sh = DisplayHeight(dpy, scr);

  /* In motion mode the window covers the desktop and the strip is drawn at the
   * inset inside it; otherwise the window is the strip. Either way the strip
   * lands at the same root coordinates, so the observer does not care which
   * mode produced it. */
  int win_x = motion ? 0 : inset_x, win_y = motion ? 0 : inset_y;
  int win_w = motion ? sw : PW, win_h = motion ? sh : PH;
  int strip_x = motion ? inset_x : 0, strip_y = motion ? inset_y : 0;

  if (win_w < PW + strip_x || win_h < PH + strip_y) {
    fprintf(stderr, "display %dx%d is too small for a %dx%d strip at +%d+%d\n",
            sw, sh, PW, PH, inset_x, inset_y);
    return 1;
  }

  XSetWindowAttributes attr;
  attr.override_redirect = managed ? False : True;
  attr.background_pixel = BlackPixel(dpy, scr);
  attr.event_mask = ExposureMask;
  Window win = XCreateWindow(dpy, root, win_x, win_y, win_w, win_h, 0,
                             CopyFromParent, InputOutput, CopyFromParent,
                             CWOverrideRedirect | CWBackPixel | CWEventMask, &attr);
  if (managed) {
    XStoreName(dpy, win, "g2g_probe");
    XSizeHints *sh = XAllocSizeHints();
    sh->flags = PPosition | PSize | PMinSize | PMaxSize;
    sh->x = win_x; sh->y = win_y;
    sh->min_width = sh->max_width = win_w;
    sh->min_height = sh->max_height = win_h;
    XSetWMNormalHints(dpy, win, sh);
    XFree(sh);
  }
  XMapRaised(dpy, win);
  if (managed) { XMoveWindow(dpy, win, win_x, win_y); XSync(dpy, False); }

  /* Draw into a pixmap and blit: a torn read of a half-drawn strip decodes to
   * a plausible wrong number rather than to a failure, which is the one class
   * of error this instrument must never produce. */
  Pixmap buf = XCreatePixmap(dpy, win, win_w, win_h, DefaultDepth(dpy, scr));
  GC gc = XCreateGC(dpy, buf, 0, NULL);

  Colormap cmap = DefaultColormap(dpy, scr);
  XColor green, white, black;
  XParseColor(dpy, cmap, "#00ff00", &green); XAllocColor(dpy, cmap, &green);
  XParseColor(dpy, cmap, "#ffffff", &white); XAllocColor(dpy, cmap, &white);
  XParseColor(dpy, cmap, "#000000", &black); XAllocColor(dpy, cmap, &black);

  unsigned long bars[6];
  {
    /* No saturated green anywhere in this palette, and not by taste: the
     * observer locates the timecode by a green mask, and a "#308030" bar in
     * the motion field is green enough to pass it. The locator then finds two
     * boxes and refuses to measure -- the instrument tripping over its own
     * workload. Greens and yellow-greens stay out. */
    const char *hexes[6] = {"#202028", "#803030", "#303080", "#502878",
                            "#804820", "#282850"};
    for (int i = 0; i < 6; i++) {
      XColor c;
      XParseColor(dpy, cmap, hexes[i], &c);
      XAllocColor(dpy, cmap, &c);
      bars[i] = c.pixel;
    }
  }

  struct timespec next;
  clock_gettime(CLOCK_MONOTONIC, &next);
  const long period_ns = 1000000000L / hz;
  long long t_end = seconds > 0 ? now_us() + (long long)(seconds * 1e6) : 0;
  unsigned frame = 0;
  unsigned lcg = 12345;

  for (;;) {
    while (XPending(dpy)) { XEvent ev; XNextEvent(dpy, &ev); } /* drain Expose */

    /* Restack periodically. override_redirect keeps the window manager from
     * moving it, not from another window being mapped above it: a panel, a
     * notification or a session dialog appearing inside the streamed desktop
     * silently covers the strip, and the observer then reports "no timecode
     * found" against a stream that is demonstrably still delivering frames.
     * Measured once here before this line existed. */
    if (raise_every > 0 && (frame % (unsigned)raise_every) == 0)
      XRaiseWindow(dpy, win);

    /* Diagnostic lever: unmap+map forces the compositor to treat the window as
     * newly presented. If a strip that fades out of the stream comes back only
     * when this fires, the compositor is not picking up the window's damage --
     * a very different fault from the window being covered or destroyed. */
    if (remap_every > 0) {
      static long long last_remap_us = 0;
      long long nowu = now_us();
      if (last_remap_us == 0) last_remap_us = nowu;
      if (nowu - last_remap_us > (long long)(remap_every * 1e6)) {
        XUnmapWindow(dpy, win);
        XFlush(dpy);
        XMapRaised(dpy, win);
        XFlush(dpy);
        last_remap_us = nowu;
      }
    }

    if (motion) {
      /* Scrolling bars: every pixel of the desktop changes colour over a few
       * frames, so a damage-driven capture path sees a full-screen update. */
      int bar_w = 160, off = (int)(frame * 13) % bar_w;
      for (int x = -bar_w + off; x < win_w; x += bar_w) {
        XSetForeground(dpy, gc, bars[((x / bar_w) + frame / 4) % 6]);
        XFillRectangle(dpy, buf, gc, x, 0, bar_w, win_h);
      }
      /* Dither grid: defeats any encoder heuristic that would treat the bars
       * as a pure translation and code them almost for free. */
      for (int i = 0; i < 240; i++) {
        lcg = lcg * 1103515245u + 12345u;
        int gx = (int)((lcg >> 16) % (unsigned)win_w);
        lcg = lcg * 1103515245u + 12345u;
        int gy = (int)((lcg >> 16) % (unsigned)win_h);
        XSetForeground(dpy, gc, (lcg & 1) ? white.pixel : black.pixel);
        XFillRectangle(dpy, buf, gc, gx, gy, 24, 24);
      }
    } else {
      XSetForeground(dpy, gc, black.pixel);
      XFillRectangle(dpy, buf, gc, 0, 0, win_w, win_h);
    }

    /* Stamp as late as possible: everything above is independent of the value,
     * so only the 13 rectangles below sit between the clock read and the blit. */
    long long t = now_us();
    unsigned v = (unsigned)((t / 1000) % (1 << BITS));

    XSetForeground(dpy, gc, green.pixel);
    XFillRectangle(dpy, buf, gc, strip_x, strip_y, PW, PH);
    for (int i = 0; i < BITS; i++) {
      int bit = (v >> (BITS - 1 - i)) & 1;
      XSetForeground(dpy, gc, bit ? white.pixel : black.pixel);
      XFillRectangle(dpy, buf, gc, strip_x + FRAME + i * BLOCK, strip_y + FRAME,
                     BLOCK, BLOCK);
    }
    XCopyArea(dpy, buf, win, gc, 0, 0, win_w, win_h, 0, 0);
    XFlush(dpy);

    frame++;
    if (t_end && now_us() >= t_end) break;

    next.tv_nsec += period_ns;
    while (next.tv_nsec >= 1000000000L) { next.tv_nsec -= 1000000000L; next.tv_sec++; }
    /* If we fell behind (a stalled X server, a descheduled container), resync
     * rather than spin through a backlog of missed deadlines at full speed. */
    struct timespec nowts;
    clock_gettime(CLOCK_MONOTONIC, &nowts);
    if (nowts.tv_sec > next.tv_sec ||
        (nowts.tv_sec == next.tv_sec && nowts.tv_nsec > next.tv_nsec))
      next = nowts;
    clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
  }

  XFreePixmap(dpy, buf);
  XFreeGC(dpy, gc);
  XDestroyWindow(dpy, win);
  XCloseDisplay(dpy);
  return 0;
}
