import warnings

import json
import random

from PyQt5.QtGui import QPen, QColor, QBrush, QPainterPath
from PyQt5.QtCore import QRectF, QObject, pyqtSignal, Qt, QTimer, pyqtProperty
from PyQt5.QtWidgets import QGraphicsRectItem, QGraphicsItemGroup, QGraphicsLineItem, QGraphicsPathItem

warnings.filterwarnings("ignore", category=DeprecationWarning)


# ----------------------------------------------------------------------------------------------------------------------
# Classes
# ----------------------------------------------------------------------------------------------------------------------


class WorkArea(QObject):
    """
    Class representing a rectangular work area on an image.
    Stores the coordinates and provides methods for serialization and deserialization.
    Manages its own graphical representation.
    """
    
    removed = pyqtSignal(object)  # Signal emitted when work area is removed
    
    def __init__(self, x=0, y=0, width=0, height=0, image_path=None):
        """
        Initialize a work area with coordinates and dimensions.
        
        Args:
            x (float): X-coordinate of the top-left corner
            y (float): Y-coordinate of the top-left corner
            width (float): Width of the work area
            height (float): Height of the work area
            image_path (str): Path to the image this work area belongs to
        """
        super().__init__()
        
        self.rect = QRectF(x, y, width, height)
        self.image_path = image_path
        self.graphics_item = None  # Reference to the main graphics item in the scene
        self.remove_button = None  # Reference to the remove button graphics item
        self.shadow_area = None  # Reference to the shadow graphics item
        
        # Create a random color for the work area
        self.work_area_pen = QPen(QColor(0, 120, 215), 2, Qt.DotLine)  # Changed to static dotted line
        
        # Animation properties (updated for pulsing)
        self._pulse_alpha = 128  # Starting alpha for pulsing (semi-transparent)
        self._pulse_direction = 1  # 1 for increasing alpha, -1 for decreasing
        self.animation_timer = QTimer()
        self.animation_timer.timeout.connect(self._update_pulse_alpha)
        self.animation_timer.setInterval(50)  # Update every 50ms for faster pulsing

    @pyqtProperty(int)  # Added for pulsing alpha
    def pulse_alpha(self):
        """Get the current pulse alpha for animation."""
        return self._pulse_alpha
    
    @pulse_alpha.setter
    def pulse_alpha(self, value):
        """Set the pulse alpha and update pen styles."""
        self._pulse_alpha = int(max(0, min(255, value)))  # Clamp to 0-255 and convert to int
        self._update_pen_style()
    
    def _update_pulse_alpha(self):
        """Update the pulse alpha for a heartbeat-like effect: quick rise, slow fall."""
        if self._pulse_direction == 1:
            # Quick increase (systole-like)
            self._pulse_alpha += 30
        else:
            # Slow decrease (diastole-like)
            self._pulse_alpha -= 10  # <-- Corrected from += to -=

        # Check direction before clamping to ensure smooth transition
        if self._pulse_alpha >= 255:
            self._pulse_alpha = 255  # Clamp to max
            self._pulse_direction = -1
        elif self._pulse_alpha <= 50:
            self._pulse_alpha = 50   # Clamp to min
            self._pulse_direction = 1
        
        self._update_pen_style()
    
    def _create_pen(self):
        """Create a pen with pulsing alpha and brighter color if animating."""
        pen = QPen(self.work_area_pen)
        if self.animation_timer.isActive():
            pen_color = QColor(self.work_area_pen.color())
            pen_color.setAlpha(self._pulse_alpha)  # Apply pulsing alpha for animation
            pen.setColor(pen_color)
        pen.setStyle(Qt.DotLine)  # Predefined dotted line (static, no movement)
        return pen

    def animate(self):
        """Start the pulsing animation for the work area rectangle."""
        if not self.animation_timer.isActive():
            self.animation_timer.start()
    
    def deanimate(self):
        """Stop the pulsing animation for the work area rectangle."""
        self.animation_timer.stop()
        self._pulse_alpha = 128  # Reset to default
        self._update_pen_style()

    def _update_pen_style(self):
        """Update the pen style of the graphics item with the current pulse alpha."""
        if self.graphics_item:
            self.graphics_item.setPen(self._create_pen())
            self.graphics_item.update()

    @classmethod
    def from_rect(cls, rect, image_path=None):
        """
        Create a WorkArea instance from a QRectF.
        
        Args:
            rect (QRectF): Rectangle defining the work area
            image_path (str): Path to the image this work area belongs to
            
        Returns:
            WorkArea: A new WorkArea instance
        """
        return cls(rect.x(), rect.y(), rect.width(), rect.height(), image_path)
        
    @classmethod
    def from_dict(cls, data, image_path=None):
        """
        Create a WorkArea instance from a dictionary.
        
        Args:
            data (dict): Dictionary containing x, y, width, and height values
            image_path (str): Path to the image this work area belongs to
            
        Returns:
            WorkArea: A new WorkArea instance
        """
        return cls(
            data.get('x', 0),
            data.get('y', 0),
            data.get('width', 0),
            data.get('height', 0),
            image_path or data.get('image_path')
        )
        
    def to_dict(self):
        """
        Convert the work area to a dictionary.
        
        Returns:
            dict: Dictionary representation of the work area
        """
        return {
            'x': self.rect.x(),
            'y': self.rect.y(),
            'width': self.rect.width(),
            'height': self.rect.height(),
            'image_path': self.image_path
        }
        
    def create_graphics(self, scene, pen_width=2, include_shadow=False):
        """
        Create and return the graphics representation of this work area.
        
        Args:
            scene: The QGraphicsScene to add the items to
            pen_width: Width of the pen for the rectangle
            include_shadow: Whether to include a shadow effect
            
        Returns:
            QGraphicsRectItem: The created rectangle item
        """
        # Create rectangle item if it doesn't exist
        if not self.graphics_item:
            self.graphics_item = QGraphicsRectItem(self.rect)
            self.work_area_pen.setWidth(pen_width)
            self.graphics_item.setPen(self._create_pen())
            
            # Store reference to the work area in the graphics item
            self.graphics_item.setData(0, "work_area")
            self.graphics_item.setData(1, self)
            
            # Add to scene
            scene.addItem(self.graphics_item)
            # Always start animation when shown
            self.animate()
        
        # Remove any existing shadow before creating a new one
        if self.shadow_area is not None and hasattr(self.shadow_area, "scene") and self.shadow_area.scene():
            self.shadow_area.scene().removeItem(self.shadow_area)
            self.shadow_area = None
            if self.graphics_item:
                self.graphics_item.setData(3, None)
        
        # Add shadow if requested
        if include_shadow:
            # Create a semi-transparent overlay for the shadow
            shadow_brush = QBrush(QColor(0, 0, 0, 150))  # Semi-transparent black
            shadow_path = QPainterPath()
            shadow_path.addRect(scene.sceneRect())  # Cover the entire scene
            shadow_path.addRect(self.rect)  # Add the work area rect
            # Subtract the work area from the overlay
            shadow_path = shadow_path.simplified()

            # Create the shadow item
            shadow_area = QGraphicsPathItem(shadow_path)
            shadow_area.setBrush(shadow_brush)
            shadow_area.setPen(QPen(Qt.NoPen))  # No outline for the shadow
            
            # Add shadow to scene with z-value below the work area
            scene.addItem(shadow_area)
            
            # Store reference to shadow
            self.shadow_area = shadow_area
            if self.graphics_item:
                self.graphics_item.setData(3, shadow_area)
        
        return self.graphics_item
        
    def create_remove_button(self, button_size=20, thickness=2):
        """
        Create a remove button (X) for this work area.
        
        Args:
            button_size: Size of the button
            thickness: Thickness of the X lines
            
        Returns:
            QGraphicsItemGroup: The button graphics group
        """
        if not self.graphics_item:
            return None
            
        # Create a group item to hold the X shape
        if not self.remove_button:
            self.remove_button = QGraphicsItemGroup(self.graphics_item)
            
            # Create two diagonal lines to form an X
            line1 = QGraphicsLineItem(0, 0, button_size, button_size, self.remove_button)
            line2 = QGraphicsLineItem(0, button_size, button_size, 0, self.remove_button)
            
            # Set the pen properties - thicker red lines
            red_pen = QPen(QColor(255, 0, 0), thickness)
            line1.setPen(red_pen)
            line2.setPen(red_pen)
            
            # Position in the top-right corner
            self.remove_button.setPos(self.rect.right() - button_size - 10, self.rect.top() + 10)
            
            # Store data to identify this button and its work area
            self.remove_button.setData(0, "remove_button")
            self.remove_button.setData(1, self)  # Store reference to the work area
            
            # Make the group item clickable
            self.remove_button.setAcceptedMouseButtons(Qt.LeftButton)
            
            # Store reference to the remove button in the graphics item
            self.graphics_item.setData(2, self.remove_button)
            
            # Override mousePressEvent for the button item
            def on_press(event):
                self.remove()
                
            self.remove_button.mousePressEvent = on_press
            
        return self.remove_button
    
    def add_to_scene(self, scene, pen_width=2, button_size=20):
        """
        Add this work area's graphics to the specified scene.
        Creates both the rectangle and remove button in one operation.
        
        Args:
            scene: The QGraphicsScene to add the items to
            pen_width: Width of the pen for the rectangle
            button_size: Size of the remove button
            
        Returns:
            bool: True if successfully added to the scene, False otherwise
        """
        # Create the main rectangle graphic
        graphics_item = self.create_graphics(scene, pen_width)
        if not graphics_item or not graphics_item.scene():
            return False
            
        # Create the remove button
        remove_button = self.create_remove_button(button_size, pen_width)
        if not remove_button:
            return False
            
        return True

    def remove_from_scene(self):
        """
        Remove this work area's graphics from its scene.
        
        Returns:
            bool: True if successfully removed, False if not in a scene
        """
        if self.shadow_area is not None and hasattr(self.shadow_area, "scene") and self.shadow_area.scene():
            self.shadow_area.scene().removeItem(self.shadow_area)
            self.shadow_area = None
        if self.graphics_item and self.graphics_item.scene():
            self.graphics_item.scene().removeItem(self.graphics_item)
            self.graphics_item = None
            self.remove_button = None
            return True
        return False
        
    def set_remove_button_visibility(self, visible):
        """
        Set the visibility of the remove button.
        
        Args:
            visible (bool): Whether the button should be visible
        """
        if self.remove_button:
            self.remove_button.setVisible(visible)
        
    def contains_point(self, point):
        """
        Check if the work area contains a point.
        
        Args:
            point: QPointF or point-like object with x() and y() methods
            
        Returns:
            bool: True if the point is inside the work area, False otherwise
        """
        return self.rect.contains(point)
        
    def intersects(self, other_rect):
        """
        Check if the work area intersects with another rectangle.
        
        Args:
            other_rect: QRectF or rect-like object
            
        Returns:
            bool: True if the rectangles intersect, False otherwise
        """
        return self.rect.intersects(other_rect)
        
    def is_valid(self):
        """
        Check if the work area has valid dimensions.
        
        Returns:
            bool: True if the work area is valid, False otherwise
        """
        return (self.rect.width() > 10 and 
                self.rect.height() > 10 and 
                self.image_path is not None)
                
    def remove(self):
        """Remove this work area and emit the removed signal."""
        # Remove graphics from scene
        self.remove_from_scene()
        
        # Emit the removed signal
        self.removed.emit(self)
            
    def __eq__(self, other):
        """
        Check if two work areas are equal.
        
        Args:
            other: Another WorkArea instance
            
        Returns:
            bool: True if the work areas have the same coordinates and image path
        """
        if not isinstance(other, WorkArea):
            return False
            
        return (
            abs(self.rect.x() - other.rect.x()) < 0.001 and
            abs(self.rect.y() - other.rect.y()) < 0.001 and
            abs(self.rect.width() - other.rect.width()) < 0.001 and
            abs(self.rect.height() - other.rect.height()) < 0.001 and
            self.image_path == other.image_path
        )
        
    def __hash__(self):
        """
        Generate a hash for the work area.
        
        Returns:
            int: Hash value
        """
        return hash((
            round(self.rect.x(), 3),
            round(self.rect.y(), 3),
            round(self.rect.width(), 3),
            round(self.rect.height(), 3),
            self.image_path
        ))

    def __del__(self):
        """Clean up the timer when the work area is deleted."""
        if hasattr(self, 'animation_timer') and self.animation_timer:
            self.animation_timer.stop()