# --- START OF FILE display.py ---

# 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 display of MFD and SAR images using OpenCV windows.
Handles window creation, positioning, image updates (called from main thread),
mouse event callbacks for the SAR window, and window destruction during shutdown.
Uses standardized logging prefixes and levels.
"""

# Standard library imports
import logging
import queue
import time
from typing import Optional  # For type hinting

# Third-party imports
import cv2
import numpy as np

# Local application imports
import config  # For default sizes and other constants
from utils import put_queue


class DisplayManager:
    """
    Manages MFD and SAR OpenCV display windows and related interactions.
    Requires image update calls (show_mfd_image, show_sar_image) to be made
    from the main GUI thread. Logs operations with the [DisplayMgr] prefix.
    """

    # --- MODIFIED FUNCTION: __init__ ---
    def __init__(
        self,
        app,
        sar_queue,
        mouse_queue,
        sar_x,
        sar_y,
        mfd_x,
        mfd_y,
        initial_sar_width: Optional[int] = None,  # <<< ADDED Argument
        initial_sar_height: Optional[int] = None,  # <<< ADDED Argument
    ):
        """
        Initializes the DisplayManager.

        Args:
            app (App): Reference to the main application instance.
            sar_queue (queue.Queue): Queue for receiving processed SAR images.
            mouse_queue (queue.Queue): Queue for sending raw mouse coordinates.
            sar_x (int): Initial X position for the SAR window.
            sar_y (int): Initial Y position for the SAR window.
            mfd_x (int): Initial X position for the MFD window.
            mfd_y (int): Initial Y position for the MFD window.
            initial_sar_width (Optional[int]): Initial width for SAR placeholder/window. Defaults to config.
            initial_sar_height (Optional[int]): Initial height for SAR placeholder/window. Defaults to config.
        """
        # Define log prefix for this class instance
        self.log_prefix = "[DisplayMgr]"
        logging.debug(f"{self.log_prefix} Initializing DisplayManager...")

        self.app = app  # Reference to main application
        self.sar_queue = sar_queue
        self.mouse_queue = mouse_queue
        self.sar_x = sar_x
        self.sar_y = sar_y
        self.mfd_x = mfd_x
        self.mfd_y = mfd_y
        self.sar_window_name = "SAR"
        self.mfd_window_name = "MFD"
        self.sar_window_initialized = False
        self.mfd_window_initialized = False
        self.sar_mouse_callback_set = False

        # Store initial dimensions from arguments or config for placeholders
        # Use passed values if provided, otherwise fall back to config defaults
        self.initial_mfd_width = (
            config.INITIAL_MFD_WIDTH
        )  # MFD size not changed by map overlay (yet)
        self.initial_mfd_height = config.INITIAL_MFD_HEIGHT
        self.initial_sar_width = (
            initial_sar_width
            if initial_sar_width is not None
            else config.INITIAL_SAR_WIDTH
        )
        self.initial_sar_height = (
            initial_sar_height
            if initial_sar_height is not None
            else config.INITIAL_SAR_HEIGHT
        )
        logging.debug(
            f"{self.log_prefix} Using Initial Sizes - MFD: {self.initial_mfd_width}x{self.initial_mfd_height}, SAR: {self.initial_sar_width}x{self.initial_sar_height}"
        )

        logging.debug(f"{self.log_prefix} DisplayManager initialization complete.")

    # --- END MODIFIED FUNCTION: __init__ ---

    # --- MODIFIED FUNCTION: initialize_display_windows ---
    def initialize_display_windows(self):
        """
        Creates and displays initial placeholder windows for MFD and SAR.
        Forces OpenCV windows to appear at startup with default background.
        MUST be called from the main GUI thread.
        Uses the initial dimensions stored during __init__.
        """
        # Use INFO level for significant initialization step
        logging.info(
            f"{self.log_prefix} Initializing MFD and SAR display windows with placeholders..."
        )
        try:
            # MFD Placeholder - Uses self.initial_mfd_... attributes
            logging.debug(
                f"{self.log_prefix} Creating placeholder MFD image ({self.initial_mfd_height}x{self.initial_mfd_width})."
            )
            placeholder_mfd = np.zeros(
                (self.initial_mfd_height, self.initial_mfd_width, 3), dtype=np.uint8
            )
            placeholder_mfd[:, :] = [30, 30, 30]  # Dark Gray BGR

            logging.debug(f"{self.log_prefix} Attempting to show placeholder MFD...")
            # show_mfd_image logs internally
            self.show_mfd_image(placeholder_mfd)
            # Log success after the call if window became initialized
            if self.mfd_window_initialized:
                logging.info(
                    f"{self.log_prefix} MFD window '{self.mfd_window_name}' initialized with placeholder."
                )
            else:
                logging.warning(
                    f"{self.log_prefix} MFD window initialization attempt finished, but window not marked as initialized."
                )

        except Exception as e:
            # Use ERROR for failure during initialization
            logging.error(
                f"{self.log_prefix} Failed to initialize MFD window with placeholder: {e}",
                exc_info=True,
            )

        try:
            # SAR Placeholder - Uses self.initial_sar_... attributes
            logging.debug(
                f"{self.log_prefix} Creating placeholder SAR image ({self.initial_sar_height}x{self.initial_sar_width})."
            )
            # Ensure placeholder is BGR (3 channels) as show_sar_image might receive colorized images later
            placeholder_sar = np.zeros(
                (self.initial_sar_height, self.initial_sar_width, 3), dtype=np.uint8
            )
            placeholder_sar[:, :] = [60, 60, 60]  # Lighter Gray BGR

            logging.debug(f"{self.log_prefix} Attempting to show placeholder SAR...")
            # show_sar_image logs internally
            self.show_sar_image(placeholder_sar)
            # Log success after the call
            if self.sar_window_initialized:
                logging.info(
                    f"{self.log_prefix} SAR window '{self.sar_window_name}' initialized with placeholder."
                )
            else:
                logging.warning(
                    f"{self.log_prefix} SAR window initialization attempt finished, but window not marked as initialized."
                )

        except Exception as e:
            # Use ERROR for failure during initialization
            logging.error(
                f"{self.log_prefix} Failed to initialize SAR window with placeholder: {e}",
                exc_info=True,
            )

    # --- END MODIFIED FUNCTION: initialize_display_windows ---

    def show_mfd_image(self, image):
        """
        Displays the MFD image in its OpenCV window. Creates/moves window on first call.
        MUST be called from the main GUI thread. Includes cv2.waitKey(1).

        Args:
            image (numpy.ndarray): The MFD image to display (BGR format).
        """
        # Use DEBUG for start of display function
        logging.debug(
            f"{self.log_prefix} Received request to show MFD image (Shape: {getattr(image, 'shape', 'N/A')})."
        )

        # Validate input - Use ERROR for invalid input causing failure
        if image is None or not isinstance(image, np.ndarray) or image.size == 0:
            logging.error(
                f"{self.log_prefix} show_mfd_image received invalid image data. Aborting display."
            )
            return
        if image.ndim != 3 or image.shape[2] != 3:
            logging.error(
                f"{self.log_prefix} show_mfd_image received non-BGR image (ndim={image.ndim}, channels={image.shape[2] if image.ndim==3 else 'N/A'}). Aborting display."
            )
            return

        try:
            window_name = self.mfd_window_name
            # Create and move window only once
            if not self.mfd_window_initialized:
                # DEBUG for first-time setup
                logging.debug(
                    f"{self.log_prefix} First call for '{window_name}'. Creating and positioning window."
                )
                cv2.imshow(window_name, image)
                try:
                    cv2.moveWindow(window_name, self.mfd_x, self.mfd_y)
                    self.mfd_window_initialized = (
                        True  # Set flag AFTER successful operations
                    )
                    # INFO log for successful window creation/move
                    logging.info(
                        f"{self.log_prefix} '{window_name}' window created and moved to ({self.mfd_x}, {self.mfd_y})."
                    )
                    cv2.waitKey(1)  # Allow window to draw/position
                except cv2.error as move_e:
                    # WARNING if move fails but window might still exist
                    logging.warning(
                        f"{self.log_prefix} Could not move '{window_name}' window on initialization: {move_e}. Window might appear elsewhere."
                    )
                    # Set flag true anyway, assuming imshow worked.
                    self.mfd_window_initialized = True
            else:
                # Just update the image - DEBUG for subsequent updates
                logging.debug(
                    f"{self.log_prefix} Updating existing MFD window '{window_name}'."
                )
                cv2.imshow(window_name, image)

            # Essential waitKey - DEBUG for calling waitKey
            # logging.debug(f"{self.log_prefix} Calling waitKey(1) for MFD window.") # Less verbose
            cv2.waitKey(1)

        except cv2.error as e:
            # Handle cases where window might have been closed manually
            # Use WARNING as it's recoverable
            if "NULL window" in str(e) or "invalid window" in str(e):
                logging.warning(
                    f"{self.log_prefix} OpenCV window '{window_name}' seems closed. Will re-initialize on next valid image."
                )
                self.mfd_window_initialized = False  # Reset flag
            else:
                # Keep EXCEPTION for other OpenCV errors
                logging.exception(
                    f"{self.log_prefix} OpenCV error displaying MFD image: {e}"
                )
        except Exception as e:
            # Keep EXCEPTION for unexpected errors
            logging.exception(
                f"{self.log_prefix} Unexpected error displaying MFD image: {e}"
            )

    def show_sar_image(self, image):
        """
        Displays the SAR image in its OpenCV window. Handles first-time setup
        (creation, move, mouse callback) and subsequent updates.
        MUST be called from the main GUI thread. Includes cv2.waitKey(1).

        Args:
            image (numpy.ndarray): The SAR image (grayscale or BGR colorized).
        """
        # DEBUG for start of display function
        logging.debug(
            f"{self.log_prefix} Received request to show SAR image (Shape: {getattr(image, 'shape', 'N/A')})."
        )

        # Validate input - ERROR for invalid input
        if image is None or not isinstance(image, np.ndarray) or image.size == 0:
            logging.error(
                f"{self.log_prefix} show_sar_image received invalid image data. Aborting display."
            )
            return
        if image.shape[0] == 0 or image.shape[1] == 0:
            logging.error(
                f"{self.log_prefix} show_sar_image received image with invalid dimensions: {image.shape}. Aborting."
            )
            return
        # Basic check for supported types (grayscale or BGR)
        if not (image.ndim == 2 or (image.ndim == 3 and image.shape[2] == 3)):
            logging.error(
                f"{self.log_prefix} show_sar_image received unsupported image format (ndim={image.ndim}, channels={image.shape[2] if image.ndim==3 else 'N/A'}). Expected Grayscale or BGR. Aborting."
            )
            return

        try:
            window_name = self.sar_window_name
            # Create, move window, and set callback only once
            if not self.sar_window_initialized:
                # DEBUG for first-time setup
                logging.debug(
                    f"{self.log_prefix} First call for '{window_name}'. Creating, positioning window, and setting callback."
                )
                cv2.imshow(window_name, image)
                try:
                    cv2.moveWindow(window_name, self.sar_x, self.sar_y)
                    self.sar_window_initialized = True  # Mark initialized
                    # INFO for successful window creation/move
                    logging.info(
                        f"{self.log_prefix} '{window_name}' window created and moved to ({self.sar_x}, {self.sar_y})."
                    )

                    # Set mouse callback immediately after successful initialization
                    if not self.sar_mouse_callback_set:
                        cv2.setMouseCallback(window_name, self.sar_mouse_callback)
                        self.sar_mouse_callback_set = True
                        # INFO for successful callback setup
                        logging.info(
                            f"{self.log_prefix} Mouse callback set for '{window_name}'."
                        )

                    cv2.waitKey(1)  # Allow window to draw/position
                except cv2.error as move_e:
                    # WARNING if move fails
                    logging.warning(
                        f"{self.log_prefix} Could not move '{window_name}' window on initialization: {move_e}."
                    )
                    self.sar_window_initialized = True  # Assume imshow worked
                    # Still try setting callback even if move failed
                    if not self.sar_mouse_callback_set:
                        try:
                            cv2.setMouseCallback(window_name, self.sar_mouse_callback)
                            self.sar_mouse_callback_set = True
                            logging.info(
                                f"{self.log_prefix} Mouse callback set for '{window_name}' (after move failed)."
                            )
                        except cv2.error as cb_e:
                            # ERROR if callback setting fails
                            logging.error(
                                f"{self.log_prefix} Failed to set mouse callback for '{window_name}': {cb_e}"
                            )
            else:
                # Just update the image - DEBUG for subsequent updates
                logging.debug(
                    f"{self.log_prefix} Updating existing SAR window '{window_name}'."
                )
                cv2.imshow(window_name, image)

            # Essential waitKey - DEBUG for calling waitKey
            # logging.debug(f"{self.log_prefix} Calling waitKey(1) for SAR window.") # Less verbose
            cv2.waitKey(1)

        except cv2.error as e:
            # WARNING for recoverable closed window error
            if "NULL window" in str(e) or "invalid window" in str(e):
                logging.warning(
                    f"{self.log_prefix} OpenCV window '{self.sar_window_name}' seems closed. Will re-initialize on next valid image."
                )
                self.sar_window_initialized = False
                self.sar_mouse_callback_set = False  # Reset callback flag too
            else:
                # Keep EXCEPTION for other OpenCV errors
                logging.exception(
                    f"{self.log_prefix} OpenCV error displaying SAR image: {e}"
                )
        except Exception as e:
            # Keep EXCEPTION for unexpected errors
            logging.exception(
                f"{self.log_prefix} Unexpected error displaying SAR image: {e}"
            )

    # Method inside DisplayManager class in display.py
    def sar_mouse_callback(self, event, x, y, flags, param):
        """
        OpenCV mouse callback for the SAR window. Processes mouse movement,
        performs rate limiting (using AppState), clamps coordinates,
        and puts raw (x, y) onto the application's mouse_queue.
        """
        # Process only mouse movement events
        if event == cv2.EVENT_MOUSEMOVE:
            # DEBUG for callback event entry
            # Make this DEBUG -1 to avoid excessive logging
            # logging.debug(
            #    f"{self.log_prefix} Mouse callback triggered: event={event}, x={x}, y={y}"
            # )

            # --- Rate Limiting (Using AppState) ---
            current_time = time.time()
            # Use a short timeout (e.g., 50ms = 20Hz)
            try:
                # --- CORRECTION HERE: Access state object ---
                last_update_time = self.app.state.last_mouse_update_time
                if (current_time - last_update_time) < 0.05:  # 50ms rate limit
                    # --- END CORRECTION ---
                    # logging.log(
                    #     logging.DEBUG - 1, f"{self.log_prefix} Mouse event rate limited."
                    # )  # Lower level debug - too verbose
                    return

                # --- CORRECTION HERE: Update state object ---
                self.app.state.last_mouse_update_time = current_time
                # --- END CORRECTION ---

            except AttributeError:
                # Log warning if state or attribute is missing (shouldn't happen normally)
                logging.warning(
                    f"{self.log_prefix} App state or 'last_mouse_update_time' attribute missing in mouse callback."
                )
                # Decide whether to continue without rate limiting or return
                return  # Return if rate limiting fails critically

            # Clamp coordinates and put on queue
            try:
                # Access display dimensions safely from app state
                current_display_width = self.app.state.sar_display_width
                current_display_height = self.app.state.sar_display_height

                if current_display_width <= 0 or current_display_height <= 0:
                    logging.warning(
                        f"{self.log_prefix} Invalid SAR display dimensions ({current_display_width}x{current_display_height}) in mouse callback."
                    )
                    return

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

                # DEBUG for putting coordinates on queue
                logging.debug(
                    f"{self.log_prefix} Putting clamped coords ({x_clamped}, {y_clamped}) onto mouse_queue."
                )
                # Put raw (x, y) on the queue for App.process_mouse_queue
                # Use the imported put_queue function
                put_queue(
                    self.app.mouse_queue,
                    (x_clamped, y_clamped),
                    queue_name="mouse",
                    app_instance=self.app,
                )

            except AttributeError as ae:
                # WARNING if app attributes are missing (redundant due to state check above?)
                logging.warning(
                    f"{self.log_prefix} App object or state attributes not available in mouse callback: {ae}"
                )
            except Exception as e:
                # Keep EXCEPTION for unexpected errors in callback
                logging.exception(f"{self.log_prefix} Error in SAR mouse callback: {e}")

    def update_sar_position(self, x, y):
        """Updates the stored position and attempts to move the SAR window."""
        # DEBUG for position update request
        logging.debug(f"{self.log_prefix} Updating SAR window position to ({x}, {y}).")
        self.sar_x = x
        self.sar_y = y
        if self.sar_window_initialized:
            try:
                cv2.moveWindow(self.sar_window_name, self.sar_x, self.sar_y)
                # INFO for successful move
                logging.info(
                    f"{self.log_prefix} Moved SAR window '{self.sar_window_name}' to ({x}, {y})"
                )
            except cv2.error as e:
                # WARNING if move fails
                logging.warning(
                    f"{self.log_prefix} Could not move SAR window '{self.sar_window_name}': {e}"
                )
        else:
            logging.debug(
                f"{self.log_prefix} SAR window not initialized, cannot move yet."
            )

    def update_mfd_position(self, x, y):
        """Updates the stored position and attempts to move the MFD window."""
        # DEBUG for position update request
        logging.debug(f"{self.log_prefix} Updating MFD window position to ({x}, {y}).")
        self.mfd_x = x
        self.mfd_y = y
        if self.mfd_window_initialized:
            try:
                cv2.moveWindow(self.mfd_window_name, self.mfd_x, self.mfd_y)
                # INFO for successful move
                logging.info(
                    f"{self.log_prefix} Moved MFD window '{self.mfd_window_name}' to ({x}, {y})"
                )
            except cv2.error as e:
                # WARNING if move fails
                logging.warning(
                    f"{self.log_prefix} Could not move MFD window '{self.mfd_window_name}': {e}"
                )
        else:
            logging.debug(
                f"{self.log_prefix} MFD window not initialized, cannot move yet."
            )

    def destroy_windows(self):
        """Explicitly destroys the managed OpenCV windows during application shutdown."""
        # INFO for start of significant cleanup action
        logging.info(
            f"{self.log_prefix} Attempting to destroy managed OpenCV windows (MFD, SAR)..."
        )
        destroyed_count = 0

        # Attempt to destroy SAR window
        # Check name existence as fallback if init flag is somehow false
        if self.sar_window_initialized or hasattr(self, "sar_window_name"):
            try:
                # DEBUG for specific window destruction
                logging.debug(
                    f"{self.log_prefix} Destroying OpenCV window: '{self.sar_window_name}'"
                )
                cv2.destroyWindow(self.sar_window_name)
                destroyed_count += 1
                self.sar_window_initialized = False  # Reset flags
                self.sar_mouse_callback_set = False
            except cv2.error as e:
                # WARNING for known error if window already closed
                logging.warning(
                    f"{self.log_prefix} Ignoring OpenCV error destroying window '{self.sar_window_name}' (may already be closed): {e}"
                )
            except Exception as e:
                # Keep EXCEPTION for unexpected errors
                logging.exception(
                    f"{self.log_prefix} Unexpected error destroying window '{self.sar_window_name}': {e}"
                )

        # Attempt to destroy MFD window
        if self.mfd_window_initialized or hasattr(self, "mfd_window_name"):
            try:
                # DEBUG for specific window destruction
                logging.debug(
                    f"{self.log_prefix} Destroying OpenCV window: '{self.mfd_window_name}'"
                )
                cv2.destroyWindow(self.mfd_window_name)
                destroyed_count += 1
                self.mfd_window_initialized = False  # Reset flag
            except cv2.error as e:
                # WARNING for known error
                logging.warning(
                    f"{self.log_prefix} Ignoring OpenCV error destroying window '{self.mfd_window_name}' (may already be closed): {e}"
                )
            except Exception as e:
                # Keep EXCEPTION for unexpected errors
                logging.exception(
                    f"{self.log_prefix} Unexpected error destroying window '{self.mfd_window_name}': {e}"
                )

        # INFO for completion of cleanup action
        logging.info(
            f"{self.log_prefix} Finished destroying managed OpenCV windows (attempted {destroyed_count})."
        )


# --- END OF FILE display.py ---
