# --- START OF FILE image_processing.py ---

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

Provides utility functions for loading, processing, and manipulating images
using OpenCV and NumPy. Includes normalization, resizing, brightness/contrast
adjustment, and color palette application. Uses standardized logging prefixes.
"""

# Standard library imports
import os
import logging

# Third-party imports
import cv2
import numpy as np

# Local application imports
import config  # Import config for constants if needed (e.g., placeholder sizes)


def load_image(path, expected_dtype):
    """
    Loads an image from the specified path with error handling and type conversion.
    Uses placeholders if loading fails.

    Args:
        path (str): The file path to the image.
        expected_dtype (numpy.dtype): The desired NumPy data type for the output image.

    Returns:
        numpy.ndarray: The loaded (and potentially converted) image, or a placeholder on error.
    """
    log_prefix = "[ImageProcessing Load]"  # Specific prefix for this function
    logging.debug(f"{log_prefix} Attempting to load image from: {path}")

    # Placeholder creation function (internal helper)
    def _create_placeholder(dtype):
        if dtype == np.uint8:
            # Assuming MFD placeholder dimensions if uint8 needed
            return np.zeros((config.MFD_HEIGHT, config.MFD_WIDTH), dtype=np.uint8)
        else:
            # Assuming SAR placeholder dimensions otherwise
            return np.zeros(
                (config.SAR_HEIGHT, config.SAR_WIDTH), dtype=config.SAR_DATA_TYPE
            )

    # Check if file exists
    if not os.path.exists(path):
        # Keep ERROR level as file not found is a significant issue if expected
        logging.error(
            f"{log_prefix} Image file not found at {path}. Using placeholder."
        )
        return _create_placeholder(expected_dtype)

    try:
        # Load image using OpenCV, preserving original depth
        img = cv2.imread(path, cv2.IMREAD_ANYDEPTH)

        if img is None:
            # Keep ERROR level for load failure
            logging.error(
                f"{log_prefix} OpenCV could not load image at {path}. Using placeholder."
            )
            return _create_placeholder(expected_dtype)

        # Log success and original type at DEBUG level
        logging.debug(
            f"{log_prefix} Image loaded successfully from {path}. Original dtype: {img.dtype}"
        )

        # Check and convert dtype if necessary
        if img.dtype != expected_dtype:
            # Use WARNING level for implicit type conversion
            logging.warning(
                f"{log_prefix} Converting image from {img.dtype} to {expected_dtype}."
            )
            try:
                img = img.astype(expected_dtype)
                logging.debug(f"{log_prefix} Image dtype conversion successful.")
            except Exception as e:
                # Keep ERROR if conversion fails
                logging.error(
                    f"{log_prefix} Failed to convert image dtype: {e}. Returning original or placeholder."
                )
                # Return original image if conversion fails but load succeeded,
                # otherwise fallback to placeholder if original img was somehow bad
                return img if img is not None else _create_placeholder(expected_dtype)

        return img

    except Exception as e:
        # Keep EXCEPTION for unexpected errors during loading/processing
        logging.exception(
            f"{log_prefix} Unexpected error loading image from {path}: {e}"
        )
        return _create_placeholder(expected_dtype)


def create_brightness_contrast_lut(brightness=0, contrast=1.0):
    """
    Creates a Look-Up Table (LUT) for adjusting brightness and contrast.

    Args:
        brightness (int): Amount to add to each pixel (-255 to 255).
        contrast (float): Factor to multiply each pixel by (> 0.0).

    Returns:
        numpy.ndarray: A 256-element uint8 LUT.
    """
    log_prefix = "[ImageProcessing LUT]"  # Specific prefix
    # DEBUG level for LUT creation details
    logging.debug(
        f"{log_prefix} Creating B/C LUT with Brightness={brightness}, Contrast={contrast:.2f}"
    )

    # Ensure contrast is slightly positive to avoid issues
    contrast = max(0.01, contrast)

    try:
        # Use vectorized operation for potentially better performance
        lut_values = np.arange(256)  # 0 to 255
        adjusted_values = (lut_values * contrast) + brightness
        # Clip values to 0-255 range and convert to uint8
        lut = np.clip(np.round(adjusted_values), 0, 255).astype(np.uint8)
        logging.debug(f"{log_prefix} B/C LUT created successfully.")
        return lut
    except Exception as e:
        # Keep EXCEPTION for errors during LUT calculation
        logging.exception(f"{log_prefix} Error creating B/C LUT:")
        # Keep ERROR for fallback action
        logging.error(f"{log_prefix} Returning identity LUT as fallback.")
        return np.arange(256, dtype=np.uint8)  # Return identity LUT on error


def apply_brightness_contrast(image, brightness=0, contrast=1.0):
    """
    Applies brightness and contrast adjustment to a uint8 image using a LUT.

    Args:
        image (numpy.ndarray): The input uint8 image.
        brightness (int): Brightness adjustment value.
        contrast (float): Contrast adjustment value.

    Returns:
        numpy.ndarray: The adjusted uint8 image, or original on error.
    """
    log_prefix = "[ImageProcessing B/C Apply]"  # Specific prefix
    # DEBUG for function entry
    logging.debug(
        f"{log_prefix} Applying B/C adjustment (B={brightness}, C={contrast:.2f})"
    )

    # Validate input type - ERROR if invalid type provided
    if image.dtype != np.uint8:
        logging.error(
            f"{log_prefix} apply_brightness_contrast requires uint8 input, got {image.dtype}. Returning original."
        )
        return image

    try:
        # Create the LUT - uses its own logging
        brightness_contrast_lut = create_brightness_contrast_lut(brightness, contrast)
        # Apply the LUT using OpenCV
        logging.debug(f"{log_prefix} Applying precomputed LUT to image...")
        adjusted_image = cv2.LUT(image, brightness_contrast_lut)
        logging.debug(f"{log_prefix} B/C adjustment applied successfully.")
        return adjusted_image
    except cv2.error as e:
        # Keep EXCEPTION for OpenCV errors
        logging.exception(f"{log_prefix} OpenCV error applying B/C LUT: {e}")
        return image  # Return original on error
    except Exception as e:
        # Keep EXCEPTION for unexpected errors
        logging.exception(f"{log_prefix} Unexpected error applying B/C adjustment: {e}")
        return image  # Return original on error


def apply_color_palette(image, palette):
    """
    Applies a named OpenCV colormap to a grayscale image.

    Args:
        image (numpy.ndarray): The input grayscale image (uint8). If BGR, attempts conversion.
        palette (str): The name of the OpenCV colormap (e.g., "JET", "HOT"). Case-insensitive.

    Returns:
        numpy.ndarray: The colorized BGR image, or a BGR version of the input on error.
    """
    log_prefix = "[ImageProcessing Palette]"  # Specific prefix
    # DEBUG for function entry
    logging.debug(f"{log_prefix} Applying color palette '{palette}'")

    # Validate input type - Allow uint8 only, attempt conversion if needed
    if image.dtype != np.uint8:
        # ERROR for unsupported input type
        logging.error(
            f"{log_prefix} apply_color_palette requires uint8 input, got {image.dtype}. Cannot apply palette."
        )
        # Try converting to BGR if grayscale-like, otherwise return original
        if image.ndim == 2:
            try:
                return cv2.cvtColor(
                    image.astype(np.uint8), cv2.COLOR_GRAY2BGR
                )  # Attempt recovery
            except:
                return image
        else:
            return image

    # Ensure input is grayscale for colormap application
    processed_image = image
    if image.ndim > 2:
        # WARNING if input is already color, attempting grayscale conversion
        logging.warning(
            f"{log_prefix} Input image has multiple channels (shape={image.shape}). Converting to grayscale before applying palette."
        )
        try:
            processed_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
            logging.debug(f"{log_prefix} Converted input image to grayscale.")
        except cv2.error as e:
            # ERROR if conversion fails
            logging.error(
                f"{log_prefix} Failed to convert input image to grayscale: {e}. Cannot apply palette."
            )
            return (
                cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) if image.ndim == 2 else image
            )  # Return BGR/Original

    # Apply the colormap
    try:
        colormap_name = f"COLORMAP_{palette.upper()}"
        colormap = getattr(cv2, colormap_name)
        # DEBUG for applying map
        logging.debug(f"{log_prefix} Applying OpenCV colormap: {colormap_name}")

        # Use optimized/vectorized approach unless disabled for debugging
        if config.DISABLE_COLORMAP_OPTIMIZATION:
            # WARNING if using non-optimized path
            logging.warning(
                f"{log_prefix} Using non-vectorized colormap application (debug option enabled)."
            )
            colorized_image = np.zeros(
                (processed_image.shape[0], processed_image.shape[1], 3), dtype=np.uint8
            )
            # This loop is slow, avoid in production
            for i in range(processed_image.shape[0]):
                for j in range(processed_image.shape[1]):
                    pixel_value = np.array([[processed_image[i, j]]], dtype=np.uint8)
                    colored_pixel = cv2.applyColorMap(pixel_value, colormap)
                    colorized_image[i, j, :] = colored_pixel[0, 0, :]
        else:
            # Standard vectorized approach
            colorized_image = cv2.applyColorMap(processed_image, colormap)

        logging.debug(f"{log_prefix} Colormap '{palette}' applied successfully.")
        return colorized_image

    except AttributeError:
        # ERROR if the specified palette doesn't exist in OpenCV
        logging.error(
            f"{log_prefix} OpenCV Colormap '{palette}' not found. Returning BGR grayscale."
        )
        return cv2.cvtColor(
            processed_image, cv2.COLOR_GRAY2BGR
        )  # Return BGR grayscale fallback
    except cv2.error as e:
        # Keep EXCEPTION for OpenCV errors during applyColorMap
        logging.exception(f"{log_prefix} OpenCV error applying colormap: {e}")
        return cv2.cvtColor(processed_image, cv2.COLOR_GRAY2BGR)  # Fallback
    except Exception as e:
        # Keep EXCEPTION for unexpected errors
        logging.exception(f"{log_prefix} Unexpected error applying colormap: {e}")
        return cv2.cvtColor(processed_image, cv2.COLOR_GRAY2BGR)  # Fallback


def normalize_image(image, target_min=0, target_max=255, target_type=np.uint8):
    """
    Normalizes the image pixel values to a specified range and data type using OpenCV.

    Args:
        image (numpy.ndarray): The input image. Can be any NumPy type OpenCV supports.
        target_min (int): The minimum value of the target range (default: 0).
        target_max (int): The maximum value of the target range (default: 255).
        target_type (numpy.dtype): The target NumPy data type (e.g., np.uint8).

    Returns:
        numpy.ndarray: The normalized image, or original image on error.
    """
    log_prefix = "[ImageProcessing Normalize]"  # Specific prefix
    # DEBUG for function entry and parameters
    logging.debug(
        f"{log_prefix} Normalizing image (shape={getattr(image, 'shape', 'N/A')}, dtype={getattr(image, 'dtype', 'N/A')}) "
        f"to range [{target_min}, {target_max}], target type: {target_type.__name__}"
    )

    if image is None or image.size == 0:
        logging.error(
            f"{log_prefix} Cannot normalize None or empty image. Returning None."
        )
        return None

    try:
        # Determine the OpenCV dtype constant based on the target NumPy type
        if target_type == np.uint8:
            cv_dtype = cv2.CV_8U
        elif target_type == np.uint16:
            cv_dtype = cv2.CV_16U
        elif target_type == np.int16:
            cv_dtype = cv2.CV_16S
        elif target_type == np.int32:
            cv_dtype = cv2.CV_32S
        elif target_type == np.float32:
            cv_dtype = cv2.CV_32F
        elif target_type == np.float64:
            cv_dtype = cv2.CV_64F
        else:
            # Use -1 to keep the original depth if target type is not explicitly handled
            # WARNING if using fallback depth
            logging.warning(
                f"{log_prefix} Unsupported target_type {target_type} for explicit OpenCV dtype. "
                f"Using source depth (-1) during normalization."
            )
            cv_dtype = -1  # Keep source depth

        # Perform normalization using OpenCV
        logging.debug(
            f"{log_prefix} Calling cv2.normalize with alpha={target_min}, beta={target_max}, dtype={cv_dtype}..."
        )
        normalized_img = cv2.normalize(
            src=image,
            dst=None,  # Create new destination array
            alpha=target_min,
            beta=target_max,
            norm_type=cv2.NORM_MINMAX,
            dtype=cv_dtype,  # Target OpenCV depth, or -1 for same as source
        )
        logging.debug(
            f"{log_prefix} cv2.normalize completed. Output dtype: {normalized_img.dtype}"
        )

        # Ensure the final NumPy dtype matches the requested target_type exactly
        if normalized_img.dtype != target_type:
            # DEBUG if casting is needed after normalization
            logging.debug(
                f"{log_prefix} Casting normalized image from {normalized_img.dtype} to required {target_type.__name__}..."
            )
            try:
                normalized_img = normalized_img.astype(target_type)
                logging.debug(f"{log_prefix} Casting successful.")
            except Exception as astype_e:
                # ERROR if final casting fails
                logging.error(
                    f"{log_prefix} Failed to cast normalized image to target type {target_type}: {astype_e}. Returning original."
                )
                return image  # Return original on casting error

        logging.debug(
            f"{log_prefix} Normalization successful. Final shape={normalized_img.shape}, dtype={normalized_img.dtype}"
        )
        return normalized_img

    except cv2.error as e:
        # Keep EXCEPTION for OpenCV errors
        logging.exception(f"{log_prefix} OpenCV error during normalization: {e}")
        return image  # Return original on OpenCV error
    except Exception as e:
        # Keep EXCEPTION for unexpected errors
        logging.exception(
            f"{log_prefix} Unexpected error during image normalization: {e}"
        )
        return image  # Return original on other errors


def resize_image(image, width, height, interpolation=cv2.INTER_LINEAR):
    """
    Resizes the image to the specified width and height using OpenCV.

    Args:
        image (numpy.ndarray): The input image.
        width (int): The target width.
        height (int): The target height.
        interpolation (int): OpenCV interpolation flag (default: cv2.INTER_LINEAR).

    Returns:
        numpy.ndarray: The resized image, or original image on error.
    """
    log_prefix = "[ImageProcessing Resize]"  # Specific prefix
    # DEBUG for function entry and parameters
    logging.debug(
        f"{log_prefix} Resizing image (shape={getattr(image, 'shape', 'N/A')}) to {width}x{height} using interpolation {interpolation}"
    )

    # Validate target size - ERROR for invalid size
    if width <= 0 or height <= 0:
        logging.error(
            f"{log_prefix} Invalid target size: {width}x{height}. Returning original."
        )
        return image
    # Validate input image - ERROR for invalid input
    if image is None or image.size == 0:
        logging.error(
            f"{log_prefix} Cannot resize None or empty image. Returning original."
        )
        return image
    # Check if resize is necessary - DEBUG for skipping unnecessary resize
    if image.shape[1] == width and image.shape[0] == height:
        logging.debug(f"{log_prefix} Image is already target size. Skipping resize.")
        return image

    try:
        # Perform resize using OpenCV
        logging.debug(f"{log_prefix} Calling cv2.resize...")
        resized_img = cv2.resize(image, (width, height), interpolation=interpolation)
        logging.debug(f"{log_prefix} Resize successful. New shape: {resized_img.shape}")
        return resized_img
    except cv2.error as e:
        # Keep EXCEPTION for OpenCV errors
        logging.exception(f"{log_prefix} OpenCV error during resize: {e}")
        return image  # Return original on error
    except Exception as e:
        # Keep EXCEPTION for unexpected errors
        logging.exception(f"{log_prefix} Unexpected error during image resize: {e}")
        return image  # Return original on error


# --- END OF FILE image_processing.py ---
