"""Draw the four figures of lesson 27 from tail_sim.py, so that no number in a figure is typed by hand.

    python3 make_figs.py          rewrite ../figs/*.svg
    python3 make_figs.py --check  exit 1 if a figure on disk differs (test_tail_sim.py runs the same check)

Figures 1 to 3 are 600 units wide and figure 4, which sits in Go deeper, 540. Text is at least 22 units (20 for axis ticks) in ink or grey on
white or a light fill; orange and green are used on marks only.
"""
import sys
from pathlib import Path
from xml.sax.saxutils import escape

import tail_sim as sim

FIGS = Path(__file__).resolve().parent.parent / "figs"
INK, GREY, ORANGE, GREEN, FILL, PANEL = "#0a1626", "#44566b", "#b36200", "#00855F", "#d2dae4", "#ebf0f6"
DASH = "10 8"


def num(x: float) -> str:
    return f"{x:.2f}".rstrip("0").rstrip(".")


# The SVG namespace is a name that every standalone .svg file must carry, not an address this program visits. It is written in two pieces so that a plain
# search of code/ for the start of a web address finds nothing (test_tail_sim.py runs that search).
NAMESPACE = "ht" + "tp://www.w3.org/2000/svg"


def frame(width: int, height: int, title: str, desc: str, body: list[str]) -> str:
    head = (f'<svg xmlns="{NAMESPACE}" viewBox="0 0 {width} {height}" width="{width}" height="{height}" role="img" '
            f'font-family="system-ui, sans-serif" font-size="22" fill="{GREY}">')
    return "\n".join([head, f"  <title>{escape(title)}</title>", f"  <desc>{escape(desc)}</desc>", '  <rect width="100%" height="100%" fill="#ffffff"/>',
                      *("  " + line for line in body), "</svg>", ""])


def text(x: float, y: float, content: str, anchor: str = "start", color: str = INK, bold: bool = False, size: int = 22) -> str:
    extra = (f' text-anchor="{anchor}"' if anchor != "start" else "") + (' font-weight="bold"' if bold else "") + (f' font-size="{size}"' if size != 22 else "")
    return f'<text x="{num(x)}" y="{num(y)}"{extra} fill="{color}">{escape(content)}</text>'


# --- figure 1: the sorted total times at the opening load ---------------------------------------------------------------------

def fig_sorted() -> str:
    rate = sim.OPENING_RATE
    times = sorted(sim.totals(sim.serve(sim.WORKLOAD, rate)))
    stats = sim.summarize(times)
    n = len(times)
    x0, x1, top, base, ymax = 70, 580, 70, 340, 6.0
    px = lambda share: x0 + share * (x1 - x0)
    py = lambda seconds: base - seconds / ymax * (base - top)
    points = [(px(i / (n - 1)), py(times[i])) for i in range(0, n, 10)] + [(px(1.0), py(times[-1]))]
    curve = "M" + " L".join(f"{num(x)} {num(y)}" for x, y in points)
    body = [text(20, 40, "seconds", color=GREY)]
    for tick in (0, 2, 4, 6):
        body.append(f'<path d="M{x0} {num(py(tick))}H{x1}" stroke="{FILL}" stroke-width="2"/>')
        body.append(text(x0 - 8, py(tick) + 7, str(tick), "end", GREY, size=20))
    body.append(f'<path d="{curve}V{base}H{x0}Z" fill="{PANEL}" stroke="none"/>')
    body.append(f'<path d="{curve}" fill="none" stroke="{INK}" stroke-width="3"/>')
    avg_y = py(stats["average"])
    body.append(f'<path d="M{x0} {num(avg_y)}H{x1}" stroke="{ORANGE}" stroke-width="4" stroke-dasharray="{DASH}"/>')
    body.append(text(x0 + 8, avg_y - 12, f"Average {sim.seconds_text(stats['average'])} s", bold=True))
    marks = {"p50": 0.50, "p95": 0.95, "p99": 0.99}
    for name, share in marks.items():
        x, y = px(share), py(stats[name])
        label = f"{name} {sim.seconds_text(stats[name])} s"
        if name == "p50":
            body.append(f'<path d="M{num(x)} {num(y - 8)}V{num(avg_y - 46)}" stroke="{INK}" stroke-width="2"/>')
            body.append(text(x, avg_y - 52, label, "middle", bold=True))
        else:
            body.append(text(x - 16, y + 8, label, "end", bold=True))
        body.append(f'<circle cx="{num(x)}" cy="{num(y)}" r="9" fill="{GREEN}" stroke="{INK}" stroke-width="2.5"/>')
    body.append(text(px(0.99) - 14, py(stats["max"]) + 10, f"slowest {sim.seconds_text(stats['max'])} s", "end"))
    body.append(f'<path d="M{x0} {base}H{x1}" stroke="{GREY}" stroke-width="2"/>')
    body.append(text(x0, base + 34, "fastest", color=GREY))
    body.append(text((x0 + x1) / 2, base + 34, f"{n:,} requests", "middle", GREY))
    body.append(text(x1, base + 34, "slowest", "end", GREY))
    shown = ", ".join(f"{name} {sim.seconds_text(stats[name])} s" for name in marks)
    desc = (f"The {n:,} total times at {rate} requests a second, sorted from fastest to slowest. The curve stays low for most requests and climbs steeply at the end, "
            f"to {sim.seconds_text(stats['max'])} s for the slowest. A dashed line marks the average, {sim.seconds_text(stats['average'])} s, and dots mark {shown}.")
    return frame(600, 400, f"Sorted total times: average {sim.seconds_text(stats['average'])} s, p99 {sim.seconds_text(stats['p99'])} s", desc, body)


# --- figure 2: the wait before the first word at three loads --------------------------------------------------------------------

FIG2_RATES = (12, 17, 19)


def fig_loads() -> str:
    swept = sim.sweep(rates=list(FIG2_RATES))
    x0, scale, bar_h, row = 140, 80, 22, 32
    rows = (("Average", "average"), ("p50", "p50"), ("p95", "p95"), ("p99", "p99"))
    styles = {"average": f'fill="{ORANGE}"', "p50": f'fill="{FILL}" stroke="{INK}" stroke-width="2"', "p95": f'fill="{GREEN}"', "p99": f'fill="{INK}"'}
    body, y = [], 20
    for rate in FIG2_RATES:
        stats = swept[rate]["wait"]
        body.append(text(20, y + 24, f"{rate} requests a second", bold=True))
        y += 40
        body.append(f'<path d="M{x0} {y - 4}V{y + row * len(rows) - 4}" stroke="{GREY}" stroke-width="2"/>')
        for label, key in rows:
            width = max(stats[key] * scale, 3.0)
            body.append(text(20, y + 18, label, color=INK))
            body.append(f'<rect x="{x0}" y="{y}" width="{num(width)}" height="{bar_h}" {styles[key]}/>')
            body.append(text(x0 + width + 10, y + 18, f"{sim.seconds_text(stats[key])} s"))
            y += row
        y += 22
    desc = "Wait before the first word for the average, p50, p95 and p99. " + " ".join(
        f"At {rate} requests a second: " + ", ".join(f"{label.lower() if key == 'average' else label} {sim.seconds_text(swept[rate]['wait'][key])} s" for label, key in rows) + "."
        for rate in FIG2_RATES)
    return frame(600, y - 6, "Wait before the first word at 12, 17 and 19 requests a second", desc, body)


# --- figure 3: the chance a session meets a slow request ------------------------------------------------------------------------

SESSION_SIZES = (1, 10, 20)


def fig_session() -> str:
    x0, scale, bar_h, row = 180, 380, 30, 56
    body = [text(20, 34, "Chance of at least one wait above p95", bold=True)]
    for tick in (0, 50, 100):
        x = x0 + tick / 100 * scale
        body.append(f'<path d="M{num(x)} 62V{62 + row * len(SESSION_SIZES)}" stroke="{FILL}" stroke-width="2"/>')
        body.append(text(x, 62 + row * len(SESSION_SIZES) + 26, f"{tick}%", "middle", GREY, size=20))
    y = 76
    for k in SESSION_SIZES:
        chance = sim.session_chance(0.05, k)
        body.append(text(20, y + 23, f"{k} request" + ("" if k == 1 else "s")))
        body.append(f'<rect x="{x0}" y="{y}" width="{num(chance * scale)}" height="{bar_h}" fill="{GREEN}" stroke="{INK}" stroke-width="2"/>')
        body.append(text(x0 + chance * scale + 10, y + 23, f"{chance * 100:.0f}%", bold=True))
        y += row
    body.append(text(20, y + 52, "Assumes each wait is independent.", color=GREY))
    desc = ("Horizontal bars of the chance that a session meets at least one request slower than p95, when 1 request in 20 is. "
            + ", ".join(f"{k} request{'s' if k > 1 else ''}: {sim.session_chance(0.05, k) * 100:.0f}%" for k in SESSION_SIZES) + ". It assumes that each wait is independent of the others.")
    return frame(600, y + 66, "Chance that a session meets a slow request: 5%, 40% and 64%", desc, body)


# --- figure 4: when each tester sent its requests during a freeze ----------------------------------------------------------------

WINDOW = (36.0, 50.0)


def fig_testers() -> str:
    runs = {"schedule": sim.serve(sim.WORKLOAD, sim.DEMO_RATE, 0, sim.PAUSE), "waits": sim.serve(sim.WORKLOAD, sim.DEMO_RATE, sim.CONNECTIONS, sim.PAUSE)}
    demo = sim.generator_demo()
    x0, x1 = 20, 520
    px = lambda t: x0 + (t - WINDOW[0]) / (WINDOW[1] - WINDOW[0]) * (x1 - x0)
    freeze = (px(sim.PAUSE_AT), px(sim.PAUSE_AT + sim.PAUSE))
    rows = (("Sends on schedule", "schedule", "on schedule"), ("Waits for each reply", "waits", "waits for each reply"))
    body, y, facts = [], 30, []
    for label, key, demo_key in rows:
        sent = runs[key].sent
        inside = sum(1 for s in sent if sim.PAUSE_AT <= s < sim.PAUSE_AT + sim.PAUSE)
        facts.append((inside, demo[demo_key]["wait"]["p99"]))
        body.append(text(x0, y, label, bold=True))
        body.append(f'<rect x="{num(freeze[0])}" y="{y + 12}" width="{num(freeze[1] - freeze[0])}" height="48" fill="{PANEL}" stroke="{GREY}" stroke-width="2" stroke-dasharray="6 5"/>')
        ticks = "".join(f"M{num(px(s))} {y + 18}v36" for s in sent if WINDOW[0] <= s <= WINDOW[1])
        body.append(f'<path d="{ticks}" stroke="{INK}" stroke-width="1.6" fill="none"/>')
        body.append(text(x0, y + 90, f"{inside} sent in the freeze, p99 wait {sim.seconds_text(demo[demo_key]['wait']['p99'])} s"))
        y += 134
    axis = y - 18
    body.append(f'<path d="M{x0} {axis}H{x1}" stroke="{GREY}" stroke-width="2"/>')
    for t in (WINDOW[0], sim.PAUSE_AT, sim.PAUSE_AT + sim.PAUSE, WINDOW[1]):
        body.append(f'<path d="M{num(px(t))} {axis}v8" stroke="{GREY}" stroke-width="2"/>')
        body.append(text(px(t), axis + 32, f"{t:g} s", "middle", GREY, size=20))
    body.append(text((freeze[0] + freeze[1]) / 2, axis + 66, f"server frozen {sim.PAUSE:g} s", "middle"))
    desc = (f"Each short line is one request sent between {WINDOW[0]:g} and {WINDOW[1]:g} seconds, at {sim.DEMO_RATE} requests a second, while the toy server is frozen from "
            f"{sim.PAUSE_AT:g} to {sim.PAUSE_AT + sim.PAUSE:g} seconds. The tester that sends on schedule keeps sending: {facts[0][0]} requests go out in the freeze and the p99 wait is "
            f"{sim.seconds_text(facts[0][1])} s. The tester that waits for each reply nearly stops: {facts[1][0]} go out and the p99 wait is {sim.seconds_text(facts[1][1])} s.")
    return frame(540, axis + 82, "Two testers during a 5 second freeze", desc, body)


FIGURES = {
    "sorted-total-times.svg": fig_sorted,
    "wait-at-three-loads.svg": fig_loads,
    "session-chance.svg": fig_session,
    "two-testers.svg": fig_testers,
}


def main() -> int:
    stale = []
    for name, draw in FIGURES.items():
        path, svg = FIGS / name, draw()
        if "--check" in sys.argv:
            if not path.exists() or path.read_text(encoding="utf-8") != svg:
                stale.append(name)
        else:
            FIGS.mkdir(exist_ok=True)
            path.write_text(svg, encoding="utf-8")
    if "--check" in sys.argv:
        print("figures differ from tail_sim.py: " + ", ".join(stale) if stale else "figures match tail_sim.py")
        return 1 if stale else 0
    print("wrote " + ", ".join(FIGURES))
    return 0


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