//! Configuration types for the detector pipeline.
//!
//! This module provides two configuration types:
//! - [`DetectorConfig`]: Pipeline-level configuration (immutable after construction)
//! - [`DetectOptions`]: Per-call options (e.g., which tag families to decode)

// ============================================================================
// DetectorConfig: Pipeline-level configuration
// ============================================================================

/// Segmentation connectivity mode.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SegmentationConnectivity {
    /// 4-connectivity: Pixels connect horizontally and vertically only.
    /// Required for separating checkerboard corners.
    Four,
    /// 8-connectivity: Pixels connect horizontally, vertically, and diagonally.
    /// Better for isolated tags with broken borders.
    Eight,
}

/// Mode for subpixel corner refinement.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CornerRefinementMode {
    /// No subpixel refinement (integer pixel precision).
    None,
    /// Edge-based refinement using gradient maxima (Default).
    Edge,
    /// GridFit: Optimizes corners by maximizing code contrast.
    GridFit,
    /// Erf: Fits a Gaussian to the gradient profile for sub-pixel edge alignment.
    Erf,
    /// Gwlf: Gradient-Weighted Line Fitting (PCA on gradients).
    Gwlf,
}

/// Mode for decoding strategy.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DecodeMode {
    /// Hard-decision decoding using Hamming distance (fastest).
    Hard,
    /// Soft-decision decoding using Log-Likelihood Ratios (better for noise/blur).
    Soft,
}

/// Mode for 3D pose estimation quality.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PoseEstimationMode {
    /// Standard IPPE + Levenberg-Marquardt with identity weights (Fast).
    Fast,
    /// Structure Tensor + Weighted Levenberg-Marquardt (Accurate, Slower).
    ///
    /// This models corner uncertainty using image gradients and weights the
    /// PnP optimization to prefer "sharp" directions, significantly improving
    /// accuracy (RMSE) at the cost of ~0.5ms per tag.
    Accurate,
}

/// Quad extraction algorithm.
#[derive(Clone, Copy, Debug, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum QuadExtractionMode {
    /// Legacy contour tracing + Douglas-Peucker + reduce-to-quad (default, backward compatible).
    #[default]
    ContourRdp,
    /// Localized Edge Drawing: anchor routing → line fitting → corner intersection.
    EdLines,
}

/// Pipeline-level configuration for the detector.
///
/// These settings affect the fundamental behavior of the detection pipeline
/// and are immutable after the `Detector` is constructed. Use the builder
/// pattern for ergonomic construction.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DetectorConfig {
    // Threshold parameters
    /// Tile size for adaptive thresholding (default: 4).
    /// Larger tiles are faster but less adaptive to local contrast.
    pub threshold_tile_size: usize,
    /// Minimum intensity range in a tile to be considered valid (default: 10).
    /// Tiles with lower range are treated as uniform (no edges).
    pub threshold_min_range: u8,

    // Adaptive filtering parameters
    /// Enable bilateral pre-filtering for edge-preserving noise reduction (default: true).
    pub enable_bilateral: bool,
    /// Bilateral spatial sigma for spatial smoothing (default: 3.0).
    pub bilateral_sigma_space: f32,
    /// Bilateral color sigma for edge preservation (default: 30.0).
    /// Higher values = more smoothing across edges.
    pub bilateral_sigma_color: f32,

    /// Enable Laplacian sharpening to enhance edges for small tags (default: true).
    pub enable_sharpening: bool,

    /// Enable adaptive threshold window sizing based on gradient (default: true).
    pub enable_adaptive_window: bool,
    /// Minimum threshold window radius for high-gradient regions (default: 2 = 5x5).
    pub threshold_min_radius: usize,
    /// Maximum threshold window radius for low-gradient regions (default: 7 = 15x15).
    pub threshold_max_radius: usize,
    /// Constant subtracted from local mean in adaptive thresholding (default: 3).
    pub adaptive_threshold_constant: i16,
    /// Gradient magnitude threshold above which the minimum window radius is used (default: 40).
    pub adaptive_threshold_gradient_threshold: u8,

    // Quad filtering parameters
    /// Minimum quad area in pixels (default: 16).
    pub quad_min_area: u32,
    /// Maximum aspect ratio of bounding box (default: 3.0).
    pub quad_max_aspect_ratio: f32,
    /// Minimum fill ratio (pixel count / bbox area) (default: 0.3).
    pub quad_min_fill_ratio: f32,
    /// Maximum fill ratio (default: 0.95).
    pub quad_max_fill_ratio: f32,
    /// Minimum edge length in pixels (default: 4.0).
    pub quad_min_edge_length: f64,
    /// Minimum edge alignment score (0.0 to 1.0)
    pub quad_min_edge_score: f64,
    /// PSF blur factor for subpixel refinement (e.g., 0.6)
    pub subpixel_refinement_sigma: f64,
    /// Minimum deviation from threshold for a pixel to be connected in threshold-model CCL (default: 2).
    pub segmentation_margin: i16,
    /// Segmentation connectivity (4-way or 8-way).
    pub segmentation_connectivity: SegmentationConnectivity,
    /// Factor to upscale the image before detection (1 = no upscaling).
    /// Increasing this to 2 allows detecting smaller tags (e.g., < 15px)
    /// at the cost of processing speed (O(N^2)). Nearest-neighbor interpolation is used.
    pub upscale_factor: usize,

    /// Decimation factor for preprocessing (1 = no decimation).
    pub decimation: usize,

    /// Number of threads for parallel processing (0 = auto).
    pub nthreads: usize,

    // Decoder parameters
    /// Minimum contrast range for Otsu-based bit classification (default: 20.0).
    /// For checkerboard patterns with densely packed tags, lower values (e.g., 10.0)
    /// can improve recall on small/blurry tags.
    pub decoder_min_contrast: f64,
    /// Strategy for refining corner positions (default: Edge).
    pub refinement_mode: CornerRefinementMode,
    /// Decoding mode (Hard vs Soft).
    pub decode_mode: DecodeMode,
    /// Maximum number of Hamming errors allowed for tag decoding (default: 2).
    /// Higher values increase recall but also increase false positive rate in noise.
    pub max_hamming_error: u32,

    // Pose estimation tuning parameters
    /// Huber delta for LM reprojection (pixels) in Fast mode.
    /// Residuals beyond this threshold are down-weighted linearly.
    /// 1.5 px is a standard robust threshold for sub-pixel corner detectors.
    pub huber_delta_px: f64,

    /// Maximum Tikhonov regularisation alpha (px^2) for ill-conditioned corners
    /// in Accurate mode. Controls the gain-scheduled regularisation of the
    /// Structure Tensor information matrix on foreshortened tags.
    pub tikhonov_alpha_max: f64,

    /// Pixel noise variance (sigma_n^2) assumed for the Structure Tensor
    /// covariance model in Accurate mode. Typical webcams: ~4.0.
    pub sigma_n_sq: f64,

    /// Radius (in pixels) of the window used for Structure Tensor computation
    /// in Accurate mode. A radius of 2 yields a 5x5 window.
    /// Smaller values (1) are better for small tags; larger (3-4) for noisy images.
    pub structure_tensor_radius: u8,

    /// Alpha parameter for GWLF adaptive transversal windowing.
    /// The search band is set to +/- max(2, alpha * edge_length).
    pub gwlf_transversal_alpha: f64,

    /// Maximum elongation (λ_max / λ_min) allowed for a component before contour tracing.
    /// 0.0 = disabled. Recommended: 15.0 to reject thin lines and non-square blobs.
    pub quad_max_elongation: f64,

    /// Minimum pixel density (pixel_count / bbox_area) required to pass the moments gate.
    /// 0.0 = disabled. Recommended: 0.2 to reject sparse/noisy regions.
    pub quad_min_density: f64,

    /// Quad extraction mode: legacy contour tracing (default) or EDLines.
    pub quad_extraction_mode: QuadExtractionMode,
}

impl Default for DetectorConfig {
    fn default() -> Self {
        Self {
            threshold_tile_size: 8,
            threshold_min_range: 10,
            enable_bilateral: false,
            bilateral_sigma_space: 0.8,
            bilateral_sigma_color: 30.0,
            enable_sharpening: false,
            enable_adaptive_window: false,
            threshold_min_radius: 2,
            threshold_max_radius: 15,
            adaptive_threshold_constant: 0,
            adaptive_threshold_gradient_threshold: 10,
            quad_min_area: 16,
            quad_max_aspect_ratio: 10.0,
            quad_min_fill_ratio: 0.10,
            quad_max_fill_ratio: 0.98,
            quad_min_edge_length: 4.0,
            quad_min_edge_score: 4.0,
            subpixel_refinement_sigma: 0.6,

            segmentation_margin: 1,
            segmentation_connectivity: SegmentationConnectivity::Eight,
            upscale_factor: 1,
            decimation: 1,
            nthreads: 0,
            decoder_min_contrast: 20.0,
            refinement_mode: CornerRefinementMode::Erf,
            decode_mode: DecodeMode::Hard,
            max_hamming_error: 2,
            huber_delta_px: 1.5,
            tikhonov_alpha_max: 0.25,
            sigma_n_sq: 4.0,
            structure_tensor_radius: 2,
            gwlf_transversal_alpha: 0.01,
            quad_max_elongation: 0.0,
            quad_min_density: 0.0,
            quad_extraction_mode: QuadExtractionMode::ContourRdp,
        }
    }
}

impl DetectorConfig {
    /// Create a new builder for `DetectorConfig`.
    #[must_use]
    pub fn builder() -> DetectorConfigBuilder {
        DetectorConfigBuilder::default()
    }

    /// Validate the configuration, returning an error if any parameter is out of range.
    ///
    /// # Errors
    ///
    /// Returns [`ConfigError`] if any parameter violates its constraints.
    pub fn validate(&self) -> Result<(), crate::error::ConfigError> {
        use crate::error::ConfigError;

        if self.threshold_tile_size < 2 {
            return Err(ConfigError::TileSizeTooSmall(self.threshold_tile_size));
        }
        if self.decimation < 1 {
            return Err(ConfigError::InvalidDecimation(self.decimation));
        }
        if self.upscale_factor < 1 {
            return Err(ConfigError::InvalidUpscaleFactor(self.upscale_factor));
        }
        if self.quad_min_fill_ratio < 0.0
            || self.quad_max_fill_ratio > 1.0
            || self.quad_min_fill_ratio >= self.quad_max_fill_ratio
        {
            return Err(ConfigError::InvalidFillRatio {
                min: self.quad_min_fill_ratio,
                max: self.quad_max_fill_ratio,
            });
        }
        if self.quad_min_edge_length <= 0.0 {
            return Err(ConfigError::InvalidEdgeLength(self.quad_min_edge_length));
        }
        Ok(())
    }

    /// High-fidelity configuration used for production accuracy evaluations.
    ///
    /// This matches the default settings used in the Python CLI and Regression suite:
    /// - Corner Refinement: `Erf`
    /// - Pre-processing: `Sharpening Enabled`
    /// - Tile Size: `8`
    #[must_use]
    pub fn production_default() -> Self {
        Self::builder()
            .refinement_mode(CornerRefinementMode::Erf)
            .enable_sharpening(true)
            .threshold_tile_size(8)
            .quad_max_elongation(20.0)
            .quad_min_density(0.15)
            .build()
    }

    /// SOTA configuration for dense multi-tag detection (pure_tags / forward scenes).
    ///
    /// Optimised for maximum recall on dense scenes with many isolated tags at varying
    /// distances, such as the ICRA 2020 `forward/pure_tags_images` benchmark.
    ///
    /// Key difference from [`production_default`]: `DecodeMode::Soft` — LLR-based
    /// decoding recovers tags that Hard-decision misses due to blur or marginal contrast,
    /// yielding ~+19pp recall on ICRA forward (96.2% vs 76.9%) at a modest RMSE cost.
    /// All other parameters match `production_default` (Erf refinement, sharpening on,
    /// moments culling) to preserve the quad candidate quality that Hard relies on.
    ///
    /// **Trade-off:** Soft decoding is ~2–4× slower on the decode phase. Use Hard
    /// (`production_default`) when throughput is the primary constraint.
    #[must_use]
    pub fn sota_pure_tags_default() -> Self {
        Self::builder()
            .refinement_mode(CornerRefinementMode::Erf)
            .enable_sharpening(true)
            .threshold_tile_size(8)
            .quad_max_elongation(20.0)
            .quad_min_density(0.15)
            .decode_mode(DecodeMode::Soft)
            .build()
    }

    /// SOTA configuration for touching/adjacent tags in checkerboard grid patterns.
    ///
    /// Designed for scenes where tag borders share a boundary (saddle corners), such
    /// as the ICRA 2020 `forward/checkerboard_corners_images` benchmark.
    ///
    /// **Non-negotiable invariants:**
    /// - `segmentation_connectivity: Four` — 8-connectivity merges adjacent tag regions
    ///   into one component; Four-connectivity correctly separates touching borders.
    /// - `decoder_min_contrast: 10.0` — packed tags are low-contrast; the default 20.0
    ///   causes the Otsu gate to reject valid tags at shared borders.
    /// - `quad_min_edge_score: 2.0` — touching borders produce weaker edge scores;
    ///   this relaxed threshold prevents false negatives on interior tag edges.
    /// - `enable_sharpening: false` — Laplacian sharpening creates halos at shared
    ///   borders, biasing the threshold and causing merged components.
    ///
    /// `DecodeMode::Soft` is added on top of the checkerboard invariants as it may
    /// further improve recall for low-contrast packed tags.
    #[must_use]
    pub fn sota_checkerboard_default() -> Self {
        Self::builder()
            .refinement_mode(CornerRefinementMode::Erf)
            .enable_sharpening(false)
            .threshold_tile_size(8)
            .quad_max_elongation(20.0)
            .quad_min_density(0.15)
            .segmentation_connectivity(SegmentationConnectivity::Four)
            .decoder_min_contrast(10.0)
            .quad_min_edge_score(2.0)
            .decode_mode(DecodeMode::Soft)
            .build()
    }

    /// Low-latency configuration for high-speed tracking.
    ///
    /// Disables heavy pre-processing and uses lighter corner refinement.
    #[must_use]
    pub fn fast_default() -> Self {
        Self::builder()
            .refinement_mode(CornerRefinementMode::Edge)
            .enable_sharpening(false)
            .threshold_tile_size(8)
            .build()
    }

    /// State-of-the-art metrology configuration for maximum pose accuracy.
    ///
    /// Bridges the deterministic GN 2D solver directly to the probabilistic 3D solver:
    ///
    /// - **Phase 1 (2D Geometric Locking):** EdLines Joint Gauss-Newton solver produces
    ///   sub-pixel corners and their per-corner 2×2 covariance matrices (from H⁻¹).
    /// - **Phase 2 (Uncertainty Translation):** GN covariances propagate to the pose
    ///   solver via `batch.corner_covariances`; the Structure Tensor is used as a
    ///   fallback only if the GN solver diverges for a given corner.
    /// - **Phase 3 (3D Metrology):** Weighted Levenberg-Marquardt minimizes Mahalanobis
    ///   distance weighted by the GN-derived corner covariances.
    ///
    /// Pre-processing filters are disabled to pass the raw PSF directly to the solver.
    /// Hard decoding is used to maintain precision; Soft decode causes a precision
    /// collapse (~10–20%) on EdLines due to the larger number of quad candidates.
    /// For maximum recall on multi-tag scenes use [`sota_pure_tags_default`].
    /// For touching-tag checkerboard grids use [`sota_checkerboard_default`].
    ///
    /// **Pose tuning targets:** `structure_tensor_radius`, `sigma_n_sq`,
    /// `tikhonov_alpha_max`, `huber_delta_px` — sweep these against your sensor profile.
    #[must_use]
    pub fn sota_metrology_default() -> Self {
        Self::builder()
            .quad_extraction_mode(QuadExtractionMode::EdLines)
            .refinement_mode(CornerRefinementMode::None)
            .enable_sharpening(false)
            .enable_bilateral(false)
            .quad_max_elongation(20.0)
            .quad_min_density(0.15)
            .decode_mode(DecodeMode::Hard)
            .threshold_tile_size(8)
            .build()
    }
}

/// Builder for [`DetectorConfig`].
#[derive(Default)]
pub struct DetectorConfigBuilder {
    threshold_tile_size: Option<usize>,
    threshold_min_range: Option<u8>,
    enable_bilateral: Option<bool>,
    bilateral_sigma_space: Option<f32>,
    bilateral_sigma_color: Option<f32>,
    enable_sharpening: Option<bool>,
    enable_adaptive_window: Option<bool>,
    threshold_min_radius: Option<usize>,
    threshold_max_radius: Option<usize>,
    adaptive_threshold_constant: Option<i16>,
    adaptive_threshold_gradient_threshold: Option<u8>,
    quad_min_area: Option<u32>,
    quad_max_aspect_ratio: Option<f32>,
    quad_min_fill_ratio: Option<f32>,
    quad_max_fill_ratio: Option<f32>,
    quad_min_edge_length: Option<f64>,
    /// Minimum gradient magnitude along edges (rejects weak candidates).
    pub quad_min_edge_score: Option<f64>,
    /// Sigma for Gaussian in subpixel refinement.
    pub subpixel_refinement_sigma: Option<f64>,
    /// Margin for threshold-model segmentation.
    pub segmentation_margin: Option<i16>,
    /// Connectivity mode for segmentation (4 or 8).
    pub segmentation_connectivity: Option<SegmentationConnectivity>,
    /// Upscale factor for low-res images (1 = no upscale).
    pub upscale_factor: Option<usize>,
    /// Minimum contrast for decoder to accept a tag.
    pub decoder_min_contrast: Option<f64>,
    /// Refinement mode.
    pub refinement_mode: Option<CornerRefinementMode>,
    /// Decoding mode.
    pub decode_mode: Option<DecodeMode>,
    /// Maximum Hamming errors.
    pub max_hamming_error: Option<u32>,
    /// GWLF transversal alpha.
    pub gwlf_transversal_alpha: Option<f64>,
    /// Maximum elongation for the moments culling gate.
    pub quad_max_elongation: Option<f64>,
    /// Minimum density for the moments culling gate.
    pub quad_min_density: Option<f64>,
    /// Quad extraction mode.
    pub quad_extraction_mode: Option<QuadExtractionMode>,
    /// Huber delta for LM reprojection (pixels).
    pub huber_delta_px: Option<f64>,
    /// Maximum Tikhonov regularisation alpha for Accurate mode.
    pub tikhonov_alpha_max: Option<f64>,
    /// Pixel noise variance for Structure Tensor covariance model.
    pub sigma_n_sq: Option<f64>,
    /// Radius of the Structure Tensor window in Accurate mode.
    pub structure_tensor_radius: Option<u8>,
}

impl DetectorConfigBuilder {
    /// Set the tile size for adaptive thresholding.
    #[must_use]
    pub fn threshold_tile_size(mut self, size: usize) -> Self {
        self.threshold_tile_size = Some(size);
        self
    }

    /// Set the minimum intensity range for valid tiles.
    #[must_use]
    pub fn threshold_min_range(mut self, range: u8) -> Self {
        self.threshold_min_range = Some(range);
        self
    }

    /// Set the minimum quad area.
    #[must_use]
    pub fn quad_min_area(mut self, area: u32) -> Self {
        self.quad_min_area = Some(area);
        self
    }

    /// Set the maximum aspect ratio.
    #[must_use]
    pub fn quad_max_aspect_ratio(mut self, ratio: f32) -> Self {
        self.quad_max_aspect_ratio = Some(ratio);
        self
    }

    /// Set the minimum fill ratio.
    #[must_use]
    pub fn quad_min_fill_ratio(mut self, ratio: f32) -> Self {
        self.quad_min_fill_ratio = Some(ratio);
        self
    }

    /// Set the maximum fill ratio.
    #[must_use]
    pub fn quad_max_fill_ratio(mut self, ratio: f32) -> Self {
        self.quad_max_fill_ratio = Some(ratio);
        self
    }

    /// Set the minimum edge length.
    #[must_use]
    pub fn quad_min_edge_length(mut self, length: f64) -> Self {
        self.quad_min_edge_length = Some(length);
        self
    }

    /// Set the minimum edge gradient score.
    #[must_use]
    pub fn quad_min_edge_score(mut self, score: f64) -> Self {
        self.quad_min_edge_score = Some(score);
        self
    }

    /// Enable or disable bilateral pre-filtering.
    #[must_use]
    pub fn enable_bilateral(mut self, enable: bool) -> Self {
        self.enable_bilateral = Some(enable);
        self
    }

    /// Set bilateral spatial sigma.
    #[must_use]
    pub fn bilateral_sigma_space(mut self, sigma: f32) -> Self {
        self.bilateral_sigma_space = Some(sigma);
        self
    }

    /// Set bilateral color sigma.
    #[must_use]
    pub fn bilateral_sigma_color(mut self, sigma: f32) -> Self {
        self.bilateral_sigma_color = Some(sigma);
        self
    }

    /// Enable or disable Laplacian sharpening.
    #[must_use]
    pub fn enable_sharpening(mut self, enable: bool) -> Self {
        self.enable_sharpening = Some(enable);
        self
    }

    /// Enable or disable adaptive threshold window sizing.
    #[must_use]
    pub fn enable_adaptive_window(mut self, enable: bool) -> Self {
        self.enable_adaptive_window = Some(enable);
        self
    }

    /// Set minimum threshold window radius.
    #[must_use]
    pub fn threshold_min_radius(mut self, radius: usize) -> Self {
        self.threshold_min_radius = Some(radius);
        self
    }

    /// Set maximum threshold window radius.
    #[must_use]
    pub fn threshold_max_radius(mut self, radius: usize) -> Self {
        self.threshold_max_radius = Some(radius);
        self
    }

    /// Set the constant subtracted from local mean in adaptive thresholding.
    #[must_use]
    pub fn adaptive_threshold_constant(mut self, c: i16) -> Self {
        self.adaptive_threshold_constant = Some(c);
        self
    }

    /// Set the gradient threshold for adaptive window sizing.
    #[must_use]
    pub fn adaptive_threshold_gradient_threshold(mut self, threshold: u8) -> Self {
        self.adaptive_threshold_gradient_threshold = Some(threshold);
        self
    }

    /// Build the configuration, using defaults for unset fields.
    #[must_use]
    pub fn build(self) -> DetectorConfig {
        let d = DetectorConfig::default();
        DetectorConfig {
            threshold_tile_size: self.threshold_tile_size.unwrap_or(d.threshold_tile_size),
            threshold_min_range: self.threshold_min_range.unwrap_or(d.threshold_min_range),
            enable_bilateral: self.enable_bilateral.unwrap_or(d.enable_bilateral),
            bilateral_sigma_space: self
                .bilateral_sigma_space
                .unwrap_or(d.bilateral_sigma_space),
            bilateral_sigma_color: self
                .bilateral_sigma_color
                .unwrap_or(d.bilateral_sigma_color),
            enable_sharpening: self.enable_sharpening.unwrap_or(d.enable_sharpening),
            enable_adaptive_window: self
                .enable_adaptive_window
                .unwrap_or(d.enable_adaptive_window),
            threshold_min_radius: self.threshold_min_radius.unwrap_or(d.threshold_min_radius),
            threshold_max_radius: self.threshold_max_radius.unwrap_or(d.threshold_max_radius),
            adaptive_threshold_constant: self
                .adaptive_threshold_constant
                .unwrap_or(d.adaptive_threshold_constant),
            adaptive_threshold_gradient_threshold: self
                .adaptive_threshold_gradient_threshold
                .unwrap_or(d.adaptive_threshold_gradient_threshold),
            quad_min_area: self.quad_min_area.unwrap_or(d.quad_min_area),
            quad_max_aspect_ratio: self
                .quad_max_aspect_ratio
                .unwrap_or(d.quad_max_aspect_ratio),
            quad_min_fill_ratio: self.quad_min_fill_ratio.unwrap_or(d.quad_min_fill_ratio),
            quad_max_fill_ratio: self.quad_max_fill_ratio.unwrap_or(d.quad_max_fill_ratio),
            quad_min_edge_length: self.quad_min_edge_length.unwrap_or(d.quad_min_edge_length),
            quad_min_edge_score: self.quad_min_edge_score.unwrap_or(d.quad_min_edge_score),
            subpixel_refinement_sigma: self
                .subpixel_refinement_sigma
                .unwrap_or(d.subpixel_refinement_sigma),
            segmentation_margin: self.segmentation_margin.unwrap_or(d.segmentation_margin),
            segmentation_connectivity: self
                .segmentation_connectivity
                .unwrap_or(d.segmentation_connectivity),
            upscale_factor: self.upscale_factor.unwrap_or(d.upscale_factor),
            decimation: 1, // Default to 1, as it's typically set via builder
            nthreads: 0,   // Default to 0
            decoder_min_contrast: self.decoder_min_contrast.unwrap_or(d.decoder_min_contrast),
            refinement_mode: self.refinement_mode.unwrap_or(d.refinement_mode),
            decode_mode: self.decode_mode.unwrap_or(d.decode_mode),
            max_hamming_error: self.max_hamming_error.unwrap_or(d.max_hamming_error),
            huber_delta_px: self.huber_delta_px.unwrap_or(d.huber_delta_px),
            tikhonov_alpha_max: self.tikhonov_alpha_max.unwrap_or(d.tikhonov_alpha_max),
            sigma_n_sq: self.sigma_n_sq.unwrap_or(d.sigma_n_sq),
            structure_tensor_radius: self
                .structure_tensor_radius
                .unwrap_or(d.structure_tensor_radius),
            gwlf_transversal_alpha: self
                .gwlf_transversal_alpha
                .unwrap_or(d.gwlf_transversal_alpha),
            quad_max_elongation: self.quad_max_elongation.unwrap_or(d.quad_max_elongation),
            quad_min_density: self.quad_min_density.unwrap_or(d.quad_min_density),
            quad_extraction_mode: self.quad_extraction_mode.unwrap_or(d.quad_extraction_mode),
        }
    }

    /// Build the configuration and validate all parameter ranges.
    ///
    /// # Errors
    ///
    /// Returns [`ConfigError`] if any parameter is out of its valid range.
    pub fn validated_build(self) -> Result<DetectorConfig, crate::error::ConfigError> {
        let config = self.build();
        config.validate()?;
        Ok(config)
    }

    /// Set the segmentation connectivity.
    #[must_use]
    pub fn segmentation_connectivity(mut self, connectivity: SegmentationConnectivity) -> Self {
        self.segmentation_connectivity = Some(connectivity);
        self
    }

    /// Set the segmentation margin for threshold-model CCL.
    #[must_use]
    pub fn segmentation_margin(mut self, margin: i16) -> Self {
        self.segmentation_margin = Some(margin);
        self
    }

    /// Set the upscale factor (1 = no upscaling, 2 = 2x, etc.).
    #[must_use]
    pub fn upscale_factor(mut self, factor: usize) -> Self {
        self.upscale_factor = Some(factor);
        self
    }

    /// Set the minimum contrast for decoder bit classification.
    /// Lower values (e.g., 10.0) improve recall on small/blurry checkerboard tags.
    #[must_use]
    pub fn decoder_min_contrast(mut self, contrast: f64) -> Self {
        self.decoder_min_contrast = Some(contrast);
        self
    }

    /// Set the corner refinement mode.
    #[must_use]
    pub fn refinement_mode(mut self, mode: CornerRefinementMode) -> Self {
        self.refinement_mode = Some(mode);
        self
    }

    /// Set the decoding mode (Hard or Soft).
    #[must_use]
    pub fn decode_mode(mut self, mode: DecodeMode) -> Self {
        self.decode_mode = Some(mode);
        self
    }

    /// Set the maximum number of Hamming errors allowed.
    #[must_use]
    pub fn max_hamming_error(mut self, errors: u32) -> Self {
        self.max_hamming_error = Some(errors);
        self
    }

    /// Set the GWLF transversal alpha.
    #[must_use]
    pub fn gwlf_transversal_alpha(mut self, alpha: f64) -> Self {
        self.gwlf_transversal_alpha = Some(alpha);
        self
    }

    /// Set the maximum elongation for the moments-based culling gate.
    /// Set to 0.0 to disable (default). Recommended: 15.0.
    #[must_use]
    pub fn quad_max_elongation(mut self, max_elongation: f64) -> Self {
        self.quad_max_elongation = Some(max_elongation);
        self
    }

    /// Set the minimum density for the moments-based culling gate.
    /// Set to 0.0 to disable (default). Recommended: 0.2.
    #[must_use]
    pub fn quad_min_density(mut self, min_density: f64) -> Self {
        self.quad_min_density = Some(min_density);
        self
    }

    /// Set the quad extraction mode (ContourRdp or EdLines).
    #[must_use]
    pub fn quad_extraction_mode(mut self, mode: QuadExtractionMode) -> Self {
        self.quad_extraction_mode = Some(mode);
        self
    }

    /// Set the Huber delta for LM reprojection (pixels).
    #[must_use]
    pub fn huber_delta_px(mut self, delta: f64) -> Self {
        self.huber_delta_px = Some(delta);
        self
    }

    /// Set the maximum Tikhonov regularisation alpha for Accurate pose mode.
    #[must_use]
    pub fn tikhonov_alpha_max(mut self, alpha: f64) -> Self {
        self.tikhonov_alpha_max = Some(alpha);
        self
    }

    /// Set the pixel noise variance for the Structure Tensor covariance model.
    #[must_use]
    pub fn sigma_n_sq(mut self, sigma_n_sq: f64) -> Self {
        self.sigma_n_sq = Some(sigma_n_sq);
        self
    }

    /// Set the Structure Tensor window radius for Accurate pose mode.
    #[must_use]
    pub fn structure_tensor_radius(mut self, radius: u8) -> Self {
        self.structure_tensor_radius = Some(radius);
        self
    }
}

// ============================================================================
// DetectOptions: Per-call detection options
// ============================================================================

/// Tag family identifier for per-call decoder selection.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TagFamily {
    /// AprilTag 16h5 family.
    AprilTag16h5,
    /// AprilTag 36h11 family (587 codes, 11-bit hamming distance).
    AprilTag36h11,
    /// ArUco 4x4_50 dictionary.
    ArUco4x4_50,
    /// ArUco 4x4_100 dictionary.
    ArUco4x4_100,
}

impl TagFamily {
    /// Returns all available tag families.
    #[must_use]
    pub const fn all() -> &'static [TagFamily] {
        &[
            TagFamily::AprilTag16h5,
            TagFamily::AprilTag36h11,
            TagFamily::ArUco4x4_50,
            TagFamily::ArUco4x4_100,
        ]
    }
}

/// Per-call detection options.
///
/// These allow customizing which tag families to decode for a specific call,
/// enabling performance optimization when you know which tags to expect.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DetectOptions {
    /// Tag families to attempt decoding. Empty means use detector defaults.
    pub families: Vec<TagFamily>,
    /// Camera intrinsics for 3D pose estimation. If None, pose is not computed.
    pub intrinsics: Option<crate::pose::CameraIntrinsics>,
    /// Physical size of the tag in world units (e.g. meters) for 3D pose estimation.
    pub tag_size: Option<f64>,
    /// Decimation factor for preprocessing (1 = no decimation).
    /// Preprocessing and segmentation operate on a downsampled image of size (W/D, H/D).
    pub decimation: usize,
    /// Mode for 3D pose estimation (Fast vs Accurate).
    pub pose_estimation_mode: PoseEstimationMode,
}

impl Default for DetectOptions {
    fn default() -> Self {
        Self {
            families: Vec::new(),
            intrinsics: None,
            tag_size: None,
            decimation: 1,
            pose_estimation_mode: PoseEstimationMode::Fast,
        }
    }
}

impl DetectOptions {
    /// Create a new builder for `DetectOptions`.
    #[must_use]
    pub fn builder() -> DetectOptionsBuilder {
        DetectOptionsBuilder::default()
    }
    /// Create options that decode only the specified tag families.
    #[must_use]
    pub fn with_families(families: &[TagFamily]) -> Self {
        Self {
            families: families.to_vec(),
            intrinsics: None,
            tag_size: None,
            decimation: 1,
            pose_estimation_mode: PoseEstimationMode::Fast,
        }
    }

    /// Create options that decode all known tag families.
    #[must_use]
    pub fn all_families() -> Self {
        Self {
            families: TagFamily::all().to_vec(),
            intrinsics: None,
            tag_size: None,
            decimation: 1,
            pose_estimation_mode: PoseEstimationMode::Fast,
        }
    }
}

/// Builder for [`DetectOptions`].
pub struct DetectOptionsBuilder {
    families: Vec<TagFamily>,
    intrinsics: Option<crate::pose::CameraIntrinsics>,
    tag_size: Option<f64>,
    decimation: usize,
    pose_estimation_mode: PoseEstimationMode,
}

impl Default for DetectOptionsBuilder {
    fn default() -> Self {
        Self {
            families: Vec::new(),
            intrinsics: None,
            tag_size: None,
            decimation: 1,
            pose_estimation_mode: PoseEstimationMode::Fast,
        }
    }
}

impl DetectOptionsBuilder {
    /// Set the tag families to decode.
    #[must_use]
    pub fn families(mut self, families: &[TagFamily]) -> Self {
        self.families = families.to_vec();
        self
    }

    /// Set camera intrinsics for pose estimation.
    #[must_use]
    pub fn intrinsics(mut self, fx: f64, fy: f64, cx: f64, cy: f64) -> Self {
        self.intrinsics = Some(crate::pose::CameraIntrinsics::new(fx, fy, cx, cy));
        self
    }

    /// Set physical tag size for pose estimation.
    #[must_use]
    pub fn tag_size(mut self, size: f64) -> Self {
        self.tag_size = Some(size);
        self
    }

    /// Set the decimation factor (1 = no decimation).
    #[must_use]
    pub fn decimation(mut self, decimation: usize) -> Self {
        self.decimation = decimation.max(1);
        self
    }

    /// Set the pose estimation mode.
    #[must_use]
    pub fn pose_estimation_mode(mut self, mode: PoseEstimationMode) -> Self {
        self.pose_estimation_mode = mode;
        self
    }

    /// Build the options.
    #[must_use]
    pub fn build(self) -> DetectOptions {
        DetectOptions {
            families: self.families,
            intrinsics: self.intrinsics,
            tag_size: self.tag_size,
            decimation: self.decimation,
            pose_estimation_mode: self.pose_estimation_mode,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[allow(clippy::float_cmp)]
    fn test_detector_config_builder() {
        let config = DetectorConfig::builder()
            .threshold_tile_size(16)
            .quad_min_area(1000)
            .build();
        assert_eq!(config.threshold_tile_size, 16);
        assert_eq!(config.quad_min_area, 1000);
        // Check defaults
        assert_eq!(config.threshold_min_range, 10);
        assert_eq!(config.quad_min_edge_score, 4.0);
        assert_eq!(config.max_hamming_error, 2);
    }

    #[test]
    #[allow(clippy::float_cmp)]
    fn test_detector_config_defaults() {
        let config = DetectorConfig::default();
        assert_eq!(config.threshold_tile_size, 8);
        assert_eq!(config.quad_min_area, 16);
        assert_eq!(config.quad_min_edge_length, 4.0);
        assert_eq!(config.max_hamming_error, 2);
    }

    #[test]
    fn test_detect_options_families() {
        let opt = DetectOptions::with_families(&[TagFamily::AprilTag36h11]);
        assert_eq!(opt.families.len(), 1);
        assert_eq!(opt.families[0], TagFamily::AprilTag36h11);
    }

    #[test]
    fn test_detect_options_default_empty() {
        let opt = DetectOptions::default();
        assert!(opt.families.is_empty());
    }

    #[test]
    fn test_all_families() {
        let opt = DetectOptions::all_families();
        assert_eq!(opt.families.len(), 4);
    }

    #[test]
    fn test_default_config_is_valid() {
        let config = DetectorConfig::default();
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_production_config_is_valid() {
        let config = DetectorConfig::production_default();
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_validation_rejects_bad_tile_size() {
        let config = DetectorConfig {
            threshold_tile_size: 1,
            ..DetectorConfig::default()
        };
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validation_rejects_bad_fill_ratio() {
        let config = DetectorConfig {
            quad_min_fill_ratio: 0.9,
            quad_max_fill_ratio: 0.5,
            ..DetectorConfig::default()
        };
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validation_rejects_negative_edge_length() {
        let config = DetectorConfig {
            quad_min_edge_length: -1.0,
            ..DetectorConfig::default()
        };
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validated_build_catches_errors() {
        let result = DetectorConfig::builder()
            .threshold_tile_size(0)
            .validated_build();
        assert!(result.is_err());
    }
}
