//! Attribute pictures drawn as vectors (plan W9): each is a small Fig
//! drawing rendered by RustFig's own rasteriser at the screen's pixel
//! density, so it is sharp at any scale and shows exactly what the canvas
//! draws (dash patterns, joins, caps, arrowheads). The compositions follow
//! xfig's 32×32 pictures (`w_icons.c`); xfig's bitmaps remain the fallback.
//
// Compositions after xfig src/w_icons.c (xfig licence, see LICENSE-xfig).

use rustfig_geom::{Affine, Point};
use rustfig_model::{
    Arc, Arrow, CapStyle, ColorRef, Document, JoinStyle, Justify, LineKind, Object, ObjectKind, PolyKind, Polyline,
    Style,
};
use rustfig_render::raster::{rasterize, Pixmap, RasterOptions};
use rustfig_render::SceneOptions;
use std::sync::Arc as Shared;

/// Fig units per icon pixel (xfig's pictures are 32×32 pixels).
const U: f64 = crate::mode_icons::U;

/// A picture of an attribute value.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VIcon {
    Line(LineKind),
    Join(JoinStyle),
    Cap(CapStyle),
    /// Arrowhead type and style (`style` 0 hollow, 1 filled).
    Arrow(i32, i32),
    /// Arrows at the start (backward) and end (forward).
    ArrowMode {
        backward: bool,
        forward: bool,
    },
    Arc {
        pie: bool,
    },
    Justify(Justify),
    /// One of xfig's bitmaps traced as a vector drawing (`mode_icons`), by
    /// its name.
    Traced(&'static str),
}

impl VIcon {
    /// Size in icon pixels.
    fn size(self) -> (usize, usize) {
        match self {
            VIcon::Traced(name) => crate::mode_icons::size(name),
            _ => (32, 32),
        }
    }
}

fn style(thickness: i32) -> Style {
    Style {
        line: LineKind::Solid,
        thickness,
        pen_color: ColorRef::BLACK,
        ..Style::default()
    }
}

/// `x`, `y` in icon pixels.
fn px(x: f64, y: f64) -> Point {
    Point::new(x * U, y * U)
}

fn line(st: Style, pts: &[(f64, f64)]) -> Object {
    let p = Polyline::new(PolyKind::Polyline, st, pts.iter().map(|&(x, y)| px(x, y)).collect());
    Object::new(ObjectKind::Polyline(p))
}

fn arrow(kind: i32, style: i32) -> Arrow {
    Arrow {
        kind,
        style,
        thickness: 1.0,
        width: 7.0 * U,
        height: 10.0 * U,
    }
}

/// The picture as a drawing in a 32×32-pixel box (Fig units, black ink;
/// hollow arrowheads are filled white, as fig2dev draws them).
fn drawing(icon: VIcon) -> Vec<Object> {
    match icon {
        VIcon::Line(kind) => {
            let st = Style {
                line: kind,
                style_val: 4.0,
                ..style(2)
            };
            vec![line(st, &[(2.0, 16.0), (30.0, 16.0)])]
        }
        VIcon::Join(join) => {
            // As xfig's pictures: a 6-pixel stroke (thickness 7) turning
            // back at 35° (inside the rasteriser's miter limit), so the
            // miter's long point, the round and the bevel are plain to see.
            // Straight edges on whole pixels.
            let st = Style { join, ..style(7) };
            vec![line(st, &[(1.0, 13.0), (18.0, 13.0), (0.8, 25.1)])]
        }
        VIcon::Cap(cap) => {
            // A 10-pixel stroke (thickness 11) between 2-pixel marks at its
            // true end points, as xfig's pictures: round and projecting caps
            // reach 5 pixels past the marks. Edges on whole pixels.
            let thick = Style { cap, ..style(11) };
            let mark = style(3);
            vec![
                line(thick, &[(10.0, 16.0), (22.0, 16.0)]),
                line(mark, &[(10.0, 7.0), (10.0, 25.0)]),
                line(mark, &[(22.0, 7.0), (22.0, 25.0)]),
            ]
        }
        VIcon::Arrow(kind, style_n) => {
            let mut p = Polyline::new(PolyKind::Polyline, style(1), vec![px(2.0, 16.0), px(28.0, 16.0)]);
            p.forward_arrow = Some(arrow(kind, style_n));
            vec![Object::new(ObjectKind::Polyline(p))]
        }
        VIcon::ArrowMode { backward, forward } => {
            let mut p = Polyline::new(PolyKind::Polyline, style(1), vec![px(3.0, 16.0), px(29.0, 16.0)]);
            let a = Arrow {
                width: 6.0 * U,
                height: 6.0 * U,
                ..arrow(1, 1)
            };
            p.forward_arrow = forward.then_some(a);
            p.backward_arrow = backward.then_some(a);
            vec![Object::new(ObjectKind::Polyline(p))]
        }
        VIcon::Arc { pie } => Arc::through(style(2), px(16.0, 3.0), px(22.0, 16.0), px(16.0, 29.0), pie)
            .map(|a| vec![Object::new(ObjectKind::Arc(a))])
            .unwrap_or_default(),
        VIcon::Traced(name) => crate::mode_icons::drawing(name).unwrap_or_default(),
        VIcon::Justify(j) => {
            // Lines of text of different lengths, aligned as `j` aligns them.
            let st = style(3);
            [(5.0, 22.0), (10.0, 10.0), (15.0, 16.0), (20.0, 12.0), (25.0, 19.0)]
                .iter()
                .map(|&(y, len)| {
                    let (x0, x1) = match j {
                        Justify::Left => (5.0, 5.0 + len),
                        Justify::Center => (16.0 - len / 2.0, 16.0 + len / 2.0),
                        Justify::Right => (27.0 - len, 27.0),
                    };
                    line(st, &[(x0, y), (x1, y)])
                })
                .collect()
        }
    }
}

/// The picture rendered at `scale` device pixels per icon pixel: white,
/// with the ink's darkness as alpha (drawn black on white, so white fills
/// vanish).
pub fn image(icon: VIcon, scale: f32) -> Option<egui::ColorImage> {
    let (w, h) = icon.size();
    render_objects(drawing(icon), w, h, scale)
}

/// Render `objs` (Fig units, 15 per icon pixel) into a `w`×`h`-pixel icon
/// at `scale` device pixels per icon pixel.
pub fn render_objects(objs: Vec<Object>, w: usize, h: usize, scale: f32) -> Option<egui::ColorImage> {
    let scale = if scale.is_finite() {
        scale.clamp(0.25, 16.0)
    } else {
        1.0
    };
    let (pw, ph) = (
        ((w as f32 * scale).round() as usize).clamp(1, 1024),
        ((h as f32 * scale).round() as usize).clamp(1, 1024),
    );
    let mut doc = Document::new();
    doc.objects = objs.into_iter().map(Shared::new).collect();
    let scene = rustfig_render::build_scene(&doc, &SceneOptions::default());
    let mut pm = Pixmap::new(pw as u32, ph as u32)?;
    pm.data_mut().fill(255); // opaque white
    let k = pw as f64 / (w as f64 * U);
    rasterize(
        &scene,
        &mut pm,
        &RasterOptions {
            transform: Affine::scale(k, k),
            min_line_px: 1.0,
            adjust_px: 0.0,
            pattern_k: 1,
            pattern: rustfig_render::pattern::PatternFrame::screen(),
        },
    );
    let pixels = pm
        .data()
        .as_chunks::<4>()
        .0
        .iter()
        .map(|p| {
            let dark = 255 - p.first().copied().unwrap_or(255);
            egui::Color32::from_rgba_premultiplied(dark, dark, dark, dark)
        })
        .collect();
    Some(egui::ColorImage {
        size: [pw, ph],
        source_size: egui::vec2(pw as f32, ph as f32),
        pixels,
    })
}

/// Paint `icon` centred in `rect` at its natural size (one icon pixel per
/// point, as xfig's bitmaps), shrunk if `rect` is smaller, tinted, at the
/// screen's pixel density (made once per icon and scale, kept in egui's
/// memory).
pub fn paint(ui: &egui::Ui, icon: VIcon, rect: egui::Rect, tint: egui::Color32) {
    let (w, h) = icon.size();
    let fit = (rect.width() / w as f32).min(rect.height() / h as f32).min(1.0);
    let scale = fit * ui.ctx().pixels_per_point();
    // Cache key: the scale in 1/8 device pixels per icon pixel.
    let key = (scale * 8.0).round() as i32;
    let id = egui::Id::new(("vector-icon", icon, key));
    let cached: Option<egui::TextureHandle> = ui.ctx().data(|d| d.get_temp(id));
    let tex = match cached {
        Some(t) => t,
        None => {
            let Some(img) = image(icon, key as f32 / 8.0) else {
                return;
            };
            let t = ui
                .ctx()
                .load_texture(format!("vector-icon-{icon:?}-{key}"), img, egui::TextureOptions::LINEAR);
            ui.ctx().data_mut(|d| d.insert_temp(id, t.clone()));
            t
        }
    };
    let r = egui::Rect::from_center_size(rect.center(), egui::vec2(w as f32 * fit, h as f32 * fit));
    ui.painter().image(
        tex.id(),
        r,
        egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)),
        tint,
    );
}

#[cfg(test)]
mod tests {
    use super::*;

    fn ink(icon: VIcon) -> usize {
        let img = image(icon, 1.0).unwrap();
        img.pixels.iter().filter(|p| p.a() > 100).count()
    }

    #[test]
    fn every_picture_has_ink() {
        let mut icons = vec![
            VIcon::Arc { pie: false },
            VIcon::Arc { pie: true },
            VIcon::Join(JoinStyle::Miter),
            VIcon::Cap(CapStyle::Round),
            VIcon::Justify(Justify::Center),
        ];
        for k in [
            LineKind::Solid,
            LineKind::Dashed,
            LineKind::Dotted,
            LineKind::DashDotted,
            LineKind::DashDoubleDotted,
            LineKind::DashTripleDotted,
        ] {
            icons.push(VIcon::Line(k));
        }
        for kind in 0..=14 {
            for st in 0..=1 {
                icons.push(VIcon::Arrow(kind, st));
            }
        }
        for (b, f) in [(false, false), (false, true), (true, false), (true, true)] {
            icons.push(VIcon::ArrowMode {
                backward: b,
                forward: f,
            });
        }
        for i in icons {
            assert!(ink(i) > 10, "{i:?} is empty");
        }
    }

    #[test]
    fn styles_differ() {
        assert!(ink(VIcon::Line(LineKind::Solid)) > ink(VIcon::Line(LineKind::Dashed)));
        assert!(ink(VIcon::Arrow(1, 1)) > ink(VIcon::Arrow(1, 0)), "filled has more ink");
        assert!(ink(VIcon::Arc { pie: true }) > ink(VIcon::Arc { pie: false }));
        assert!(
            ink(VIcon::ArrowMode {
                backward: true,
                forward: true
            }) > ink(VIcon::ArrowMode {
                backward: false,
                forward: false
            })
        );
    }

    /// The cap and join pictures differ visibly, as xfig's do: each step
    /// (butt, round, projecting; bevel, round, miter) adds ink.
    #[test]
    fn caps_and_joins_are_told_apart() {
        let cap = |c| ink(VIcon::Cap(c));
        assert!(cap(CapStyle::Butt) + 4 < cap(CapStyle::Round));
        assert!(cap(CapStyle::Round) < cap(CapStyle::Projecting));
        let join = |j| ink(VIcon::Join(j));
        assert!(join(JoinStyle::Bevel) + 4 < join(JoinStyle::Round));
        assert!(join(JoinStyle::Round) + 4 < join(JoinStyle::Miter));
    }

    #[test]
    fn sharper_at_higher_density() {
        let lo = image(VIcon::Join(JoinStyle::Round), 1.0).unwrap();
        let hi = image(VIcon::Join(JoinStyle::Round), 3.0).unwrap();
        assert_eq!(hi.size, [96, 96]);
        let a = |img: &egui::ColorImage| img.pixels.iter().filter(|p| p.a() > 100).count() as f64;
        // The same shape: ink grows with the area (×9), within anti-aliasing.
        let ratio = a(&hi) / a(&lo);
        assert!((7.0..11.0).contains(&ratio), "{ratio}");
    }
}
