#!/usr/bin/env python3
"""The DISPLAY step for the p8v2 generation: derive everything from measured facts.

Reads tests/pprog/gen1.tsv, which contains only things that were measured about a
(variant, sample) pair, and applies #hutter_metrics to produce every derived
quantity: the class (c) per-byte rate, the class (b) extrapolation, S/U, k, and
the ranking.  No compressor runs.  Changing the reporting policy means editing
this file and re-running it, not re-running a single compression -- which is the
whole reason generating and displaying are split.

#hutter_metrics, restated so this file is checkable against it:

  (a) FIXED       binary + header + k1 table + backward LPP.
                  No extrapolation; contributes fixed/DSS.
  (b) SUB-LINEAR  the token section, capped at 65536 * 4 = 262144 bytes at k=2.
                  Extrapolated as model_b(M) * log(DSS)/log(M), capped.
  (c) LINEAR      the gap section + the recorded bytes.  a = trace(M)/M.

      S/U = fixed/DSS + model_b(DSS)/DSS + a          k = log(S/U)/log(0.99)

Every derived-for-another-DSS number is labelled EXTRAPOLATED with its basis.

  python3 tests/pprog/gen1-report [gen1.tsv] [--dss 1e9]
"""

import math
import os
import sys

CAP_K2 = 65536 * 4          # architectural cap on the class (b) token section at k=2


def load(path):
    rows = []
    with open(path) as f:
        head = f.readline().rstrip("\n").split("\t")
        for line in f:
            line = line.rstrip("\n")
            if not line.strip():
                continue
            vals = line.split("\t")
            if len(vals) < len(head):          # SKIPPED rows are short by design
                rows.append(dict(zip(head, vals + [""] * (len(head) - len(vals)))))
                continue
            rows.append(dict(zip(head, vals)))
    return rows


def num(r, k, default=0):
    try:
        return float(r.get(k, "") or default)
    except ValueError:
        return default


def derive(r, dss):
    """Measured -> derived.  Everything here is #hutter_metrics and nothing else."""
    M = num(r, "U")
    fixed = num(r, "binary_bytes") + 48 + 256 + num(r, "p_backward")
    model_b = num(r, "p_token")
    trace = num(r, "p_gap") + num(r, "p_trace")
    d = dict(M=M, fixed=fixed, model_b=model_b, trace=trace)
    d["a"] = trace / M if M > 0 else 0.0
    if M > 1 and model_b > 0:
        d["model_b_dss"] = min(model_b * math.log(dss) / math.log(M), CAP_K2)
    else:
        d["model_b_dss"] = model_b
    d["su"] = fixed / dss + d["model_b_dss"] / dss + d["a"]
    d["k"] = math.log(d["su"]) / math.log(0.99) if d["su"] > 0 else 0.0
    return d


def main(argv):
    here = os.path.dirname(os.path.abspath(__file__))
    path = os.path.join(here, "gen1.tsv")
    dss = 1e9
    args = argv[1:]
    while args:
        if args[0] == "--dss":
            dss = float(args[1]); args = args[2:]
        else:
            path = args[0]; args = args[1:]
    rows = load(path)
    if not rows:
        sys.stderr.write("gen1-report: no rows in %s\n" % path)
        return 1

    # The generation is read off the TSV's own name, so this file renders either one.
    base = os.path.basename(path)
    gen = base[3] if base.startswith("gen") and len(base) > 3 and base[3].isdigit() else "?"
    print("p8v2 generation %s -- derived view, per #hutter_metrics" % gen)
    print("source: %s   target DSS: %g" % (path, dss))
    print()
    print("MEASURED columns are from the runs. Everything under EXTRAPOLATED is derived for a DSS")
    print("of %g from a sample of the stated size and is not a measurement of anything at that size." % dss)
    print()

    samples = []
    for r in rows:
        if r["sample"] not in samples:
            samples.append(r["sample"])

    for s in samples:
        sub = [r for r in rows if r["sample"] == s]
        done = [r for r in sub if r.get("U", "").strip().isdigit()]
        skipped = [r for r in sub if r not in done]
        if not done:
            print("== sample %s: every run skipped" % s)
            for r in skipped:
                print("   %-14s SKIPPED %s" % (r["variant"], r.get("axes", "")))
            print()
            continue
        M = int(done[0]["U"])
        print("== sample %s  (M = %d bytes)" % (s, M))
        print("   %-14s %-7s %10s %10s %10s %9s | %12s %9s %8s"
              % ("variant", "axes", "fixed_B", "model_B", "trace_B", "a", "model_B@DSS", "S/U", "k"))
        print("   %-14s %-7s %10s %10s %10s %9s | %12s %9s %8s"
              % ("", "", "MEASURED", "MEASURED", "MEASURED", "MEASURED", "EXTRAP", "EXTRAP", "EXTRAP"))
        scored = []
        for i, r in enumerate(done):
            d = derive(r, dss)
            scored.append((d["a"], i, r, d))
        for a, _, r, d in sorted(scored):
            print("   %-14s %-7s %10d %10d %10d %9.4f | %12.0f %9.6f %8.1f"
                  % (r["variant"].replace("wordsv2-", ""), r.get("axes", ""),
                     d["fixed"], d["model_b"], d["trace"], d["a"],
                     d["model_b_dss"], d["su"], d["k"]))
        for r in skipped:
            why = (r.get("N", "") or "no reason recorded").replace("SKIPPED: ", "", 1)
            print("   %-14s %-7s SKIPPED: %s"
                  % (r["variant"].replace("wordsv2-", ""), r.get("axes", "?"), why))
        print()

    # The class (c) rate as a function of M: the only honest way to say anything about 10^9 from a
    # prefix is to show whether a is still moving. One column per sample, one row per variant.
    print("== class (c) rate a, against sample size  (MEASURED; read the trend, not any one point)")
    order = [s for s in samples]
    variants = []
    for r in rows:
        if r["variant"] not in variants:
            variants.append(r["variant"])
    print("   %-14s %s" % ("variant", "".join("%12s" % s for s in order)))
    for v in variants:
        cells = []
        for s in order:
            hit = [r for r in rows if r["variant"] == v and r["sample"] == s
                   and r.get("U", "").strip().isdigit()]
            cells.append("%12.4f" % derive(hit[0], dss)["a"] if hit else "%12s" % "-")
        print("   %-14s %s" % (v.replace("wordsv2-", ""), "".join(cells)))
    print()

    # The k=2 layer's own diagnostics, which is what the keep rules are judged on at this k.
    if any(r.get("cov_positions", "").strip().isdigit() for r in rows):
        print("== k=2 layer diagnostics  (MEASURED; #hutter_metrics: the model is bounded at %d B," % CAP_K2)
        print("   i.e. %.1e of S/U at 1e9, so a keep rule cannot be judged by compression at k=2)"
              % (CAP_K2 / 1e9))
        print("   %-14s %-6s %9s %9s %9s %9s %9s %10s"
              % ("variant", "sample", "positions", "coverage", "hits", "removed", "added", "net/model_B"))
        for r in rows:
            if not r.get("cov_positions", "").strip().isdigit():
                continue
            pos = num(r, "cov_positions"); cov = num(r, "coverage"); hit = num(r, "hits")
            rem = num(r, "removed"); add = num(r, "added"); tb = num(r, "p_token")
            net = rem - add
            print("   %-14s %-6s %9d %9d %9d %9d %9d %10s"
                  % (r["variant"].replace("wordsv2-", ""), r["sample"], pos, cov, hit, rem, add,
                     ("%.3f" % (net / tb)) if tb > 0 else "-"))
        print()

    # Costs, so an expensive row says why it was expensive (#hutter_run_costs).
    if any(r.get("cost_shape", "").strip() for r in rows):
        print("== cost  (#hutter_run_costs: a cost is reported with the shape that causes it)")
        seen = set()
        for r in rows:
            key = (r.get("cost_shape", ""), r.get("cost_why", ""))
            if not key[0] or key in seen:
                continue
            seen.add(key)
            print("   %s\n     because %s" % (key[0], key[1]))
        print("   slowest rows:")
        timed = [r for r in rows if r.get("secs", "").strip()]
        for r in sorted(timed, key=lambda r: -num(r, "secs"))[:5]:
            print("     %-14s %-6s %8.1f s   %s"
                  % (r["variant"].replace("wordsv2-", ""), r["sample"], num(r, "secs"),
                     r.get("cost_shape", "")))
        print()
    return 0


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