#!/usr/bin/env python3
"""
PPT Master - Shape Boolean SVG Fragment Tool

Combine closed SVG shapes or resolvable text outlines and print the resulting
canonical SVG path fragment to stdout. Result geometry is in SVG-root coordinate
space: replace the operands at their original z-order under the final semantic
or structured parent, never under the old transformed ancestor. The source SVG
is read-only; this tool never rewrites the page.

Usage:
    python3 scripts/shape_boolean_svg.py render SVG_FILE \
        --operation OPERATION --source ID --source ID --id OUTPUT_ID

Examples:
    python3 scripts/shape_boolean_svg.py render slide.svg \
        --operation intersect --source circle --source card --id overlap
    python3 scripts/shape_boolean_svg.py render slide.svg \
        --operation subtract --source body --source cutout --id result \
        --fill "#2563EB" --stroke none
    python3 scripts/shape_boolean_svg.py render cover.svg \
        --operation subtract --source scrim --source chapter-number --id reveal

Dependencies:
    skia-pathops, local PPT Master modules, and uharfbuzz for text operands
"""

from __future__ import annotations

import argparse
import math
import sys
from pathlib import Path
from typing import Sequence

from console_encoding import configure_utf8_stdio


configure_utf8_stdio()


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description=(
            "Print the Boolean result of closed SVG shapes or resolvable text "
            "outlines as canonical SVG path fragments in SVG-root coordinate "
            "space. Insert the result at the original z-order under the final "
            "semantic or structured parent, never under the old transformed "
            "ancestor. The source file is never modified."
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    render_parser = subparsers.add_parser(
        "render",
        help="Print SVG-root-coordinate Boolean-result paths to stdout.",
    )
    render_parser.add_argument(
        "svg_file",
        type=Path,
        help="Source SVG containing the operand elements.",
    )
    render_parser.add_argument(
        "--operation",
        required=True,
        choices=("union", "combine", "fragment", "intersect", "subtract"),
        help="PowerPoint-compatible merge-shapes operation.",
    )
    render_parser.add_argument(
        "--source",
        action="append",
        required=True,
        dest="source_ids",
        metavar="ID",
        help=(
            "Operand element id in merge order; repeat at least twice. "
            "The first operand is primary for subtract and style inheritance."
        ),
    )
    render_parser.add_argument(
        "--id",
        required=True,
        dest="output_id",
        help="Stable id for the result, or the base id for fragment results.",
    )
    render_parser.add_argument(
        "--font-dir",
        action="append",
        default=[],
        dest="font_dirs",
        type=Path,
        metavar="PATH",
        help=(
            "Additional font directory for text operands; repeat as needed. "
            "Explicit directories are searched before system font directories."
        ),
    )
    render_parser.add_argument(
        "--fill",
        help="Override the primary shape's solid SVG fill, or use none.",
    )
    render_parser.add_argument(
        "--fill-opacity",
        type=float,
        help="Override fill opacity from 0 to 1.",
    )
    render_parser.add_argument(
        "--stroke",
        help="Override the primary shape's solid SVG stroke, or use none.",
    )
    render_parser.add_argument(
        "--stroke-width",
        type=float,
        help="Override stroke width in SVG page units.",
    )
    render_parser.add_argument(
        "--stroke-opacity",
        type=float,
        help="Override stroke opacity from 0 to 1.",
    )
    return parser


def main(argv: Sequence[str] | None = None) -> int:
    parser = build_parser()
    args = parser.parse_args(argv)

    try:
        style = _style_from_args(args)
        _validate_render_args(args)
        from svg_to_pptx.shape_boolean import (
            render_boolean_svg_fragments,
        )

        fragment = render_boolean_svg_fragments(
            args.svg_file,
            operation=args.operation,
            source_ids=args.source_ids,
            output_id=args.output_id,
            style=style or None,
            font_dirs=args.font_dirs,
        )
    except (OSError, RuntimeError, ValueError) as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1

    print(fragment)
    return 0


def _validate_render_args(args: argparse.Namespace) -> None:
    if len(args.source_ids) < 2:
        raise ValueError("--source must be repeated at least twice")
    if len(set(args.source_ids)) != len(args.source_ids):
        raise ValueError("--source ids must be unique")
    if not args.output_id.strip():
        raise ValueError("--id must not be empty")
    for font_dir in args.font_dirs:
        if not font_dir.is_dir():
            raise ValueError(f"--font-dir is not a directory: {font_dir}")


def _style_from_args(args: argparse.Namespace) -> dict[str, str]:
    style: dict[str, str] = {}
    if args.fill is not None:
        style["fill"] = args.fill
    if args.fill_opacity is not None:
        _validate_opacity("--fill-opacity", args.fill_opacity)
        style["fill-opacity"] = str(args.fill_opacity)
    if args.stroke is not None:
        style["stroke"] = args.stroke
    if args.stroke_width is not None:
        if not math.isfinite(args.stroke_width) or args.stroke_width < 0:
            raise ValueError("--stroke-width must be greater than or equal to 0")
        style["stroke-width"] = str(args.stroke_width)
    if args.stroke_opacity is not None:
        _validate_opacity("--stroke-opacity", args.stroke_opacity)
        style["stroke-opacity"] = str(args.stroke_opacity)
    return style


def _validate_opacity(option: str, value: float) -> None:
    if not math.isfinite(value) or not 0 <= value <= 1:
        raise ValueError(f"{option} must be between 0 and 1")


if __name__ == "__main__":
    raise SystemExit(main())
