use std::{error::Error, fmt, path::PathBuf, sync::Arc, time::Instant};

use ab_glyph::{Font, FontArc, Glyph, ScaleFont, point};
use mocari::{
    ExpressionManager, ModelRuntime, MotionPlayer,
    assets::{DecodedTexture, load_model_runtime},
    core::Matrix44,
    expression::load_expression,
    moc3::{Moc3DrawableMesh, Moc3DrawableVertex},
    motion::load_motion,
    render::wgpu::{
        WgpuClippingPlan, WgpuClippingResources, WgpuLive2dRenderer, WgpuMaskRenderTarget,
        WgpuMeshBuffers, WgpuTexture, WgpuTransform, preferred_surface_format,
    },
};
use winit::{
    application::ApplicationHandler,
    dpi::{LogicalSize, PhysicalPosition, PhysicalSize},
    event::{ElementState, MouseButton, WindowEvent},
    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
    window::{Window, WindowId},
};

const WINDOW_WIDTH: u32 = 900;
const WINDOW_HEIGHT: u32 = 900;
const MASK_TEXTURE_SIZE: u32 = 256;
const MODEL_VIEW_FILL: f32 = 1.85;
const BUTTON_X: f64 = 16.0;
const SWITCH_BUTTON_Y: f64 = 16.0;
const MOTION_BUTTON_Y: f64 = 70.0;
const EXPRESSION_BUTTON_Y: f64 = 124.0;
const PREV_PARAMETER_BUTTON_Y: f64 = 178.0;
const NEXT_PARAMETER_BUTTON_Y: f64 = 232.0;
const RESET_PARAMETER_BUTTON_Y: f64 = 286.0;
const PARAMETER_LABEL_Y: f64 = 346.0;
const PARAMETER_SLIDER_Y: f64 = 386.0;
const BUTTON_WIDTH: f64 = 168.0;
const BUTTON_HEIGHT: f64 = 42.0;
const BUTTON_RGBA: &[u8] = &[46, 65, 78, 235];
const SLIDER_X: f64 = 16.0;
const SLIDER_WIDTH: f64 = 260.0;
const SLIDER_HEIGHT: f64 = 18.0;
const SLIDER_TRACK_RGBA: &[u8] = &[78, 90, 98, 235];
const SLIDER_FILL_RGBA: &[u8] = &[76, 149, 208, 245];
const TEXT_HEIGHT_PX: f32 = 22.0;
const TEXT_RGBA: [u8; 4] = [232, 238, 242, 255];

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum ButtonAction {
    SwitchModel,
    PlayMotion,
    PlayExpression,
    PreviousParameter,
    NextParameter,
    ResetParameter,
}

#[derive(Debug, Copy, Clone, PartialEq)]
struct ButtonSpec {
    action: ButtonAction,
    label: &'static str,
    top: f64,
}

const BUTTON_SPECS: &[ButtonSpec] = &[
    ButtonSpec {
        action: ButtonAction::SwitchModel,
        label: "Switch Model",
        top: SWITCH_BUTTON_Y,
    },
    ButtonSpec {
        action: ButtonAction::PlayMotion,
        label: "Play Motion",
        top: MOTION_BUTTON_Y,
    },
    ButtonSpec {
        action: ButtonAction::PlayExpression,
        label: "Play Expression",
        top: EXPRESSION_BUTTON_Y,
    },
    ButtonSpec {
        action: ButtonAction::PreviousParameter,
        label: "Prev Param",
        top: PREV_PARAMETER_BUTTON_Y,
    },
    ButtonSpec {
        action: ButtonAction::NextParameter,
        label: "Next Param",
        top: NEXT_PARAMETER_BUTTON_Y,
    },
    ButtonSpec {
        action: ButtonAction::ResetParameter,
        label: "Reset Param",
        top: RESET_PARAMETER_BUTTON_Y,
    },
];

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct ModelSpec {
    name: &'static str,
    path: &'static str,
}

const MODEL_SPECS: &[ModelSpec] = &[
    ModelSpec {
        name: "Wanko",
        path: "assets/models/Wanko/Wanko.model3.json",
    },
    ModelSpec {
        name: "Hiyori",
        path: "assets/models/Hiyori/Hiyori.model3.json",
    },
    ModelSpec {
        name: "Haru",
        path: "assets/models/Haru/Haru.model3.json",
    },
    ModelSpec {
        name: "Ren",
        path: "assets/models/Ren/Ren.model3.json",
    },
    ModelSpec {
        name: "Mark",
        path: "assets/models/Mark/Mark.model3.json",
    },
    ModelSpec {
        name: "Rice",
        path: "assets/models/Rice/Rice.model3.json",
    },
    ModelSpec {
        name: "Natori",
        path: "assets/models/Natori/Natori.model3.json",
    },
    ModelSpec {
        name: "Mao",
        path: "assets/models/Mao/Mao.model3.json",
    },
];

fn main() -> Result<(), Box<dyn Error>> {
    let event_loop = EventLoop::new()?;
    event_loop.set_control_flow(ControlFlow::Wait);

    let mut app = ShowModelApp::default();
    event_loop.run_app(&mut app)?;
    Ok(())
}

#[derive(Default)]
struct ShowModelApp {
    state: Option<WindowState>,
}

impl ApplicationHandler for ShowModelApp {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        if self.state.is_some() {
            return;
        }

        let attributes = Window::default_attributes()
            .with_title(window_title(MODEL_SPECS[0]))
            .with_inner_size(LogicalSize::new(WINDOW_WIDTH, WINDOW_HEIGHT));
        let window = match event_loop.create_window(attributes) {
            Ok(window) => Arc::new(window),
            Err(error) => {
                eprintln!("failed to create window: {error}");
                event_loop.exit();
                return;
            }
        };

        match pollster::block_on(WindowState::new(window)) {
            Ok(state) => {
                state.window.request_redraw();
                self.state = Some(state);
            }
            Err(error) => {
                eprintln!("failed to initialize renderer: {error}");
                event_loop.exit();
            }
        }
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        window_id: WindowId,
        event: WindowEvent,
    ) {
        let Some(state) = self.state.as_mut() else {
            return;
        };
        if state.window.id() != window_id {
            return;
        }

        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::Resized(size) => {
                if let Err(error) = state.resize(size) {
                    eprintln!("resize failed: {error}");
                    event_loop.exit();
                }
            }
            WindowEvent::ScaleFactorChanged { .. } => {
                if let Err(error) = state.resize(state.window.inner_size()) {
                    eprintln!("resize failed: {error}");
                    event_loop.exit();
                }
            }
            WindowEvent::CursorMoved { position, .. } => {
                state.cursor_position = Some(position);
                if state.dragging_parameter_slider
                    && let Err(error) = state.update_parameter_slider(position.x)
                {
                    eprintln!("parameter slider failed: {error}");
                    event_loop.exit();
                }
            }
            WindowEvent::MouseInput {
                state: button_state,
                button,
                ..
            } => {
                if button == MouseButton::Left {
                    let result = match button_state {
                        ElementState::Pressed => state.handle_left_press(),
                        ElementState::Released => {
                            state.dragging_parameter_slider = false;
                            Ok(())
                        }
                    };
                    if let Err(error) = result {
                        eprintln!("button action failed: {error}");
                        event_loop.exit();
                    }
                }
            }
            WindowEvent::RedrawRequested => {
                if let Err(error) = state.render() {
                    eprintln!("render failed: {error}");
                    event_loop.exit();
                }
            }
            _ => {}
        }
    }

    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
        if let Some(state) = self.state.as_ref()
            && state.needs_continuous_redraw()
        {
            state.window.request_redraw();
        }
    }
}

struct WindowState {
    window: Arc<Window>,
    surface: wgpu::Surface<'static>,
    device: wgpu::Device,
    queue: wgpu::Queue,
    config: wgpu::SurfaceConfiguration,
    renderer: WgpuLive2dRenderer,
    font: FontArc,
    model_index: usize,
    model: LoadedModel,
    button_buffers: WgpuMeshBuffers,
    button_texture: WgpuTexture,
    button_uis: Vec<ButtonUi>,
    ui_transform: WgpuTransform,
    parameter_label: LabelQuad,
    slider_track_buffers: WgpuMeshBuffers,
    slider_fill_buffers: WgpuMeshBuffers,
    slider_track_texture: WgpuTexture,
    slider_fill_texture: WgpuTexture,
    fps_label: LabelQuad,
    fps_meter: FpsMeter,
    selected_parameter_index: Option<usize>,
    dragging_parameter_slider: bool,
    cursor_position: Option<PhysicalPosition<f64>>,
    last_frame: Instant,
    rng: u64,
}

struct LoadedModel {
    runtime: ModelRuntime,
    motions: Vec<PathBuf>,
    expressions: Vec<PathBuf>,
    player: Option<MotionPlayer>,
    expression_manager: ExpressionManager,
    dirty: bool,
    mesh_buffers: WgpuMeshBuffers,
    textures: Vec<WgpuTexture>,
    clipping_resources: WgpuClippingResources,
    mask_target: WgpuMaskRenderTarget,
    transform: WgpuTransform,
    model_bounds: ModelBounds,
}

struct LabelQuad {
    buffers: WgpuMeshBuffers,
    texture: WgpuTexture,
}

struct ButtonUi {
    transform: WgpuTransform,
    label: LabelQuad,
}

struct FpsMeter {
    sample_frames: u32,
    sample_elapsed: f32,
    total_frames: u64,
    total_elapsed: f32,
    last_presented_at: Option<Instant>,
    label: String,
}

impl FpsMeter {
    fn new() -> Self {
        Self {
            sample_frames: 0,
            sample_elapsed: 0.0,
            total_frames: 0,
            total_elapsed: 0.0,
            last_presented_at: None,
            label: "FPS -- AVG --".to_owned(),
        }
    }

    fn label(&self) -> &str {
        &self.label
    }

    fn reset(&mut self) {
        *self = Self::new();
    }

    fn record_present(&mut self, now: Instant) -> Option<String> {
        let previous = self.last_presented_at.replace(now)?;
        let delta = now.duration_since(previous).as_secs_f32();
        if delta <= 0.0 {
            return None;
        }
        if delta > 0.25 {
            self.sample_frames = 0;
            self.sample_elapsed = 0.0;
            return None;
        }

        self.sample_frames += 1;
        self.sample_elapsed += delta;
        self.total_frames += 1;
        self.total_elapsed += delta;
        if self.sample_elapsed < 0.5 {
            return None;
        }

        let fps = self.sample_frames as f32 / self.sample_elapsed;
        let average = self.total_frames as f32 / self.total_elapsed;
        self.sample_frames = 0;
        self.sample_elapsed = 0.0;
        let label = format!("FPS {:.0} AVG {:.0}", fps, average);
        if label == self.label {
            None
        } else {
            self.label = label.clone();
            Some(label)
        }
    }
}

impl WindowState {
    async fn new(window: Arc<Window>) -> Result<Self, Box<dyn Error>> {
        let size = window.inner_size();
        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
        let surface = instance.create_surface(window.clone())?;
        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::HighPerformance,
                force_fallback_adapter: false,
                compatible_surface: Some(&surface),
                apply_limit_buckets: false,
            })
            .await?;
        let (device, queue) = adapter
            .request_device(&wgpu::DeviceDescriptor {
                label: Some("show_model.device"),
                required_features: wgpu::Features::empty(),
                required_limits: wgpu::Limits::default(),
                ..Default::default()
            })
            .await?;

        let mut config = surface
            .get_default_config(&adapter, size.width.max(1), size.height.max(1))
            .ok_or(ExampleError("surface is not supported by this adapter"))?;
        let capabilities = surface.get_capabilities(&adapter);
        config.format = preferred_surface_format(&capabilities.formats)
            .ok_or(ExampleError("surface exposes no texture formats"))?;
        config.present_mode = preferred_present_mode(&capabilities.present_modes);
        config.desired_maximum_frame_latency = 3;
        surface.configure(&device, &config);

        let renderer = WgpuLive2dRenderer::new(&device, config.format);
        let font = load_font()?;
        let model_index = 0;
        let model =
            load_rendered_model(&renderer, &device, &queue, MODEL_SPECS[model_index], size)?;
        let button_buffers = create_button_quad_buffers(&device, size, SWITCH_BUTTON_Y)?;
        let button_texture = renderer.create_rgba8_texture(&device, &queue, 1, 1, BUTTON_RGBA)?;
        let button_uis = create_button_uis(&renderer, &device, &queue, &font, size)?;
        let ui_transform = renderer.create_transform(&device, &Matrix44::identity());
        let selected_parameter_index = initial_parameter_selection(&model.runtime);
        let parameter_label = create_parameter_label_quad(
            &renderer,
            &device,
            &queue,
            &font,
            &model.runtime,
            selected_parameter_index,
            size,
        )?;
        let slider_track_buffers = create_slider_track_buffers(&device, size)?;
        let slider_fill_buffers = create_slider_fill_buffers(
            &device,
            size,
            selected_parameter_normalized(&model.runtime, selected_parameter_index),
        )?;
        let slider_track_texture =
            renderer.create_rgba8_texture(&device, &queue, 1, 1, SLIDER_TRACK_RGBA)?;
        let slider_fill_texture =
            renderer.create_rgba8_texture(&device, &queue, 1, 1, SLIDER_FILL_RGBA)?;
        let fps_meter = FpsMeter::new();
        let fps_label =
            create_fps_label_quad(&renderer, &device, &queue, &font, fps_meter.label(), size)?;
        window.set_title(&window_title(MODEL_SPECS[model_index]));

        let now = Instant::now();
        Ok(Self {
            window,
            surface,
            device,
            queue,
            config,
            renderer,
            font,
            model_index,
            model,
            button_buffers,
            button_texture,
            button_uis,
            ui_transform,
            parameter_label,
            slider_track_buffers,
            slider_fill_buffers,
            slider_track_texture,
            slider_fill_texture,
            fps_label,
            fps_meter,
            selected_parameter_index,
            dragging_parameter_slider: false,
            cursor_position: None,
            last_frame: now,
            rng: 0x9e37_79b9_7f4a_7c15,
        })
    }

    fn resize(&mut self, size: PhysicalSize<u32>) -> Result<(), Box<dyn Error>> {
        if size.width == 0 || size.height == 0 {
            return Ok(());
        }

        self.config.width = size.width;
        self.config.height = size.height;
        self.surface.configure(&self.device, &self.config);
        self.model.transform = self.renderer.create_transform(
            &self.device,
            &fit_model_matrix(self.model.model_bounds, size),
        );
        self.button_buffers = create_button_quad_buffers(&self.device, size, SWITCH_BUTTON_Y)?;
        self.button_uis =
            create_button_uis(&self.renderer, &self.device, &self.queue, &self.font, size)?;
        self.slider_track_buffers = create_slider_track_buffers(&self.device, size)?;
        self.fps_label = create_fps_label_quad(
            &self.renderer,
            &self.device,
            &self.queue,
            &self.font,
            self.fps_meter.label(),
            size,
        )?;
        self.refresh_parameter_controls()?;
        Ok(())
    }

    fn button_action_at_cursor(&self) -> Option<ButtonAction> {
        let position = self.cursor_position?;
        BUTTON_SPECS
            .iter()
            .find(|spec| button_rect(spec.top).contains(position.x, position.y))
            .map(|spec| spec.action)
    }

    fn slider_hit(&self) -> bool {
        self.cursor_position
            .map(|position| slider_rect(PARAMETER_SLIDER_Y).contains(position.x, position.y))
            .unwrap_or(false)
    }

    fn handle_left_press(&mut self) -> Result<(), Box<dyn Error>> {
        if let Some(action) = self.button_action_at_cursor() {
            match action {
                ButtonAction::SwitchModel => self.switch_to_next_model(),
                ButtonAction::PlayMotion => self.play_random_motion(),
                ButtonAction::PlayExpression => self.play_random_expression(),
                ButtonAction::PreviousParameter => self.select_previous_parameter(),
                ButtonAction::NextParameter => self.select_next_parameter(),
                ButtonAction::ResetParameter => self.reset_selected_parameter(),
            }
        } else if self.slider_hit() {
            self.dragging_parameter_slider = true;
            if let Some(position) = self.cursor_position {
                self.update_parameter_slider(position.x)?;
            }
            Ok(())
        } else {
            Ok(())
        }
    }

    fn play_random_motion(&mut self) -> Result<(), Box<dyn Error>> {
        if self.model.motions.is_empty() {
            return Ok(());
        }
        let pick = (self.next_rng() % self.model.motions.len() as u64) as usize;
        let motion = load_motion(&self.model.motions[pick])?;
        self.model.player = Some(MotionPlayer::new(motion));
        self.model.dirty = true;
        self.reset_fps_label()?;
        self.last_frame = Instant::now();
        self.window.request_redraw();
        Ok(())
    }

    fn play_random_expression(&mut self) -> Result<(), Box<dyn Error>> {
        if self.model.expressions.is_empty() {
            return Ok(());
        }
        let pick = (self.next_rng() % self.model.expressions.len() as u64) as usize;
        let expression = load_expression(&self.model.expressions[pick])?;
        self.model.expression_manager.play(expression);
        self.model.dirty = true;
        self.reset_fps_label()?;
        self.last_frame = Instant::now();
        self.window.request_redraw();
        Ok(())
    }

    fn select_previous_parameter(&mut self) -> Result<(), Box<dyn Error>> {
        if let Some(current) = self.selected_parameter_index {
            self.selected_parameter_index =
                previous_parameter_index(current, self.model.runtime.parameter_ids().len());
            self.refresh_parameter_controls()?;
            self.window.request_redraw();
        }
        Ok(())
    }

    fn select_next_parameter(&mut self) -> Result<(), Box<dyn Error>> {
        if let Some(current) = self.selected_parameter_index {
            self.selected_parameter_index =
                next_parameter_index(current, self.model.runtime.parameter_ids().len());
            self.refresh_parameter_controls()?;
            self.window.request_redraw();
        }
        Ok(())
    }

    fn reset_selected_parameter(&mut self) -> Result<(), Box<dyn Error>> {
        let Some(index) = self.selected_parameter_index else {
            return Ok(());
        };
        self.model.runtime.clear_parameter_override_by_index(index);
        if let Some(default) = self.model.runtime.parameter_default_by_index(index) {
            self.model.runtime.set_parameter_by_index(index, default);
        }
        self.model.dirty = true;
        self.refresh_parameter_controls()?;
        self.window.request_redraw();
        Ok(())
    }

    fn update_parameter_slider(&mut self, x: f64) -> Result<(), Box<dyn Error>> {
        let Some(index) = self.selected_parameter_index else {
            return Ok(());
        };
        let value = slider_rect(PARAMETER_SLIDER_Y).normalized_value(x);
        self.model
            .runtime
            .set_parameter_override_normalized_by_index(index, value);
        self.model.dirty = true;
        self.refresh_parameter_controls()?;
        self.window.request_redraw();
        Ok(())
    }

    fn refresh_parameter_controls(&mut self) -> Result<(), Box<dyn Error>> {
        let size = self.window.inner_size();
        self.parameter_label = create_parameter_label_quad(
            &self.renderer,
            &self.device,
            &self.queue,
            &self.font,
            &self.model.runtime,
            self.selected_parameter_index,
            size,
        )?;
        self.slider_fill_buffers = create_slider_fill_buffers(
            &self.device,
            size,
            selected_parameter_normalized(&self.model.runtime, self.selected_parameter_index),
        )?;
        Ok(())
    }

    fn next_rng(&mut self) -> u64 {
        self.rng ^= self.rng << 13;
        self.rng ^= self.rng >> 7;
        self.rng ^= self.rng << 17;
        self.rng
    }

    fn needs_continuous_redraw(&self) -> bool {
        self.model.player.is_some() || self.model.expression_manager.active_expression_count() > 0
    }

    fn reset_fps_label(&mut self) -> Result<(), Box<dyn Error>> {
        self.fps_meter.reset();
        self.fps_label = create_fps_label_quad(
            &self.renderer,
            &self.device,
            &self.queue,
            &self.font,
            self.fps_meter.label(),
            self.window.inner_size(),
        )?;
        Ok(())
    }

    fn advance_motion(&mut self) -> Result<(), Box<dyn Error>> {
        let now = Instant::now();
        let delta = now.duration_since(self.last_frame).as_secs_f32();
        self.last_frame = now;

        advance_model_frame(
            &self.renderer,
            &self.device,
            &self.queue,
            &mut self.model,
            delta,
        )?;
        Ok(())
    }

    fn switch_to_next_model(&mut self) -> Result<(), Box<dyn Error>> {
        let Some(next_index) = next_model_index(self.model_index, MODEL_SPECS.len()) else {
            return Ok(());
        };
        let spec = MODEL_SPECS[next_index];
        let model = load_rendered_model(
            &self.renderer,
            &self.device,
            &self.queue,
            spec,
            self.window.inner_size(),
        )?;

        self.model_index = next_index;
        self.model = model;
        self.fps_meter.reset();
        self.fps_label = create_fps_label_quad(
            &self.renderer,
            &self.device,
            &self.queue,
            &self.font,
            self.fps_meter.label(),
            self.window.inner_size(),
        )?;
        let now = Instant::now();
        self.last_frame = now;
        self.selected_parameter_index = initial_parameter_selection(&self.model.runtime);
        self.refresh_parameter_controls()?;
        self.window.set_title(&window_title(spec));
        self.window.request_redraw();
        Ok(())
    }

    fn render(&mut self) -> Result<(), Box<dyn Error>> {
        self.advance_motion()?;

        let frame = match self.surface.get_current_texture() {
            wgpu::CurrentSurfaceTexture::Success(frame)
            | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
                return Ok(());
            }
            wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
                self.resize(self.window.inner_size())?;
                return Ok(());
            }
            wgpu::CurrentSurfaceTexture::Validation => {
                return Err(Box::new(ExampleError("failed to acquire surface texture")));
            }
        };

        let view = frame
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());
        let mut encoder = self
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("show_model.encoder"),
            });

        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("show_model.mask_pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: self.model.mask_target.view(),
                    depth_slice: None,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });

            self.renderer.draw_masks_with_textures(
                &mut pass,
                &self.model.mesh_buffers,
                &self.model.clipping_resources,
                &self.model.textures,
            )?;
        }

        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("show_model.main_pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    depth_slice: None,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color {
                            r: 0.08,
                            g: 0.09,
                            b: 0.10,
                            a: 1.0,
                        }),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });

            self.renderer.draw_with_textures_clipping_and_transform(
                &mut pass,
                &self.model.mesh_buffers,
                &self.model.textures,
                &self.model.clipping_resources,
                &self.model.mask_target,
                &self.model.transform,
            )?;
            for button_ui in &self.button_uis {
                self.renderer.draw_with_textures_and_transform(
                    &mut pass,
                    &self.button_buffers,
                    std::slice::from_ref(&self.button_texture),
                    &button_ui.transform,
                )?;
            }
            self.renderer.draw_with_textures_and_transform(
                &mut pass,
                &self.slider_track_buffers,
                std::slice::from_ref(&self.slider_track_texture),
                &self.ui_transform,
            )?;
            self.renderer.draw_with_textures_and_transform(
                &mut pass,
                &self.slider_fill_buffers,
                std::slice::from_ref(&self.slider_fill_texture),
                &self.ui_transform,
            )?;
            for button_ui in &self.button_uis {
                self.renderer.draw_with_textures_and_transform(
                    &mut pass,
                    &button_ui.label.buffers,
                    std::slice::from_ref(&button_ui.label.texture),
                    &self.ui_transform,
                )?;
            }
            self.renderer.draw_with_textures_and_transform(
                &mut pass,
                &self.parameter_label.buffers,
                std::slice::from_ref(&self.parameter_label.texture),
                &self.ui_transform,
            )?;
            self.renderer.draw_with_textures_and_transform(
                &mut pass,
                &self.fps_label.buffers,
                std::slice::from_ref(&self.fps_label.texture),
                &self.ui_transform,
            )?;
        }

        self.window.pre_present_notify();
        self.queue.submit([encoder.finish()]);
        self.queue.present(frame);
        if let Some(label) = self.fps_meter.record_present(Instant::now()) {
            self.fps_label = create_fps_label_quad(
                &self.renderer,
                &self.device,
                &self.queue,
                &self.font,
                &label,
                self.window.inner_size(),
            )?;
            self.window.request_redraw();
        }
        Ok(())
    }
}

fn window_title(model: ModelSpec) -> String {
    format!("Live2D - {}", model.name)
}

fn next_model_index(current: usize, count: usize) -> Option<usize> {
    if count == 0 {
        None
    } else {
        Some((current + 1) % count)
    }
}

fn preferred_present_mode(supported_modes: &[wgpu::PresentMode]) -> wgpu::PresentMode {
    [wgpu::PresentMode::Immediate, wgpu::PresentMode::Mailbox]
        .into_iter()
        .find(|mode| supported_modes.contains(mode))
        .unwrap_or(wgpu::PresentMode::AutoNoVsync)
}

fn initial_parameter_selection(runtime: &ModelRuntime) -> Option<usize> {
    const PREFERRED_PARAMETERS: &[&str] = &[
        "ParamEyeLOpen",
        "ParamEyeROpen",
        "ParamEyeOpen",
        "ParamMouthOpenY",
    ];
    PREFERRED_PARAMETERS
        .iter()
        .find_map(|id| runtime.parameter_index(id))
        .or_else(|| (!runtime.parameter_ids().is_empty()).then_some(0))
}

fn previous_parameter_index(current: usize, count: usize) -> Option<usize> {
    if count == 0 {
        None
    } else {
        Some((current + count - 1) % count)
    }
}

fn next_parameter_index(current: usize, count: usize) -> Option<usize> {
    if count == 0 {
        None
    } else {
        Some((current + 1) % count)
    }
}

fn selected_parameter_normalized(runtime: &ModelRuntime, index: Option<usize>) -> f32 {
    let Some(index) = index else {
        return 0.0;
    };
    runtime
        .parameter_override_normalized_value_by_index(index)
        .or_else(|| runtime.parameter_normalized_value_by_index(index))
        .unwrap_or(0.0)
}

fn parameter_label_text(runtime: &ModelRuntime, index: Option<usize>) -> String {
    let Some(index) = index else {
        return "No parameters".to_owned();
    };
    let Some(info) = runtime.parameter_info_by_index(index) else {
        return "No parameters".to_owned();
    };
    let value = runtime
        .parameter_override_value_by_index(index)
        .unwrap_or_else(|| info.value());
    format!(
        "{} {:.2} [{:.2}..{:.2}]",
        info.id(),
        value,
        info.minimum(),
        info.maximum()
    )
}

fn load_rendered_model(
    renderer: &WgpuLive2dRenderer,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    spec: ModelSpec,
    surface_size: PhysicalSize<u32>,
) -> Result<LoadedModel, Box<dyn Error>> {
    let loaded = load_model_runtime(spec.path)?;
    let runtime = loaded.runtime().clone();
    let model_bounds = ModelBounds::from_drawables(runtime.meshes())
        .ok_or(ExampleError("model has no drawable bounds"))?;
    let textures = create_textures(renderer, device, queue, loaded.textures())?;
    let motions = motion_paths(&runtime, loaded.model_dir());
    let expressions = expression_paths(&runtime, loaded.model_dir());

    let mut model = LoadedModel {
        runtime,
        motions,
        expressions,
        player: None,
        expression_manager: ExpressionManager::new(),
        dirty: false,
        mesh_buffers: WgpuMeshBuffers::from_drawables(device, &[])
            .ok_or(ExampleError("failed to create mesh buffers"))?,
        textures,
        clipping_resources: renderer.create_clipping_resources(
            device,
            &WgpuClippingPlan::from_mesh_buffers(
                &WgpuMeshBuffers::from_drawables(device, &[])
                    .ok_or(ExampleError("failed to create mesh buffers"))?,
            ),
        )?,
        mask_target: renderer.create_mask_render_target(device, MASK_TEXTURE_SIZE)?,
        transform: renderer.create_transform(device, &fit_model_matrix(model_bounds, surface_size)),
        model_bounds,
    };
    rebuild_model_gpu(renderer, device, &mut model)?;
    Ok(model)
}

fn rebuild_model_gpu(
    renderer: &WgpuLive2dRenderer,
    device: &wgpu::Device,
    model: &mut LoadedModel,
) -> Result<(), Box<dyn Error>> {
    let mesh_buffers = WgpuMeshBuffers::from_drawables(device, model.runtime.meshes())
        .ok_or(ExampleError("failed to create mesh buffers"))?;
    model.mesh_buffers = mesh_buffers;
    rebuild_model_clipping(renderer, device, model)?;
    Ok(())
}

fn update_model_gpu(
    renderer: &WgpuLive2dRenderer,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    model: &mut LoadedModel,
) -> Result<(), Box<dyn Error>> {
    let update = match model
        .mesh_buffers
        .update_drawables(queue, model.runtime.meshes())
    {
        Ok(update) => update,
        Err(_) => return rebuild_model_gpu(renderer, device, model),
    };

    if update.bounds_changed() || update.visibility_changed() {
        update_model_clipping(renderer, device, queue, model)?;
    }
    Ok(())
}

fn advance_model_frame(
    renderer: &WgpuLive2dRenderer,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    model: &mut LoadedModel,
    delta: f32,
) -> Result<(), Box<dyn Error>> {
    let animating =
        model.player.is_some() || model.expression_manager.active_expression_count() > 0;
    if !model.dirty && !animating {
        return Ok(());
    }

    model.runtime.reset_parameters();
    model.runtime.reset_part_opacities();
    if let Some(player) = model.player.as_mut() {
        player.tick(delta);
        player.apply(&mut model.runtime);
        if player.is_finished() {
            model.player = None;
        }
    }
    model.expression_manager.tick(delta);
    model.expression_manager.apply(&mut model.runtime);
    model.runtime.apply_parameter_overrides();
    model.runtime.apply_pose(delta);
    if model.runtime.update_meshes().is_none() {
        return Err(Box::new(ExampleError("failed to rebuild model meshes")));
    }
    update_model_gpu(renderer, device, queue, model)?;
    model.dirty = false;
    Ok(())
}

fn rebuild_model_clipping(
    renderer: &WgpuLive2dRenderer,
    device: &wgpu::Device,
    model: &mut LoadedModel,
) -> Result<(), Box<dyn Error>> {
    let mesh_buffers = &model.mesh_buffers;
    let mut clipping_plan = WgpuClippingPlan::from_mesh_buffers(mesh_buffers);
    clipping_plan.prepare_single_texture_masks(mesh_buffers)?;
    model.clipping_resources = renderer.create_clipping_resources(device, &clipping_plan)?;
    Ok(())
}

fn update_model_clipping(
    renderer: &WgpuLive2dRenderer,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    model: &mut LoadedModel,
) -> Result<(), Box<dyn Error>> {
    let mesh_buffers = &model.mesh_buffers;
    let mut clipping_plan = WgpuClippingPlan::from_mesh_buffers(mesh_buffers);
    clipping_plan.prepare_single_texture_masks(mesh_buffers)?;
    if !renderer.update_clipping_resources(queue, &mut model.clipping_resources, &clipping_plan)? {
        model.clipping_resources = renderer.create_clipping_resources(device, &clipping_plan)?;
    }
    Ok(())
}

fn motion_paths(runtime: &ModelRuntime, model_dir: Option<&std::path::Path>) -> Vec<PathBuf> {
    let Some(model_dir) = model_dir else {
        return Vec::new();
    };
    runtime
        .model()
        .motions()
        .values()
        .flatten()
        .map(|reference| model_dir.join(reference.file()))
        .collect()
}

fn expression_paths(runtime: &ModelRuntime, model_dir: Option<&std::path::Path>) -> Vec<PathBuf> {
    let Some(model_dir) = model_dir else {
        return Vec::new();
    };
    runtime
        .model()
        .expressions()
        .iter()
        .map(|reference| model_dir.join(reference.file()))
        .collect()
}

fn load_font() -> Result<FontArc, Box<dyn Error>> {
    const CANDIDATES: &[&str] = &[
        "C:/Windows/Fonts/segoeui.ttf",
        "C:/Windows/Fonts/arial.ttf",
        "/System/Library/Fonts/Supplemental/Arial.ttf",
        "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
        "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
    ];
    for path in CANDIDATES {
        if let Ok(bytes) = std::fs::read(path)
            && let Ok(font) = FontArc::try_from_vec(bytes)
        {
            return Ok(font);
        }
    }
    Err(Box::new(ExampleError("no usable system font found")))
}

fn create_label_quad(
    renderer: &WgpuLive2dRenderer,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    font: &FontArc,
    text: &str,
    surface_size: PhysicalSize<u32>,
    top: f64,
) -> Result<LabelQuad, Box<dyn Error>> {
    let (width, height, rgba) = rasterize_text(font, text);
    let texture = renderer.create_rgba8_texture(device, queue, width, height, &rgba)?;

    let pad_x = BUTTON_X + 14.0;
    let pad_y = top + (BUTTON_HEIGHT - f64::from(height)) * 0.5;
    let mesh = textured_quad_mesh(
        surface_size,
        pad_x,
        pad_y,
        f64::from(width),
        f64::from(height),
    )
    .ok_or(ExampleError("invalid label size"))?;
    let buffers = WgpuMeshBuffers::from_drawables(device, &[mesh])
        .ok_or(ExampleError("failed to create label buffers"))?;

    Ok(LabelQuad { buffers, texture })
}

fn create_button_uis(
    renderer: &WgpuLive2dRenderer,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    font: &FontArc,
    surface_size: PhysicalSize<u32>,
) -> Result<Vec<ButtonUi>, Box<dyn Error>> {
    BUTTON_SPECS
        .iter()
        .map(|spec| {
            let transform =
                renderer.create_transform(device, &button_offset_matrix(surface_size, spec.top));
            let label = create_label_quad(
                renderer,
                device,
                queue,
                font,
                spec.label,
                surface_size,
                spec.top,
            )?;
            Ok(ButtonUi { transform, label })
        })
        .collect()
}

fn create_parameter_label_quad(
    renderer: &WgpuLive2dRenderer,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    font: &FontArc,
    runtime: &ModelRuntime,
    index: Option<usize>,
    surface_size: PhysicalSize<u32>,
) -> Result<LabelQuad, Box<dyn Error>> {
    create_text_label_quad(
        renderer,
        device,
        queue,
        font,
        &parameter_label_text(runtime, index),
        surface_size,
        [BUTTON_X, PARAMETER_LABEL_Y],
    )
}

fn create_fps_label_quad(
    renderer: &WgpuLive2dRenderer,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    font: &FontArc,
    text: &str,
    surface_size: PhysicalSize<u32>,
) -> Result<LabelQuad, Box<dyn Error>> {
    let (width, height, rgba) = rasterize_text(font, text);
    let texture = renderer.create_rgba8_texture(device, queue, width, height, &rgba)?;
    let x = (f64::from(surface_size.width) - f64::from(width) - 16.0).max(16.0);
    let mesh = textured_quad_mesh(surface_size, x, 16.0, f64::from(width), f64::from(height))
        .ok_or(ExampleError("invalid fps label size"))?;
    let buffers = WgpuMeshBuffers::from_drawables(device, &[mesh])
        .ok_or(ExampleError("failed to create fps label buffers"))?;

    Ok(LabelQuad { buffers, texture })
}

fn create_text_label_quad(
    renderer: &WgpuLive2dRenderer,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    font: &FontArc,
    text: &str,
    surface_size: PhysicalSize<u32>,
    position: [f64; 2],
) -> Result<LabelQuad, Box<dyn Error>> {
    let (width, height, rgba) = rasterize_text(font, text);
    let texture = renderer.create_rgba8_texture(device, queue, width, height, &rgba)?;
    let mesh = textured_quad_mesh(
        surface_size,
        position[0],
        position[1],
        f64::from(width),
        f64::from(height),
    )
    .ok_or(ExampleError("invalid label size"))?;
    let buffers = WgpuMeshBuffers::from_drawables(device, &[mesh])
        .ok_or(ExampleError("failed to create label buffers"))?;

    Ok(LabelQuad { buffers, texture })
}

fn rasterize_text(font: &FontArc, text: &str) -> (u32, u32, Vec<u8>) {
    let scale = TEXT_HEIGHT_PX;
    let ascent = font.as_scaled(scale).ascent();
    let mut pen_x = 2.0f32;
    let baseline = ascent + 2.0;
    let mut placed: Vec<Glyph> = Vec::new();

    for character in text.chars() {
        let glyph = font
            .glyph_id(character)
            .with_scale_and_position(scale, point(pen_x, baseline));
        pen_x += font.as_scaled(scale).h_advance(glyph.id);
        placed.push(glyph);
    }

    let width = (pen_x.ceil() as u32 + 2).max(1);
    let height = (TEXT_HEIGHT_PX.ceil() as u32 + 6).max(1);
    let mut rgba = vec![0u8; (width * height * 4) as usize];

    for glyph in placed {
        if let Some(outlined) = font.outline_glyph(glyph) {
            let bounds = outlined.px_bounds();
            outlined.draw(|x, y, coverage| {
                let px = x as i32 + bounds.min.x as i32;
                let py = y as i32 + bounds.min.y as i32;
                if px < 0 || py < 0 || px as u32 >= width || py as u32 >= height {
                    return;
                }
                let index = ((py as u32 * width + px as u32) * 4) as usize;
                let alpha = (coverage * 255.0) as u8;
                if alpha > rgba[index + 3] {
                    rgba[index] = TEXT_RGBA[0];
                    rgba[index + 1] = TEXT_RGBA[1];
                    rgba[index + 2] = TEXT_RGBA[2];
                    rgba[index + 3] = alpha;
                }
            });
        }
    }

    (width, height, rgba)
}

fn create_textures(
    renderer: &WgpuLive2dRenderer,
    device: &wgpu::Device,
    queue: &wgpu::Queue,
    textures: &[DecodedTexture],
) -> Result<Vec<WgpuTexture>, Box<dyn Error>> {
    textures
        .iter()
        .map(|texture| {
            renderer
                .create_rgba8_texture(
                    device,
                    queue,
                    texture.width(),
                    texture.height(),
                    texture.rgba(),
                )
                .map_err(|error| Box::new(error) as Box<dyn Error>)
        })
        .collect()
}

#[derive(Debug, Copy, Clone, PartialEq)]
struct ButtonRect {
    x: f64,
    y: f64,
    width: f64,
    height: f64,
}

impl ButtonRect {
    fn contains(self, x: f64, y: f64) -> bool {
        x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height
    }
}

#[derive(Debug, Copy, Clone, PartialEq)]
struct SliderRect {
    x: f64,
    y: f64,
    width: f64,
    height: f64,
}

impl SliderRect {
    fn contains(self, x: f64, y: f64) -> bool {
        x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height
    }

    fn normalized_value(self, x: f64) -> f32 {
        ((x - self.x) / self.width).clamp(0.0, 1.0) as f32
    }
}

fn button_rect(top: f64) -> ButtonRect {
    ButtonRect {
        x: BUTTON_X,
        y: top,
        width: BUTTON_WIDTH,
        height: BUTTON_HEIGHT,
    }
}

fn slider_rect(top: f64) -> SliderRect {
    SliderRect {
        x: SLIDER_X,
        y: top,
        width: SLIDER_WIDTH,
        height: SLIDER_HEIGHT,
    }
}

fn create_button_quad_buffers(
    device: &wgpu::Device,
    surface_size: PhysicalSize<u32>,
    top: f64,
) -> Result<WgpuMeshBuffers, Box<dyn Error>> {
    create_rect_quad_buffers(
        device,
        surface_size,
        BUTTON_X,
        top,
        BUTTON_WIDTH,
        BUTTON_HEIGHT,
    )
}

fn create_slider_track_buffers(
    device: &wgpu::Device,
    surface_size: PhysicalSize<u32>,
) -> Result<WgpuMeshBuffers, Box<dyn Error>> {
    let slider = slider_rect(PARAMETER_SLIDER_Y);
    create_rect_quad_buffers(
        device,
        surface_size,
        slider.x,
        slider.y,
        slider.width,
        slider.height,
    )
}

fn create_slider_fill_buffers(
    device: &wgpu::Device,
    surface_size: PhysicalSize<u32>,
    normalized: f32,
) -> Result<WgpuMeshBuffers, Box<dyn Error>> {
    let slider = slider_rect(PARAMETER_SLIDER_Y);
    let width = (slider.width * f64::from(normalized.clamp(0.0, 1.0))).max(1.0);
    create_rect_quad_buffers(
        device,
        surface_size,
        slider.x,
        slider.y,
        width,
        slider.height,
    )
}

fn create_rect_quad_buffers(
    device: &wgpu::Device,
    surface_size: PhysicalSize<u32>,
    x: f64,
    y: f64,
    width: f64,
    height: f64,
) -> Result<WgpuMeshBuffers, Box<dyn Error>> {
    let mesh = textured_quad_mesh(surface_size, x, y, width, height)
        .ok_or(ExampleError("invalid rectangle size"))?;
    WgpuMeshBuffers::from_drawables(device, &[mesh])
        .ok_or_else(|| Box::new(ExampleError("failed to create rectangle buffers")).into())
}

fn button_offset_matrix(surface_size: PhysicalSize<u32>, top: f64) -> Matrix44 {
    let delta = (top - SWITCH_BUTTON_Y) / f64::from(surface_size.height.max(1)) * 2.0;
    let mut matrix = Matrix44::identity();
    matrix.translate(0.0, -delta as f32);
    matrix
}

fn textured_quad_mesh(
    surface_size: PhysicalSize<u32>,
    x: f64,
    y: f64,
    width: f64,
    height: f64,
) -> Option<Moc3DrawableMesh> {
    if surface_size.width == 0 || surface_size.height == 0 {
        return None;
    }

    let right = (x + width).min(f64::from(surface_size.width));
    let bottom = (y + height).min(f64::from(surface_size.height));
    if right <= x || bottom <= y {
        return None;
    }

    let left = pixel_x_to_ndc(x, surface_size.width);
    let right = pixel_x_to_ndc(right, surface_size.width);
    let top = pixel_y_to_ndc(y, surface_size.height);
    let bottom = pixel_y_to_ndc(bottom, surface_size.height);

    Some(Moc3DrawableMesh::from_parts(
        0,
        0,
        1.0,
        0.0,
        vec![
            Moc3DrawableVertex::new([left, top], [0.0, 0.0]),
            Moc3DrawableVertex::new([right, top], [1.0, 0.0]),
            Moc3DrawableVertex::new([right, bottom], [1.0, 1.0]),
            Moc3DrawableVertex::new([left, bottom], [0.0, 1.0]),
        ],
        vec![0, 1, 2, 0, 2, 3],
        Vec::new(),
    ))
}

fn pixel_x_to_ndc(x: f64, width: u32) -> f32 {
    ((x / f64::from(width)) * 2.0 - 1.0) as f32
}

fn pixel_y_to_ndc(y: f64, height: u32) -> f32 {
    (1.0 - (y / f64::from(height)) * 2.0) as f32
}

#[derive(Debug, Copy, Clone, PartialEq)]
struct ModelBounds {
    min_x: f32,
    min_y: f32,
    max_x: f32,
    max_y: f32,
}

impl ModelBounds {
    fn from_drawables(drawables: &[Moc3DrawableMesh]) -> Option<Self> {
        let mut bounds: Option<Self> = None;

        for vertex in drawables.iter().flat_map(Moc3DrawableMesh::vertices) {
            let [x, y] = vertex.position();
            bounds = Some(match bounds {
                Some(bounds) => Self {
                    min_x: bounds.min_x.min(x),
                    min_y: bounds.min_y.min(y),
                    max_x: bounds.max_x.max(x),
                    max_y: bounds.max_y.max(y),
                },
                None => Self {
                    min_x: x,
                    min_y: y,
                    max_x: x,
                    max_y: y,
                },
            });
        }

        bounds.filter(|bounds| bounds.width() > 0.0 && bounds.height() > 0.0)
    }

    fn width(self) -> f32 {
        self.max_x - self.min_x
    }

    fn height(self) -> f32 {
        self.max_y - self.min_y
    }

    fn center_x(self) -> f32 {
        (self.min_x + self.max_x) * 0.5
    }

    fn center_y(self) -> f32 {
        (self.min_y + self.max_y) * 0.5
    }
}

fn fit_model_matrix(bounds: ModelBounds, surface_size: PhysicalSize<u32>) -> Matrix44 {
    let aspect = surface_size.width as f32 / surface_size.height as f32;
    let fit_x = MODEL_VIEW_FILL / (bounds.width() * aspect);
    let fit_y = MODEL_VIEW_FILL / bounds.height();
    let scale_y = fit_x.min(fit_y);
    let scale_x = scale_y / aspect;

    let mut matrix = Matrix44::identity();
    matrix.scale(scale_x, scale_y);
    matrix.translate(-bounds.center_x() * scale_x, -bounds.center_y() * scale_y);
    matrix
}

#[derive(Debug)]
struct ExampleError(&'static str);

impl fmt::Display for ExampleError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.0)
    }
}

impl Error for ExampleError {}

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

    #[test]
    fn fit_model_matrix_centers_and_fits_square_surface() {
        let bounds = ModelBounds {
            min_x: -2.0,
            min_y: -1.0,
            max_x: 2.0,
            max_y: 3.0,
        };

        let matrix = fit_model_matrix(bounds, PhysicalSize::new(100, 100));

        assert_close(matrix.transform_x(bounds.center_x()), 0.0);
        assert_close(matrix.transform_y(bounds.center_y()), 0.0);
        assert!(matrix.transform_x(bounds.min_x) >= -1.0);
        assert!(matrix.transform_x(bounds.max_x) <= 1.0);
        assert!(matrix.transform_y(bounds.min_y) >= -1.0);
        assert!(matrix.transform_y(bounds.max_y) <= 1.0);
    }

    #[test]
    fn fit_model_matrix_preserves_pixels_on_wide_surface() {
        let bounds = ModelBounds {
            min_x: 0.0,
            min_y: 0.0,
            max_x: 1.0,
            max_y: 1.0,
        };

        let matrix = fit_model_matrix(bounds, PhysicalSize::new(200, 100));

        assert_close(matrix.scale_x() * 200.0, matrix.scale_y() * 100.0);
    }

    #[test]
    fn model_specs_point_at_bundled_models() {
        let names = MODEL_SPECS
            .iter()
            .map(|model| model.name)
            .collect::<Vec<_>>();

        assert_eq!(
            names,
            vec![
                "Wanko", "Hiyori", "Haru", "Ren", "Mark", "Rice", "Natori", "Mao"
            ]
        );
        for model in MODEL_SPECS {
            assert!(
                std::path::Path::new(model.path).exists(),
                "missing model asset: {}",
                model.path
            );
        }
    }

    #[test]
    fn model_specs_load_drawable_meshes() {
        for spec in MODEL_SPECS {
            let model = load_model_runtime(spec.path).expect(spec.path);

            assert!(
                !model.runtime().meshes().is_empty(),
                "model has no drawable meshes: {}",
                spec.path
            );
            assert!(
                !model.textures().is_empty(),
                "model has no textures: {}",
                spec.path
            );
        }
    }

    #[test]
    fn next_model_index_advances_and_wraps() {
        assert_eq!(next_model_index(0, 3), Some(1));
        assert_eq!(next_model_index(2, 3), Some(0));
        assert_eq!(next_model_index(0, 0), None);
    }

    #[test]
    fn button_hit_test_uses_top_left_rect() {
        let rect = button_rect(SWITCH_BUTTON_Y);

        assert!(rect.contains(20.0, 20.0));
        assert!(rect.contains(rect.x + rect.width, rect.y + rect.height));
        assert!(!rect.contains(rect.x + rect.width + 1.0, rect.y));
        assert!(!rect.contains(rect.x, rect.y + rect.height + 1.0));
    }

    #[test]
    fn motion_button_sits_below_switch_button() {
        let switch = button_rect(SWITCH_BUTTON_Y);
        let motion = button_rect(MOTION_BUTTON_Y);

        assert!(motion.y > switch.y + switch.height - 0.0001);
        assert_eq!(switch.x, motion.x);
    }

    #[test]
    fn expression_button_sits_below_motion_button() {
        let motion = button_rect(MOTION_BUTTON_Y);
        let expression = button_rect(EXPRESSION_BUTTON_Y);

        assert!(expression.y > motion.y + motion.height - 0.0001);
        assert_eq!(motion.x, expression.x);
    }

    #[test]
    fn expression_paths_follow_model3_references() {
        let loaded = load_model_runtime("assets/models/Mao/Mao.model3.json").unwrap();
        let paths = expression_paths(loaded.runtime(), loaded.model_dir());

        assert_eq!(paths.len(), 8);
        assert_eq!(
            paths[0],
            std::path::PathBuf::from("assets/models/Mao/expressions/exp_01.exp3.json")
        );
        for path in paths {
            assert!(
                path.exists(),
                "missing expression asset: {}",
                path.display()
            );
        }
    }

    #[test]
    fn parameter_buttons_stack_below_expression_button() {
        let expression = button_rect(EXPRESSION_BUTTON_Y);
        let previous = button_rect(PREV_PARAMETER_BUTTON_Y);
        let next = button_rect(NEXT_PARAMETER_BUTTON_Y);
        let reset = button_rect(RESET_PARAMETER_BUTTON_Y);

        assert!(previous.y > expression.y + expression.height - 0.0001);
        assert!(next.y > previous.y + previous.height - 0.0001);
        assert!(reset.y > next.y + next.height - 0.0001);
        assert_eq!(expression.x, previous.x);
    }

    #[test]
    fn initial_parameter_selection_prefers_eye_open_parameter() {
        let loaded = load_model_runtime("assets/models/Haru/Haru.model3.json").unwrap();
        let index = initial_parameter_selection(loaded.runtime()).unwrap();

        assert_eq!(loaded.runtime().parameter_ids()[index], "ParamEyeLOpen");
    }

    #[test]
    fn parameter_index_navigation_wraps() {
        assert_eq!(next_parameter_index(0, 3), Some(1));
        assert_eq!(next_parameter_index(2, 3), Some(0));
        assert_eq!(previous_parameter_index(0, 3), Some(2));
        assert_eq!(previous_parameter_index(1, 3), Some(0));
        assert_eq!(next_parameter_index(0, 0), None);
        assert_eq!(previous_parameter_index(0, 0), None);
    }

    #[test]
    fn slider_position_maps_to_clamped_normalized_value() {
        let slider = slider_rect(PARAMETER_SLIDER_Y);

        assert_close(slider.normalized_value(slider.x - 20.0), 0.0);
        assert_close(
            slider.normalized_value(slider.x + slider.width * 0.25),
            0.25,
        );
        assert_close(slider.normalized_value(slider.x + slider.width + 20.0), 1.0);
    }

    fn assert_close(actual: f32, expected: f32) {
        assert!((actual - expected).abs() < 0.0001);
    }
}
