use std::collections::HashMap;

use bone_text::{FontFace, ShapedLine, ShapedText, Shaper, TessellatedOutline, load_font};
use bone_types::SketchDimensionId;
use lyon_tessellation::FillTessellator;
use swash::{FontRef, scale::ScaleContext, zeno::Point as ZenoPoint};
use wgpu::util::DeviceExt;

use crate::RenderTargets;
use crate::camera::Camera2;
use crate::gpu::{Gpu, PICK_FORMAT};
use crate::pipelines::text_common::{shape_line, tessellate_at};
use crate::scene::SketchScene;
use crate::snapshot::{Style, TextStyle};

#[repr(C, align(16))]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct TextUniform {
    clip_from_world: [f32; 16],
    text_color: [f32; 4],
    pixels_per_mm: f32,
    _pad0: f32,
    _pad1: f32,
    _pad2: f32,
}

const UNIFORM_SIZE: u64 = core::mem::size_of::<TextUniform>() as u64;

#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct TextVertex {
    anchor_mm: [f32; 2],
    offset_px: [f32; 2],
    pick_id: u32,
    _pad: u32,
}

const VERTEX_STRIDE: u64 = core::mem::size_of::<TextVertex>() as u64;

struct Cached {
    text: String,
    size_px: f32,
    tess: TessellatedOutline,
}

impl Cached {
    fn matches(&self, text: &str, size_px: f32) -> bool {
        self.size_px.to_bits() == size_px.to_bits() && self.text == text
    }
}

pub struct TextPipeline {
    device: wgpu::Device,
    queue: wgpu::Queue,
    pipeline: wgpu::RenderPipeline,
    uniform_buffer: wgpu::Buffer,
    bind_group: wgpu::BindGroup,
    font: FontRef<'static>,
    shaper: Shaper,
    scale_context: ScaleContext,
    tessellator: FillTessellator,
    cache: HashMap<SketchDimensionId, Cached>,
}

impl TextPipeline {
    #[must_use]
    pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat) -> Self {
        let device = gpu.device().clone();
        let queue = gpu.queue().clone();
        let bind_group_layout = create_bind_group_layout(&device);
        let pipeline = create_pipeline(&device, &bind_group_layout, color_format);
        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("bone-render:text-uniform"),
            size: UNIFORM_SIZE,
            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
            mapped_at_creation: false,
        });
        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("bone-render:text-bg"),
            layout: &bind_group_layout,
            entries: &[wgpu::BindGroupEntry {
                binding: 0,
                resource: uniform_buffer.as_entire_binding(),
            }],
        });
        Self {
            device,
            queue,
            pipeline,
            uniform_buffer,
            bind_group,
            font: load_font(FontFace::Mono),
            shaper: Shaper::new(),
            scale_context: ScaleContext::new(),
            tessellator: FillTessellator::new(),
            cache: HashMap::new(),
        }
    }

    pub fn prepare(&mut self, scene: &SketchScene, style: &Style) {
        let size_px = style.text().font_size_px();
        let mut prev = std::mem::take(&mut self.cache);
        self.cache = scene.dimensions().iter().fold(
            HashMap::with_capacity(scene.dimensions().len()),
            |mut acc, d| {
                let id = d.dimension();
                let text = d.text();
                let entry = match prev.remove(&id) {
                    Some(c) if c.matches(text, size_px) => c,
                    _ => Cached {
                        text: text.to_owned(),
                        size_px,
                        tess: tessellate(
                            text,
                            size_px,
                            &self.font,
                            &mut self.shaper,
                            &mut self.scale_context,
                            &mut self.tessellator,
                        ),
                    },
                };
                acc.insert(id, entry);
                acc
            },
        );
    }

    pub fn draw(
        &self,
        encoder: &mut wgpu::CommandEncoder,
        targets: RenderTargets<'_>,
        camera: Camera2,
        style: &Style,
        scene: &SketchScene,
    ) {
        let (vertices, indices) = assemble_geometry(scene, &self.cache);
        if indices.is_empty() {
            return;
        }
        let uniform = build_uniform(camera, style.text());
        self.queue
            .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniform));
        let vertex_buffer = self
            .device
            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some("bone-render:text-vertices"),
                contents: bytemuck::cast_slice(&vertices),
                usage: wgpu::BufferUsages::VERTEX,
            });
        let index_buffer = self
            .device
            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
                label: Some("bone-render:text-indices"),
                contents: bytemuck::cast_slice(&indices),
                usage: wgpu::BufferUsages::INDEX,
            });
        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("bone-render:text-pass"),
            color_attachments: &[
                Some(wgpu::RenderPassColorAttachment {
                    view: targets.color,
                    resolve_target: None,
                    depth_slice: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Load,
                        store: wgpu::StoreOp::Store,
                    },
                }),
                Some(wgpu::RenderPassColorAttachment {
                    view: targets.pick,
                    resolve_target: None,
                    depth_slice: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Load,
                        store: wgpu::StoreOp::Store,
                    },
                }),
            ],
            depth_stencil_attachment: None,
            timestamp_writes: None,
            occlusion_query_set: None,
            multiview_mask: None,
        });
        pass.set_pipeline(&self.pipeline);
        pass.set_bind_group(0, &self.bind_group, &[]);
        pass.set_vertex_buffer(0, vertex_buffer.slice(..));
        pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
        let len = indices.len();
        let Ok(count) = u32::try_from(len) else {
            panic!("text index count {len} exceeds u32::MAX");
        };
        pass.draw_indexed(0..count, 0, 0..1);
    }

    #[must_use]
    pub fn cache_len(&self) -> usize {
        self.cache.len()
    }
}

impl core::fmt::Debug for TextPipeline {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("TextPipeline")
            .field("cache_len", &self.cache.len())
            .finish_non_exhaustive()
    }
}

const VERTEX_ATTRS: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![
    0 => Float32x2,
    1 => Float32x2,
    2 => Uint32,
];

fn create_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
        label: Some("bone-render:text-bgl"),
        entries: &[wgpu::BindGroupLayoutEntry {
            binding: 0,
            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
            ty: wgpu::BindingType::Buffer {
                ty: wgpu::BufferBindingType::Uniform,
                has_dynamic_offset: false,
                min_binding_size: wgpu::BufferSize::new(UNIFORM_SIZE),
            },
            count: None,
        }],
    })
}

fn create_pipeline(
    device: &wgpu::Device,
    bind_group_layout: &wgpu::BindGroupLayout,
    color_format: wgpu::TextureFormat,
) -> wgpu::RenderPipeline {
    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
        label: Some("bone-render:text-shader"),
        source: wgpu::ShaderSource::Wgsl(include_str!("text.wgsl").into()),
    });
    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
        label: Some("bone-render:text-layout"),
        bind_group_layouts: &[Some(bind_group_layout)],
        immediate_size: 0,
    });
    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
        label: Some("bone-render:text-pipeline"),
        layout: Some(&pipeline_layout),
        vertex: wgpu::VertexState {
            module: &shader,
            entry_point: Some("vs"),
            compilation_options: wgpu::PipelineCompilationOptions::default(),
            buffers: &[wgpu::VertexBufferLayout {
                array_stride: VERTEX_STRIDE,
                step_mode: wgpu::VertexStepMode::Vertex,
                attributes: &VERTEX_ATTRS,
            }],
        },
        fragment: Some(wgpu::FragmentState {
            module: &shader,
            entry_point: Some("fs"),
            compilation_options: wgpu::PipelineCompilationOptions::default(),
            targets: &[
                Some(wgpu::ColorTargetState {
                    format: color_format,
                    blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
                    write_mask: wgpu::ColorWrites::ALL,
                }),
                Some(wgpu::ColorTargetState {
                    format: PICK_FORMAT,
                    blend: None,
                    write_mask: wgpu::ColorWrites::ALL,
                }),
            ],
        }),
        primitive: wgpu::PrimitiveState {
            topology: wgpu::PrimitiveTopology::TriangleList,
            strip_index_format: None,
            front_face: wgpu::FrontFace::Ccw,
            cull_mode: None,
            polygon_mode: wgpu::PolygonMode::Fill,
            conservative: false,
            unclipped_depth: false,
        },
        depth_stencil: None,
        multisample: wgpu::MultisampleState::default(),
        multiview_mask: None,
        cache: None,
    })
}

#[allow(clippy::cast_possible_truncation)]
fn build_uniform(camera: Camera2, text: TextStyle) -> TextUniform {
    TextUniform {
        clip_from_world: camera.clip_from_world_mm(),
        text_color: text.color().to_rgba_array(),
        pixels_per_mm: camera.zoom().value() as f32,
        _pad0: 0.0,
        _pad1: 0.0,
        _pad2: 0.0,
    }
}

#[allow(clippy::cast_possible_truncation)]
fn assemble_geometry(
    scene: &SketchScene,
    cache: &HashMap<SketchDimensionId, Cached>,
) -> (Vec<TextVertex>, Vec<u32>) {
    scene
        .dimensions()
        .iter()
        .fold((Vec::new(), Vec::new()), |(mut vs, mut is), d| {
            let Some(cached) = cache.get(&d.dimension()) else {
                return (vs, is);
            };
            let (ax, ay) = d.anchor_mm().coords_mm();
            let anchor = [ax as f32, ay as f32];
            let pick_id = d.pick().raw();
            let Ok(base) = u32::try_from(vs.len()) else {
                panic!("text vertex count exceeds u32::MAX");
            };
            vs.extend(cached.tess.vertices_px.iter().map(|offset| TextVertex {
                anchor_mm: anchor,
                offset_px: *offset,
                pick_id,
                _pad: 0,
            }));
            is.extend(cached.tess.indices.iter().map(|idx| base + idx));
            (vs, is)
        })
}

fn tessellate(
    text: &str,
    size_px: f32,
    font: &FontRef<'_>,
    shaper: &mut Shaper,
    scale_ctx: &mut ScaleContext,
    fill: &mut FillTessellator,
) -> TessellatedOutline {
    let layout = shape_line(text, size_px, FontFace::Mono, shaper);
    let metrics = font.metrics(&[]).scale(size_px);
    let center = label_center(&layout, metrics.cap_height);
    tessellate_at(&layout, font, scale_ctx, fill, center)
}

fn label_center(layout: &ShapedText, cap_height: f32) -> ZenoPoint {
    let visible_advance = layout
        .lines
        .first()
        .map_or(0.0, ShapedLine::visible_advance_px);
    ZenoPoint::new(-visible_advance * 0.5, -cap_height * 0.5)
}
