// libm used for f32::round() method
#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use core_maths::CoreFloat as _;

use read_fonts::tables::trak::TrackTableEntry;
use read_fonts::types::{BigEndian, Fixed};
use read_fonts::FontData;

use crate::hb::{buffer::hb_buffer_t, face::Scale, hb_font_t, ot_shape_plan::hb_ot_shape_plan_t};

pub fn apply(
    _plan: &hb_ot_shape_plan_t,
    face: &hb_font_t,
    scale: Scale,
    point_size: Option<f32>,
    buffer: &mut hb_buffer_t,
) -> Option<()> {
    let trak = face.aat_tables.trak.as_ref()?;
    let mut ptem = point_size.unwrap_or(0.0);
    if ptem <= 0.0 {
        ptem = 12.0; // CoreText fallback
    }

    if !buffer.have_positions {
        buffer.clear_positions();
    }

    let advance_to_add = if buffer.direction.is_horizontal() {
        scale.scale_x_f(trak.get_h_tracking(ptem, 0.0))
    } else {
        scale.scale_y_f(trak.get_v_tracking(ptem, 0.0))
    };

    foreach_grapheme!(buffer, start, end, {
        if buffer.direction.is_horizontal() {
            buffer.pos[start].x_advance += advance_to_add;
        } else {
            buffer.pos[start].y_advance += advance_to_add;
        }
    });

    Some(())
}

trait TrakExt {
    fn get_h_tracking(&self, ptem: f32, track: f32) -> f32;
    fn get_v_tracking(&self, ptem: f32, track: f32) -> f32;
}

impl TrakExt for read_fonts::tables::trak::Trak<'_> {
    // Keep tracking fractional; scale_x_f rounds once, matching HarfBuzz's
    // em_scalef (the interpolated value is generally not a whole font unit).
    fn get_h_tracking(&self, ptem: f32, track: f32) -> f32 {
        self.horiz()
            .transpose()
            .ok()
            .flatten()
            .map_or(0.0, |t| t.get_tracking(self.offset_data(), ptem, track))
    }

    fn get_v_tracking(&self, ptem: f32, track: f32) -> f32 {
        self.vert()
            .transpose()
            .ok()
            .flatten()
            .map_or(0.0, |t| t.get_tracking(self.offset_data(), ptem, track))
    }
}

trait TrackDataExt {
    fn get_tracking(&self, offset_data: FontData, ptem: f32, track: f32) -> f32;
}

impl TrackDataExt for read_fonts::tables::trak::TrackData<'_> {
    fn get_tracking(&self, offset_data: FontData, ptem: f32, track: f32) -> f32 {
        let Ok(sizes) = self.size_table(offset_data) else {
            return 0.0;
        };

        if sizes.is_empty() {
            return 0.0;
        }

        let tracks = self.track_table();
        if tracks.is_empty() {
            return 0.0;
        }

        let get_value = |entry: &TrackTableEntry| {
            let Ok(values) = entry.per_size_values(offset_data, sizes.len() as u16) else {
                return 0.0;
            };
            entry.get_value(ptem, sizes, values)
        };

        if tracks.len() == 1 {
            return tracks.first().map_or(0.0, get_value);
        }

        let mut i = 0;
        let mut j = tracks.len() - 1;

        while i + 1 < tracks.len() && tracks.get(i + 1).map_or(0.0, |t| t.track().to_f32()) <= track
        {
            i += 1;
        }
        while j > 0 && tracks.get(j - 1).map_or(0.0, |t| t.track().to_f32()) >= track {
            j -= 1;
        }

        if i == j {
            return tracks.get(i).map_or(0.0, get_value);
        }

        let t0 = tracks.get(i).map_or(0.0, |t| t.track().to_f32());
        let t1 = tracks.get(j).map_or(0.0, |t| t.track().to_f32());
        let interp = if (t1 - t0).abs() < f32::EPSILON {
            0.0
        } else {
            (track - t0) / (t1 - t0)
        };

        let a = tracks.get(i).map_or(0.0, get_value);
        let b = tracks.get(j).map_or(0.0, get_value);
        a + interp * (b - a)
    }
}

trait TrackEntryExt {
    fn get_value(&self, ptem: f32, sizes: &[BigEndian<Fixed>], values: &[BigEndian<i16>]) -> f32;
}

impl TrackEntryExt for TrackTableEntry {
    fn get_value(&self, ptem: f32, sizes: &[BigEndian<Fixed>], values: &[BigEndian<i16>]) -> f32 {
        let n = sizes.len().min(values.len());
        if n == 0 {
            return 0.0;
        }

        for i in 0..n {
            let size_pt = sizes.get(i).map_or(0.0, |f| f.get().to_f32());
            if size_pt >= ptem {
                if i == 0 {
                    return values.first().map(|v| v.get() as f32).unwrap_or_default();
                }

                let s0 = sizes.get(i - 1).map_or(0.0, |f| f.get().to_f32());
                let s1 = size_pt;
                let v0 = values
                    .get(i - 1)
                    .map(|v| v.get() as f32)
                    .unwrap_or_default();
                let v1 = values.get(i).map(|v| v.get() as f32).unwrap_or_default();

                if (s1 - s0).abs() < f32::EPSILON {
                    return (v0 + v1) * 0.5;
                }

                let t = (ptem - s0) / (s1 - s0);
                return v0 + t * (v1 - v0);
            }
        }

        values
            .get(n - 1)
            .map(|v| v.get() as f32)
            .unwrap_or_default()
    }
}
