//! PDF transparency module
//!
//! Handles transparency groups, blend modes, soft masks, and alpha compositing
//! per PDF 1.4+ transparency model.

use crate::fitz::error::{Error, Result};
use crate::fitz::geometry::Rect;
use crate::pdf::object::{Dict, Name, Object};

// ============================================================================
// BlendMode
// ============================================================================

/// PDF blend modes for compositing source over backdrop.
/// All modes operate on normalized [0, 1] color components.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlendMode {
    Normal,
    Multiply,
    Screen,
    Overlay,
    Darken,
    Lighten,
    ColorDodge,
    ColorBurn,
    HardLight,
    SoftLight,
    Difference,
    Exclusion,
    Hue,
    Saturation,
    Color,
    Luminosity,
}

impl BlendMode {
    /// Parse blend mode from PDF name (e.g. /Normal, /Multiply).
    pub fn from_pdf_name(name: &str) -> Option<Self> {
        match name {
            "Normal" => Some(Self::Normal),
            "Multiply" => Some(Self::Multiply),
            "Screen" => Some(Self::Screen),
            "Overlay" => Some(Self::Overlay),
            "Darken" => Some(Self::Darken),
            "Lighten" => Some(Self::Lighten),
            "ColorDodge" => Some(Self::ColorDodge),
            "ColorBurn" => Some(Self::ColorBurn),
            "HardLight" => Some(Self::HardLight),
            "SoftLight" => Some(Self::SoftLight),
            "Difference" => Some(Self::Difference),
            "Exclusion" => Some(Self::Exclusion),
            "Hue" => Some(Self::Hue),
            "Saturation" => Some(Self::Saturation),
            "Color" => Some(Self::Color),
            "Luminosity" => Some(Self::Luminosity),
            _ => None,
        }
    }

    /// Apply blend mode between backdrop (cb) and source (cs) components.
    /// Both inputs must be in [0, 1]. Returns blended value in [0, 1].
    #[inline]
    pub fn blend(self, backdrop: f64, source: f64) -> f64 {
        let cb = backdrop.clamp(0.0, 1.0);
        let cs = source.clamp(0.0, 1.0);
        match self {
            Self::Normal => cs,
            Self::Multiply => cb * cs,
            Self::Screen => 1.0 - (1.0 - cb) * (1.0 - cs),
            Self::Overlay => {
                if cb < 0.5 {
                    2.0 * cb * cs
                } else {
                    1.0 - 2.0 * (1.0 - cb) * (1.0 - cs)
                }
            }
            Self::Darken => cb.min(cs),
            Self::Lighten => cb.max(cs),
            Self::ColorDodge => {
                if cs >= 1.0 {
                    1.0
                } else if cb <= 0.0 {
                    0.0
                } else {
                    (cb / (1.0 - cs)).min(1.0)
                }
            }
            Self::ColorBurn => {
                if cs <= 0.0 {
                    0.0
                } else if cb >= 1.0 {
                    1.0
                } else {
                    (1.0 - (1.0 - cb) / cs).max(0.0)
                }
            }
            Self::HardLight => {
                if cs < 0.5 {
                    2.0 * cb * cs
                } else {
                    1.0 - 2.0 * (1.0 - cb) * (1.0 - cs)
                }
            }
            Self::SoftLight => {
                // W3C/PDF formula
                if cs <= 0.5 {
                    cb - (1.0 - 2.0 * cs) * cb * (1.0 - cb)
                } else {
                    let g = if cb <= 0.25 {
                        ((16.0 * cb - 12.0) * cb + 4.0) * cb
                    } else {
                        cb.sqrt()
                    };
                    cb + (2.0 * cs - 1.0) * (g - cb)
                }
            }
            Self::Difference => (cb - cs).abs(),
            Self::Exclusion => cb + cs - 2.0 * cb * cs,
            Self::Hue | Self::Saturation | Self::Color | Self::Luminosity => {
                // Non-separable: operate in HSL-like space. Use component-wise
                // approximation for single-channel; full impl would need RGB.
                cs
            }
        }
    }

    /// Blend RGB triple. For separable modes, applies per-channel.
    /// For Hue/Saturation/Color/Luminosity, uses HSL conversion.
    pub fn blend_rgb(self, backdrop: [f64; 3], source: [f64; 3]) -> [f64; 3] {
        let cb = [
            backdrop[0].clamp(0.0, 1.0),
            backdrop[1].clamp(0.0, 1.0),
            backdrop[2].clamp(0.0, 1.0),
        ];
        let cs = [
            source[0].clamp(0.0, 1.0),
            source[1].clamp(0.0, 1.0),
            source[2].clamp(0.0, 1.0),
        ];
        match self {
            Self::Hue => {
                let (_, sb, sc) = rgb_to_hsl(cs[0], cs[1], cs[2]);
                let (hb, _, _) = rgb_to_hsl(cb[0], cb[1], cb[2]);
                hsl_to_rgb(hb, sb, sc)
            }
            Self::Saturation => {
                let (hb, _, sc) = rgb_to_hsl(cb[0], cb[1], cb[2]);
                let (_, sb, _) = rgb_to_hsl(cs[0], cs[1], cs[2]);
                hsl_to_rgb(hb, sb, sc)
            }
            Self::Color => {
                let (_, _, lb) = rgb_to_hsl(cb[0], cb[1], cb[2]);
                let (hs, ss, _) = rgb_to_hsl(cs[0], cs[1], cs[2]);
                hsl_to_rgb(hs, ss, lb)
            }
            Self::Luminosity => {
                let (hb, sb, _) = rgb_to_hsl(cb[0], cb[1], cb[2]);
                let (_, _, ls) = rgb_to_hsl(cs[0], cs[1], cs[2]);
                hsl_to_rgb(hb, sb, ls)
            }
            _ => [
                self.blend(cb[0], cs[0]),
                self.blend(cb[1], cs[1]),
                self.blend(cb[2], cs[2]),
            ],
        }
    }
}

fn rgb_to_hsl(r: f64, g: f64, b: f64) -> (f64, f64, f64) {
    let max = r.max(g).max(b);
    let min = r.min(g).min(b);
    let l = (max + min) / 2.0;
    if (max - min).abs() < 1e-10 {
        return (0.0, 0.0, l);
    }
    let d = max - min;
    let s = if l > 0.5 {
        d / (2.0 - max - min)
    } else {
        d / (max + min)
    };
    let h = if (max - r).abs() < 1e-10 {
        (g - b) / d + (if g < b { 6.0 } else { 0.0 })
    } else if (max - g).abs() < 1e-10 {
        (b - r) / d + 2.0
    } else {
        (r - g) / d + 4.0
    };
    let h = h / 6.0;
    (h, s, l)
}

fn hsl_to_rgb(h: f64, s: f64, l: f64) -> [f64; 3] {
    if s.abs() < 1e-10 {
        return [l, l, l];
    }
    let q = if l < 0.5 {
        l * (1.0 + s)
    } else {
        l + s - l * s
    };
    let p = 2.0 * l - q;
    let hue2rgb = |t: f64| {
        let t = if t < 0.0 {
            t + 1.0
        } else if t > 1.0 {
            t - 1.0
        } else {
            t
        };
        if t < 1.0 / 6.0 {
            p + (q - p) * 6.0 * t
        } else if t < 0.5 {
            q
        } else if t < 2.0 / 3.0 {
            p + (q - p) * (2.0 / 3.0 - t) * 6.0
        } else {
            p
        }
    };
    [hue2rgb(h + 1.0 / 3.0), hue2rgb(h), hue2rgb(h - 1.0 / 3.0)]
}

// ============================================================================
// TransparencyGroup
// ============================================================================

/// Transparency group from /Group dictionary in a form XObject.
#[derive(Debug, Clone)]
pub struct TransparencyGroup {
    /// Color space for the group (e.g. DeviceRGB, DeviceGray).
    pub colorspace: Option<String>,
    /// If true, group is isolated (initial backdrop is backdrop_color).
    pub isolated: bool,
    /// If true, each object knocks out previous objects in the group.
    pub knockout: bool,
    /// Bounding box of the group.
    pub bbox: Option<Rect>,
}

impl TransparencyGroup {
    /// Parse /Group dictionary from a form XObject dictionary.
    pub fn parse_transparency_group(dict: &Dict) -> Result<Option<Self>> {
        let group_dict = match dict.get(&Name::new("Group")) {
            Some(Object::Dict(d)) => d,
            Some(Object::Ref(_)) => {
                return Err(Error::pdf(
                    "Group must be resolved before parsing (inline dict expected)",
                ));
            }
            _ => return Ok(None),
        };
        let subtype = group_dict
            .get(&Name::new("S"))
            .and_then(|o| o.as_name())
            .map(|n| n.as_str());
        if subtype != Some("Transparency") {
            return Ok(None);
        }
        let colorspace = group_dict
            .get(&Name::new("CS"))
            .and_then(|o| o.as_name())
            .map(|n| n.as_str().to_string());
        let isolated = group_dict
            .get(&Name::new("I"))
            .and_then(|o| o.as_bool())
            .unwrap_or(false);
        let knockout = group_dict
            .get(&Name::new("K"))
            .and_then(|o| o.as_bool())
            .unwrap_or(false);
        let bbox = group_dict
            .get(&Name::new("BBox"))
            .and_then(|o| o.as_array())
            .and_then(|arr| {
                if arr.len() >= 4 {
                    let vals: Vec<f32> = arr
                        .iter()
                        .filter_map(|o| o.as_real().map(|r| r as f32))
                        .collect();
                    if vals.len() >= 4 {
                        Some(Rect::new(vals[0], vals[1], vals[2], vals[3]))
                    } else {
                        None
                    }
                } else {
                    None
                }
            });
        Ok(Some(Self {
            colorspace,
            isolated,
            knockout,
            bbox,
        }))
    }
}

// ============================================================================
// SoftMask
// ============================================================================

/// Soft mask type: Alpha or Luminosity.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SoftMaskType {
    Alpha,
    Luminosity,
}

/// Soft mask from /SMask in graphics state.
#[derive(Debug, Clone)]
pub struct SoftMask {
    pub mask_type: SoftMaskType,
    pub group: TransparencyGroup,
    pub backdrop_color: Option<Vec<f64>>,
    pub transfer_function: Option<String>,
}

impl SoftMask {
    /// Parse /SMask from graphics state dictionary.
    /// Expects resolved SMask dictionary (not a reference).
    pub fn parse_soft_mask(dict: &Dict) -> Result<Option<Self>> {
        let smask_obj = match dict.get(&Name::new("SMask")) {
            Some(Object::Dict(d)) => d.clone(),
            Some(Object::Ref(_)) => {
                return Err(Error::pdf(
                    "SMask must be resolved before parsing (inline dict expected)",
                ));
            }
            _ => return Ok(None),
        };
        let mask_type = smask_obj
            .get(&Name::new("S"))
            .and_then(|o| o.as_name())
            .map(|n| n.as_str());
        let mask_type = match mask_type {
            Some("Alpha") => SoftMaskType::Alpha,
            Some("Luminosity") => SoftMaskType::Luminosity,
            _ => return Err(Error::pdf("SMask S must be /Alpha or /Luminosity")),
        };
        let group_dict = smask_obj
            .get(&Name::new("G"))
            .and_then(|o| o.as_dict())
            .cloned()
            .ok_or_else(|| Error::pdf("SMask G (group) is required"))?;
        let group = TransparencyGroup::parse_transparency_group(&group_dict)?
            .ok_or_else(|| Error::pdf("SMask G must be a transparency group"))?;
        let backdrop_color = smask_obj
            .get(&Name::new("BC"))
            .and_then(|o| o.as_array())
            .map(|arr| arr.iter().filter_map(|o| o.as_real()).collect::<Vec<_>>());
        let transfer_function = smask_obj
            .get(&Name::new("TR"))
            .and_then(|o| o.as_name())
            .map(|n| n.as_str().to_string());
        Ok(Some(Self {
            mask_type,
            group,
            backdrop_color,
            transfer_function,
        }))
    }
}

// ============================================================================
// ExtendedGraphicsState
// ============================================================================

/// Extended graphics state from /ExtGState dictionary.
#[derive(Debug, Clone, Default)]
pub struct ExtendedGraphicsState {
    /// Fill opacity (ca), [0, 1], default 1.
    pub fill_opacity: f64,
    /// Stroke opacity (CA), [0, 1], default 1.
    pub stroke_opacity: f64,
    /// Blend mode.
    pub blend_mode: Option<BlendMode>,
    /// Soft mask.
    pub soft_mask: Option<SoftMask>,
    /// Overprint for stroke (OP).
    pub overprint_stroke: Option<bool>,
    /// Overprint for fill (op).
    pub overprint_fill: Option<bool>,
    /// Overprint mode (OPM), 0 or 1.
    pub overprint_mode: Option<i32>,
    /// Alpha is shape (AIS).
    pub alpha_is_shape: Option<bool>,
}

impl ExtendedGraphicsState {
    /// Parse /ExtGState dictionary.
    pub fn parse_extended_graphics_state(dict: &Dict) -> Result<Self> {
        let mut gs = Self::default();
        if let Some(obj) = dict.get(&Name::new("ca")) {
            if let Some(r) = obj.as_real() {
                gs.fill_opacity = r.clamp(0.0, 1.0);
            }
        }
        if let Some(obj) = dict.get(&Name::new("CA")) {
            if let Some(r) = obj.as_real() {
                gs.stroke_opacity = r.clamp(0.0, 1.0);
            }
        }
        if let Some(obj) = dict.get(&Name::new("BM")) {
            if let Some(n) = obj.as_name() {
                gs.blend_mode = BlendMode::from_pdf_name(n.as_str());
            } else if let Some(arr) = obj.as_array() {
                if let Some(first) = arr.first().and_then(|o| o.as_name()) {
                    gs.blend_mode = BlendMode::from_pdf_name(first.as_str());
                }
            }
        }
        if let Some(obj) = dict.get(&Name::new("SMask")) {
            if let Object::Dict(d) = obj {
                gs.soft_mask = SoftMask::parse_soft_mask(d)?;
            }
        }
        if let Some(obj) = dict.get(&Name::new("OP")) {
            if let Some(b) = obj.as_bool() {
                gs.overprint_stroke = Some(b);
                gs.overprint_fill = Some(b);
            }
        }
        if let Some(obj) = dict.get(&Name::new("op")) {
            if let Some(b) = obj.as_bool() {
                gs.overprint_fill = Some(b);
            }
        }
        if let Some(obj) = dict.get(&Name::new("OPM")) {
            if let Some(i) = obj.as_int() {
                gs.overprint_mode = Some(i as i32);
            }
        }
        if let Some(obj) = dict.get(&Name::new("AIS")) {
            if let Some(b) = obj.as_bool() {
                gs.alpha_is_shape = Some(b);
            }
        }
        Ok(gs)
    }
}

// ============================================================================
// Alpha compositing
// ============================================================================

/// Alpha compositing: Porter-Duff "source over" formula.
/// result = (source * source_alpha + backdrop * backdrop_alpha * (1 - source_alpha)) / result_alpha
/// where result_alpha = source_alpha + backdrop_alpha * (1 - source_alpha)
#[inline]
pub fn composite(
    source_color: f64,
    source_alpha: f64,
    backdrop_color: f64,
    backdrop_alpha: f64,
) -> (f64, f64) {
    let sa = source_alpha.clamp(0.0, 1.0);
    let ba = backdrop_alpha.clamp(0.0, 1.0);
    let result_alpha = sa + ba * (1.0 - sa);
    if result_alpha < 1e-10 {
        return (0.0, 0.0);
    }
    let result_color = (source_color * sa + backdrop_color * ba * (1.0 - sa)) / result_alpha;
    (result_color, result_alpha)
}

/// Composite RGBA source over RGBA backdrop.
pub fn composite_rgba(source: [f64; 4], backdrop: [f64; 4]) -> [f64; 4] {
    let (r, a) = composite(source[0], source[3], backdrop[0], backdrop[3]);
    let (g, _) = composite(source[1], source[3], backdrop[1], backdrop[3]);
    let (b, _) = composite(source[2], source[3], backdrop[2], backdrop[3]);
    [r, g, b, a]
}

/// Composite a transparency group onto backdrop.
/// - isolated: use group's initial backdrop (e.g. backdrop_color) instead of page
/// - knockout: each object knocks out previous objects in the group
pub fn composite_with_group(
    group_result: [f64; 4],
    backdrop: [f64; 4],
    isolated: bool,
    _knockout: bool,
    backdrop_color: Option<&[f64]>,
) -> [f64; 4] {
    let effective_backdrop = if isolated {
        backdrop_color.map(|bc| {
            let n = bc.len().min(4);
            let mut b = [0.0, 0.0, 0.0, 1.0];
            for (i, &v) in bc.iter().take(n).enumerate() {
                b[i] = v;
            }
            b
        })
    } else {
        None
    };
    let back = effective_backdrop.unwrap_or(backdrop);
    composite_rgba(group_result, back)
}

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

    fn assert_near(a: f64, b: f64, eps: f64) {
        assert!(
            (a - b).abs() < eps,
            "expected {} ≈ {}, diff = {}",
            a,
            b,
            (a - b).abs()
        );
    }

    #[test]
    fn test_blend_normal() {
        assert_near(BlendMode::Normal.blend(0.5, 0.8), 0.8, 1e-10);
        assert_near(BlendMode::Normal.blend(0.0, 1.0), 1.0, 1e-10);
    }

    #[test]
    fn test_blend_multiply() {
        assert_near(BlendMode::Multiply.blend(0.5, 0.5), 0.25, 1e-10);
        assert_near(BlendMode::Multiply.blend(1.0, 0.5), 0.5, 1e-10);
        assert_near(BlendMode::Multiply.blend(0.0, 0.5), 0.0, 1e-10);
    }

    #[test]
    fn test_blend_screen() {
        assert_near(BlendMode::Screen.blend(0.5, 0.5), 0.75, 1e-10);
        assert_near(BlendMode::Screen.blend(0.0, 0.5), 0.5, 1e-10);
        assert_near(BlendMode::Screen.blend(1.0, 0.5), 1.0, 1e-10);
    }

    #[test]
    fn test_blend_overlay() {
        // cb<0.5: 2*cb*cs = 2*0.25*0.5 = 0.25
        assert_near(BlendMode::Overlay.blend(0.25, 0.5), 0.25, 1e-10);
        // cb>=0.5: 1 - 2*(1-cb)*(1-cs) = 1 - 2*0.25*0.5 = 0.75
        assert_near(BlendMode::Overlay.blend(0.75, 0.5), 0.75, 1e-10);
    }

    #[test]
    fn test_blend_darken() {
        assert_near(BlendMode::Darken.blend(0.3, 0.7), 0.3, 1e-10);
        assert_near(BlendMode::Darken.blend(0.8, 0.2), 0.2, 1e-10);
    }

    #[test]
    fn test_blend_lighten() {
        assert_near(BlendMode::Lighten.blend(0.3, 0.7), 0.7, 1e-10);
        assert_near(BlendMode::Lighten.blend(0.8, 0.2), 0.8, 1e-10);
    }

    #[test]
    fn test_blend_color_dodge() {
        assert_near(BlendMode::ColorDodge.blend(0.5, 0.5), 1.0, 1e-10);
        assert_near(BlendMode::ColorDodge.blend(0.5, 0.0), 0.5, 1e-10);
        assert_near(BlendMode::ColorDodge.blend(0.0, 0.5), 0.0, 1e-10);
    }

    #[test]
    fn test_blend_color_burn() {
        assert_near(BlendMode::ColorBurn.blend(0.5, 0.5), 0.0, 1e-10);
        assert_near(BlendMode::ColorBurn.blend(0.5, 1.0), 0.5, 1e-10);
        assert_near(BlendMode::ColorBurn.blend(1.0, 0.5), 1.0, 1e-10);
    }

    #[test]
    fn test_blend_hard_light() {
        // cs<0.5: 2*cb*cs = 2*0.5*0.25 = 0.25
        assert_near(BlendMode::HardLight.blend(0.5, 0.25), 0.25, 1e-10);
        // cs>=0.5: 1 - 2*(1-cb)*(1-cs) = 1 - 2*0.5*0.25 = 0.75
        assert_near(BlendMode::HardLight.blend(0.5, 0.75), 0.75, 1e-10);
    }

    #[test]
    fn test_blend_soft_light() {
        assert_near(BlendMode::SoftLight.blend(0.5, 0.5), 0.5, 1e-6);
        assert_near(BlendMode::SoftLight.blend(0.25, 0.25), 0.15625, 1e-6);
    }

    #[test]
    fn test_blend_difference() {
        assert_near(BlendMode::Difference.blend(0.8, 0.3), 0.5, 1e-10);
        assert_near(BlendMode::Difference.blend(0.3, 0.8), 0.5, 1e-10);
    }

    #[test]
    fn test_blend_exclusion() {
        assert_near(BlendMode::Exclusion.blend(0.5, 0.5), 0.5, 1e-10);
        // cb + cs - 2*cb*cs: 0.8 + 0.2 - 0.32 = 0.68
        assert_near(BlendMode::Exclusion.blend(0.8, 0.2), 0.68, 1e-10);
    }

    #[test]
    fn test_blend_hue_saturation_color_luminosity() {
        // Non-separable modes: at least return valid values
        let r = BlendMode::Hue.blend_rgb([0.5, 0.5, 0.5], [1.0, 0.0, 0.0]);
        assert!(r[0] >= 0.0 && r[0] <= 1.0);
        assert!(r[1] >= 0.0 && r[1] <= 1.0);
        assert!(r[2] >= 0.0 && r[2] <= 1.0);
    }

    #[test]
    fn test_blend_from_pdf_name() {
        assert_eq!(BlendMode::from_pdf_name("Normal"), Some(BlendMode::Normal));
        assert_eq!(
            BlendMode::from_pdf_name("Multiply"),
            Some(BlendMode::Multiply)
        );
        assert_eq!(BlendMode::from_pdf_name("Unknown"), None);
    }

    #[test]
    fn test_parse_transparency_group() {
        use crate::pdf::object::Object;
        let mut d: Dict = HashMap::new();
        d.insert(Name::new("Group"), {
            let mut g = Dict::new();
            g.insert(Name::new("S"), Object::Name(Name::new("Transparency")));
            g.insert(Name::new("I"), Object::Bool(true));
            g.insert(Name::new("K"), Object::Bool(false));
            g.insert(
                Name::new("BBox"),
                Object::Array(vec![
                    Object::Real(0.0),
                    Object::Real(0.0),
                    Object::Real(100.0),
                    Object::Real(100.0),
                ]),
            );
            Object::Dict(g)
        });
        let tg = TransparencyGroup::parse_transparency_group(&d).unwrap();
        let tg = tg.expect("should parse");
        assert!(tg.isolated);
        assert!(!tg.knockout);
        assert_eq!(tg.bbox.unwrap().x0, 0.0);
        assert_eq!(tg.bbox.unwrap().x1, 100.0);
    }

    #[test]
    fn test_parse_soft_mask() {
        use crate::pdf::object::Object;
        // G is a form XObject dict with /Group containing the transparency group
        let mut group_inner: Dict = HashMap::new();
        group_inner.insert(Name::new("S"), Object::Name(Name::new("Transparency")));
        group_inner.insert(Name::new("I"), Object::Bool(false));
        let mut form_xobj: Dict = HashMap::new();
        form_xobj.insert(Name::new("Group"), Object::Dict(group_inner));
        let mut sm: Dict = HashMap::new();
        sm.insert(Name::new("S"), Object::Name(Name::new("Alpha")));
        sm.insert(Name::new("G"), Object::Dict(form_xobj));
        let mut d: Dict = HashMap::new();
        d.insert(Name::new("SMask"), Object::Dict(sm));
        let mask = SoftMask::parse_soft_mask(&d).unwrap();
        let mask = mask.expect("should parse");
        assert_eq!(mask.mask_type, SoftMaskType::Alpha);
    }

    #[test]
    fn test_parse_extended_graphics_state() {
        use crate::pdf::object::Object;
        let mut d: Dict = HashMap::new();
        d.insert(Name::new("ca"), Object::Real(0.5));
        d.insert(Name::new("CA"), Object::Real(0.8));
        d.insert(Name::new("BM"), Object::Name(Name::new("Multiply")));
        let gs = ExtendedGraphicsState::parse_extended_graphics_state(&d).unwrap();
        assert_near(gs.fill_opacity, 0.5, 1e-10);
        assert_near(gs.stroke_opacity, 0.8, 1e-10);
        assert_eq!(gs.blend_mode, Some(BlendMode::Multiply));
    }

    #[test]
    fn test_composite() {
        let (c, a) = composite(1.0, 0.5, 0.0, 1.0);
        assert_near(c, 0.5, 1e-10);
        assert_near(a, 1.0, 1e-10);

        let (c, a) = composite(1.0, 1.0, 0.0, 1.0);
        assert_near(c, 1.0, 1e-10);
        assert_near(a, 1.0, 1e-10);

        let (c, a) = composite(0.0, 0.0, 0.5, 1.0);
        assert_near(c, 0.5, 1e-10);
        assert_near(a, 1.0, 1e-10);
    }

    #[test]
    fn test_composite_rgba() {
        let src = [1.0, 0.0, 0.0, 0.5];
        let back = [0.0, 1.0, 0.0, 1.0];
        let out = composite_rgba(src, back);
        assert_near(out[0], 0.5, 1e-6);
        assert_near(out[1], 0.5, 1e-6);
        assert_near(out[2], 0.0, 1e-6);
        assert_near(out[3], 1.0, 1e-6);
    }

    #[test]
    fn test_composite_with_group() {
        let group = [0.5, 0.5, 0.5, 0.5];
        let backdrop = [0.0, 0.0, 0.0, 1.0];
        let bc: Vec<f64> = vec![1.0, 1.0, 1.0];
        let out = composite_with_group(group, backdrop, true, false, Some(&bc));
        assert!(out[0] >= 0.0 && out[0] <= 1.0);
        assert!(out[3] >= 0.0 && out[3] <= 1.0);
    }
}
