/*
 * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved.
 * //
 * // Redistribution and use in source and binary forms, with or without modification,
 * // are permitted provided that the following conditions are met:
 * //
 * // 1.  Redistributions of source code must retain the above copyright notice, this
 * // list of conditions and the following disclaimer.
 * //
 * // 2.  Redistributions in binary form must reproduce the above copyright notice,
 * // this list of conditions and the following disclaimer in the documentation
 * // and/or other materials provided with the distribution.
 * //
 * // 3.  Neither the name of the copyright holder nor the names of its
 * // contributors may be used to endorse or promote products derived from
 * // this software without specific prior written permission.
 * //
 * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
use crate::cicp::create_rec709_parametric;
use crate::matan::is_curve_linear16;
use crate::math::m_clamp;
use crate::mlaf::{mlaf, neg_mlaf};
use crate::transform::PointeeSizeExpressible;
use crate::writer::FloatToFixedU8Fixed8;
use crate::{CmsError, ColorProfile, DataColorSpace, Rgb, TransferCharacteristics};
use num_traits::AsPrimitive;
use pxfm::{dirty_powf, f_pow, f_powf};

#[derive(Clone, Debug)]
pub enum ToneReprCurve {
    Lut(Vec<u16>),
    Parametric(Vec<f32>),
}

impl ToneReprCurve {
    pub fn inverse(&self) -> Result<ToneReprCurve, CmsError> {
        match self {
            ToneReprCurve::Lut(lut) => {
                let inverse_length = lut.len().max(256);
                Ok(ToneReprCurve::Lut(invert_lut(lut, inverse_length)))
            }
            ToneReprCurve::Parametric(parametric) => ParametricCurve::new(parametric)
                .and_then(|x| x.invert())
                .map(|x| ToneReprCurve::Parametric([x.g, x.a, x.b, x.c, x.d, x.e, x.f].to_vec()))
                .ok_or(CmsError::BuildTransferFunction),
        }
    }

    /// Creates tone curve evaluator
    pub fn make_linear_evaluator(
        &self,
    ) -> Result<Box<dyn ToneCurveEvaluator + Send + Sync>, CmsError> {
        match self {
            ToneReprCurve::Lut(lut) => {
                if lut.is_empty() {
                    return Ok(Box::new(ToneCurveEvaluatorLinear {}));
                }
                if lut.len() == 1 {
                    let gamma = u8_fixed_8number_to_float(lut[0]);
                    return Ok(Box::new(ToneCurveEvaluatorPureGamma { gamma }));
                }
                let converted_curve = lut.iter().map(|&x| x as f32 / 65535.0).collect::<Vec<_>>();
                Ok(Box::new(ToneCurveLutEvaluator {
                    lut: converted_curve,
                }))
            }
            ToneReprCurve::Parametric(parametric) => {
                let parametric_curve =
                    ParametricCurve::new(parametric).ok_or(CmsError::BuildTransferFunction)?;
                Ok(Box::new(ToneCurveParametricEvaluator {
                    parametric: parametric_curve,
                }))
            }
        }
    }

    /// Creates tone curve evaluator from transfer characteristics
    pub fn make_cicp_linear_evaluator(
        transfer_characteristics: TransferCharacteristics,
    ) -> Result<Box<dyn ToneCurveEvaluator + Send + Sync>, CmsError> {
        if !transfer_characteristics.has_transfer_curve() {
            return Err(CmsError::BuildTransferFunction);
        }
        Ok(Box::new(ToneCurveCicpLinearEvaluator {
            trc: transfer_characteristics,
        }))
    }

    /// Creates tone curve inverse evaluator
    pub fn make_gamma_evaluator(
        &self,
    ) -> Result<Box<dyn ToneCurveEvaluator + Send + Sync>, CmsError> {
        match self {
            ToneReprCurve::Lut(lut) => {
                if lut.is_empty() {
                    return Ok(Box::new(ToneCurveEvaluatorLinear {}));
                }
                if lut.len() == 1 {
                    let gamma = 1. / u8_fixed_8number_to_float(lut[0]);
                    return Ok(Box::new(ToneCurveEvaluatorPureGamma { gamma }));
                }
                let inverted_lut = invert_lut(lut, 16384);
                let converted_curve = inverted_lut
                    .iter()
                    .map(|&x| x as f32 / 65535.0)
                    .collect::<Vec<_>>();
                Ok(Box::new(ToneCurveLutEvaluator {
                    lut: converted_curve,
                }))
            }
            ToneReprCurve::Parametric(parametric) => {
                let parametric_curve = ParametricCurve::new(parametric)
                    .and_then(|x| x.invert())
                    .ok_or(CmsError::BuildTransferFunction)?;
                Ok(Box::new(ToneCurveParametricEvaluator {
                    parametric: parametric_curve,
                }))
            }
        }
    }

    /// Creates tone curve inverse evaluator from transfer characteristics
    pub fn make_cicp_gamma_evaluator(
        transfer_characteristics: TransferCharacteristics,
    ) -> Result<Box<dyn ToneCurveEvaluator + Send + Sync>, CmsError> {
        if !transfer_characteristics.has_transfer_curve() {
            return Err(CmsError::BuildTransferFunction);
        }
        Ok(Box::new(ToneCurveCicpGammaEvaluator {
            trc: transfer_characteristics,
        }))
    }
}

struct ToneCurveCicpLinearEvaluator {
    trc: TransferCharacteristics,
}

struct ToneCurveCicpGammaEvaluator {
    trc: TransferCharacteristics,
}

impl ToneCurveEvaluator for ToneCurveCicpLinearEvaluator {
    fn evaluate_tristimulus(&self, rgb: Rgb<f32>) -> Rgb<f32> {
        Rgb::new(
            self.trc.linearize(rgb.r as f64) as f32,
            self.trc.linearize(rgb.g as f64) as f32,
            self.trc.linearize(rgb.b as f64) as f32,
        )
    }

    fn evaluate_value(&self, value: f32) -> f32 {
        self.trc.linearize(value as f64) as f32
    }
}

impl ToneCurveEvaluator for ToneCurveCicpGammaEvaluator {
    fn evaluate_tristimulus(&self, rgb: Rgb<f32>) -> Rgb<f32> {
        Rgb::new(
            self.trc.gamma(rgb.r as f64) as f32,
            self.trc.gamma(rgb.g as f64) as f32,
            self.trc.gamma(rgb.b as f64) as f32,
        )
    }

    fn evaluate_value(&self, value: f32) -> f32 {
        self.trc.gamma(value as f64) as f32
    }
}

struct ToneCurveLutEvaluator {
    lut: Vec<f32>,
}

impl ToneCurveEvaluator for ToneCurveLutEvaluator {
    fn evaluate_value(&self, value: f32) -> f32 {
        lut_interp_linear_float(value, &self.lut)
    }

    fn evaluate_tristimulus(&self, rgb: Rgb<f32>) -> Rgb<f32> {
        Rgb::new(
            lut_interp_linear_float(rgb.r, &self.lut),
            lut_interp_linear_float(rgb.g, &self.lut),
            lut_interp_linear_float(rgb.b, &self.lut),
        )
    }
}

pub(crate) fn build_trc_table(num_entries: i32, eotf: impl Fn(f64) -> f64) -> Vec<u16> {
    let mut table = vec![0u16; num_entries as usize];

    for (i, table_value) in table.iter_mut().enumerate() {
        let x: f64 = i as f64 / (num_entries - 1) as f64;
        let y: f64 = eotf(x);
        let mut output: f64;
        output = y * 65535.0 + 0.5;
        if output > 65535.0 {
            output = 65535.0
        }
        if output < 0.0 {
            output = 0.0
        }
        *table_value = output.floor() as u16;
    }
    table
}

/// Creates Tone Reproduction curve from gamma
pub fn curve_from_gamma(gamma: f32) -> ToneReprCurve {
    ToneReprCurve::Lut(vec![gamma.to_u8_fixed8()])
}

#[derive(Debug)]
struct ParametricCurve {
    g: f32,
    a: f32,
    b: f32,
    c: f32,
    d: f32,
    e: f32,
    f: f32,
}

impl ParametricCurve {
    #[allow(clippy::many_single_char_names)]
    fn new(params: &[f32]) -> Option<ParametricCurve> {
        // convert from the variable number of parameters
        // contained in profiles to a unified representation.
        let g: f32 = params[0];
        match params[1..] {
            [] => Some(ParametricCurve {
                g,
                a: 1.,
                b: 0.,
                c: 1.,
                d: 0.,
                e: 0.,
                f: 0.,
            }),
            [a, b] => Some(ParametricCurve {
                g,
                a,
                b,
                c: 0.,
                d: -b / a,
                e: 0.,
                f: 0.,
            }),
            [a, b, c] => Some(ParametricCurve {
                g,
                a,
                b,
                c: 0.,
                d: -b / a,
                e: c,
                f: c,
            }),
            [a, b, c, d] => Some(ParametricCurve {
                g,
                a,
                b,
                c,
                d,
                e: 0.,
                f: 0.,
            }),
            [a, b, c, d, e, f] => Some(ParametricCurve {
                g,
                a,
                b,
                c,
                d,
                e,
                f,
            }),
            _ => None,
        }
    }

    fn is_linear(&self) -> bool {
        (self.g - 1.0).abs() < 1e-5
            && (self.a - 1.0).abs() < 1e-5
            && self.b.abs() < 1e-5
            && self.c.abs() < 1e-5
    }

    fn eval(&self, x: f32) -> f32 {
        if x < self.d {
            self.c * x + self.f
        } else {
            f_powf(self.a * x + self.b, self.g) + self.e
        }
    }

    #[allow(dead_code)]
    #[allow(clippy::many_single_char_names)]
    fn invert(&self) -> Option<ParametricCurve> {
        // First check if the function is continuous at the cross-over point d.
        let d1 = f_powf(self.a * self.d + self.b, self.g) + self.e;
        let d2 = self.c * self.d + self.f;

        if (d1 - d2).abs() > 0.1 {
            return None;
        }
        let d = d1;

        // y = (a * x + b)^g + e
        // y - e = (a * x + b)^g
        // (y - e)^(1/g) = a*x + b
        // (y - e)^(1/g) - b = a*x
        // (y - e)^(1/g)/a - b/a = x
        // ((y - e)/a^g)^(1/g) - b/a = x
        // ((1/(a^g)) * y - e/(a^g))^(1/g) - b/a = x
        let a = 1. / f_powf(self.a, self.g);
        let b = -self.e / f_powf(self.a, self.g);
        let g = 1. / self.g;
        let e = -self.b / self.a;

        // y = c * x + f
        // y - f = c * x
        // y/c - f/c = x
        let (c, f);
        if d <= 0. {
            c = 1.;
            f = 0.;
        } else {
            c = 1. / self.c;
            f = -self.f / self.c;
        }

        // if self.d > 0. and self.c == 0 as is likely with type 1 and 2 parametric function
        // then c and f will not be finite.
        if !(g.is_finite()
            && a.is_finite()
            && b.is_finite()
            && c.is_finite()
            && d.is_finite()
            && e.is_finite()
            && f.is_finite())
        {
            return None;
        }

        Some(ParametricCurve {
            g,
            a,
            b,
            c,
            d,
            e,
            f,
        })
    }
}

#[inline]
pub(crate) fn u8_fixed_8number_to_float(x: u16) -> f32 {
    // 0x0000 = 0.
    // 0x0100 = 1.
    // 0xffff = 255  + 255/256
    (x as i32 as f64 / 256.0) as f32
}

fn passthrough_table<T: PointeeSizeExpressible, const N: usize, const BIT_DEPTH: usize>()
-> Box<[f32; N]> {
    let mut gamma_table = Box::new([0f32; N]);
    let max_value = if T::FINITE {
        (1 << BIT_DEPTH) - 1
    } else {
        T::NOT_FINITE_LINEAR_TABLE_SIZE - 1
    };
    let cap_values = if T::FINITE {
        (1u32 << BIT_DEPTH) as usize
    } else {
        T::NOT_FINITE_LINEAR_TABLE_SIZE
    };
    assert!(cap_values <= N, "Invalid lut table construction");
    let scale_value = 1f64 / max_value as f64;
    for (i, g) in gamma_table.iter_mut().enumerate().take(cap_values) {
        *g = (i as f64 * scale_value) as f32;
    }

    gamma_table
}

fn linear_forward_table<T: PointeeSizeExpressible, const N: usize, const BIT_DEPTH: usize>(
    gamma: u16,
) -> Box<[f32; N]> {
    let mut gamma_table = Box::new([0f32; N]);
    let gamma_float: f32 = u8_fixed_8number_to_float(gamma);
    let max_value = if T::FINITE {
        (1 << BIT_DEPTH) - 1
    } else {
        T::NOT_FINITE_LINEAR_TABLE_SIZE - 1
    };
    let cap_values = if T::FINITE {
        (1u32 << BIT_DEPTH) as usize
    } else {
        T::NOT_FINITE_LINEAR_TABLE_SIZE
    };
    assert!(cap_values <= N, "Invalid lut table construction");
    let scale_value = 1f64 / max_value as f64;
    for (i, g) in gamma_table.iter_mut().enumerate().take(cap_values) {
        *g = f_pow(i as f64 * scale_value, gamma_float as f64) as f32;
    }

    gamma_table
}

#[inline(always)]
pub(crate) fn lut_interp_linear_float(x: f32, table: &[f32]) -> f32 {
    let value = x.min(1.).max(0.) * (table.len() - 1) as f32;

    let upper: i32 = value.ceil() as i32;
    let lower: i32 = value.floor() as i32;

    let diff = upper as f32 - value;
    let tu = table[upper as usize];
    mlaf(neg_mlaf(tu, tu, diff), table[lower as usize], diff)
}

/// Lut interpolation float where values is already clamped
#[inline(always)]
#[allow(dead_code)]
pub(crate) fn lut_interp_linear_float_clamped(x: f32, table: &[f32]) -> f32 {
    let value = x * (table.len() - 1) as f32;

    let upper: i32 = value.ceil() as i32;
    let lower: i32 = value.floor() as i32;

    let diff = upper as f32 - value;
    let tu = table[upper as usize];
    mlaf(neg_mlaf(tu, tu, diff), table[lower as usize], diff)
}

#[inline]
pub(crate) fn lut_interp_linear(input_value: f64, table: &[u16]) -> f32 {
    let mut input_value = input_value;
    if table.is_empty() {
        return input_value as f32;
    }

    input_value *= (table.len() - 1) as f64;

    let upper: i32 = input_value.ceil() as i32;
    let lower: i32 = input_value.floor() as i32;
    let w0 = table[(upper as usize).min(table.len() - 1)] as f64;
    let w1 = 1. - (upper as f64 - input_value);
    let w2 = table[(lower as usize).min(table.len() - 1)] as f64;
    let w3 = upper as f64 - input_value;
    let value: f32 = mlaf(w2 * w3, w0, w1) as f32;
    value * (1.0 / 65535.0)
}

fn linear_lut_interpolate<T: PointeeSizeExpressible, const N: usize, const BIT_DEPTH: usize>(
    table: &[u16],
) -> Box<[f32; N]> {
    let mut gamma_table = Box::new([0f32; N]);
    let max_value = if T::FINITE {
        (1 << BIT_DEPTH) - 1
    } else {
        T::NOT_FINITE_LINEAR_TABLE_SIZE - 1
    };
    let cap_values = if T::FINITE {
        (1u32 << BIT_DEPTH) as usize
    } else {
        T::NOT_FINITE_LINEAR_TABLE_SIZE
    };
    assert!(cap_values <= N, "Invalid lut table construction");
    let scale_value = 1f64 / max_value as f64;
    for (i, g) in gamma_table.iter_mut().enumerate().take(cap_values) {
        *g = lut_interp_linear(i as f64 * scale_value, table);
    }
    gamma_table
}

fn linear_curve_parametric<T: PointeeSizeExpressible, const N: usize, const BIT_DEPTH: usize>(
    params: &[f32],
) -> Option<Box<[f32; N]>> {
    let params = ParametricCurve::new(params)?;
    let mut gamma_table = Box::new([0f32; N]);
    let max_value = if T::FINITE {
        (1 << BIT_DEPTH) - 1
    } else {
        T::NOT_FINITE_LINEAR_TABLE_SIZE - 1
    };
    let cap_value = if T::FINITE {
        1 << BIT_DEPTH
    } else {
        T::NOT_FINITE_LINEAR_TABLE_SIZE
    };
    let scale_value = 1f32 / max_value as f32;
    for (i, g) in gamma_table.iter_mut().enumerate().take(cap_value) {
        let x = i as f32 * scale_value;
        *g = m_clamp(params.eval(x), 0.0, 1.0);
    }
    Some(gamma_table)
}

fn linear_curve_parametric_s<const N: usize>(params: &[f32]) -> Option<Box<[f32; N]>> {
    let params = ParametricCurve::new(params)?;
    let mut gamma_table = Box::new([0f32; N]);
    let scale_value = 1f32 / (N - 1) as f32;
    for (i, g) in gamma_table.iter_mut().enumerate().take(N) {
        let x = i as f32 * scale_value;
        *g = m_clamp(params.eval(x), 0.0, 1.0);
    }
    Some(gamma_table)
}

pub(crate) fn make_gamma_linear_table<
    T: Default + Copy + 'static + PointeeSizeExpressible,
    const BUCKET: usize,
    const N: usize,
>(
    bit_depth: usize,
) -> Box<[T; BUCKET]>
where
    f32: AsPrimitive<T>,
{
    let mut table = Box::new([T::default(); BUCKET]);
    let max_range = if T::FINITE {
        (1f64 / ((N - 1) as f64 / (1 << bit_depth) as f64)) as f32
    } else {
        (1f64 / ((N - 1) as f64)) as f32
    };
    for (v, output) in table.iter_mut().take(N).enumerate() {
        if T::FINITE {
            *output = (v as f32 * max_range).round().as_();
        } else {
            *output = (v as f32 * max_range).as_();
        }
    }
    table
}

#[inline]
fn lut_interp_linear_gamma_impl<
    T: Default + Copy + 'static + PointeeSizeExpressible,
    const N: usize,
    const BIT_DEPTH: usize,
>(
    input_value: u32,
    table: &[u16],
) -> T
where
    u32: AsPrimitive<T>,
{
    // Start scaling input_value to the length of the array: GAMMA_CAP*(length-1).
    // We'll divide out the GAMMA_CAP next
    let mut value: u32 = input_value * (table.len() - 1) as u32;
    let cap_value = N - 1;
    // equivalent to ceil(value/GAMMA_CAP)
    let upper: u32 = value.div_ceil(cap_value as u32);
    // equivalent to floor(value/GAMMA_CAP)
    let lower: u32 = value / cap_value as u32;
    // interp is the distance from upper to value scaled to 0..GAMMA_CAP
    let interp: u32 = value % cap_value as u32;
    let lw_value = table[lower as usize];
    let hw_value = table[upper as usize];
    // the table values range from 0..65535
    value = mlaf(
        hw_value as u32 * interp,
        lw_value as u32,
        (N - 1) as u32 - interp,
    ); // 0..(65535*GAMMA_CAP)

    // round and scale
    let max_colors = if T::FINITE { (1 << BIT_DEPTH) - 1 } else { 1 };
    value += (cap_value * 65535 / max_colors / 2) as u32; // scale to 0...max_colors
    value /= (cap_value * 65535 / max_colors) as u32;
    value.as_()
}

#[inline]
fn lut_interp_linear_gamma_impl_f32<
    T: Default + Copy + 'static + PointeeSizeExpressible,
    const N: usize,
    const BIT_DEPTH: usize,
>(
    input_value: u32,
    table: &[u16],
) -> T
where
    f32: AsPrimitive<T>,
{
    // Start scaling input_value to the length of the array: GAMMA_CAP*(length-1).
    // We'll divide out the GAMMA_CAP next
    let guess: u32 = input_value * (table.len() - 1) as u32;
    let cap_value = N - 1;
    // equivalent to ceil(value/GAMMA_CAP)
    let upper: u32 = guess.div_ceil(cap_value as u32);
    // equivalent to floor(value/GAMMA_CAP)
    let lower: u32 = guess / cap_value as u32;
    // interp is the distance from upper to value scaled to 0..GAMMA_CAP
    let interp: u32 = guess % cap_value as u32;
    let lw_value = table[lower as usize];
    let hw_value = table[upper as usize];
    // the table values range from 0..65535
    let mut value = mlaf(
        hw_value as f32 * interp as f32,
        lw_value as f32,
        (N - 1) as f32 - interp as f32,
    ); // 0..(65535*GAMMA_CAP)

    // round and scale
    let max_colors = if T::FINITE { (1 << BIT_DEPTH) - 1 } else { 1 };
    value /= (cap_value * 65535 / max_colors) as f32;
    value.as_()
}

#[doc(hidden)]
pub trait GammaLutInterpolate {
    fn gamma_lut_interp<
        T: Default + Copy + 'static + PointeeSizeExpressible,
        const N: usize,
        const BIT_DEPTH: usize,
    >(
        input_value: u32,
        table: &[u16],
    ) -> T
    where
        u32: AsPrimitive<T>,
        f32: AsPrimitive<T>;
}

macro_rules! gamma_lut_interp_fixed {
    ($i_type: ident) => {
        impl GammaLutInterpolate for $i_type {
            #[inline]
            fn gamma_lut_interp<
                T: Default + Copy + 'static + PointeeSizeExpressible,
                const N: usize,
                const BIT_DEPTH: usize,
            >(
                input_value: u32,
                table: &[u16],
            ) -> T
            where
                u32: AsPrimitive<T>,
            {
                lut_interp_linear_gamma_impl::<T, N, BIT_DEPTH>(input_value, table)
            }
        }
    };
}

gamma_lut_interp_fixed!(u8);
gamma_lut_interp_fixed!(u16);

macro_rules! gammu_lut_interp_float {
    ($f_type: ident) => {
        impl GammaLutInterpolate for $f_type {
            #[inline]
            fn gamma_lut_interp<
                T: Default + Copy + 'static + PointeeSizeExpressible,
                const N: usize,
                const BIT_DEPTH: usize,
            >(
                input_value: u32,
                table: &[u16],
            ) -> T
            where
                f32: AsPrimitive<T>,
                u32: AsPrimitive<T>,
            {
                lut_interp_linear_gamma_impl_f32::<T, N, BIT_DEPTH>(input_value, table)
            }
        }
    };
}

gammu_lut_interp_float!(f32);
gammu_lut_interp_float!(f64);

pub(crate) fn make_gamma_lut<
    T: Default + Copy + 'static + PointeeSizeExpressible + GammaLutInterpolate,
    const BUCKET: usize,
    const N: usize,
    const BIT_DEPTH: usize,
>(
    table: &[u16],
) -> Box<[T; BUCKET]>
where
    u32: AsPrimitive<T>,
    f32: AsPrimitive<T>,
{
    let mut new_table = Box::new([T::default(); BUCKET]);
    for (v, output) in new_table.iter_mut().take(N).enumerate() {
        *output = T::gamma_lut_interp::<T, N, BIT_DEPTH>(v as u32, table);
    }
    new_table
}

#[inline]
pub(crate) fn lut_interp_linear16(input_value: u16, table: &[u16]) -> u16 {
    // Start scaling input_value to the length of the array: 65535*(length-1).
    // We'll divide out the 65535 next
    let mut value: u32 = input_value as u32 * (table.len() as u32 - 1);
    let upper: u16 = value.div_ceil(65535) as u16; // equivalent to ceil(value/65535)
    let lower: u16 = (value / 65535) as u16; // equivalent to floor(value/65535)
    // interp is the distance from upper to value scaled to 0..65535
    let interp: u32 = value % 65535; // 0..65535*65535
    value = (table[upper as usize] as u32 * interp
        + table[lower as usize] as u32 * (65535 - interp))
        / 65535;
    value as u16
}

#[inline]
pub(crate) fn lut_interp_linear16_boxed<const N: usize>(input_value: u16, table: &[u16; N]) -> u16 {
    // Start scaling input_value to the length of the array: 65535*(length-1).
    // We'll divide out the 65535 next
    let mut value: u32 = input_value as u32 * (table.len() as u32 - 1);
    let upper: u16 = value.div_ceil(65535) as u16; // equivalent to ceil(value/65535)
    let lower: u16 = (value / 65535) as u16; // equivalent to floor(value/65535)
    // interp is the distance from upper to value scaled to 0..65535
    let interp: u32 = value % 65535; // 0..65535*65535
    value = (table[upper as usize] as u32 * interp
        + table[lower as usize] as u32 * (65535 - interp))
        / 65535;
    value as u16
}

fn make_gamma_pow_table<
    T: Default + Copy + 'static + PointeeSizeExpressible,
    const BUCKET: usize,
    const N: usize,
>(
    gamma: f32,
    bit_depth: usize,
) -> Box<[T; BUCKET]>
where
    f32: AsPrimitive<T>,
{
    let mut table = Box::new([T::default(); BUCKET]);
    let scale = 1f32 / (N - 1) as f32;
    let cap = ((1 << bit_depth) - 1) as f32;
    if T::FINITE {
        for (v, output) in table.iter_mut().take(N).enumerate() {
            *output = (cap * f_powf(v as f32 * scale, gamma)).round().as_();
        }
    } else {
        for (v, output) in table.iter_mut().take(N).enumerate() {
            *output = (cap * f_powf(v as f32 * scale, gamma)).as_();
        }
    }
    table
}

fn make_gamma_parametric_table<
    T: Default + Copy + 'static + PointeeSizeExpressible,
    const BUCKET: usize,
    const N: usize,
    const BIT_DEPTH: usize,
>(
    parametric_curve: ParametricCurve,
) -> Box<[T; BUCKET]>
where
    f32: AsPrimitive<T>,
{
    let mut table = Box::new([T::default(); BUCKET]);
    let scale = 1f32 / (N - 1) as f32;
    let cap = ((1 << BIT_DEPTH) - 1) as f32;
    if T::FINITE {
        for (v, output) in table.iter_mut().take(N).enumerate() {
            *output = (cap * parametric_curve.eval(v as f32 * scale))
                .round()
                .as_();
        }
    } else {
        for (v, output) in table.iter_mut().take(N).enumerate() {
            *output = (cap * parametric_curve.eval(v as f32 * scale)).as_();
        }
    }
    table
}

#[inline]
fn compare_parametric(src: &[f32], dst: &[f32]) -> bool {
    for (src, dst) in src.iter().zip(dst.iter()) {
        if (src - dst).abs() > 1e-4 {
            return false;
        }
    }
    true
}

fn lut_inverse_interp16(value: u16, lut_table: &[u16]) -> u16 {
    let mut l: i32 = 1; // 'int' Give spacing for negative values
    let mut r: i32 = 0x10000;
    let mut x: i32 = 0;
    let mut res: i32;
    let length = lut_table.len() as i32;

    let mut num_zeroes: i32 = 0;
    for &item in lut_table.iter() {
        if item == 0 {
            num_zeroes += 1
        } else {
            break;
        }
    }

    if num_zeroes == 0 && value as i32 == 0 {
        return 0u16;
    }
    let mut num_of_polys: i32 = 0;
    for &item in lut_table.iter().rev() {
        if item == 0xffff {
            num_of_polys += 1
        } else {
            break;
        }
    }
    // Does the curve belong to this case?
    if num_zeroes > 1 || num_of_polys > 1 {
        let a_0: i32;
        let b_0: i32;
        // Identify if value fall downto 0 or FFFF zone
        if value as i32 == 0 {
            return 0u16;
        }
        // if (Value == 0xFFFF) return 0xFFFF;
        // else restrict to valid zone
        if num_zeroes > 1 {
            a_0 = (num_zeroes - 1) * 0xffff / (length - 1);
            l = a_0 - 1
        }
        if num_of_polys > 1 {
            b_0 = (length - 1 - num_of_polys) * 0xffff / (length - 1);
            r = b_0 + 1
        }
    }
    if r <= l {
        // If this happens LutTable is not invertible
        return 0u16;
    }

    while r > l {
        x = (l + r) / 2;
        res = lut_interp_linear16((x - 1) as u16, lut_table) as i32;
        if res == value as i32 {
            // Found exact match.
            return (x - 1) as u16;
        }
        if res > value as i32 {
            r = x - 1
        } else {
            l = x + 1
        }
    }

    // Not found, should we interpolate?

    // Get surrounding nodes
    debug_assert!(x >= 1);

    let val2: f64 = (length - 1) as f64 * ((x - 1) as f64 / 65535.0);
    let cell0: i32 = val2.floor() as i32;
    let cell1: i32 = val2.ceil() as i32;
    if cell0 == cell1 {
        return x as u16;
    }

    let y0: f64 = lut_table[cell0 as usize] as f64;
    let x0: f64 = 65535.0 * cell0 as f64 / (length - 1) as f64;
    let y1: f64 = lut_table[cell1 as usize] as f64;
    let x1: f64 = 65535.0 * cell1 as f64 / (length - 1) as f64;
    let a: f64 = (y1 - y0) / (x1 - x0);
    let b: f64 = mlaf(y0, -a, x0);
    if a.abs() < 0.01f64 {
        return x as u16;
    }
    let f: f64 = (value as i32 as f64 - b) / a;
    if f < 0.0 {
        return 0u16;
    }
    if f >= 65535.0 {
        return 0xffffu16;
    }
    (f + 0.5f64).floor() as u16
}

fn lut_inverse_interp16_boxed<const N: usize>(value: u16, lut_table: &[u16; N]) -> u16 {
    let mut l: i32 = 1; // 'int' Give spacing for negative values
    let mut r: i32 = 0x10000;
    let mut x: i32 = 0;
    let mut res: i32;
    let length = lut_table.len() as i32;

    let mut num_zeroes: i32 = 0;
    for &item in lut_table.iter() {
        if item == 0 {
            num_zeroes += 1
        } else {
            break;
        }
    }

    if num_zeroes == 0 && value as i32 == 0 {
        return 0u16;
    }
    let mut num_of_polys: i32 = 0;
    for &item in lut_table.iter().rev() {
        if item == 0xffff {
            num_of_polys += 1
        } else {
            break;
        }
    }
    // Does the curve belong to this case?
    if num_zeroes > 1 || num_of_polys > 1 {
        let a_0: i32;
        let b_0: i32;
        // Identify if value fall downto 0 or FFFF zone
        if value as i32 == 0 {
            return 0u16;
        }
        // if (Value == 0xFFFF) return 0xFFFF;
        // else restrict to valid zone
        if num_zeroes > 1 {
            a_0 = (num_zeroes - 1) * 0xffff / (length - 1);
            l = a_0 - 1
        }
        if num_of_polys > 1 {
            b_0 = (length - 1 - num_of_polys) * 0xffff / (length - 1);
            r = b_0 + 1
        }
    }
    if r <= l {
        // If this happens LutTable is not invertible
        return 0u16;
    }

    while r > l {
        x = (l + r) / 2;
        res = lut_interp_linear16_boxed((x - 1) as u16, lut_table) as i32;
        if res == value as i32 {
            // Found exact match.
            return (x - 1) as u16;
        }
        if res > value as i32 {
            r = x - 1
        } else {
            l = x + 1
        }
    }

    // Not found, should we interpolate?

    // Get surrounding nodes
    debug_assert!(x >= 1);

    let val2: f64 = (length - 1) as f64 * ((x - 1) as f64 / 65535.0);
    let cell0: i32 = val2.floor() as i32;
    let cell1: i32 = val2.ceil() as i32;
    if cell0 == cell1 {
        return x as u16;
    }

    let y0: f64 = lut_table[cell0 as usize] as f64;
    let x0: f64 = 65535.0 * cell0 as f64 / (length - 1) as f64;
    let y1: f64 = lut_table[cell1 as usize] as f64;
    let x1: f64 = 65535.0 * cell1 as f64 / (length - 1) as f64;
    let a: f64 = (y1 - y0) / (x1 - x0);
    let b: f64 = mlaf(y0, -a, x0);
    if a.abs() < 0.01f64 {
        return x as u16;
    }
    let f: f64 = (value as i32 as f64 - b) / a;
    if f < 0.0 {
        return 0u16;
    }
    if f >= 65535.0 {
        return 0xffffu16;
    }
    (f + 0.5f64).floor() as u16
}

fn invert_lut(table: &[u16], out_length: usize) -> Vec<u16> {
    // For now, we invert the lut by creating a lut of size out_length
    // and attempting to look up a value for each entry using lut_inverse_interp16
    let mut output = vec![0u16; out_length];
    let scale_value = 65535f64 / (out_length - 1) as f64;
    for (i, out) in output.iter_mut().enumerate() {
        let x: f64 = i as f64 * scale_value;
        let input: u16 = (x + 0.5f64).floor() as u16;
        *out = lut_inverse_interp16(input, table);
    }
    output
}

fn invert_lut_boxed<const N: usize>(table: &[u16; N], out_length: usize) -> Vec<u16> {
    // For now, we invert the lut by creating a lut of size out_length
    // and attempting to look up a value for each entry using lut_inverse_interp16
    let mut output = vec![0u16; out_length];
    let scale_value = 65535f64 / (out_length - 1) as f64;
    for (i, out) in output.iter_mut().enumerate() {
        let x: f64 = i as f64 * scale_value;
        let input: u16 = (x + 0.5f64).floor() as u16;
        *out = lut_inverse_interp16_boxed(input, table);
    }
    output
}

impl ToneReprCurve {
    pub(crate) fn to_clut(&self) -> Result<Vec<f32>, CmsError> {
        match self {
            ToneReprCurve::Lut(lut) => {
                if lut.is_empty() {
                    let passthrough_table = passthrough_table::<f32, 16384, 1>();
                    Ok(passthrough_table.to_vec())
                } else {
                    Ok(lut
                        .iter()
                        .map(|&x| x as f32 * (1. / 65535.))
                        .collect::<Vec<_>>())
                }
            }
            ToneReprCurve::Parametric(_) => {
                let curve = self
                    .build_linearize_table::<f32, 65535, 1>()
                    .ok_or(CmsError::InvalidTrcCurve)?;
                let max_value = f32::NOT_FINITE_LINEAR_TABLE_SIZE - 1;
                let sliced = &curve[..max_value];
                Ok(sliced.to_vec())
            }
        }
    }

    pub(crate) fn build_linearize_table<
        T: PointeeSizeExpressible,
        const N: usize,
        const BIT_DEPTH: usize,
    >(
        &self,
    ) -> Option<Box<[f32; N]>> {
        match self {
            ToneReprCurve::Parametric(params) => linear_curve_parametric::<T, N, BIT_DEPTH>(params),
            ToneReprCurve::Lut(data) => match data.len() {
                0 => Some(passthrough_table::<T, N, BIT_DEPTH>()),
                1 => Some(linear_forward_table::<T, N, BIT_DEPTH>(data[0])),
                _ => Some(linear_lut_interpolate::<T, N, BIT_DEPTH>(data)),
            },
        }
    }

    pub(crate) fn build_gamma_table<
        T: Default + Copy + 'static + PointeeSizeExpressible + GammaLutInterpolate,
        const BUCKET: usize,
        const N: usize,
        const BIT_DEPTH: usize,
    >(
        &self,
    ) -> Option<Box<[T; BUCKET]>>
    where
        f32: AsPrimitive<T>,
        u32: AsPrimitive<T>,
    {
        match self {
            ToneReprCurve::Parametric(params) => {
                if params.len() == 5 {
                    let srgb_params = vec![2.4, 1. / 1.055, 0.055 / 1.055, 1. / 12.92, 0.04045];
                    let rec709_params = create_rec709_parametric();

                    let mut lc_params: [f32; 5] = [0.; 5];
                    for (dst, src) in lc_params.iter_mut().zip(params.iter()) {
                        *dst = *src;
                    }

                    if compare_parametric(lc_params.as_slice(), srgb_params.as_slice()) {
                        return Some(
                            TransferCharacteristics::Srgb
                                .make_gamma_table::<T, BUCKET, N>(BIT_DEPTH),
                        );
                    }

                    if compare_parametric(lc_params.as_slice(), rec709_params.as_slice()) {
                        return Some(
                            TransferCharacteristics::Bt709
                                .make_gamma_table::<T, BUCKET, N>(BIT_DEPTH),
                        );
                    }
                }

                let parametric_curve = ParametricCurve::new(params);
                if let Some(v) = parametric_curve?
                    .invert()
                    .map(|x| make_gamma_parametric_table::<T, BUCKET, N, BIT_DEPTH>(x))
                {
                    return Some(v);
                }

                let mut gamma_table_uint = Box::new([0; N]);

                let inverted_size: usize = N;
                let gamma_table = linear_curve_parametric_s::<N>(params)?;
                for (&src, dst) in gamma_table.iter().zip(gamma_table_uint.iter_mut()) {
                    *dst = (src * 65535f32) as u16;
                }
                let inverted = invert_lut_boxed(&gamma_table_uint, inverted_size);
                Some(make_gamma_lut::<T, BUCKET, N, BIT_DEPTH>(&inverted))
            }
            ToneReprCurve::Lut(data) => match data.len() {
                0 => Some(make_gamma_linear_table::<T, BUCKET, N>(BIT_DEPTH)),
                1 => Some(make_gamma_pow_table::<T, BUCKET, N>(
                    1. / u8_fixed_8number_to_float(data[0]),
                    BIT_DEPTH,
                )),
                _ => {
                    let mut inverted_size = data.len();
                    if inverted_size < 256 {
                        inverted_size = 256
                    }
                    let inverted = invert_lut(data, inverted_size);
                    Some(make_gamma_lut::<T, BUCKET, N, BIT_DEPTH>(&inverted))
                }
            },
        }
    }
}

impl ColorProfile {
    /// Produces LUT for 8 bit tone linearization
    pub fn build_8bit_lin_table(
        &self,
        trc: &Option<ToneReprCurve>,
    ) -> Result<Box<[f32; 256]>, CmsError> {
        trc.as_ref()
            .and_then(|trc| trc.build_linearize_table::<u8, 256, 8>())
            .ok_or(CmsError::BuildTransferFunction)
    }

    /// Produces LUT for Gray transfer curve with N depth
    pub fn build_gray_linearize_table<
        T: PointeeSizeExpressible,
        const N: usize,
        const BIT_DEPTH: usize,
    >(
        &self,
    ) -> Result<Box<[f32; N]>, CmsError> {
        self.gray_trc
            .as_ref()
            .and_then(|trc| trc.build_linearize_table::<T, N, BIT_DEPTH>())
            .ok_or(CmsError::BuildTransferFunction)
    }

    /// Produces LUT for Red transfer curve with N depth
    pub fn build_r_linearize_table<
        T: PointeeSizeExpressible,
        const N: usize,
        const BIT_DEPTH: usize,
    >(
        &self,
        use_cicp: bool,
    ) -> Result<Box<[f32; N]>, CmsError> {
        if use_cicp {
            if let Some(tc) = self.cicp.as_ref().map(|c| c.transfer_characteristics) {
                if tc.has_transfer_curve() {
                    return Ok(tc.make_linear_table::<T, N, BIT_DEPTH>());
                }
            }
        }
        self.red_trc
            .as_ref()
            .and_then(|trc| trc.build_linearize_table::<T, N, BIT_DEPTH>())
            .ok_or(CmsError::BuildTransferFunction)
    }

    /// Produces LUT for Green transfer curve with N depth
    pub fn build_g_linearize_table<
        T: PointeeSizeExpressible,
        const N: usize,
        const BIT_DEPTH: usize,
    >(
        &self,
        use_cicp: bool,
    ) -> Result<Box<[f32; N]>, CmsError> {
        if use_cicp {
            if let Some(tc) = self.cicp.as_ref().map(|c| c.transfer_characteristics) {
                if tc.has_transfer_curve() {
                    return Ok(tc.make_linear_table::<T, N, BIT_DEPTH>());
                }
            }
        }
        self.green_trc
            .as_ref()
            .and_then(|trc| trc.build_linearize_table::<T, N, BIT_DEPTH>())
            .ok_or(CmsError::BuildTransferFunction)
    }

    /// Produces LUT for Blue transfer curve with N depth
    pub fn build_b_linearize_table<
        T: PointeeSizeExpressible,
        const N: usize,
        const BIT_DEPTH: usize,
    >(
        &self,
        use_cicp: bool,
    ) -> Result<Box<[f32; N]>, CmsError> {
        if use_cicp {
            if let Some(tc) = self.cicp.as_ref().map(|c| c.transfer_characteristics) {
                if tc.has_transfer_curve() {
                    return Ok(tc.make_linear_table::<T, N, BIT_DEPTH>());
                }
            }
        }
        self.blue_trc
            .as_ref()
            .and_then(|trc| trc.build_linearize_table::<T, N, BIT_DEPTH>())
            .ok_or(CmsError::BuildTransferFunction)
    }

    /// Build gamma table for 8 bit depth
    /// Only 4092 first bins are used and values scaled in 0..255
    pub fn build_8bit_gamma_table(
        &self,
        trc: &Option<ToneReprCurve>,
        use_cicp: bool,
    ) -> Result<Box<[u16; 65536]>, CmsError> {
        self.build_gamma_table::<u16, 65536, 4092, 8>(trc, use_cicp)
    }

    /// Build gamma table for 10 bit depth
    /// Only 8192 first bins are used and values scaled in 0..1023
    pub fn build_10bit_gamma_table(
        &self,
        trc: &Option<ToneReprCurve>,
        use_cicp: bool,
    ) -> Result<Box<[u16; 65536]>, CmsError> {
        self.build_gamma_table::<u16, 65536, 8192, 10>(trc, use_cicp)
    }

    /// Build gamma table for 12 bit depth
    /// Only 16384 first bins are used and values scaled in 0..4095
    pub fn build_12bit_gamma_table(
        &self,
        trc: &Option<ToneReprCurve>,
        use_cicp: bool,
    ) -> Result<Box<[u16; 65536]>, CmsError> {
        self.build_gamma_table::<u16, 65536, 16384, 12>(trc, use_cicp)
    }

    /// Build gamma table for 16 bit depth
    /// Only 16384 first bins are used and values scaled in 0..65535
    pub fn build_16bit_gamma_table(
        &self,
        trc: &Option<ToneReprCurve>,
        use_cicp: bool,
    ) -> Result<Box<[u16; 65536]>, CmsError> {
        self.build_gamma_table::<u16, 65536, 65536, 16>(trc, use_cicp)
    }

    /// Builds gamma table checking CICP for Transfer characteristics first.
    pub fn build_gamma_table<
        T: Default + Copy + 'static + PointeeSizeExpressible + GammaLutInterpolate,
        const BUCKET: usize,
        const N: usize,
        const BIT_DEPTH: usize,
    >(
        &self,
        trc: &Option<ToneReprCurve>,
        use_cicp: bool,
    ) -> Result<Box<[T; BUCKET]>, CmsError>
    where
        f32: AsPrimitive<T>,
        u32: AsPrimitive<T>,
    {
        if use_cicp {
            if let Some(tc) = self.cicp.as_ref().map(|c| c.transfer_characteristics) {
                if tc.has_transfer_curve() {
                    return Ok(tc.make_gamma_table::<T, BUCKET, N>(BIT_DEPTH));
                }
            }
        }
        trc.as_ref()
            .and_then(|trc| trc.build_gamma_table::<T, BUCKET, N, BIT_DEPTH>())
            .ok_or(CmsError::BuildTransferFunction)
    }

    /// Checks if profile gamma can work in extended precision and we have implementation for this
    pub(crate) fn try_extended_gamma_evaluator(
        &self,
    ) -> Option<Box<dyn ToneCurveEvaluator + Send + Sync>> {
        if let Some(tc) = self.cicp.as_ref().map(|c| c.transfer_characteristics) {
            if tc.has_transfer_curve() {
                return Some(Box::new(ToneCurveCicpEvaluator {
                    rgb_trc: tc.extended_gamma_tristimulus(),
                    trc: tc.extended_gamma_single(),
                }));
            }
        }
        if !self.are_all_trc_the_same() {
            return None;
        }
        let reference_trc = if self.color_space == DataColorSpace::Gray {
            self.gray_trc.as_ref()
        } else {
            self.red_trc.as_ref()
        };
        if let Some(red_trc) = reference_trc {
            return Self::make_gamma_evaluator_all_the_same(red_trc);
        }
        None
    }

    fn make_gamma_evaluator_all_the_same(
        red_trc: &ToneReprCurve,
    ) -> Option<Box<dyn ToneCurveEvaluator + Send + Sync>> {
        match red_trc {
            ToneReprCurve::Lut(lut) => {
                if lut.is_empty() {
                    return Some(Box::new(ToneCurveEvaluatorLinear {}));
                }
                if lut.len() == 1 {
                    let gamma = 1. / u8_fixed_8number_to_float(lut[0]);
                    return Some(Box::new(ToneCurveEvaluatorPureGamma { gamma }));
                }
                None
            }
            ToneReprCurve::Parametric(params) => {
                if params.len() == 5 {
                    let srgb_params = vec![2.4, 1. / 1.055, 0.055 / 1.055, 1. / 12.92, 0.04045];
                    let rec709_params = create_rec709_parametric();

                    let mut lc_params: [f32; 5] = [0.; 5];
                    for (dst, src) in lc_params.iter_mut().zip(params.iter()) {
                        *dst = *src;
                    }

                    if compare_parametric(lc_params.as_slice(), srgb_params.as_slice()) {
                        return Some(Box::new(ToneCurveCicpEvaluator {
                            rgb_trc: TransferCharacteristics::Srgb.extended_gamma_tristimulus(),
                            trc: TransferCharacteristics::Srgb.extended_gamma_single(),
                        }));
                    }

                    if compare_parametric(lc_params.as_slice(), rec709_params.as_slice()) {
                        return Some(Box::new(ToneCurveCicpEvaluator {
                            rgb_trc: TransferCharacteristics::Bt709.extended_gamma_tristimulus(),
                            trc: TransferCharacteristics::Bt709.extended_gamma_single(),
                        }));
                    }
                }

                let parametric_curve = ParametricCurve::new(params);
                if let Some(v) = parametric_curve?.invert() {
                    return Some(Box::new(ToneCurveParametricEvaluator { parametric: v }));
                }
                None
            }
        }
    }

    /// Check if all TRC are the same
    pub(crate) fn are_all_trc_the_same(&self) -> bool {
        if self.color_space == DataColorSpace::Gray {
            return true;
        }
        if let (Some(red_trc), Some(green_trc), Some(blue_trc)) =
            (&self.red_trc, &self.green_trc, &self.blue_trc)
        {
            if !matches!(
                (red_trc, green_trc, blue_trc),
                (
                    ToneReprCurve::Lut(_),
                    ToneReprCurve::Lut(_),
                    ToneReprCurve::Lut(_),
                ) | (
                    ToneReprCurve::Parametric(_),
                    ToneReprCurve::Parametric(_),
                    ToneReprCurve::Parametric(_)
                )
            ) {
                return false;
            }
            if let (ToneReprCurve::Lut(lut0), ToneReprCurve::Lut(lut1), ToneReprCurve::Lut(lut2)) =
                (red_trc, green_trc, blue_trc)
            {
                if lut0 == lut1 || lut1 == lut2 {
                    return true;
                }
            }
            if let (
                ToneReprCurve::Parametric(lut0),
                ToneReprCurve::Parametric(lut1),
                ToneReprCurve::Parametric(lut2),
            ) = (red_trc, green_trc, blue_trc)
            {
                if lut0 == lut1 || lut1 == lut2 {
                    return true;
                }
            }
        }
        false
    }

    /// Checks if profile is matrix shaper, have same TRC and TRC is linear.
    pub(crate) fn is_linear_matrix_shaper(&self) -> bool {
        if !self.is_matrix_shaper() {
            return false;
        }
        if !self.are_all_trc_the_same() {
            return false;
        }
        if let Some(red_trc) = &self.red_trc {
            return match red_trc {
                ToneReprCurve::Lut(lut) => {
                    if lut.is_empty() {
                        return true;
                    }
                    if is_curve_linear16(lut) {
                        return true;
                    }
                    false
                }
                ToneReprCurve::Parametric(params) => {
                    if let Some(curve) = ParametricCurve::new(params) {
                        return curve.is_linear();
                    }
                    false
                }
            };
        }
        false
    }

    /// Checks if profile linearization can work in extended precision and we have implementation for this
    pub(crate) fn try_extended_linearizing_evaluator(
        &self,
    ) -> Option<Box<dyn ToneCurveEvaluator + Send + Sync>> {
        if let Some(tc) = self.cicp.as_ref().map(|c| c.transfer_characteristics) {
            if tc.has_transfer_curve() {
                return Some(Box::new(ToneCurveCicpEvaluator {
                    rgb_trc: tc.extended_linear_tristimulus(),
                    trc: tc.extended_linear_single(),
                }));
            }
        }
        if !self.are_all_trc_the_same() {
            return None;
        }
        let reference_trc = if self.color_space == DataColorSpace::Gray {
            self.gray_trc.as_ref()
        } else {
            self.red_trc.as_ref()
        };
        if let Some(red_trc) = reference_trc {
            if let Some(value) = Self::make_linear_curve_evaluator_all_the_same(red_trc) {
                return value;
            }
        }
        None
    }

    fn make_linear_curve_evaluator_all_the_same(
        evaluator_curve: &ToneReprCurve,
    ) -> Option<Option<Box<dyn ToneCurveEvaluator + Send + Sync>>> {
        match evaluator_curve {
            ToneReprCurve::Lut(lut) => {
                if lut.is_empty() {
                    return Some(Some(Box::new(ToneCurveEvaluatorLinear {})));
                }
                if lut.len() == 1 {
                    let gamma = u8_fixed_8number_to_float(lut[0]);
                    return Some(Some(Box::new(ToneCurveEvaluatorPureGamma { gamma })));
                }
            }
            ToneReprCurve::Parametric(params) => {
                if params.len() == 5 {
                    let srgb_params = vec![2.4, 1. / 1.055, 0.055 / 1.055, 1. / 12.92, 0.04045];
                    let rec709_params = create_rec709_parametric();

                    let mut lc_params: [f32; 5] = [0.; 5];
                    for (dst, src) in lc_params.iter_mut().zip(params.iter()) {
                        *dst = *src;
                    }

                    if compare_parametric(lc_params.as_slice(), srgb_params.as_slice()) {
                        return Some(Some(Box::new(ToneCurveCicpEvaluator {
                            rgb_trc: TransferCharacteristics::Srgb.extended_linear_tristimulus(),
                            trc: TransferCharacteristics::Srgb.extended_linear_single(),
                        })));
                    }

                    if compare_parametric(lc_params.as_slice(), rec709_params.as_slice()) {
                        return Some(Some(Box::new(ToneCurveCicpEvaluator {
                            rgb_trc: TransferCharacteristics::Bt709.extended_linear_tristimulus(),
                            trc: TransferCharacteristics::Bt709.extended_linear_single(),
                        })));
                    }
                }

                let parametric_curve = ParametricCurve::new(params);
                if let Some(v) = parametric_curve {
                    return Some(Some(Box::new(ToneCurveParametricEvaluator {
                        parametric: v,
                    })));
                }
            }
        }
        None
    }
}

pub(crate) struct ToneCurveCicpEvaluator {
    rgb_trc: fn(Rgb<f32>) -> Rgb<f32>,
    trc: fn(f32) -> f32,
}

pub(crate) struct ToneCurveParametricEvaluator {
    parametric: ParametricCurve,
}

pub(crate) struct ToneCurveEvaluatorPureGamma {
    gamma: f32,
}

pub(crate) struct ToneCurveEvaluatorLinear {}

impl ToneCurveEvaluator for ToneCurveCicpEvaluator {
    fn evaluate_tristimulus(&self, rgb: Rgb<f32>) -> Rgb<f32> {
        (self.rgb_trc)(rgb)
    }

    fn evaluate_value(&self, value: f32) -> f32 {
        (self.trc)(value)
    }
}

impl ToneCurveEvaluator for ToneCurveParametricEvaluator {
    fn evaluate_tristimulus(&self, rgb: Rgb<f32>) -> Rgb<f32> {
        Rgb::new(
            self.parametric.eval(rgb.r),
            self.parametric.eval(rgb.g),
            self.parametric.eval(rgb.b),
        )
    }

    fn evaluate_value(&self, value: f32) -> f32 {
        self.parametric.eval(value)
    }
}

impl ToneCurveEvaluator for ToneCurveEvaluatorPureGamma {
    fn evaluate_tristimulus(&self, rgb: Rgb<f32>) -> Rgb<f32> {
        Rgb::new(
            dirty_powf(rgb.r, self.gamma),
            dirty_powf(rgb.g, self.gamma),
            dirty_powf(rgb.b, self.gamma),
        )
    }

    fn evaluate_value(&self, value: f32) -> f32 {
        dirty_powf(value, self.gamma)
    }
}

impl ToneCurveEvaluator for ToneCurveEvaluatorLinear {
    fn evaluate_tristimulus(&self, rgb: Rgb<f32>) -> Rgb<f32> {
        rgb
    }

    fn evaluate_value(&self, value: f32) -> f32 {
        value
    }
}

pub trait ToneCurveEvaluator {
    fn evaluate_tristimulus(&self, rgb: Rgb<f32>) -> Rgb<f32>;
    fn evaluate_value(&self, value: f32) -> f32;
}
