#!/usr/bin/env python3
"""Measure the rendered caption frames produced by test-run.sh (stdlib only).

Reads the raw rgb24 frames that ffmpeg wrote and reports:
  A. Cap height of the sample caption at each font size, on a 1080x1920 frame and
     after the frame is scaled to a 390 CSS px wide phone view.
  B. The background under the caption on two real clips: how bright it gets, and
     what contrast a caption of each style has against the colour its letters
     actually touch.
  C. How often the caption band of a real clip contains a pixel as bright as
     white text (from the ffmpeg signalstats log).
"""
import os
import re
import sys

OUT = sys.argv[1]
FONT = sys.argv[2]
PHONE_W = int(sys.argv[3])
FRAME_W = int(sys.argv[4])
FRAME_H = int(sys.argv[5])
TEXT = sys.argv[6]
SCALE = PHONE_W / FRAME_W
ROW = FRAME_W * 3

STYLES = [
    ("white-plain", (255, 255, 255)),
    ("white-shadow", (255, 255, 255)),
    ("white-outline", (255, 255, 255)),
    ("white-box60", (255, 255, 255)),
    ("white-box80", (255, 255, 255)),
    ("white-box100", (255, 255, 255)),
    ("yellow-box60", (255, 226, 74)),
]


def load(name):
    with open(os.path.join(OUT, name), "rb") as fh:
        data = fh.read()
    assert len(data) == ROW * FRAME_H, "%s: expected %d bytes, got %d" % (name, ROW * FRAME_H, len(data))
    return data


def lum(r, g, b):
    def ch(c):
        cs = c / 255.0
        return cs / 12.92 if cs <= 0.04045 else ((cs + 0.055) / 1.055) ** 2.4
    return 0.2126 * ch(r) + 0.7152 * ch(g) + 0.0722 * ch(b)


def ratio(a, b):
    la, lb = lum(*a), lum(*b)
    hi, lo = (la, lb) if la >= lb else (lb, la)
    return (hi + 0.05) / (lo + 0.05)


def pct(vals, p):
    if not vals:
        return 0.0
    v = sorted(vals)
    return v[max(0, min(len(v) - 1, int(round((p / 100.0) * (len(v) - 1)))))]


def close(c, fill, tol):
    return abs(c[0] - fill[0]) <= tol and abs(c[1] - fill[1]) <= tol and abs(c[2] - fill[2]) <= tol


print("=" * 100)
print("A. CAP HEIGHT BY FONT SIZE   sample caption: %r" % TEXT)
print("=" * 100)
print("font file: %s" % FONT)
print("frame: %dx%d, phone view: %d CSS px wide (scale %.5f)" % (FRAME_W, FRAME_H, PHONE_W, SCALE))
print("%-9s %-14s %-16s %-16s %-17s %-16s %-14s" % ("size_px", "cap_height_px", "text_width_px", "pct_frame_width", "cap_css_390px", "pct_of_844px", "chars_fit"))
size_rows = []
for name in sorted(os.listdir(OUT)):
    if not (name.startswith("size-") and name.endswith(".raw")):
        continue
    size = int(name[5:-4])
    data = load(name)
    top = bottom = None
    for y in range(FRAME_H):
        row = data[y * ROW:(y + 1) * ROW]
        if row.count(0) != len(row):
            if top is None:
                top = y
            bottom = y
    cap = bottom - top + 1
    # width of the same caption at this size
    left = right = None
    for x in range(FRAME_W):
        hit = False
        for y in range(top, bottom + 1):
            o = y * ROW + x * 3
            if data[o] or data[o + 1] or data[o + 2]:
                hit = True
                break
        if hit:
            if left is None:
                left = x
            right = x
    width = right - left + 1
    n_chars = len(TEXT)
    # characters that fit in 80% of the frame width at this size
    per_char = width / float(n_chars)
    chars_fit = int((FRAME_W * 0.8) // per_char)
    row = {"size_px": size, "cap_height_px": cap, "cap_ratio": round(cap / size, 4),
           "pct_frame_height": round(100.0 * cap / FRAME_H, 2),
           "text_width_px": width, "pct_frame_width": round(100.0 * width / FRAME_W, 1),
           "chars_fit_in_80pct": chars_fit,
           "cap_css_390px": round(cap * SCALE, 2)}
    size_rows.append(row)
    print("%-9d %-14d %-16d %-16s %-17s %-16s %-14s" % (
        row["size_px"], row["cap_height_px"], row["text_width_px"], str(row["pct_frame_width"]) + "%",
        str(row["cap_css_390px"]) + " px", str(round(100.0 * row["cap_css_390px"] / 844, 2)) + "%",
        str(chars_fit) + " of " + str(n_chars)))

print()
print("=" * 100)
print("B. WHAT THE CAPTION LETTERS ACTUALLY TOUCH, ON TWO REAL CLIPS")
print("=" * 100)
print("A support pixel is solid when its four neighbours have almost the same brightness")
print("(that rejects antialiased letter edges). 'Worst' is the lowest contrast found")
print("between the caption fill and a support pixel next to it.")
print()
contrast_rows = []
for clip in ("dark", "bright"):
    plain = load("%s-plain.raw" % clip)
    # background brightness in the caption area, with the caption removed
    bg_lums = []
    for y in range(1180, 1440):
        base = y * ROW
        for x in range(0, FRAME_W):
            o = base + x * 3
            bg_lums.append(lum(plain[o], plain[o + 1], plain[o + 2]))
    bg_max = max(bg_lums)
    bg_p90 = pct(bg_lums, 90)
    bg_med = pct(bg_lums, 50)
    print("-" * 100)
    print("%s clip: background in the caption band, caption removed: brightest pixel %.3f, "
          "90th percentile %.3f, median %.3f" % (clip, bg_max, bg_p90, bg_med))
    bg_max_rgb = (0, 0, 0)
    bg_max_val = -1.0
    for y in range(1180, 1440):
        base = y * ROW
        for x in range(FRAME_W):
            o = base + x * 3
            c = (plain[o], plain[o + 1], plain[o + 2])
            l = lum(*c)
            if l > bg_max_val:
                bg_max_val = l
                bg_max_rgb = c
    print("   a plain white caption has no separation there: %.2f:1 against the brightest pixel "
          "in the band (RGB %d,%d,%d)" % (ratio((255, 255, 255), bg_max_rgb), bg_max_rgb[0], bg_max_rgb[1], bg_max_rgb[2]))
    for name, fill in STYLES:
        path = "%s-%s.raw" % (clip, name)
        if not os.path.exists(os.path.join(OUT, path)):
            continue
        styled = load(path)
        # solid fill pixels
        fill_pixels = []
        for y in range(1180, 1440):
            base = y * ROW
            for x in range(FRAME_W):
                o = base + x * 3
                c = (styled[o], styled[o + 1], styled[o + 2])
                if close(c, fill, 30):
                    fill_pixels.append((x, y))
        if not fill_pixels:
            continue
        ratios = []
        for (x, y) in fill_pixels:
            best = None
            for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1), (2, 0), (-2, 0), (0, 2), (0, -2)):
                nx, ny = x + dx, y + dy
                if not (0 <= nx < FRAME_W and 0 <= ny < FRAME_H):
                    continue
                o = ny * ROW + nx * 3
                c = (styled[o], styled[o + 1], styled[o + 2])
                if close(c, fill, 30):
                    continue
                # solid test: the four neighbours of this candidate must be close in brightness
                neigh = []
                for ex, ey in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                    qx, qy = nx + ex, ny + ey
                    if not (0 <= qx < FRAME_W and 0 <= qy < FRAME_H):
                        neigh = None
                        break
                    q = qy * ROW + qx * 3
                    neigh.append(lum(styled[q], styled[q + 1], styled[q + 2]))
                if not neigh:
                    continue
                if max(neigh) - min(neigh) > 0.08:
                    continue  # antialiased edge, not a solid support colour
                if best is None or ratio(fill, c) < ratio(fill, best):
                    best = c
            if best is not None:
                ratios.append(ratio(fill, best))
        row = {"clip": clip, "style": name, "fill": "#%02X%02X%02X" % fill,
               "fill_pixels": len(fill_pixels),
               "contrast_median": round(pct(ratios, 50), 2),
               "contrast_p10": round(pct(ratios, 10), 2),
               "contrast_min": round(min(ratios), 2),
               "bg_max_lum": round(bg_max, 3)}
        contrast_rows.append(row)
        print("   %-15s fill %s   contrast against what the letters touch: median %6.2f:1, "
              "worst 10%% %6.2f:1, lowest %6.2f:1" % (
                  name, row["fill"], row["contrast_median"], row["contrast_p10"], row["contrast_min"]))
    print()

print("=" * 100)
print("C. HOW BRIGHT IS THE CAPTION BAND OVER TIME? (ffmpeg signalstats)")
print("=" * 100)
band_rows = []
for clip in ("dark", "bright"):
    path = os.path.join(OUT, "stats-%s.txt" % clip)
    if not os.path.exists(path):
        continue
    ymax, yavg = [], []
    for line in open(path):
        m = re.search(r"lavfi\.signalstats\.YMAX=([0-9.]+)", line)
        if m:
            ymax.append(float(m.group(1)))
        m = re.search(r"lavfi\.signalstats\.YAVG=([0-9.]+)", line)
        if m:
            yavg.append(float(m.group(1)))
    if not ymax:
        continue
    bright_frames = sum(1 for v in ymax if v >= 250)
    row = {"clip": clip, "frames": len(ymax), "frames_with_white_pixel": bright_frames,
           "pct": round(100.0 * bright_frames / max(1, len(ymax)), 1),
           "ymax_median": round(pct(ymax, 50), 1), "ymax_max": round(max(ymax), 1),
           "yavg_median": round(pct(yavg, 50), 1)}
    band_rows.append(row)
    print("%-7s %3d frames at 10 fps. %3d frames (%.1f%%) contain a pixel as bright as white text "
          "(YMAX 255). Median YMAX %.0f, max %.0f, median band brightness YAVG %.0f." % (
              clip, row["frames"], row["frames_with_white_pixel"], row["pct"],
              row["ymax_median"], row["ymax_max"], row["yavg_median"]))

print()
print("=" * 100)
print("D. SUMMARY")
print("=" * 100)
print("cap height rows: %d, contrast rows: %d, band rows: %d" % (
    len(size_rows), len(contrast_rows), len(band_rows)))
