//! Generic bilinear-interpolation lookup against a Heimann "Table.c"-style
//! object-temperature calibration table (datasheet section 12.5).
//!
//! Heimann calibrates each sensor/optics/gain combination with its own
//! table, selected via a "table number" stored in the sensor's EEPROM. Only
//! tables using an equidistant signal axis (`EQUIADTABLE` in Heimann's
//! reference code) are supported here, which turns the row lookup into a
//! plain shift instead of a per-row search. All of the reference tables we
//! have for the 16x16 sensor use this layout.
//!
//! Both lookups below clamp out-of-range inputs to the nearest edge of the
//! table (linear extrapolation) instead of failing, so a pixel briefly
//! outside the calibrated range just gets a best-effort estimate rather than
//! an error.

/// The ambient-temperature column to use for a whole frame, located once via
/// [`LookupTable::locate_ambient_column`] since it doesn't vary pixel to
/// pixel.
pub struct AmbientColumn {
    index: usize,
    delta_dk: i32,
}

pub struct LookupTable<const ROWS: usize, const COLS: usize> {
    /// Heimann's table number (EEPROM `TN`) that this table was made for.
    pub table_number: u16,
    /// `values[row][column]` is the object temperature in deci-Kelvin.
    /// `0` marks an entry outside the sensor's factory-calibrated range.
    pub values: &'static [[u16; COLS]; ROWS],
    /// Ambient temperature (deci-Kelvin) of each column.
    pub ta_columns_dk: [u16; COLS],
    /// Scale used to turn a sensitivity-compensated pixel signal into
    /// "digits" comparable to this table (Heimann's `PCSCALEVAL`).
    pub pixel_signal_scale: f32,
    /// Signal digits between two adjacent rows (`ADEQUIDISTANCE`).
    pub row_step_digits: i32,
    /// `log2(row_step_digits)`; turns a signal into a row index with a shift
    /// instead of a division (`ADEXPBITS`).
    pub row_exp_bits: u32,
    /// Added to a signal before locating its row, so that signals near zero
    /// still map to a valid (non-negative) row index (`TABLEOFFSET`).
    pub row_offset_digits: i32,
    /// Deci-Kelvin between two adjacent columns (`TAEQUIDISTANCE`).
    pub column_step_dk: i32,
}

impl<const ROWS: usize, const COLS: usize> LookupTable<ROWS, COLS> {
    /// Locate the ambient-temperature column for `ambient_dk`, clamped to
    /// the table's calibrated range.
    pub fn locate_ambient_column(&self, ambient_dk: i32) -> AmbientColumn {
        let last_selectable_column = COLS - 2; // `index + 1` must stay in bounds
        let index = (0..=last_selectable_column)
            .rev()
            .find(|&i| self.ta_columns_dk[i] as i32 <= ambient_dk)
            .unwrap_or(0);
        AmbientColumn {
            index,
            delta_dk: ambient_dk - self.ta_columns_dk[index] as i32,
        }
    }

    /// Bi-linear interpolation of the object temperature (deci-Kelvin) for
    /// one pixel's sensitivity-compensated signal (in digits).
    pub fn lookup(&self, column: &AmbientColumn, signal_digits: i32) -> i32 {
        let shifted_signal = (signal_digits + self.row_offset_digits) >> self.row_exp_bits;
        let row = shifted_signal.clamp(0, (ROWS - 2) as i32) as usize;

        let c = column.index;
        let top_left = self.values[row][c] as i32;
        let top_right = self.values[row][c + 1] as i32;
        let bottom_left = self.values[row + 1][c] as i32;
        let bottom_right = self.values[row + 1][c + 1] as i32;

        // interpolate along the ambient-temperature axis, at this row and the next
        let at_row = (top_right - top_left) * column.delta_dk / self.column_step_dk + top_left;
        let at_next_row =
            (bottom_right - bottom_left) * column.delta_dk / self.column_step_dk + bottom_left;

        // interpolate along the signal axis, between those two results
        let row_start_digits = row as i32 * self.row_step_digits;
        let position_in_row = (signal_digits + self.row_offset_digits) - row_start_digits;
        (at_next_row - at_row) * position_in_row / self.row_step_digits + at_row
    }
}
