"""Main conversion pipeline: photo → embroidery pattern.

Orchestrates preprocessing, color quantization, region segmentation,
stitch generation, optimization, and pattern assembly.

v0.2: Added pull compensation, auto fill angle, enhanced preprocessing,
      brand palettes, SVG preview.
"""

import cv2
import numpy as np
from typing import List, Tuple, Optional
import sys
import os

from .preprocess import detect_background, auto_crop_to_subject
from .quantize import quantize_colors
from .segment import extract_regions, scale_regions_to_mm
from .fill import generate_fill_stitch_segments, generate_outline_stitches, _rasterize_region, _RASTER_PPM
from .optimize import optimize_stitch_order, split_long_stitches
from .compensate import apply_pull_compensation, apply_fill_compensation
from .auto_angle import compute_optimal_fill_angle
from .palettes import get_palette
from .formats.common import (
    EmbroideryPattern, ThreadColor, StitchType
)
from .formats.dst import write_dst
from .formats.pes import write_pes
from .formats.jef import write_jef


FORMAT_WRITERS = {
    '.dst': write_dst,
    '.pes': write_pes,
    '.jef': write_jef,
}



def convert_photo_to_embroidery(
    image_path: str,
    output_path: str,
    # Size
    target_size_mm: float = 100.0,
    # Colors
    n_colors: int = 8,
    use_thread_palette: bool = True,
    thread_brand: str = "janome",
    # Stitch parameters
    fill_density: float = 0.136,
    stitch_length: float = 3.0,
    fill_angle: float = 45.0,
    auto_angle: bool = False,
    underlay: bool = True,
    outline: bool = True,
    outline_satin: bool = False,
    # Thread
    thread_width: float = 0.136,
    # Compensation
    pull_compensation: float = 0.0,
    # Processing
    blur_radius: int = 3,
    auto_crop: bool = False,
    skip_background: bool = False,
    strict_colors: bool = False,
    # Output
    verbose: bool = True,
) -> EmbroideryPattern:
    """Convert a photo to an embroidery pattern.

    Args:
        image_path: Path to input image.
        output_path: Path to output embroidery file (.dst, .pes, .jef).
        target_size_mm: Target size for the longest side in mm.
        n_colors: Number of thread colors.
        use_thread_palette: Snap to real thread colors.
        thread_brand: Thread brand for palette ('janome', 'brother', 'madeira').
        fill_density: Fill row spacing in mm.
        stitch_length: Maximum stitch length in mm.
        fill_angle: Base fill angle in degrees.
        auto_angle: Automatically optimize fill angle per region.
        underlay: Whether to add underlay stitches.
        outline: Whether to add outline stitches.
        outline_satin: Use satin stitch for outlines.
        pull_compensation: Pull compensation in mm (0=off).
        blur_radius: Gaussian blur radius for preprocessing.
        auto_crop: Auto-crop to subject.
        skip_background: Skip stitching detected background color.
        strict_colors: Keep exactly n_colors in the output palette.
        verbose: Print progress messages.

    Returns:
        The generated EmbroideryPattern.
    """
    def log(msg):
        if verbose:
            print(msg, file=sys.stderr)

    requested_n_colors = n_colors

    # 1. Load and preprocess image
    log(f"[1/7] Loading image: {image_path}")
    img = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
    if img is None:
        raise FileNotFoundError(f"Cannot load image: {image_path}")

    # Extract alpha mask before converting to 3-channel RGB.
    # Transparent pixels are the background in alpha-channel images.
    # Erode the mask by 1px to discard anti-aliased edge pixels that
    # produce spurious tiny regions with stray fill stitches.
    alpha_mask: Optional[np.ndarray] = None
    if img.ndim == 3 and img.shape[2] == 4:
        alpha_mask = img[:, :, 3] > 127  # True = opaque (foreground)
        kernel = np.ones((3, 3), np.uint8)
        alpha_mask = cv2.erode(
            alpha_mask.astype(np.uint8), kernel, iterations=1
        ).astype(bool)
        img = img[:, :, :3]
    elif img.ndim == 2:
        img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

    img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    h, w = img_rgb.shape[:2]
    log(f"  Original: {w}x{h}px")

    # Auto-crop
    if auto_crop:
        img_rgb, crop_box = auto_crop_to_subject(img_rgb)
        if alpha_mask is not None:
            x, y, cw, ch = crop_box
            alpha_mask = alpha_mask[y:y+ch, x:x+cw]
        h, w = img_rgb.shape[:2]
        log(f"  Auto-cropped to: {w}x{h}px")

    # Detect background
    bg_color = None
    if alpha_mask is not None:
        # Alpha channel defines the background — skip color-based detection.
        skip_background = True
        log("  Transparent background detected via alpha channel")
    elif skip_background:
        bg_color = detect_background(img_rgb, method='corner')
        if bg_color is None:
            bg_color = detect_background(img_rgb, method='edge')
        if bg_color:
            log(f"  Background detected: RGB{bg_color}")
    else:
        pass  # No background skipping unless explicitly requested

    # Calculate dimensions from longest side
    if w >= h:
        target_width_mm = target_size_mm
        target_height_mm = target_size_mm * (h / w)
    else:
        target_height_mm = target_size_mm
        target_width_mm = target_size_mm * (w / h)
    log(f"  Target: {target_width_mm:.0f}x{target_height_mm:.0f}mm")

    # 2. Enhanced preprocessing
    log("[2/7] Preprocessing...")
    max_dim = 800
    if max(h, w) > max_dim:
        scale = max_dim / max(h, w)
        new_size = (int(w * scale), int(h * scale))
        img_rgb = cv2.resize(img_rgb, new_size,
                             interpolation=cv2.INTER_AREA)
        if alpha_mask is not None:
            alpha_mask = cv2.resize(
                alpha_mask.astype(np.uint8), new_size,
                interpolation=cv2.INTER_NEAREST,
            ).astype(bool)
    if blur_radius > 0:
        k = blur_radius * 2 + 1
        img_rgb = cv2.GaussianBlur(img_rgb, (k, k), 0)

    h, w = img_rgb.shape[:2]

    # Median filter to remove anti-aliasing and JPEG artifacts.
    # Median preserves edges while replacing isolated intermediate-color
    # pixels with the dominant neighbor color, preventing K-means from
    # creating spurious clusters for transition pixels.
    img_rgb = cv2.medianBlur(img_rgb, 3)

    # 3. Color quantization
    log(f"[3/7] Quantizing to {n_colors} colors (palette: {thread_brand})...")
    custom_pal = None
    if use_thread_palette:
        custom_pal = get_palette(thread_brand)

    max_palette_colors = len(custom_pal) if use_thread_palette else 256
    if strict_colors and requested_n_colors > max_palette_colors:
        raise ValueError(
            "force-colorsでは指定色数が上限を超えています: "
            f"{requested_n_colors} > {max_palette_colors}"
        )

    label_map, palette = quantize_colors(
        img_rgb, n_colors, use_thread_palette,
        custom_palette=custom_pal,
        merge_close=not strict_colors,
        fg_mask=alpha_mask,
    )
    log(f"  Palette: {len(palette)} colors")

    # Mark transparent pixels as -1 so they are excluded from
    # region segmentation entirely.
    if alpha_mask is not None:
        label_map[~alpha_mask] = -1

    if strict_colors and len(palette) != requested_n_colors:
        raise RuntimeError(
            "force-colors有効時に指定色数を確保できませんでした: "
            f"{len(palette)} / {requested_n_colors}"
        )

    # 4. Region segmentation
    log("[4/7] Segmenting regions...")
    regions = extract_regions(
        label_map,
        palette,
        min_area=1,
        morph_cleanup=True,
        simplify_epsilon_min=0.5,
    )

    # Filter out background regions by color.
    # Only skip the single palette color closest to the detected
    # background — never distance-threshold, which would also
    # discard light foreground colors (e.g., light hair, pale skin).
    if skip_background and bg_color:
        best_idx = None
        best_dist = float('inf')
        for idx, pc in enumerate(palette):
            d = _color_distance(pc, bg_color)
            if d < best_dist:
                best_dist = d
                best_idx = idx
        bg_palette_color = palette[best_idx] if best_idx is not None else None
        before = len(regions)
        if bg_palette_color is not None:
            regions = [r for r in regions
                       if r.color_rgb != bg_palette_color]
        log(f"  Skipped {before - len(regions)} background regions")

    log(f"  Found {len(regions)} regions")

    # Scale to mm
    regions = scale_regions_to_mm(
        regions, (h, w), target_width_mm, target_height_mm
    )

    # 5. Optimize stitch order
    log("[5/7] Optimizing stitch order...")
    regions = optimize_stitch_order(regions)

    # 6. Generate stitches
    log("[6/7] Generating stitches...")
    pattern = EmbroideryPattern(name=os.path.splitext(
        os.path.basename(image_path))[0])

    # Build color map
    color_set = {}
    for region in regions:
        rgb = region.color_rgb
        if rgb not in color_set:
            color_set[rgb] = ThreadColor(
                r=rgb[0], g=rgb[1], b=rgb[2],
                name=f"Color {len(color_set) + 1}",
            )

    if strict_colors:
        # Ensure exact palette count even when segmentation drops tiny regions.
        for rgb in palette:
            if len(color_set) >= requested_n_colors:
                break
            if rgb not in color_set:
                color_set[rgb] = ThreadColor(
                    r=rgb[0], g=rgb[1], b=rgb[2],
                    name=f"Color {len(color_set) + 1}",
                )

        if len(color_set) < requested_n_colors and custom_pal is not None:
            for entry in custom_pal:
                rgb = tuple(entry[:3])
                if rgb in color_set:
                    continue
                color_set[rgb] = ThreadColor(
                    r=rgb[0], g=rgb[1], b=rgb[2],
                    name=f"Color {len(color_set) + 1}",
                )
                if len(color_set) >= requested_n_colors:
                    break

        if len(color_set) != requested_n_colors:
            raise RuntimeError(
                "force-colors有効時に最終色数を一致させられませんでした: "
                f"{len(color_set)} / {requested_n_colors}"
            )

    pattern.colors = list(color_set.values())

    current_color = None
    total_regions = len(regions)
    comp_mm = pull_compensation
    thin_fill_skipped = 0

    # Compute a single fill angle from the largest region overall.
    # All regions use the same angle for uniform appearance.
    global_angle = fill_angle
    if auto_angle:
        largest_region = None
        for region in regions:
            if region.contour_mm is None:
                continue
            if largest_region is None or region.area > largest_region.area:
                largest_region = region
        if largest_region is not None:
            global_angle = compute_optimal_fill_angle(
                largest_region.contour_mm, fill_angle
            )

    for i, region in enumerate(regions):
        if region.contour_mm is None:
            continue

        if i % max(1, total_regions // 10) == 0:
            log(f"  Processing region {i + 1}/{total_regions}...")

        # Color change
        if region.color_rgb != current_color:
            if current_color is not None:
                pattern.add_trim()
                pattern.add_color_change()
            current_color = region.color_rgb

        region_angle = global_angle

        # Use raw (non-simplified) contour for fill to avoid gaps
        # from polygon simplification. Simplified contour is for outline.
        fill_contour = region.contour_raw_mm if region.contour_raw_mm is not None \
            else region.contour_mm
        fill_holes = region.holes_raw_mm if region.holes_raw_mm else \
            (region.holes_mm if region.holes_mm else [])
        contour = region.contour_mm

        if comp_mm > 0:
            contour = apply_pull_compensation(contour, comp_mm)
            fill_contour = apply_pull_compensation(fill_contour, comp_mm)

        skip_fill = False
        effective_spacing = fill_density

        # Skip fill for regions too thin to hold even 2 fill rows.
        # These get outline only, avoiding broken/dashed fill artifacts.
        if fill_contour is not None and len(fill_contour) >= 5:
            pts = fill_contour.astype(np.float32).reshape(-1, 1, 2)
            rect = cv2.minAreaRect(pts)
            min_dim = min(rect[1][0], rect[1][1])
            if min_dim < effective_spacing * 2:
                skip_fill = True

        if skip_fill:
            thin_fill_skipped += 1
            fill_segments = []
        else:
            fill_segments = generate_fill_stitch_segments(
                contour=fill_contour,
                holes=fill_holes,
                fill_angle=region_angle,
                row_spacing=effective_spacing,
                stitch_length=stitch_length,
                underlay=underlay,
            )

            # Apply fill compensation
            if comp_mm > 0 and fill_segments:
                compensated_segments = []
                for segment in fill_segments:
                    compensated = apply_fill_compensation(
                        segment, contour, comp_mm, region_angle
                    )
                    if compensated:
                        compensated_segments.append(compensated)
                fill_segments = compensated_segments

        for fill_segment in fill_segments:
            if not fill_segment:
                continue

            # Split long stitches
            fill_segment = split_long_stitches(fill_segment, max_length=7.0)
            if not fill_segment:
                continue

            # Jump to first stitch of each disconnected span
            if pattern.stitches:
                pattern.add_stitch(
                    fill_segment[0][0], fill_segment[0][1], StitchType.JUMP
                )

            # Add fill stitches
            for x, y in fill_segment:
                pattern.add_stitch(x, y, StitchType.NORMAL)

        # Outline stitches — raw contour をそのまま使用。
        # Chaikin スムージングは凹部をショートカットするため使わない。
        # 凹部でステッチの直線がリージョン外を通る場合は
        # コンターパスに沿った中間ステッチを自動挿入する。
        raw_contour = region.contour_raw_mm if region.contour_raw_mm is not None \
            else region.contour_mm
        if outline and raw_contour is not None:
            outline_contour = raw_contour
            outline_mask, o_ox, o_oy = _rasterize_region(
                fill_contour, fill_holes)
            outline_pts = generate_outline_stitches(
                contour=outline_contour,
                stitch_length=1.5,
                satin_width=1.2,
                satin=outline_satin,
                region_mask=outline_mask,
                mask_ox=o_ox,
                mask_oy=o_oy,
                mask_ppm=_RASTER_PPM,
            )
            if outline_pts:
                outline_pts = split_long_stitches(outline_pts, max_length=7.0)
                pattern.add_stitch(outline_pts[0][0], outline_pts[0][1],
                                   StitchType.JUMP)
                for x, y in outline_pts:
                    pattern.add_stitch(x, y, StitchType.NORMAL)

    # End
    pattern.add_stitch(0, 0, StitchType.END)

    # 7. Write output
    log(f"[7/7] Writing output: {output_path}")

    ext = '.' + output_path.rsplit('.', 1)[-1].lower() if '.' in output_path \
        else '.dst'
    writer = FORMAT_WRITERS.get(ext)
    if writer is None:
        raise ValueError(
            f"Unsupported format: {ext}. "
            f"Supported: {', '.join(FORMAT_WRITERS.keys())}"
        )

    writer(pattern, output_path)

    log("")
    log(pattern.summary())
    log(f"\nOutput: {output_path}")

    return pattern


def generate_preview(pattern: EmbroideryPattern,
                     output_path: str,
                     width: int = 4000,
                     bg_color: Tuple[int, int, int] = (240, 235, 220),
                     thread_width_mm: float = 0.136):
    """Generate a raster preview image of the embroidery pattern.

    Args:
        pattern: Embroidery pattern to preview.
        output_path: Output image path.
        width: Canvas width in pixels.
        bg_color: Background color.
        thread_width_mm: Thread width in mm for line thickness.
            Use 0 for 1px stitch lines.
    """
    bounds = pattern.get_bounds()
    pat_w = bounds[2] - bounds[0]
    pat_h = bounds[3] - bounds[1]

    if pat_w <= 0 or pat_h <= 0:
        return

    # UI要素のスケール係数（width=800 を基準に比例）
    ui = width / 800.0

    margin = int(20 * ui)
    scale = (width - 2 * margin) / pat_w
    height = int(pat_h * scale) + 2 * margin

    import math as _math
    if thread_width_mm > 0:
        # 切り上げでフィル行間の隙間を防ぐ
        thread_px = max(1, _math.ceil(thread_width_mm * scale))
    else:
        thread_px = 1

    img = np.full((height, width, 3), bg_color, dtype=np.uint8)

    # Hoop outline (decorative dashed ellipse, matching SVG dasharray 8,4)
    hoop_cx = width // 2
    hoop_cy = height // 2
    hoop_rx = (width - margin) // 2
    hoop_ry = (height - margin) // 2
    hoop_color = (144, 176, 192)  # #c0b090 in BGR
    hoop_thickness = max(1, round(2 * ui))
    import math
    # Sample enough points for smooth curves
    n_pts = 2000
    angles = [2 * math.pi * i / n_pts for i in range(n_pts + 1)]
    pts_x = [hoop_cx + hoop_rx * math.cos(a) for a in angles]
    pts_y = [hoop_cy + hoop_ry * math.sin(a) for a in angles]
    # Walk along the ellipse accumulating arc length
    dash_on, gap_off = 8.0 * ui, 4.0 * ui
    arc = 0.0
    drawing = True
    seg_start = 0
    for i in range(1, len(angles)):
        dx = pts_x[i] - pts_x[i - 1]
        dy = pts_y[i] - pts_y[i - 1]
        arc += math.sqrt(dx * dx + dy * dy)
        threshold = dash_on if drawing else gap_off
        if arc >= threshold:
            if drawing:
                for j in range(seg_start, i):
                    cv2.line(img,
                             (int(pts_x[j]), int(pts_y[j])),
                             (int(pts_x[j + 1]), int(pts_y[j + 1])),
                             hoop_color, hoop_thickness, cv2.LINE_AA)
            drawing = not drawing
            seg_start = i
            arc = 0.0

    current_color_idx = 0
    colors = pattern.colors
    prev_x, prev_y = None, None

    for stitch in pattern.stitches:
        if stitch.stitch_type == StitchType.COLOR_CHANGE:
            current_color_idx += 1
            prev_x, prev_y = None, None
            continue

        if stitch.stitch_type in (StitchType.JUMP, StitchType.TRIM):
            prev_x, prev_y = None, None
            continue

        if stitch.stitch_type == StitchType.END:
            break

        px = int((stitch.x - bounds[0]) * scale) + margin
        py = int((stitch.y - bounds[1]) * scale) + margin

        if prev_x is not None and prev_y is not None:
            color_idx = min(current_color_idx, len(colors) - 1)
            if colors:
                c = colors[color_idx]
                bgr = (int(c.b), int(c.g), int(c.r))
            else:
                bgr = (0, 0, 0)

            cv2.line(img, (prev_x, prev_y), (px, py), bgr, thread_px,
                     cv2.LINE_AA)

        prev_x, prev_y = px, py

    # Info text and color legend (matching SVG preview)
    font_scale = 0.4 * ui
    font_thickness = max(1, round(ui))
    info_y = height - int(8 * ui)
    w_mm = bounds[2] - bounds[0]
    h_mm = bounds[3] - bounds[1]
    info = (f"{w_mm:.0f}x{h_mm:.0f}mm | "
            f"{pattern.stitch_count()} stitches | "
            f"{len(colors)} colors")

    cv2.putText(img, info, (margin, info_y),
                cv2.FONT_HERSHEY_SIMPLEX, font_scale, (136, 136, 136),
                font_thickness, cv2.LINE_AA)

    # Color swatches
    swatch_size = int(14 * ui)
    swatch_gap = int(4 * ui)
    swatch_border = max(1, round(ui))
    legend_x = width - margin - len(colors) * (swatch_size + swatch_gap)
    legend_y = height - swatch_size - int(6 * ui)
    for i, color in enumerate(colors):
        cx = legend_x + i * (swatch_size + swatch_gap)
        bgr = (int(color.b), int(color.g), int(color.r))
        cv2.rectangle(img, (cx, legend_y),
                      (cx + swatch_size, legend_y + swatch_size),
                      bgr, cv2.FILLED)
        cv2.rectangle(img, (cx, legend_y),
                      (cx + swatch_size, legend_y + swatch_size),
                      (102, 102, 102), swatch_border)

    cv2.imwrite(output_path, img)



def _color_distance(c1: Tuple[int, int, int],
                    c2: Tuple[int, int, int]) -> float:
    """Euclidean distance between two RGB colors."""
    return sum((a - b) ** 2 for a, b in zip(c1, c2)) ** 0.5
