"""
Alignment and distribution manager for pyPhotoAlbum
"""

from typing import List, Tuple
from pyPhotoAlbum.models import BaseLayoutElement


class ElementMaximizer:
    """
    Handles element maximization using a crystal growth algorithm.
    Breaks down the complex maximize_pattern logic into atomic, testable methods.
    """

    def __init__(self, elements: List[BaseLayoutElement], page_size: Tuple[float, float], min_gap: float):
        """
        Initialize the maximizer with elements and constraints.

        Args:
            elements: List of elements to maximize
            page_size: (width, height) of the page in mm
            min_gap: Minimum gap to maintain between elements and borders (in mm)
        """
        self.elements = elements
        self.page_width, self.page_height = page_size
        self.min_gap = min_gap
        self.changes: List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]] = []
        self._record_initial_states()

    def _record_initial_states(self) -> None:
        """Record initial positions and sizes for undo functionality."""
        for elem in self.elements:
            self.changes.append((elem, elem.position, elem.size))

    def check_collision(self, elem_idx: int, new_size: Tuple[float, float]) -> bool:
        """
        Check if element with new_size would collide with boundaries or other elements.

        Args:
            elem_idx: Index of the element to check
            new_size: Proposed new size (width, height)

        Returns:
            True if collision detected, False otherwise
        """
        elem = self.elements[elem_idx]
        x, y = elem.position
        w, h = new_size

        # Check page boundaries
        if x < self.min_gap or y < self.min_gap:
            return True
        if x + w > self.page_width - self.min_gap:
            return True
        if y + h > self.page_height - self.min_gap:
            return True

        # Check collision with other elements
        for i, other in enumerate(self.elements):
            if i == elem_idx:
                continue

            other_x, other_y = other.position
            other_w, other_h = other.size

            # Calculate distances between rectangles
            horizontal_gap = max(
                other_x - (x + w), x - (other_x + other_w)  # Other is to the right  # Other is to the left
            )

            vertical_gap = max(other_y - (y + h), y - (other_y + other_h))  # Other is below  # Other is above

            # If rectangles overlap or are too close in both dimensions
            if horizontal_gap < self.min_gap and vertical_gap < self.min_gap:
                return True

        return False

    def find_max_scale(
        self,
        elem_idx: int,
        current_scale: float,
        max_search_scale: float = 3.0,
        tolerance: float = 0.001,
        max_iterations: int = 20,
    ) -> float:
        """
        Use binary search to find the maximum scale factor for an element.

        Args:
            elem_idx: Index of the element
            current_scale: Current scale factor
            max_search_scale: Maximum scale to search up to (relative to current_scale)
            tolerance: Convergence tolerance for binary search
            max_iterations: Maximum binary search iterations

        Returns:
            Maximum scale factor that doesn't cause collision
        """
        old_size = self.changes[elem_idx][2]

        # Binary search for maximum scale
        low, high = current_scale, current_scale * max_search_scale
        best_scale = current_scale

        for _ in range(max_iterations):
            mid = (low + high) / 2.0
            test_size = (old_size[0] * mid, old_size[1] * mid)

            if self.check_collision(elem_idx, test_size):
                high = mid
            else:
                best_scale = mid
                low = mid

            if high - low < tolerance:
                break

        return best_scale

    def grow_iteration(self, scales: List[float], growth_rate: float) -> bool:
        """
        Perform one iteration of the growth algorithm.

        Args:
            scales: Current scale factors for each element
            growth_rate: Percentage to grow each iteration (0.05 = 5%)

        Returns:
            True if any element grew, False otherwise
        """
        any_growth = False

        for i, elem in enumerate(self.elements):
            old_size = self.changes[i][2]

            # Try to grow this element
            new_scale = scales[i] * (1.0 + growth_rate)
            new_size = (old_size[0] * new_scale, old_size[1] * new_scale)

            if not self.check_collision(i, new_size):
                scales[i] = new_scale
                elem.size = new_size
                any_growth = True
            else:
                # Can't grow uniformly, try to find maximum possible scale
                max_scale = self.find_max_scale(i, scales[i])
                if max_scale > scales[i]:
                    scales[i] = max_scale
                    elem.size = (old_size[0] * max_scale, old_size[1] * max_scale)
                    any_growth = True

        return any_growth

    def check_element_collision(self, elem: BaseLayoutElement, new_pos: Tuple[float, float]) -> bool:
        """
        Check if moving an element to new_pos would cause collision with other elements.

        Args:
            elem: The element to check
            new_pos: Proposed new position (x, y)

        Returns:
            True if collision detected, False otherwise
        """
        x, y = new_pos
        w, h = elem.size

        for other in self.elements:
            if other is elem:
                continue
            ox, oy = other.position
            ow, oh = other.size

            # Check if rectangles overlap (with min_gap consideration)
            if (
                abs((x + w / 2) - (ox + ow / 2)) < (w + ow) / 2 + self.min_gap
                and abs((y + h / 2) - (oy + oh / 2)) < (h + oh) / 2 + self.min_gap
            ):
                return True

        return False

    def center_element_horizontally(self, elem: BaseLayoutElement) -> None:
        """
        Micro-adjust element position to center horizontally in available space.

        Args:
            elem: Element to center
        """
        x, y = elem.position
        w, h = elem.size

        # Calculate available space on each side
        space_left = x - self.min_gap
        space_right = (self.page_width - self.min_gap) - (x + w)

        if space_left >= 0 and space_right >= 0:
            adjust_x = (space_right - space_left) / 4.0  # Gentle centering
            new_x = max(self.min_gap, min(self.page_width - w - self.min_gap, x + adjust_x))

            # Verify this doesn't cause collision
            old_pos = elem.position
            new_pos = (new_x, y)

            if not self.check_element_collision(elem, new_pos):
                elem.position = new_pos

    def center_element_vertically(self, elem: BaseLayoutElement) -> None:
        """
        Micro-adjust element position to center vertically in available space.

        Args:
            elem: Element to center
        """
        x, y = elem.position
        w, h = elem.size

        # Calculate available space on each side
        space_top = y - self.min_gap
        space_bottom = (self.page_height - self.min_gap) - (y + h)

        if space_top >= 0 and space_bottom >= 0:
            adjust_y = (space_bottom - space_top) / 4.0
            new_y = max(self.min_gap, min(self.page_height - h - self.min_gap, y + adjust_y))

            # Verify this doesn't cause collision
            old_pos = elem.position
            new_pos = (x, new_y)

            if not self.check_element_collision(elem, new_pos):
                elem.position = new_pos

    def center_elements(self) -> None:
        """Center all elements slightly within their constrained space."""
        for elem in self.elements:
            self.center_element_horizontally(elem)
            self.center_element_vertically(elem)

    def maximize(
        self, max_iterations: int = 100, growth_rate: float = 0.05
    ) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
        """
        Execute the maximization algorithm.

        Args:
            max_iterations: Maximum number of growth iterations
            growth_rate: Percentage to grow each iteration (0.05 = 5%)

        Returns:
            List of (element, old_position, old_size) tuples for undo
        """
        scales = [1.0] * len(self.elements)

        # Growth algorithm - iterative expansion
        for _ in range(max_iterations):
            if not self.grow_iteration(scales, growth_rate):
                break

        # Center elements slightly within their constrained space
        self.center_elements()

        return self.changes


class AlignmentManager:
    """Manages alignment and distribution operations on multiple elements"""

    @staticmethod
    def get_bounds(elements: List[BaseLayoutElement]) -> Tuple[float, float, float, float]:
        """
        Get the bounding box of multiple elements.

        Returns:
            (min_x, min_y, max_x, max_y)
        """
        if not elements:
            return (0, 0, 0, 0)

        min_x = min(elem.position[0] for elem in elements)
        min_y = min(elem.position[1] for elem in elements)
        max_x = max(elem.position[0] + elem.size[0] for elem in elements)
        max_y = max(elem.position[1] + elem.size[1] for elem in elements)

        return (min_x, min_y, max_x, max_y)

    @staticmethod
    def align_left(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
        """
        Align all elements to the leftmost element.

        Returns:
            List of (element, old_position) tuples for undo
        """
        if len(elements) < 2:
            return []

        min_x = min(elem.position[0] for elem in elements)
        changes = []

        for elem in elements:
            old_pos = elem.position
            elem.position = (min_x, elem.position[1])
            changes.append((elem, old_pos))

        return changes

    @staticmethod
    def align_right(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
        """
        Align all elements to the rightmost element.

        Returns:
            List of (element, old_position) tuples for undo
        """
        if len(elements) < 2:
            return []

        max_right = max(elem.position[0] + elem.size[0] for elem in elements)
        changes = []

        for elem in elements:
            old_pos = elem.position
            new_x = max_right - elem.size[0]
            elem.position = (new_x, elem.position[1])
            changes.append((elem, old_pos))

        return changes

    @staticmethod
    def align_top(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
        """
        Align all elements to the topmost element.

        Returns:
            List of (element, old_position) tuples for undo
        """
        if len(elements) < 2:
            return []

        min_y = min(elem.position[1] for elem in elements)
        changes = []

        for elem in elements:
            old_pos = elem.position
            elem.position = (elem.position[0], min_y)
            changes.append((elem, old_pos))

        return changes

    @staticmethod
    def align_bottom(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
        """
        Align all elements to the bottommost element.

        Returns:
            List of (element, old_position) tuples for undo
        """
        if len(elements) < 2:
            return []

        max_bottom = max(elem.position[1] + elem.size[1] for elem in elements)
        changes = []

        for elem in elements:
            old_pos = elem.position
            new_y = max_bottom - elem.size[1]
            elem.position = (elem.position[0], new_y)
            changes.append((elem, old_pos))

        return changes

    @staticmethod
    def align_horizontal_center(
        elements: List[BaseLayoutElement],
    ) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
        """
        Align all elements to horizontal center.

        Returns:
            List of (element, old_position) tuples for undo
        """
        if len(elements) < 2:
            return []

        # Calculate average center
        centers = [elem.position[0] + elem.size[0] / 2 for elem in elements]
        avg_center = sum(centers) / len(centers)

        changes = []
        for elem in elements:
            old_pos = elem.position
            new_x = avg_center - elem.size[0] / 2
            elem.position = (new_x, elem.position[1])
            changes.append((elem, old_pos))

        return changes

    @staticmethod
    def align_vertical_center(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
        """
        Align all elements to vertical center.

        Returns:
            List of (element, old_position) tuples for undo
        """
        if len(elements) < 2:
            return []

        # Calculate average center
        centers = [elem.position[1] + elem.size[1] / 2 for elem in elements]
        avg_center = sum(centers) / len(centers)

        changes = []
        for elem in elements:
            old_pos = elem.position
            new_y = avg_center - elem.size[1] / 2
            elem.position = (elem.position[0], new_y)
            changes.append((elem, old_pos))

        return changes

    @staticmethod
    def make_same_size(
        elements: List[BaseLayoutElement],
    ) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
        """
        Make all elements the same size as the first element.

        Returns:
            List of (element, old_position, old_size) tuples for undo
        """
        if len(elements) < 2:
            return []

        target_size = elements[0].size
        changes = []

        for elem in elements[1:]:
            old_pos = elem.position
            old_size = elem.size
            elem.size = target_size
            changes.append((elem, old_pos, old_size))

        return changes

    @staticmethod
    def make_same_width(
        elements: List[BaseLayoutElement],
    ) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
        """
        Make all elements the same width as the first element.

        Returns:
            List of (element, old_position, old_size) tuples for undo
        """
        if len(elements) < 2:
            return []

        target_width = elements[0].size[0]
        changes = []

        for elem in elements[1:]:
            old_pos = elem.position
            old_size = elem.size
            elem.size = (target_width, elem.size[1])
            changes.append((elem, old_pos, old_size))

        return changes

    @staticmethod
    def make_same_height(
        elements: List[BaseLayoutElement],
    ) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
        """
        Make all elements the same height as the first element.

        Returns:
            List of (element, old_position, old_size) tuples for undo
        """
        if len(elements) < 2:
            return []

        target_height = elements[0].size[1]
        changes = []

        for elem in elements[1:]:
            old_pos = elem.position
            old_size = elem.size
            elem.size = (elem.size[0], target_height)
            changes.append((elem, old_pos, old_size))

        return changes

    @staticmethod
    def distribute_horizontally(
        elements: List[BaseLayoutElement],
    ) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
        """
        Distribute elements evenly across horizontal span.

        Returns:
            List of (element, old_position) tuples for undo
        """
        if len(elements) < 3:
            return []

        # Sort by x position
        sorted_elements = sorted(elements, key=lambda e: e.position[0])

        # Get leftmost and rightmost positions
        min_x = sorted_elements[0].position[0]
        max_x = sorted_elements[-1].position[0]

        # Calculate spacing between centers
        total_span = max_x - min_x
        spacing = total_span / (len(sorted_elements) - 1)

        changes = []
        for i, elem in enumerate(sorted_elements):
            old_pos = elem.position
            new_x = min_x + (i * spacing)
            elem.position = (new_x, elem.position[1])
            changes.append((elem, old_pos))

        return changes

    @staticmethod
    def distribute_vertically(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
        """
        Distribute elements evenly across vertical span.

        Returns:
            List of (element, old_position) tuples for undo
        """
        if len(elements) < 3:
            return []

        # Sort by y position
        sorted_elements = sorted(elements, key=lambda e: e.position[1])

        # Get topmost and bottommost positions
        min_y = sorted_elements[0].position[1]
        max_y = sorted_elements[-1].position[1]

        # Calculate spacing between centers
        total_span = max_y - min_y
        spacing = total_span / (len(sorted_elements) - 1)

        changes = []
        for i, elem in enumerate(sorted_elements):
            old_pos = elem.position
            new_y = min_y + (i * spacing)
            elem.position = (elem.position[0], new_y)
            changes.append((elem, old_pos))

        return changes

    @staticmethod
    def space_horizontally(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
        """
        Distribute elements with equal spacing between them horizontally.

        Returns:
            List of (element, old_position) tuples for undo
        """
        if len(elements) < 3:
            return []

        # Sort by x position
        sorted_elements = sorted(elements, key=lambda e: e.position[0])

        # Get leftmost and rightmost boundaries
        min_x = sorted_elements[0].position[0]
        max_right = sorted_elements[-1].position[0] + sorted_elements[-1].size[0]

        # Calculate total width of all elements
        total_width = sum(elem.size[0] for elem in sorted_elements)

        # Calculate available space and spacing
        available_space = max_right - min_x - total_width
        spacing = available_space / (len(sorted_elements) - 1)

        changes = []
        current_x = min_x

        for elem in sorted_elements:
            old_pos = elem.position
            elem.position = (current_x, elem.position[1])
            changes.append((elem, old_pos))
            current_x += elem.size[0] + spacing

        return changes

    @staticmethod
    def space_vertically(elements: List[BaseLayoutElement]) -> List[Tuple[BaseLayoutElement, Tuple[float, float]]]:
        """
        Distribute elements with equal spacing between them vertically.

        Returns:
            List of (element, old_position) tuples for undo
        """
        if len(elements) < 3:
            return []

        # Sort by y position
        sorted_elements = sorted(elements, key=lambda e: e.position[1])

        # Get topmost and bottommost boundaries
        min_y = sorted_elements[0].position[1]
        max_bottom = sorted_elements[-1].position[1] + sorted_elements[-1].size[1]

        # Calculate total height of all elements
        total_height = sum(elem.size[1] for elem in sorted_elements)

        # Calculate available space and spacing
        available_space = max_bottom - min_y - total_height
        spacing = available_space / (len(sorted_elements) - 1)

        changes = []
        current_y = min_y

        for elem in sorted_elements:
            old_pos = elem.position
            elem.position = (elem.position[0], current_y)
            changes.append((elem, old_pos))
            current_y += elem.size[1] + spacing

        return changes

    @staticmethod
    def fit_to_page_width(
        element: BaseLayoutElement, page_width: float
    ) -> Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]:
        """
        Resize element to fit page width while maintaining aspect ratio.

        Args:
            element: The element to resize
            page_width: The page width in mm

        Returns:
            Tuple of (element, old_position, old_size) for undo
        """
        old_pos = element.position
        old_size = element.size

        # Calculate aspect ratio
        aspect_ratio = old_size[1] / old_size[0]

        # Set new size
        new_width = page_width
        new_height = page_width * aspect_ratio
        element.size = (new_width, new_height)

        return (element, old_pos, old_size)

    @staticmethod
    def fit_to_page_height(
        element: BaseLayoutElement, page_height: float
    ) -> Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]:
        """
        Resize element to fit page height while maintaining aspect ratio.

        Args:
            element: The element to resize
            page_height: The page height in mm

        Returns:
            Tuple of (element, old_position, old_size) for undo
        """
        old_pos = element.position
        old_size = element.size

        # Calculate aspect ratio
        aspect_ratio = old_size[0] / old_size[1]

        # Set new size
        new_height = page_height
        new_width = page_height * aspect_ratio
        element.size = (new_width, new_height)

        return (element, old_pos, old_size)

    @staticmethod
    def fit_to_page(
        element: BaseLayoutElement, page_width: float, page_height: float
    ) -> Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]:
        """
        Resize element to fit within page dimensions while maintaining aspect ratio.

        Args:
            element: The element to resize
            page_width: The page width in mm
            page_height: The page height in mm

        Returns:
            Tuple of (element, old_position, old_size) for undo
        """
        old_pos = element.position
        old_size = element.size

        # Calculate aspect ratios
        element_aspect = old_size[0] / old_size[1]
        page_aspect = page_width / page_height

        # Determine which dimension to fit to
        if element_aspect > page_aspect:
            # Element is wider than page - fit to width
            new_width = page_width
            new_height = page_width / element_aspect
        else:
            # Element is taller than page - fit to height
            new_height = page_height
            new_width = page_height * element_aspect

        element.size = (new_width, new_height)

        return (element, old_pos, old_size)

    @staticmethod
    def maximize_pattern(
        elements: List[BaseLayoutElement],
        page_size: Tuple[float, float],
        min_gap: float = 2.0,
        max_iterations: int = 100,
        growth_rate: float = 0.05,
    ) -> List[Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]]:
        """
        Maximize element sizes using a crystal growth algorithm.
        Elements grow until they are close to borders or each other.

        Args:
            elements: List of elements to maximize
            page_size: (width, height) of the page in mm
            min_gap: Minimum gap to maintain between elements and borders (in mm)
            max_iterations: Maximum number of growth iterations
            growth_rate: Percentage to grow each iteration (0.05 = 5%)

        Returns:
            List of (element, old_position, old_size) tuples for undo
        """
        if not elements:
            return []

        maximizer = ElementMaximizer(elements, page_size, min_gap)
        return maximizer.maximize(max_iterations, growth_rate)

    @staticmethod
    def expand_to_bounds(
        element: BaseLayoutElement,
        page_size: Tuple[float, float],
        other_elements: List[BaseLayoutElement],
        min_gap: float = 10.0,
    ) -> Tuple[BaseLayoutElement, Tuple[float, float], Tuple[float, float]]:
        """
        Expand a single element until it is min_gap away from page edges or other elements.

        This function expands an element from its current position and size, growing it
        in all directions (up, down, left, right) until it reaches:
        - The page boundaries (with min_gap margin)
        - Another element on the same page (with min_gap spacing)

        The element expands independently in width and height to fill all available space.

        Args:
            element: The element to expand
            page_size: (width, height) of the page in mm
            other_elements: List of other elements on the same page (excluding the target element)
            min_gap: Minimum gap to maintain between element and boundaries/other elements (in mm)

        Returns:
            Tuple of (element, old_position, old_size) for undo
        """
        page_width, page_height = page_size
        old_pos = element.position
        old_size = element.size

        x, y = element.position
        w, h = element.size

        # Calculate maximum expansion in each direction
        # Start with page boundaries
        max_left = x - min_gap  # How much we can expand left
        max_right = (page_width - min_gap) - (x + w)  # How much we can expand right
        max_top = y - min_gap  # How much we can expand up
        max_bottom = (page_height - min_gap) - (y + h)  # How much we can expand down

        # Check constraints from other elements
        # We need to be conservative and check ALL elements against ALL expansion directions
        for other in other_elements:
            ox, oy = other.position
            ow, oh = other.size

            # Calculate the other element's bounds
            other_left = ox
            other_right = ox + ow
            other_top = oy
            other_bottom = oy + oh

            # Calculate current element's bounds
            elem_left = x
            elem_right = x + w
            elem_top = y
            elem_bottom = y + h

            # Check leftward expansion
            # An element blocks leftward expansion if:
            # 1. It's to the left of our left edge (other_right <= elem_left)
            # 2. Its vertical range would overlap with ANY part of our vertical extent
            if other_right <= elem_left:
                # Check if vertical ranges overlap (current OR after any vertical expansion)
                # Conservative: assume we might expand vertically to page bounds
                if not (other_bottom <= elem_top - min_gap or other_top >= elem_bottom + min_gap):
                    # This element blocks leftward expansion
                    available_left = elem_left - other_right - min_gap
                    max_left = min(max_left, available_left)

            # Check rightward expansion
            if other_left >= elem_right:
                # Check if vertical ranges overlap
                if not (other_bottom <= elem_top - min_gap or other_top >= elem_bottom + min_gap):
                    # This element blocks rightward expansion
                    available_right = other_left - elem_right - min_gap
                    max_right = min(max_right, available_right)

            # Check upward expansion
            if other_bottom <= elem_top:
                # Check if horizontal ranges overlap
                if not (other_right <= elem_left - min_gap or other_left >= elem_right + min_gap):
                    # This element blocks upward expansion
                    available_top = elem_top - other_bottom - min_gap
                    max_top = min(max_top, available_top)

            # Check downward expansion
            if other_top >= elem_bottom:
                # Check if horizontal ranges overlap
                if not (other_right <= elem_left - min_gap or other_left >= elem_right + min_gap):
                    # This element blocks downward expansion
                    available_bottom = other_top - elem_bottom - min_gap
                    max_bottom = min(max_bottom, available_bottom)

        # Ensure non-negative expansion
        max_left = max(0, max_left)
        max_right = max(0, max_right)
        max_top = max(0, max_top)
        max_bottom = max(0, max_bottom)

        # Expand to fill all available space (no aspect ratio constraint)
        width_increase = max_left + max_right
        height_increase = max_top + max_bottom

        # Calculate new size
        new_width = w + width_increase
        new_height = h + height_increase

        # Calculate new position (expand from center to maintain relative position)
        # Distribute the expansion proportionally to available space on each side
        if max_left + max_right > 0:
            left_ratio = max_left / (max_left + max_right)
            new_x = x - (width_increase * left_ratio)
        else:
            new_x = x

        if max_top + max_bottom > 0:
            top_ratio = max_top / (max_top + max_bottom)
            new_y = y - (height_increase * top_ratio)
        else:
            new_y = y

        # Apply the new position and size
        element.position = (new_x, new_y)
        element.size = (new_width, new_height)

        return (element, old_pos, old_size)
