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

//! Visualize the intermediate stages of `vello_common` in an SVG.

#![allow(
    clippy::cast_possible_truncation,
    reason = "this is only a debug tool, so we can ignore them"
)]

use clap::Parser;
use std::collections::HashSet;
use std::path;
use svg::node::element::path::Data;
use svg::node::element::{Line as SvgLine, Path, Rectangle};
use svg::{Document, Node};
use vello_common::fearless_simd::Level;
use vello_common::flatten::{FlattenCtx, Line};
use vello_common::geometry::RectU16;
use vello_common::kurbo::{Affine, BezPath, Cap, Join, Stroke, StrokeCtx};
use vello_common::peniko::Fill;
use vello_common::strip::{
    Strip, StripAlphaFillSegment, StripFillSegment, visit_strip_fill_segments,
};
use vello_common::tile::{Tile, Tiles};
use vello_common::{flatten, strip};

fn main() {
    let args = Args::parse();

    let mut document =
        Document::new().set("viewBox", (-10, -10, args.width + 20, args.height + 20));

    let mut line_buf = vec![];
    let mut tiles = Tiles::new(Level::new(), args.width, args.height);
    let mut strip_buf = vec![];
    let mut alpha_buf = vec![];

    let stages = &args.stages;

    // Not super efficient doing it this way, but it doesn't really matter.

    if stages.iter().any(|s| s.requires_flatten()) {
        if !args.stroke {
            flatten::fill(
                Level::new(),
                &args.path,
                Affine::IDENTITY,
                &mut line_buf,
                &mut FlattenCtx::default(),
                RectU16::new(0, 0, args.width, args.height),
            );
        } else {
            let stroke = Stroke {
                width: args.stroke_width as f64,
                join: Join::Bevel,
                start_cap: Cap::Butt,
                end_cap: Cap::Butt,
                ..Default::default()
            };
            flatten::stroke(
                Level::new(),
                &args.path,
                &stroke,
                Affine::IDENTITY,
                &mut line_buf,
                &mut FlattenCtx::default(),
                &mut StrokeCtx::default(),
                RectU16::new(0, 0, args.width, args.height),
            );
        }
    }

    if stages.iter().any(|s| s.requires_tiling()) {
        tiles.make_tiles_analytic_aa(Level::new(), &line_buf, args.width, args.height);
        tiles.sort_tiles();
    }

    if stages.iter().any(|s| s.requires_strips()) {
        strip::render(
            Level::new(),
            &tiles,
            &mut strip_buf,
            &mut alpha_buf,
            args.fill_rule,
            None,
            &line_buf,
        );
    }

    draw_grid(&mut document, args.width, args.height);

    if stages.contains(&Stage::LineSegments) {
        draw_line_segments(&mut document, &line_buf);
    }

    if stages.contains(&Stage::TileAreas) {
        draw_tile_areas(&mut document, &tiles);
    }

    if stages.contains(&Stage::StripAreas) {
        draw_strip_areas(&mut document, &strip_buf, &alpha_buf);
    }

    if stages.contains(&Stage::Strips) {
        draw_strips(&mut document, &strip_buf, &alpha_buf);
    }

    if stages.contains(&Stage::FillSegments) {
        draw_fill_segments(
            &mut document,
            &strip_buf,
            &alpha_buf,
            args.width,
            args.height,
        );
    }

    let path = path::absolute("debug.svg").unwrap();
    eprintln!("Saved debug output to '{}'", path.display());
    svg::save(path, &document).unwrap();
}

fn draw_grid(document: &mut Document, width: u16, height: u16) {
    let border_data = Data::new()
        .move_to((0, 0))
        .line_to((width, 0))
        .line_to((width, height))
        .line_to((0, height))
        .close();

    let border = Path::new()
        .set("stroke-width", 0.2)
        .set("fill", "none")
        .set("vectorEffect", "non-scaling-stroke")
        .set("stroke", "black")
        .set("d", border_data);

    let grid_line = |data: Data| {
        Path::new()
            .set("stroke", "grey")
            .set("stroke-opacity", 0.3)
            .set("stroke-width", 0.1)
            .set("vectorEffect", "non-scaling-stroke")
            .set("d", data)
    };

    for i in 1..height {
        let data = Data::new().move_to((0, i)).line_to((width, i));

        document.append(grid_line(data));
    }

    for i in 1..width {
        let data = Data::new().move_to((i, 0)).line_to((i, height));

        document.append(grid_line(data));
    }

    document.append(border);
}

fn draw_line_segments(document: &mut Document, line_buf: &[Line]) {
    let svg_line = SvgLine::new()
        .set("stroke-width", 0.1)
        .set("fill", "none")
        .set("fill-opacity", 0.1);

    for line in line_buf {
        let dy = line.p1.y - line.p0.y;
        let color = if dy < 0. {
            // Lines oriented upwards add to winding.
            "green"
        } else if dy > 0. {
            // Lines oriented upwards subtract from winding.
            "red"
        } else {
            // Horizontal lines don't impact winding.
            "grey"
        };

        let svg_line = svg_line
            .clone()
            .set("x1", line.p0.x)
            .set("y1", line.p0.y)
            .set("x2", line.p1.x)
            .set("y2", line.p1.y)
            .set("stroke", color);
        document.append(svg_line);
    }
}

fn draw_tile_areas(document: &mut Document, tiles: &Tiles) {
    let mut seen = HashSet::new();

    for i in 0..tiles.len() {
        let tile = tiles.get(i);
        let x = tile.x * Tile::WIDTH;
        let y = tile.y * Tile::HEIGHT;

        if seen.contains(&(x, y)) {
            continue;
        }

        let color = "darkblue";

        let rect = Rectangle::new()
            .set("x", x)
            .set("y", y)
            .set("width", Tile::WIDTH)
            .set("height", Tile::HEIGHT)
            .set("fill", color)
            .set("stroke", color)
            .set("stroke-opacity", 1.0)
            .set("stroke-width", 0.2)
            .set("fill-opacity", 0.1);

        document.append(rect);

        seen.insert((x, y));
    }
}

fn draw_strip_areas(document: &mut Document, strips: &[Strip], alphas: &[u8]) {
    for i in 0..strips.len() {
        let strip = &strips[i];
        let x = strip.x;
        let y = strip.strip_y();

        let end = strips
            .get(i + 1)
            .map(|s| s.alpha_idx() / u32::from(Tile::HEIGHT))
            .unwrap_or(alphas.len() as u32);

        let width = end - strip.alpha_idx() / u32::from(Tile::HEIGHT);

        // TODO: Account for even-odd?
        let color = if strip.fill_gap() { "red" } else { "limegreen" };

        let rect = Rectangle::new()
            .set("x", x)
            .set("y", y * Tile::HEIGHT)
            .set("width", width)
            .set("height", Tile::HEIGHT)
            .set("stroke", color)
            .set("fill", color)
            .set("fill-opacity", 0.4)
            .set("stroke-opacity", 0.6)
            .set("stroke-width", 0.2);

        document.append(rect);
    }
}

fn draw_strips(document: &mut Document, strips: &[Strip], alphas: &[u8]) {
    for s in 0..strips.len() {
        let strip = &strips[s];

        let end = strips
            .get(s + 1)
            .map(|st| st.alpha_idx() / u32::from(Tile::HEIGHT))
            .unwrap_or(alphas.len() as u32);

        let width = u16::try_from(end - strip.alpha_idx() / u32::from(Tile::HEIGHT)).unwrap();

        // TODO: Account for even-odd?
        let color = if strip.fill_gap() { "red" } else { "limegreen" };

        for x in 0..width {
            for y in 0..Tile::HEIGHT {
                let alpha = alphas[strip.alpha_idx() as usize
                    + usize::from(x) * usize::from(Tile::HEIGHT)
                    + usize::from(y)];
                let rect = Rectangle::new()
                    .set("x", strip.x + x)
                    .set("y", strip.y + y)
                    .set("width", 1)
                    .set("height", 1)
                    .set("fill", color)
                    .set("fill-opacity", alpha as f32 / 255.0);

                document.append(rect);
            }
        }
    }
}

fn draw_fill_segments(
    document: &mut Document,
    strips: &[Strip],
    alphas: &[u8],
    width: u16,
    height: u16,
) {
    let tile_bounds = RectU16::new(
        0,
        0,
        width.div_ceil(Tile::WIDTH),
        height.div_ceil(Tile::HEIGHT),
    );

    visit_strip_fill_segments(
        strips,
        tile_bounds,
        document,
        |document, segment| draw_alpha_fill_segment(document, segment, alphas, width, height),
        |document, segment| draw_fill_segment(document, segment, width, height),
    );
}

fn draw_alpha_fill_segment(
    document: &mut Document,
    segment: StripAlphaFillSegment,
    alphas: &[u8],
    width: u16,
    height: u16,
) {
    let x0 = segment.x0();
    let x1 = segment.x1().min(width);
    let y0 = segment.y();
    let y1 = y0.saturating_add(Tile::HEIGHT).min(height);

    for x in x0..x1 {
        for y in y0..y1 {
            let alpha_idx = segment.alpha_idx as usize
                + usize::from(x - x0) * usize::from(Tile::HEIGHT)
                + usize::from(y - y0);
            let rect = Rectangle::new()
                .set("x", x)
                .set("y", y)
                .set("width", 1)
                .set("height", 1)
                .set("fill", "yellow")
                .set("fill-opacity", f32::from(alphas[alpha_idx]) / 255.0);

            document.append(rect);
        }
    }
}

fn draw_fill_segment(document: &mut Document, segment: StripFillSegment, width: u16, height: u16) {
    let x0 = segment.x0();
    let x1 = segment.x1().min(width);
    let y0 = segment.y();
    let y1 = y0.saturating_add(Tile::HEIGHT).min(height);
    let rect = Rectangle::new()
        .set("x", x0)
        .set("y", y0)
        .set("width", x1 - x0)
        .set("height", y1 - y0)
        .set("fill", "blue");

    document.append(rect);
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Stage {
    /// Draw the flattened lines of the path.
    LineSegments,
    /// Draw the tile areas covered by the path.
    TileAreas,
    /// Draw the stripped areas.
    StripAreas,
    /// Draw the strips with their alpha masks.
    Strips,
    /// Draw the fill segments formed by the strips.
    FillSegments,
}

impl Stage {
    fn requires_flatten(&self) -> bool {
        matches!(self, Self::LineSegments) || self.requires_tiling()
    }
    fn requires_tiling(&self) -> bool {
        matches!(self, Self::TileAreas) || self.requires_strips()
    }

    fn requires_strips(&self) -> bool {
        matches!(self, Self::StripAreas | Self::Strips | Self::FillSegments)
    }
}

impl std::str::FromStr for Stage {
    type Err = String;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match input.to_lowercase().as_str() {
            "ls" | "line_segments" => Ok(Self::LineSegments),
            "ta" | "tile_areas" => Ok(Self::TileAreas),
            "sa" | "strip_areas" => Ok(Self::StripAreas),
            "s" | "strips" => Ok(Self::Strips),
            "fs" | "fill_segments" => Ok(Self::FillSegments),
            _ => Err(format!(
                "invalid stage: {input}. Expected one of `line_segments`, `tile_areas`, `strip_areas`, `strips`, or `fill_segments`, or their acronym"
            )),
        }
    }
}

#[derive(Parser, Debug)]
struct Args {
    /// The width of the viewport.
    #[arg(long, default_value_t = 50, value_parser = parse_dim)]
    pub width: u16,
    /// The height of the viewport.
    #[arg(long, default_value_t = 50, value_parser = parse_dim)]
    pub height: u16,
    /// The SVG path that should be drawn.
    #[arg(short, long, value_parser = parse_path)]
    pub path: BezPath,
    /// Whether the path should be stroked (if false, it will be filled).
    #[arg(short, long, default_value_t = false)]
    pub stroke: bool,
    /// The stroke width for stroking operations.
    #[arg(long, default_value_t = 1.0)]
    pub stroke_width: f32,
    /// The fill rule used for fill operations.
    #[arg(long, default_value = "nonzero", value_parser = parse_fill_rule)]
    pub fill_rule: Fill,
    /// The stages of the pipeline that should be included in the SVG.
    #[arg(long, num_args = 1.., value_delimiter = ',', default_value = "ls,ta,ti,sa,s", value_parser = parse_stages)]
    pub stages: Vec<Stage>,
}

fn parse_stages(val: &str) -> Result<Stage, String> {
    val.parse::<Stage>()
}

fn parse_dim(val: &str) -> Result<u16, String> {
    let parsed = val
        .parse::<u16>()
        .map_err(|_| "width/height must be a positive integer")?;
    if parsed > 500 {
        Err(
            "the width/height cannot be larger than 500 (otherwise the SVG will be very slow)."
                .to_string(),
        )
    } else {
        Ok(parsed)
    }
}

fn parse_path(val: &str) -> Result<BezPath, String> {
    BezPath::from_svg(val).map_err(|_| "failed to parse the SVG path".to_string())
}

fn parse_fill_rule(val: &str) -> Result<Fill, String> {
    match val {
        "nonzero" => Ok(Fill::NonZero),
        "evenodd" => Ok(Fill::EvenOdd),
        _ => Err(format!(
            "unsupported fill rule: {val}. Expected one of `nonzero` or `evenodd`"
        )),
    }
}
