#!/usr/bin/env python3
"""Generate a triangulated mesh graphic as an SVG file.

Example:
    python scripts/generate_triangulated_mesh.py --width 1920 --height 1080 --output mesh.svg
"""

from __future__ import annotations

import argparse
import math
import random
from dataclasses import dataclass
from pathlib import Path


@dataclass(frozen=True)
class Point:
    x: float
    y: float


def clamp(value: float, low: float, high: float) -> float:
    return max(low, min(high, value))


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Generate a triangulated mesh SVG.")
    parser.add_argument("--width", type=int, required=True, help="Graphic width in pixels.")
    parser.add_argument("--height", type=int, required=True, help="Graphic height in pixels.")
    parser.add_argument(
        "--output",
        type=Path,
        default=Path("triangulated-mesh.svg"),
        help="Output SVG path (default: triangulated-mesh.svg)",
    )
    parser.add_argument(
        "--cell-size",
        type=float,
        default=120.0,
        help="Approximate grid cell size in pixels when x/y step are not set.",
    )
    parser.add_argument(
        "--x-step",
        type=float,
        default=None,
        help="Horizontal point spacing in pixels (overrides --cell-size on x-axis).",
    )
    parser.add_argument(
        "--y-step",
        type=float,
        default=None,
        help="Vertical point spacing in pixels (overrides --cell-size on y-axis).",
    )
    parser.add_argument(
        "--jitter",
        type=float,
        default=0.35,
        help="Point jitter amount as a fraction of cell size (0.0-0.5).",
    )
    parser.add_argument(
        "--seed",
        type=int,
        default=None,
        help="Random seed for reproducible output.",
    )
    parser.add_argument(
        "--stroke",
        type=float,
        default=1.25,
        help="Edge stroke width in pixels.",
    )
    parser.add_argument(
        "--dot-radius",
        type=float,
        default=1.9,
        help="Vertex dot radius in pixels.",
    )
    parser.add_argument(
        "--line-color",
        type=str,
        default="#334155",
        help="Edge color as hex (default: #334155).",
    )
    parser.add_argument(
        "--dot-color",
        type=str,
        default="#0f172a",
        help="Dot color as hex (default: #0f172a).",
    )
    parser.add_argument(
        "--background",
        type=str,
        default="#f8fafc",
        help="Background color as hex (default: #f8fafc).",
    )
    return parser.parse_args()


def build_points(
    width: int,
    height: int,
    x_step: float,
    y_step: float,
    jitter_frac: float,
    rng: random.Random,
) -> list[list[Point]]:
    cols = max(2, math.ceil(width / x_step) + 1)
    rows = max(2, math.ceil(height / y_step) + 1)

    points: list[list[Point]] = []
    jitter = clamp(jitter_frac, 0.0, 0.5) * min(x_step, y_step)

    for row in range(rows):
        y = (height * row) / (rows - 1)
        row_points: list[Point] = []
        for col in range(cols):
            x = (width * col) / (cols - 1)

            # Keep border anchored to make the mesh fill the canvas cleanly.
            if 0 < row < rows - 1 and 0 < col < cols - 1:
                x += rng.uniform(-jitter, jitter)
                y_jittered = y + rng.uniform(-jitter, jitter)
            else:
                y_jittered = y

            row_points.append(Point(x=clamp(x, 0.0, float(width)), y=clamp(y_jittered, 0.0, float(height))))
        points.append(row_points)

    return points


def generate_triangles(points: list[list[Point]], rng: random.Random) -> list[tuple[Point, Point, Point]]:
    triangles: list[tuple[Point, Point, Point]] = []
    rows = len(points)
    cols = len(points[0]) if rows else 0

    for row in range(rows - 1):
        for col in range(cols - 1):
            p00 = points[row][col]
            p10 = points[row][col + 1]
            p01 = points[row + 1][col]
            p11 = points[row + 1][col + 1]

            if rng.random() < 0.5:
                triangles.append((p00, p10, p11))
                triangles.append((p00, p11, p01))
            else:
                triangles.append((p00, p10, p01))
                triangles.append((p10, p11, p01))

    return triangles


def svg_polygon(points: tuple[Point, Point, Point], stroke: str, stroke_width: float) -> str:
    pts = " ".join(f"{p.x:.2f},{p.y:.2f}" for p in points)
    return (
        f'<polygon points="{pts}" fill="none" '
        f'stroke="{stroke}" stroke-width="{stroke_width:.3f}" stroke-linejoin="round" />'
    )


def write_svg(
    width: int,
    height: int,
    triangles: list[tuple[Point, Point, Point]],
    stroke_width: float,
    dot_radius: float,
    line_color: str,
    dot_color: str,
    background: str,
    output: Path,
    points: list[list[Point]],
) -> None:
    elements: list[str] = []

    for tri in triangles:
        elements.append(svg_polygon(tri, stroke=line_color, stroke_width=stroke_width))

    circles: list[str] = []
    for row in points:
        for p in row:
            circles.append(f'<circle cx="{p.x:.2f}" cy="{p.y:.2f}" r="{dot_radius:.2f}" fill="{dot_color}" />')

    svg = "\n".join(
        [
            '<?xml version="1.0" encoding="UTF-8"?>',
            f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">',
            f'<rect x="0" y="0" width="100%" height="100%" fill="{background}" />',
            *elements,
            *circles,
            "</svg>",
        ]
    )

    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(svg, encoding="utf-8")


def main() -> None:
    args = parse_args()

    if args.width <= 0 or args.height <= 0:
        raise SystemExit("--width and --height must be positive integers")

    if args.cell_size <= 1:
        raise SystemExit("--cell-size must be > 1")

    x_step = args.x_step if args.x_step is not None else args.cell_size
    y_step = args.y_step if args.y_step is not None else args.cell_size

    if x_step <= 1 or y_step <= 1:
        raise SystemExit("--x-step and --y-step must be > 1")

    rng = random.Random(args.seed)
    points = build_points(args.width, args.height, x_step, y_step, args.jitter, rng)
    triangles = generate_triangles(points, rng)
    write_svg(
        args.width,
        args.height,
        triangles,
        args.stroke,
        args.dot_radius,
        args.line_color,
        args.dot_color,
        args.background,
        args.output,
        points,
    )

    print(f"Generated {len(triangles)} triangles -> {args.output}")


if __name__ == "__main__":
    main()
