#!/usr/bin/env python3
"""
Convert a black/white mask image to a Godot .tres polygon resource.
"""

import argparse
import secrets
import sys
from pathlib import Path

import cv2
import numpy as np
from shapely.geometry import Polygon
from shapely.ops import unary_union


ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"


def to_base36(num: int) -> str:
    """Convert integer to lowercase base36 (like Godot)."""
    if num == 0:
        return "0"
    chars = []
    while num:
        num, rem = divmod(num, 36)
        chars.append(ALPHABET[rem])
    return "".join(reversed(chars))


def generate_godot_uid() -> str:
    """Generate a random Godot-compatible UID."""
    value = secrets.randbits(64)
    return to_base36(value)


def find_largest_contour(contours: list) -> np.ndarray:
    """Find the largest contour by area."""
    if not contours:
        return None
    return max(contours, key=cv2.contourArea)


def contours_to_polygon(contours: list, mode: str) -> np.ndarray | None:
    """
    Convert contours to a single polygon.

    Args:
        contours: List of contours from cv2.findContours
        mode: "convex_hull", "largest_only", or "multiple"

    Returns:
        Single contour as numpy array (or None for "multiple" mode)
    """
    if not contours:
        return None

    if mode == "largest_only":
        return find_largest_contour(contours)

    if mode == "convex_hull":
        all_points = np.vstack(contours)
        return cv2.convexHull(all_points)

    if mode == "multiple":
        return None

    raise ValueError(f"Unknown mode: {mode}")


def process_contour(
    contour: np.ndarray, padding: float, target_points: int
) -> np.ndarray:
    """
    Process a single contour: pad, simplify, and ensure clockwise.
    """
    padded = pad_polygon(contour, padding)
    simplified = simplify_to_target_points(padded, target_points)
    return ensure_clockwise(simplified)


def simplify_to_target_points(contour: np.ndarray, target_points: int) -> np.ndarray:
    """
    Simplify a contour to approximately target number of points.
    Uses Douglas-Peucker algorithm with iterative epsilon adjustment.
    """
    if len(contour) <= target_points:
        return contour

    contour = contour.squeeze()
    if len(contour.shape) == 1:
        return contour.reshape(-1, 1, 2)

    perimeter = cv2.arcLength(contour, True)
    epsilon_low = 0.0
    epsilon_high = perimeter * 0.1

    best_result = contour

    for _ in range(50):
        epsilon_mid = (epsilon_low + epsilon_high) / 2
        simplified = cv2.approxPolyDP(contour, epsilon_mid, True)

        if len(simplified) == target_points:
            return simplified

        if len(simplified) > target_points:
            epsilon_low = epsilon_mid
        else:
            epsilon_high = epsilon_mid
            best_result = simplified

        if abs(len(simplified) - target_points) <= 1:
            best_result = simplified
            break

    return best_result


def pad_polygon(points: np.ndarray, padding: float) -> np.ndarray:
    """
    Expand polygon outward by padding pixels using Shapely buffer.
    """
    if padding <= 0:
        return points

    points_2d = points.squeeze()
    if len(points_2d.shape) == 1:
        return points

    polygon = Polygon(points_2d)

    if not polygon.is_valid:
        polygon = polygon.buffer(0)

    buffered = polygon.buffer(padding)

    if buffered.is_empty:
        return points

    if buffered.geom_type == "MultiPolygon":
        buffered = max(buffered.geoms, key=lambda p: p.area)

    exterior_coords = list(buffered.exterior.coords)[:-1]
    return np.array(exterior_coords, dtype=np.float32).reshape(-1, 1, 2)


def ensure_clockwise(points: np.ndarray) -> np.ndarray:
    """
    Ensure points are in clockwise order (visually).
    In image coordinates (Y-down), clockwise means positive area.
    """
    points_2d = points.squeeze()
    if len(points_2d.shape) == 1:
        return points

    area = 0.0
    n = len(points_2d)
    for i in range(n):
        j = (i + 1) % n
        area += points_2d[i][0] * points_2d[j][1]
        area -= points_2d[j][0] * points_2d[i][1]

    if area < 0:
        return points[::-1]

    return points


def write_preview(
    original_image: np.ndarray, points: np.ndarray, output_path: Path
) -> None:
    """
    Write a preview image showing the polygon drawn on the mask.
    """
    preview = cv2.cvtColor(original_image, cv2.COLOR_GRAY2BGR)
    pts = points.squeeze().astype(np.int32).reshape(-1, 1, 2)
    cv2.polylines(preview, [pts], True, (0, 255, 0), 2)
    for i, pt in enumerate(pts):
        cv2.circle(preview, tuple(pt[0]), 4, (0, 0, 255), -1)
        cv2.putText(
            preview,
            str(i),
            (pt[0][0] + 5, pt[0][1] - 5),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.4,
            (255, 255, 0),
            1,
        )
    cv2.imwrite(str(output_path), preview)


def write_tres(points: np.ndarray, output_path: Path, uid: str) -> None:
    """
    Write polygon points to a Godot .tres resource file.
    """
    points_2d = points.squeeze()
    if len(points_2d.shape) == 1:
        raise ValueError("Invalid points array")

    coords = []
    for pt in points_2d:
        coords.extend([round(pt[0], 2), round(pt[1], 2)])

    coords_str = ", ".join(str(c) for c in coords)

    script_uid = "uid://dtemboas3bi8y"

    content = f"""[gd_resource type="Resource" script_class="PolygonPointsResource" format=3 uid="uid://{uid}"]

[ext_resource type="Script" uid="{script_uid}" path="res://PolygonPointsResource.gd" id="1_ppr"]

[resource]
script = ExtResource("1_ppr")
points = PackedVector2Array({coords_str})
metadata/_custom_type_script = "{script_uid}"
"""

    output_path.write_text(content)


def main():
    parser = argparse.ArgumentParser(
        description="Convert a black/white mask to a Godot polygon .tres resource"
    )
    parser.add_argument("image", type=Path, help="Input mask image (black/white)")
    parser.add_argument(
        "--padding",
        type=float,
        default=0,
        help="Pixels to expand polygon outward (default: 0)",
    )
    parser.add_argument(
        "--target-points",
        type=int,
        default=10,
        help="Target number of polygon points (default: 10)",
    )
    parser.add_argument(
        "--output",
        type=Path,
        default=None,
        help="Output .tres file path (default: <image_stem>.tres)",
    )
    parser.add_argument(
        "--mode",
        choices=["convex_hull", "largest_only", "multiple"],
        default="convex_hull",
        help="How to handle multiple regions: convex_hull, largest_only, or multiple (default: convex_hull)",
    )
    parser.add_argument(
        "--preview",
        type=Path,
        default=None,
        help="Output preview PNG showing polygon on mask",
    )
    parser.add_argument(
        "--min-area",
        type=int,
        default=150,
        help="Minimum contour area to include (default: 150)",
    )

    args = parser.parse_args()

    if not args.image.exists():
        print(f"Error: Image not found: {args.image}", file=sys.stderr)
        sys.exit(1)

    image = cv2.imread(str(args.image), cv2.IMREAD_GRAYSCALE)
    if image is None:
        print(f"Error: Could not load image: {args.image}", file=sys.stderr)
        sys.exit(1)

    _, binary = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY)
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    if not contours:
        print("Error: No contours found in mask", file=sys.stderr)
        sys.exit(1)

    contours = [c for c in contours if cv2.contourArea(c) >= args.min_area]

    if not contours:
        print("Error: No contours meet minimum area requirement", file=sys.stderr)
        sys.exit(1)

    if args.mode == "multiple":
        contours = sorted(contours, key=cv2.contourArea, reverse=True)

        output_base = args.output if args.output else args.image.with_suffix("")
        output_dir = output_base.parent
        output_stem = output_base.stem

        all_points = []
        for i, contour in enumerate(contours):
            processed = process_contour(contour, args.padding, args.target_points)
            all_points.append(processed)

            output_path = output_dir / f"{output_stem}_{i}.tres"
            uid = generate_godot_uid()
            write_tres(processed, output_path, uid)
            print(f"Created: {output_path}")
            print(f"  Points: {len(processed.squeeze())}")
            print(f"  UID: uid://{uid}")

        if args.preview:
            preview = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
            colors = [
                (0, 255, 0),
                (255, 0, 0),
                (0, 0, 255),
                (255, 255, 0),
                (255, 0, 255),
                (0, 255, 255),
            ]
            for i, points in enumerate(all_points):
                color = colors[i % len(colors)]
                pts = points.squeeze().astype(np.int32).reshape(-1, 1, 2)
                cv2.polylines(preview, [pts], True, color, 2)
                for j, pt in enumerate(pts):
                    cv2.circle(preview, tuple(pt[0]), 4, (0, 0, 255), -1)
                    cv2.putText(
                        preview,
                        f"{i}.{j}",
                        (pt[0][0] + 5, pt[0][1] - 5),
                        cv2.FONT_HERSHEY_SIMPLEX,
                        0.4,
                        (255, 255, 0),
                        1,
                    )
            cv2.imwrite(str(args.preview), preview)
            print(f"Preview: {args.preview}")

        print(f"Total polygons: {len(all_points)}")
    else:
        polygon = contours_to_polygon(contours, args.mode)
        if polygon is None:
            print("Error: No polygon generated", file=sys.stderr)
            sys.exit(1)
        padded = pad_polygon(polygon, args.padding)
        simplified = simplify_to_target_points(padded, args.target_points)
        clockwise = ensure_clockwise(simplified)

        output_path = args.output
        if output_path is None:
            output_path = args.image.with_suffix(".tres")

        uid = generate_godot_uid()
        write_tres(clockwise, output_path, uid)

        if args.preview:
            write_preview(image, clockwise, args.preview)
            print(f"Preview: {args.preview}")

        print(f"Created: {output_path}")
        print(f"Points: {len(clockwise.squeeze())}")
        print(f"UID: uid://{uid}")


if __name__ == "__main__":
    main()
