"""Base classes for the postprocessing filter system."""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Dict, List, Optional

import numpy as np

if TYPE_CHECKING:
    from wled_controller.core.filters.image_pool import ImagePool


@dataclass
class FilterOptionDef:
    """Describes a single configurable option for a filter."""

    key: str
    label: str
    option_type: str  # "float" | "int" | "bool" | "select" | "string"
    default: Any
    min_value: Any
    max_value: Any
    step: Any
    choices: Optional[List[Dict[str, str]]] = None  # for "select": [{value, label}]
    max_length: Optional[int] = None  # for "string" type

    def to_dict(self) -> dict:
        d = {
            "key": self.key,
            "label": self.label,
            "type": self.option_type,
            "default": self.default,
            "min_value": self.min_value,
            "max_value": self.max_value,
            "step": self.step,
        }
        if self.choices is not None:
            d["choices"] = self.choices
        if self.max_length is not None:
            d["max_length"] = self.max_length
        return d


class PostprocessingFilter(ABC):
    """Base class for all postprocessing filters.

    Each filter operates on a full image (np.ndarray H×W×3 uint8).
    Filters that preserve dimensions modify in-place and return None.
    Filters that change dimensions return a new array from the image pool.

    Filters that also support 1D LED strip arrays (N×3 uint8) should
    leave ``supports_strip = True`` (the default).  The base class
    provides a generic ``process_strip`` that reshapes (N,3) → (1,N,3),
    delegates to ``process_image``, and reshapes back.  Subclasses may
    override for a more efficient implementation.

    Filters that are purely spatial (auto-crop, downscaler, flip) should
    set ``supports_strip = False``.
    """

    filter_id: str = ""
    filter_name: str = ""
    supports_idle_frames: bool = False
    supports_strip: bool = True

    def __init__(self, options: Dict[str, Any]):
        """Initialize filter with validated options."""
        self.options = self.validate_options(options)

    @classmethod
    @abstractmethod
    def get_options_schema(cls) -> List[FilterOptionDef]:
        """Return the list of configurable options for this filter type."""
        ...

    @abstractmethod
    def process_image(self, image: np.ndarray, image_pool: "ImagePool") -> Optional[np.ndarray]:
        """Process image.

        Args:
            image: Input image as np.ndarray (H, W, 3) dtype=uint8.
            image_pool: Shared pool for acquiring new arrays when dimensions change.

        Returns:
            None if modified in-place (same dimensions).
            New np.ndarray from image_pool if dimensions changed.
        """
        ...

    def process_strip(self, strip: np.ndarray) -> Optional[np.ndarray]:
        """Process a 1D LED strip array (N, 3) uint8.

        Default implementation reshapes to (1, N, 3), calls process_image
        with a no-op pool, and reshapes back.  Override for filters that
        need strip-specific behaviour or use ImagePool.

        Returns:
            None if modified in-place.
            New np.ndarray if a new array was created.
        """
        from wled_controller.core.filters.image_pool import ImagePool

        img = strip[np.newaxis, :, :]  # (1, N, 3)
        pool = ImagePool(max_size=2)
        result = self.process_image(img, pool)
        if result is not None:
            out = result[0]  # (N, 3)
            pool.release_all()
            return out
        # Modified in-place — extract back
        np.copyto(strip, img[0])
        pool.release_all()
        return None

    @classmethod
    def validate_options(cls, options: dict) -> dict:
        """Validate and clamp options against the schema. Returns cleaned dict."""
        schema = cls.get_options_schema()
        cleaned = {}
        for opt_def in schema:
            raw = options.get(opt_def.key, opt_def.default)
            if opt_def.option_type == "float":
                val = float(raw)
            elif opt_def.option_type == "int":
                val = int(raw)
            elif opt_def.option_type == "bool":
                val = bool(raw) if not isinstance(raw, bool) else raw
            elif opt_def.option_type == "select":
                val = str(raw) if raw is not None else opt_def.default
            elif opt_def.option_type == "string":
                val = str(raw) if raw is not None else opt_def.default
            else:
                val = raw
            # Clamp to range (skip for non-numeric types)
            if opt_def.option_type not in ("bool", "select", "string"):
                if opt_def.min_value is not None and val < opt_def.min_value:
                    val = opt_def.min_value
                if opt_def.max_value is not None and val > opt_def.max_value:
                    val = opt_def.max_value
            cleaned[opt_def.key] = val
        return cleaned

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({self.options})"
