// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::path::Path;
use std::sync::OnceLock;
use usvg::tiny_skia_path::PathSegment;
use usvg::{Group, Node};
use vello_common::fearless_simd::Level;
use vello_common::flatten::{FlattenCtx, Line};
use vello_common::geometry::RectU16;
use vello_common::kurbo::{Affine, BezPath, Stroke, StrokeCtx};
use vello_common::peniko::Fill;
use vello_common::strip::Strip;
use vello_common::tile::Tiles;
use vello_common::{flatten, strip};

static DATA: OnceLock<Vec<DataItem>> = OnceLock::new();

pub fn get_data_items() -> &'static [DataItem] {
    DATA.get_or_init(|| {
        let data_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("data");
        let mut data = vec![];

        // Always use ghostscript tiger.
        data.push(DataItem::from_path(
            &Path::new(env!("CARGO_MANIFEST_DIR"))
                .join("../../examples/assets/Ghostscript_Tiger.svg"),
        ));

        for entry in std::fs::read_dir(&data_dir).unwrap() {
            let entry = entry.unwrap();
            let path = entry.path();

            if path.extension().and_then(|e| e.to_str()) == Some("svg") {
                data.push(DataItem::from_path(&path));
            }
        }

        data
    })
}

#[derive(Clone, Debug)]
pub struct DataItem {
    pub name: String,
    pub fills: Vec<FilledPath>,
    pub strokes: Vec<StrokedPath>,
    pub width: u16,
    pub height: u16,
}

impl DataItem {
    fn from_path(path: &Path) -> Self {
        let file_name = { path.file_stem().unwrap().to_string_lossy().to_string() };

        let data = std::fs::read(path).unwrap();
        let tree = usvg::Tree::from_data(&data, &usvg::Options::default()).unwrap();
        let mut ctx = ConversionContext::new();
        convert(&mut ctx, tree.root());

        Self {
            name: file_name,
            fills: ctx.fills,
            strokes: ctx.strokes,
            #[expect(
                clippy::cast_possible_truncation,
                reason = "It's okay to ignore for benchmarking."
            )]
            width: tree.size().width() as u16,
            #[expect(
                clippy::cast_possible_truncation,
                reason = "It's okay to ignore for benchmarking."
            )]
            height: tree.size().height() as u16,
        }
    }

    /// Get the raw flattened lines of both fills and strokes.
    pub fn lines(&self) -> Vec<Line> {
        let mut line_buf = vec![];
        let mut temp_buf = vec![];

        for path in &self.fills {
            flatten::fill(
                Level::new(),
                &path.path,
                path.transform,
                &mut temp_buf,
                &mut FlattenCtx::default(),
                RectU16::new(0, 0, self.width, self.height),
            );
            line_buf.extend(&temp_buf);
        }

        for path in &self.strokes {
            let stroke = Stroke {
                width: path.stroke_width as f64,
                ..Default::default()
            };
            flatten::stroke(
                Level::new(),
                &path.path,
                &stroke,
                path.transform,
                &mut temp_buf,
                &mut FlattenCtx::default(),
                &mut StrokeCtx::default(),
                RectU16::new(0, 0, self.width, self.height),
            );
            line_buf.extend(&temp_buf);
        }

        line_buf
    }

    /// Get the expanded strokes.
    pub fn expanded_strokes(&self) -> Vec<BezPath> {
        let mut paths = vec![];
        let mut stroke_ctx = StrokeCtx::default();

        for path in &self.strokes {
            let stroke = Stroke {
                width: path.stroke_width as f64,
                ..Default::default()
            };
            flatten::expand_stroke(path.path.iter(), &stroke, 0.25, &mut stroke_ctx);
            paths.push(stroke_ctx.output().clone());
        }

        paths
    }

    /// Get the unsorted tiles.
    pub fn unsorted_tiles(&self) -> Tiles {
        let mut tiles = Tiles::new(Level::new(), self.width, self.height);
        let lines = self.lines();
        tiles.make_tiles_analytic_aa(Level::new(), &lines, self.width, self.height);

        tiles
    }

    /// Get the sorted tiles.
    pub fn sorted_tiles(&self) -> Tiles {
        let mut tiles = self.unsorted_tiles();
        tiles.sort_tiles();

        tiles
    }

    /// Get the alpha buffer and rendered strips.
    pub fn strips(&self) -> (Vec<u8>, Vec<Strip>) {
        let mut strip_buf = vec![];
        let mut alpha_buf = vec![];
        let lines = self.lines();
        let tiles = self.sorted_tiles();

        strip::render(
            Level::baseline(),
            &tiles,
            &mut strip_buf,
            &mut alpha_buf,
            Fill::NonZero,
            None,
            &lines,
        );

        (alpha_buf, strip_buf)
    }
}

fn convert(ctx: &mut ConversionContext, g: &Group) {
    ctx.push(convert_transform(&g.transform()));

    for child in g.children() {
        match child {
            Node::Group(group) => {
                convert(ctx, group);
            }
            Node::Path(p) => {
                let converted = convert_path_data(p);

                if p.fill().is_some() {
                    ctx.add_filled_path(converted.clone());
                }

                if let Some(stroke) = p.stroke() {
                    ctx.add_stroked_path(converted, stroke.width().get());
                }
            }
            Node::Image(_) | Node::Text(_) => {}
        }
    }

    ctx.pop();
}

#[derive(Debug, Clone)]
pub struct FilledPath {
    pub path: BezPath,
    pub transform: Affine,
}

#[derive(Debug, Clone)]
pub struct StrokedPath {
    pub path: BezPath,
    pub transform: Affine,
    pub stroke_width: f32,
}

#[derive(Debug)]
struct ConversionContext {
    stack: Vec<Affine>,
    fills: Vec<FilledPath>,
    strokes: Vec<StrokedPath>,
}

impl ConversionContext {
    fn new() -> Self {
        Self {
            stack: vec![],
            fills: vec![],
            strokes: vec![],
        }
    }

    fn push(&mut self, transform: Affine) {
        let new = *self.stack.last().unwrap_or(&Affine::IDENTITY) * transform;
        self.stack.push(new);
    }

    fn add_filled_path(&mut self, path: BezPath) {
        self.fills.push(FilledPath {
            path,
            transform: self.get(),
        });
    }

    fn add_stroked_path(&mut self, path: BezPath, stroke_width: f32) {
        self.strokes.push(StrokedPath {
            path,
            transform: self.get(),
            stroke_width,
        });
    }

    fn get(&self) -> Affine {
        *self.stack.last().unwrap_or(&Affine::IDENTITY)
    }

    fn pop(&mut self) {
        self.stack.pop();
    }
}

fn convert_transform(transform: &usvg::Transform) -> Affine {
    Affine::new([
        transform.sx as f64,
        transform.ky as f64,
        transform.kx as f64,
        transform.sy as f64,
        transform.tx as f64,
        transform.ty as f64,
    ])
}

fn convert_path_data(path: &usvg::Path) -> BezPath {
    let mut bez_path = BezPath::new();

    for e in path.data().segments() {
        match e {
            PathSegment::MoveTo(p) => {
                bez_path.move_to((p.x, p.y));
            }
            PathSegment::LineTo(p) => {
                bez_path.line_to((p.x, p.y));
            }
            PathSegment::QuadTo(p1, p2) => {
                bez_path.quad_to((p1.x, p1.y), (p2.x, p2.y));
            }
            PathSegment::CubicTo(p1, p2, p3) => {
                bez_path.curve_to((p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y));
            }
            PathSegment::Close => {
                bez_path.close_path();
            }
        }
    }

    bez_path
}
