"""Rebuild article figures from published summaries and the retained RoboLab table.

Requires matplotlib and numpy. Run from any directory. SVGs include mobile layouts;
PNG counterparts support visual inspection. No inferred uncertainty is plotted.
"""
from html.parser import HTMLParser
import json
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.patches import Patch
import numpy as np

OUT = Path(__file__).resolve().parent
ASSETS = OUT.parent
PAPER, INK, MUTED, GRID = "#f8eee8", "#211c1a", "#75655e", "#dcc7bc"
NEWTON, PHYSX = "#287b78", "#b65f32"
SEEDS = [NEWTON, "#8273a6", "#647d9e", "#a17b21", "#b64d48"]
plt.rcParams.update({
    "font.family": "DejaVu Sans", "font.size": 11, "text.color": INK,
    "axes.labelcolor": INK, "xtick.color": MUTED, "ytick.color": INK,
    "axes.facecolor": PAPER, "figure.facecolor": PAPER,
    "axes.spines.top": False, "axes.spines.right": False,
    "axes.edgecolor": GRID, "svg.fonttype": "path",
    "savefig.facecolor": PAPER, "axes.titleweight": "bold",
})


def save(fig, name, mobile):
    stem = name + ("-mobile" if mobile else "")
    for ext in ("svg", "png"):
        fig.savefig(OUT / f"{stem}.{ext}", dpi=150)
    plt.close(fig)


def xgrid(ax):
    ax.set_axisbelow(True)
    ax.grid(axis="x", color=GRID, alpha=.7)
    ax.spines["left"].set_visible(False)
    ax.tick_params(axis="y", length=0)


def engine_legend(fig, y=.99):
    fig.legend([
        Line2D([], [], color=PHYSX, marker="o", linestyle="", markersize=7),
        Line2D([], [], color=NEWTON, marker="D", linestyle="", markersize=6),
    ], ["Evaluated in PhysX", "Evaluated in Newton"],
        loc="upper center", bbox_to_anchor=(.5, y), ncol=2,
        frameon=False, fontsize=10, handletextpad=.4, columnspacing=1.2)


def baseline(mobile):
    data = json.loads((ASSETS / "study-2026-10-09/summary.json").read_text())
    engines = [("mujoco_cpu", "MuJoCo CPU"), ("newton_mjwarp", "Newton"),
               ("physx_gpu", "PhysX GPU"), ("genesis", "Genesis")]
    fig, ax = plt.subplots(figsize=(4.7, 4.6) if mobile else (8.5, 3.9))
    groups = np.arange(len(engines))
    for seed, color in enumerate(SEEDS):
        counts = [round(data["engines"][engine]["0"]["success_rate_across_seeds"]["new_50"]["values"][seed] * 50)
                  for engine, _ in engines]
        positions = groups + (seed-2)*.15
        ax.bar(positions, counts, width=.135, color=color, zorder=3)
        if seed in (0, 4):
            ax.annotate(str(counts[2]), (positions[2], counts[2]), xytext=(0, 4),
                        textcoords="offset points", ha="center", color=color, fontsize=10)
    ax.set(xticks=groups, xticklabels=["MuJoCo\nCPU", "Newton", "PhysX\nGPU", "Genesis"],
           ylim=(0, 55), yticks=range(0, 51, 10), xlim=(-.55, 3.55),
           ylabel="Successful starts (out of 50)")
    ax.set_axisbelow(True)
    ax.grid(axis="y", color=GRID, alpha=.7)
    ax.tick_params(axis="x", length=0, pad=8)
    fig.legend([Patch(facecolor=c) for c in SEEDS],
               [f"Seed {i}" for i in range(5)], loc="upper center", ncol=3 if mobile else 5,
               frameon=False, fontsize=10, handletextpad=.4, columnspacing=1, handlelength=1)
    fig.subplots_adjust(left=.17 if mobile else .10, right=.97,
                        top=.81 if mobile else .85, bottom=.17)
    save(fig, "insertion-seeds", mobile)


def stiffness(mobile):
    data = json.loads((ASSETS / "settings-ablation-2026-10-19/screen-summary.json").read_text())["conditions"]
    names = ["mujoco-timeconst-20ms", "mujoco-nominal", "mujoco-timeconst-5ms",
             "mujoco-timeconst-2ms", "mujoco-timeconst-0.5ms-step-0.25ms"]
    x = [20, 10, 5, 2, .5]
    fig, axes = plt.subplots(2, 1, sharex=True, figsize=(4.7, 6.1) if mobile else (8.5, 5.4))
    for seed, marker in [(0, "o"), (2, "s"), (4, "^")]:
        y = [data[name]["policies"][f"seed{seed}"]["success"] for name in names]
        axes[0].plot(x, y, color=SEEDS[seed], marker=marker, lw=2, label=f"Seed {seed}")
    axes[0].set(ylim=(-2, 54), yticks=[0, 10, 20, 30, 40, 50], ylabel="Successes / 50 starts")
    axes[0].legend(loc="lower left", ncol=3, frameon=False, fontsize=10, columnspacing=.7, handlelength=1.3)
    force = [data[name]["policies"]["seed4"]["median_peak_force_N"] for name in names]
    axes[1].plot(x, force, color=SEEDS[4], marker="^", lw=2)
    axes[1].axhline(82, color=MUTED, ls="--", lw=1)
    axes[1].text(22, 85, "PhysX nominal: 82 N", color=MUTED, fontsize=10)
    axes[1].set(ylim=(0, 100), yticks=[0, 20, 40, 60, 80, 100],
                ylabel="Seed 4 median peak (N)", xlabel="Contact time constant (ms) · stiffer →")
    for ax in axes:
        ax.set_xscale("log")
        ax.set_xlim(25, .35)
        ax.set_xticks(x, ["20", "10", "5", "2", "0.5*"])
        ax.minorticks_off()
        ax.grid(axis="y", color=GRID, alpha=.7)
        ax.axvline(10, color=GRID, ls=":", zorder=0)
        ax.set_axisbelow(True)
    axes[0].text(10, 56, "Scene default", ha="center", fontsize=10, color=MUTED)
    fig.subplots_adjust(left=.18 if mobile else .12, right=.97, top=.92, bottom=.12, hspace=.15)
    save(fig, "contact-stiffness", mobile)


def lift(mobile):
    data = json.loads((ASSETS / "lift-2026-10-24/summary.json").read_text())["final_evaluation"]
    fig, axes = plt.subplots(2, 1, figsize=(4.7, 6.8)) if mobile else plt.subplots(1, 2, figsize=(9.5, 3.7), sharey=True)
    for ax, trained, label in zip(axes, ["N", "P"], ["Newton-trained policy", "PhysX-trained policy"]):
        for row, condition in enumerate(["standard", "play", "nominal"]):
            values = {engine: data[f"{trained}19998-on-{engine}-{condition}"]["success_rate"]*100 for engine in ["N", "P"]}
            ax.plot(list(values.values()), [row, row], color=GRID, lw=3, zorder=1)
            for engine, color, marker in [("P", PHYSX, "o"), ("N", NEWTON, "D")]:
                value = values[engine]
                ax.scatter(value, row, color=color, marker=marker, s=58, zorder=3)
                ax.annotate(f"{value:.0f}%", (value, row), xytext=(0, 10 if engine=="N" else -17),
                            textcoords="offset points", ha="center", color=color, fontsize=10)
        ax.set(xlim=(0, 102), xticks=[0, 25, 50, 75, 100], ylim=(2.6, -.6),
               yticks=range(3), yticklabels=["Standard", "Playback", "Nominal"], xlabel="Success (%)")
        ax.set_title(label, loc="left", fontsize=12, pad=13)
        xgrid(ax)
    engine_legend(fig)
    fig.subplots_adjust(left=.23 if mobile else .13, right=.96, top=.86 if mobile else .74,
                        bottom=.09 if mobile else .17, hspace=.60, wspace=.28)
    save(fig, "lift-transfer", mobile)


class Table(HTMLParser):
    def __init__(self, table_id):
        super().__init__()
        self.table_id, self.active, self.cell = table_id, False, None
        self.rows = []
    def handle_starttag(self, tag, attrs):
        if tag == "table": self.active = dict(attrs).get("id") == self.table_id
        if self.active and tag == "tr": self.rows.append([])
        if self.active and tag in ("td", "th"): self.cell = ""
    def handle_data(self, text):
        if self.cell is not None: self.cell += text
    def handle_endtag(self, tag):
        if self.active and tag in ("td", "th"):
            self.rows[-1].append(" ".join(self.cell.split())); self.cell = None
        if tag == "table": self.active = False


def robolab(mobile):
    table = Table("robolab-b0-table")
    table.feed((ASSETS.parent / "index.html").read_text())
    rows = table.rows[1:]
    assert len(rows) == 4
    fig, axes = plt.subplots(3, 1, figsize=(4.7, 10.8)) if mobile else plt.subplots(1, 3, figsize=(11, 4.1), sharey=True)
    for ax, col, title, limit, ticks in zip(axes, [1, 3, 4],
            ["Task completion", "Failed grasps", "Drops"], [50, 35, 60],
            [[0, 25, 50], [0, 10, 20, 30], [0, 20, 40, 60]]):
        for row, values in enumerate(rows):
            counts = [int(s.strip()) for s in values[col].split("/")]
            ax.plot(counts, [row, row], color=GRID, lw=2, zorder=1)
            for index, (value, color, marker) in enumerate(zip(counts, [PHYSX, NEWTON], ["o", "D"])):
                # A larger circle and smaller diamond remain visible at tied values without moving the data.
                ax.scatter(value, row, facecolors=color,
                           edgecolors=color, linewidths=1.6 if index==0 else 1,
                           marker=marker, s=64 if index==0 else 28, zorder=3+index)
                ax.annotate(str(value), (value, row), xytext=(0, 10 if index==0 else -17),
                            textcoords="offset points", ha="center", color=color, fontsize=9.5)
        ax.set(xlim=(-2, limit+2), xticks=ticks, ylim=(3.6, -.6),
               yticks=range(4), yticklabels=["Cube → bowl", "Banana → bowl", "Banana → plate", "Bottle → pail"],
               xlabel="Successes / 50 episodes" if col==1 else "Events across 50 episodes")
        ax.set_title(title, loc="left", fontsize=12, pad=12)
        xgrid(ax)
    engine_legend(fig)
    fig.subplots_adjust(left=.32 if mobile else .17, right=.96, top=.91 if mobile else .76,
                        bottom=.06 if mobile else .17, hspace=.60, wspace=.20)
    save(fig, "robolab-handling", mobile)


if __name__ == "__main__":
    for mobile in [False, True]:
        for plot in [baseline, stiffness, lift, robolab]:
            plot(mobile)
