//! GPU buffers for SDF text quads (Phase 2b of the text-shader initiative).
//!
//! Unlike images (one texture per entity), all glyphs share ONE atlas texture
//! and every glyph is a quad in a single instance-less vertex buffer, so the
//! whole layout draws in one call. Group 1 is just `{ atlas texture, sampler }`;
//! per-glyph colour and draw-order live in the vertices.
//!
//! Vertex positions use the same double-single relative-to-eye encoding as
//! wires/hatches/images so text stays precise at large drawing coordinates.

// Not yet driven by the render loop — that hook-up (Pipeline fields, per-frame
// upload, a render pass, and suppressing the old stroke text) is the final
// integration step, done with the app running to verify pixels.
#![allow(dead_code)]

use iced::wgpu;
use iced::wgpu::util::DeviceExt;

use crate::scene::text::glyph_quads::GlyphQuad;
use crate::scene::text::sdf_atlas::GlyphAtlas;

// ── Vertex ────────────────────────────────────────────────────────────────

#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TextVertex {
    pub pos: [f32; 3],
    pub pos_low: [f32; 3],
    pub uv: [f32; 2],
    pub color: [f32; 4],
    pub draw_depth: f32,
} // 52 bytes, no padding holes (all f32)

impl TextVertex {
    pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
        const ATTRS: &[wgpu::VertexAttribute] = &[
            wgpu::VertexAttribute {
                offset: std::mem::offset_of!(TextVertex, pos) as u64,
                shader_location: 0,
                format: wgpu::VertexFormat::Float32x3,
            },
            wgpu::VertexAttribute {
                offset: std::mem::offset_of!(TextVertex, pos_low) as u64,
                shader_location: 1,
                format: wgpu::VertexFormat::Float32x3,
            },
            wgpu::VertexAttribute {
                offset: std::mem::offset_of!(TextVertex, uv) as u64,
                shader_location: 2,
                format: wgpu::VertexFormat::Float32x2,
            },
            wgpu::VertexAttribute {
                offset: std::mem::offset_of!(TextVertex, color) as u64,
                shader_location: 3,
                format: wgpu::VertexFormat::Float32x4,
            },
            wgpu::VertexAttribute {
                offset: std::mem::offset_of!(TextVertex, draw_depth) as u64,
                shader_location: 4,
                format: wgpu::VertexFormat::Float32,
            },
        ];
        wgpu::VertexBufferLayout {
            array_stride: std::mem::size_of::<TextVertex>() as u64,
            step_mode: wgpu::VertexStepMode::Vertex,
            attributes: ATTRS,
        }
    }
}

/// Split an f64 into the double-single (high f32, low residual f32) pair the
/// shaders reconstruct relative-to-eye.
pub(crate) fn split_ds(v: f64) -> (f32, f32) {
    let high = v as f32;
    (high, (v - high as f64) as f32)
}

/// Append the two triangles (6 vertices) for each glyph quad of one text run
/// into `out`, placing the run-local quad corners into world space:
/// `world = corner * anno + origin`, then double-single split.
///
/// UV mapping (atlas row 0 = top; quad corners are BL, BR, TR, TL):
///   BL -> (u_min, v_max)  BR -> (u_max, v_max)
///   TR -> (u_max, v_min)  TL -> (u_min, v_min)
pub fn push_glyph_vertices(
    out: &mut Vec<TextVertex>,
    quads: &[GlyphQuad],
    origin: [f64; 3],
    anno: f64,
    color: [f32; 4],
    draw_depth: f32,
) {
    for q in quads {
        let mk = |ci: usize, uv: [f32; 2]| -> TextVertex {
            let c = q.corners[ci];
            let wx = c[0] as f64 * anno + origin[0];
            let wy = c[1] as f64 * anno + origin[1];
            let wz = origin[2];
            let (xh, xl) = split_ds(wx);
            let (yh, yl) = split_ds(wy);
            let (zh, zl) = split_ds(wz);
            TextVertex {
                pos: [xh, yh, zh],
                pos_low: [xl, yl, zl],
                uv,
                color,
                draw_depth,
            }
        };
        let bl = [q.uv_min[0], q.uv_max[1]];
        let br = [q.uv_max[0], q.uv_max[1]];
        let tr = [q.uv_max[0], q.uv_min[1]];
        let tl = [q.uv_min[0], q.uv_min[1]];
        out.push(mk(0, bl));
        out.push(mk(1, br));
        out.push(mk(2, tr));
        out.push(mk(0, bl));
        out.push(mk(2, tr));
        out.push(mk(3, tl));
    }
}

/// Slide every glyph vertex by a world-space delta, re-splitting the
/// double-single position. Lets a grip drag move already-shaped text by
/// translating the drag-start glyphs each frame instead of re-tessellating
/// (re-shaping) the run on every cursor move (issue #316).
pub fn translate_verts(verts: &[TextVertex], delta: [f64; 3]) -> Vec<TextVertex> {
    verts
        .iter()
        .map(|v| {
            let wx = v.pos[0] as f64 + v.pos_low[0] as f64 + delta[0];
            let wy = v.pos[1] as f64 + v.pos_low[1] as f64 + delta[1];
            let wz = v.pos[2] as f64 + v.pos_low[2] as f64 + delta[2];
            let (xh, xl) = split_ds(wx);
            let (yh, yl) = split_ds(wy);
            let (zh, zl) = split_ds(wz);
            TextVertex {
                pos: [xh, yh, zh],
                pos_low: [xl, yl, zl],
                ..*v
            }
        })
        .collect()
}

// ── Atlas texture ───────────────────────────────────────────────────────────

/// The shared glyph atlas on the GPU: a single-channel (R8) SDF texture plus
/// its sampler and bind group.
pub struct TextAtlasGpu {
    pub bind_group: wgpu::BindGroup,
    _texture: wgpu::Texture,
    _sampler: wgpu::Sampler,
}

impl TextAtlasGpu {
    /// Bind-group layout for group 1: `{ R8 atlas texture, linear sampler }`.
    pub fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("text.atlas.bgl"),
            entries: &[
                wgpu::BindGroupLayoutEntry {
                    binding: 0,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Texture {
                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
                        view_dimension: wgpu::TextureViewDimension::D2,
                        multisampled: false,
                    },
                    count: None,
                },
                wgpu::BindGroupLayoutEntry {
                    binding: 1,
                    visibility: wgpu::ShaderStages::FRAGMENT,
                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
                    count: None,
                },
            ],
        })
    }

    /// Upload the atlas texels (R8Unorm) and build the bind group.
    pub fn upload(
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        atlas: &GlyphAtlas,
        bgl1: &wgpu::BindGroupLayout,
    ) -> Self {
        let (w, h) = (atlas.width(), atlas.height());
        let texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("text.atlas.texture"),
            size: wgpu::Extent3d {
                width: w,
                height: h,
                depth_or_array_layers: 1,
            },
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::R8Unorm,
            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        });
        queue.write_texture(
            texture.as_image_copy(),
            atlas.data(),
            wgpu::TexelCopyBufferLayout {
                offset: 0,
                bytes_per_row: Some(w), // R8: 1 byte per texel
                rows_per_image: Some(h),
            },
            wgpu::Extent3d {
                width: w,
                height: h,
                depth_or_array_layers: 1,
            },
        );
        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("text.atlas.sampler"),
            address_mode_u: wgpu::AddressMode::ClampToEdge,
            address_mode_v: wgpu::AddressMode::ClampToEdge,
            address_mode_w: wgpu::AddressMode::ClampToEdge,
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            mipmap_filter: wgpu::FilterMode::Nearest,
            ..Default::default()
        });
        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("text.atlas.bind_group"),
            layout: bgl1,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(&view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::Sampler(&sampler),
                },
            ],
        });
        Self {
            bind_group,
            _texture: texture,
            _sampler: sampler,
        }
    }
}

// ── Pipeline ─────────────────────────────────────────────────────────────────

/// Build the base and highlight text pipelines. The base mirrors the image
/// pipeline (`LessEqual` depth with write); the highlight variant always draws
/// without changing depth, matching the selected-wire xray overlay.
/// `frame_bgl` is group 0 (shared uniforms), `atlas_bgl` is group 1 (the atlas).
pub fn create_pipelines(
    device: &wgpu::Device,
    frame_bgl: &wgpu::BindGroupLayout,
    atlas_bgl: &wgpu::BindGroupLayout,
    color_format: wgpu::TextureFormat,
    sample_count: u32,
) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) {
    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
        label: Some("text.wgsl"),
        source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/text.wgsl").into()),
    });
    let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
        label: Some("text.pipeline.layout"),
        bind_group_layouts: &[frame_bgl, atlas_bgl],
        push_constant_ranges: &[],
    });
    let create = |label, depth_write_enabled, depth_compare| {
        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some(label),
            layout: Some(&layout),
            vertex: wgpu::VertexState {
                module: &shader,
                entry_point: Some("vs_main"),
                buffers: &[TextVertex::layout()],
                compilation_options: Default::default(),
            },
            fragment: Some(wgpu::FragmentState {
                module: &shader,
                entry_point: Some("fs_main"),
                targets: &[Some(wgpu::ColorTargetState {
                    format: color_format,
                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
                    write_mask: wgpu::ColorWrites::ALL,
                })],
                compilation_options: Default::default(),
            }),
            primitive: wgpu::PrimitiveState {
                topology: wgpu::PrimitiveTopology::TriangleList,
                ..Default::default()
            },
            depth_stencil: Some(wgpu::DepthStencilState {
                format: wgpu::TextureFormat::Depth24PlusStencil8,
                depth_write_enabled,
                depth_compare,
                stencil: wgpu::StencilState::default(),
                bias: wgpu::DepthBiasState::default(),
            }),
            multisample: wgpu::MultisampleState {
                count: sample_count,
                mask: !0,
                alpha_to_coverage_enabled: false,
            },
            multiview: None,
            cache: None,
        })
    };
    (
        create("text.pipeline", true, wgpu::CompareFunction::LessEqual),
        create(
            "text.highlight.pipeline",
            false,
            wgpu::CompareFunction::Always,
        ),
    )
}

/// Upload a finished vertex list; `None` if empty.
pub fn upload_vertices(device: &wgpu::Device, verts: &[TextVertex]) -> Option<wgpu::Buffer> {
    if verts.is_empty() {
        return None;
    }
    Some(device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
        label: Some("text.vbuf"),
        contents: bytemuck::cast_slice(verts),
        usage: wgpu::BufferUsages::VERTEX,
    }))
}

// ── Tests ─────────────────────────────────────────────────────────────────

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

    fn recon(v: &TextVertex) -> [f64; 3] {
        [
            v.pos[0] as f64 + v.pos_low[0] as f64,
            v.pos[1] as f64 + v.pos_low[1] as f64,
            v.pos[2] as f64 + v.pos_low[2] as f64,
        ]
    }

    #[test]
    fn quad_places_corners_and_uv() {
        let q = GlyphQuad {
            corners: [[0.0, 0.0], [8.0, 0.0], [8.0, 9.0], [0.0, 9.0]],
            uv_min: [0.10, 0.20],
            uv_max: [0.30, 0.40],
        };
        let mut out = Vec::new();
        // Large origin exercises the double-single precision path.
        push_glyph_vertices(&mut out, &[q], [1_000_000.0, 2_000_000.0, 0.0], 1.0, [1.0; 4], 0.0);
        assert_eq!(out.len(), 6, "two triangles per glyph");

        // Vertex 0 = BL corner (0,0) -> world origin, uv = (u_min, v_max).
        let bl = recon(&out[0]);
        assert!((bl[0] - 1_000_000.0).abs() < 1e-2 && (bl[1] - 2_000_000.0).abs() < 1e-2);
        assert_eq!(out[0].uv, [0.10, 0.40]);

        // Vertex 2 = TR corner (8,9) -> origin + (8,9), uv = (u_max, v_min).
        let tr = recon(&out[2]);
        assert!((tr[0] - 1_000_008.0).abs() < 1e-2 && (tr[1] - 2_000_009.0).abs() < 1e-2);
        assert_eq!(out[2].uv, [0.30, 0.20]);
    }

    #[test]
    fn annotation_scale_scales_corner_offsets() {
        let q = GlyphQuad {
            corners: [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]],
            uv_min: [0.0, 0.0],
            uv_max: [1.0, 1.0],
        };
        let mut out = Vec::new();
        push_glyph_vertices(&mut out, &[q], [0.0, 0.0, 0.0], 2.0, [1.0; 4], 0.0);
        // TR corner (10,10) at anno 2.0 -> (20, 20).
        let tr = recon(&out[2]);
        assert!((tr[0] - 20.0).abs() < 1e-4 && (tr[1] - 20.0).abs() < 1e-4);
    }

    #[test]
    fn empty_run_yields_no_vertices() {
        let mut out = Vec::new();
        push_glyph_vertices(&mut out, &[], [0.0, 0.0, 0.0], 1.0, [1.0; 4], 0.0);
        assert!(out.is_empty());
    }
}
