#!/usr/bin/env python3
"""
Split a PNG file into multiple SVG files based on brightness/color clusters.
Useful for multi-color 3D printing when the source is a rasterized logo.
"""

import argparse
import numpy as np
from PIL import Image
from pathlib import Path
import cv2
import xml.etree.ElementTree as ET


def extract_color_regions(image_path: Path, dark_threshold: int = 30, mid_min: int = 108, mid_max: int = 118):
    """
    Extract different color regions from PNG based on brightness.
    Uses tighter thresholds to avoid antialiasing artifacts.
    Returns dict of {color_name: binary_mask}
    """
    img = Image.open(image_path).convert('RGBA')
    img_array = np.array(img)

    # Get alpha mask (non-transparent pixels)
    alpha = img_array[..., 3] > 128

    # Calculate brightness for each pixel
    rgb = img_array[..., :3]
    brightness = np.mean(rgb, axis=-1)

    # Create masks for different brightness levels
    masks = {}

    # Dark pixels (black text) - very tight threshold to avoid antialiasing
    dark_mask = alpha & (brightness < dark_threshold)
    if np.sum(dark_mask) > 100:
        masks['black'] = dark_mask

    # Mid-range pixels (grey icon) - tight range to get only the core grey
    mid_mask = alpha & (brightness >= mid_min) & (brightness <= mid_max)
    if np.sum(mid_mask) > 100:
        masks['grey'] = mid_mask

    return masks


def compute_contour_area(contour):
    """Compute the signed area of a contour (positive = clockwise, negative = counter-clockwise)"""
    if len(contour) < 3:
        return 0
    # Shoelace formula
    x = contour[:, 0]
    y = contour[:, 1]
    return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))


def mask_to_svg_paths_with_holes(mask: np.ndarray, simplify_epsilon: float = 1.0):
    """
    Convert a binary mask to SVG path data with proper hole handling.
    Returns list of path d attributes with fill-rule evenodd.
    """
    # Convert to uint8 for OpenCV
    mask_uint8 = (mask * 255).astype(np.uint8)

    # Find contours with hierarchy (to detect holes)
    contours, hierarchy = cv2.findContours(mask_uint8, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)

    if hierarchy is None or len(contours) == 0:
        return []

    # Simplify contours
    simplified_contours = []
    for contour in contours:
        if simplify_epsilon > 0:
            simplified = cv2.approxPolyDP(contour, simplify_epsilon, True)
        else:
            simplified = contour
        simplified_contours.append(simplified)

    # Group contours by parent/child relationship
    # hierarchy format: [Next, Previous, First_Child, Parent]
    hierarchy = hierarchy[0]

    # Find all top-level contours (no parent)
    top_level_indices = [i for i in range(len(hierarchy)) if hierarchy[i][3] == -1]

    paths = []

    for top_idx in top_level_indices:
        contour = simplified_contours[top_idx]

        if len(contour) < 3:
            continue

        # Start with outer contour
        path_data = contour_to_svg_path(contour)

        # Find all children (holes) of this contour
        child_idx = hierarchy[top_idx][2]
        while child_idx != -1:
            child_contour = simplified_contours[child_idx]
            if len(child_contour) >= 3:
                # Add hole to the same path (evenodd fill-rule will handle it)
                path_data += " " + contour_to_svg_path(child_contour)

            # Move to next sibling
            child_idx = hierarchy[child_idx][0]

        paths.append(path_data)

    return paths


def contour_to_svg_path(contour):
    """Convert OpenCV contour to SVG path data"""
    # OpenCV contours are shape (N, 1, 2)
    points = contour.reshape(-1, 2)

    if len(points) < 2:
        return ""

    path_data = f"M {points[0, 0]},{points[0, 1]}"
    for point in points[1:]:
        path_data += f" L {point[0]},{point[1]}"
    path_data += " Z"

    return path_data


def get_mask_bounds(mask: np.ndarray):
    """Get the bounding box of a binary mask"""
    rows = np.any(mask, axis=1)
    cols = np.any(mask, axis=0)

    if not np.any(rows) or not np.any(cols):
        return None

    y_min, y_max = np.where(rows)[0][[0, -1]]
    x_min, x_max = np.where(cols)[0][[0, -1]]

    return (x_min, y_min, x_max + 1, y_max + 1)


def create_svg_from_mask(mask: np.ndarray, output_path: Path, color: str, image_size: tuple, crop_to_bounds: bool = True):
    """Create SVG file from binary mask with proper hole handling and optional cropping"""
    width, height = image_size

    # Get bounding box of the mask
    if crop_to_bounds:
        bounds = get_mask_bounds(mask)
        if bounds is None:
            print(f"WARNING: No content found for {color}")
            return

        x_min, y_min, x_max, y_max = bounds
        cropped_width = x_max - x_min
        cropped_height = y_max - y_min

        # Crop the mask
        cropped_mask = mask[y_min:y_max, x_min:x_max]
    else:
        x_min, y_min = 0, 0
        cropped_width, cropped_height = width, height
        cropped_mask = mask

    # Create SVG root with cropped dimensions
    svg = ET.Element('svg', {
        'xmlns': 'http://www.w3.org/2000/svg',
        'width': str(cropped_width),
        'height': str(cropped_height),
        'viewBox': f'0 0 {cropped_width} {cropped_height}'
    })

    # Convert mask to paths with hole detection
    paths = mask_to_svg_paths_with_holes(cropped_mask, simplify_epsilon=1.0)

    # Color mapping
    color_hex = {
        'black': '#000000',
        'grey': '#808080',
        'gray': '#808080',
        'white': '#FFFFFF',
    }.get(color, color)

    # Add paths to SVG (no translation needed since we cropped the mask)
    for path_data in paths:
        ET.SubElement(svg, 'path', {
            'd': path_data,
            'fill': color_hex,
            'fill-rule': 'evenodd'
        })

    # Write SVG
    tree = ET.ElementTree(svg)
    ET.indent(tree, space='  ')
    tree.write(output_path, encoding='utf-8', xml_declaration=True)

    if crop_to_bounds:
        print(f"Created {output_path} with color {color} ({len(paths)} shape groups, cropped to {cropped_width}x{cropped_height})")
    else:
        print(f"Created {output_path} with color {color} ({len(paths)} shape groups)")


def main():
    parser = argparse.ArgumentParser(
        description='Split PNG into separate SVG files by brightness/color'
    )
    parser.add_argument('input', help='Input PNG file')
    parser.add_argument('--output-dir', '-o', help='Output directory (default: same as input)')
    parser.add_argument('--prefix', '-p', help='Prefix for output files (default: input filename)')
    parser.add_argument('--dark-threshold', type=int, default=30,
                       help='Brightness threshold for dark/black elements (default: 30)')
    parser.add_argument('--mid-min', type=int, default=108,
                       help='Minimum brightness for mid/grey elements (default: 108)')
    parser.add_argument('--mid-max', type=int, default=118,
                       help='Maximum brightness for mid/grey elements (default: 118)')
    parser.add_argument('--no-crop', action='store_true',
                       help='Do not crop to bounding box (default: crop enabled)')
    args = parser.parse_args()

    input_path = Path(args.input)

    if not input_path.exists():
        print(f"ERROR: File not found: {input_path}")
        return 1

    # Setup output directory
    if args.output_dir:
        output_dir = Path(args.output_dir)
        output_dir.mkdir(parents=True, exist_ok=True)
    else:
        output_dir = input_path.parent

    # Setup prefix
    prefix = args.prefix if args.prefix else input_path.stem

    # Load image to get size
    img = Image.open(input_path)
    image_size = img.size

    # Extract color regions
    print(f"Analyzing colors in {input_path}...")
    masks = extract_color_regions(input_path, args.dark_threshold, args.mid_min, args.mid_max)

    if not masks:
        print("No color regions found!")
        return 1

    print(f"\nFound {len(masks)} color regions:")
    for color, mask in masks.items():
        pixel_count = np.sum(mask)
        print(f"  {color}: {pixel_count:,} pixels")

    # Create SVG for each color
    print(f"\nCreating separate SVG files...")
    crop_enabled = not args.no_crop
    for color, mask in masks.items():
        output_path = output_dir / f"{prefix}_{color}.svg"
        create_svg_from_mask(mask, output_path, color, image_size, crop_to_bounds=crop_enabled)

    print(f"\nDone! Created {len(masks)} SVG files in {output_dir}")


if __name__ == '__main__':
    main()
