//! End-to-end MSDF generation benchmark across font categories.
//!
//! Run with:
//!   cargo bench -p klyff_msdf --features skrifa --bench msdf_generation

use criterion::{Criterion, Throughput, criterion_group, criterion_main};
use klyff_msdf::{
    AtlasRegionSize, GpuAtlasRegion, MsdfGenerator, MtsdfGpuWriter, ReadGlyphOutlineError,
    SkrifaOutlineProvider,
};
use skrifa::{FontRef, MetadataProvider};
use std::hint::black_box;

const PPEM: f32 = 64.0;
const PADDING_X: usize = 8;
const PADDING_Y: usize = 8;

struct Scenario {
    name: &'static str,
    font_bytes: &'static [u8],
    chars: Vec<char>,
}

fn scenarios() -> Vec<Scenario> {
    let alphanumeric: Vec<char> = (0x0021u32..=0x007E).filter_map(char::from_u32).collect();

    // CJK Unified Ideographs sample — stride through the block to get a
    // representative spread of glyph complexity without rendering all 20k.
    let cjk: Vec<char> = (0x4E00u32..=0x9FFF)
        .step_by(64)
        .filter_map(char::from_u32)
        .collect();

    let icons: Vec<char> = [
        0xE000u32, 0xE002, 0xE00B, 0xE035, 0xE062, 0xE0C4, 0xE052, 0xE057, 0xE06F, 0xE07A, 0xE0A1,
        0xE0B5, 0xE100, 0xE150, 0xE200, 0xE2A0, 0xE300, 0xE400,
    ]
    .into_iter()
    .filter_map(char::from_u32)
    .collect();

    vec![
        Scenario {
            name: "alphanumeric",
            font_bytes: include_bytes!("../../../fonts/OpenSans-Regular.ttf"),
            chars: alphanumeric,
        },
        Scenario {
            name: "cjk",
            font_bytes: include_bytes!("../../../fonts/NotoSansSC-Regular.otf"),
            chars: cjk,
        },
        Scenario {
            name: "icons",
            font_bytes: include_bytes!("../../../fonts/Allkin-Regular.ttf"),
            chars: icons,
        },
    ]
}

fn bench_scenario(c: &mut Criterion, scenario: &Scenario) {
    let font_ref = FontRef::new(scenario.font_bytes).expect("valid font");
    let charmap = font_ref.charmap();
    let glyph_ids: Vec<skrifa::GlyphId> = scenario
        .chars
        .iter()
        .filter_map(|c| charmap.map(*c))
        .collect();

    let mut group = c.benchmark_group(format!("msdf_generation/{}", scenario.name));
    group.throughput(Throughput::Elements(glyph_ids.len() as u64));

    group.bench_function("klyff", |b| {
        let mut generator = MsdfGenerator::new();
        b.iter(|| {
            for gid in &glyph_ids {
                let provider = SkrifaOutlineProvider::new(font_ref.clone(), *gid);
                let mut outline = match generator.read_glyph(&provider) {
                    Ok(o) => o,
                    Err(ReadGlyphOutlineError::EmptyGlyph)
                    | Err(ReadGlyphOutlineError::ShouldRasterize)
                    | Err(ReadGlyphOutlineError::NotFound)
                    | Err(ReadGlyphOutlineError::Other(_)) => continue,
                };
                let size = AtlasRegionSize {
                    inner_width: (outline.width_em() * PPEM).round().max(1.0) as usize,
                    inner_height: (outline.height_em() * PPEM).round().max(1.0) as usize,
                    padding_x: PADDING_X,
                    padding_y: PADDING_Y,
                };
                let bytes = outline.generate_mtsdf(size);
                black_box(bytes);
            }
        });
    });

    if let Some(gpu) = GpuCtx::new() {
        group.bench_function("klyff_gpu", |b| {
            let mut generator = MsdfGenerator::new();
            let mut writer = MtsdfGpuWriter::new(&gpu.device);
            // Single-layer atlas big enough to hold the whole scenario via a
            // trivial row packer reset per iteration.
            let tex_size: u32 = 4096;
            let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
                label: Some("bench atlas"),
                size: wgpu::Extent3d {
                    width: tex_size,
                    height: tex_size,
                    depth_or_array_layers: 1,
                },
                mip_level_count: 1,
                sample_count: 1,
                dimension: wgpu::TextureDimension::D2,
                format: wgpu::TextureFormat::Rgba8Unorm,
                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
                    | wgpu::TextureUsages::TEXTURE_BINDING,
                view_formats: &[],
            });

            b.iter(|| {
                let mut cursor_x: u32 = 0;
                let mut cursor_y: u32 = 0;
                let mut row_h: u32 = 0;
                for gid in &glyph_ids {
                    let provider = SkrifaOutlineProvider::new(font_ref.clone(), *gid);
                    let outline = match generator.read_glyph(&provider) {
                        Ok(o) => o,
                        Err(_) => continue,
                    };
                    let inner_w = (outline.width_em() * PPEM).round().max(1.0) as u32;
                    let inner_h = (outline.height_em() * PPEM).round().max(1.0) as u32;
                    let w = inner_w + PADDING_X as u32 * 2;
                    let h = inner_h + PADDING_Y as u32 * 2;
                    if cursor_x + w > tex_size {
                        cursor_x = 0;
                        cursor_y += row_h;
                        row_h = 0;
                    }
                    if cursor_y + h > tex_size {
                        // Skip remaining glyphs; bench texture is full.
                        break;
                    }
                    let region = GpuAtlasRegion {
                        layer: 0,
                        min_x: cursor_x,
                        min_y: cursor_y,
                        width: w,
                        height: h,
                        padding_x: PADDING_X as u32,
                        padding_y: PADDING_Y as u32,
                    };
                    cursor_x += w;
                    row_h = row_h.max(h);
                    writer.add_glyph(outline, region);
                }

                let mut encoder =
                    gpu.device
                        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                            label: Some("bench encoder"),
                        });
                writer.write_glyphs(&gpu.device, &gpu.queue, &mut encoder, &texture);
                let cb = encoder.finish();
                gpu.queue.submit([cb]);
                gpu.device
                    .poll(wgpu::PollType::wait_indefinitely())
                    .unwrap();
            });
        });
    } else {
        eprintln!(
            "no wgpu adapter available; skipping {} klyff_gpu",
            scenario.name
        );
    }

    group.finish();
}

struct GpuCtx {
    device: wgpu::Device,
    queue: wgpu::Queue,
}

impl GpuCtx {
    fn new() -> Option<Self> {
        let instance = wgpu::Instance::default();
        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
            power_preference: wgpu::PowerPreference::HighPerformance,
            force_fallback_adapter: false,
            compatible_surface: None,
        }))
        .ok()?;
        let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
            label: Some("bench device"),
            required_features: wgpu::Features::empty(),
            required_limits: wgpu::Limits::default(),
            memory_hints: wgpu::MemoryHints::Performance,
            trace: wgpu::Trace::Off,
            experimental_features: wgpu::ExperimentalFeatures::disabled(),
        }))
        .ok()?;
        Some(Self { device, queue })
    }
}

fn bench_all(c: &mut Criterion) {
    for scenario in &scenarios() {
        bench_scenario(c, scenario);
    }
}

criterion_group!(benches, bench_all);
criterion_main!(benches);
