/// This file implements a lighter alternative version of the Jakob
/// 2019 spectral upsampling method.  Instead of using the entire 3D
/// looking table, we use two 2d slices of the table and interpolate
/// between the evaluated spectral values calculated from those tables.
///
/// The provides similar color matching as full Jakob, at the expense of
/// somewhat lower quality spectrums, and the inability to precalculate
/// the coefficents for even more efficient evaluation later on.
use glam::Vec4;

/// How many polynomial coefficients?
const RGB2SPEC_N_COEFFS: usize = 3;

// Include tables generated by the build.rs script
include!(concat!(env!("OUT_DIR"), "/jakob_table_inc.rs"));

#[inline]
pub fn rec709_to_spectrum_p4(lambdas: Vec4, rgb: (f32, f32, f32)) -> Vec4 {
    small_rgb_to_spectrum_p4(
        REC709_TABLE,
        REC709_TABLE_RES,
        REC709_TABLE_MID_VALUE,
        lambdas,
        rgb,
    )
}

#[inline]
pub fn rec2020_to_spectrum_p4(lambdas: Vec4, rgb: (f32, f32, f32)) -> Vec4 {
    small_rgb_to_spectrum_p4(
        REC2020_TABLE,
        REC2020_TABLE_RES,
        REC2020_TABLE_MID_VALUE,
        lambdas,
        rgb,
    )
}

#[inline]
pub fn aces_to_spectrum_p4(lambdas: Vec4, rgb: (f32, f32, f32)) -> Vec4 {
    small_rgb_to_spectrum_p4(
        ACES_TABLE,
        ACES_TABLE_RES,
        ACES_TABLE_MID_VALUE,
        lambdas,
        rgb,
    )
}

//===============================================================
// Core functions, specialized above for specific color spaces.

#[inline(always)]
#[allow(clippy::many_single_char_names)]
fn small_rgb_to_spectrum_p4(
    table: &[[(f32, f32, f32); 2]],
    table_res: usize,
    table_mid_value: f32,
    lambdas: Vec4,
    rgb: (f32, f32, f32),
) -> Vec4 {
    // Determine largest RGB component, and calculate the other two
    // components scaled for lookups.
    let (i, max_val, x, y) = if rgb.0 > rgb.1 && rgb.0 > rgb.2 {
        (0, rgb.0, rgb.1, rgb.2)
    } else if rgb.1 > rgb.2 {
        (1, rgb.1, rgb.2, rgb.0)
    } else {
        (2, rgb.2, rgb.0, rgb.1)
    };
    if max_val == 0.0 {
        // If max_val is zero, just return zero.  This avoids NaN's from
        // divide by zero.  This is also correct, since it's black.
        return Vec4::splat(0.0);
    }
    let x = x * 63.0 / max_val;
    let y = y * 63.0 / max_val;

    // Calculate lookup coordinates.
    let xi = (x as usize).min(table_res - 2);
    let yi = (y as usize).min(table_res - 2);
    let offset = (table_res * table_res * i) + (yi * table_res) + xi;
    let dx = 1;
    let dy = table_res;

    // Look up values from table.
    let a0 = table[offset];
    let a1 = table[offset + dx];
    let a2 = table[offset + dy];
    let a3 = table[offset + dy + dx];

    // Convert to SIMD format for faster interpolation.
    let a0 = [
        Vec4::new(a0[0].0, a0[0].1, a0[0].2, 0.0),
        Vec4::new(a0[1].0, a0[1].1, a0[1].2, 0.0),
    ];
    let a1 = [
        Vec4::new(a1[0].0, a1[0].1, a1[0].2, 0.0),
        Vec4::new(a1[1].0, a1[1].1, a1[1].2, 0.0),
    ];
    let a2 = [
        Vec4::new(a2[0].0, a2[0].1, a2[0].2, 0.0),
        Vec4::new(a2[1].0, a2[1].1, a2[1].2, 0.0),
    ];
    let a3 = [
        Vec4::new(a3[0].0, a3[0].1, a3[0].2, 0.0),
        Vec4::new(a3[1].0, a3[1].1, a3[1].2, 0.0),
    ];

    // Do interpolation.
    let x1: f32 = x - xi as f32;
    let x0: f32 = 1.0 - x1 as f32;
    let y1: f32 = y - yi as f32;
    let y0: f32 = 1.0 - y1 as f32;
    let b0 = [(a0[0] * x0) + (a1[0] * x1), (a0[1] * x0) + (a1[1] * x1)];
    let b1 = [(a2[0] * x0) + (a3[0] * x1), (a2[1] * x0) + (a3[1] * x1)];
    let c = [(b0[0] * y0) + (b1[0] * y1), (b0[1] * y0) + (b1[1] * y1)];

    // Evaluate the spectral function and return the result.
    if max_val <= table_mid_value {
        rgb2spec_eval_4([c[0].x(), c[0].y(), c[0].z()], lambdas) * (1.0 / table_mid_value) * max_val
    } else if max_val < 1.0 {
        let n = (max_val - table_mid_value) / (1.0 - table_mid_value);
        let s0 = rgb2spec_eval_4([c[0].x(), c[0].y(), c[0].z()], lambdas);
        let s1 = rgb2spec_eval_4([c[1].x(), c[1].y(), c[1].z()], lambdas);
        (s0 * (1.0 - n)) + (s1 * n)
    } else {
        rgb2spec_eval_4([c[1].x(), c[1].y(), c[1].z()], lambdas) * max_val
    }
}

//============================================================
// Coefficient -> eval functions

#[inline(always)]
fn rgb2spec_fma_4(a: Vec4, b: Vec4, c: Vec4) -> Vec4 {
    (a * b) + c
}

fn rgb2spec_eval_4(coeff: [f32; RGB2SPEC_N_COEFFS], lambda: Vec4) -> Vec4 {
    let co0 = Vec4::splat(coeff[0]);
    let co1 = Vec4::splat(coeff[1]);
    let co2 = Vec4::splat(coeff[2]);

    let x = rgb2spec_fma_4(rgb2spec_fma_4(co0, lambda, co1), lambda, co2);

    let y = {
        // TODO: replace this with a SIMD sqrt op.
        let (x, y, z, w) = rgb2spec_fma_4(x, x, Vec4::splat(1.0)).into();
        Vec4::new(x.sqrt(), y.sqrt(), z.sqrt(), w.sqrt()).reciprocal()
    };

    rgb2spec_fma_4(Vec4::splat(0.5) * x, y, Vec4::splat(0.5))
}
