//! Solder-paste **dispensing positions** — an Adom-computed overlay.
//!
//! Standard PCB CAD shows a solid F.Paste aperture (the stencil opening). Adom's
//! paste-jet process doesn't use a stencil — it *jets* solder paste dot-by-dot. So
//! a footprint in the Adom ecosystem also carries the actual dispensing positions:
//! a grid of ~300 µm paste dots tiled across each aperture — and kept CLEAR of any
//! thermal via, so jetted paste never sits over a via barrel (it would wick down
//! the hole during reflow, starving the joint and causing voids). This module
//! parses the pads from a `.kicad_mod`, computes those dots, and emits two SVG
//! layer groups (aperture outlines + dispensing dots) in the same page coordinates
//! the footprint is rendered at, so they overlay exactly.

use regex::Regex;

/// One paste aperture: center, size (pre-rotation), rotation in degrees.
pub struct Aperture {
    pub cx: f64,
    pub cy: f64,
    pub w: f64,
    pub h: f64,
    pub rot: f64,
}

const DOT_DIA: f64 = 0.30; // 300 µm jetted paste dot
const DOT_PITCH: f64 = 0.25; // center-to-center spacing — slight intentional overlap of 0.30mm dots (john/adom-solder-ball-layer-standard)
const MARGIN: f64 = 0.06; // keep dots just inside the aperture edge
const MAX_DOTS: usize = 600; // safety cap so a huge thermal pad can't explode the DOM
/// Keep-out radius (mm) from a thermal-via centre — paste dots inside this are
/// dropped so jetted paste never lands over a via barrel. ≈ via-ring radius (0.30)
/// + dot radius (0.15) + a little clearance.
const VIA_KEEPOUT: f64 = 0.55;

/// Detect the exposed thermal pad among the paste apertures and return its
/// auto-generated thermal-via centres (footprint-local mm). MUST mirror
/// `fp_render::heatsink_vias` so the paste keep-out lines up with the rendered vias.
fn heatsink_vias(aps: &[Aperture]) -> Vec<(f64, f64)> {
    let ep = aps.iter()
        .filter(|a| a.w.min(a.h) >= 1.8 && { let r = a.w / a.h.max(0.001); (0.6..=1.67).contains(&r) })
        .max_by(|a, b| (a.w * a.h).partial_cmp(&(b.w * b.h)).unwrap_or(std::cmp::Ordering::Equal));
    let ep = match ep { Some(p) => p, None => return Vec::new() };
    let pitch = 1.2_f64;
    let margin = 0.6 / 2.0 + 0.15;
    let span = |dim: f64| (dim - 2.0 * margin).max(0.0);
    let nx = ((span(ep.w) / pitch).floor() as i32 + 1).max(1);
    let ny = ((span(ep.h) / pitch).floor() as i32 + 1).max(1);
    let x0 = ep.cx - (nx as f64 - 1.0) * pitch / 2.0;
    let y0 = ep.cy - (ny as f64 - 1.0) * pitch / 2.0;
    let mut out = Vec::new();
    for j in 0..ny { for i in 0..nx { out.push((x0 + i as f64 * pitch, y0 + j as f64 * pitch)); } }
    out
}

/// Parse SMD pad apertures (pads that get solder paste) from a `.kicad_mod`.
pub fn paste_apertures(kicad_mod: &str) -> Vec<Aperture> {
    let mut out = Vec::new();
    let bytes = kicad_mod.as_bytes();
    let at_re = Regex::new(r"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)(?:\s+(-?[\d.]+))?\)").unwrap();
    let size_re = Regex::new(r"\(size\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
    // Walk each "(pad " occurrence and take a balanced-paren slice as the block.
    let mut search = 0;
    while let Some(rel) = kicad_mod[search..].find("(pad ") {
        let start = search + rel;
        let mut depth = 0i32;
        let mut end = start;
        for i in start..kicad_mod.len() {
            match bytes[i] {
                b'(' => depth += 1,
                b')' => {
                    depth -= 1;
                    if depth == 0 {
                        end = i;
                        break;
                    }
                }
                _ => {}
            }
        }
        let block = &kicad_mod[start..=end.min(kicad_mod.len() - 1)];
        search = end + 1;
        // Only SMD pads get jetted paste. Skip NPTH/edge pads.
        let smd = block.contains(" smd ");
        if !smd {
            continue;
        }
        if let (Some(a), Some(s)) = (at_re.captures(block), size_re.captures(block)) {
            let cx = a[1].parse().unwrap_or(0.0);
            let cy = a[2].parse().unwrap_or(0.0);
            let rot = a.get(3).and_then(|m| m.as_str().parse().ok()).unwrap_or(0.0);
            let w: f64 = s[1].parse().unwrap_or(0.0);
            let h: f64 = s[2].parse().unwrap_or(0.0);
            if w <= 0.0 || h <= 0.0 {
                continue;
            }
            let has_paste = block.contains("F.Paste") || !block.contains("(layers"); // default smd → paste
            // The exposed thermal pad still gets jetted paste (in the via gaps) even
            // when the source omits F.Paste — the paddle needs solder to bond, and
            // Adom jets paste directly (no stencil), so F.Paste in the file isn't a
            // prerequisite. Matches the exposed-pad test in heatsink_vias().
            let square = { let r = w / h.max(0.001); (0.6..=1.67).contains(&r) };
            let is_exposed_pad = w.min(h) >= 1.8 && square;
            if has_paste || is_exposed_pad {
                out.push(Aperture { cx, cy, w, h, rot });
            }
        }
    }
    out
}

/// Emit two SVG `<g>` layer groups (apertures + 300 µm dispensing dots) in page
/// coordinates: footprint-local (ax,ay) maps to (ox+ax, oy+ay). Returns
/// `(apertures_group, dots_group, dot_count)`.
pub fn paste_overlay_svg(aps: &[Aperture], ox: f64, oy: f64) -> (String, String, usize) {
    let mut apert = String::from(r#"<g class="adom-layer-paste-apertures">"#);
    let mut dots = String::from(r#"<g class="adom-layer-paste-dots">"#);
    let mut n = 0usize;
    // Thermal-via centres on the exposed pad — paste dots that land on one are
    // dropped so jetted paste doesn't wick down the barrel during reflow.
    let vias = heatsink_vias(aps);

    for ap in aps {
        let (px, py) = (ox + ap.cx, oy + ap.cy);
        let (rad, cos, sin) = {
            let r = ap.rot.to_radians();
            (r, r.cos(), r.sin())
        };
        let _ = rad;
        // Aperture outline (rotated rect via transform).
        apert.push_str(&format!(
            r##"<rect x="{x:.3}" y="{y:.3}" width="{w:.3}" height="{h:.3}" rx="0.05" transform="rotate({rot:.2} {px:.3} {py:.3})" fill="none" stroke="#8aa0b8" stroke-width="0.03" stroke-dasharray="0.12 0.08" opacity="0.7"/>"##,
            x = px - ap.w / 2.0, y = py - ap.h / 2.0, w = ap.w, h = ap.h, rot = ap.rot, px = px, py = py,
        ));

        // Dispensing dot grid, centered in the aperture, inset by MARGIN.
        let usable_w = (ap.w - 2.0 * MARGIN - DOT_DIA).max(0.0);
        let usable_h = (ap.h - 2.0 * MARGIN - DOT_DIA).max(0.0);
        let nx = (usable_w / DOT_PITCH).floor() as i32 + 1;
        let ny = (usable_h / DOT_PITCH).floor() as i32 + 1;
        let span_x = (nx - 1) as f64 * DOT_PITCH;
        let span_y = (ny - 1) as f64 * DOT_PITCH;
        for ix in 0..nx {
            for iy in 0..ny {
                if n >= MAX_DOTS {
                    break;
                }
                // local offset within aperture (centered), then rotate.
                let lx = -span_x / 2.0 + ix as f64 * DOT_PITCH;
                let ly = -span_y / 2.0 + iy as f64 * DOT_PITCH;
                // Footprint-local dot centre (before the page offset) — used to test
                // against the thermal vias, which are in footprint-local coords.
                let local_dx = ap.cx + lx * cos - ly * sin;
                let local_dy = ap.cy + lx * sin + ly * cos;
                if vias.iter().any(|&(vx, vy)| ((local_dx - vx).powi(2) + (local_dy - vy).powi(2)).sqrt() < VIA_KEEPOUT) {
                    continue; // paste dot would sit over a via — skip it
                }
                let dx = px + lx * cos - ly * sin;
                let dy = py + lx * sin + ly * cos;
                dots.push_str(&format!(
                    r##"<circle cx="{dx:.3}" cy="{dy:.3}" r="{r:.3}" fill="#37c8a6" opacity="0.92"/>"##,
                    dx = dx, dy = dy, r = DOT_DIA / 2.0,
                ));
                n += 1;
            }
        }
    }
    apert.push_str("</g>");
    dots.push_str("</g>");
    (apert, dots, n)
}