#!/usr/bin/env python3
"""trace: deterministic image-to-SVG tracing for exact shape geometry.

Runs fully local (no vision API, no credentials): coordinates come from the
actual pixels, so use this instead of asking a vision model for shapes,
sizes, or outlines. Flat, high-contrast graphics only; text becomes curves.
"""

import argparse
from pathlib import Path
import re
import sys
import tempfile

WHITE_FILLS = {"#ffffff", "#fff", "white"}

# Speckle filtering drops components under a fixed pixel area, so a 30px icon
# binarizes to nothing at 1x while the same icon enlarged traces cleanly.
# Enlarge anything smaller than this; full-size screenshots already clear it.
TARGET_MIN_SIDE = 256


def strip_background(svg):
    """Drop the leading full-canvas white path vtracer emits for the background."""
    match = re.search(r"<path [^>]*/>", svg)
    if match:
        fill = re.search(r'fill="([^"]+)"', match.group(0))
        if fill and fill.group(1).strip().lower() in WHITE_FILLS:
            return svg.replace(match.group(0), "", 1)
    return svg


def truncate_decimals(svg, places=2):
    return re.sub(r"-?\d+\.\d{3,}", lambda m: f"{float(m.group()):.{places}f}", svg)


def write_svg(path, svg):
    """Write the exact UTF-8 payload whose byte count is reported to callers."""
    payload = svg.encode("utf-8")
    return Path(path).expanduser().write_bytes(payload)


def prepare_input(path, region, scale):
    """Crop/upscale with Pillow when asked; return the path vtracer reads and the scale used.

    `scale` may be None, meaning "pick one from the size": small shapes are
    enlarged until they survive speckle filtering. Without Pillow we cannot
    measure the image, so that choice falls back to no upscale.
    """
    try:
        from PIL import Image
    except ImportError:
        if region or (scale or 1) != 1:
            sys.exit("trace: --region/--scale require Pillow; install the optional dependency pillow first")
        return str(path), 1
    with Image.open(path) as image:
        if region:
            try:
                x1, y1, x2, y2 = (int(v) for v in region.split(","))
            except ValueError:
                sys.exit("trace: --region expects four integers: X1,Y1,X2,Y2 (pixels)")
            box = (max(0, min(x1, x2)), max(0, min(y1, y2)),
                   min(image.width, max(x1, x2)), min(image.height, max(y1, y2)))
            if box[2] <= box[0] or box[3] <= box[1]:
                sys.exit(f"trace: --region {region} is empty after clamping to {image.width}x{image.height}")
            image = image.crop(box)
        if scale is None:
            shortest = max(min(image.width, image.height), 1)
            scale = max(2 if region else 1, -(-TARGET_MIN_SIDE // shortest))
        if not region and scale == 1:
            return str(path), 1
        if scale != 1:
            image = image.resize((image.width * scale, image.height * scale), Image.LANCZOS)
        handle = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
        image.save(handle.name)
        return handle.name, scale


def main():
    parser = argparse.ArgumentParser(
        prog="trace",
        description="Trace an image into SVG with exact pixel-derived geometry (local, deterministic)",
    )
    parser.add_argument("image", help="path to the image")
    parser.add_argument("--region", metavar="X1,Y1,X2,Y2",
                        help="crop to this pixel box first (e.g. a box from ground)")
    parser.add_argument("--scale", type=int, metavar="N",
                        help="upscale N times before tracing (default: enough to clear "
                             f"{TARGET_MIN_SIDE}px on the shorter side, so small icons survive)")
    parser.add_argument("--polygon", action="store_true",
                        help="polygon mode for boxy diagrams/wireframes (default: spline for curved shapes)")
    parser.add_argument("--color", action="store_true",
                        help="keep colors (default: black-and-white, which is far more compact)")
    parser.add_argument("-o", "--output", help="write the SVG here instead of stdout")
    args = parser.parse_args()
    try:
        import vtracer
    except ImportError:
        parser.exit(1, "trace: requires vtracer; install the optional dependency vtracer first\n")
    source = Path(args.image).expanduser()
    if not source.is_file():
        parser.exit(1, f"trace: Image not found: {source}\n")
    work, scale = prepare_input(source, args.region, args.scale)
    with tempfile.NamedTemporaryFile(suffix=".svg", delete=False) as out:
        vtracer.convert_image_to_svg_py(
            work, out.name,
            colormode="color" if args.color else "binary",
            filter_speckle=8, corner_threshold=40,
            mode="polygon" if args.polygon else "spline",
        )
        svg = Path(out.name).read_text()
    svg = truncate_decimals(strip_background(svg))
    paths = svg.count("<path")
    if not paths:
        # This is where an agent decides the tool cannot see the shape and goes
        # back to guessing. Name the recoveries instead, cheapest first.
        print("trace: 0 paths - nothing survived binarization. Try a larger --scale, "
              "--region to crop closer to the shape, or pre-invert a light-on-dark image. "
              "--color is a last resort: on an anti-aliased image it splits every gray "
              "level into its own path.", file=sys.stderr)
    if args.output:
        bytes_written = write_svg(args.output, svg)
        print(f"wrote {args.output} ({bytes_written} bytes, {paths} paths, traced at {scale}x)")
    else:
        print(svg)


if __name__ == "__main__":
    main()
