// Wipeout GPU buffers — the legacy per-primitive fill renderer, now used ONLY
// for wipeout masks (solid fills drawn after wires to hide them). Real hatch
// fills go through the canonical `hatch_gpu.rs` batched renderer.
//
// It retains a general pattern/gradient capability (mode 0/2 below) and the
// `MAX_FAMILIES = 16` cap, but wipeouts are always solid (mode 1, zero pattern
// families), so the cap never truncates anything in practice. The family code
// is kept only as a latent non-batched fallback; do NOT read `MAX_FAMILIES` as
// a limit on visible hatch fills.
//
// Group 1 bindings per fill:
//   binding 0 — HatchUniformData  (64 bytes)   mode, color, angle_offset, scale, gradient params
//   binding 1 — BoundaryData      (16384 bytes) boundary polygon vertices
//   binding 2 — FamilyBatchData   (1296 bytes)  up to 16 line families + 128 dash values (unused for wipeouts)

use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily, MAX_HATCH_BOUNDARY_VERTS};
use iced::wgpu;
use iced::wgpu::util::DeviceExt;

// ── Constants ──────────────────────────────────────────────────────────────

pub const MAX_FAMILIES: usize = 16;
pub const MAX_DASHES: usize = 128;

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

#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct HatchVertex {
    pub pos: [f32; 3],
    pub _pad: f32,
}

impl HatchVertex {
    pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
        wgpu::VertexBufferLayout {
            array_stride: std::mem::size_of::<HatchVertex>() as u64,
            step_mode: wgpu::VertexStepMode::Vertex,
            attributes: &[wgpu::VertexAttribute {
                offset: 0,
                shader_location: 0,
                format: wgpu::VertexFormat::Float32x3,
            }],
        }
    }
}

// ── Uniform structs ───────────────────────────────────────────────────────

/// Per-hatch parameters (binding 0).  Must be 96 bytes (16-byte aligned).
///
/// `mode` encoding:
///   0 → Pattern  (families in FamilyBatchData)
///   1 → Solid
///   2 → Gradient (grad_cos/sin/min/range used)
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct HatchUniformData {
    pub color: [f32; 4],     //  0: primary RGBA / gradient start
    pub color2: [f32; 4],    // 16: gradient end color
    pub mode: u32,           // 32: 0=pattern, 1=solid, 2=gradient
    pub vertex_count: u32,   // 36: boundary vertex count
    pub angle_offset: f32,   // 40: pattern rotation (radians)
    pub scale: f32,          // 44: pattern scale multiplier
    pub grad_cos: f32,       // 48: gradient direction cos
    pub grad_sin: f32,       // 52: gradient direction sin
    pub grad_min: f32,       // 56: gradient proj_min
    pub grad_range: f32,     // 60: gradient proj_range
    pub origin: [f32; 2],    // 64: hatch-local origin (boundary AABB centre),
    //                       //     high half of the double-single anchor.
    //                       //     The pattern fragment shader subtracts this
    //                       //     from xz before the perp/perp_step quotient
    //                       //     so f32 doesn't catastrophically cancel when
    //                       //     world coords are large and pattern spacing
    //                       //     is small (e.g. UTM drawing + sub-mm hatch).
    pub origin_low: [f32; 2], // 72: anchor low residual (double-single) so the
    //                        //     vertex clip position stays precise at UTM scale.
    pub draw_depth: f32,      // 80: signed (-1,1) draw-order bias
    pub _pad: [f32; 3],       // 84..96
} // total 96 bytes

/// Boundary polygon (binding 1).  Matches WGSL `Boundary`.
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct BoundaryData {
    pub verts: [[f32; 4]; MAX_HATCH_BOUNDARY_VERTS], // world XY in .xy, .zw unused
}

/// One line family packed for the shader (48 bytes).
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct LineFamilyGpu {
    pub cos_a: f32,      // cos(angle_rad)
    pub sin_a: f32,      // sin(angle_rad)
    pub x0: f32,         // family origin x
    pub y0: f32,         // family origin y
    pub dx: f32,         // step vector x
    pub dy: f32,         // step vector y
    pub perp_step: f32,  // -dx*sin_a + dy*cos_a  (perpendicular spacing)
    pub along_step: f32, //  dx*cos_a + dy*sin_a  (along-line phase shift per step)
    pub line_width: f32, // |perp_step| * 0.08
    pub period: f32,     // sum of |dash values|  (0 = solid)
    pub n_dashes: u32,   // number of dash entries (0 = solid)
    pub dash_off: u32,   // index into FamilyBatchData::dash_values
}

/// All line families + dash values for one hatch (binding 2).
/// Total size: 16×48 + 32×16 + 4×4 = 768 + 512 + 16 = 1296 bytes.
/// dash_values is [f32; 4]×32 so each element is vec4<f32> (16-byte stride, valid in uniform).
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct FamilyBatchData {
    pub families: [LineFamilyGpu; MAX_FAMILIES], // 768 bytes
    pub dash_values: [[f32; 4]; 32],             // 512 bytes (128 f32s as 32×vec4)
    pub n_families: u32,                         //   4 bytes
    pub _pad: [u32; 3],                          //  12 bytes
} // 1296 bytes total

// ── Per-hatch GPU handle ───────────────────────────────────────────────────

pub struct WipeoutGpu {
    pub vertex_buffer: wgpu::Buffer,
    pub bind_group: wgpu::BindGroup,
    /// World-space XY bounding rect [min_x, min_y, max_x, max_y] of the
    /// boundary polygon. Used by the per-frame LOD pass to skip hatches
    /// whose entire footprint projects to less than ~2 px (Phase 3.3).
    pub world_aabb: [f32; 4],
    _uniform_buf: wgpu::Buffer,
    _boundary_buf: wgpu::Buffer,
    _family_buf: wgpu::Buffer,
}

impl WipeoutGpu {
    pub fn new(device: &wgpu::Device, model: &HatchModel, bgl1: &wgpu::BindGroupLayout) -> Self {
        // ── Decode pattern mode ──────────────────────────────────────────
        let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
            HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
            HatchPattern::Pattern(_) => (0u32, [0.0f32; 4], 0.0f32, 0.0f32),
            HatchPattern::Gradient { angle_deg, color2, .. } => {
                let r = angle_deg.to_radians();
                (2u32, *color2, r.cos(), r.sin())
            }
        };

        // ── Bounding box ─────────────────────────────────────────────────
        let (mut min_x, mut max_x, mut min_y, mut max_y) = (
            f32::INFINITY,
            f32::NEG_INFINITY,
            f32::INFINITY,
            f32::NEG_INFINITY,
        );
        for &[x, y] in model.boundary.iter() {
            if !x.is_finite() || !y.is_finite() {
                continue;
            }
            min_x = min_x.min(x);
            max_x = max_x.max(x);
            min_y = min_y.min(y);
            max_y = max_y.max(y);
        }

        let max_spacing = match &model.pattern {
            HatchPattern::Pattern(families) => families
                .iter()
                .map(|f| perp_spacing(f).abs())
                .fold(0.0f32, f32::max),
            _ => 5.0,
        };
        let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt();
        let pad = (diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0);
        // ── Pattern phase anchor.
        //
        // Boundary verts are already stored as small f32 offsets from
        // `model.world_origin` (an f64 anchor near the AABB centre), so
        // the in_polygon ray-cast and the pattern math both run at full
        // f32 precision in the fragment shader. The remaining question
        // is *which* offset-rel point we hand to the vertex shader as
        // `h.origin` (so the quad lands at the correct WCS position
        // after `view_proj * (v.pos + h.origin)`).
        //
        // For pattern alignment across adjacent hatches with the same
        // family, snap `h.origin` to the family's perp/along grid in
        // f64. Without the snap, each hatch's pattern phase is per-hatch
        // and stripes don't continue across hatch boundaries (you see
        // pattern misalignment along a road etc).
        let mut origin = model.world_origin;
        if let HatchPattern::Pattern(families) = &model.pattern {
            if let Some(fam) = families.first() {
                // Must mirror the shader's pattern math exactly. QCAD PAT
                // convention (matching build_family_batch): `dy` is the
                // perpendicular spacing and `dx` is the along-line phase
                // shift, both in family-local space. Multiplied by
                // `model.scale` in the fragment shader. Combined angle
                // (family + pattern_angle) rotates the perp/along axes.
                let angle = (fam.angle_deg.to_radians() + model.angle_offset) as f64;
                let cos_a = angle.cos();
                let sin_a = angle.sin();
                let scale = model.scale as f64;
                let perp_step = fam.dy as f64 * scale;
                let along_step = fam.dx as f64 * scale;
                let o_perp = -origin[0] * sin_a + origin[1] * cos_a;
                let o_along = origin[0] * cos_a + origin[1] * sin_a;
                let snapped_perp = if perp_step.abs() > 1e-12 {
                    (o_perp / perp_step).round() * perp_step
                } else {
                    o_perp
                };
                let snapped_along = if along_step.abs() > 1e-12 {
                    (o_along / along_step).round() * along_step
                } else {
                    o_along
                };
                origin = [
                    -snapped_perp * sin_a + snapped_along * cos_a,
                    snapped_perp * cos_a + snapped_along * sin_a,
                ];
            }
        }
        // Boundary buffer + quad now live in "snap-shifted" hatch-local
        // coords: subtract any drift between model.world_origin and the
        // snapped origin so the in_polygon ray-cast in fragment shader
        // (which reads the boundary verts straight) lines up with v.xz.
        let drift = [
            (model.world_origin[0] - origin[0]) as f32,
            (model.world_origin[1] - origin[1]) as f32,
        ];
        let (x0, x1, y0, y1) = (
            min_x + drift[0] - pad,
            max_x + drift[0] + pad,
            min_y + drift[1] - pad,
            max_y + drift[1] + pad,
        );

        let quad = [
            HatchVertex {
                pos: [x0, y0, 0.0],
                _pad: 0.0,
            },
            HatchVertex {
                pos: [x1, y0, 0.0],
                _pad: 0.0,
            },
            HatchVertex {
                pos: [x1, y1, 0.0],
                _pad: 0.0,
            },
            HatchVertex {
                pos: [x0, y0, 0.0],
                _pad: 0.0,
            },
            HatchVertex {
                pos: [x1, y1, 0.0],
                _pad: 0.0,
            },
            HatchVertex {
                pos: [x0, y1, 0.0],
                _pad: 0.0,
            },
        ];
        let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("hatch.vbuf"),
            contents: bytemuck::cast_slice(&quad),
            usage: wgpu::BufferUsages::VERTEX,
        });

        // ── Gradient: projection range (in snapped-local space) ──────────
        let (grad_min, grad_range) = if mode == 2 {
            let projs: Vec<f32> = model
                .boundary
                .iter()
                .filter(|v| v[0].is_finite() && v[1].is_finite())
                .map(|&[x, y]| (x + drift[0]) * grad_cos + (y + drift[1]) * grad_sin)
                .collect();
            if projs.is_empty() {
                (0.0, 1.0)
            } else {
                let proj_min = projs.iter().cloned().fold(f32::INFINITY, f32::min);
                let proj_max = projs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
                (proj_min, (proj_max - proj_min).max(1e-4))
            }
        } else {
            (0.0, 1.0)
        };

        // ── HatchUniformData ─────────────────────────────────────────────
        let n = model.boundary.len().min(MAX_HATCH_BOUNDARY_VERTS);
        let uniform_data = HatchUniformData {
            color: model.color,
            color2,
            mode,
            vertex_count: n as u32,
            angle_offset: model.angle_offset,
            scale: model.scale,
            grad_cos,
            grad_sin,
            grad_min,
            grad_range,
            origin: [origin[0] as f32, origin[1] as f32],
            origin_low: [
                (origin[0] - origin[0] as f32 as f64) as f32,
                (origin[1] - origin[1] as f32 as f64) as f32,
            ],
            draw_depth: model.draw_depth,
            _pad: [0.0; 3],
        };

        // ── BoundaryData (in snapped-local space) ────────────────────────
        let mut boundary_data = BoundaryData {
            verts: [[0.0; 4]; MAX_HATCH_BOUNDARY_VERTS],
        };
        for (i, &[x, y]) in model
            .boundary
            .iter()
            .take(MAX_HATCH_BOUNDARY_VERTS)
            .enumerate()
        {
            // Path-break separators go up as the finite GPU sentinel, not
            // NaN — the shader NaN self-compare is folded to `true` on some
            // drivers (#386, #416) — and must not be shifted by `drift`.
            if x.is_finite() && y.is_finite() {
                boundary_data.verts[i] = [x + drift[0], y + drift[1], 0.0, 0.0];
            } else {
                let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP;
                boundary_data.verts[i] = [s, s, 0.0, 0.0];
            }
        }

        // ── FamilyBatchData ───────────────────────────────────────────────
        let family_batch = build_family_batch(&model.pattern);

        // ── GPU buffers ───────────────────────────────────────────────────
        let _uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("hatch.uniforms"),
            contents: bytemuck::bytes_of(&uniform_data),
            usage: wgpu::BufferUsages::UNIFORM,
        });
        let _boundary_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("hatch.boundary"),
            contents: bytemuck::bytes_of(&boundary_data),
            usage: wgpu::BufferUsages::UNIFORM,
        });
        let _family_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
            label: Some("hatch.families"),
            contents: bytemuck::bytes_of(&family_batch),
            usage: wgpu::BufferUsages::UNIFORM,
        });

        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("hatch.bind_group1"),
            layout: bgl1,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: _uniform_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: _boundary_buf.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: _family_buf.as_entire_binding(),
                },
            ],
        });

        // Boundary vertices are stored as f32 offsets from
        // `model.world_origin` (so f32 keeps precision at UTM-scale
        // WCS). The pipeline-level frustum cull (Phase 2.3
        // `aabb_offscreen`) projects through `view_proj`, which expects
        // local-space (world_offset-subtracted) coordinates — the same
        // space as `world_origin`. Add the origin back so `world_aabb`
        // is in that absolute local space rather than relative to the
        // anchor. (Sub-pixel LOD via `aabb_below_pixel` is unaffected:
        // it measures a projected diagonal, invariant under translation.)
        let ox = model.world_origin[0] as f32;
        let oy = model.world_origin[1] as f32;
        let world_aabb = if min_x.is_finite() && min_y.is_finite() {
            [min_x + ox, min_y + oy, max_x + ox, max_y + oy]
        } else {
            [min_x, min_y, max_x, max_y]
        };

        Self {
            vertex_buffer,
            bind_group,
            world_aabb,
            _uniform_buf,
            _boundary_buf,
            _family_buf,
        }
    }
}

// ── Helpers ───────────────────────────────────────────────────────────────

/// Perpendicular spacing between adjacent parallel lines.
/// QCAD PAT format stores (dx, dy) in line-local coordinates: dy = perpendicular, dx = along-line.
fn perp_spacing(f: &PatFamily) -> f32 {
    f.dy
}

/// Pack pattern families into the GPU batch struct.
fn build_family_batch(pattern: &HatchPattern) -> FamilyBatchData {
    let mut batch = FamilyBatchData {
        families: [LineFamilyGpu {
            cos_a: 0.0,
            sin_a: 0.0,
            x0: 0.0,
            y0: 0.0,
            dx: 0.0,
            dy: 0.0,
            perp_step: 0.0,
            along_step: 0.0,
            line_width: 0.0,
            period: 0.0,
            n_dashes: 0,
            dash_off: 0,
        }; MAX_FAMILIES],
        dash_values: [[0.0; 4]; 32],
        n_families: 0,
        _pad: [0; 3],
    };

    let HatchPattern::Pattern(families) = pattern else {
        return batch;
    };

    let mut dash_cursor: usize = 0;

    for (fi, family) in families.iter().take(MAX_FAMILIES).enumerate() {
        let angle_r = family.angle_deg.to_radians();
        let cos_a = angle_r.cos();
        let sin_a = angle_r.sin();
        // QCAD PAT local-frame convention: dy = perpendicular spacing, dx = along-line phase shift.
        let perp_step = family.dy;
        let along_step = family.dx;
        let line_width = 0.0_f32; // unused: shader uses screen-space derivative for 1px lines

        // Dash pattern: collect up to available space.
        let n_avail = MAX_DASHES.saturating_sub(dash_cursor);
        let n_dashes = family.dashes.len().min(n_avail);
        let period: f32 = family.dashes[..n_dashes].iter().map(|d| d.abs()).sum();

        let dash_off = dash_cursor as u32;
        for &d in &family.dashes[..n_dashes] {
            batch.dash_values[dash_cursor / 4][dash_cursor % 4] = d;
            dash_cursor += 1;
        }

        batch.families[fi] = LineFamilyGpu {
            cos_a,
            sin_a,
            x0: family.x0,
            y0: family.y0,
            dx: family.dx,
            dy: family.dy,
            perp_step,
            along_step,
            line_width,
            period: if n_dashes > 0 { period } else { 0.0 },
            n_dashes: n_dashes as u32,
            dash_off,
        };
        batch.n_families += 1;

        if dash_cursor >= MAX_DASHES {
            break;
        }
    }

    batch
}
