use lyon_tessellation::{
    BuffersBuilder, FillOptions, FillTessellator, FillVertex, TessellationError, VertexBuffers,
    path::{Path as LyonPath, builder::WithSvg, math::Point as LyonPoint, path::BuilderImpl},
};
use swash::{
    scale::outline::Outline,
    zeno::{PathBuilder, PathData as _, Point as ZenoPoint},
};

const FILL_TOLERANCE_PX: f32 = 0.2;

#[derive(Clone, Debug, Default, PartialEq)]
pub struct TessellatedOutline {
    pub vertices_px: Vec<[f32; 2]>,
    pub indices: Vec<u32>,
}

impl TessellatedOutline {
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.indices.is_empty()
    }
}

pub fn append_outline(builder: &mut WithSvg<BuilderImpl>, outline: &Outline, offset: ZenoPoint) {
    let mut adapter = ZenoToLyon::with_offset(builder, offset);
    outline.path().copy_to(&mut adapter);
}

#[must_use]
pub fn outline_to_path(outline: &Outline) -> LyonPath {
    let mut builder = LyonPath::svg_builder();
    append_outline(&mut builder, outline, ZenoPoint::new(0.0, 0.0));
    builder.build()
}

pub fn tessellate_path(
    path: &LyonPath,
    fill: &mut FillTessellator,
) -> Result<TessellatedOutline, TessellationError> {
    let mut buffers: VertexBuffers<[f32; 2], u32> = VertexBuffers::new();
    fill.tessellate_path(
        path,
        &FillOptions::default().with_tolerance(FILL_TOLERANCE_PX),
        &mut BuffersBuilder::new(&mut buffers, |v: FillVertex| {
            [v.position().x, v.position().y]
        }),
    )?;
    Ok(TessellatedOutline {
        vertices_px: buffers.vertices,
        indices: buffers.indices,
    })
}

struct ZenoToLyon<'a> {
    builder: &'a mut WithSvg<BuilderImpl>,
    offset: ZenoPoint,
    current: ZenoPoint,
    open: bool,
}

impl<'a> ZenoToLyon<'a> {
    fn with_offset(builder: &'a mut WithSvg<BuilderImpl>, offset: ZenoPoint) -> Self {
        Self {
            builder,
            offset,
            current: ZenoPoint::new(0.0, 0.0),
            open: false,
        }
    }

    fn map(&self, p: ZenoPoint) -> LyonPoint {
        LyonPoint::new(p.x + self.offset.x, p.y + self.offset.y)
    }
}

impl PathBuilder for ZenoToLyon<'_> {
    fn current_point(&self) -> ZenoPoint {
        self.current
    }

    fn move_to(&mut self, to: impl Into<ZenoPoint>) -> &mut Self {
        let p = to.into();
        self.current = p;
        self.builder.move_to(self.map(p));
        self.open = true;
        self
    }

    fn line_to(&mut self, to: impl Into<ZenoPoint>) -> &mut Self {
        let p = to.into();
        self.current = p;
        self.builder.line_to(self.map(p));
        self
    }

    fn quad_to(&mut self, control: impl Into<ZenoPoint>, to: impl Into<ZenoPoint>) -> &mut Self {
        let c = control.into();
        let p = to.into();
        self.current = p;
        self.builder.quadratic_bezier_to(self.map(c), self.map(p));
        self
    }

    fn curve_to(
        &mut self,
        control1: impl Into<ZenoPoint>,
        control2: impl Into<ZenoPoint>,
        to: impl Into<ZenoPoint>,
    ) -> &mut Self {
        let c1 = control1.into();
        let c2 = control2.into();
        let p = to.into();
        self.current = p;
        self.builder
            .cubic_bezier_to(self.map(c1), self.map(c2), self.map(p));
        self
    }

    fn close(&mut self) -> &mut Self {
        if self.open {
            self.builder.close();
            self.open = false;
        }
        self
    }
}

#[cfg(test)]
mod tests {
    use super::{
        FILL_TOLERANCE_PX, TessellatedOutline, ZenoToLyon, append_outline, outline_to_path,
        tessellate_path,
    };
    use lyon_tessellation::{
        FillTessellator,
        path::{Event, Path as LyonPath, math::Point as LyonPoint},
    };
    use swash::zeno::{PathBuilder, Point as ZenoPoint};

    fn zero() -> ZenoPoint {
        ZenoPoint::new(0.0, 0.0)
    }

    #[test]
    fn forwards_zero_offset_path_unchanged() {
        let mut builder = LyonPath::svg_builder();
        {
            let mut adapter = ZenoToLyon::with_offset(&mut builder, zero());
            adapter
                .move_to(ZenoPoint::new(0.0, 0.0))
                .line_to(ZenoPoint::new(2.0, 0.0))
                .line_to(ZenoPoint::new(2.0, 1.0))
                .close();
        }
        let path = builder.build();
        assert!(path.iter().count() >= 4);
    }

    #[test]
    fn applies_offset_to_every_emitted_point() {
        let mut shifted = LyonPath::svg_builder();
        {
            let mut adapter = ZenoToLyon::with_offset(&mut shifted, ZenoPoint::new(10.0, -5.0));
            adapter
                .move_to(ZenoPoint::new(0.0, 0.0))
                .line_to(ZenoPoint::new(1.0, 1.0))
                .close();
        }
        let path = shifted.build();
        let xs_within_offset = path.iter().all(|event| {
            let bbox = bounding_box(event);
            bbox.iter().all(|(x, y)| *x >= 10.0 && *y >= -5.0)
        });
        assert!(
            xs_within_offset,
            "every emitted point must respect the offset"
        );
    }

    fn bounding_box(event: Event<LyonPoint, LyonPoint>) -> Vec<(f32, f32)> {
        match event {
            Event::Begin { at } | Event::End { last: at, .. } => vec![(at.x, at.y)],
            Event::Line { from, to } => vec![(from.x, from.y), (to.x, to.y)],
            Event::Quadratic { from, ctrl, to } => {
                vec![(from.x, from.y), (ctrl.x, ctrl.y), (to.x, to.y)]
            }
            Event::Cubic {
                from,
                ctrl1,
                ctrl2,
                to,
            } => vec![
                (from.x, from.y),
                (ctrl1.x, ctrl1.y),
                (ctrl2.x, ctrl2.y),
                (to.x, to.y),
            ],
        }
    }

    #[test]
    fn current_point_tracks_last_emitted_point() {
        let mut builder = LyonPath::svg_builder();
        let mut adapter = ZenoToLyon::with_offset(&mut builder, zero());
        adapter.move_to(ZenoPoint::new(3.0, 4.0));
        let cp = adapter.current_point();
        assert!((cp.x - 3.0).abs() < f32::EPSILON);
        assert!((cp.y - 4.0).abs() < f32::EPSILON);
        adapter.line_to(ZenoPoint::new(7.0, 8.0));
        let cp = adapter.current_point();
        assert!((cp.x - 7.0).abs() < f32::EPSILON);
        assert!((cp.y - 8.0).abs() < f32::EPSILON);
    }

    #[test]
    fn close_without_open_subpath_is_noop() {
        let mut builder = LyonPath::svg_builder();
        let mut adapter = ZenoToLyon::with_offset(&mut builder, zero());
        adapter.close();
        let _ = builder.build();
    }

    #[test]
    fn fill_tolerance_is_subpixel_default() {
        const _: () = assert!(FILL_TOLERANCE_PX > 0.0);
        const _: () = assert!(FILL_TOLERANCE_PX < 1.0);
    }

    #[test]
    fn tessellate_path_emits_triangles_for_filled_quad() {
        let mut builder = LyonPath::svg_builder();
        {
            let mut adapter = ZenoToLyon::with_offset(&mut builder, zero());
            adapter
                .move_to(ZenoPoint::new(0.0, 0.0))
                .line_to(ZenoPoint::new(1.0, 0.0))
                .line_to(ZenoPoint::new(1.0, 1.0))
                .line_to(ZenoPoint::new(0.0, 1.0))
                .close();
        }
        let path = builder.build();
        let mut fill = FillTessellator::new();
        let Ok(result) = tessellate_path(&path, &mut fill) else {
            panic!("tessellation must succeed for a filled quad");
        };
        assert!(!result.is_empty());
        assert!(result.indices.len().is_multiple_of(3));
        assert!(
            result
                .vertices_px
                .iter()
                .all(|[x, y]| { (0.0..=1.0).contains(x) && (0.0..=1.0).contains(y) })
        );
    }

    #[test]
    fn tessellate_empty_path_returns_empty_result() {
        let path = LyonPath::svg_builder().build();
        let mut fill = FillTessellator::new();
        let Ok(result) = tessellate_path(&path, &mut fill) else {
            panic!("empty path tessellation must succeed");
        };
        assert!(result.is_empty());
        assert!(result.vertices_px.is_empty());
    }

    #[test]
    fn tessellated_outline_default_is_empty() {
        let t = TessellatedOutline::default();
        assert!(t.is_empty());
        assert!(t.vertices_px.is_empty());
        assert!(t.indices.is_empty());
    }

    #[test]
    fn append_outline_into_lyon_uses_offset() {
        use swash::scale::outline::Outline;
        let outline = Outline::new();
        let mut builder = LyonPath::svg_builder();
        append_outline(&mut builder, &outline, ZenoPoint::new(5.0, 6.0));
        let path = builder.build();
        assert_eq!(path.iter().count(), 0, "empty outline emits no events");
    }

    #[test]
    fn outline_to_path_handles_empty_outline() {
        use swash::scale::outline::Outline;
        let outline = Outline::new();
        let path = outline_to_path(&outline);
        assert_eq!(path.iter().count(), 0);
    }
}
