        target_min: Minimum output value (inclusive).
        target_max: Maximum output value (inclusive).
        target_type: NumPy dtype for output array (default: np.uint8).

    Returns:
        Normalized NumPy array of dtype `target_type`, or None on error.
    """
"""Image processing helpers copied into VideoReceiverSFP for standalone
operation. Implements the same behavior used by ControlPanel so output
images are visually identical.

Provides:
- normalize_image: uses OpenCV `cv2.normalize` with dtype handling
- create_brightness_contrast_lut
- apply_brightness_contrast
- apply_color_palette: wraps `cv2.applyColorMap`
- resize_image: thin wrapper over `cv2.resize`
"""
from typing import Optional
import logging
import numpy as np

try:
    import cv2
except Exception:
    cv2 = None  # type: ignore


def create_brightness_contrast_lut(brightness: int = 0, contrast: float = 1.0) -> np.ndarray:
    """Create a 256-element uint8 LUT equivalent to ControlPanel's.

    Args:
        brightness: int (-255..255)
        contrast: float (>0)
    Returns:
        numpy.ndarray shape (256,) dtype uint8
    """
    log_prefix = "[VRSFP B/C LUT]"
    logging.getLogger().debug(f"{log_prefix} Creating B/C LUT B={brightness} C={contrast}")
    contrast = max(0.01, float(contrast))
    lut_values = np.arange(256, dtype=np.float32)
    adjusted = (lut_values * contrast) + float(brightness)
    lut = np.clip(np.round(adjusted), 0, 255).astype(np.uint8)
    return lut


def apply_brightness_contrast(image: np.ndarray, brightness: int = 0, contrast: float = 1.0) -> np.ndarray:
    """Apply B/C using a LUT (expects uint8 input). Returns image or original on error."""
    log_prefix = "[VRSFP B/C Apply]"
    if image is None or not hasattr(image, 'dtype'):
        logging.getLogger().warning(f"{log_prefix} invalid image")
        return image
    if image.dtype != np.uint8:
        logging.getLogger().warning(f"{log_prefix} expected uint8, got {image.dtype}")
        return image
    try:
        lut = create_brightness_contrast_lut(brightness, contrast)
        if cv2 is not None:
            return cv2.LUT(image, lut)
        else:
            return image
    except Exception:
        logging.getLogger().exception(f"{log_prefix} failed to apply B/C")
        return image


def apply_color_palette(image: np.ndarray, palette: str) -> np.ndarray:
    """Apply an OpenCV colormap by name (returns BGR image).

    palette should be names like 'JET', 'HOT', etc. Case-insensitive.
    """
    log_prefix = "[VRSFP Palette]"
    if image is None or cv2 is None:
        logging.getLogger().warning(f"{log_prefix} cv2 not available or image is None")
        return image
    if image.dtype != np.uint8:
        logging.getLogger().warning(f"{log_prefix} expected uint8 image for palette application")
        try:
            img_gray = image.astype(np.uint8)
        except Exception:
            return image
    else:
        img_gray = image

    try:
        colormap_name = f"COLORMAP_{palette.upper()}"
        colormap = getattr(cv2, colormap_name)
        colorized = cv2.applyColorMap(img_gray, colormap)
        return colorized
    except AttributeError:
        logging.getLogger().error(f"{log_prefix} palette {palette} not found; returning BGR grayscale")
        return cv2.cvtColor(img_gray, cv2.COLOR_GRAY2BGR)
    except Exception:
        logging.getLogger().exception(f"{log_prefix} error applying palette")
        return cv2.cvtColor(img_gray, cv2.COLOR_GRAY2BGR)


def normalize_image(image: Optional[np.ndarray], target_min: int = 0, target_max: int = 255, target_type=np.uint8) -> Optional[np.ndarray]:
    """Normalize image using OpenCV to match ControlPanel behavior.

    Uses cv2.normalize with dtype mapping and final cast to `target_type`.
    """
    log_prefix = "[VRSFP Normalize]"
    logging.getLogger().debug(f"{log_prefix} Normalizing image (dtype={getattr(image,'dtype',None)}) to {target_type}")
    if image is None or getattr(image, 'size', 0) == 0:
        logging.getLogger().warning(f"{log_prefix} called with None or empty image")
        return None
    try:
        # decide cv dtype
        if target_type == np.uint8:
            cv_dtype = cv2.CV_8U if cv2 is not None else -1
        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:
            cv_dtype = -1

        if cv2 is not None:
            normalized = cv2.normalize(src=image, dst=None, alpha=target_min, beta=target_max, norm_type=cv2.NORM_MINMAX, dtype=cv_dtype)
        else:
            # fallback to simple numpy minmax
            arr = np.asarray(image)
            vmin = float(np.min(arr))
            vmax = float(np.max(arr))
            if vmin == vmax:
                return np.full(arr.shape, target_min, dtype=target_type)
            scale = (target_max - target_min) / (vmax - vmin)
            out = (arr.astype(np.float64) - vmin) * scale + target_min
            normalized = np.clip(out, target_min, target_max)

        if normalized.dtype != target_type:
            try:
                normalized = normalized.astype(target_type)
            except Exception:
                logging.getLogger().exception(f"{log_prefix} failed casting to target_type")
                return image
        return normalized
    except Exception:
        logging.getLogger().exception(f"{log_prefix} unexpected error")
        return image


def resize_image(image: np.ndarray, width: int, height: int, interpolation=None) -> Optional[np.ndarray]:
    if interpolation is None:
        interpolation = cv2.INTER_LINEAR if cv2 is not None else None
    if image is None or width <= 0 or height <= 0:
        return image
    try:
        if cv2 is not None:
            return cv2.resize(image, (width, height), interpolation=interpolation)
        else:
            return image
    except Exception:
        logging.getLogger().exception('[VRSFP Resize] failed')
        return None
