use bone_jig::icon::{IconMaterial, IconModel, Prim, icon_camera, icon_style};
use bone_jig::models::model;
use bone_jig::offscreen_context;
use bone_render::{
    SnapshotFrame, SolidRenderer, ViewportExtent, ViewportPx, encode_png, encode_png_rgba,
};
use bone_types::{DisplayMode, IconId, LinearRgba};

const UPDATE_ENV: &str = "BONE_UPDATE_ICON_SMOKE";
const TILE: u32 = 96;
const COLS: u32 = 8;

fn box_model() -> IconModel {
    IconModel::new(vec![Prim::cuboid(
        [0.0, 0.0, 0.0],
        [0.6, 0.6, 0.6],
        IconMaterial::new(LinearRgba::new(0.82, 0.84, 0.88, 1.0)),
    )])
}

fn render(
    renderer: &mut SolidRenderer,
    ctx: &bone_render::OffscreenContext,
    m: &IconModel,
) -> SnapshotFrame {
    let (scene, edges) = m.tessellate();
    let Ok(frame) = renderer.render_display(
        ctx,
        &scene,
        &edges,
        icon_camera(m.view()),
        &icon_style(),
        DisplayMode::ShadedWithEdges,
    ) else {
        panic!("render_display failed");
    };
    frame
}

fn visible_pixels(frame: &SnapshotFrame, background: [u8; 4]) -> usize {
    frame
        .rgba()
        .chunks_exact(4)
        .filter(|px| px[0] != background[0] || px[1] != background[1] || px[2] != background[2])
        .count()
}

#[test]
fn box_icon_renders_visible_geometry() {
    let extent = ViewportExtent::square(ViewportPx::new(256));
    let Ok(ctx) = offscreen_context(extent) else {
        panic!("offscreen context with the software adapter is unavailable");
    };
    let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format());
    let frame = render(&mut renderer, &ctx, &box_model());
    let background = icon_style().background().to_rgba8();
    assert!(
        visible_pixels(&frame, background) > 1000,
        "expected the box to cover many pixels",
    );

    if std::env::var(UPDATE_ENV).is_ok() {
        let Ok(png) = encode_png(&frame) else {
            panic!("encode_png failed");
        };
        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../../target/icon_smoke.png");
        let Ok(()) = std::fs::write(&path, &png) else {
            panic!("write smoke png");
        };
    }
}

#[test]
fn every_icon_model_renders_visible_geometry() {
    let extent = ViewportExtent::square(ViewportPx::new(TILE));
    let Ok(ctx) = offscreen_context(extent) else {
        panic!("offscreen context with the software adapter is unavailable");
    };
    let mut renderer = SolidRenderer::new(ctx.gpu(), ctx.color_format());
    let background = icon_style().background().to_rgba8();

    let frames: Vec<(IconId, SnapshotFrame)> = IconId::ALL
        .iter()
        .map(|&id| (id, render(&mut renderer, &ctx, &model(id))))
        .collect();

    frames.iter().for_each(|(id, frame)| {
        let visible = visible_pixels(frame, background);
        assert!(
            visible > 60,
            "icon {id:?} rendered nearly empty: {visible} visible pixels",
        );
    });

    if std::env::var(UPDATE_ENV).is_ok() {
        write_contact_sheet(&frames, background);
    }
}

fn write_contact_sheet(frames: &[(IconId, SnapshotFrame)], background: [u8; 4]) {
    let count = u32::try_from(frames.len()).unwrap_or(0);
    let rows = count.div_ceil(COLS);
    let width = COLS * TILE;
    let height = rows * TILE;
    let mut sheet = vec![0u8; (width * height * 4) as usize];
    sheet
        .chunks_exact_mut(4)
        .for_each(|px| px.copy_from_slice(&background));

    frames.iter().enumerate().for_each(|(index, (_, frame))| {
        let idx = u32::try_from(index).unwrap_or(0);
        let col = idx % COLS;
        let row = idx / COLS;
        let ox = col * TILE;
        let oy = row * TILE;
        (0..TILE).for_each(|y| {
            let src_start = (y * TILE * 4) as usize;
            let dst_start = (((oy + y) * width + ox) * 4) as usize;
            let span = (TILE * 4) as usize;
            sheet[dst_start..dst_start + span]
                .copy_from_slice(&frame.rgba()[src_start..src_start + span]);
        });
    });

    let extent = ViewportExtent::new(ViewportPx::new(width), ViewportPx::new(height));
    let Ok(png) = encode_png_rgba(extent, &sheet) else {
        panic!("encode_png_rgba failed");
    };
    let path =
        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/icon_gallery.png");
    let Ok(()) = std::fs::write(&path, &png) else {
        panic!("write gallery png");
    };
}
