# --- START OF FILE app_state.py ---

# app_state.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.

Centralized state management for the Control Panel application.

Holds shared application state, including SAR/MFD parameters, georeferencing info,
current image data, LUTs, test mode status, statistics, SAR overlay shift,
map display state, and cached data for efficient map recomposition. Provides methods
for controlled updates where necessary.
"""

# Standard library imports
import logging
import threading
import time
import math
from typing import Optional, Dict, Any, Tuple, List

# Third-party imports
import numpy as np

# Try to import PIL for type hinting the image attributes
try:
    from PIL import Image

    # Define ImageType for convenience if PIL is available
    ImageType = Image.Image
except ImportError:
    # Define ImageType as None if PIL is not available, for type hinting compatibility
    ImageType = None  # type: ignore

# Local application imports
from controlpanel import config  # For default values


class AppState:
    """Class to hold and manage the application's shared state."""

    def __init__(self):
        """Initializes the application state with default values."""
        log_prefix = "[AppState Init]"
        logging.debug(f"{log_prefix} Initializing application state...")

        # --- General Application State ---
        self.shutting_down: bool = False

        # --- SAR Display Parameters ---
        self.sar_contrast: float = config.DEFAULT_SAR_CONTRAST
        self.sar_brightness: int = config.DEFAULT_SAR_BRIGHTNESS
        self.sar_palette: str = config.DEFAULT_SAR_PALETTE
        self.sar_display_width: int = config.INITIAL_SAR_WIDTH
        self.sar_display_height: int = config.INITIAL_SAR_HEIGHT

        # --- SAR Recording State ---
        self.sar_recording_enabled: bool = config.DEFAULT_SAR_RECORDING_ENABLED

        # --- SAR Data & Geo-referencing ---
        # Note: current_sar_normalized is for DISPLAY. Recorder needs RAW data.
        self.current_sar_normalized: Optional[np.ndarray] = np.zeros(
            (config.SAR_HEIGHT, config.SAR_WIDTH), dtype=np.uint8
        )
        # Stores the latest received GeoInfo (reference point, scale, orientation) in RADIANS
        self.current_sar_geo_info: Dict[str, Any] = self._initialize_geo_info()
        # Brightness/Contrast Look-Up Table for SAR display processing
        self.brightness_contrast_lut: Optional[np.ndarray] = np.arange(
            256, dtype=np.uint8
        )
        # --- SAR Overlay Shift State ---
        # User-defined shift (in degrees) to apply to SAR overlay position on the map
        self.sar_lat_shift_deg: float = 0.0  # Latitude shift
        self.sar_lon_shift_deg: float = 0.0  # Longitude shift

        # --- SAR Overlay Cache for Recomposition ---
        # Last SAR image after B/C, Palette (uint8 BGR or GRAY), before warping.
        # Used for fast recomposition when only alpha changes.
        self.last_processed_sar_for_overlay: Optional[np.ndarray] = None
        # Last calculated perspective transformation matrix for the SAR overlay
        self.last_sar_warp_matrix: Optional[np.ndarray] = None
        # Last calculated SAR corner pixel coordinates on the *stitched* map image
        self.last_sar_corners_pixels_map: Optional[List[Tuple[int, int]]] = None

        # --- MFD Display Parameters & Data ---
        # Dictionary holding MFD category definitions (colors, intensities, pixel indices)
        self.mfd_params: Dict[str, Any] = self._initialize_mfd_params()
        # MFD Look-Up Table (maps index values to BGR colors)
        self.mfd_lut: np.ndarray = np.zeros((256, 3), dtype=np.uint8)
        # Current MFD image represented as an array of index values (uint8)
        self.current_mfd_indices: Optional[np.ndarray] = np.zeros(
            (config.MFD_HEIGHT, config.MFD_WIDTH), dtype=np.uint8
        )

        # --- Test Mode State ---
        # Flag indicating if test mode is active
        self.test_mode_active: bool = config.ENABLE_TEST_MODE
        # Pre-generated MFD index image used in test mode
        self.test_mfd_image_indices: Optional[np.ndarray] = None
        # Pre-generated raw SAR image data used in test mode
        self.test_sar_image_raw: Optional[np.ndarray] = None
        # MFD index data loaded for local mode (if USE_LOCAL_IMAGES is True)
        self.local_mfd_image_data_indices: Optional[np.ndarray] = None
        # Raw SAR data loaded for local mode (if USE_LOCAL_IMAGES is True)
        self.local_sar_image_data_raw: Optional[np.ndarray] = None

        # --- Network State ---
        # (Potentially add network status indicators here if needed)

        # --- Performance / Statistics ---
        self._stats_lock = threading.Lock()  # Lock for thread-safe access to statistics
        self.mfd_fps: float = 0.0  # Calculated MFD frames per second
        self.sar_fps: float = 0.0  # Calculated SAR frames per second
        self.mfd_frame_count: int = 0  # Frame counter for MFD FPS calculation
        self.sar_frame_count: int = 0  # Frame counter for SAR FPS calculation
        self.mfd_start_time: float = time.time()  # Start time for MFD FPS interval
        self.sar_update_time: float = (
            time.time()
        )  # Last update time for SAR FPS calculation
        self._dropped_sar_queue: int = 0  # Counter for dropped SAR display queue items
        self._dropped_mfd_queue: int = 0  # Counter for dropped MFD display queue items
        self._dropped_tkinter_queue: int = 0  # Counter for dropped Tkinter queue items
        self._dropped_mouse_queue: int = 0  # Counter for dropped raw mouse queue items
        self._incomplete_sar_rx: int = 0  # Counter for incomplete SAR image receptions
        self._incomplete_mfd_rx: int = 0  # Counter for incomplete MFD image receptions

        # --- Map Related State ---
        # Scaling factor applied to the displayed map window relative to stitched image size
        self.map_display_scale_factor: float = self._parse_map_scale_factor(
            config.DEFAULT_MAP_SIZE
        )
        # Last downloaded and stitched BASE map image (PIL format, before overlay/scalebar)
        self.last_map_image_pil: Optional[ImageType] = None
        # Flag indicating if SAR overlay should be drawn on the map
        self.map_sar_overlay_enabled: bool = config.DEFAULT_MAP_SAR_OVERLAY_ENABLED
        # Alpha transparency value for the SAR overlay (0.0=transparent, 1.0=opaque)
        self.map_sar_overlay_alpha: float = max(
            0.0, min(1.0, config.DEFAULT_MAP_SAR_OVERLAY_ALPHA)
        )
        # Last displayed map image *after* composition (including overlay, scale bar, etc.)
        # Used for the "Save Map View" feature.
        self.last_composed_map_pil: Optional[ImageType] = None
        # Information about the currently displayed map, needed for geo coordinate conversion
        # Updated by MapIntegrationManager whenever the map view changes.
        self.map_current_bounds_deg: Optional[Tuple[float, float, float, float]] = (
            None  # (west, south, east, north)
        )
        self.map_current_zoom: Optional[int] = None  # Zoom level
        self.map_current_shape_px: Optional[Tuple[int, int]] = (
            None  # (height, width) of displayed map window
        )

        # --- UI Related State ---
        # Last time the SAR mouse coordinates were processed (for rate limiting)
        self.last_mouse_update_time: float = 0.0
        # Last time the Map mouse coordinates were processed (for rate limiting)
        self.last_map_mouse_update_time: float = 0.0

        # Last click pixel coordinates on the displayed SAR window (x, y)
        self.last_sar_click_coords: Optional[Tuple[int, int]] = None
        # Last click pixel coordinates on the displayed Map window (x, y)
        self.last_map_click_coords: Optional[Tuple[int, int]] = None

        # Flag to control SAR metadata display in UI
        self.display_sar_metadata: bool = False
        # Last raw SAR metadata string received for display
        self.last_sar_metadata_str: Optional[str] = None

        logging.debug(f"{log_prefix} Application state initialized.")

    # --- Helper Methods for Initialization ---
    def _parse_map_scale_factor(self, factor_str: str) -> float:
        """
        Parses a map scale factor string (e.g., "N:1", "1:N") into a float multiplier.
        Returns 1.0 on error.
        """
        log_prefix = "[AppState Helper]"
        try:
            if not isinstance(factor_str, str) or ":" not in factor_str:
                raise ValueError("Invalid format")
            parts = factor_str.split(":")
            val1_str, val2_str = parts[0].strip(), parts[1].strip()
            if len(parts) != 2:
                raise ValueError("Invalid parts")
            val1 = int(val1_str)
            val2 = int(val2_str)
            if val1 <= 0 or val2 <= 0:
                raise ValueError("Values must be positive")
            if val1 == 1 and val2 == 1:
                scale = 1.0
            elif val2 == 1:
                scale = float(val1)
            elif val1 == 1:
                scale = 1.0 / float(val2)
            else:
                raise ValueError("Invalid format combination")
            logging.debug(
                f"{log_prefix} Parsed map scale factor '{factor_str}' to {scale:.4f}"
            )
            return scale
        except Exception as e:
            logging.error(
                f"{log_prefix} Error parsing map scale factor '{factor_str}': {e}. Using 1.0."
            )
            return 1.0

    def _initialize_mfd_params(self) -> Dict[str, Any]:
        """Initializes the dictionary containing MFD category parameters based on config."""
        log_prefix = "[AppState MFD Params]"
        logging.debug(f"{log_prefix} Initializing MFD parameter dictionary.")
        params = {
            "categories": {  # BGR colors
                "Occlusion": {
                    "color": (0, 0, 0),
                    "intensity": config.DEFAULT_MFD_INTENSITY,
                    "pixels": [0, 1],
                },
                "Cat A": {
                    "color": (255, 255, 255),
                    "intensity": config.DEFAULT_MFD_INTENSITY,
                    "pixels": [2],
                },
                "Cat B": {
                    "color": (255, 255, 255),
                    "intensity": config.DEFAULT_MFD_INTENSITY,
                    "pixels": [3, 18],
                },
                "Cat C": {
                    "color": (255, 255, 255),
                    "intensity": config.DEFAULT_MFD_INTENSITY,
                    "pixels": [4, 5, 6, 16],
                },
                "Cat C1": {
                    "color": (255, 255, 255),
                    "intensity": config.DEFAULT_MFD_INTENSITY,
                    "pixels": [7, 8, 9],
                },
                "Cat C2": {
                    "color": (255, 255, 255),
                    "intensity": config.DEFAULT_MFD_INTENSITY,
                    "pixels": [10, 11, 12],
                },
                "Cat C3": {
                    "color": (255, 255, 255),
                    "intensity": config.DEFAULT_MFD_INTENSITY,
                    "pixels": [13, 14, 15],
                },
                "Reserved": {
                    "color": (0, 0, 0),
                    "intensity": config.DEFAULT_MFD_INTENSITY,
                    "pixels": list(range(17, 32)),
                },
            },
            "raw_map_intensity": config.DEFAULT_MFD_RAW_MAP_INTENSITY,
        }
        # Create reverse mapping for LUT generation
        params["pixel_to_category"] = {
            p: cat for cat, data in params["categories"].items() for p in data["pixels"]
        }
        logging.debug(f"{log_prefix} MFD parameters initialized.")
        return params

    def _initialize_geo_info(self) -> Dict[str, Any]:
        """Initializes the default SAR GeoInfo dictionary based on config values (angles in RADIANS)."""
        log_prefix = "[AppState Geo Init]"
        logging.debug(f"{log_prefix} Initializing default SAR GeoInfo.")
        try:
            w, h = config.SAR_WIDTH, config.SAR_HEIGHT
            lat_deg, lon_deg = config.SAR_CENTER_LAT, config.SAR_CENTER_LON
            size_km = config.SAR_IMAGE_SIZE_KM
            scale_x = (size_km * 1000.0) / w if w > 0 else 0.0
            scale_y = (size_km * 1000.0) / h if h > 0 else 0.0
            geo_info = {
                "lat": math.radians(lat_deg),  # Latitude in RADIANS
                "lon": math.radians(lon_deg),  # Longitude in RADIANS
                "ref_x": w // 2 if w > 0 else 0,  # Reference pixel X coordinate
                "ref_y": h // 2 if h > 0 else 0,  # Reference pixel Y coordinate
                "scale_x": scale_x,  # Meters per pixel along X-axis
                "scale_y": scale_y,  # Meters per pixel along Y-axis
                "width_px": w,  # Full image width in pixels
                "height_px": h,  # Full image height in pixels
                "orientation": 0.0,  # Angle in RADIANS (0=North up)
                "valid": False,  # Initially invalid until real data arrives
            }
            logging.debug(f"{log_prefix} Default SAR GeoInfo initialized.")
            return geo_info
        except Exception as e:
            logging.exception(f"{log_prefix} Error initializing default GeoInfo:")
            # Return a minimal invalid structure on error
            return {
                "lat": 0.0,
                "lon": 0.0,
                "ref_x": 0,
                "ref_y": 0,
                "scale_x": 0.0,
                "scale_y": 0.0,
                "width_px": 0,
                "height_px": 0,
                "orientation": 0.0,
                "valid": False,
            }

    # --- Methods for Safe Stat Updates/Reads ---
    def increment_dropped_count(self, queue_name: str):
        """Safely increments the drop counter for the specified queue name (thread-safe)."""
        with self._stats_lock:
            if queue_name == "sar":
                self._dropped_sar_queue += 1
            elif queue_name == "mfd":
                self._dropped_mfd_queue += 1
            elif queue_name == "tkinter":
                self._dropped_tkinter_queue += 1
            elif queue_name == "mouse":
                self._dropped_mouse_queue += 1
            # else: logging.warning(...) # Optional: Log unknown queue

    def increment_incomplete_rx_count(self, image_type: str):
        """Safely increments the counter for incomplete image receptions (thread-safe)."""
        with self._stats_lock:
            if image_type == "sar":
                self._incomplete_sar_rx += 1
            elif image_type == "mfd":
                self._incomplete_mfd_rx += 1
            # else: logging.warning(...) # Optional: Log unknown type

    def get_statistics(self) -> Dict[str, int]:
        """Returns a dictionary containing current statistic counters (thread-safe read)."""
        with self._stats_lock:
            # Create a copy of the current stats
            stats = {
                "dropped_sar_q": self._dropped_sar_queue,
                "dropped_mfd_q": self._dropped_mfd_queue,
                "dropped_tk_q": self._dropped_tkinter_queue,
                "dropped_mouse_q": self._dropped_mouse_queue,
                "incomplete_sar_rx": self._incomplete_sar_rx,
                "incomplete_mfd_rx": self._incomplete_mfd_rx,
            }
        return stats

    def reset_statistics(self):
        """Resets all statistic counters and FPS calculation variables (thread-safe)."""
        log_prefix = "[AppState Stats]"
        logging.info(f"{log_prefix} Resetting statistic counters.")
        with self._stats_lock:
            self._dropped_sar_queue = 0
            self._dropped_mfd_queue = 0
            self._dropped_tkinter_queue = 0
            self._dropped_mouse_queue = 0
            self._incomplete_sar_rx = 0
            self._incomplete_mfd_rx = 0
            self.mfd_fps = 0.0
            self.sar_fps = 0.0
            self.mfd_frame_count = 0
            self.sar_frame_count = 0
            self.mfd_start_time = time.time()
            self.sar_update_time = time.time()
        logging.debug(f"{log_prefix} Statistic counters reset.")

    # --- Methods for state updates ---
    def update_map_scale_factor(self, factor_str: str):
        """Updates the map display scale factor based on a string representation."""
        log_prefix = "[AppState Update]"
        try:
            new_scale = self._parse_map_scale_factor(factor_str)
            if abs(new_scale - self.map_display_scale_factor) > 1e-6:
                self.map_display_scale_factor = new_scale
                logging.info(
                    f"{log_prefix} Map display scale factor updated to {new_scale:.4f}"
                )
            # else: logging.debug(...) # Reduce verbosity
        except Exception as e:
            logging.error(
                f"{log_prefix} Failed to update map scale factor from '{factor_str}': {e}"
            )

    def update_map_overlay_params(
        self, enabled: Optional[bool] = None, alpha: Optional[float] = None
    ):
        """Updates the SAR overlay enabled state and/or alpha value. Clamps alpha [0.0, 1.0]."""
        log_prefix = "[AppState Update]"
        updated = False
        if enabled is not None and enabled != self.map_sar_overlay_enabled:
            self.map_sar_overlay_enabled = enabled
            logging.info(
                f"{log_prefix} Map SAR overlay enabled state updated to: {enabled}"
            )
            updated = True
        if alpha is not None:
            alpha_clamped = max(0.0, min(1.0, alpha))
            if abs(alpha_clamped - self.map_sar_overlay_alpha) > 1e-6:
                self.map_sar_overlay_alpha = alpha_clamped
                logging.info(
                    f"{log_prefix} Map SAR overlay alpha updated to: {alpha_clamped:.3f}"
                )
                updated = True
            elif alpha != alpha_clamped:
                logging.warning(
                    f"{log_prefix} Provided alpha value {alpha:.3f} clamped to {alpha_clamped:.3f}"
                )
        # if not updated: logging.debug(...) # Reduce verbosity

    def update_sar_overlay_shift(self, lat_shift_deg: float, lon_shift_deg: float):
        """Updates the latitude and longitude shift values for the SAR overlay."""
        log_prefix = "[AppState Update]"
        updated = False
        if (
            math.isfinite(lat_shift_deg)
            and abs(lat_shift_deg - self.sar_lat_shift_deg) > 1e-9
        ):
            self.sar_lat_shift_deg = lat_shift_deg
            logging.info(
                f"{log_prefix} SAR overlay Latitude shift updated to: {lat_shift_deg:.6f} degrees."
            )
            updated = True
        elif not math.isfinite(lat_shift_deg):
            logging.warning(f"{log_prefix} Invalid latitude shift: {lat_shift_deg}.")
        if (
            math.isfinite(lon_shift_deg)
            and abs(lon_shift_deg - self.sar_lon_shift_deg) > 1e-9
        ):
            self.sar_lon_shift_deg = lon_shift_deg
            logging.info(
                f"{log_prefix} SAR overlay Longitude shift updated to: {lon_shift_deg:.6f} degrees."
            )
            updated = True
        elif not math.isfinite(lon_shift_deg):
            logging.warning(f"{log_prefix} Invalid longitude shift: {lon_shift_deg}.")
        # if not updated: logging.debug(...) # Reduce verbosity

    def update_sar_recording_enabled(self, enabled: bool):
        """Safely updates the SAR recording enabled flag."""
        log_prefix = "[AppState Update]"
        if enabled != self.sar_recording_enabled:
            self.sar_recording_enabled = enabled
            logging.info(
                f"{log_prefix} SAR recording enabled state updated to: {enabled}"
            )
        # else: logging.debug(...) # Reduce verbosity

    def update_sar_parameters(
        self,
        contrast: Optional[float] = None,
        brightness: Optional[int] = None,
        palette: Optional[str] = None,
    ):
        """Updates SAR display parameters (contrast, brightness, palette) if changed."""
        log_prefix = "[AppState Update]"
        updated = False
        if contrast is not None and abs(contrast - self.sar_contrast) > 1e-6:
            self.sar_contrast = contrast
            updated = True
        if brightness is not None and brightness != self.sar_brightness:
            self.sar_brightness = brightness
            updated = True
        if palette is not None and palette != self.sar_palette:
            self.sar_palette = palette
            updated = True
        # if updated: logging.debug(...) # Reduce verbosity

    def update_sar_display_size(self, width: int, height: int):
        """Updates the target display size for the SAR image. Validates input."""
        log_prefix = "[AppState Update]"
        if width > 0 and height > 0:
            if width != self.sar_display_width or height != self.sar_display_height:
                self.sar_display_width = width
                self.sar_display_height = height
                logging.info(
                    f"{log_prefix} SAR display size updated to {width}x{height}"
                )
            # else: logging.debug(...) # Reduce verbosity
        else:
            logging.warning(
                f"{log_prefix} Attempted invalid SAR display size: {width}x{height}"
            )

    def set_sar_data(
        self, normalized_image: Optional[np.ndarray], geo_info: Optional[Dict[str, Any]]
    ):
        """Updates the current normalized SAR image and its associated GeoInfo."""
        log_prefix = "[AppState Update]"
        # Update image if valid
        if (
            normalized_image is not None
            and isinstance(normalized_image, np.ndarray)
            and normalized_image.size > 0
        ):
            self.current_sar_normalized = normalized_image
            # logging.debug(...) # Reduce verbosity
        else:
            logging.warning(f"{log_prefix} Invalid normalized SAR image provided.")
        # Update GeoInfo if valid
        if geo_info is not None and isinstance(geo_info, dict):
            self.current_sar_geo_info = geo_info
            # logging.debug(...) # Reduce verbosity
        else:
            logging.warning(f"{log_prefix} Invalid geo_info provided.")
            self.current_sar_geo_info = self._initialize_geo_info()  # Reset to default

    def set_mfd_indices(self, indices_image: Optional[np.ndarray]):
        """Updates the current MFD index image."""
        log_prefix = "[AppState Update]"
        if (
            indices_image is not None
            and isinstance(indices_image, np.ndarray)
            and indices_image.size > 0
        ):
            self.current_mfd_indices = indices_image
            # logging.debug(...) # Reduce verbosity
        else:
            logging.warning(f"{log_prefix} Invalid MFD indices image provided.")


# --- END OF FILE app_state.py ---
