// AgX tone mapping pipeline, spectral gamut mapping, and log/linear transfer functions.
// References:
//   - AgX: Troy Sobotka / AgX project (MIT license)
//   - Kraken: Jed Smith / Troy Sobotka
//   - Apple Log: Apple Log Profile White Paper, September 2023
//   - Various camera log curves: colour-science/colour (BSD-3-Clause),
//     https://github.com/colour-science/colour
//   - CAT16 chromatic adaptation: CIE TC 1-90 (2016)

use rayon::prelude::*;
use std::f32::consts::PI;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Gamut {
    Ap0, Ap1, P3D65, P3D60, Rec2020, Rec709,
    Awg3, Awg4, Rwg, SGamut3, SGamut3Cine,
    BlackmagicWg, CanonCinema, DaVinciWg, EGamut,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Transfer {
    Linear,
    AcesCct,
    ArriLogC3,
    ArriLogC4,
    RedLog3G10,
    SonySLog3,
    SonySLog2,
    BmFilmGen5,
    CanonLog3,
    CanonLog2,
    DaVinciIntermediate,
    FilmlightTLog,
    AgxLogKraken,
    VLog,
    FLog2,
    FLog2C,
    AppleLog2,
    HLG,
    PQ,
    DNG,
    DI,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputTransfer {
    Linear,
    SrgbInverseEotf,
    Bt1886InverseEotf,
}

#[derive(Debug, Clone)]
pub struct AgxConfig {
    pub inset_red: f32,
    pub inset_green: f32,
    pub inset_blue: f32,
    pub rotate_red: f32,
    pub rotate_green: f32,
    pub rotate_blue: f32,
    pub outset_red: f32,
    pub outset_green: f32,
    pub outset_blue: f32,
    pub toe_power: f32,
    pub shoulder_power: f32,
    pub slope: f32,
    pub in_gamut: Gamut,
    pub in_transfer: Transfer,
    pub working_curve: Transfer,
    pub working_mid_grey: f32,
    pub out_gamut: Gamut,
    pub out_transfer: OutputTransfer,
    pub log_output: bool,
}

impl Default for AgxConfig {
    fn default() -> Self {
        Self {
            inset_red: 0.2,
            inset_green: 0.2,
            inset_blue: 0.2,
            rotate_red: 0.0,
            rotate_green: 0.0,
            rotate_blue: 0.0,
            outset_red: 0.0,
            outset_green: 0.0,
            outset_blue: 0.0,
            toe_power: 3.0,
            shoulder_power: 3.25,
            slope: 2.0,
            in_gamut: Gamut::Rec709,
            in_transfer: Transfer::Linear,
            working_curve: Transfer::AgxLogKraken,
            working_mid_grey: 0.606060,
            out_gamut: Gamut::Rec709,
            out_transfer: OutputTransfer::SrgbInverseEotf,
            log_output: false,
        }
    }
}

pub struct AgxPipeline {
    config: AgxConfig,
    inset_mat: [f32; 9],
    outset_mat: [f32; 9],
    gamut_mat: [f32; 9],
    out_gamma: f32,
    log_floor: [f32; 3],
    mid_grey_lin: f32,
}

impl AgxPipeline {
    pub fn new(config: AgxConfig) -> Self {
        let in_chrom = gamut_chromaticities(config.in_gamut);
        let out_chrom = gamut_chromaticities(config.out_gamut);

        let inset_chrom = inset_primaries(
            in_chrom,
            config.inset_red,
            config.inset_green,
            config.inset_blue,
            config.rotate_red,
            config.rotate_green,
            config.rotate_blue,
        );
        let outset_chrom = inset_primaries(
            in_chrom,
            config.outset_red,
            config.outset_green,
            config.outset_blue,
            config.rotate_red,
            config.rotate_green,
            config.rotate_blue,
        );

        let inset_mat = rgb_to_rgb(inset_chrom, in_chrom);
        let outset_mat = rgb_to_rgb(in_chrom, outset_chrom);
        let gamut_mat = rgb_to_rgb(in_chrom, out_chrom);

        let out_gamma = match config.out_transfer {
            OutputTransfer::Linear => 1.0,
            OutputTransfer::SrgbInverseEotf => 2.2,
            OutputTransfer::Bt1886InverseEotf => 2.4,
        };

        let log_floor = log_to_lin([0.0, 0.0, 0.0], config.in_transfer);

        let mid_grey_lin = log_to_lin(
            [config.working_mid_grey, config.working_mid_grey, config.working_mid_grey],
            config.working_curve,
        )[0];

        Self {
            config,
            inset_mat,
            outset_mat,
            gamut_mat,
            out_gamma,
            log_floor,
            mid_grey_lin,
        }
    }

    #[inline]
    pub fn process_pixel(&self, r: f32, g: f32, b: f32) -> [f32; 3] {
        let mut rgb = [r, g, b];

        rgb = log_to_lin(rgb, self.config.in_transfer);

        rgb[0] = rgb[0].max(self.log_floor[0]);
        rgb[1] = rgb[1].max(self.log_floor[1]);
        rgb[2] = rgb[2].max(self.log_floor[2]);

        rgb = mat_mul_vec3(&self.inset_mat, rgb);

        rgb = lin_to_log(rgb, self.config.working_curve);
        let log_rgb = rgb;

        let mg = 0.5;
        let lmg = self.config.working_mid_grey;
        rgb[0] = tone_scale(rgb[0], self.config.shoulder_power, self.config.toe_power, self.config.slope, lmg, mg, 1.0, 0.0);
        rgb[1] = tone_scale(rgb[1], self.config.shoulder_power, self.config.toe_power, self.config.slope, lmg, mg, 1.0, 0.0);
        rgb[2] = tone_scale(rgb[2], self.config.shoulder_power, self.config.toe_power, self.config.slope, lmg, mg, 1.0, 0.0);

        if self.config.log_output {
            return log_rgb;
        }

        rgb[0] = rgb[0].powf(2.2);
        rgb[1] = rgb[1].powf(2.2);
        rgb[2] = rgb[2].powf(2.2);

        let lum = (rgb[0] + rgb[1] + rgb[2]) * (1.0 / 3.0);
        let max_ch = rgb[0].max(rgb[1]).max(rgb[2]);
        if max_ch > 0.85 {
            let t = ((max_ch - 0.85) / 0.15).min(1.0);
            rgb[0] = rgb[0] + (lum - rgb[0]) * t;
            rgb[1] = rgb[1] + (lum - rgb[1]) * t;
            rgb[2] = rgb[2] + (lum - rgb[2]) * t;
        }

        rgb = mat_mul_vec3(&self.outset_mat, rgb);

        rgb = mat_mul_vec3(&self.gamut_mat, rgb);

        rgb = inverse_eotf(rgb, self.out_gamma);

        rgb[0] = rgb[0].max(0.0);
        rgb[1] = rgb[1].max(0.0);
        rgb[2] = rgb[2].max(0.0);

        rgb
    }

    pub fn process_frame(&self, pixels: &mut [f32]) {
        pixels.par_chunks_exact_mut(3).for_each(|chunk| {
            let out = self.process_pixel(chunk[0], chunk[1], chunk[2]);
            chunk[0] = out[0];
            chunk[1] = out[1];
            chunk[2] = out[2];
        });
    }

    pub fn config(&self) -> &AgxConfig {
        &self.config
    }
}

#[inline]
fn mat_mul_vec3(m: &[f32; 9], v: [f32; 3]) -> [f32; 3] {
    [
        v[0] * m[0] + v[1] * m[3] + v[2] * m[6],
        v[0] * m[1] + v[1] * m[4] + v[2] * m[7],
        v[0] * m[2] + v[1] * m[5] + v[2] * m[8],
    ]
}

#[derive(Clone, Copy)]
struct Chromaticities {
    r: [f32; 2],
    g: [f32; 2],
    b: [f32; 2],
    w: [f32; 2],
}

fn gamut_chromaticities(g: Gamut) -> Chromaticities {
    const D65: [f32; 2] = [0.3127, 0.3290];
    match g {
        Gamut::Ap0 => Chromaticities { r: [0.7347, 0.2653], g: [0.0000, 1.0000], b: [0.0001, -0.0770], w: D65 },
        Gamut::Ap1 => Chromaticities { r: [0.7130, 0.2930], g: [0.1650, 0.8300], b: [0.1280, 0.0440], w: D65 },
        Gamut::Rec709 => Chromaticities { r: [0.6400, 0.3300], g: [0.3000, 0.6000], b: [0.1500, 0.0600], w: D65 },
        Gamut::Rec2020 => Chromaticities { r: [0.7080, 0.2920], g: [0.1700, 0.7970], b: [0.1310, 0.0460], w: D65 },
        Gamut::P3D65 => Chromaticities { r: [0.6800, 0.3200], g: [0.2650, 0.6900], b: [0.1500, 0.0600], w: D65 },
        Gamut::P3D60 => Chromaticities { r: [0.6800, 0.3200], g: [0.2650, 0.6900], b: [0.1500, 0.0600], w: D65 },
        Gamut::SGamut3Cine => Chromaticities { r: [0.7660, 0.2750], g: [0.2250, 0.8000], b: [0.0890, -0.0870], w: D65 },
        Gamut::Awg3 => Chromaticities { r: [0.6840, 0.3130], g: [0.2210, 0.8480], b: [0.0861, -0.1020], w: D65 },
        Gamut::Awg4 => Chromaticities { r: [0.6800, 0.3150], g: [0.2200, 0.8500], b: [0.0860, -0.1000], w: D65 },
        Gamut::Rwg => Chromaticities { r: [0.7300, 0.2800], g: [0.1400, 0.8550], b: [0.1000, -0.0900], w: D65 },
        Gamut::SGamut3 => Chromaticities { r: [0.7500, 0.2700], g: [0.2100, 0.8000], b: [0.1000, -0.0500], w: D65 },
        Gamut::BlackmagicWg => Chromaticities { r: [0.7500, 0.2700], g: [0.2100, 0.8000], b: [0.1000, -0.0500], w: D65 },
        Gamut::CanonCinema => Chromaticities { r: [0.7400, 0.2700], g: [0.1700, 0.7900], b: [0.0800, -0.1000], w: D65 },
        Gamut::DaVinciWg => Chromaticities { r: [0.7350, 0.2650], g: [0.2150, 0.8100], b: [0.1200, -0.0500], w: D65 },
        Gamut::EGamut => Chromaticities { r: [0.7300, 0.2800], g: [0.1700, 0.8000], b: [0.1000, -0.0600], w: D65 },
    }
}

fn rgb_to_rgb(src: Chromaticities, dst: Chromaticities) -> [f32; 9] {
    let src_to_xyz = rgb_to_xyz(src);
    let xyz_to_dst = xyz_to_rgb(dst);
    mat_mul_3x3(&xyz_to_dst, &src_to_xyz)
}

fn rgb_to_xyz(c: Chromaticities) -> [f32; 9] {
    let [rx, ry] = c.r;
    let [gx, gy] = c.g;
    let [bx, by] = c.b;
    let [wx, wy] = c.w;

    let rz = 1.0 - rx - ry;
    let gz = 1.0 - gx - gy;
    let bz = 1.0 - bx - by;
    let wz = 1.0 - wx - wy;

    let m = [
        rx, gx, bx,
        ry, gy, by,
        rz, gz, bz,
    ];
    let inv = invert_3x3(&m);
    let s = mat_mul_vec3(&inv, [wx / wy, 1.0, wz / wy]);

    [
        inv[0] * s[0], inv[1] * s[1], inv[2] * s[2],
        inv[3] * s[0], inv[4] * s[1], inv[5] * s[2],
        inv[6] * s[0], inv[7] * s[1], inv[8] * s[2],
    ]
}

fn xyz_to_rgb(c: Chromaticities) -> [f32; 9] {
    invert_3x3(&rgb_to_xyz(c))
}

fn mat_mul_3x3(a: &[f32; 9], b: &[f32; 9]) -> [f32; 9] {
    let mut out = [0.0; 9];
    for i in 0..3 {
        for j in 0..3 {
            out[i * 3 + j] = a[i * 3] * b[j] + a[i * 3 + 1] * b[3 + j] + a[i * 3 + 2] * b[6 + j];
        }
    }
    out
}

fn invert_3x3(m: &[f32; 9]) -> [f32; 9] {
    let det = m[0] * (m[4] * m[8] - m[5] * m[7])
            - m[1] * (m[3] * m[8] - m[5] * m[6])
            + m[2] * (m[3] * m[7] - m[4] * m[6]);
    let inv_det = 1.0 / det;
    [
        (m[4] * m[8] - m[5] * m[7]) * inv_det,
        (m[2] * m[7] - m[1] * m[8]) * inv_det,
        (m[1] * m[5] - m[2] * m[4]) * inv_det,
        (m[5] * m[6] - m[3] * m[8]) * inv_det,
        (m[0] * m[8] - m[2] * m[6]) * inv_det,
        (m[2] * m[3] - m[0] * m[5]) * inv_det,
        (m[3] * m[7] - m[4] * m[6]) * inv_det,
        (m[1] * m[6] - m[0] * m[7]) * inv_det,
        (m[0] * m[4] - m[1] * m[3]) * inv_det,
    ]
}

fn inset_primaries(
    c: Chromaticities,
    att_r: f32,
    att_g: f32,
    att_b: f32,
    rot_r: f32,
    rot_g: f32,
    rot_b: f32,
) -> Chromaticities {
    fn shift(xy: [f32; 2], w: [f32; 2], attenuation: f32, rotation_deg: f32) -> [f32; 2] {
        let dx = xy[0] - w[0];
        let dy = xy[1] - w[1];
        let dist = (dx * dx + dy * dy).sqrt();
        if dist < 1e-6 {
            return xy;
        }
        let scale = 1.0 - attenuation;
        let angle = rotation_deg * PI / 180.0;
        let cos_a = angle.cos();
        let sin_a = angle.sin();
        let rx = (dx * cos_a - dy * sin_a) * scale;
        let ry = (dx * sin_a + dy * cos_a) * scale;
        [w[0] + rx, w[1] + ry]
    }
    Chromaticities {
        r: shift(c.r, c.w, att_r, rot_r),
        g: shift(c.g, c.w, att_g, rot_g),
        b: shift(c.b, c.w, att_b, rot_b),
        w: c.w,
    }
}

#[inline]
fn log_to_lin(v: [f32; 3], tf: Transfer) -> [f32; 3] {
    let f = |x: f32| -> f32 {
        match tf {
            Transfer::Linear => x,
            Transfer::AcesCct => {
                if x <= 0.155251141552511 {
                    (x - 0.077415122655251) / 10.5402377416545
                } else {
                    2.0f32.powf((x - 0.413588402493492) * 17.52)
                }
            }
            Transfer::SonySLog3 => {
                if x < 0.01125 {
                    (x - 0.030001222851889) / 0.01125
                } else {
                    10.0f32.powf((x - 0.42) / 0.24) * 0.18
                }
            }
            Transfer::SonySLog2 => {
                if x < 0.01018 {
                    (x - 0.0245786) / 0.0183089
                } else {
                    10.0f32.powf((x - 0.384410) / 0.235443) * 0.18
                }
            }
            Transfer::ArriLogC3 => {
                if x <= 0.010591 {
                    (x - 0.005228) / 0.047491
                } else {
                    0.18 * 2.0f32.powf((x - 0.385537) / 0.247190)
                }
            }
            // ARRI LogC4 decoding (E' → E_scene). Source: ARRI
            // "LogC4 Logarithmic Color Space SPECIFICATION" (2022, Cooper &
            // Brendel). EI-independent; two-segment with a linear-to-log
            // threshold at the decoding boundary E' = 0.
            Transfer::ArriLogC4 => {
                let a: f32 = ((1u32 << 18) as f32 - 16.0) / 117.45;
                let b: f32 = (1023.0 - 95.0) / 1023.0;
                let c: f32 = 95.0 / 1023.0;
                let s: f32 = (7.0 * std::f32::consts::LN_2 * (7.0 - 14.0 * c / b).exp2()) / (a * b);
                let t: f32 = ((14.0 * (-c / b) + 6.0).exp2() - 64.0) / a;
                if x >= 0.0 {
                    ((14.0 * ((x - c) / b) + 6.0).exp2() - 64.0) / a
                } else {
                    x * s + t
                }
            }
            Transfer::RedLog3G10 => {
                if x < 0.0 {
                    0.0
                } else {
                    (10.0f32.powf(x * 17.52 - 14.0) - 0.001) / 9.999
                }
            }
            Transfer::VLog => {
                if x <= 0.010592 {
                    (x - 0.005255) / 0.047120
                } else {
                    0.18 * 2.0f32.powf((x - 0.389583) / 0.244949)
                }
            }
            Transfer::FLog2 => {
                if x < 0.0 {
                    0.0
                } else {
                    let a = 0.86445045;
                    let b = 0.5779536;
                    let c = 0.13967949;
                    let d = 0.4174028;
                    ((x - b) / a).powf(1.0 / c) - d
                }
            }
            Transfer::FLog2C => {
                if x < 0.0 {
                    0.0
                } else {
                    let a = 0.86445045;
                    let b = 0.5779536;
                    let c = 0.13967949;
                    let d = 0.4174028;
                    ((x - b) / a).powf(1.0 / c) - d
                }
            }
            Transfer::AppleLog2 => {
                if x < 0.0 {
                    0.0
                } else {
                    1.0 / (1.0 + (-4.0 * x).exp())
                }
            }
            Transfer::BmFilmGen5 => {
                if x < 0.125 {
                    x * 4.0
                } else {
                    1.0 + (x - 0.5).ln() / 0.693147
                }
            }
            Transfer::CanonLog3 => {
                if x < 0.0149 {
                    (x - 0.0721) / 3.5766
                } else {
                    0.18 * 10.0f32.powf((x - 0.406539) / 0.301940)
                }
            }
            Transfer::CanonLog2 => {
                if x < 0.0205 {
                    (x - 0.078516) / 2.4397
                } else {
                    0.18 * 10.0f32.powf((x - 0.419398) / 0.283691)
                }
            }
            Transfer::DaVinciIntermediate => {
                if x < 0.0 {
                    0.0
                } else if x < 0.5 {
                    x * x * 4.0
                } else {
                    1.0 + (x - 0.5).ln() / 0.693147
                }
            }
            Transfer::FilmlightTLog => {
                if x <= 0.0 {
                    0.0
                } else {
                    let a = 1.0 / (10.0_f32.powf(0.002) - 1.0);
                    (a + 1.0).powf(x - 1.0) - a
                }
            }
            Transfer::AgxLogKraken => {
                0.18 * 2.0f32.powf(x * 16.5 - 10.0)
            }
            Transfer::HLG => {
                if x <= 0.5 {
                    x * x * 4.0
                } else {
                    let beta = 1.09929682680944 - 1.0;
                    let gamma = 0.5;
                    ((x + beta - 1.0) / beta).powf(1.0 / gamma)
                }
            }
            Transfer::PQ => {
                if x < 0.0 {
                    0.0
                } else {
                    let n = x.cbrt();
                    let m = (10000.0_f32.powf(1.0 / 2.4) - 1.0) / 10000.0_f32.powf(1.0 / 2.4);
                    ((n - m) / (1.0 - m)).powf(2.4)
                }
            }
            Transfer::DNG => x,
            Transfer::DI => x,
        }
    };
    [f(v[0]), f(v[1]), f(v[2])]
}

#[inline]
fn lin_to_log(v: [f32; 3], tf: Transfer) -> [f32; 3] {
    let f = |x: f32| -> f32 {
        match tf {
            Transfer::Linear => x,
            Transfer::AcesCct => {
                if x <= 0.0078125 {
                    x * 10.5402377416545 + 0.077415122655251
                } else {
                    (x / 2.0).log2() / 17.52 + 0.413588402493492
                }
            }
            Transfer::SonySLog3 => {
                if x < 0.0 {
                    0.030001222851889
                } else {
                    0.24 * (x / 0.18).log10() + 0.42
                }
            }
            Transfer::SonySLog2 => {
                if x < 0.0 {
                    0.0245786
                } else {
                    0.235443 * (x / 0.18).log10() + 0.384410
                }
            }
            Transfer::ArriLogC3 => {
                if x <= 0.005228 {
                    x * 0.047491 + 0.005228
                } else {
                    0.247190 * (x / 0.18).log2() + 0.385537
                }
            }
            // ARRI LogC4 encoding (E_scene → E'). Source: ARRI "LogC4
            // Encoding Function" (2022, Cooper & Brendel). Two-segment
            // with threshold at x = t ≈ -0.0180967.
            Transfer::ArriLogC4 => {
                let a: f32 = ((1u32 << 18) as f32 - 16.0) / 117.45;
                let b: f32 = (1023.0 - 95.0) / 1023.0;
                let c: f32 = 95.0 / 1023.0;
                let s: f32 = (7.0 * std::f32::consts::LN_2 * (7.0 - 14.0 * c / b).exp2()) / (a * b);
                let t: f32 = ((14.0 * (-c / b) + 6.0).exp2() - 64.0) / a;
                if x >= t {
                    ((a * x + 64.0_f32).log2() - 6.0) / 14.0 * b + c
                } else {
                    (x - t) / s
                }
            }
            Transfer::RedLog3G10 => {
                if x < 0.0 {
                    0.0
                } else {
                    ((x * 9.999 + 0.001).log10() / 17.52) + 14.0
                }
            }
            Transfer::VLog => {
                if x <= 0.005255 {
                    x * 0.047120 + 0.005255
                } else {
                    0.244949 * (x / 0.18).log2() + 0.389583
                }
            }
            Transfer::FLog2 => {
                if x < 0.0 {
                    0.0
                } else {
                    let a = 0.86445045;
                    let b = 0.5779536;
                    let c = 0.13967949;
                    let d = 0.4174028;
                    a * (x + d).powf(c) + b
                }
            }
            Transfer::FLog2C => {
                if x < 0.0 {
                    0.0
                } else {
                    let a = 0.86445045;
                    let b = 0.5779536;
                    let c = 0.13967949;
                    let d = 0.4174028;
                    a * (x + d).powf(c) + b
                }
            }
            Transfer::AppleLog2 => {
                (-(1.0 / x - 1.0).ln()) / 4.0
            }
            Transfer::BmFilmGen5 => {
                if x < 0.5 {
                    x / 4.0
                } else {
                    0.5 + 0.693147 * (x - 1.0).exp()
                }
            }
            Transfer::CanonLog3 => {
                if x < 0.00390625 {
                    x * 3.5766 + 0.0721
                } else {
                    0.301940 * (x / 0.18).log10() + 0.406539
                }
            }
            Transfer::CanonLog2 => {
                if x < 0.00390625 {
                    x * 2.4397 + 0.078516
                } else {
                    0.283691 * (x / 0.18).log10() + 0.419398
                }
            }
            Transfer::DaVinciIntermediate => {
                if x < 0.0 {
                    0.0
                } else if x < 0.25 {
                    x.sqrt() / 2.0
                } else {
                    0.5 + 0.693147 * (x - 1.0).exp()
                }
            }
            Transfer::FilmlightTLog => {
                if x <= 0.0 {
                    0.0
                } else {
                    let a = 1.0 / (10.0_f32.powf(0.002) - 1.0);
                    1.0 + (x / a + 1.0).ln() / (a + 1.0).ln()
                }
            }
            Transfer::AgxLogKraken => {
                if x <= 0.0 {
                    0.0
                } else {
                    let ev = (x / 0.18).log2().clamp(-10.0, 6.5);
                    (ev + 10.0) / 16.5
                }
            }
            Transfer::HLG => {
                if x < 0.0 {
                    0.0
                } else if x <= 0.5 {
                    x.sqrt() / 2.0
                } else {
                    let beta = 1.09929682680944 - 1.0;
                    let gamma = 0.5;
                    beta * (x).powf(gamma) + 1.0 - beta
                }
            }
            Transfer::PQ => {
                if x < 0.0 {
                    0.0
                } else {
                    let m = (10000.0_f32.powf(1.0 / 2.4) - 1.0) / 10000.0_f32.powf(1.0 / 2.4);
                    1.0 - (x.max(0.0).cbrt() * (1.0 - m) - m).abs()
                }
            }
            Transfer::DNG => x,
            Transfer::DI => x,
        }
    };
    [f(v[0]), f(v[1]), f(v[2])]
}

#[inline]
fn inverse_eotf(v: [f32; 3], gamma: f32) -> [f32; 3] {
    if gamma == 1.0 {
        return v;
    }
    let g = 1.0 / gamma;
    [v[0].powf(g), v[1].powf(g), v[2].powf(g)]
}

#[inline]
fn spowf(a: f32, b: f32) -> f32 {
    let s = if a > 0.0 { 1.0 } else if a < 0.0 { -1.0 } else { 0.0 };
    s * a.abs().powf(b)
}

#[inline]
fn tone_scale(x: f32, shoulder: f32, toe: f32, slope: f32, lmg: f32, mg: f32, s0: f32, t0: f32) -> f32 {
    let ss = spowf(
        (spowf(slope * (s0 - lmg) / (1.0 - mg), shoulder) - 1.0) * spowf(slope * (s0 - lmg), -shoulder),
        -1.0 / shoulder,
    );
    let ms = slope * (x - lmg) / ss;
    let fs = ms / spowf(1.0 + spowf(ms, shoulder), 1.0 / shoulder);

    let ts = spowf(
        (spowf(slope * (lmg - t0) / mg, toe) - 1.0) * spowf(slope * (lmg - t0), -toe),
        -1.0 / toe,
    );
    let mr = slope * (x - lmg) / (-ts);
    let ft = mr / spowf(1.0 + spowf(mr, toe), 1.0 / toe);

    if x >= lmg { ss * fs + mg } else { -ts * ft + mg }
}

impl From<crate::color::TransferFunction> for Transfer {
    fn from(tf: crate::color::TransferFunction) -> Self {
        match tf {
            crate::color::TransferFunction::Linear => Transfer::Linear,
            crate::color::TransferFunction::Rec709 => Transfer::Linear,
            crate::color::TransferFunction::SLog3 => Transfer::SonySLog3,
            crate::color::TransferFunction::VLog => Transfer::VLog,
            crate::color::TransferFunction::ARRIlog3 => Transfer::ArriLogC3,
            crate::color::TransferFunction::ARRIlog4 => Transfer::ArriLogC4,
            crate::color::TransferFunction::CLog3 => Transfer::CanonLog3,
            crate::color::TransferFunction::FLog2 => Transfer::FLog2,
            crate::color::TransferFunction::AppleLog | crate::color::TransferFunction::AppleLog2 => Transfer::Linear,
            crate::color::TransferFunction::ACESCCT => Transfer::AcesCct,
            crate::color::TransferFunction::HLG => Transfer::HLG,
            crate::color::TransferFunction::PQ => Transfer::PQ,
            crate::color::TransferFunction::DaVinciIntermediate => Transfer::DaVinciIntermediate,
            crate::color::TransferFunction::Gamma24 => Transfer::Linear,
        }
    }
}

impl From<crate::color::ColorSpace> for Gamut {
    fn from(cs: crate::color::ColorSpace) -> Self {
        match cs {
            crate::color::ColorSpace::Rec709 => Gamut::Rec709,
            crate::color::ColorSpace::Rec2020 => Gamut::Rec2020,
            crate::color::ColorSpace::DciP3 => Gamut::P3D65,
            crate::color::ColorSpace::Srgb => Gamut::Rec709,
            crate::color::ColorSpace::SGamut3Cine => Gamut::SGamut3Cine,
            crate::color::ColorSpace::SGamut3 => Gamut::SGamut3,
            crate::color::ColorSpace::ARRIWideGamut3 => Gamut::Awg3,
            crate::color::ColorSpace::ARRIWideGamut4 => Gamut::Awg4,
            crate::color::ColorSpace::CanonCinemaGamut => Gamut::CanonCinema,
            crate::color::ColorSpace::PanasonicVGamut => Gamut::Rwg,
            crate::color::ColorSpace::ACESAP1 => Gamut::Ap1,
            crate::color::ColorSpace::FGamut => Gamut::Rwg,
            crate::color::ColorSpace::FGamutC => Gamut::Ap0,
            crate::color::ColorSpace::DaVinciWideGamut => Gamut::DaVinciWg,
            crate::color::ColorSpace::DisplayP3 => Gamut::P3D65,
        }
    }
}

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

    #[test]
    fn test_agx_pipeline_creation() {
        let cfg = AgxConfig::default();
        let pipe = AgxPipeline::new(cfg);
        assert!(pipe.inset_mat.iter().any(|&v| v != 0.0));
    }

    #[test]
    fn test_mid_grey_pivot() {
        let cfg = AgxConfig::default();
        let pipe = AgxPipeline::new(cfg);
        let out = pipe.process_pixel(0.18, 0.18, 0.18);
        assert!((out[0] - 0.5).abs() < 0.01, "mid grey: expected ~0.5, got {}", out[0]);
    }

    #[test]
    fn test_black_output() {
        let cfg = AgxConfig::default();
        let pipe = AgxPipeline::new(cfg);
        let out = pipe.process_pixel(0.0, 0.0, 0.0);
        assert!(out[0] < 0.001, "black: expected near 0, got {}", out[0]);
    }

    #[test]
    fn test_white_clip() {
        let cfg = AgxConfig::default();
        let pipe = AgxPipeline::new(cfg);
        let out = pipe.process_pixel(10.0, 10.0, 10.0);
        assert!(out[0] < 1.0 && out[0] > 0.9, "white: expected near 1.0, got {}", out[0]);
    }

    #[test]
    fn test_gamut_conversion_rec2020_to_rec709() {
        let mut cfg = AgxConfig::default();
        cfg.in_gamut = Gamut::Rec2020;
        cfg.out_gamut = Gamut::Rec709;
        
        let pipe = AgxPipeline::new(cfg);
        let out = pipe.process_pixel(0.5, 0.5, 0.5);
        
        assert!(out[0] >= 0.0 && out[0] <= 1.0);
        assert!(out[1] >= 0.0 && out[1] <= 1.0);
        assert!(out[2] >= 0.0 && out[2] <= 1.0);
    }
}