# --- START OF FILE map_display.py ---

# map_display.py
"""
THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED.

Manages the dedicated OpenCV window for displaying the map overlay.
Applies scaling based on AppState factor before displaying in a fixed-size
(autosized) window and handles mouse events on the map window.
"""

# Standard library imports
import logging
import math
import time  # Needed for mouse event rate limiting
from typing import Optional, TYPE_CHECKING, Tuple

# Third-party imports
import cv2
import numpy as np

# Try to import PIL for type hinting and image conversion
try:
    from PIL import Image

    ImageType = Image.Image
except ImportError:
    Image = None  # type: ignore
    ImageType = None  # type: ignore

# Local application imports
from controlpanel import config
from controlpanel.utils.utils import put_queue  # Needed for sending mouse coordinates to the app queue

# Type hinting for App reference
if TYPE_CHECKING:
    # Import the specific main application class name
    from controlpanel.app_main import ControlPanelApp


class MapDisplayWindow:
    """
    Manages the OpenCV window used to display the map.
    Applies scaling based on AppState factor before displaying in a fixed-size window.
    Handles mouse events within the map window.
    """

    MAX_DISPLAY_WIDTH = config.MAX_MAP_DISPLAY_WIDTH
    MAX_DISPLAY_HEIGHT = config.MAX_MAP_DISPLAY_HEIGHT

    def __init__(
        self,
        app: "ControlPanelApp",  # Accept the main application instance
        window_name: str = "Map Overlay",
        x_pos: int = 100,
        y_pos: int = 100,
    ):
        """
        Initializes the MapDisplayWindow manager.

        Args:
            app (ControlPanelApp): Reference to the main App instance (for AppState and queue access).
            window_name (str): The name for the OpenCV window.
            x_pos (int): Initial X position for the window.
            y_pos (int): Initial Y position for the window.
        """
        self._log_prefix = "[MapDisplay]"
        if Image is None:
            logging.critical(
                f"{self._log_prefix} 'Pillow' library not found. Map display will likely fail."
            )

        self.app = app  # Store reference to the main application instance
        self.window_name = window_name
        self.x_pos = x_pos
        self.y_pos = y_pos
        self.window_initialized = False
        # --- >>> START OF NEW ATTRIBUTE <<< ---
        # Flag to track if the mouse callback has been successfully set
        self.map_mouse_callback_set = False
        # --- >>> END OF NEW ATTRIBUTE <<< ---
        # Store the shape of the image actually sent to imshow for coordinate clamping
        self._last_displayed_shape: Tuple[int, int] = (0, 0)  # (height, width)
        logging.debug(
            f"{self._log_prefix} Initializing window manager for '{self.window_name}'."
        )

    def show_map(self, map_image_pil: Optional[ImageType]):
        """
        Displays the map image. Converts PIL to BGR, applies scaling based on
        AppState factor, shows in a fixed-size (autosized) OpenCV window,
        and ensures the mouse callback is set. Recreates the window if the
        scaled image size changes.

        Args:
            map_image_pil (Optional[ImageType]): The map image (PIL format) to display.
                                                 If None, shows a placeholder/error image.
        """
        log_prefix = f"{self._log_prefix} ShowMap"  # Shortened log prefix

        # --- 1. Prepare Base Image (Placeholder or Convert PIL) ---
        map_to_display_bgr = None
        if map_image_pil is None:
            logging.warning(
                f"{log_prefix} Received None image payload. Generating placeholder."
            )
            map_to_display_bgr = self._create_placeholder_image()
        elif Image is not None and isinstance(map_image_pil, Image.Image):
            logging.debug(
                f"{log_prefix} Converting PIL Image payload (Size: {map_image_pil.size}, Mode: {map_image_pil.mode})..."
            )
            map_to_display_bgr = self._convert_pil_to_bgr(map_image_pil)
        else:
            logging.error(
                f"{log_prefix} Received unexpected payload type: {type(map_image_pil)}. Using fallback."
            )
            map_to_display_bgr = self._create_placeholder_image()

        # Final safety check if conversion/placeholder creation failed
        if map_to_display_bgr is None:
            logging.error(
                f"{log_prefix} map_to_display_bgr is None before scaling. Using minimal fallback."
            )
            map_to_display_bgr = np.full((256, 256, 3), 60, dtype=np.uint8)

        final_image_to_show = None
        try:
            # --- 2. Apply Scaling based on AppState Factor ---
            scale_factor = self.app.state.map_display_scale_factor
            img_h, img_w = map_to_display_bgr.shape[:2]
            logging.debug(
                f"{log_prefix} Base image size: {img_w}x{img_h}. Scale factor: {scale_factor:.4f}"
            )

            if abs(scale_factor - 1.0) > 1e-6 and img_w > 0 and img_h > 0:
                target_display_w = max(1, int(round(img_w * scale_factor)))
                target_display_h = max(1, int(round(img_h * scale_factor)))

                if target_display_w != img_w or target_display_h != img_h:
                    interpolation = (
                        cv2.INTER_LINEAR if scale_factor > 1.0 else cv2.INTER_AREA
                    )
                    logging.debug(
                        f"{log_prefix} Resizing image to {target_display_w}x{target_display_h}..."
                    )
                    final_image_to_show = cv2.resize(
                        map_to_display_bgr,
                        (target_display_w, target_display_h),
                        interpolation=interpolation,
                    )
                else:
                    final_image_to_show = map_to_display_bgr
            else:
                final_image_to_show = map_to_display_bgr

            if final_image_to_show is None:
                logging.error(
                    f"{log_prefix} Image is None after scaling attempt. Using base image."
                )
                final_image_to_show = map_to_display_bgr

            # --- 3. Display using OpenCV (WINDOW_AUTOSIZE) ---
            final_shape = final_image_to_show.shape
            final_h, final_w = final_shape[:2]
            logging.debug(
                f"{log_prefix} Final image shape for display: {final_w}x{final_h}"
            )

            # --- Window Recreation Logic ---
            if (
                self.window_initialized
                and (final_h, final_w) != self._last_displayed_shape
            ):
                logging.info(
                    f"{log_prefix} Image size changed ({self._last_displayed_shape} -> {(final_h, final_w)}). Recreating window..."
                )
                try:
                    cv2.destroyWindow(self.window_name)
                    cv2.waitKey(5)  # Brief wait
                    self.window_initialized = False
                    self.map_mouse_callback_set = False  # Reset callback flag too
                    logging.debug(f"{log_prefix} Destroyed existing window.")
                except cv2.error as e:
                    logging.warning(
                        f"{log_prefix} Error destroying window before recreate: {e}"
                    )
                    self.window_initialized = False
                    self.map_mouse_callback_set = False

            # --- Window Creation and Callback Setup ---
            if not self.window_initialized:
                logging.debug(
                    f"{log_prefix} Ensuring window '{self.window_name}' exists (AUTOSIZE) and is positioned..."
                )
                cv2.namedWindow(self.window_name, cv2.WINDOW_AUTOSIZE)
                try:
                    cv2.moveWindow(self.window_name, self.x_pos, self.y_pos)
                    self.window_initialized = True
                    logging.info(
                        f"{log_prefix} Window '{self.window_name}' (AUTOSIZE) created/moved to ({self.x_pos}, {self.y_pos})."
                    )
                    # --- >>> START OF NEW CODE <<< ---
                    # Attempt to set mouse callback immediately after creation/move
                    if not self.map_mouse_callback_set:
                        try:
                            cv2.setMouseCallback(
                                self.window_name, self._map_mouse_callback
                            )
                            self.map_mouse_callback_set = True
                            logging.info(
                                f"{log_prefix} Mouse callback set for '{self.window_name}'."
                            )
                        except cv2.error as cb_e:
                            logging.error(
                                f"{log_prefix} Failed to set mouse callback for '{self.window_name}' on creation: {cb_e}"
                            )
                    # --- >>> END OF NEW CODE <<< ---
                except cv2.error as move_e:
                    logging.warning(
                        f"{log_prefix} Could not move '{self.window_name}' window after creation: {move_e}."
                    )
                    self.window_initialized = True  # Assume window created
                    # --- >>> START OF NEW CODE <<< ---
                    # Still try to set callback even if move failed
                    if not self.map_mouse_callback_set:
                        try:
                            cv2.setMouseCallback(
                                self.window_name, self._map_mouse_callback
                            )
                            self.map_mouse_callback_set = True
                            logging.info(
                                f"{log_prefix} Mouse callback set for '{self.window_name}' (after move failed)."
                            )
                        except cv2.error as cb_e:
                            logging.error(
                                f"{log_prefix} Failed to set mouse callback for '{self.window_name}' after move failed: {cb_e}"
                            )
                    # --- >>> END OF NEW CODE <<< ---

            # --- Display Image ---
            cv2.imshow(self.window_name, final_image_to_show)
            self._last_displayed_shape = (
                final_h,
                final_w,
            )  # Store the shape we just displayed
            logging.debug(
                f"{log_prefix} Displayed final scaled image in window '{self.window_name}'."
            )

            # --- >>> START OF NEW CODE <<< ---
            # Ensure callback is set if window already existed but callback wasn't set
            if self.window_initialized and not self.map_mouse_callback_set:
                try:
                    cv2.setMouseCallback(self.window_name, self._map_mouse_callback)
                    self.map_mouse_callback_set = True
                    logging.info(
                        f"{log_prefix} Mouse callback set for existing window '{self.window_name}'."
                    )
                except cv2.error as cb_e:
                    logging.error(
                        f"{log_prefix} Failed to set mouse callback for existing window '{self.window_name}': {cb_e}"
                    )
            # --- >>> END OF NEW CODE <<< ---

            cv2.waitKey(1)  # Allow OpenCV to process events

        except cv2.error as e:
            if (
                "NULL window" in str(e)
                or "invalid window" in str(e)
                or "checkView" in str(e)
            ):
                logging.warning(
                    f"{log_prefix} OpenCV window '{self.window_name}' seems closed or invalid."
                )
                self.window_initialized = False
                self.map_mouse_callback_set = False  # Reset flag
                self._last_displayed_shape = (0, 0)
            else:
                logging.exception(f"{log_prefix} OpenCV error during map display: {e}")
        except Exception as e:
            logging.exception(
                f"{log_prefix} Unexpected error displaying map image: {e}"
            )
            # Consider resetting flags on unexpected errors
            # self.window_initialized = False
            # self.map_mouse_callback_set = False
            # self._last_displayed_shape = (0, 0)

    def _create_placeholder_image(self) -> Optional[np.ndarray]:
        """Creates a BGR NumPy placeholder image."""
        log_prefix = f"{self._log_prefix} Placeholder"
        if Image is None:
            logging.error(
                f"{log_prefix} Pillow not available, using minimal NumPy fallback."
            )
            return np.full((256, 256, 3), 60, dtype=np.uint8)  # Minimal fallback
        try:
            tile_size = (
                self.app.map_integration_manager._map_service.tile_size
                if hasattr(self.app, "map_integration_manager")
                and self.app.map_integration_manager
                and hasattr(self.app.map_integration_manager, "_map_service")
                else 256
            )
            placeholder_size = (tile_size, tile_size)
            placeholder_color_rgb = getattr(
                config, "OFFLINE_MAP_PLACEHOLDER_COLOR", (200, 200, 200)
            )
            if not (
                isinstance(placeholder_color_rgb, tuple)
                and len(placeholder_color_rgb) == 3
            ):
                placeholder_color_rgb = (200, 200, 200)

            placeholder_pil = Image.new(
                "RGB", placeholder_size, color=placeholder_color_rgb
            )
            placeholder_np = np.array(placeholder_pil)
            return cv2.cvtColor(placeholder_np, cv2.COLOR_RGB2BGR)
        except Exception as ph_err:
            logging.exception(f"{log_prefix} Failed to create placeholder image:")
            return np.full((256, 256, 3), 60, dtype=np.uint8)  # Minimal fallback

    def _convert_pil_to_bgr(self, img_pil: ImageType) -> Optional[np.ndarray]:
        """Converts a PIL Image to NumPy BGR format."""
        log_prefix = f"{self._log_prefix} ConvertPIL"
        try:
            img_np = np.array(img_pil)
            if img_np.ndim == 2:  # Grayscale
                return cv2.cvtColor(img_np, cv2.COLOR_GRAY2BGR)
            elif img_np.ndim == 3 and img_np.shape[2] == 3:  # RGB
                return cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
            elif img_np.ndim == 3 and img_np.shape[2] == 4:  # RGBA
                return cv2.cvtColor(img_np, cv2.COLOR_RGBA2BGR)
            else:
                raise ValueError(
                    f"Unsupported NumPy shape after PIL conversion: {img_np.shape}"
                )
        except Exception as e:
            logging.exception(f"{log_prefix} Error converting PIL image to BGR:")
            return None

    # --- >>> START OF NEW MOUSE CALLBACK METHOD <<< ---
    def _map_mouse_callback(self, event, x, y, flags, param):
        # ... (controllo evento LBUTTONDOWN e clamping come prima) ...
        log_prefix_cb = f"{self._log_prefix} MouseClick"
        if event == cv2.EVENT_LBUTTONDOWN:
            logging.debug(
                f"{log_prefix_cb} Map Window Left Click detected at ({x}, {y})"
            )
            try:
                current_display_height, current_display_width = (
                    self._last_displayed_shape
                )
                if current_display_width <= 0 or current_display_height <= 0:
                    return

                x_clamped = max(0, min(x, current_display_width - 1))
                y_clamped = max(0, min(y, current_display_height - 1))

                # --- Action 1: Queue pixel coords for geo calculation (original command) ---
                geo_command = "MAP_MOUSE_COORDS"
                geo_payload = (x_clamped, y_clamped)
                logging.debug(
                    f"{log_prefix_cb} Putting command '{geo_command}' payload {geo_payload} onto tkinter_queue for geo calc."
                )
                put_queue(
                    queue_obj=self.app.tkinter_queue,
                    item=(geo_command, geo_payload),
                    queue_name="tkinter",
                    app_instance=self.app,
                )

                # --- >>> START OF NEW ACTION <<< ---
                # --- Action 2: Queue pixel coords to update marker state ---
                click_command = "MAP_CLICK_UPDATE"
                click_payload = (x_clamped, y_clamped)
                logging.debug(
                    f"{log_prefix_cb} Putting command '{click_command}' payload {click_payload} onto tkinter_queue for marker state."
                )
                put_queue(
                    queue_obj=self.app.tkinter_queue,
                    item=(click_command, click_payload),
                    queue_name="tkinter",
                    app_instance=self.app,
                )
                # --- >>> END OF NEW ACTION <<< ---

            except Exception as e:
                logging.exception(f"{log_prefix_cb} Error in map mouse callback:")

    # --- >>> END OF NEW MOUSE CALLBACK METHOD <<< ---

    def destroy_window(self):
        """Explicitly destroys the managed OpenCV window."""
        log_prefix = f"{self._log_prefix} Destroy"
        logging.info(f"{log_prefix} Attempting to destroy window: '{self.window_name}'")
        if self.window_initialized or hasattr(self, "window_name"):
            try:
                cv2.destroyWindow(self.window_name)
                self.window_initialized = False
                self.map_mouse_callback_set = False  # Reset flag
                self._last_displayed_shape = (0, 0)
                logging.info(
                    f"{log_prefix} Window '{self.window_name}' destroyed successfully."
                )
            except cv2.error as e:
                logging.warning(
                    f"{log_prefix} Ignoring error destroying window '{self.window_name}' (may already be closed): {e}"
                )
            except Exception as e:
                logging.exception(
                    f"{log_prefix} Unexpected error destroying window '{self.window_name}': {e}"
                )
        else:
            logging.debug(
                f"{log_prefix} Window '{self.window_name}' was not initialized or name unknown."
            )


# --- END OF FILE map_display.py ---
