"""
Snapping system for pyPhotoAlbum
Provides grid snapping, edge snapping, and custom guide snapping
"""

import math
from typing import Any, Dict, List, Tuple, Optional
from dataclasses import dataclass


@dataclass
class Guide:
    """Represents a snapping guide (vertical or horizontal line)"""

    position: float  # Position in mm
    orientation: str  # 'vertical' or 'horizontal'

    def serialize(self) -> dict:
        """Serialize guide to dictionary"""
        return {"position": self.position, "orientation": self.orientation}

    @staticmethod
    def deserialize(data: dict) -> "Guide":
        """Deserialize guide from dictionary"""
        return Guide(position=data.get("position", 0), orientation=data.get("orientation", "vertical"))


@dataclass
class SnapResizeParams:
    """Parameters for snap resize operations"""

    position: Tuple[float, float]
    size: Tuple[float, float]
    dx: float
    dy: float
    resize_handle: str
    page_size: Tuple[float, float]
    dpi: int = 300
    project: Optional[Any] = None


class SnappingSystem:
    """Manages snapping behavior for layout elements"""

    def __init__(self, snap_threshold_mm: float = 5.0):
        """
        Initialize snapping system

        Args:
            snap_threshold_mm: Distance in mm within which snapping occurs
        """
        self.snap_threshold_mm = snap_threshold_mm
        self.grid_size_mm = 10.0  # Grid spacing in mm
        self.snap_to_grid = False
        self.snap_to_edges = True
        self.snap_to_guides = True
        self.guides: List[Guide] = []

    def add_guide(self, position: float, orientation: str):
        """Add a new guide"""
        guide = Guide(position=position, orientation=orientation)
        self.guides.append(guide)
        return guide

    def remove_guide(self, guide: Guide):
        """Remove a guide"""
        if guide in self.guides:
            self.guides.remove(guide)

    def clear_guides(self):
        """Remove all guides"""
        self.guides.clear()

    def snap_position(
        self,
        position: Tuple[float, float],
        size: Tuple[float, float],
        page_size: Tuple[float, float],
        dpi: int = 300,
        project=None,
    ) -> Tuple[float, float]:
        """
        Apply snapping to a position using combined distance threshold

        Args:
            position: Current position (x, y) in pixels
            size: Element size (width, height) in pixels
            page_size: Page size (width, height) in mm
            dpi: DPI for conversion
            project: Optional project for global snapping settings

        Returns:
            Snapped position (x, y) in pixels
        """
        x, y = position
        width, height = size
        page_width_mm, page_height_mm = page_size

        # Use project settings if available, otherwise use local settings
        if project:
            snap_to_grid = project.snap_to_grid
            snap_to_edges = project.snap_to_edges
            snap_to_guides = project.snap_to_guides
            grid_size_mm = project.grid_size_mm
            snap_threshold_mm = project.snap_threshold_mm
        else:
            snap_to_grid = self.snap_to_grid
            snap_to_edges = self.snap_to_edges
            snap_to_guides = self.snap_to_guides
            grid_size_mm = self.grid_size_mm
            snap_threshold_mm = self.snap_threshold_mm

        # Convert threshold from mm to pixels
        snap_threshold_px = snap_threshold_mm * dpi / 25.4

        # Collect all potential snap points for both edges of the element
        snap_points = []

        # 1. Page edge snap points
        if snap_to_edges:
            page_width_px = page_width_mm * dpi / 25.4
            page_height_px = page_height_mm * dpi / 25.4

            # Corners where element's top-left can snap
            snap_points.extend(
                [
                    (0, 0),  # Top-left corner
                    (page_width_px - width, 0),  # Top-right corner
                    (0, page_height_px - height),  # Bottom-left corner
                    (page_width_px - width, page_height_px - height),  # Bottom-right corner
                ]
            )

            # Edge positions (element aligned to edge on one axis)
            snap_points.extend(
                [
                    (0, y),  # Left edge
                    (page_width_px - width, y),  # Right edge
                    (x, 0),  # Top edge
                    (x, page_height_px - height),  # Bottom edge
                ]
            )

        # 2. Grid snap points
        if snap_to_grid:
            grid_size_px = grid_size_mm * dpi / 25.4
            page_width_px = page_width_mm * dpi / 25.4
            page_height_px = page_height_mm * dpi / 25.4

            # Calculate grid intersection points within range
            x_start = max(0, int((x - snap_threshold_px) / grid_size_px)) * grid_size_px
            x_end = min(page_width_px, int((x + snap_threshold_px) / grid_size_px + 1) * grid_size_px)
            y_start = max(0, int((y - snap_threshold_px) / grid_size_px)) * grid_size_px
            y_end = min(page_height_px, int((y + snap_threshold_px) / grid_size_px + 1) * grid_size_px)

            grid_x = x_start
            while grid_x <= x_end:
                grid_y = y_start
                while grid_y <= y_end:
                    snap_points.append((grid_x, grid_y))
                    # Also snap element's far edge to grid
                    if grid_x >= width:
                        snap_points.append((grid_x - width, grid_y))
                    if grid_y >= height:
                        snap_points.append((grid_x, grid_y - height))
                    grid_y += grid_size_px
                grid_x += grid_size_px

        # 3. Guide snap points
        if snap_to_guides:
            vertical_guides = [g.position * dpi / 25.4 for g in self.guides if g.orientation == "vertical"]
            horizontal_guides = [g.position * dpi / 25.4 for g in self.guides if g.orientation == "horizontal"]

            # Guide intersections (when both vertical and horizontal guides exist)
            for vg in vertical_guides:
                for hg in horizontal_guides:
                    snap_points.append((vg, hg))
                    # Also snap element's far edge to intersections
                    snap_points.append((vg - width, hg))
                    snap_points.append((vg, hg - height))
                    snap_points.append((vg - width, hg - height))

        # Find the nearest snap point using Euclidean distance
        best_snap_point = None
        best_distance = snap_threshold_px

        for snap_x, snap_y in snap_points:
            distance = math.sqrt((x - snap_x) ** 2 + (y - snap_y) ** 2)
            if distance < best_distance:
                best_snap_point = (snap_x, snap_y)
                best_distance = distance

        # Return snapped position or original position
        if best_snap_point:
            return best_snap_point
        else:
            return (x, y)

    def snap_resize(self, params: SnapResizeParams) -> Tuple[Tuple[float, float], Tuple[float, float]]:
        """
        Apply snapping during resize operations

        Args:
            params: SnapResizeParams containing all resize parameters

        Returns:
            Tuple of (snapped_position, snapped_size) in pixels
        """
        x, y = params.position
        width, height = params.size
        page_width_mm, page_height_mm = params.page_size

        # Use project settings if available, otherwise use local settings
        if params.project:
            snap_threshold_mm = params.project.snap_threshold_mm
        else:
            snap_threshold_mm = self.snap_threshold_mm

        # Convert threshold from mm to pixels
        snap_threshold_px = snap_threshold_mm * params.dpi / 25.4

        # Calculate new position and size based on resize handle
        new_x, new_y = x, y
        new_width, new_height = width, height

        # Apply resize based on handle
        if params.resize_handle in ["nw", "n", "ne"]:
            # Top edge moving
            new_y = y + params.dy
            new_height = height - params.dy

        if params.resize_handle in ["sw", "s", "se"]:
            # Bottom edge moving
            new_height = height + params.dy

        if params.resize_handle in ["nw", "w", "sw"]:
            # Left edge moving
            new_x = x + params.dx
            new_width = width - params.dx

        if params.resize_handle in ["ne", "e", "se"]:
            # Right edge moving
            new_width = width + params.dx

        # Now apply snapping to the edges that are being moved
        # Use _snap_edge_to_targets consistently for all edges

        # Snap left edge (for nw, w, sw handles)
        if params.resize_handle in ["nw", "w", "sw"]:
            # Try to snap the left edge
            snapped_left = self._snap_edge_to_targets(
                new_x, page_width_mm, params.dpi, snap_threshold_px, "vertical", params.project
            )
            if snapped_left is not None:
                # Adjust width to compensate for position change
                width_adjustment = new_x - snapped_left
                new_x = snapped_left
                new_width += width_adjustment

        # Snap right edge (for ne, e, se handles)
        if params.resize_handle in ["ne", "e", "se"]:
            # Calculate right edge position
            right_edge = new_x + new_width
            # Try to snap the right edge
            snapped_right = self._snap_edge_to_targets(
                right_edge, page_width_mm, params.dpi, snap_threshold_px, "vertical", params.project
            )
            if snapped_right is not None:
                new_width = snapped_right - new_x

        # Snap top edge (for nw, n, ne handles)
        if params.resize_handle in ["nw", "n", "ne"]:
            # Try to snap the top edge
            snapped_top = self._snap_edge_to_targets(
                new_y, page_height_mm, params.dpi, snap_threshold_px, "horizontal", params.project
            )
            if snapped_top is not None:
                # Adjust height to compensate for position change
                height_adjustment = new_y - snapped_top
                new_y = snapped_top
                new_height += height_adjustment

        # Snap bottom edge (for sw, s, se handles)
        if params.resize_handle in ["sw", "s", "se"]:
            # Calculate bottom edge position
            bottom_edge = new_y + new_height
            # Try to snap the bottom edge
            snapped_bottom = self._snap_edge_to_targets(
                bottom_edge, page_height_mm, params.dpi, snap_threshold_px, "horizontal", params.project
            )
            if snapped_bottom is not None:
                new_height = snapped_bottom - new_y

        # Ensure minimum size
        min_size = 10  # Minimum 10 pixels
        new_width = max(new_width, min_size)
        new_height = max(new_height, min_size)

        return ((new_x, new_y), (new_width, new_height))

    def _snap_edge_to_targets(
        self,
        edge_position: float,
        page_size_mm: float,
        dpi: int,
        snap_threshold_px: float,
        orientation: str,
        project=None,
    ) -> Optional[float]:
        """
        Snap an edge position to available targets (grid, edges, guides)

        Args:
            edge_position: Current edge position in pixels
            page_size_mm: Page size along axis in mm
            dpi: DPI for conversion
            snap_threshold_px: Snap threshold in pixels
            orientation: 'vertical' for x-axis, 'horizontal' for y-axis
            project: Optional project for global snapping settings

        Returns:
            Snapped edge position in pixels, or None if no snap
        """
        # Use project settings if available, otherwise use local settings
        if project:
            snap_to_grid = project.snap_to_grid
            snap_to_edges = project.snap_to_edges
            snap_to_guides = project.snap_to_guides
            grid_size_mm = project.grid_size_mm
        else:
            snap_to_grid = self.snap_to_grid
            snap_to_edges = self.snap_to_edges
            snap_to_guides = self.snap_to_guides
            grid_size_mm = self.grid_size_mm

        snap_candidates: List[Tuple[float, float]] = []

        # 1. Page edge snapping
        if snap_to_edges:
            # Snap to start edge (0)
            snap_candidates.append((0.0, abs(edge_position - 0)))

            # Snap to end edge
            page_size_px = page_size_mm * dpi / 25.4
            snap_candidates.append((page_size_px, abs(edge_position - page_size_px)))

        # 2. Grid snapping
        if snap_to_grid:
            grid_size_px = grid_size_mm * dpi / 25.4

            # Snap to nearest grid line
            nearest_grid = round(edge_position / grid_size_px) * grid_size_px
            snap_candidates.append((nearest_grid, abs(edge_position - nearest_grid)))

        # 3. Guide snapping
        if snap_to_guides:
            for guide in self.guides:
                if guide.orientation == orientation:
                    guide_pos_px = guide.position * dpi / 25.4
                    snap_candidates.append((guide_pos_px, abs(edge_position - guide_pos_px)))

        # Find the best snap candidate within threshold
        best_snap = None
        best_distance = snap_threshold_px

        for snap_pos, distance in snap_candidates:
            if distance < best_distance:
                best_snap = snap_pos
                best_distance = distance

        return best_snap

    def get_snap_lines(self, page_size: Tuple[float, float], dpi: int = 300) -> dict:
        """
        Get all snap lines for visualization

        Args:
            page_size: Page size (width, height) in mm
            dpi: DPI for conversion

        Returns:
            Dictionary with 'grid', 'edges', and 'guides' lists
        """
        page_width_mm, page_height_mm = page_size
        page_width_px = page_width_mm * dpi / 25.4
        page_height_px = page_height_mm * dpi / 25.4

        result: Dict[str, List[Tuple[str, float]]] = {"grid": [], "edges": [], "guides": []}

        # Grid lines
        if self.snap_to_grid:
            grid_size_px = self.grid_size_mm * dpi / 25.4

            # Vertical grid lines
            x: float = 0
            while x <= page_width_px:
                result["grid"].append(("vertical", x))
                x += grid_size_px

            # Horizontal grid lines
            y: float = 0
            while y <= page_height_px:
                result["grid"].append(("horizontal", y))
                y += grid_size_px

        # Edge lines
        if self.snap_to_edges:
            result["edges"].extend(
                [("vertical", 0), ("vertical", page_width_px), ("horizontal", 0), ("horizontal", page_height_px)]
            )

        # Guide lines
        if self.snap_to_guides:
            for guide in self.guides:
                guide_pos_px = guide.position * dpi / 25.4
                result["guides"].append((guide.orientation, guide_pos_px))

        return result

    def serialize(self) -> dict:
        """Serialize snapping system to dictionary"""
        return {
            "snap_threshold_mm": self.snap_threshold_mm,
            "grid_size_mm": self.grid_size_mm,
            "snap_to_grid": self.snap_to_grid,
            "snap_to_edges": self.snap_to_edges,
            "snap_to_guides": self.snap_to_guides,
            "guides": [guide.serialize() for guide in self.guides],
        }

    def deserialize(self, data: dict):
        """Deserialize from dictionary"""
        self.snap_threshold_mm = data.get("snap_threshold_mm", 5.0)
        self.grid_size_mm = data.get("grid_size_mm", 10.0)
        self.snap_to_grid = data.get("snap_to_grid", False)
        self.snap_to_edges = data.get("snap_to_edges", True)
        self.snap_to_guides = data.get("snap_to_guides", True)

        self.guides = []
        for guide_data in data.get("guides", []):
            self.guides.append(Guide.deserialize(guide_data))
