import tkinter as tk
from tkinter import Menu
from PIL import Image
from typing import Optional, Callable
import sys
import os

# Add parent directory to path for imports (since we're in utils/ subdirectory)
_parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _parent_dir not in sys.path:
    sys.path.insert(0, _parent_dir)

from SelectionArea import SelectionArea
from ImageContainer import ImageContainer
from ImageManipulation.ManipulationList import GetImageManipulationList


class AreaSelectionHandler:
    """Handler for area selection functionality in the GUI.
    
    This class encapsulates all area selection operations including rectangle,
    circle, and lasso selections, coordinate conversion, and applying
    manipulations to selected areas.
    """
    
    def __init__(self, 
                 canvas: tk.Canvas,
                 get_current_image: Callable,
                 get_canvas_dimensions: Callable,
                 render_image: Callable,
                 root_window: tk.Tk,
                 selection_area: SelectionArea):
        """Initialize the area selection handler.
        
        Args:
            canvas: The tkinter Canvas widget for drawing selections
            get_current_image: Function that returns the current ImageContainer
            get_canvas_dimensions: Function that returns (width, height) tuple
            render_image: Function to re-render the current image
            root_window: The root tkinter window
            selection_area: The SelectionArea instance to manage
        """
        self._canvas = canvas
        self._get_current_image = get_current_image
        self._get_canvas_dimensions = get_canvas_dimensions
        self._render_image = render_image
        self._root = root_window
        self._selectionArea = selection_area
        
        # Selection state
        self._selectionMode = False
        self._selectionType = "rectangle"  # "rectangle", "circle", or "lasso"
        self._selectionStartX = None
        self._selectionStartY = None
        self._selectionEndX = None
        self._selectionEndY = None
        self._selectionRectangle = None
        self._lassoPath = []  # List of canvas coordinates for lasso drawing
        self._lassoLine = None  # Canvas line item for lasso path
    
    def toggle_selection_mode(self) -> None:
        """Toggle interactive selection mode on/off."""
        self._selectionMode = not self._selectionMode
        if self._selectionMode:
            self._root.config(cursor="crosshair")
        else:
            self._root.config(cursor="")
            self.clear_selection()
    
    def toggle_selection_shape(self) -> None:
        """Toggle selection shape between rectangle, circle, and lasso."""
        if self._selectionArea is None:
            return
        # Cycle through: rectangle -> circle -> lasso -> rectangle
        current_shape = self._selectionArea.shape
        if current_shape == "rectangle":
            new_shape = "circle"
        elif current_shape == "circle":
            new_shape = "lasso"
        else:  # lasso
            new_shape = "rectangle"
        
        self._selectionArea.set_shape(new_shape)
        self._selectionType = new_shape
        # Refresh selection drawing if active
        if self._selectionArea.is_active and self._selectionArea.is_valid():
            self._render_image()
    
    def on_mouse_click(self, event) -> bool:
        """Handle mouse click for selection.
        
        Returns:
            True if the click was handled by selection, False otherwise
        """
        # Handle selection mode
        if not self._selectionMode:
            return False
        
        current_image = self._get_current_image()
        if current_image is None:
            return False
        
        # Handle lasso mode
        if self._selectionType == "lasso":
            # Clear any existing selection
            self.clear_selection()
            # Start new lasso path
            self._lassoPath = []
            canvas_x = self._canvas.canvasx(event.x)
            canvas_y = self._canvas.canvasy(event.y)
            self._lassoPath.append((canvas_x, canvas_y))
            return True
        
        # Handle rectangle/circle mode
        # Clear any existing selection
        self.clear_selection()
        
        # Store starting coordinates
        self._selectionStartX = event.x
        self._selectionStartY = event.y
        return True
    
    def on_mouse_drag(self, event) -> bool:
        """Handle mouse drag for selection.
        
        Returns:
            True if the drag was handled by selection, False otherwise
        """
        # Handle selection mode
        if not self._selectionMode:
            return False
        
        current_image = self._get_current_image()
        if current_image is None:
            return False
        
        # Handle lasso mode
        if self._selectionType == "lasso" and self._lassoPath:
            # Add point to lasso path
            canvas_x = self._canvas.canvasx(event.x)
            canvas_y = self._canvas.canvasy(event.y)
            self._lassoPath.append((canvas_x, canvas_y))
            
            # Draw lasso path
            if len(self._lassoPath) > 1:
                # Clear previous lasso line
                if self._lassoLine:
                    self._canvas.delete(self._lassoLine)
                # Draw new lasso path
                self._lassoLine = self._canvas.create_line(
                    *[coord for point in self._lassoPath for coord in point],
                    fill="red", width=3, smooth=False
                )
            return True
        
        # Handle rectangle/circle mode
        if self._selectionStartX is None:
            return False
        
        # Clear previous shape
        if self._selectionRectangle:
            self._canvas.delete(self._selectionRectangle)
        
        # Draw shape outline depending on selection shape
        if self._selectionArea.shape == "circle":
            self._selectionRectangle = self._canvas.create_oval(
                self._selectionStartX, self._selectionStartY, event.x, event.y,
                outline="red", width=3, fill=""
            )
        else:
            self._selectionRectangle = self._canvas.create_rectangle(
                self._selectionStartX, self._selectionStartY, event.x, event.y,
                outline="red", width=3, fill=""
            )
        return True
    
    def on_mouse_release(self, event) -> bool:
        """Handle mouse release to finalize selection.
        
        Returns:
            True if the release was handled by selection, False otherwise
        """
        # Handle selection mode
        if not self._selectionMode:
            return False
        
        current_image = self._get_current_image()
        if current_image is None:
            return False
        
        # Handle lasso mode
        if self._selectionType == "lasso" and self._lassoPath:
            if len(self._lassoPath) >= 3:
                # Convert lasso path from canvas coordinates to image coordinates
                pil_image = current_image.getImage()
                img_width, img_height = pil_image.size
                
                # Get canvas dimensions for scaling
                canvas_width, canvas_height = self._get_canvas_dimensions()
                if canvas_width <= 0 or canvas_height <= 0:
                    return True
                
                # Calculate scaling factors
                scale_x = img_width / canvas_width
                scale_y = img_height / canvas_height
                
                # Convert canvas coordinates to image coordinates
                image_path = [(int(x * scale_x), int(y * scale_y)) for x, y in self._lassoPath]
                
                # Set the lasso path in selection area
                self._selectionArea.set_lasso_path(image_path)
                
                # Redraw the selection highlight to ensure it's visible
                self._render_image()
            return True
        
        # Handle rectangle/circle mode
        if self._selectionStartX is None:
            return False
        
        # Store ending coordinates
        self._selectionEndX = event.x
        self._selectionEndY = event.y
        
        # Convert screen coordinates to image coordinates
        selection_coords = self._convert_to_image_coordinates(
            self._selectionStartX, self._selectionStartY, 
            self._selectionEndX, self._selectionEndY
        )
        
        if selection_coords:
            # Set the selection area
            self._selectionArea.set_coordinates(
                selection_coords["left"], selection_coords["top"],
                selection_coords["right"], selection_coords["bottom"]
            )
            # Redraw the selection highlight to ensure it's visible
            self._render_image()
        return True
    
    def on_right_click(self, event, get_default_params: Callable) -> bool:
        """Handle right-click to show context menu for manipulation options.
        
        Args:
            event: The mouse event
            get_default_params: Function to get default parameters for a manipulation
            
        Returns:
            True if the right-click was handled, False otherwise
        """
        if not self._selectionArea or not self._selectionArea.is_active:
            return False
        
        if not self._selectionArea.is_valid():
            return False
        
        # Create context menu with all available manipulations
        context_menu = tk.Menu(self._root, tearoff=0)
        
        # Add crop to selection option first (most common use case)
        context_menu.add_command(label="Crop to Selection", command=self.crop_to_selection)
        context_menu.add_separator()
        
        # Add manipulation options
        manipulations = GetImageManipulationList()
        for manipulation in manipulations:
            manipulation_name = manipulation.getManipulationName()
            context_menu.add_command(
                label=f"Apply {manipulation_name} to Selection",
                command=lambda m=manipulation: self.apply_manipulation_to_selection(m, get_default_params)
            )
        
        context_menu.add_separator()
        context_menu.add_command(label="Clear Selection", command=self.clear_selection)
        
        # Show context menu at cursor position
        try:
            context_menu.tk_popup(event.x_root, event.y_root)
        finally:
            context_menu.grab_release()
        
        return True
    
    def _convert_to_image_coordinates(self, start_x, start_y, end_x, end_y):
        """Convert screen coordinates to image coordinates."""
        current_image = self._get_current_image()
        if current_image is None:
            return None
        
        pil_image = current_image.getImage()
        if pil_image is None:
            return None
        
        # Convert widget coordinates to canvas coordinates (accounts for borders/scroll)
        canvas_x1 = self._canvas.canvasx(start_x)
        canvas_y1 = self._canvas.canvasy(start_y)
        canvas_x2 = self._canvas.canvasx(end_x)
        canvas_y2 = self._canvas.canvasy(end_y)
        
        # Get image dimensions
        img_width, img_height = pil_image.size
        
        # Get canvas dimensions for scaling
        canvas_width, canvas_height = self._get_canvas_dimensions()
        if canvas_width <= 0 or canvas_height <= 0:
            return None
        
        # Calculate scaling factors to convert canvas coordinates to image coordinates
        scale_x = img_width / canvas_width
        scale_y = img_height / canvas_height
        
        # Convert canvas coordinates to image coordinates
        left = int(min(canvas_x1, canvas_x2) * scale_x)
        top = int(min(canvas_y1, canvas_y2) * scale_y)
        right = int(max(canvas_x1, canvas_x2) * scale_x)
        bottom = int(max(canvas_y1, canvas_y2) * scale_y)
        
        # Ensure coordinates are within image bounds
        left = max(0, min(left, img_width))
        top = max(0, min(top, img_height))
        right = max(left, min(right, img_width))
        bottom = max(top, min(bottom, img_height))
        
        # Only return valid selection coordinates
        if right > left and bottom > top:
            return {"left": left, "top": top, "right": right, "bottom": bottom}
        
        return None
    
    def crop_to_selection(self) -> None:
        """Crop the entire image to the selected area."""
        current_image = self._get_current_image()
        if current_image is None or not self._selectionArea or not self._selectionArea.is_active:
            return
        
        # Get the selection coordinates
        coords = self._selectionArea.get_coordinates()
        if not coords:
            return
        
        left, top, right, bottom = coords
        
        # Validate coordinates
        if right <= left or bottom <= top:
            return
        
        # Get the original image
        original_image = current_image.getImage()
        if original_image is None:
            return
        
        img_width, img_height = original_image.size
        
        # Ensure coordinates are within image bounds
        left = max(0, min(left, img_width))
        top = max(0, min(top, img_height))
        right = max(left, min(right, img_width))
        bottom = max(top, min(bottom, img_height))
        
        # Validate final coordinates
        if right <= left or bottom <= top:
            return
        
        try:
            # Take undo snapshot
            current_image.snapshot()
            
            # Use SelectionArea's crop_image method which handles all shapes including lasso
            cropped_image = self._selectionArea.crop_image(original_image)
            
            if cropped_image is None:
                return
            
            # For lasso, we get an image with transparency, so convert to RGB if needed
            # Otherwise keep the cropped image as is
            if self._selectionArea.shape == "lasso" and cropped_image.mode == "RGBA":
                # Create a white background for lasso crops
                background = Image.new("RGB", cropped_image.size, (255, 255, 255))
                background.paste(cropped_image, (0, 0), cropped_image.split()[3] if cropped_image.mode == "RGBA" else None)
                cropped_image = background
            
            # Update the current image
            current_image._imageData = cropped_image
            
            # Clear selection before re-rendering
            self.clear_selection()
            
            # Re-render the image (without selection)
            self._render_image()
        except Exception as e:
            # Silently handle errors - crop operation failed
            pass
    
    def apply_manipulation_to_selection(self, manipulation, get_default_params: Callable) -> None:
        """Apply the specified manipulation to the selected area only."""
        current_image = self._get_current_image()
        if current_image is None or not self._selectionArea.is_active:
            return
        
        # Take undo snapshot
        current_image.snapshot()
        
        # Get the original image
        original_image = current_image.getImage()
        
        # Crop the selection area
        selected_area = self._selectionArea.crop_image(original_image)
        if selected_area is None:
            return
        
        # Create a temporary ImageContainer for the selected area
        temp_container = ImageContainer()
        temp_container._imageData = selected_area
        
        # Apply manipulation to the selected area
        # Use default parameters if none specified
        params = get_default_params(manipulation)
        manipulation.manipulateImage(temp_container, params)
        
        # Get the processed selection
        processed_selection = temp_container.getImage()
        
        # Apply the processed selection back to the original image
        result_image = self._selectionArea.apply_to_image(original_image, processed_selection)
        
        # Update the current image
        current_image._imageData = result_image
        
        # Clear selection before re-rendering
        self.clear_selection()
        
        # Re-render the image (without selection)
        self._render_image()
    
    def clear_selection(self) -> None:
        """Clear the selection rectangle and reset selection state."""
        if self._selectionRectangle:
            self._canvas.delete(self._selectionRectangle)
            self._selectionRectangle = None
        if self._lassoLine:
            self._canvas.delete(self._lassoLine)
            self._lassoLine = None
        self._lassoPath = []
        self._selectionStartX = None
        self._selectionStartY = None
        self._selectionEndX = None
        self._selectionEndY = None
        self._selectionArea.clear()
    
    def draw_selection_highlight(self) -> None:
        """Draw a colored highlight over the selected area."""
        if not self._selectionArea.is_active or not self._selectionArea.is_valid():
            return
        
        coords = self._selectionArea.get_coordinates()
        if not coords:
            return
        
        left, top, right, bottom = coords
        
        # Convert image coordinates to canvas coordinates
        current_image = self._get_current_image()
        if current_image is None:
            return
        
        pil_image = current_image.getImage()
        if pil_image is None:
            return
        
        img_width, img_height = pil_image.size
        
        # Get canvas dimensions for scaling
        canvas_width, canvas_height = self._get_canvas_dimensions()
        if canvas_width <= 0 or canvas_height <= 0:
            return
        
        # Calculate scaling factors
        scale_x = canvas_width / img_width
        scale_y = canvas_height / img_height
        
        # Convert coordinates
        canvas_left = int(left * scale_x)
        canvas_top = int(top * scale_y)
        canvas_right = int(right * scale_x)
        canvas_bottom = int(bottom * scale_y)
        
        # Draw colored edges only
        if self._selectionArea.shape == "lasso" and self._selectionArea.lasso_path:
            # Convert lasso path from image coordinates to canvas coordinates
            canvas_path = [(int(x * scale_x), int(y * scale_y)) for x, y in self._selectionArea.lasso_path]
            if len(canvas_path) >= 3:
                # Close the path by adding the first point at the end
                closed_path = canvas_path + [canvas_path[0]]
                self._canvas.create_line(
                    *[coord for point in closed_path for coord in point],
                    fill="red", width=3, smooth=False
                )
        elif self._selectionArea.shape == "circle":
            self._canvas.create_oval(
                canvas_left, canvas_top, canvas_right, canvas_bottom,
                outline="red", width=3, fill=""
            )
        else:
            self._canvas.create_rectangle(
                canvas_left, canvas_top, canvas_right, canvas_bottom,
                outline="red", width=3, fill=""
            )
    
    @property
    def selection_mode(self) -> bool:
        """Get the current selection mode state."""
        return self._selectionMode
    
    @property
    def selection_type(self) -> str:
        """Get the current selection type."""
        return self._selectionType
    
    @property
    def selection_area(self) -> SelectionArea:
        """Get the SelectionArea instance."""
        return self._selectionArea

