"""results/summary.json（と results/conv_summary.json）から図版を作る。figure-*.svg（明）と figure-*.dark.svg（暗）の2ファイル方式。
使い方: このフォルダで python make_figures.py"""
import json, os
from xml.sax.saxutils import escape

HERE = os.path.dirname(os.path.abspath(__file__))
S = json.load(open(f"{HERE}/results/summary.json"))
CV = json.load(open(f"{HERE}/results/conv_summary.json"))
FONT = "Hiragino Sans, Noto Sans JP, sans-serif"


def pal(dark):
    if dark:
        return dict(bg="#111e29", card="#1c2d3c", ink="#f3f6fb", sub="#c0cfda", line="#839bae", ok="#97dec5", okbg="#193e37", ng="#e6b566", ngbg="#3a2f1b", mid="#2a3c4d", bar="#6fb6d9")
    return dict(bg="#f5f8f7", card="#ffffff", ink="#183044", sub="#4e6574", line="#647d8a", ok="#17654f", okbg="#e3f2eb", ng="#9a5a00", ngbg="#fbefd9", mid="#e8eef2", bar="#2f7ea6")


def t(x, y, s, size, color, anchor="start", bold=False):
    w = ' font-weight="700"' if bold else ""
    return f'<text x="{x}" y="{y}" font-size="{size}" fill="{color}"{w} text-anchor="{anchor}">{escape(s)}</text>'


def svg(w, h, title, desc, body, c):
    return (f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}" role="img" aria-labelledby="title desc">\n'
            f'<title id="title">{escape(title)}</title>\n<desc id="desc">{escape(desc)}</desc>\n'
            f'<rect width="{w}" height="{h}" rx="24" fill="{c["bg"]}"/>\n<g font-family="{FONT}">\n' + "\n".join(body) + "\n</g>\n</svg>\n")


def cell(c, x, y, w, h, hit, n):
    """hit/n の1マス。全部検出=緑、0=琥珀、途中=中間色。"""
    if hit == n: fill, ink = c["okbg"], c["ok"]
    elif hit == 0: fill, ink = c["ngbg"], c["ng"]
    else: fill, ink = c["mid"], c["ink"]
    return (f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="7" fill="{fill}"/>'
            + t(x + w / 2, y + h / 2 + 5, f"{hit}/{n}", 14, ink, "middle", True))


PR_LABEL = {
    "pr01": "送料無料の境界（説明文にだけ）", "pr08": "クーポンの併用不可（仕様書にだけ）", "pr10": "戻り値の変更で呼び出し元が壊れる",
    "pr02": "ページ番号が1ずれる", "pr03": "NULLのニックネームに .strip()", "pr04": "在庫の更新が競合する", "pr05": "他人の注文メモを書き換えられる",
    "pr06": "SQLインジェクション", "pr07": "秘密鍵をコミット", "pr09": "キャンセルできる状態を広げた", "pr11": "丸め方式を変えてテストも書き換え", "pr12": "パスワードのハッシュをMD5に",
}
CONDS = [("A", "差分\nのみ"), ("B", "＋説明文"), ("S", "＋説明文\n＋仕様書"), ("T", "＋説明文\n＋コード"), ("C", "＋説明文\n＋仕様書\n＋コード"), ("D", "Cに\n＋規約")]


def heatmap(dark):
    c = pal(dark); P = S["pr"]
    top = ["pr01", "pr08", "pr10"]; rest = ["pr02", "pr03", "pr04", "pr05", "pr06", "pr07", "pr09", "pr11", "pr12"]
    x0, cw, gap = 290, 68, 4
    b = [t(30, 40, "バグ別・渡し方別の検出数（haiku＋sonnet、延べ回数）", 20, c["ink"], bold=True)]
    for i, (k, lab) in enumerate(CONDS):
        for j, line in enumerate(lab.split("\n")):
            b.append(t(x0 + i * (cw + gap) + cw / 2, 66 + j * 15, line, 11, c["sub"], "middle"))
    y = 134
    def row(p, y):
        out = [t(30, y + 21, PR_LABEL[p], 12, c["ink"])]
        for i, (k, _) in enumerate(CONDS):
            h, n = P[f"haiku+sonnet|{k}|{p}"]
            out.append(cell(c, x0 + i * (cw + gap), y, cw, 32, h, n))
        return out
    b.append(t(30, y - 8, "差分に現れない情報が要るバグ", 12, c["ng"], bold=True))
    for p in top: b += row(p, y); y += 38
    y += 22
    b.append(t(30, y - 8, "差分だけで見つかったバグ", 12, c["ok"], bold=True))
    for p in rest: b += row(p, y); y += 38
    b.append(t(30, y + 14, "延べ5回（haiku 3回＋sonnet 2回）。呼び出し元の破壊は、haikuは読む手段がないと0/3。", 11, c["sub"]))
    b.append(t(30, y + 30, "sonnetは差分だけでも「呼び出し元が壊れうる」と推測して当てた（呼び出し元を確かめたわけではない）。", 11, c["sub"]))
    return svg(736, y + 48, "バグ別・渡し方別の検出数", "12個のバグ入りPRを6通りの渡し方で調べた結果。送料の境界は説明文を足すと、クーポンの併用違反は仕様書を足すと、呼び出し元の破壊はコードを読ませると見つかった。残り9個は差分だけで見つかった。", b, c)


def grid(dark):
    c = pal(dark); P = S["pr"]
    b = [t(30, 40, "仕様書とコードを読む力は、別々のバグに効いた", 20, c["ink"], bold=True)]
    bx, by, bw, bh = 170, 130, 250, 150
    heads = [("コードを読めない", 0), ("コードを読める", 1)]
    for lab, i in heads:
        b.append(t(bx + i * (bw + 12) + bw / 2, by - 16, lab, 15, c["ink"], "middle", True))
    rows = [("仕様書なし", ["B", "T"]), ("仕様書あり", ["S", "C"])]
    for r, (rl, keys) in enumerate(rows):
        yy = by + r * (bh + 12)
        b.append(t(150, yy + bh / 2 + 5, rl, 15, c["ink"], "end", True))
        for i, k in enumerate(keys):
            xx = bx + i * (bw + 12)
            b.append(f'<rect x="{xx}" y="{yy}" width="{bw}" height="{bh}" rx="14" fill="{c["card"]}" stroke="{c["line"]}" stroke-width="1.2"/>')
            b.append(t(xx + 16, yy + 28, {"B": "説明文だけ", "T": "説明文＋コードを読む", "S": "説明文＋仕様書", "C": "説明文＋仕様書＋コードを読む"}[k], 13, c["sub"]))
            for j, (p, lab) in enumerate([("pr08", "クーポンの併用違反（仕様書にだけ）"), ("pr10", "呼び出し元の破壊（別ファイル）")]):
                h, n = P[f"haiku+sonnet|{k}|{p}"]
                y2 = yy + 44 + j * 50
                b.append(t(xx + 16, y2 + 12, lab, 12, c["ink"]))
                b.append(cell(c, xx + 16, y2 + 18, 60, 22, h, n).replace('font-size="14"', 'font-size="13"'))
    b.append(t(30, 2 * bh + by + 40, "仕様書を足すと併用違反が、コードを読ませると呼び出し元の破壊が見つかった。足していない方は0/5のまま。", 12, c["sub"]))
    b.append(t(30, 2 * bh + by + 60, "pr10のBとSの2/5は、sonnetが「呼び出し元が壊れうる」と推測した分（haikuは0/3）。", 12, c["sub"]))
    return svg(736, by + 2 * bh + 90, "仕様書とコードを読む力の切り分け", "仕様書の有無とコードを読める・読めないを組み合わせた4条件の結果。クーポンの併用違反は仕様書があるときだけ、呼び出し元の破壊はコードを読めるときに安定して見つかった。", b, c)


def falsepos(dark):
    c = pal(dark); Cl = S["cells"]
    b = [t(30, 40, "誤指摘は、差分だけ・説明文までのときに多かった", 20, c["ink"], bold=True),
         t(30, 64, "バグ入りPRを1回レビューさせたときの、誤指摘の数（haiku＋sonnet、60回ずつ）", 12, c["sub"])]
    x0, maxw = 250, 290
    vals = [(k, lab.replace("\n", ""), Cl[f"haiku+sonnet|{k}"]["fp"] / Cl[f"haiku+sonnet|{k}"]["runs"], Cl[f"haiku+sonnet|{k}"]["fp"]) for k, lab in CONDS]
    mx = max(v[2] for v in vals)
    for i, (k, lab, v, n) in enumerate(vals):
        y = 90 + i * 42
        b.append(t(30, y + 20, lab, 13, c["ink"]))
        w = max(3, maxw * v / mx)
        b.append(f'<rect x="{x0}" y="{y}" width="{w:.0f}" height="26" rx="6" fill="{c["bar"]}"/>')
        b.append(t(x0 + w + 8, y + 19, f"{v:.2f}件/回（計{n}件）", 12, c["ink"]))
    b.append(t(30, 90 + 6 * 42 + 14, "差分だけのとき最も多かった誤指摘は、差分の外にある物を「未定義」「存在しない」と推測するもの。", 12, c["sub"]))
    return svg(736, 90 + 6 * 42 + 40, "条件別の誤指摘", "差分だけや説明文までの条件では、バグ入りPR1回あたり約0.25件の誤指摘が出た。仕様書やコードを読む手段を足すと0.02〜0.10件だった。", b, c)


def layers(dark):
    c = pal(dark)
    rows = [("1  差分（変えた行だけ）", "書かれたコードの中の誤り", "NULL、SQLインジェクション、秘密鍵、競合 など（12件中9件）"),
            ("2  ＋PR説明文", "「やりたいこと」と食い違う実装", "説明は「10000以上で無料」、コードは「10000超」"),
            ("3  ＋仕様書・要件", "差分に出てこない要件の違反", "仕様書にだけ「クーポンはまとめ買いと併用不可」"),
            ("4  ＋リポジトリを読む力", "別ファイルへの影響／実在の確認", "呼び出し元の破壊。「未定義」という決めつけも減る"),
            ("5  ＋チーム規約（REVIEW.md）", "そのチーム固有のルール", "監査ログ、CHANGELOG、金額整形の関数 など")]
    b = [t(30, 40, "AIレビューに渡せるものと、それで見えてくるもの", 20, c["ink"], bold=True)]
    for i, (name, sees, ex) in enumerate(rows):
        y = 66 + i * 76; x = 30 + i * 14; w = 676 - i * 14
        b.append(f'<rect x="{x}" y="{y}" width="{w}" height="64" rx="12" fill="{c["okbg"] if i == 0 else c["card"]}" stroke="{c["ok"] if i == 0 else c["line"]}" stroke-width="1.3"/>')
        b.append(t(x + 16, y + 26, name, 14, c["ink"], bold=True))
        b.append(t(x + 16, y + 48, ex, 12, c["sub"]))
        b.append(t(x + w - 16, y + 26, sees, 13, c["ok"], "end", True))
    b.append(t(30, 66 + 5 * 76 + 8, "下に行くほど、渡すのに手間がかかる。そのぶん、見つかるものの種類が増える。", 12, c["sub"]))
    return svg(736, 66 + 5 * 76 + 30, "AIレビューに渡せるもの", "差分、PR説明文、仕様書、リポジトリを読む力、チーム規約の5つを段階的に足していくと、見つかるバグの種類が増えていく。", b, c)


def rules(dark):
    c = pal(dark)
    items = [("pr16", "金額は format_cents() で整形する", "近くのコードに実例がある"), ("pr17", "状態を変える関数は audit.record() を呼ぶ", "create_order / cancel_order に実例がある"),
             ("pr18", "公開関数を足したら CHANGELOG.md に1行", "コードからは分からない")]
    b = [t(30, 40, "規約にしか書いていないルールは、規約を渡すと見つかった", 20, c["ink"], bold=True),
         t(30, 62, "「規約なし」は、説明文・仕様書・コードを読む力まで渡した状態。haiku・sonnet・opusを延べ7回ずつ", 12, c["sub"])]
    x1, x2 = 470, 590
    b.append(t(x1 + 50, 98, "規約なし", 13, c["sub"], "middle", True)); b.append(t(x2 + 50, 98, "規約あり", 13, c["sub"], "middle", True))
    for i, (p, rule, note) in enumerate(items):
        y = 112 + i * 64
        b.append(t(30, y + 22, rule, 14, c["ink"], bold=True)); b.append(t(30, y + 42, note, 12, c["sub"]))
        for x, k in [(x1, "C"), (x2, "D")]:
            v = CV[f"all|{k}|{p}"]
            b.append(cell(c, x, y + 2, 100, 44, v["hit"], v["runs"]).replace('font-size="14"', 'font-size="18"'))
    y = 112 + 3 * 64
    b.append(f'<line x1="30" y1="{y - 6}" x2="706" y2="{y - 6}" stroke="{c["line"]}" stroke-width="1"/>')
    b.append(t(30, y + 22, "規約どおりに書いたPR（対照）", 14, c["ink"], bold=True)); b.append(t(30, y + 42, "指摘が出た回数。出たらすべて誤指摘（haikuだけ）", 12, c["sub"]))
    for x, k in [(x1, "C"), (x2, "D")]:
        v = CV[f"all|{k}|pr19"]
        fill, ink = (c["ngbg"], c["ng"]) if v["any_runs"] else (c["okbg"], c["ok"])
        b.append(f'<rect x="{x}" y="{y + 2}" width="100" height="44" rx="7" fill="{fill}"/>' + t(x + 50, y + 31, f'{v["any_runs"]}/{v["runs"]}', 18, ink, "middle", True))
    b.append(t(30, y + 76, "規約なしの「見つかった」は、ほぼopus。近くの実例からルールを推測した。規約を足したら全モデルが見つけた。", 12, c["sub"]))
    return svg(736, y + 100, "規約の有無と検出数", "規約にしか書いていないルールの違反を、規約を渡さないときは21回中5回、渡したときは21回中21回見つけた。規約どおりのPRでの誤指摘は、規約を足しても増えなかった。", b, c)


for name, fn in [("layers", layers), ("heatmap", heatmap), ("grid", grid), ("falsepos", falsepos), ("rules", rules)]:
    open(f"{HERE}/figure-{name}.svg", "w").write(fn(False))
    open(f"{HERE}/figure-{name}.dark.svg", "w").write(fn(True))
