use std::collections::{HashMap, VecDeque};
use std::error::Error;

use bytemuck::{Pod, Zeroable};
use cosmic_text::{
    Attrs, Buffer, CacheKey, FontSystem, Metrics, Shaping, SwashCache, SwashContent, SwashImage,
};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use ruin_ui::{
    BoxShadowKind, ClipRegion, Color, DisplayItem, GlyphInstance, Point, PreparedImage,
    PreparedText, Rect, RoundedRect, SceneSnapshot, ShadowRect, UiSize,
};
use tracing::trace;
use wgpu::util::DeviceExt;

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct Vertex {
    position: [f32; 2],
    world_position: [f32; 2],
    rect: [f32; 4],
    fill_color: [f32; 4],
    border_color: [f32; 4],
    shape_params: [f32; 4],
    clip_rect: [f32; 4],
    rounded_clip_rect: [f32; 4],
    clip_params: [f32; 2],
    shadow_base_rect: [f32; 4],
    shadow_base_params: [f32; 2],
}

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct TextVertex {
    position: [f32; 2],
    world_position: [f32; 2],
    uv: [f32; 2],
    color: [f32; 4],
    clip_rect: [f32; 4],
    rounded_clip_rect: [f32; 4],
    clip_params: [f32; 2],
}

#[derive(Clone, Copy, Debug, Default)]
struct ActiveClip {
    rect_active: bool,
    rect: Option<Rect>,
    rounded: Option<(Rect, f32)>,
}

impl ActiveClip {
    fn resolved_rect(self) -> Option<Rect> {
        if !self.rect_active {
            return None;
        }
        Some(self.rect.unwrap_or_else(empty_clip_rect))
    }
}

#[derive(Clone, Copy, Debug)]
struct PixelRect {
    left: i32,
    top: i32,
    right: i32,
    bottom: i32,
}

impl PixelRect {
    fn width(self) -> u32 {
        (self.right - self.left).max(0) as u32
    }

    fn height(self) -> u32 {
        (self.bottom - self.top).max(0) as u32
    }
}

struct GlyphBitmap {
    rect: PixelRect,
    content: SwashContent,
    color: Color,
    data: Vec<u8>,
}

#[derive(Clone, Copy, Debug)]
struct PreparedGlyphBitmap {
    rect: PixelRect,
    cache_key: CacheKey,
    color: Color,
}

struct RasterizedText {
    origin_offset: Point,
    draw_size: UiSize,
    texture_size: UiSize,
    pixels: Vec<u8>,
}

struct CachedTextTexture {
    _texture: wgpu::Texture,
    bind_group: wgpu::BindGroup,
    origin_offset: Point,
    size: UiSize,
}

struct CachedImageTexture {
    _texture: wgpu::Texture,
    bind_group: wgpu::BindGroup,
}

#[derive(Default)]
struct AtlasTextPerfStats {
    /// Total glyphs in all text nodes in the scene.
    glyphs_total: u32,
    /// Glyphs skipped because their line is outside the clip rect.
    glyphs_clip_culled: u32,
}

#[allow(dead_code)]
struct GlyphAtlas {
    texture: wgpu::Texture,
    bind_group: wgpu::BindGroup,
    glyphs: HashMap<AtlasGlyphKey, AtlasGlyph>,
    cursor_x: u32,
    cursor_y: u32,
    row_height: u32,
}

#[derive(Clone, Copy, Debug)]
#[allow(dead_code)]
struct AtlasGlyph {
    atlas_rect: AtlasRect,
    placement_left: i32,
    placement_top: i32,
}

#[derive(Clone, Copy, Debug)]
#[allow(dead_code)]
struct AtlasRect {
    x: u32,
    y: u32,
    width: u32,
    height: u32,
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct AtlasGlyphKey {
    cache_key: CacheKey,
    color: (u8, u8, u8, u8),
}

struct UploadedText {
    bind_group: wgpu::BindGroup,
    vertex_buffer: wgpu::Buffer,
    vertex_count: u32,
}

struct UploadedImage {
    bind_group: wgpu::BindGroup,
    vertex_buffer: wgpu::Buffer,
    vertex_count: u32,
}

#[allow(dead_code)]
struct UploadedAtlasText {
    vertex_buffer: wgpu::Buffer,
    vertex_count: u32,
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct TextTextureKey {
    layout_key: u64,
    bounds: Option<(i32, i32)>,
    clip: Option<(i32, i32, i32, i32)>,
    font_size_bits: u32,
    line_height_bits: u32,
    color: (u8, u8, u8, u8),
}

const VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 11] = [
    wgpu::VertexAttribute {
        offset: 0,
        shader_location: 0,
        format: wgpu::VertexFormat::Float32x2,
    },
    wgpu::VertexAttribute {
        offset: 8,
        shader_location: 1,
        format: wgpu::VertexFormat::Float32x2,
    },
    wgpu::VertexAttribute {
        offset: 16,
        shader_location: 2,
        format: wgpu::VertexFormat::Float32x4,
    },
    wgpu::VertexAttribute {
        offset: 32,
        shader_location: 3,
        format: wgpu::VertexFormat::Float32x4,
    },
    wgpu::VertexAttribute {
        offset: 48,
        shader_location: 4,
        format: wgpu::VertexFormat::Float32x4,
    },
    wgpu::VertexAttribute {
        offset: 64,
        shader_location: 5,
        format: wgpu::VertexFormat::Float32x4,
    },
    wgpu::VertexAttribute {
        offset: 80,
        shader_location: 6,
        format: wgpu::VertexFormat::Float32x4,
    },
    wgpu::VertexAttribute {
        offset: 96,
        shader_location: 7,
        format: wgpu::VertexFormat::Float32x4,
    },
    wgpu::VertexAttribute {
        offset: 112,
        shader_location: 8,
        format: wgpu::VertexFormat::Float32x2,
    },
    wgpu::VertexAttribute {
        offset: 120,
        shader_location: 9,
        format: wgpu::VertexFormat::Float32x4,
    },
    wgpu::VertexAttribute {
        offset: 136,
        shader_location: 10,
        format: wgpu::VertexFormat::Float32x2,
    },
];
const TEXT_VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 7] = [
    wgpu::VertexAttribute {
        offset: 0,
        shader_location: 0,
        format: wgpu::VertexFormat::Float32x2,
    },
    wgpu::VertexAttribute {
        offset: 8,
        shader_location: 1,
        format: wgpu::VertexFormat::Float32x2,
    },
    wgpu::VertexAttribute {
        offset: 16,
        shader_location: 2,
        format: wgpu::VertexFormat::Float32x2,
    },
    wgpu::VertexAttribute {
        offset: 24,
        shader_location: 3,
        format: wgpu::VertexFormat::Float32x4,
    },
    wgpu::VertexAttribute {
        offset: 40,
        shader_location: 4,
        format: wgpu::VertexFormat::Float32x4,
    },
    wgpu::VertexAttribute {
        offset: 56,
        shader_location: 5,
        format: wgpu::VertexFormat::Float32x4,
    },
    wgpu::VertexAttribute {
        offset: 72,
        shader_location: 6,
        format: wgpu::VertexFormat::Float32x2,
    },
];

impl Vertex {
    const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
        array_stride: std::mem::size_of::<Vertex>() as u64,
        step_mode: wgpu::VertexStepMode::Vertex,
        attributes: &VERTEX_ATTRIBUTES,
    };

    fn layout() -> wgpu::VertexBufferLayout<'static> {
        Self::LAYOUT
    }
}

impl TextVertex {
    const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
        array_stride: std::mem::size_of::<TextVertex>() as u64,
        step_mode: wgpu::VertexStepMode::Vertex,
        attributes: &TEXT_VERTEX_ATTRIBUTES,
    };

    fn layout() -> wgpu::VertexBufferLayout<'static> {
        Self::LAYOUT
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RenderError {
    Lost,
    Outdated,
    Timeout,
    Occluded,
    Validation,
}

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct RendererFrameStats {
    pub scene_version: u64,
    pub scene_items: usize,
    pub quad_vertices: usize,
    pub image_batches: usize,
    pub atlas_text_vertices: u32,
    pub fallback_text_batches: usize,
    pub fallback_text_vertices: u32,
    pub atlas_glyphs_total: u32,
    pub atlas_glyphs_clip_culled: u32,
    pub text_prepare_ms: f64,
    pub render_ms: f64,
}

pub struct WgpuSceneRenderer {
    surface: wgpu::Surface<'static>,
    device: wgpu::Device,
    queue: wgpu::Queue,
    config: wgpu::SurfaceConfiguration,
    quad_pipeline: wgpu::RenderPipeline,
    text_pipeline: wgpu::RenderPipeline,
    text_bind_group_layout: wgpu::BindGroupLayout,
    text_sampler: wgpu::Sampler,
    font_system: FontSystem,
    swash_cache: SwashCache,
    text_cache: HashMap<TextTextureKey, CachedTextTexture>,
    text_cache_order: VecDeque<TextTextureKey>,
    image_cache: HashMap<u64, CachedImageTexture>,
    image_cache_order: VecDeque<u64>,
    #[allow(dead_code)]
    glyph_atlas: GlyphAtlas,
    scale_factor: f32,
    base_color: Color,
    last_frame_stats: RendererFrameStats,
}

const MAX_TEXT_CACHE_ENTRIES: usize = 64;
const MAX_IMAGE_CACHE_ENTRIES: usize = 64;
const GLYPH_ATLAS_SIZE: u32 = 2048;
#[allow(dead_code)]
const GLYPH_ATLAS_PADDING: u32 = 1;

impl WgpuSceneRenderer {
    pub fn new(
        target: impl HasDisplayHandle + HasWindowHandle + Send + Sync + 'static,
        width: u32,
        height: u32,
        base_color: Color,
    ) -> Result<Self, Box<dyn Error>> {
        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
        let surface = instance.create_surface(target)?;
        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
            power_preference: wgpu::PowerPreference::HighPerformance,
            force_fallback_adapter: false,
            compatible_surface: Some(&surface),
        }))?;
        let (device, queue) =
            pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default()))?;

        let caps = surface.get_capabilities(&adapter);
        let format = caps
            .formats
            .iter()
            .copied()
            .find(wgpu::TextureFormat::is_srgb)
            .unwrap_or(caps.formats[0]);
        let alpha_mode = if base_color.a < 255
            && caps
                .alpha_modes
                .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
        {
            wgpu::CompositeAlphaMode::PreMultiplied
        } else {
            caps.alpha_modes[0]
        };
        let config = wgpu::SurfaceConfiguration {
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            format,
            width: width.max(1),
            height: height.max(1),
            // RUIN prioritizes immediate resize/input feedback over frame pacing
            // here. Keep latency to one frame and let callers opt into a more
            // power-conservative policy once presentation settings are exposed.
            present_mode: wgpu::PresentMode::AutoNoVsync,
            desired_maximum_frame_latency: 1,
            alpha_mode,
            view_formats: vec![],
        };
        surface.configure(&device, &config);

        let quad_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("ruin-ui-renderer-wgpu-quad-shader"),
            source: wgpu::ShaderSource::Wgsl(
                r#"
struct VertexIn {
    @location(0) position: vec2<f32>,
    @location(1) world_position: vec2<f32>,
    @location(2) rect: vec4<f32>,
    @location(3) fill_color: vec4<f32>,
    @location(4) border_color: vec4<f32>,
    @location(5) shape_params: vec4<f32>,
    @location(6) clip_rect: vec4<f32>,
    @location(7) rounded_clip_rect: vec4<f32>,
    @location(8) clip_params: vec2<f32>,
    @location(9) shadow_base_rect: vec4<f32>,
    @location(10) shadow_base_params: vec2<f32>,
};

struct VertexOut {
    @builtin(position) position: vec4<f32>,
    @location(0) world_position: vec2<f32>,
    @location(1) rect: vec4<f32>,
    @location(2) fill_color: vec4<f32>,
    @location(3) border_color: vec4<f32>,
    @location(4) shape_params: vec4<f32>,
    @location(5) clip_rect: vec4<f32>,
    @location(6) rounded_clip_rect: vec4<f32>,
    @location(7) clip_params: vec2<f32>,
    @location(8) shadow_base_rect: vec4<f32>,
    @location(9) shadow_base_params: vec2<f32>,
};

fn sd_round_rect(point: vec2<f32>, rect: vec4<f32>, radius: f32) -> f32 {
    let center = vec2<f32>((rect.x + rect.z) * 0.5, (rect.y + rect.w) * 0.5);
    let half_size = vec2<f32>((rect.z - rect.x) * 0.5, (rect.w - rect.y) * 0.5);
    let effective_radius = max(radius, 0.0);
    let q = abs(point - center) - max(half_size - vec2<f32>(effective_radius, effective_radius), vec2<f32>(0.0, 0.0));
    return length(max(q, vec2<f32>(0.0, 0.0))) + min(max(q.x, q.y), 0.0) - effective_radius;
}

fn rounded_rect_alpha(point: vec2<f32>, rect: vec4<f32>, radius: f32) -> f32 {
    let distance = sd_round_rect(point, rect, radius);
    return 1.0 - smoothstep(-0.5, 0.5, distance);
}

fn blurred_round_rect_alpha(point: vec2<f32>, rect: vec4<f32>, radius: f32, blur: f32) -> f32 {
    let softness = max(blur, 0.001);
    let distance = sd_round_rect(point, rect, radius);
    return 1.0 - smoothstep(0.0, softness, distance);
}

fn apply_clip_alpha(point: vec2<f32>, clip_rect: vec4<f32>, rounded_clip_rect: vec4<f32>, clip_params: vec2<f32>) -> f32 {
    var alpha = 1.0;
    if clip_params.x > 0.5 {
        if point.x < clip_rect.x || point.y < clip_rect.y || point.x > clip_rect.z || point.y > clip_rect.w {
            return 0.0;
        }
    }
    if clip_params.y > 0.0 {
        alpha = alpha * rounded_rect_alpha(point, rounded_clip_rect, clip_params.y);
    }
    return alpha;
}

@vertex
fn vs_main(input: VertexIn) -> VertexOut {
    var out: VertexOut;
    out.position = vec4(input.position, 0.0, 1.0);
    out.world_position = input.world_position;
    out.rect = input.rect;
    out.fill_color = input.fill_color;
    out.border_color = input.border_color;
    out.shape_params = input.shape_params;
    out.clip_rect = input.clip_rect;
    out.rounded_clip_rect = input.rounded_clip_rect;
    out.clip_params = input.clip_params;
    out.shadow_base_rect = input.shadow_base_rect;
    out.shadow_base_params = input.shadow_base_params;
    return out;
}

@fragment
fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
    var color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
    if input.shape_params.z < 0.5 {
        let outer = rounded_rect_alpha(input.world_position, input.rect, input.shape_params.x);
        var fill_mask = outer;
        var border_mask = 0.0;
        if input.shape_params.y > 0.0 {
            let inner_rect = vec4<f32>(
                input.rect.x + input.shape_params.y,
                input.rect.y + input.shape_params.y,
                input.rect.z - input.shape_params.y,
                input.rect.w - input.shape_params.y,
            );
            let inner = rounded_rect_alpha(input.world_position, inner_rect, max(input.shape_params.x - input.shape_params.y, 0.0));
            fill_mask = inner;
            border_mask = max(outer - inner, 0.0);
        }
        color = input.fill_color * fill_mask + input.border_color * border_mask;
    } else if input.shape_params.z < 1.5 {
        let shadow = blurred_round_rect_alpha(
            input.world_position,
            input.rect,
            input.shape_params.x,
            input.shape_params.w,
        );
        let source = rounded_rect_alpha(
            input.world_position,
            input.shadow_base_rect,
            input.shadow_base_params.x,
        );
        color = input.fill_color * max(shadow * (1.0 - source), 0.0);
    } else {
        let inside = rounded_rect_alpha(
            input.world_position,
            input.shadow_base_rect,
            input.shadow_base_params.x,
        );
        let shifted = blurred_round_rect_alpha(
            input.world_position,
            input.rect,
            input.shape_params.x,
            input.shape_params.w,
        );
        color = input.fill_color * max(inside * (1.0 - shifted), 0.0);
    }
    color.a = color.a * apply_clip_alpha(
        input.world_position,
        input.clip_rect,
        input.rounded_clip_rect,
        input.clip_params,
    );
    if color.a <= 0.001 {
        discard;
    }
    return color;
}
"#
                .into(),
            ),
        });

        let text_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("ruin-ui-renderer-wgpu-text-shader"),
            source: wgpu::ShaderSource::Wgsl(
                r#"
struct VertexIn {
    @location(0) position: vec2<f32>,
    @location(1) world_position: vec2<f32>,
    @location(2) uv: vec2<f32>,
    @location(3) color: vec4<f32>,
    @location(4) clip_rect: vec4<f32>,
    @location(5) rounded_clip_rect: vec4<f32>,
    @location(6) clip_params: vec2<f32>,
};

struct VertexOut {
    @builtin(position) position: vec4<f32>,
    @location(0) world_position: vec2<f32>,
    @location(1) uv: vec2<f32>,
    @location(2) color: vec4<f32>,
    @location(3) clip_rect: vec4<f32>,
    @location(4) rounded_clip_rect: vec4<f32>,
    @location(5) clip_params: vec2<f32>,
};

@group(0) @binding(0) var text_texture: texture_2d<f32>;
@group(0) @binding(1) var text_sampler: sampler;

fn sd_round_rect(point: vec2<f32>, rect: vec4<f32>, radius: f32) -> f32 {
    let center = vec2<f32>((rect.x + rect.z) * 0.5, (rect.y + rect.w) * 0.5);
    let half_size = vec2<f32>((rect.z - rect.x) * 0.5, (rect.w - rect.y) * 0.5);
    let effective_radius = max(radius, 0.0);
    let q = abs(point - center) - max(half_size - vec2<f32>(effective_radius, effective_radius), vec2<f32>(0.0, 0.0));
    return length(max(q, vec2<f32>(0.0, 0.0))) + min(max(q.x, q.y), 0.0) - effective_radius;
}

fn rounded_rect_alpha(point: vec2<f32>, rect: vec4<f32>, radius: f32) -> f32 {
    let distance = sd_round_rect(point, rect, radius);
    return 1.0 - smoothstep(-0.5, 0.5, distance);
}

fn apply_clip_alpha(point: vec2<f32>, clip_rect: vec4<f32>, rounded_clip_rect: vec4<f32>, clip_params: vec2<f32>) -> f32 {
    var alpha = 1.0;
    if clip_params.x > 0.5 {
        if point.x < clip_rect.x || point.y < clip_rect.y || point.x > clip_rect.z || point.y > clip_rect.w {
            return 0.0;
        }
    }
    if clip_params.y > 0.0 {
        alpha = alpha * rounded_rect_alpha(point, rounded_clip_rect, clip_params.y);
    }
    return alpha;
}

@vertex
fn vs_main(input: VertexIn) -> VertexOut {
    var out: VertexOut;
    out.position = vec4(input.position, 0.0, 1.0);
    out.world_position = input.world_position;
    out.uv = input.uv;
    out.color = input.color;
    out.clip_rect = input.clip_rect;
    out.rounded_clip_rect = input.rounded_clip_rect;
    out.clip_params = input.clip_params;
    return out;
}

@fragment
fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
    var color = textureSample(text_texture, text_sampler, input.uv) * input.color;
    color.a = color.a * apply_clip_alpha(
        input.world_position,
        input.clip_rect,
        input.rounded_clip_rect,
        input.clip_params,
    );
    if color.a <= 0.001 {
        discard;
    }
    return color;
}
"#
                .into(),
            ),
        });

        let quad_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("ruin-ui-renderer-wgpu-quad-pipeline-layout"),
            bind_group_layouts: &[],
            immediate_size: 0,
        });
        let quad_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("ruin-ui-renderer-wgpu-quad-pipeline"),
            layout: Some(&quad_pipeline_layout),
            vertex: wgpu::VertexState {
                module: &quad_shader,
                entry_point: Some("vs_main"),
                buffers: &[Vertex::layout()],
                compilation_options: wgpu::PipelineCompilationOptions::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &quad_shader,
                entry_point: Some("fs_main"),
                targets: &[Some(wgpu::ColorTargetState {
                    format,
                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: wgpu::PipelineCompilationOptions::default(),
            }),
            primitive: wgpu::PrimitiveState::default(),
            depth_stencil: None,
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        });

        let text_bind_group_layout =
            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: Some("ruin-ui-renderer-wgpu-text-bind-group-layout"),
                entries: &[
                    wgpu::BindGroupLayoutEntry {
                        binding: 0,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Texture {
                            multisampled: false,
                            view_dimension: wgpu::TextureViewDimension::D2,
                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        },
                        count: None,
                    },
                    wgpu::BindGroupLayoutEntry {
                        binding: 1,
                        visibility: wgpu::ShaderStages::FRAGMENT,
                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                        count: None,
                    },
                ],
            });
        let text_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
            label: Some("ruin-ui-renderer-wgpu-text-pipeline-layout"),
            bind_group_layouts: &[Some(&text_bind_group_layout)],
            immediate_size: 0,
        });
        let text_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("ruin-ui-renderer-wgpu-text-pipeline"),
            layout: Some(&text_pipeline_layout),
            vertex: wgpu::VertexState {
                module: &text_shader,
                entry_point: Some("vs_main"),
                buffers: &[TextVertex::layout()],
                compilation_options: wgpu::PipelineCompilationOptions::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &text_shader,
                entry_point: Some("fs_main"),
                targets: &[Some(wgpu::ColorTargetState {
                    format,
                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: wgpu::PipelineCompilationOptions::default(),
            }),
            primitive: wgpu::PrimitiveState::default(),
            depth_stencil: None,
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        });

        let text_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("ruin-ui-renderer-wgpu-text-sampler"),
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
            ..Default::default()
        });
        let glyph_atlas = create_glyph_atlas(&device, &text_bind_group_layout, &text_sampler);

        Ok(Self {
            surface,
            device,
            queue,
            config,
            quad_pipeline,
            text_pipeline,
            text_bind_group_layout,
            text_sampler,
            font_system: FontSystem::new(),
            swash_cache: SwashCache::new(),
            text_cache: HashMap::new(),
            text_cache_order: VecDeque::new(),
            image_cache: HashMap::new(),
            image_cache_order: VecDeque::new(),
            glyph_atlas,
            scale_factor: 1.0,
            base_color,
            last_frame_stats: RendererFrameStats::default(),
        })
    }

    pub fn size(&self) -> (u32, u32) {
        (self.config.width, self.config.height)
    }

    pub fn resize(&mut self, width: u32, height: u32) {
        self.config.width = width.max(1);
        self.config.height = height.max(1);
        self.surface.configure(&self.device, &self.config);
    }

    pub fn set_scale_factor(&mut self, scale_factor: f32) {
        let next = scale_factor.max(1.0);
        if (self.scale_factor - next).abs() <= f32::EPSILON {
            return;
        }
        self.scale_factor = next;
        // Text cache is scale-dependent; flush on scale transitions (e.g. display move).
        self.text_cache.clear();
        self.text_cache_order.clear();
        self.glyph_atlas = create_glyph_atlas(
            &self.device,
            &self.text_bind_group_layout,
            &self.text_sampler,
        );
    }

    pub fn last_frame_stats(&self) -> RendererFrameStats {
        self.last_frame_stats
    }

    pub fn render(&mut self, scene: &SceneSnapshot) -> Result<(), RenderError> {
        self.render_with_logical_size(scene, scene.logical_size)
    }

    pub fn render_with_logical_size(
        &mut self,
        scene: &SceneSnapshot,
        logical_size: UiSize,
    ) -> Result<(), RenderError> {
        let render_start = std::time::Instant::now();
        let vertices = build_vertices(scene, logical_size);
        let frame = match self.surface.get_current_texture() {
            wgpu::CurrentSurfaceTexture::Success(frame)
            | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
            wgpu::CurrentSurfaceTexture::Lost => return Err(RenderError::Lost),
            wgpu::CurrentSurfaceTexture::Outdated => return Err(RenderError::Outdated),
            wgpu::CurrentSurfaceTexture::Timeout => return Err(RenderError::Timeout),
            wgpu::CurrentSurfaceTexture::Occluded => return Err(RenderError::Occluded),
            wgpu::CurrentSurfaceTexture::Validation => return Err(RenderError::Validation),
        };
        let view = frame
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());
        let vertex_buffer = self
            .device
            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some("ruin-ui-renderer-wgpu-vertices"),
                contents: bytemuck::cast_slice(&vertices),
                usage: wgpu::BufferUsages::VERTEX,
            });
        let text_prepare_start = std::time::Instant::now();
        let mut uploaded_images = Vec::new();
        let mut uploaded_texts = Vec::new();
        let mut atlas_perf = AtlasTextPerfStats::default();
        let uploaded_atlas_text =
            self.prepare_uploaded_atlas_text(scene, logical_size, &mut atlas_perf);
        let mut clip_stack = Vec::new();
        let mut active_clip = ActiveClip::default();
        for item in &scene.items {
            match item {
                DisplayItem::PushClip(region) => {
                    push_clip_state(&mut clip_stack, &mut active_clip, *region)
                }
                DisplayItem::PopClip => pop_clip_state(&mut clip_stack, &mut active_clip),
                DisplayItem::Image(image) => {
                    if let Some(uploaded) =
                        self.prepare_uploaded_image(image, logical_size, active_clip)
                    {
                        uploaded_images.push(uploaded);
                    }
                }
                DisplayItem::Text(text) => {
                    if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
                        continue;
                    }
                    if let Some(uploaded) =
                        self.prepare_uploaded_text(text, logical_size, active_clip)
                    {
                        uploaded_texts.push(uploaded);
                    }
                }
                _ => {}
            }
        }
        let text_prepare_ms = text_prepare_start.elapsed().as_secs_f64() * 1_000.0;
        let mut encoder = self
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("ruin-ui-renderer-wgpu-encoder"),
            });
        {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("ruin-ui-renderer-wgpu-pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    depth_slice: None,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear({
                            let a = self.base_color.a as f64 / 255.0;
                            // Premultiplied: multiply RGB by alpha so the compositor
                            // can blend correctly when alpha_mode is PreMultiplied.
                            wgpu::Color {
                                r: (self.base_color.r as f64 / 255.0) * a,
                                g: (self.base_color.g as f64 / 255.0) * a,
                                b: (self.base_color.b as f64 / 255.0) * a,
                                a,
                            }
                        }),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            if !vertices.is_empty() {
                pass.set_pipeline(&self.quad_pipeline);
                pass.set_vertex_buffer(0, vertex_buffer.slice(..));
                pass.draw(0..vertices.len() as u32, 0..1);
            }
            if !uploaded_images.is_empty() {
                pass.set_pipeline(&self.text_pipeline);
                for image in &uploaded_images {
                    pass.set_bind_group(0, &image.bind_group, &[]);
                    pass.set_vertex_buffer(0, image.vertex_buffer.slice(..));
                    pass.draw(0..image.vertex_count, 0..1);
                }
            }
            if let Some(atlas_text) = uploaded_atlas_text.as_ref() {
                pass.set_pipeline(&self.text_pipeline);
                pass.set_bind_group(0, &self.glyph_atlas.bind_group, &[]);
                pass.set_vertex_buffer(0, atlas_text.vertex_buffer.slice(..));
                pass.draw(0..atlas_text.vertex_count, 0..1);
            }
            if !uploaded_texts.is_empty() {
                pass.set_pipeline(&self.text_pipeline);
                for text in &uploaded_texts {
                    pass.set_bind_group(0, &text.bind_group, &[]);
                    pass.set_vertex_buffer(0, text.vertex_buffer.slice(..));
                    pass.draw(0..text.vertex_count, 0..1);
                }
            }
        }
        self.queue.submit([encoder.finish()]);
        frame.present();
        self.last_frame_stats = RendererFrameStats {
            scene_version: scene.version,
            scene_items: scene.items.len(),
            quad_vertices: vertices.len(),
            image_batches: uploaded_images.len(),
            atlas_text_vertices: uploaded_atlas_text
                .as_ref()
                .map_or(0_u32, |text| text.vertex_count),
            fallback_text_batches: uploaded_texts.len(),
            fallback_text_vertices: uploaded_texts.iter().map(|text| text.vertex_count).sum(),
            atlas_glyphs_total: atlas_perf.glyphs_total,
            atlas_glyphs_clip_culled: atlas_perf.glyphs_clip_culled,
            text_prepare_ms,
            render_ms: render_start.elapsed().as_secs_f64() * 1_000.0,
        };
        trace!(
            target: "ruin_ui_renderer_wgpu::perf",
            scene_version = self.last_frame_stats.scene_version,
            scene_items = self.last_frame_stats.scene_items,
            quad_vertices = self.last_frame_stats.quad_vertices,
            image_batches = self.last_frame_stats.image_batches,
            atlas_text_vertices = self.last_frame_stats.atlas_text_vertices,
            fallback_text_batches = self.last_frame_stats.fallback_text_batches,
            fallback_text_vertices = self.last_frame_stats.fallback_text_vertices,
            atlas_glyphs_total = self.last_frame_stats.atlas_glyphs_total,
            atlas_glyphs_clip_culled = self.last_frame_stats.atlas_glyphs_clip_culled,
            text_prepare_ms = self.last_frame_stats.text_prepare_ms,
            render_ms = self.last_frame_stats.render_ms,
            "rendered scene"
        );
        Ok(())
    }

    fn rasterize_text(
        &mut self,
        text: &PreparedText,
        raster_clip: Option<Rect>,
    ) -> Option<RasterizedText> {
        if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
            return self.rasterize_prepared_text(text, raster_clip, self.scale_factor);
        }

        let clip = raster_clip.map(|clip| scale_rect_to_pixel_rect(clip, self.scale_factor));
        let glyphs = self.collect_glyph_bitmaps(text, clip);
        let clip = clip
            .or_else(|| {
                text.bounds.map(|bounds| PixelRect {
                    left: 0,
                    top: 0,
                    right: (bounds.width * self.scale_factor).ceil() as i32,
                    bottom: (bounds.height * self.scale_factor).ceil() as i32,
                })
            })
            .or_else(|| glyph_union(&glyphs))?;
        if clip.width() == 0 || clip.height() == 0 {
            return None;
        }

        let mut pixels = vec![0u8; clip.width() as usize * clip.height() as usize * 4];
        for glyph in glyphs {
            blit_glyph(&mut pixels, clip, &glyph);
        }

        Some(RasterizedText {
            origin_offset: Point::new(
                clip.left as f32 / self.scale_factor,
                clip.top as f32 / self.scale_factor,
            ),
            draw_size: UiSize::new(
                clip.width() as f32 / self.scale_factor,
                clip.height() as f32 / self.scale_factor,
            ),
            texture_size: UiSize::new(clip.width() as f32, clip.height() as f32),
            pixels,
        })
    }

    fn rasterize_prepared_text(
        &mut self,
        text: &PreparedText,
        raster_clip: Option<Rect>,
        scale_factor: f32,
    ) -> Option<RasterizedText> {
        let clip = raster_clip.map(|clip| scale_rect_to_pixel_rect(clip, scale_factor));
        let glyphs = self.collect_prepared_glyphs(text, raster_clip, clip, scale_factor);
        let clip = raster_clip
            .map(|clip| scale_rect_to_pixel_rect(clip, scale_factor))
            .or_else(|| {
                text.bounds.map(|bounds| PixelRect {
                    left: 0,
                    top: 0,
                    right: (bounds.width * scale_factor).ceil() as i32,
                    bottom: (bounds.height * scale_factor).ceil() as i32,
                })
            })
            .or_else(|| glyph_union_prepared(&glyphs))?;
        if clip.width() == 0 || clip.height() == 0 {
            return None;
        }

        let mut pixels = vec![0u8; clip.width() as usize * clip.height() as usize * 4];
        for glyph in glyphs {
            let Some(image) = self
                .swash_cache
                .get_image(&mut self.font_system, glyph.cache_key)
                .as_ref()
            else {
                continue;
            };
            blit_cached_image(&mut pixels, clip, glyph.rect, image, glyph.color);
        }

        Some(RasterizedText {
            origin_offset: Point::new(
                clip.left as f32 / scale_factor,
                clip.top as f32 / scale_factor,
            ),
            draw_size: UiSize::new(
                clip.width() as f32 / scale_factor,
                clip.height() as f32 / scale_factor,
            ),
            texture_size: UiSize::new(clip.width() as f32, clip.height() as f32),
            pixels,
        })
    }

    fn collect_prepared_glyphs(
        &mut self,
        text: &PreparedText,
        logical_clip: Option<Rect>,
        pixel_clip: Option<PixelRect>,
        scale_factor: f32,
    ) -> Vec<PreparedGlyphBitmap> {
        let absolute_clip = logical_clip.map(|clip| {
            Rect::new(
                text.origin.x + clip.origin.x,
                text.origin.y + clip.origin.y,
                clip.size.width,
                clip.size.height,
            )
        });
        let visible_glyphs = clip_visible_glyphs(text, absolute_clip);
        let mut glyphs = Vec::with_capacity(visible_glyphs.len());
        for glyph in visible_glyphs {
            let Some(cache_key) = glyph
                .cache_key
                .map(|cache_key| scale_glyph_cache_key(cache_key, scale_factor))
            else {
                continue;
            };
            let Some(image) = self
                .swash_cache
                .get_image(&mut self.font_system, cache_key)
                .as_ref()
            else {
                continue;
            };
            let width = image.placement.width as i32;
            let height = image.placement.height as i32;
            if width == 0 || height == 0 {
                continue;
            }

            // Glyph positions are LOCAL (origin-relative); no subtraction needed.
            let local_x = (glyph.position.x * scale_factor).round() as i32;
            let local_y = (glyph.position.y * scale_factor).round() as i32;
            let rect = PixelRect {
                left: local_x + image.placement.left,
                top: local_y - image.placement.top,
                right: local_x + image.placement.left + width,
                bottom: local_y - image.placement.top + height,
            };
            if pixel_clip.is_some_and(|clip| !pixel_rects_intersect(rect, clip)) {
                continue;
            }
            glyphs.push(PreparedGlyphBitmap {
                rect,
                cache_key,
                color: resolve_glyph_color(glyph.color, text.default_color),
            });
        }
        glyphs
    }

    fn collect_glyph_bitmaps(
        &mut self,
        text: &PreparedText,
        raster_clip: Option<PixelRect>,
    ) -> Vec<GlyphBitmap> {
        let mut buffer = Buffer::new_empty(Metrics::new(
            text.font_size * self.scale_factor,
            text.line_height * self.scale_factor,
        ));
        {
            let mut borrowed = buffer.borrow_with(&mut self.font_system);
            borrowed.set_size(
                text.bounds.map(|bounds| bounds.width * self.scale_factor),
                text.bounds.map(|bounds| bounds.height * self.scale_factor),
            );
            borrowed.set_text(&text.text, &Attrs::new(), Shaping::Advanced, None);
        }

        let mut glyphs = Vec::new();
        for run in buffer.layout_runs() {
            if let Some(clip) = raster_clip {
                let line_top = run.line_top.floor() as i32;
                let line_bottom = (run.line_top + run.line_height).ceil() as i32;
                if line_bottom < clip.top || line_top > clip.bottom {
                    continue;
                }
            }
            for glyph in run.glyphs {
                let physical = glyph.physical((0.0, run.line_y), 1.0);
                let Some(image) = self
                    .swash_cache
                    .get_image(&mut self.font_system, physical.cache_key)
                    .as_ref()
                else {
                    continue;
                };
                let width = image.placement.width as i32;
                let height = image.placement.height as i32;
                if width == 0 || height == 0 {
                    continue;
                }
                let rect = PixelRect {
                    left: physical.x + image.placement.left,
                    top: physical.y - image.placement.top,
                    right: physical.x + image.placement.left + width,
                    bottom: physical.y - image.placement.top + height,
                };
                if raster_clip.is_some_and(|clip| !pixel_rects_intersect(rect, clip)) {
                    continue;
                }
                glyphs.push(GlyphBitmap {
                    rect,
                    content: image.content,
                    color: glyph
                        .color_opt
                        .map_or(text.default_color, color_from_cosmic),
                    data: image.data.clone(),
                });
            }
        }
        glyphs
    }

    fn prepare_uploaded_atlas_text(
        &mut self,
        scene: &SceneSnapshot,
        logical_size: UiSize,
        perf: &mut AtlasTextPerfStats,
    ) -> Option<UploadedAtlasText> {
        let mut vertices = Vec::new();
        let mut clip_stack = Vec::new();
        let mut active_clip = ActiveClip::default();

        for item in &scene.items {
            match item {
                DisplayItem::PushClip(region) => {
                    push_clip_state(&mut clip_stack, &mut active_clip, *region)
                }
                DisplayItem::PopClip => pop_clip_state(&mut clip_stack, &mut active_clip),
                DisplayItem::Text(text) => {
                    if text.glyphs.iter().any(|glyph| glyph.cache_key.is_none()) {
                        continue;
                    }

                    let text_bounds_clip = text.bounds.map(|bounds| {
                        Rect::new(text.origin.x, text.origin.y, bounds.width, bounds.height)
                    });
                    let logical_clip_rect =
                        match (active_clip.rect_active, active_clip.rect, text_bounds_clip) {
                            (true, Some(active_clip_rect), Some(text_bounds_rect)) => {
                                intersect_rects(Some(active_clip_rect), Some(text_bounds_rect))
                            }
                            (true, Some(active_clip_rect), None) => Some(active_clip_rect),
                            (true, None, _) => None,
                            (false, _, Some(text_bounds_rect)) => Some(text_bounds_rect),
                            (false, _, None) => None,
                        };
                    if active_clip.rect_active && logical_clip_rect.is_none() {
                        continue;
                    }
                    let clip_rect = logical_clip_rect
                        .map(|rect| scale_rect_to_pixel_rect(rect, self.scale_factor));

                    // Only iterate glyphs whose lines fall within the clip rect.
                    // For large text nodes (e.g. full Cargo.lock) this reduces
                    // iteration from O(all_glyphs) to O(visible_glyphs).
                    let all_count = text.glyphs.len();
                    let visible = clip_visible_glyphs(text, logical_clip_rect);
                    perf.glyphs_total += all_count as u32;
                    perf.glyphs_clip_culled += (all_count - visible.len()) as u32;

                    for glyph in visible {
                        let Some(cache_key) = glyph
                            .cache_key
                            .map(|cache_key| scale_glyph_cache_key(cache_key, self.scale_factor))
                        else {
                            continue;
                        };
                        let resolved_color = resolve_glyph_color(glyph.color, text.default_color);
                        let Some(atlas_glyph) = self.ensure_atlas_glyph(cache_key, resolved_color)
                        else {
                            continue;
                        };

                        // Atlas glyphs are rasterized in device pixels while scene geometry remains
                        // logical. Build and clip in device space, then convert back for vertices.
                        let abs_x =
                            ((text.origin.x + glyph.position.x) * self.scale_factor).round() as i32;
                        let abs_y =
                            ((text.origin.y + glyph.position.y) * self.scale_factor).round() as i32;
                        let glyph_rect = PixelRect {
                            left: abs_x + atlas_glyph.placement_left,
                            top: abs_y - atlas_glyph.placement_top,
                            right: abs_x
                                + atlas_glyph.placement_left
                                + atlas_glyph.atlas_rect.width as i32,
                            bottom: abs_y - atlas_glyph.placement_top
                                + atlas_glyph.atlas_rect.height as i32,
                        };
                        push_glyph_vertices(
                            &mut vertices,
                            glyph_rect,
                            atlas_glyph.atlas_rect,
                            clip_rect,
                            GlyphVertexContext {
                                logical_size,
                                scale_factor: self.scale_factor,
                                color: resolved_color,
                                clip: active_clip,
                            },
                        );
                    }
                }
                _ => {}
            }
        }

        if vertices.is_empty() {
            return None;
        }

        let vertex_buffer = self
            .device
            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some("ruin-ui-renderer-wgpu-atlas-text-vertices"),
                contents: bytemuck::cast_slice(&vertices),
                usage: wgpu::BufferUsages::VERTEX,
            });
        Some(UploadedAtlasText {
            vertex_buffer,
            vertex_count: vertices.len() as u32,
        })
    }

    #[allow(dead_code)]
    fn ensure_atlas_glyph(&mut self, cache_key: CacheKey, color: Color) -> Option<AtlasGlyph> {
        let key = AtlasGlyphKey {
            cache_key,
            color: (color.r, color.g, color.b, color.a),
        };
        if let Some(glyph) = self.glyph_atlas.glyphs.get(&key) {
            return Some(*glyph);
        }

        let (placement_left, placement_top, width, height, pixels) = {
            let image = self
                .swash_cache
                .get_image(&mut self.font_system, cache_key)
                .as_ref()?;
            let width = image.placement.width;
            let height = image.placement.height;
            if width == 0 || height == 0 {
                return None;
            }
            (
                image.placement.left,
                image.placement.top,
                width,
                height,
                rasterize_image_rgba(image, color),
            )
        };

        let atlas_rect = self.reserve_atlas_rect(width, height)?;
        self.queue.write_texture(
            wgpu::TexelCopyTextureInfo {
                texture: &self.glyph_atlas.texture,
                mip_level: 0,
                origin: wgpu::Origin3d {
                    x: atlas_rect.x,
                    y: atlas_rect.y,
                    z: 0,
                },
                aspect: wgpu::TextureAspect::All,
            },
            &pixels,
            wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(width * 4),
                rows_per_image: Some(height),
            },
            wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
        );

        let glyph = AtlasGlyph {
            atlas_rect,
            placement_left,
            placement_top,
        };
        self.glyph_atlas.glyphs.insert(key, glyph);
        Some(glyph)
    }

    #[allow(dead_code)]
    fn reserve_atlas_rect(&mut self, width: u32, height: u32) -> Option<AtlasRect> {
        if width == 0 || height == 0 || width > GLYPH_ATLAS_SIZE || height > GLYPH_ATLAS_SIZE {
            return None;
        }

        if self.glyph_atlas.cursor_x + width > GLYPH_ATLAS_SIZE {
            self.glyph_atlas.cursor_x = 0;
            self.glyph_atlas.cursor_y = self
                .glyph_atlas
                .cursor_y
                .saturating_add(self.glyph_atlas.row_height);
            self.glyph_atlas.row_height = 0;
        }

        if self.glyph_atlas.cursor_y + height > GLYPH_ATLAS_SIZE {
            self.glyph_atlas = create_glyph_atlas(
                &self.device,
                &self.text_bind_group_layout,
                &self.text_sampler,
            );
        }

        if self.glyph_atlas.cursor_x + width > GLYPH_ATLAS_SIZE
            || self.glyph_atlas.cursor_y + height > GLYPH_ATLAS_SIZE
        {
            return None;
        }

        let rect = AtlasRect {
            x: self.glyph_atlas.cursor_x,
            y: self.glyph_atlas.cursor_y,
            width,
            height,
        };
        self.glyph_atlas.cursor_x = self
            .glyph_atlas
            .cursor_x
            .saturating_add(width + GLYPH_ATLAS_PADDING);
        self.glyph_atlas.row_height = self
            .glyph_atlas
            .row_height
            .max(height + GLYPH_ATLAS_PADDING);
        Some(rect)
    }

    fn prepare_uploaded_image(
        &mut self,
        image: &PreparedImage,
        logical_size: UiSize,
        clip: ActiveClip,
    ) -> Option<UploadedImage> {
        let key = image.resource.id();
        if !self.image_cache.contains_key(&key) {
            let cached = self.create_cached_image_texture(image);
            self.image_cache.insert(key, cached);
        }
        self.touch_image_cache_entry(key);
        let cached = self
            .image_cache
            .get(&key)
            .expect("image cache entry should exist after insertion");
        let vertices = build_image_vertices(image.rect, image.uv_rect, logical_size, clip)?;
        let vertex_buffer = self
            .device
            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some("ruin-ui-renderer-wgpu-image-vertices"),
                contents: bytemuck::cast_slice(&vertices),
                usage: wgpu::BufferUsages::VERTEX,
            });
        Some(UploadedImage {
            bind_group: cached.bind_group.clone(),
            vertex_buffer,
            vertex_count: vertices.len() as u32,
        })
    }

    fn prepare_uploaded_text(
        &mut self,
        text: &PreparedText,
        logical_size: UiSize,
        clip: ActiveClip,
    ) -> Option<UploadedText> {
        let raster_clip = text_raster_clip(text, clip, logical_size)?;
        let key = text_texture_key(text, raster_clip, self.scale_factor);
        if !self.text_cache.contains_key(&key) {
            let rasterized = self.rasterize_text(text, raster_clip)?;
            let cached = self.create_cached_text_texture(&rasterized);
            self.text_cache.insert(key.clone(), cached);
        }
        self.touch_text_cache_entry(&key);

        let cached = self
            .text_cache
            .get(&key)
            .expect("text cache entry should exist after insertion");
        let origin = Point::new(
            text.origin.x + cached.origin_offset.x,
            text.origin.y + cached.origin_offset.y,
        );
        let vertices = build_text_vertices(origin, cached.size, logical_size, clip)?;
        let vertex_buffer = self
            .device
            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some("ruin-ui-renderer-wgpu-text-vertices"),
                contents: bytemuck::cast_slice(&vertices),
                usage: wgpu::BufferUsages::VERTEX,
            });

        Some(UploadedText {
            bind_group: cached.bind_group.clone(),
            vertex_buffer,
            vertex_count: vertices.len() as u32,
        })
    }

    fn touch_text_cache_entry(&mut self, key: &TextTextureKey) {
        if let Some(position) = self
            .text_cache_order
            .iter()
            .position(|existing| existing == key)
        {
            self.text_cache_order.remove(position);
        }
        self.text_cache_order.push_back(key.clone());
        while self.text_cache_order.len() > MAX_TEXT_CACHE_ENTRIES {
            let Some(evicted) = self.text_cache_order.pop_front() else {
                break;
            };
            self.text_cache.remove(&evicted);
        }
    }

    fn touch_image_cache_entry(&mut self, key: u64) {
        if let Some(position) = self
            .image_cache_order
            .iter()
            .position(|existing| *existing == key)
        {
            self.image_cache_order.remove(position);
        }
        self.image_cache_order.push_back(key);
        while self.image_cache_order.len() > MAX_IMAGE_CACHE_ENTRIES {
            let Some(evicted) = self.image_cache_order.pop_front() else {
                break;
            };
            self.image_cache.remove(&evicted);
        }
    }

    fn create_cached_text_texture(&self, text: &RasterizedText) -> CachedTextTexture {
        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
            label: Some("ruin-ui-renderer-wgpu-texture"),
            size: wgpu::Extent3d {
                width: text.texture_size.width as u32,
                height: text.texture_size.height as u32,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba8UnormSrgb,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        });
        self.queue.write_texture(
            wgpu::TexelCopyTextureInfo {
                texture: &texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            &text.pixels,
            wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(text.texture_size.width as u32 * 4),
                rows_per_image: Some(text.texture_size.height as u32),
            },
            wgpu::Extent3d {
                width: text.texture_size.width as u32,
                height: text.texture_size.height as u32,
                depth_or_array_layers: 1,
            },
        );
        let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("ruin-ui-renderer-wgpu-text-bind-group"),
            layout: &self.text_bind_group_layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(&texture_view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::Sampler(&self.text_sampler),
                },
            ],
        });
        CachedTextTexture {
            _texture: texture,
            bind_group,
            origin_offset: text.origin_offset,
            size: text.draw_size,
        }
    }

    fn create_cached_image_texture(&self, image: &PreparedImage) -> CachedImageTexture {
        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
            label: Some("ruin-ui-renderer-wgpu-image-texture"),
            size: wgpu::Extent3d {
                width: image.resource.width(),
                height: image.resource.height(),
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba8UnormSrgb,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        });
        self.queue.write_texture(
            wgpu::TexelCopyTextureInfo {
                texture: &texture,
                mip_level: 0,
                origin: wgpu::Origin3d::ZERO,
                aspect: wgpu::TextureAspect::All,
            },
            image.resource.pixels(),
            wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(image.resource.width() * 4),
                rows_per_image: Some(image.resource.height()),
            },
            wgpu::Extent3d {
                width: image.resource.width(),
                height: image.resource.height(),
                depth_or_array_layers: 1,
            },
        );
        let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("ruin-ui-renderer-wgpu-image-bind-group"),
            layout: &self.text_bind_group_layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(&texture_view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::Sampler(&self.text_sampler),
                },
            ],
        });
        CachedImageTexture {
            _texture: texture,
            bind_group,
        }
    }
}

fn create_glyph_atlas(
    device: &wgpu::Device,
    text_bind_group_layout: &wgpu::BindGroupLayout,
    text_sampler: &wgpu::Sampler,
) -> GlyphAtlas {
    let texture = device.create_texture(&wgpu::TextureDescriptor {
        label: Some("ruin-ui-renderer-wgpu-glyph-atlas"),
        size: wgpu::Extent3d {
            width: GLYPH_ATLAS_SIZE,
            height: GLYPH_ATLAS_SIZE,
            depth_or_array_layers: 1,
        },
        mip_level_count: 1,
        sample_count: 1,
        dimension: wgpu::TextureDimension::D2,
        format: wgpu::TextureFormat::Rgba8UnormSrgb,
        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
        view_formats: &[],
    });
    let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
    let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
        label: Some("ruin-ui-renderer-wgpu-glyph-atlas-bind-group"),
        layout: text_bind_group_layout,
        entries: &[
            wgpu::BindGroupEntry {
                binding: 0,
                resource: wgpu::BindingResource::TextureView(&texture_view),
            },
            wgpu::BindGroupEntry {
                binding: 1,
                resource: wgpu::BindingResource::Sampler(text_sampler),
            },
        ],
    });
    GlyphAtlas {
        texture,
        bind_group,
        glyphs: HashMap::new(),
        cursor_x: 0,
        cursor_y: 0,
        row_height: 0,
    }
}

/// Resolve `Color::SENTINEL` to `default_color`; return other colors unchanged.
#[inline]
fn resolve_glyph_color(color: Color, default_color: Color) -> Color {
    if color == Color::SENTINEL {
        default_color
    } else {
        color
    }
}

fn color_from_cosmic(color: cosmic_text::Color) -> Color {
    Color::rgba(color.r(), color.g(), color.b(), color.a())
}

#[allow(dead_code)]
fn rasterize_image_rgba(image: &SwashImage, color: Color) -> Vec<u8> {
    let width = image.placement.width as usize;
    let height = image.placement.height as usize;
    let mut pixels = vec![0u8; width * height * 4];

    for y in 0..height {
        for x in 0..width {
            let rgba = match image.content {
                SwashContent::Mask => {
                    let alpha = image.data[y * width + x];
                    [color.r, color.g, color.b, scale_alpha(color.a, alpha)]
                }
                SwashContent::Color => {
                    let index = (y * width + x) * 4;
                    [
                        image.data[index],
                        image.data[index + 1],
                        image.data[index + 2],
                        image.data[index + 3],
                    ]
                }
                SwashContent::SubpixelMask => {
                    let index = (y * width + x) * 3;
                    let coverage = average3(
                        image.data[index],
                        image.data[index + 1],
                        image.data[index + 2],
                    );
                    [color.r, color.g, color.b, scale_alpha(color.a, coverage)]
                }
            };
            let dest = (y * width + x) * 4;
            pixels[dest..dest + 4].copy_from_slice(&rgba);
        }
    }

    pixels
}

fn glyph_union(glyphs: &[GlyphBitmap]) -> Option<PixelRect> {
    let first = glyphs.first()?;
    let mut union = first.rect;
    for glyph in &glyphs[1..] {
        union.left = union.left.min(glyph.rect.left);
        union.top = union.top.min(glyph.rect.top);
        union.right = union.right.max(glyph.rect.right);
        union.bottom = union.bottom.max(glyph.rect.bottom);
    }
    Some(union)
}

fn glyph_union_prepared(glyphs: &[PreparedGlyphBitmap]) -> Option<PixelRect> {
    let first = glyphs.first()?;
    let mut union = first.rect;
    for glyph in &glyphs[1..] {
        union.left = union.left.min(glyph.rect.left);
        union.top = union.top.min(glyph.rect.top);
        union.right = union.right.max(glyph.rect.right);
        union.bottom = union.bottom.max(glyph.rect.bottom);
    }
    Some(union)
}

fn blit_glyph(pixels: &mut [u8], clip: PixelRect, glyph: &GlyphBitmap) {
    let width = clip.width() as usize;
    let glyph_width = glyph.rect.width() as usize;
    let glyph_height = glyph.rect.height() as usize;
    if glyph_width == 0 || glyph_height == 0 {
        return;
    }

    for y in 0..glyph_height {
        let dest_y = glyph.rect.top + y as i32;
        if dest_y < clip.top || dest_y >= clip.bottom {
            continue;
        }
        for x in 0..glyph_width {
            let dest_x = glyph.rect.left + x as i32;
            if dest_x < clip.left || dest_x >= clip.right {
                continue;
            }
            let src = match glyph.content {
                SwashContent::Mask => {
                    let alpha = glyph.data[y * glyph_width + x];
                    [
                        glyph.color.r,
                        glyph.color.g,
                        glyph.color.b,
                        scale_alpha(glyph.color.a, alpha),
                    ]
                }
                SwashContent::Color => {
                    let index = (y * glyph_width + x) * 4;
                    [
                        glyph.data[index],
                        glyph.data[index + 1],
                        glyph.data[index + 2],
                        glyph.data[index + 3],
                    ]
                }
                SwashContent::SubpixelMask => {
                    let index = (y * glyph_width + x) * 3;
                    let coverage = average3(
                        glyph.data[index],
                        glyph.data[index + 1],
                        glyph.data[index + 2],
                    );
                    [
                        glyph.color.r,
                        glyph.color.g,
                        glyph.color.b,
                        scale_alpha(glyph.color.a, coverage),
                    ]
                }
            };

            let dest_index =
                (((dest_y - clip.top) as usize * width) + (dest_x - clip.left) as usize) * 4;
            blend_rgba(&mut pixels[dest_index..dest_index + 4], src);
        }
    }
}

fn blit_cached_image(
    pixels: &mut [u8],
    clip: PixelRect,
    rect: PixelRect,
    image: &SwashImage,
    color: Color,
) {
    let width = clip.width() as usize;
    let glyph_width = rect.width() as usize;
    let glyph_height = rect.height() as usize;
    if glyph_width == 0 || glyph_height == 0 {
        return;
    }

    for y in 0..glyph_height {
        let dest_y = rect.top + y as i32;
        if dest_y < clip.top || dest_y >= clip.bottom {
            continue;
        }
        for x in 0..glyph_width {
            let dest_x = rect.left + x as i32;
            if dest_x < clip.left || dest_x >= clip.right {
                continue;
            }
            let src = match image.content {
                SwashContent::Mask => {
                    let alpha = image.data[y * glyph_width + x];
                    [color.r, color.g, color.b, scale_alpha(color.a, alpha)]
                }
                SwashContent::Color => {
                    let index = (y * glyph_width + x) * 4;
                    [
                        image.data[index],
                        image.data[index + 1],
                        image.data[index + 2],
                        image.data[index + 3],
                    ]
                }
                SwashContent::SubpixelMask => {
                    let index = (y * glyph_width + x) * 3;
                    let coverage = average3(
                        image.data[index],
                        image.data[index + 1],
                        image.data[index + 2],
                    );
                    [color.r, color.g, color.b, scale_alpha(color.a, coverage)]
                }
            };

            let dest_index =
                (((dest_y - clip.top) as usize * width) + (dest_x - clip.left) as usize) * 4;
            blend_rgba(&mut pixels[dest_index..dest_index + 4], src);
        }
    }
}

fn scale_alpha(color_alpha: u8, coverage: u8) -> u8 {
    ((color_alpha as u16 * coverage as u16) / 255) as u8
}

fn average3(a: u8, b: u8, c: u8) -> u8 {
    ((a as u16 + b as u16 + c as u16) / 3) as u8
}

fn blend_rgba(dst: &mut [u8], src: [u8; 4]) {
    let src_alpha = src[3] as f32 / 255.0;
    if src_alpha <= 0.0 {
        return;
    }
    let dst_alpha = dst[3] as f32 / 255.0;
    let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
    if out_alpha <= 0.0 {
        dst.copy_from_slice(&[0, 0, 0, 0]);
        return;
    }

    for channel in 0..3 {
        let src_value = src[channel] as f32 / 255.0;
        let dst_value = dst[channel] as f32 / 255.0;
        let out = (src_value * src_alpha + dst_value * dst_alpha * (1.0 - src_alpha)) / out_alpha;
        dst[channel] = (out * 255.0).round().clamp(0.0, 255.0) as u8;
    }
    dst[3] = (out_alpha * 255.0).round().clamp(0.0, 255.0) as u8;
}

fn build_text_vertices(
    origin: Point,
    size: UiSize,
    logical_size: UiSize,
    clip: ActiveClip,
) -> Option<[TextVertex; 6]> {
    let rect = Rect::new(origin.x, origin.y, size.width, size.height);
    build_textured_vertices(
        rect,
        (0.0, 0.0, 1.0, 1.0),
        [1.0, 1.0, 1.0, 1.0],
        logical_size,
        clip,
    )
}

fn build_image_vertices(
    rect: Rect,
    uv_rect: (f32, f32, f32, f32),
    logical_size: UiSize,
    clip: ActiveClip,
) -> Option<[TextVertex; 6]> {
    build_textured_vertices(rect, uv_rect, [1.0, 1.0, 1.0, 1.0], logical_size, clip)
}

fn build_textured_vertices(
    rect: Rect,
    uv_rect: (f32, f32, f32, f32),
    color: [f32; 4],
    logical_size: UiSize,
    clip: ActiveClip,
) -> Option<[TextVertex; 6]> {
    let (rect, (u0, v0, u1, v1)) = clip_textured_rect(rect, uv_rect, clip.resolved_rect())?;
    let left = to_ndc_x(rect.origin.x, logical_size.width.max(1.0));
    let right = to_ndc_x(rect.origin.x + rect.size.width, logical_size.width.max(1.0));
    let top = to_ndc_y(rect.origin.y, logical_size.height.max(1.0));
    let bottom = to_ndc_y(
        rect.origin.y + rect.size.height,
        logical_size.height.max(1.0),
    );
    let clip_rect = clip_rect_array(clip);
    let (rounded_clip_rect, clip_params) = rounded_clip_arrays(clip);

    Some([
        TextVertex {
            position: [left, top],
            world_position: [rect.origin.x, rect.origin.y],
            uv: [u0, v0],
            color,
            clip_rect,
            rounded_clip_rect,
            clip_params,
        },
        TextVertex {
            position: [left, bottom],
            world_position: [rect.origin.x, rect.origin.y + rect.size.height],
            uv: [u0, v1],
            color,
            clip_rect,
            rounded_clip_rect,
            clip_params,
        },
        TextVertex {
            position: [right, top],
            world_position: [rect.origin.x + rect.size.width, rect.origin.y],
            uv: [u1, v0],
            color,
            clip_rect,
            rounded_clip_rect,
            clip_params,
        },
        TextVertex {
            position: [right, top],
            world_position: [rect.origin.x + rect.size.width, rect.origin.y],
            uv: [u1, v0],
            color,
            clip_rect,
            rounded_clip_rect,
            clip_params,
        },
        TextVertex {
            position: [left, bottom],
            world_position: [rect.origin.x, rect.origin.y + rect.size.height],
            uv: [u0, v1],
            color,
            clip_rect,
            rounded_clip_rect,
            clip_params,
        },
        TextVertex {
            position: [right, bottom],
            world_position: [
                rect.origin.x + rect.size.width,
                rect.origin.y + rect.size.height,
            ],
            uv: [u1, v1],
            color,
            clip_rect,
            rounded_clip_rect,
            clip_params,
        },
    ])
}

#[allow(dead_code)]
#[derive(Clone, Copy)]
struct GlyphVertexContext {
    logical_size: UiSize,
    scale_factor: f32,
    color: Color,
    clip: ActiveClip,
}

#[allow(dead_code)]
fn push_glyph_vertices(
    vertices: &mut Vec<TextVertex>,
    glyph_rect: PixelRect,
    atlas_rect: AtlasRect,
    clip_rect: Option<PixelRect>,
    context: GlyphVertexContext,
) {
    let Some((dest_rect, uv_rect)) = clipped_glyph_quad(glyph_rect, atlas_rect, clip_rect) else {
        return;
    };

    let scale_factor = context.scale_factor.max(1.0);
    let logical_left = dest_rect.left as f32 / scale_factor;
    let logical_top = dest_rect.top as f32 / scale_factor;
    let logical_right = dest_rect.right as f32 / scale_factor;
    let logical_bottom = dest_rect.bottom as f32 / scale_factor;
    let left = to_ndc_x(logical_left, context.logical_size.width.max(1.0));
    let right = to_ndc_x(logical_right, context.logical_size.width.max(1.0));
    let top = to_ndc_y(logical_top, context.logical_size.height.max(1.0));
    let bottom = to_ndc_y(logical_bottom, context.logical_size.height.max(1.0));

    let color = color_to_f32(context.color);
    let clip_rect = clip_rect_array(context.clip);
    let (rounded_clip_rect, clip_params) = rounded_clip_arrays(context.clip);
    vertices.extend_from_slice(&[
        TextVertex {
            position: [left, top],
            world_position: [logical_left, logical_top],
            uv: [uv_rect.0, uv_rect.1],
            color,
            clip_rect,
            rounded_clip_rect,
            clip_params,
        },
        TextVertex {
            position: [left, bottom],
            world_position: [logical_left, logical_bottom],
            uv: [uv_rect.0, uv_rect.3],
            color,
            clip_rect,
            rounded_clip_rect,
            clip_params,
        },
        TextVertex {
            position: [right, top],
            world_position: [logical_right, logical_top],
            uv: [uv_rect.2, uv_rect.1],
            color,
            clip_rect,
            rounded_clip_rect,
            clip_params,
        },
        TextVertex {
            position: [right, top],
            world_position: [logical_right, logical_top],
            uv: [uv_rect.2, uv_rect.1],
            color,
            clip_rect,
            rounded_clip_rect,
            clip_params,
        },
        TextVertex {
            position: [left, bottom],
            world_position: [logical_left, logical_bottom],
            uv: [uv_rect.0, uv_rect.3],
            color,
            clip_rect,
            rounded_clip_rect,
            clip_params,
        },
        TextVertex {
            position: [right, bottom],
            world_position: [logical_right, logical_bottom],
            uv: [uv_rect.2, uv_rect.3],
            color,
            clip_rect,
            rounded_clip_rect,
            clip_params,
        },
    ]);
}

fn color_to_f32(color: Color) -> [f32; 4] {
    [
        color.r as f32 / 255.0,
        color.g as f32 / 255.0,
        color.b as f32 / 255.0,
        color.a as f32 / 255.0,
    ]
}

fn build_vertices(scene: &SceneSnapshot, logical_size: UiSize) -> Vec<Vertex> {
    let width = logical_size.width.max(1.0);
    let height = logical_size.height.max(1.0);
    let mut vertices = Vec::new();
    let mut clip_stack = Vec::new();
    let mut active_clip = ActiveClip::default();

    for item in &scene.items {
        match item {
            DisplayItem::PushClip(region) => {
                push_clip_state(&mut clip_stack, &mut active_clip, *region)
            }
            DisplayItem::PopClip => pop_clip_state(&mut clip_stack, &mut active_clip),
            DisplayItem::Quad(quad) => push_quad_vertices(
                &mut vertices,
                quad.rect,
                quad.color,
                width,
                height,
                active_clip,
            ),
            DisplayItem::RoundedRect(rounded_rect) => {
                push_rounded_rect_vertices(&mut vertices, *rounded_rect, width, height, active_clip)
            }
            DisplayItem::ShadowRect(shadow_rect) => {
                push_shadow_rect_vertices(&mut vertices, *shadow_rect, width, height, active_clip)
            }
            _ => {}
        }
    }

    vertices
}

fn push_quad_vertices(
    vertices: &mut Vec<Vertex>,
    rect: Rect,
    color: Color,
    width: f32,
    height: f32,
    clip: ActiveClip,
) {
    push_shape_vertices(
        vertices,
        rect,
        rect,
        Some(color),
        None,
        [0.0, 0.0, 0.0, 0.0],
        width,
        height,
        clip,
        None,
        0.0,
    );
}

fn push_rounded_rect_vertices(
    vertices: &mut Vec<Vertex>,
    rounded_rect: RoundedRect,
    width: f32,
    height: f32,
    clip: ActiveClip,
) {
    push_shape_vertices(
        vertices,
        rounded_rect.rect,
        rounded_rect.rect,
        rounded_rect.fill,
        rounded_rect.border_color,
        [rounded_rect.radius, rounded_rect.border_width, 0.0, 0.0],
        width,
        height,
        clip,
        None,
        0.0,
    );
}

fn push_shadow_rect_vertices(
    vertices: &mut Vec<Vertex>,
    shadow_rect: ShadowRect,
    width: f32,
    height: f32,
    clip: ActiveClip,
) {
    let blur_extent = shadow_blur_extent(shadow_rect.blur);
    let geometry_rect = match shadow_rect.kind {
        BoxShadowKind::Outer => expand_rect(shadow_rect.rect, blur_extent),
        BoxShadowKind::Inner => shadow_rect.source_rect,
    };
    if geometry_rect.size.width <= 0.0 || geometry_rect.size.height <= 0.0 {
        return;
    }

    let shadow_kind = match shadow_rect.kind {
        BoxShadowKind::Outer => 1.0,
        BoxShadowKind::Inner => 2.0,
    };

    push_shape_vertices(
        vertices,
        geometry_rect,
        shadow_rect.rect,
        Some(shadow_rect.color),
        None,
        [shadow_rect.radius, 0.0, shadow_kind, blur_extent],
        width,
        height,
        clip,
        Some(shadow_rect.source_rect),
        shadow_rect.source_radius,
    );
}

#[allow(clippy::too_many_arguments)]
fn push_shape_vertices(
    vertices: &mut Vec<Vertex>,
    geometry_rect: Rect,
    shader_rect: Rect,
    fill_color: Option<Color>,
    border_color: Option<Color>,
    shape_params: [f32; 4],
    width: f32,
    height: f32,
    clip: ActiveClip,
    shadow_base_rect: Option<Rect>,
    shadow_base_radius: f32,
) {
    let left = to_ndc_x(geometry_rect.origin.x, width);
    let right = to_ndc_x(geometry_rect.origin.x + geometry_rect.size.width, width);
    let top = to_ndc_y(geometry_rect.origin.y, height);
    let bottom = to_ndc_y(geometry_rect.origin.y + geometry_rect.size.height, height);
    let rect_data = rect_to_array(shader_rect);
    let fill_color = fill_color.map_or([0.0, 0.0, 0.0, 0.0], color_to_f32);
    let border_color = border_color.map_or([0.0, 0.0, 0.0, 0.0], color_to_f32);
    let clip_rect = clip_rect_array(clip);
    let (rounded_clip_rect, clip_params) = rounded_clip_arrays(clip);
    let shadow_base_rect = shadow_base_rect.map_or([0.0, 0.0, 0.0, 0.0], rect_to_array);

    vertices.extend_from_slice(&[
        Vertex {
            position: [left, top],
            world_position: [geometry_rect.origin.x, geometry_rect.origin.y],
            rect: rect_data,
            fill_color,
            border_color,
            shape_params,
            clip_rect,
            rounded_clip_rect,
            clip_params,
            shadow_base_rect,
            shadow_base_params: [shadow_base_radius, 0.0],
        },
        Vertex {
            position: [left, bottom],
            world_position: [
                geometry_rect.origin.x,
                geometry_rect.origin.y + geometry_rect.size.height,
            ],
            rect: rect_data,
            fill_color,
            border_color,
            shape_params,
            clip_rect,
            rounded_clip_rect,
            clip_params,
            shadow_base_rect,
            shadow_base_params: [shadow_base_radius, 0.0],
        },
        Vertex {
            position: [right, top],
            world_position: [
                geometry_rect.origin.x + geometry_rect.size.width,
                geometry_rect.origin.y,
            ],
            rect: rect_data,
            fill_color,
            border_color,
            shape_params,
            clip_rect,
            rounded_clip_rect,
            clip_params,
            shadow_base_rect,
            shadow_base_params: [shadow_base_radius, 0.0],
        },
        Vertex {
            position: [right, top],
            world_position: [
                geometry_rect.origin.x + geometry_rect.size.width,
                geometry_rect.origin.y,
            ],
            rect: rect_data,
            fill_color,
            border_color,
            shape_params,
            clip_rect,
            rounded_clip_rect,
            clip_params,
            shadow_base_rect,
            shadow_base_params: [shadow_base_radius, 0.0],
        },
        Vertex {
            position: [left, bottom],
            world_position: [
                geometry_rect.origin.x,
                geometry_rect.origin.y + geometry_rect.size.height,
            ],
            rect: rect_data,
            fill_color,
            border_color,
            shape_params,
            clip_rect,
            rounded_clip_rect,
            clip_params,
            shadow_base_rect,
            shadow_base_params: [shadow_base_radius, 0.0],
        },
        Vertex {
            position: [right, bottom],
            world_position: [
                geometry_rect.origin.x + geometry_rect.size.width,
                geometry_rect.origin.y + geometry_rect.size.height,
            ],
            rect: rect_data,
            fill_color,
            border_color,
            shape_params,
            clip_rect,
            rounded_clip_rect,
            clip_params,
            shadow_base_rect,
            shadow_base_params: [shadow_base_radius, 0.0],
        },
    ]);
}

fn rect_to_array(rect: Rect) -> [f32; 4] {
    [
        rect.origin.x,
        rect.origin.y,
        rect.origin.x + rect.size.width,
        rect.origin.y + rect.size.height,
    ]
}

fn expand_rect(rect: Rect, inset: f32) -> Rect {
    Rect::new(
        rect.origin.x - inset,
        rect.origin.y - inset,
        rect.size.width + inset * 2.0,
        rect.size.height + inset * 2.0,
    )
}

fn scale_rect_to_pixel_rect(rect: Rect, scale: f32) -> PixelRect {
    PixelRect {
        left: (rect.origin.x * scale).floor() as i32,
        top: (rect.origin.y * scale).floor() as i32,
        right: ((rect.origin.x + rect.size.width) * scale).ceil() as i32,
        bottom: ((rect.origin.y + rect.size.height) * scale).ceil() as i32,
    }
}

fn pixel_rects_intersect(first: PixelRect, second: PixelRect) -> bool {
    first.left < second.right
        && first.right > second.left
        && first.top < second.bottom
        && first.bottom > second.top
}

fn shadow_blur_extent(blur: f32) -> f32 {
    blur.max(0.0) * 2.0
}

fn clip_rect_array(clip: ActiveClip) -> [f32; 4] {
    clip.resolved_rect()
        .map_or([0.0, 0.0, 0.0, 0.0], rect_to_array)
}

fn rounded_clip_arrays(clip: ActiveClip) -> ([f32; 4], [f32; 2]) {
    let rounded_clip_rect = clip
        .rounded
        .map_or([0.0, 0.0, 0.0, 0.0], |(rect, _)| rect_to_array(rect));
    let clip_params = [
        if clip.rect_active { 1.0 } else { 0.0 },
        clip.rounded.map_or(0.0, |(_, radius)| radius),
    ];
    (rounded_clip_rect, clip_params)
}

fn push_clip_state(stack: &mut Vec<ActiveClip>, active: &mut ActiveClip, region: ClipRegion) {
    stack.push(*active);
    if region.radius > 0.0 {
        active.rounded = Some((region.rect, region.radius));
    }
    active.rect = if active.rect_active {
        if active.rect.is_none() {
            None
        } else {
            intersect_rects(active.rect, Some(region.rect))
        }
    } else {
        Some(region.rect)
    };
    active.rect_active = true;
}

fn pop_clip_state(stack: &mut Vec<ActiveClip>, active: &mut ActiveClip) {
    *active = stack.pop().unwrap_or_default();
}

fn intersect_rects(first: Option<Rect>, second: Option<Rect>) -> Option<Rect> {
    match (first, second) {
        (Some(a), Some(b)) => {
            let left = a.origin.x.max(b.origin.x);
            let top = a.origin.y.max(b.origin.y);
            let right = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
            let bottom = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
            if right <= left || bottom <= top {
                None
            } else {
                Some(Rect::new(left, top, right - left, bottom - top))
            }
        }
        (Some(rect), None) | (None, Some(rect)) => Some(rect),
        (None, None) => None,
    }
}

fn text_raster_clip(
    text: &PreparedText,
    clip: ActiveClip,
    logical_size: UiSize,
) -> Option<Option<Rect>> {
    let mut absolute_clip = Some(Rect::new(0.0, 0.0, logical_size.width, logical_size.height));
    if clip.rect_active {
        absolute_clip = intersect_rects(absolute_clip, Some(clip.resolved_rect()?));
    }
    if let Some(bounds) = text.bounds {
        let text_bounds = Rect::new(text.origin.x, text.origin.y, bounds.width, bounds.height);
        absolute_clip = intersect_rects(absolute_clip, Some(text_bounds));
    }
    absolute_clip?;

    Some(absolute_clip.map(|rect| {
        Rect::new(
            rect.origin.x - text.origin.x,
            rect.origin.y - text.origin.y,
            rect.size.width,
            rect.size.height,
        )
    }))
}

fn empty_clip_rect() -> Rect {
    Rect::new(1.0, 1.0, -1.0, -1.0)
}

fn to_ndc_x(x: f32, width: f32) -> f32 {
    (x / width) * 2.0 - 1.0
}

fn to_ndc_y(y: f32, height: f32) -> f32 {
    1.0 - (y / height) * 2.0
}

#[allow(dead_code)]
fn clipped_glyph_quad(
    glyph_rect: PixelRect,
    atlas_rect: AtlasRect,
    clip_rect: Option<PixelRect>,
) -> Option<(PixelRect, (f32, f32, f32, f32))> {
    if glyph_rect.width() == 0 || glyph_rect.height() == 0 {
        return None;
    }

    let clipped = if let Some(clip) = clip_rect {
        PixelRect {
            left: glyph_rect.left.max(clip.left),
            top: glyph_rect.top.max(clip.top),
            right: glyph_rect.right.min(clip.right),
            bottom: glyph_rect.bottom.min(clip.bottom),
        }
    } else {
        glyph_rect
    };

    if clipped.width() == 0 || clipped.height() == 0 {
        return None;
    }

    let u0 =
        (atlas_rect.x as f32 + (clipped.left - glyph_rect.left) as f32) / GLYPH_ATLAS_SIZE as f32;
    let v0 =
        (atlas_rect.y as f32 + (clipped.top - glyph_rect.top) as f32) / GLYPH_ATLAS_SIZE as f32;
    let u1 = (atlas_rect.x as f32 + atlas_rect.width as f32
        - (glyph_rect.right - clipped.right) as f32)
        / GLYPH_ATLAS_SIZE as f32;
    let v1 = (atlas_rect.y as f32 + atlas_rect.height as f32
        - (glyph_rect.bottom - clipped.bottom) as f32)
        / GLYPH_ATLAS_SIZE as f32;

    Some((clipped, (u0, v0, u1, v1)))
}

fn clip_textured_rect(
    rect: Rect,
    uv_rect: (f32, f32, f32, f32),
    clip_rect: Option<Rect>,
) -> Option<(Rect, (f32, f32, f32, f32))> {
    let clipped = if let Some(clip) = clip_rect {
        let left = rect.origin.x.max(clip.origin.x);
        let top = rect.origin.y.max(clip.origin.y);
        let right = (rect.origin.x + rect.size.width).min(clip.origin.x + clip.size.width);
        let bottom = (rect.origin.y + rect.size.height).min(clip.origin.y + clip.size.height);
        if right <= left || bottom <= top {
            return None;
        }
        Rect::new(left, top, right - left, bottom - top)
    } else {
        rect
    };

    if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
        return None;
    }

    let (u0, v0, u1, v1) = uv_rect;
    let clipped_u0 = u0 + (u1 - u0) * ((clipped.origin.x - rect.origin.x) / rect.size.width);
    let clipped_v0 = v0 + (v1 - v0) * ((clipped.origin.y - rect.origin.y) / rect.size.height);
    let clipped_u1 = u1
        - (u1 - u0)
            * (((rect.origin.x + rect.size.width) - (clipped.origin.x + clipped.size.width))
                / rect.size.width);
    let clipped_v1 = v1
        - (v1 - v0)
            * (((rect.origin.y + rect.size.height) - (clipped.origin.y + clipped.size.height))
                / rect.size.height);

    Some((clipped, (clipped_u0, clipped_v0, clipped_u1, clipped_v1)))
}

/// Returns the sub-slice of `text.glyphs` whose lines overlap `clip`.
///
/// Glyphs are stored in text-layout order (top-to-bottom, left-to-right).
/// We binary-search `text.lines` for the first and last visible line, then
/// return only those glyph indices.  This turns O(all_glyphs) iteration into
/// O(log(lines) + visible_glyphs) — critical for large text nodes in scroll
/// boxes where only a few lines are on screen at once.
fn clip_visible_glyphs(text: &PreparedText, clip: Option<Rect>) -> &[GlyphInstance] {
    let Some(clip) = clip else {
        return &text.glyphs;
    };
    let lines = &text.lines;
    if lines.is_empty() {
        return &text.glyphs;
    }

    // Convert clip bounds to local (origin-relative) Y coordinates.
    let local_top = clip.origin.y - text.origin.y;
    let local_bottom = local_top + clip.size.height;

    // First line whose bottom edge reaches or passes the clip top.
    let start = lines.partition_point(|l| l.rect.origin.y + l.rect.size.height <= local_top);
    // First line whose top edge is strictly past the clip bottom.
    let end = lines.partition_point(|l| l.rect.origin.y <= local_bottom);

    if start >= end || start >= lines.len() {
        return &text.glyphs[0..0];
    }

    let glyph_start = lines[start].glyph_start;
    let glyph_end = lines[end - 1].glyph_end;
    &text.glyphs[glyph_start..glyph_end.min(text.glyphs.len())]
}

fn text_texture_key(
    text: &PreparedText,
    raster_clip: Option<Rect>,
    scale_factor: f32,
) -> TextTextureKey {
    TextTextureKey {
        layout_key: text.layout_cache_key(),
        bounds: text.bounds.map(|bounds| {
            (
                (bounds.width * scale_factor).ceil() as i32,
                (bounds.height * scale_factor).ceil() as i32,
            )
        }),
        clip: raster_clip
            .map(|clip| pixel_rect_tuple(scale_rect_to_pixel_rect(clip, scale_factor))),
        font_size_bits: text.font_size.to_bits(),
        line_height_bits: text.line_height.to_bits(),
        color: (
            text.default_color.r,
            text.default_color.g,
            text.default_color.b,
            text.default_color.a,
        ),
    }
}

fn pixel_rect_tuple(rect: PixelRect) -> (i32, i32, i32, i32) {
    (rect.left, rect.top, rect.right, rect.bottom)
}

fn scale_glyph_cache_key(cache_key: CacheKey, scale_factor: f32) -> CacheKey {
    CacheKey {
        font_size_bits: (f32::from_bits(cache_key.font_size_bits) * scale_factor.max(1.0))
            .to_bits(),
        ..cache_key
    }
}

#[cfg(test)]
mod tests {
    use super::{
        ActiveClip, AtlasRect, GlyphVertexContext, PixelRect, blend_rgba, build_text_vertices,
        build_vertices, clip_rect_array, clip_visible_glyphs, push_clip_state, push_glyph_vertices,
        rounded_clip_arrays, scale_glyph_cache_key, text_texture_key,
    };
    use cosmic_text::{CacheKey, CacheKeyFlags, SubpixelBin, fontdb};
    use ruin_ui::{
        ClipRegion, Color, Point, PreparedText, Rect, SceneSnapshot, TextStyle, TextSystem,
        TextWrap, UiSize,
    };

    #[test]
    fn quad_scenes_expand_to_six_vertices_per_quad() {
        let mut scene = SceneSnapshot::new(1, UiSize::new(100.0, 50.0));
        scene.push_quad(
            Rect::new(0.0, 0.0, 100.0, 50.0),
            Color::rgb(0x11, 0x22, 0x33),
        );
        scene.push_quad(
            Rect::new(10.0, 10.0, 20.0, 20.0),
            Color::rgb(0x44, 0x55, 0x66),
        );
        scene.push_text(PreparedText::monospace(
            "ignored",
            Point::new(4.0, 8.0),
            16.0,
            8.0,
            Color::rgb(0xFF, 0xFF, 0xFF),
        ));

        let vertices = build_vertices(&scene, scene.logical_size);
        assert_eq!(vertices.len(), 12);
    }

    #[test]
    fn blend_rgba_composes_source_over_destination() {
        let mut dst = [0, 0, 255, 255];
        blend_rgba(&mut dst, [255, 0, 0, 128]);
        assert!(dst[0] > 0);
        assert!(dst[2] > 0);
        assert_eq!(dst[3], 255);
    }

    #[test]
    fn text_texture_key_ignores_absolute_origin() {
        // Two PreparedTexts with the same content but different origins must
        // produce the same TextTextureKey (the key stores local glyph offsets).
        let first = PreparedText::monospace(
            "x",
            Point::new(20.0, 30.0),
            16.0,
            8.0,
            Color::rgb(0xEE, 0xEE, 0xEE),
        );
        let second = PreparedText::monospace(
            "x",
            Point::new(60.0, 90.0),
            16.0,
            8.0,
            Color::rgb(0xEE, 0xEE, 0xEE),
        );

        assert_eq!(
            text_texture_key(&first, None, 1.0),
            text_texture_key(&second, None, 1.0)
        );
    }

    #[test]
    fn text_texture_key_normalizes_clip_to_device_pixels() {
        let text = PreparedText::monospace(
            "scrolling text",
            Point::new(20.0, 30.0),
            16.0,
            8.0,
            Color::rgb(0xEE, 0xEE, 0xEE),
        );

        let first = text_texture_key(&text, Some(Rect::new(0.001, 4.001, 80.001, 20.001)), 2.0);
        let second = text_texture_key(&text, Some(Rect::new(0.002, 4.002, 80.002, 20.002)), 2.0);
        let different_pixel =
            text_texture_key(&text, Some(Rect::new(0.6, 4.001, 80.001, 20.001)), 2.0);

        assert_eq!(first, second);
        assert_ne!(first, different_pixel);
    }

    #[test]
    fn clip_visible_glyphs_returns_only_lines_intersecting_clip() {
        let mut text_system = TextSystem::new();
        let style = TextStyle::new(16.0, Color::rgb(0xEE, 0xEE, 0xEE))
            .with_wrap(TextWrap::None)
            .with_line_height(20.0);
        let text = text_system.prepare("abcd\nefgh", Point::new(10.0, 20.0), &style);
        let visible = clip_visible_glyphs(&text, Some(Rect::new(10.0, 40.0, 100.0, 20.0)));

        assert_eq!(visible.len(), 4);
        assert_eq!(visible[0].text_start, 5);
        assert_eq!(visible[3].text_end, 9);
    }

    #[test]
    fn scale_glyph_cache_key_scales_font_size_only() {
        let key = CacheKey {
            font_id: fontdb::ID::dummy(),
            glyph_id: 42,
            font_size_bits: 13.0_f32.to_bits(),
            x_bin: SubpixelBin::One,
            y_bin: SubpixelBin::Two,
            font_weight: fontdb::Weight::BOLD,
            flags: CacheKeyFlags::FAKE_ITALIC,
        };

        let scaled = scale_glyph_cache_key(key, 2.0);

        assert_eq!(f32::from_bits(scaled.font_size_bits), 26.0);
        assert_eq!(scaled.font_id, key.font_id);
        assert_eq!(scaled.glyph_id, key.glyph_id);
        assert_eq!(scaled.x_bin, key.x_bin);
        assert_eq!(scaled.y_bin, key.y_bin);
        assert_eq!(scaled.font_weight, key.font_weight);
        assert_eq!(scaled.flags, key.flags);
    }

    #[test]
    fn scaled_atlas_vertices_convert_device_pixels_to_logical_space() {
        let mut vertices = Vec::new();
        push_glyph_vertices(
            &mut vertices,
            PixelRect {
                left: 20,
                top: 10,
                right: 40,
                bottom: 30,
            },
            AtlasRect {
                x: 0,
                y: 0,
                width: 20,
                height: 20,
            },
            None,
            GlyphVertexContext {
                logical_size: UiSize::new(100.0, 100.0),
                scale_factor: 2.0,
                color: Color::rgb(0xFF, 0xFF, 0xFF),
                clip: ActiveClip::default(),
            },
        );

        assert_eq!(vertices.len(), 6);
        assert_eq!(vertices[0].world_position, [10.0, 5.0]);
        assert_eq!(vertices[5].world_position, [20.0, 15.0]);
    }

    #[test]
    fn nested_clip_with_empty_intersection_stays_clipped() {
        let mut clip_stack = Vec::new();
        let mut active_clip = ActiveClip::default();

        push_clip_state(
            &mut clip_stack,
            &mut active_clip,
            ClipRegion {
                rect: Rect::new(0.0, 0.0, 100.0, 100.0),
                radius: 0.0,
            },
        );
        push_clip_state(
            &mut clip_stack,
            &mut active_clip,
            ClipRegion {
                rect: Rect::new(150.0, 150.0, 50.0, 50.0),
                radius: 8.0,
            },
        );

        assert!(active_clip.rect_active);
        assert_eq!(active_clip.rect, None);
        let clip_rect = clip_rect_array(active_clip);
        assert!(clip_rect[0] > clip_rect[2]);
        assert!(clip_rect[1] > clip_rect[3]);
        let (_, clip_params) = rounded_clip_arrays(active_clip);
        assert_eq!(clip_params[0], 1.0);
        assert!(
            build_text_vertices(
                Point::new(160.0, 160.0),
                UiSize::new(24.0, 12.0),
                UiSize::new(400.0, 400.0),
                active_clip,
            )
            .is_none()
        );
    }

    #[test]
    fn deeper_nested_clip_cannot_revive_empty_parent_clip() {
        let mut clip_stack = Vec::new();
        let mut active_clip = ActiveClip::default();

        push_clip_state(
            &mut clip_stack,
            &mut active_clip,
            ClipRegion {
                rect: Rect::new(0.0, 0.0, 100.0, 100.0),
                radius: 0.0,
            },
        );
        push_clip_state(
            &mut clip_stack,
            &mut active_clip,
            ClipRegion {
                rect: Rect::new(150.0, 150.0, 50.0, 50.0),
                radius: 12.0,
            },
        );
        push_clip_state(
            &mut clip_stack,
            &mut active_clip,
            ClipRegion {
                rect: Rect::new(160.0, 160.0, 24.0, 12.0),
                radius: 8.0,
            },
        );

        assert!(active_clip.rect_active);
        assert_eq!(active_clip.rect, None);
        assert!(
            build_text_vertices(
                Point::new(162.0, 162.0),
                UiSize::new(16.0, 8.0),
                UiSize::new(400.0, 400.0),
                active_clip,
            )
            .is_none()
        );
    }
}
