"""Generate the labelled quadrilateral sequencing example figure for the paper.

Renders the deep embedding G' of a small maximal planar graph, fills each
quadrilateral with a color encoding its type, and overlays the index it
occupies in the canonical sequence Q_1, ..., Q_N. The output is a PDF placed
next to paper.tex.
"""
import os, sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")))  # repo root for `lib`
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))  # sibling experiment modules
import argparse
from pathlib import Path
from typing import Any

import matplotlib
matplotlib.use("Agg")
import matplotlib.patches as patches  # noqa: E402  pylint: disable=wrong-import-position
import matplotlib.pyplot as plt  # noqa: E402  pylint: disable=wrong-import-position

from sage.all import Graph, graphs  # type: ignore[attr-defined]  # pylint: disable=no-name-in-module,wrong-import-position  # noqa: E402
from sage.misc.randstate import set_random_seed  # type: ignore[import-not-found]  # noqa: E402

from lib.tutte_embedding import tutte_embedding  # noqa: E402
from plane_depth_sequencing import (  # noqa: E402
    _level_edge_of_face,
    _quad_type,
    _quad_vertices,
    quadrilateral_sequencing,
)


def _planar_pos(
    g_prime: Graph,
    outer_face: list[Any],
    layout: str,
) -> dict[Any, tuple[float, float]]:
    """Compute positions for g_prime under either 'tutte' or 'sage_planar' layout."""
    if layout == "tutte":
        return tutte_embedding(g_prime, outer_face)
    if layout == "sage_planar":
        g_prime.is_planar(set_embedding=True, set_pos=True)
        pos = g_prime.get_pos()
        assert pos is not None, "Sage failed to set planar positions"
        return {v: (float(p[0]), float(p[1])) for v, p in pos.items()}
    raise ValueError(f"Unknown layout: {layout}")


MOVE_NAMES = {0: "AD", 1: "LA", 2: "J", 3: "RC"}
TYPE_COLORS = {
    "shallow_diamond": "#FFE0B2",
    "deep_diamond": "#B2DFDB",
    "s_quad": "#F8BBD0",
}


def _ordered_quad_vertices(quad: frozenset, depth_labelling: dict[Any, int]) -> list[Any]:
    """Return the 4 quad vertices in cyclic order: e1, a, e2, b."""
    f1, f2 = list(quad)
    level_edge = _level_edge_of_face(f1, depth_labelling)
    e1, e2 = list(level_edge)
    a = next(v for v in f1 if v not in level_edge)
    b = next(v for v in f2 if v not in level_edge)
    return [e1, a, e2, b]


def _pick_outer_cap_face(
    g_prime: Graph,
    outer_cap_vertex: Any,
    outer_cycle: list[Any],
) -> list[Any]:
    """Return the vertex list of an outer-cap face (the face used as outer in the plane drawing)."""
    g_prime.is_planar(set_embedding=True)
    embedding = g_prime.get_embedding()
    outer_set = set(outer_cycle)
    for face in g_prime.faces(embedding):
        verts = [u for u, _ in face]
        if outer_cap_vertex in verts and len(set(verts) & outer_set) == 2:
            return verts
    raise RuntimeError("No outer-cap face found in G' embedding")


def _draw_figure(
    g: Graph,
    outer_cycle: list[Any],
    out_path: Path,
    layout: str = "sage_planar",
) -> dict[str, Any]:
    """Render and save the labelled sequencing figure. Returns summary stats."""
    result = quadrilateral_sequencing(g, outer_cycle)
    g_prime: Graph = result["deep_embedding"]
    depth_labelling: dict[Any, int] = result["depth_labelling"]
    sequence = result["sequence"]
    move_codes = result["move_codes"]
    outer_cap_vertex = result["outer_cap_vertex"]

    outer_face = _pick_outer_cap_face(g_prime, outer_cap_vertex, outer_cycle)
    pos = _planar_pos(g_prime, outer_face, layout)

    fig, ax = plt.subplots(figsize=(9, 9))

    for i, quad in enumerate(sequence):
        ordered = _ordered_quad_vertices(quad, depth_labelling)
        poly_pts = [pos[v] for v in ordered]
        qt = _quad_type(quad, depth_labelling)
        polygon = patches.Polygon(
            poly_pts,
            closed=True,
            facecolor=TYPE_COLORS[qt],
            edgecolor="none",
            alpha=0.65,
            zorder=1,
        )
        ax.add_patch(polygon)

    for u, v in g_prime.edges(labels=False):
        x1, y1 = pos[u]
        x2, y2 = pos[v]
        if depth_labelling[u] == depth_labelling[v]:
            ax.plot([x1, x2], [y1, y2], linestyle=(0, (3, 2)), color="#666666", linewidth=1.0, zorder=2)
        else:
            ax.plot([x1, x2], [y1, y2], "-", color="black", linewidth=1.0, zorder=2)

    outer_set = set(outer_cycle)
    for v, (x, y) in pos.items():
        if v == outer_cap_vertex:
            color = "#D32F2F"
        elif v in outer_set:
            color = "#1976D2"
        else:
            color = "black"
        ax.scatter([x], [y], s=28, color=color, zorder=5, edgecolors="white", linewidths=0.8)
        name = "$x^{*}$" if v == outer_cap_vertex else str(v)
        ax.annotate(
            name,
            (x, y),
            xytext=(7, 6),
            textcoords="offset points",
            fontsize=9,
            zorder=8,
            color="#222222",
            bbox={"boxstyle": "round,pad=0.05", "facecolor": "white", "edgecolor": "none", "alpha": 0.85},
        )

    for i, quad in enumerate(sequence):
        ordered = _ordered_quad_vertices(quad, depth_labelling)
        poly_pts = [pos[v] for v in ordered]
        cx = sum(p[0] for p in poly_pts) / 4
        cy = sum(p[1] for p in poly_pts) / 4
        if i == 0:
            label = "$Q_{1}$"
        else:
            label = f"$Q_{{{i + 1}}}^{{\\mathrm{{{MOVE_NAMES[move_codes[i - 1]]}}}}}$"
        ax.text(
            cx,
            cy,
            label,
            ha="center",
            va="center",
            fontsize=12,
            zorder=7,
            bbox={"boxstyle": "round,pad=0.22", "facecolor": "white", "edgecolor": "#444444", "alpha": 0.95},
        )

    legend_handles = [
        patches.Patch(color=TYPE_COLORS["shallow_diamond"], label="shallow diamond"),
        patches.Patch(color=TYPE_COLORS["deep_diamond"], label="deep diamond"),
        patches.Patch(color=TYPE_COLORS["s_quad"], label="S quad"),
    ]
    ax.legend(handles=legend_handles, loc="lower right", fontsize=10, framealpha=0.95)

    ax.set_aspect("equal")
    ax.axis("off")
    plt.tight_layout()
    out_path.parent.mkdir(parents=True, exist_ok=True)
    plt.savefig(out_path, format="pdf", bbox_inches="tight")
    plt.close(fig)

    return {
        "n_vertices_g": g.order(),
        "n_vertices_gprime": g_prime.order(),
        "n_quads": len(sequence),
        "move_codes": move_codes,
        "outer_cycle": outer_cycle,
        "outer_cap_vertex": outer_cap_vertex,
        "depth_labelling": depth_labelling,
    }


def _build_example(seed: int, n: int) -> tuple[Graph, list[Any]]:
    """Build a random triangulation and pick a deterministic outer cycle."""
    set_random_seed(seed)
    g = graphs.RandomTriangulation(n)
    g.is_planar(set_embedding=True)
    embedding = g.get_embedding()
    faces = g.faces(embedding)
    outer_cycle = [u for u, _ in faces[0]]
    return g, outer_cycle


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--seed", type=int, default=141)
    parser.add_argument("--n", type=int, default=9)
    parser.add_argument(
        "--out",
        type=Path,
        default=Path("papers/plane_depth_sequencing/example_figure.pdf"),
    )
    parser.add_argument("--layout", choices=("tutte", "sage_planar"), default="sage_planar")
    args = parser.parse_args()

    g, outer_cycle = _build_example(args.seed, args.n)
    summary = _draw_figure(g, outer_cycle, args.out, layout=args.layout)

    print(f"Wrote {args.out}")
    print(f"  |V(G)|={summary['n_vertices_g']}, |V(G')|={summary['n_vertices_gprime']}, "
          f"N={summary['n_quads']}")
    print(f"  outer cycle: {summary['outer_cycle']}, x* = {summary['outer_cap_vertex']}")
    code_str = "".join(str(c) for c in summary["move_codes"])
    print(f"  move-code string: {code_str}")


if __name__ == "__main__":
    main()
