//! SVG theming for symbols + footprints.
//!
//! Symbol theming ports the regex-replacement pass from
//! gallia/symbol-creator/lib/svg-theme.js applyDarkTheme() into Rust.
//! It transforms KiCad CLI's default-themed symbol SVG into the Adom
//! dark palette (canvas #0d1117, body gradient, white pin labels,
//! amber group labels). The `<style>` body is wrapped in CDATA so the
//! output is valid standalone XML — applyDarkTheme as written embeds a
//! literal `<g class="stroked-text">` inside a CSS comment, which trips
//! strict XML parsers when SVG is loaded via <object> or as a wiki
//! asset.
//!
//! Footprint theming keeps kicad-cli's native KiCad palette (the same
//! red/yellow/magenta the FpView app shows) and just paints them on
//! the canonical FpView dark canvas with a 20-unit PCB-style grid
//! (rgba(255,255,255,0.03) lines), mirroring
//! gallia/viewer/kicad-footprint-viewer.js generateFootprintViewer().

use regex::Regex;

const KICAD_BODY_STROKE: &str = "#840000";
const KICAD_BODY_FILL: &str = "#ffffc2";
const KICAD_TEXT_STROKE: &str = "#A90000";
const KICAD_HIDDEN_TEXT: &str = "#006464";

// Adom dark theme target colors.
const CANVAS_BG: &str = "#0d1117";
const BODY_STROKE: &str = "#30363d";
const BODY_FILL: &str = "#1a2332";
const BODY_FILL_GRAD: &str = "#141c27";
const PIN_WIRE: &str = "#4a5568";
const PIN_NAME: &str = "#ffffff";
const REF_TEXT: &str = "#484f58";
const PIN_DOT: &str = "#4a5568";

/// Apply the Adom dark theme to a KiCad-CLI-exported symbol SVG.
pub fn apply_dark_theme_to_symbol(raw: &str) -> String {
    let mut s: String = raw
        .trim_start_matches('\u{feff}')
        .replace("\r\n", "\n");

    // Strip XML decl and DOCTYPE.
    s = strip_first_match(&s, r#"<\?xml[^?]*\?>"#);
    s = strip_first_match(&s, r#"<!DOCTYPE[^>]*>"#);
    s = strip_all_matches(&s, r#"<title>[^<]*</title>"#);
    s = strip_all_matches(&s, r#"<desc>[^<]*</desc>"#);
    let s = s.trim();

    // Replace KiCad's default white background rect (if present) — older
    // KiCad outputs may include one, newer ones don't. We also unconditionally
    // inject a viewBox-sized canvas <rect> below so the symbol always has
    // an opaque Adom-bg behind it.
    let bg_rect_re = Regex::new(r#"(<rect[^>]*fill=")(?i)white("[^>]*>)"#).unwrap();
    let s = bg_rect_re.replace(s, format!("${{1}}{}${{2}}", CANVAS_BG)).to_string();

    // Pull viewBox so we know how big to make the canvas bg rect.
    let (vb_x, vb_y, vb_w, vb_h) =
        match Regex::new(r#"viewBox="(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)""#)
            .unwrap()
            .captures(&s)
        {
            Some(c) => (
                c[1].parse::<f64>().unwrap_or(0.0),
                c[2].parse::<f64>().unwrap_or(0.0),
                c[3].parse::<f64>().unwrap_or(100.0),
                c[4].parse::<f64>().unwrap_or(100.0),
            ),
            None => (0.0, 0.0, 100.0, 100.0),
        };
    // Canvas: solid Adom dark + the same atmosphere SymView's
    // .canvas-wrap div adds via CSS — radial teal glow at center +
    // 20-unit PCB grid. Baked into the SVG so the thumbnail looks
    // identical to SymView when shown standalone (no HTML wrapper).
    let cx = vb_x + vb_w / 2.0;
    let cy = vb_y + vb_h / 2.0;
    let glow_r = (vb_w.max(vb_h)) * 0.7;
    let canvas_bg_rect = format!(
r##"<defs>
  <radialGradient id="adom-canvas-glow" cx="{cx}" cy="{cy}" r="{r}" gradientUnits="userSpaceOnUse">
    <stop offset="0%"  stop-color="#00b8b0" stop-opacity="0.06"/>
    <stop offset="70%" stop-color="#00b8b0" stop-opacity="0"/>
  </radialGradient>
  <pattern id="adom-canvas-grid" x="0" y="0" width="2.54" height="2.54" patternUnits="userSpaceOnUse">
    <line x1="0" y1="0" x2="2.54" y2="0" stroke="rgba(255,255,255,0.025)" stroke-width="0.06"/>
    <line x1="0" y1="0" x2="0" y2="2.54" stroke="rgba(255,255,255,0.025)" stroke-width="0.06"/>
  </pattern>
</defs>
<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="{bg}"/>
<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="url(#adom-canvas-grid)"/>
<rect x="{x}" y="{y}" width="{w}" height="{h}" fill="url(#adom-canvas-glow)"/>"##,
        x = vb_x, y = vb_y, w = vb_w, h = vb_h,
        cx = cx, cy = cy, r = glow_r, bg = CANVAS_BG,
    );

    // Inject a <defs> + <style> right after the opening <svg ...>.
    // CDATA-wrap the style body so the literal `<g class=...>` inside the
    // CSS comment doesn't trip strict XML parsers.
    let defs_and_style = format!(
        r#"
<defs>
  <linearGradient id="adom-body-grad" x1="0" y1="0" x2="0" y2="1">
    <stop offset="0%" stop-color="{body_fill}"/>
    <stop offset="100%" stop-color="{body_fill_grad}"/>
  </linearGradient>
</defs>
<style><![CDATA[
  /* Stroked text groups (KiCad renders text as paths in <g class="stroked-text">) */
  g.stroked-text path {{
    stroke: {pin_name} !important;
    fill: {pin_name} !important;
  }}
  text[opacity="0"] {{
    fill: {pin_name} !important;
  }}
  .pin-highlight path {{
    stroke: #00e6dc !important;
  }}
  .pin-highlight g.stroked-text path {{
    stroke: #00e6dc !important;
  }}
  .group-highlight g.stroked-text path {{
    stroke: #FFD180 !important;
  }}
]]></style>"#,
        body_fill = BODY_FILL,
        body_fill_grad = BODY_FILL_GRAD,
        pin_name = PIN_NAME,
    );

    let svg_open = Regex::new(r#"(<svg[^>]*>)"#).unwrap();
    let s = svg_open
        .replace(s.as_ref(), format!("${{1}}\n{}\n{}", defs_and_style, canvas_bg_rect))
        .to_string();

    // Direct color replacements — CASE-INSENSITIVE on the hex digits
    // because kicad-cli emits both #840000 and #FFFFC2 (uppercase) and
    // the gallia source pattern uses /gi. Use Regex with `(?i)` for the
    // hex tail; literal prefix stays case-sensitive.
    let s = ci_replace_all(&s, &[
        (format!("stroke:{}", KICAD_BODY_STROKE),      format!("stroke:{}", PIN_WIRE)),
        (format!("stroke=\"{}\"", KICAD_BODY_STROKE),  format!("stroke=\"{}\"", PIN_WIRE)),
        (format!("fill:{}", KICAD_BODY_STROKE),        format!("fill:{}", PIN_DOT)),
        (format!("fill=\"{}\"", KICAD_BODY_STROKE),    format!("fill=\"{}\"", PIN_DOT)),
        (format!("fill:{}", KICAD_BODY_FILL),          "fill:url(#adom-body-grad)".to_string()),
        (format!("fill=\"{}\"", KICAD_BODY_FILL),      "fill=\"url(#adom-body-grad)\"".to_string()),
        (format!("stroke:{}", KICAD_TEXT_STROKE),      format!("stroke:{}", PIN_NAME)),
        (format!("stroke=\"{}\"", KICAD_TEXT_STROKE),  format!("stroke=\"{}\"", PIN_NAME)),
        (format!("fill:{}", KICAD_TEXT_STROKE),        format!("fill:{}", PIN_NAME)),
        (format!("fill=\"{}\"", KICAD_TEXT_STROKE),    format!("fill=\"{}\"", PIN_NAME)),
        (format!("stroke:{}", KICAD_HIDDEN_TEXT),      format!("stroke:{}", REF_TEXT)),
        (format!("fill:{}", KICAD_HIDDEN_TEXT),        format!("fill:{}", REF_TEXT)),
    ]);
    // rgb(...) form for the body fill — kicad-cli sometimes uses rgb()
    // notation for the same #FFFFC2 yellow.
    let rgb_body = Regex::new(r#"fill="rgb\(\s*255\s*,\s*255\s*,\s*194\s*\)""#).unwrap();
    let s = rgb_body.replace_all(&s, "fill=\"url(#adom-body-grad)\"").to_string();

    // Strip the root <g> default fill:#000000 so symbol body shapes don't
    // come out solid black on the dark canvas.
    let root_g_black = Regex::new(r#"(<g\s+style="fill:#000000[^"]*")"#).unwrap();
    let s = root_g_black
        .replace(&s, |c: &regex::Captures| {
            c[1].replace("fill:#000000", "fill:none")
        })
        .to_string();

    s
}

/// Re-theme a KiCad-CLI footprint SVG to match the FpView app:
///   - dark canvas #0d1117 + 20-unit PCB-style grid behind the geometry
///   - strip invisible REF**/value text + stroked-text groups (else they
///     inflate the viewBox)
///   - keep kicad-cli's native pad/silk/courtyard colors as-is, just
///     painted on the dark grid
pub fn theme_footprint_svg(raw: &str) -> String {
    let mut s = raw.replace("\r\n", "\n");
    s = strip_first_match(&s, r#"<\?xml[^?]*\?>"#);
    s = strip_first_match(&s, r#"<!DOCTYPE[^>]*>"#);
    s = strip_all_matches(&s, r#"<title>[^<]*</title>"#);
    s = strip_all_matches(&s, r#"<desc>[^<]*</desc>"#);
    s = strip_all_matches(&s, r#"<text[^>]*opacity="0"[^<]*</text>"#);
    s = strip_all_dotall(&s, r#"<g class="stroked-text">.*?</g>"#);
    let s = s.trim();

    // Pull the SVG's viewBox so the bg rect covers the entire canvas.
    let vb = match Regex::new(r#"viewBox="(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)""#)
        .unwrap()
        .captures(s)
    {
        Some(c) => (
            c[1].parse::<f64>().unwrap_or(-50.0),
            c[2].parse::<f64>().unwrap_or(-50.0),
            c[3].parse::<f64>().unwrap_or(100.0),
            c[4].parse::<f64>().unwrap_or(100.0),
        ),
        None => (-50.0, -50.0, 100.0, 100.0),
    };

    let defs = r#"
<defs>
  <pattern id="adom-pcb-grid" x="0" y="0" width="1" height="1" patternUnits="userSpaceOnUse">
    <line x1="0" y1="0" x2="1" y2="0" stroke="rgba(255,255,255,0.03)" stroke-width="0.05"/>
    <line x1="0" y1="0" x2="0" y2="1" stroke="rgba(255,255,255,0.03)" stroke-width="0.05"/>
  </pattern>
</defs>"#;
    let bg = format!(
        "<rect x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\" fill=\"{CANVAS_BG}\"/>\n<rect x=\"{x}\" y=\"{y}\" width=\"{w}\" height=\"{h}\" fill=\"url(#adom-pcb-grid)\"/>",
        x = vb.0, y = vb.1, w = vb.2, h = vb.3,
    );

    let svg_open = Regex::new(r#"(<svg[^>]*>)"#).unwrap();
    let s = svg_open
        .replace(s, format!("${{1}}\n{}\n{}", defs, bg))
        .to_string();

    // Strip any KiCad-CLI default white-ish page-background rect that
    // would render in front of our dark canvas.
    let white_bg_rect = Regex::new(
        r#"<rect[^>]*fill="(?i)#?(?:f{2,6}|white)"[^>]*/?>"#,
    ).unwrap();
    let s = white_bg_rect.replace_all(&s, "").to_string();

    s
}

fn strip_first_match(s: &str, pat: &str) -> String {
    Regex::new(pat).unwrap().replace(s, "").to_string()
}
fn strip_all_matches(s: &str, pat: &str) -> String {
    Regex::new(pat).unwrap().replace_all(s, "").to_string()
}
fn strip_all_dotall(s: &str, pat: &str) -> String {
    Regex::new(&format!(r"(?s){}", pat))
        .unwrap()
        .replace_all(s, "")
        .to_string()
}
fn simple_replace_all(s: &str, pairs: &[(String, String)]) -> String {
    let mut out = s.to_string();
    for (from, to) in pairs {
        out = out.replace(from, to);
    }
    out
}

/// Case-insensitive replace_all on each (from, to) pair. The literal
/// prefix (`fill:`, `stroke="`) stays case-sensitive; only the hex tail
/// after the `#` is matched insensitively. This matches the JS source
/// `/.../gi` semantics from gallia/symbol-creator/lib/svg-theme.js.
fn ci_replace_all(s: &str, pairs: &[(String, String)]) -> String {
    let mut out = s.to_string();
    for (from, to) in pairs {
        // Split prefix-up-to-and-including-#  vs. hex-tail at the first '#'
        // so we can keep the prefix literal and apply (?i) to the hex.
        let pat = if let Some(hash_idx) = from.find('#') {
            let prefix = regex::escape(&from[..hash_idx + 1]);
            let hex = regex::escape(&from[hash_idx + 1..]);
            format!("{prefix}(?i){hex}")
        } else {
            regex::escape(from)
        };
        let re = match Regex::new(&pat) {
            Ok(r) => r,
            Err(_) => continue,
        };
        out = re.replace_all(&out, to.as_str()).to_string();
    }
    out
}