###########################################################################
#    OCRFeeder - The complete OCR suite
#    Copyright (C) 2009-2013 Joaquim Rocha <me@joaquimrocha.com>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
###########################################################################

"""
Cairo-based replacement for GooCanvas BoxesArea.

This module provides a pure GTK3 + Cairo implementation of the canvas
for displaying images with selectable/editable rectangular regions.
"""

import gi
gi.require_foreign('cairo')
from gi.repository import Gtk, Gdk, GObject, GdkPixbuf
import cairo


class Box:
    """A draggable, resizable rectangle representing an OCR region."""

    MOUSE_STATE_NORMAL = 0
    MOUSE_STATE_TOP_DRAG = 1 << 0
    MOUSE_STATE_BOTTOM_DRAG = 1 << 1
    MOUSE_STATE_LEFT_DRAG = 1 << 2
    MOUSE_STATE_RIGHT_DRAG = 1 << 3

    CURSOR_CHANGE_MAX_DISTANCE = 5

    def __init__(self, x, y, width, height, fill_rgba, stroke_rgba):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.fill_rgba = fill_rgba  # tuple (r, g, b, a) with values 0-255
        self.stroke_rgba = stroke_rgba
        self.start_point = None
        self._drag_offset = None
        self._canvas = None  # Reference to parent canvas for redraws

    def set_property(self, name, value):
        """GooCanvas compatibility: set a property by name."""
        # Map GooCanvas property names to our attributes
        prop_map = {
            'x': 'x',
            'y': 'y',
            'width': 'width',
            'height': 'height',
            'fill-color-rgba': 'fill_rgba',
            'fill_color_rgba': 'fill_rgba',
            'stroke-color-rgba': 'stroke_rgba',
            'stroke_color_rgba': 'stroke_rgba',
        }
        attr_name = prop_map.get(name, name.replace('-', '_'))
        if attr_name in ('fill_rgba', 'stroke_rgba') and isinstance(value, int):
            # Convert integer RGBA to tuple
            value = (
                (value >> 24) & 0xFF,
                (value >> 16) & 0xFF,
                (value >> 8) & 0xFF,
                value & 0xFF
            )
        setattr(self, attr_name, value)
        if self._canvas:
            self._canvas.queue_draw()

    def contains_point(self, px, py):
        """Check if a point is inside the box."""
        return (self.x <= px <= self.x + self.width and
                self.y <= py <= self.y + self.height)

    def get_edge_at_point(self, px, py, threshold):
        """
        Determine which edge(s) are near the given point.
        Returns a mouse state flag indicating which edges.
        """
        state = self.MOUSE_STATE_NORMAL

        # Check top edge
        if abs(py - self.y) < threshold and self.x <= px <= self.x + self.width:
            state |= self.MOUSE_STATE_TOP_DRAG
        # Check bottom edge
        if abs(py - (self.y + self.height)) < threshold and self.x <= px <= self.x + self.width:
            state |= self.MOUSE_STATE_BOTTOM_DRAG
        # Check left edge
        if abs(px - self.x) < threshold and self.y <= py <= self.y + self.height:
            state |= self.MOUSE_STATE_LEFT_DRAG
        # Check right edge
        if abs(px - (self.x + self.width)) < threshold and self.y <= py <= self.y + self.height:
            state |= self.MOUSE_STATE_RIGHT_DRAG

        return state

    def overlaps(self, other):
        """Check if this box overlaps with another box."""
        return not (self.x + self.width <= other.x or
                    other.x + other.width <= self.x or
                    self.y + self.height <= other.y or
                    other.y + other.height <= self.y)

    def draw(self, cr, selected=False, selected_stroke_rgba=None):
        """Draw the box using Cairo."""
        # Fill
        r, g, b, a = [c / 255.0 for c in self.fill_rgba]
        cr.set_source_rgba(r, g, b, a)
        cr.rectangle(self.x, self.y, self.width, self.height)
        cr.fill()

        # Stroke
        if selected and selected_stroke_rgba:
            r, g, b, a = [c / 255.0 for c in selected_stroke_rgba]
        else:
            r, g, b, a = [c / 255.0 for c in self.stroke_rgba]
        cr.set_source_rgba(r, g, b, a)
        cr.set_line_width(2.0)
        cr.rectangle(self.x, self.y, self.width, self.height)
        cr.stroke()


class BoxesArea(Gtk.DrawingArea):
    """
    A Cairo-based canvas for displaying an image with editable box regions.
    
    This replaces the GooCanvas-based implementation with pure GTK3 + Cairo.
    """

    MINIMUM_ZOOM_HEIGHT = 50
    IMAGE_FRAME_THICKNESS = 5
    IMAGE_FRAME_COLOR = (0.44, 0.44, 0.44)  # #717171

    __gsignals__ = {
        'selected_box' : (GObject.SIGNAL_RUN_LAST,
                     GObject.TYPE_NONE,
                     (GObject.TYPE_PYOBJECT,)),
        'removed_box' : (GObject.SIGNAL_RUN_LAST,
                     GObject.TYPE_NONE,
                     (GObject.TYPE_PYOBJECT,)),
        'updated_box' : (GObject.SIGNAL_RUN_LAST,
                     GObject.TYPE_NONE,
                     (GObject.TYPE_PYOBJECT,)),
        'deselected_box' : (GObject.SIGNAL_RUN_LAST,
                            GObject.TYPE_NONE,
                            (GObject.TYPE_PYOBJECT,)),
        'dragged_box' : (GObject.SIGNAL_RUN_LAST,
                     GObject.TYPE_NONE,
                     (GObject.TYPE_PYOBJECT,)),
        'changed_zoom' : (GObject.SIGNAL_RUN_LAST,
                     GObject.TYPE_NONE,
                     (GObject.TYPE_FLOAT,))
        }

    def __init__(self, image_path):
        super().__init__()

        self.set_can_focus(True)
        self.add_events(
            Gdk.EventMask.BUTTON_PRESS_MASK |
            Gdk.EventMask.BUTTON_RELEASE_MASK |
            Gdk.EventMask.POINTER_MOTION_MASK |
            Gdk.EventMask.SCROLL_MASK |
            Gdk.EventMask.KEY_PRESS_MASK
        )

        # Image
        self.pixbuf = None
        self.image_width = 0
        self.image_height = 0

        # Transform
        self._scale = 1.0

        # Boxes
        self.boxes = []
        self.selected_areas = []
        self.currently_created_area = None

        # Colors (RGBA 0-255)
        self.area_fill_rgba = (94, 156, 235, 150)
        self.area_stroke_rgba = (94, 156, 235, 250)
        self.area_selected_stroke_rgba = (255, 255, 0, 255)

        # Drag state
        self._drag_start = None
        self._drag_box = None
        self._resize_state = Box.MOUSE_STATE_NORMAL

        # Load image
        self.setBackgroundImage(image_path)

        # Connect signals
        self.connect('draw', self._on_draw)
        self.connect('button-press-event', self._on_button_press)
        self.connect('button-release-event', self._on_button_release)
        self.connect('motion-notify-event', self._on_motion_notify)
        self.connect('scroll-event', self._on_scroll)
        self.connect('key-press-event', self._on_key_press)

    def setBackgroundImage(self, image_path):
        """Load and set the background image."""
        self.pixbuf = GdkPixbuf.Pixbuf.new_from_file(image_path)
        self.image_width = self.pixbuf.get_width()
        self.image_height = self.pixbuf.get_height()
        self._update_size()
        self.queue_draw()

    def _update_size(self):
        """Update the widget size based on image and scale."""
        width = int(self.image_width * self._scale * 2)
        height = int(self.image_height * self._scale * 1.5)
        self.set_size_request(width, height)

    def _on_draw(self, widget, cr):
        """Handle the draw signal."""
        # Apply transform
        cr.save()
        cr.scale(self._scale, self._scale)

        # Draw background
        allocation = self.get_allocation()
        cr.set_source_rgb(0.9, 0.9, 0.9)
        cr.rectangle(0, 0, allocation.width / self._scale, allocation.height / self._scale)
        cr.fill()

        # Draw image
        if self.pixbuf:
            Gdk.cairo_set_source_pixbuf(cr, self.pixbuf, 0, 0)
            cr.paint()

            # Draw frame (shadow effect)
            r, g, b = self.IMAGE_FRAME_COLOR
            cr.set_source_rgb(r, g, b)
            # Right edge
            cr.rectangle(self.image_width, 0,
                         self.IMAGE_FRAME_THICKNESS, self.image_height + self.IMAGE_FRAME_THICKNESS)
            cr.fill()
            # Bottom edge
            cr.rectangle(0, self.image_height,
                         self.image_width + 1, self.IMAGE_FRAME_THICKNESS)
            cr.fill()

        # Draw all boxes
        for box in self.boxes:
            is_selected = box in self.selected_areas
            box.draw(cr, is_selected, self.area_selected_stroke_rgba)

        # Draw currently created area
        if self.currently_created_area:
            self.currently_created_area.draw(cr, True, self.area_selected_stroke_rgba)

        cr.restore()
        return False

    def _screen_to_canvas(self, x, y):
        """Convert screen coordinates to canvas coordinates."""
        return x / self._scale, y / self._scale

    def _on_button_press(self, widget, event):
        """Handle mouse button press."""
        self.grab_focus()

        if event.button != 1:
            return False

        cx, cy = self._screen_to_canvas(event.x, event.y)

        # Check if clicking on an existing box
        clicked_box = None
        for box in reversed(self.boxes):  # Check topmost first
            edge_state = box.get_edge_at_point(cx, cy, Box.CURSOR_CHANGE_MAX_DISTANCE / self._scale)
            if edge_state != Box.MOUSE_STATE_NORMAL:
                clicked_box = box
                self._resize_state = edge_state
                break
            elif box.contains_point(cx, cy):
                clicked_box = box
                self._resize_state = Box.MOUSE_STATE_NORMAL
                break

        if clicked_box:
            # Handle selection
            if not (event.state & Gdk.ModifierType.SHIFT_MASK):
                self.deselectAreas()
            self.selectArea(clicked_box)

            self._drag_box = clicked_box
            self._drag_start = (cx, cy)
            clicked_box._drag_offset = (cx - clicked_box.x, cy - clicked_box.y)
            return True

        # Starting new selection on image
        if 0 <= cx <= self.image_width and 0 <= cy <= self.image_height:
            self.deselectAreas()
            self.currently_created_area = Box(
                cx, cy, 0, 0,
                self.area_fill_rgba,
                self.area_stroke_rgba
            )
            self.currently_created_area.start_point = (cx, cy)
            self._drag_start = (cx, cy)
            self.queue_draw()
            return True

        return False

    def _on_button_release(self, widget, event):
        """Handle mouse button release."""
        if event.button != 1:
            return False

        if self.currently_created_area:
            # Finish creating new area
            if self.currently_created_area.width >= 5 and self.currently_created_area.height >= 5:
                self.currently_created_area._canvas = self
                self.boxes.append(self.currently_created_area)
                self._handle_overlapped_areas(self.currently_created_area)
                self.selectArea(self.currently_created_area)
            self.currently_created_area = None

        if self._drag_box:
            self._handle_overlapped_areas(self._drag_box)
            self.emit('updated_box', self._drag_box)

        self._drag_box = None
        self._drag_start = None
        self._resize_state = Box.MOUSE_STATE_NORMAL
        self._set_cursor(None)
        self.queue_draw()
        return True

    def _on_motion_notify(self, widget, event):
        """Handle mouse motion."""
        cx, cy = self._screen_to_canvas(event.x, event.y)

        # Dragging to create new area
        if self.currently_created_area and self._drag_start:
            start_x, start_y = self.currently_created_area.start_point
            end_x = max(0, min(cx, self.image_width))
            end_y = max(0, min(cy, self.image_height))

            x = min(start_x, end_x)
            y = min(start_y, end_y)
            w = abs(end_x - start_x)
            h = abs(end_y - start_y)

            self.currently_created_area.x = x
            self.currently_created_area.y = y
            self.currently_created_area.width = w
            self.currently_created_area.height = h
            self.queue_draw()
            return True

        # Dragging/resizing existing box
        if self._drag_box and self._drag_start:
            box = self._drag_box
            if self._resize_state == Box.MOUSE_STATE_NORMAL:
                # Moving the box
                dx, dy = box._drag_offset
                new_x = cx - dx
                new_y = cy - dy
                # Clamp to image bounds
                new_x = max(0, min(new_x, self.image_width - box.width))
                new_y = max(0, min(new_y, self.image_height - box.height))
                box.x = new_x
                box.y = new_y
            else:
                # Resizing
                self._resize_box(box, cx, cy)

            self.emit('dragged_box', box)
            self.queue_draw()
            return True

        # Update cursor based on position
        self._update_cursor_for_position(cx, cy)
        return True

    def _resize_box(self, box, cx, cy):
        """Resize a box based on drag state."""
        min_size = 5

        if self._resize_state & Box.MOUSE_STATE_TOP_DRAG:
            new_y = int(max(0, cy))
            new_height = box.height + (box.y - new_y)
            if new_height >= min_size:
                box.y = new_y
                box.height = new_height

        if self._resize_state & Box.MOUSE_STATE_BOTTOM_DRAG:
            new_height = min(self.image_height, cy) - box.y
            if new_height >= min_size:
                box.height = new_height

        if self._resize_state & Box.MOUSE_STATE_LEFT_DRAG:
            new_x = int(max(0, cx))
            new_width = box.width + (box.x - new_x)
            if new_width >= min_size:
                box.x = new_x
                box.width = new_width

        if self._resize_state & Box.MOUSE_STATE_RIGHT_DRAG:
            new_width = min(self.image_width, cx) - box.x
            if new_width >= min_size:
                box.width = new_width

    def _update_cursor_for_position(self, cx, cy):
        """Update cursor based on mouse position over boxes."""
        for box in reversed(self.boxes):
            edge_state = box.get_edge_at_point(cx, cy, Box.CURSOR_CHANGE_MAX_DISTANCE / self._scale)
            if edge_state != Box.MOUSE_STATE_NORMAL:
                cursor = self._get_cursor_for_state(edge_state)
                self._set_cursor(cursor)
                return
            elif box.contains_point(cx, cy):
                self._set_cursor('grab')
                return

        self._set_cursor(None)

    def _get_cursor_for_state(self, state):
        """Get cursor name for a resize state."""
        if state == Box.MOUSE_STATE_TOP_DRAG:
            return 'n-resize'
        elif state == Box.MOUSE_STATE_BOTTOM_DRAG:
            return 's-resize'
        elif state == Box.MOUSE_STATE_LEFT_DRAG:
            return 'w-resize'
        elif state == Box.MOUSE_STATE_RIGHT_DRAG:
            return 'e-resize'
        elif state == (Box.MOUSE_STATE_TOP_DRAG | Box.MOUSE_STATE_LEFT_DRAG):
            return 'nw-resize'
        elif state == (Box.MOUSE_STATE_TOP_DRAG | Box.MOUSE_STATE_RIGHT_DRAG):
            return 'ne-resize'
        elif state == (Box.MOUSE_STATE_BOTTOM_DRAG | Box.MOUSE_STATE_LEFT_DRAG):
            return 'sw-resize'
        elif state == (Box.MOUSE_STATE_BOTTOM_DRAG | Box.MOUSE_STATE_RIGHT_DRAG):
            return 'se-resize'
        return None

    def _set_cursor(self, cursor_name):
        """Set the cursor for the widget."""
        window = self.get_window()
        if window:
            if cursor_name:
                display = Gdk.Display.get_default()
                cursor = Gdk.Cursor.new_from_name(display, cursor_name)
                window.set_cursor(cursor)
            else:
                window.set_cursor(None)

    def _on_scroll(self, widget, event):
        """Handle scroll events for zooming."""
        if event.state & Gdk.ModifierType.CONTROL_MASK:
            if event.direction == Gdk.ScrollDirection.UP:
                self.zoom(0.05)
                return True
            elif event.direction == Gdk.ScrollDirection.DOWN:
                self.zoom(-0.05)
                return True
        return False

    def _on_key_press(self, widget, event):
        """Handle key press events."""
        key_name = Gdk.keyval_name(event.keyval).lower()

        # Zoom with Ctrl+Up/Down
        if event.state & Gdk.ModifierType.CONTROL_MASK:
            if key_name == 'up':
                self.zoom(0.2)
                return True
            elif key_name == 'down':
                self.zoom(-0.2)
                return True

        # Move selected boxes with arrow keys
        if self.selected_areas and key_name in ['left', 'up', 'right', 'down']:
            step = 10 if event.state & Gdk.ModifierType.SHIFT_MASK else 2
            for box in self.selected_areas:
                if key_name == 'down':
                    box.y = min(box.y + step, self.image_height - box.height)
                elif key_name == 'up':
                    box.y = max(box.y - step, 0)
                elif key_name == 'right':
                    box.x = min(box.x + step, self.image_width - box.width)
                elif key_name == 'left':
                    box.x = max(box.x - step, 0)
                self.emit('updated_box', box)
            self.queue_draw()
            return True

        # Delete selected boxes
        if key_name in ['delete', 'backspace']:
            self.deleteSelectedAreas()
            return True

        return False

    def _handle_overlapped_areas(self, area):
        """Remove boxes that overlap with the given area."""
        to_remove = []
        for box in self.boxes:
            if box != area and box not in self.selected_areas and area.overlaps(box):
                to_remove.append(box)

        for box in to_remove:
            self.boxes.remove(box)
            self.emit('removed_box', box)

    def _select_surrounding_area(self, offset):
        """Select a box relative to current selection."""
        if not self.boxes:
            return
        current_idx = 0
        if self.selected_areas:
            current = self.selected_areas[-1]
            if current in self.boxes:
                current_idx = self.boxes.index(current)
        new_idx = (current_idx + offset) % len(self.boxes)
        self.deselectAreas()
        self.selectArea(self.boxes[new_idx])

    # Public API (compatible with original GooCanvas implementation)

    def get_scale(self):
        """Get current zoom scale."""
        return self._scale

    def set_scale(self, scale):
        """Set zoom scale."""
        self._scale = scale
        self._update_size()
        self.queue_draw()

    def zoom(self, zoom_value, add_zoom=True):
        """Zoom the canvas."""
        if add_zoom:
            new_zoom = self._scale + zoom_value
        else:
            new_zoom = zoom_value

        if new_zoom * self.image_height >= self.MINIMUM_ZOOM_HEIGHT:
            self._scale = new_zoom
            self._update_size()
            self.emit('changed_zoom', self._scale)
            self.queue_draw()

    def getImageSize(self):
        """Get the image dimensions."""
        return self.image_height, self.image_width

    def selectArea(self, area):
        """Select a box."""
        if area not in self.selected_areas:
            self.selected_areas.append(area)
        self.emit('selected_box', area)
        self.queue_draw()

    def deselectAreas(self):
        """Deselect all boxes."""
        for area in self.selected_areas[:]:
            self.selected_areas.remove(area)
            self.emit('deselected_box', area)
        self.queue_draw()

    def deselectArea(self, area):
        """Deselect a specific box."""
        if area in self.selected_areas:
            self.selected_areas.remove(area)
            self.emit('deselected_box', area)
            self.queue_draw()
            return True
        return False

    def selectAllAreas(self):
        """Select all boxes."""
        for area in self.boxes:
            self.selectArea(area)

    def getSelectedAreas(self):
        """Get list of selected boxes."""
        return self.selected_areas

    def deleteSelectedAreas(self):
        """Delete all selected boxes."""
        for area in self.selected_areas[:]:
            if area in self.boxes:
                self.boxes.remove(area)
            self.emit('removed_box', area)
        self.selected_areas = []
        self.queue_draw()

    def selectNextArea(self):
        """Select the next box."""
        self._select_surrounding_area(-1)

    def selectPreviousArea(self):
        """Select the previous box."""
        self._select_surrounding_area(1)

    def addArea(self, dimensions):
        """Add a new box with given dimensions."""
        x, y, width, height = dimensions
        new_area = Box(x, y, width, height, self.area_fill_rgba, self.area_stroke_rgba)
        new_area._canvas = self
        self._handle_overlapped_areas(new_area)
        self.boxes.append(new_area)
        self.queue_draw()
        return new_area

    def clearAreas(self):
        """Remove all boxes."""
        self.boxes = []
        self.selected_areas = []
        self.queue_draw()

    def getAllAreas(self):
        """Get all boxes."""
        return self.boxes[:]

    def setAreaFillRgba(self, rgba):
        """Set the fill color for new boxes."""
        self.area_fill_rgba = rgba

    def setAreaStrokeRgba(self, rgba):
        """Set the stroke color for new boxes."""
        self.area_stroke_rgba = rgba

    # Compatibility shims for code that accesses box properties via .props

    def grab_focus(self, item=None):
        """Focus the widget."""
        super().grab_focus()
