123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710
/**
 * .kicad_sym Generator
 *
 * Creates KiCad symbol files from structured pin data.
 *
 * Category A (ICs): Generates rectangle body with pins on sides, group labels.
 * Category B (Discrete): Fetches baseline from KiCad standard library, customizes properties.
 */

import { symGet } from '../../viewer/kicad-api-client.js';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

// ── Baseline cache ──
// Caches fetched KiCad standard library symbols to avoid repeated service-kicad calls.
const __dirname = dirname(fileURLToPath(import.meta.url));
const CACHE_DIR = join(__dirname, '..', 'cache');
mkdirSync(CACHE_DIR, { recursive: true });

async function cachedSymGet(library, symbolName) {
  const cacheFile = join(CACHE_DIR, `${library}--${symbolName}.kicad_sym`);
  if (existsSync(cacheFile)) {
    return readFileSync(cacheFile, 'utf-8');
  }
  const content = await symGet(library, symbolName);
  writeFileSync(cacheFile, content, 'utf-8');
  return content;
}

// Common discrete baselines to pre-warm cache on startup
const COMMON_BASELINES = [
  { library: 'Device', symbol: 'R' },
  { library: 'Device', symbol: 'C' },
  { library: 'Device', symbol: 'C_Polarized' },
  { library: 'Device', symbol: 'L' },
  { library: 'Device', symbol: 'D' },
  { library: 'Device', symbol: 'D_Zener' },
  { library: 'Device', symbol: 'LED' },
  { library: 'Transistor_FET', symbol: 'Q_NMOS_GSD' },
  { library: 'Transistor_FET', symbol: 'Q_NMOS_GDS' },
  { library: 'Transistor_FET', symbol: 'Q_PMOS_GSD' },
  { library: 'Transistor_FET', symbol: 'Q_PMOS_GDS' },
  { library: 'Transistor_BJT', symbol: 'Q_NPN_BCE' },
  { library: 'Transistor_BJT', symbol: 'Q_PNP_BCE' },
];

/**
 * Pre-warm the baseline cache with common discrete components.
 * Call once at service startup. Skips already-cached symbols.
 */
export async function warmBaselineCache() {
  let cached = 0, fetched = 0, failed = 0;
  for (const { library, symbol } of COMMON_BASELINES) {
    const cacheFile = join(CACHE_DIR, `${library}--${symbol}.kicad_sym`);
    if (existsSync(cacheFile)) { cached++; continue; }
    try {
      await cachedSymGet(library, symbol);
      fetched++;
    } catch (err) {
      failed++;
    }
  }
  return { cached, fetched, failed, total: COMMON_BASELINES.length };
}

// ── Pin angle constants ──
const ANGLE_LEFT_SIDE = 0;    // Pin extends right from tip → left side of body
const ANGLE_RIGHT_SIDE = 180; // Pin extends left from tip → right side of body
const ANGLE_BOTTOM_SIDE = 90; // Pin extends up from tip → bottom side of body
const ANGLE_TOP_SIDE = 270;   // Pin extends down from tip → top side of body

const PIN_LENGTH = 2.54;
const PIN_PITCH = 2.54;        // Within a group
const GROUP_GAP = 3.81;        // Between groups (1.5 grid steps — tight but clear separation)
const LABEL_OFFSET = 2.0;      // Group label above first pin in group
const BODY_TOP_MARGIN = 2.54;  // Margin above topmost group label (includes text height)
const BODY_BOTTOM_MARGIN = 2.54; // Below lowest pin
const MIN_BODY_WIDTH = 20;     // Minimum body width (mm)
const BODY_PADDING = 4;        // Horizontal padding inside body for pin names

// ── Standard font sizes (mm) — enforced by validation ──
// These are the canonical sizes for all symbols. kicad-validate.js checks conformance.
export const FONT_SIZES = {
  pinName:    1.27,   // Pin name text (KiCad default)
  pinNumber:  1.27,   // Pin number text (KiCad default)
  property:   1.27,   // Reference, Value, Footprint, Datasheet, etc.
  groupLabel: 0.762,  // Group label text (smaller, inside body)
  chipName:   1.27,   // Centered chip name (when enabled)
};

/**
 * Generate a .kicad_sym file for a Category A (IC) component.
 *
 * @param {object} input
 * @param {string} input.symbolName
 * @param {string} input.manufacturer
 * @param {string} input.package
 * @param {string} input.description
 * @param {string} input.datasheetUrl
 * @param {string} [input.referencePrefix='U']
 * @param {Array<{name: string, number: string, type: string, group?: string}>} input.pins
 * @param {Array<{name: string}>} [input.groups]
 * @param {object} [input.preferences]
 * @param {string} [input.preferences.pinLayout='left-right']
 * @param {boolean} [input.preferences.chipNameCentered=false]
 * @param {string} [input.preferences.partNumber='none']
 * @param {object} [input.preferences.distributorPn] - e.g. { mouser: 'XXX-XXXXX' }
 * @returns {string} Complete .kicad_sym file content
 */
export function generateICSym(input) {
  const {
    symbolName,
    manufacturer = '',
    package: pkg = '',
    description = '',
    datasheetUrl = '',
    referencePrefix = 'U',
    pins,
    groups = [],
    preferences = {},
  } = input;

  const pinLayout = preferences.pinLayout || 'left-right';
  const chipNameCentered = preferences.chipNameCentered || false;

  // Group pins by their group name, preserving order
  const groupMap = new Map();
  for (const pin of pins) {
    const g = pin.group || '_ungrouped';
    if (!groupMap.has(g)) groupMap.set(g, []);
    groupMap.get(g).push(pin);
  }

  // Decide which pins go on which side
  const leftPins = [];
  const rightPins = [];
  const topPins = [];
  const bottomPins = [];

  for (const [groupName, groupPins] of groupMap) {
    // Check if any pin in the group has an explicit side override
    const explicitSide = groupPins[0]?.side; // All pins in a group share the same side
    const allSameSide = explicitSide && groupPins.every(p => p.side === explicitSide);

    if (allSameSide) {
      // Explicit side override takes priority
      if (explicitSide === 'left') leftPins.push({ groupName, pins: groupPins });
      else if (explicitSide === 'right') rightPins.push({ groupName, pins: groupPins });
      else if (explicitSide === 'top') topPins.push({ groupName, pins: groupPins });
      else if (explicitSide === 'bottom') bottomPins.push({ groupName, pins: groupPins });
    } else {
      // Auto-determine side based on pin types and layout preference
      const hasInputs = groupPins.some(p => ['input', 'bidirectional'].includes(p.type));
      const hasPower = groupPins.some(p => ['power_in', 'power_out'].includes(p.type));
      const hasOutputs = groupPins.some(p => ['output', 'tri_state'].includes(p.type));
      const isGround = groupPins.every(p => p.type === 'power_in' && /^(GND|VSS|AGND|DGND|EP)/i.test(p.name));

      if (pinLayout === 'all-sides') {
        if (isGround) bottomPins.push({ groupName, pins: groupPins });
        else if (hasPower && !hasInputs && !hasOutputs) topPins.push({ groupName, pins: groupPins });
        else if (hasOutputs && !hasInputs) rightPins.push({ groupName, pins: groupPins });
        else leftPins.push({ groupName, pins: groupPins });
      } else {
        // left-right only
        if (isGround) leftPins.push({ groupName, pins: groupPins });
        else if (hasOutputs && !hasInputs) rightPins.push({ groupName, pins: groupPins });
        else if (hasPower && !hasInputs) rightPins.push({ groupName, pins: groupPins });
        else leftPins.push({ groupName, pins: groupPins });
      }
    }
  }

  // Calculate Y positions for left/right side groups
  function layoutVerticalSide(sideGroups) {
    const positioned = [];
    let y = 0;
    for (let i = 0; i < sideGroups.length; i++) {
      const { groupName, pins: gPins } = sideGroups[i];
      if (i > 0) y -= GROUP_GAP;

      // Group label position
      const labelY = y;
      y -= LABEL_OFFSET;

      for (const pin of gPins) {
        positioned.push({ ...pin, y });
        y -= PIN_PITCH;
      }

      if (groupName !== '_ungrouped') {
        positioned.labelPositions = positioned.labelPositions || [];
        positioned.labelPositions.push({ name: groupName, y: labelY });
      }
    }
    return { pins: positioned.filter(p => p.name), labels: positioned.labelPositions || [], totalHeight: Math.abs(y) };
  }

  // Calculate X positions for top/bottom side groups
  // Mirrors layoutVerticalSide: label is placed to the LEFT of its pin group
  // (analogous to how vertical labels are placed ABOVE their pin group)
  function layoutHorizontalSide(sideGroups, startX) {
    const positioned = [];
    let x = startX;
    for (let i = 0; i < sideGroups.length; i++) {
      const { groupName, pins: gPins } = sideGroups[i];
      if (i > 0) x += GROUP_GAP;

      // Group label position (to the left of pins, like vertical labels are above pins)
      const labelX = x;
      if (groupName !== '_ungrouped') {
        x += LABEL_OFFSET; // advance past label before placing pins
      }

      for (const pin of gPins) {
        positioned.push({ ...pin, x });
        x += PIN_PITCH;
      }

      if (groupName !== '_ungrouped') {
        positioned.labelPositions = positioned.labelPositions || [];
        positioned.labelPositions.push({ name: groupName, x: labelX });
      }
    }
    return { pins: positioned.filter(p => p.name), labels: positioned.labelPositions || [], totalWidth: x - startX };
  }

  const leftLayout = layoutVerticalSide(leftPins);
  const rightLayout = layoutVerticalSide(rightPins);

  // Determine body dimensions — account for top/bottom pins
  const hasTopBottom = topPins.length > 0 || bottomPins.length > 0;

  // Body width based on longest pin names (for left-right layout) OR top/bottom pin count
  const longestLeft = leftLayout.pins.reduce((max, p) => Math.max(max, p.name.length), 0);
  const longestRight = rightLayout.pins.reduce((max, p) => Math.max(max, p.name.length), 0);
  const nameWidth = (longestLeft + longestRight) * 1.0 + BODY_PADDING * 2;

  // For top/bottom: compute required width from pin count + group gaps + label offsets
  const topPinCount = topPins.reduce((s, g) => s + g.pins.length, 0);
  const bottomPinCount = bottomPins.reduce((s, g) => s + g.pins.length, 0);
  const topGroupGaps = Math.max(0, topPins.length - 1) * GROUP_GAP;
  const bottomGroupGaps = Math.max(0, bottomPins.length - 1) * GROUP_GAP;
  const topLabelOffsets = topPins.filter(g => g.groupName !== '_ungrouped').length * LABEL_OFFSET;
  const bottomLabelOffsets = bottomPins.filter(g => g.groupName !== '_ungrouped').length * LABEL_OFFSET;
  const topWidth = topPinCount * PIN_PITCH + topGroupGaps + topLabelOffsets + BODY_PADDING * 2;
  const bottomWidth = bottomPinCount * PIN_PITCH + bottomGroupGaps + bottomLabelOffsets + BODY_PADDING * 2;

  const bodyWidth = Math.max(nameWidth, MIN_BODY_WIDTH, topWidth, bottomWidth);
  const halfWidth = bodyWidth / 2;

  // Round to grid
  const bodyLeft = -Math.ceil(halfWidth / 2.54) * 2.54;
  const bodyRight = Math.ceil(halfWidth / 2.54) * 2.54;

  // Layout top/bottom pins centered within the body
  // First pass: compute total width starting at 0
  const topDry = layoutHorizontalSide(topPins, 0);
  const bottomDry = layoutHorizontalSide(bottomPins, 0);
  // Center: shift so pin group is centered between bodyLeft and bodyRight
  const topStartX = -topDry.totalWidth / 2;
  const bottomStartX = -bottomDry.totalWidth / 2;
  const topLayout = layoutHorizontalSide(topPins, topStartX);
  const bottomLayout = layoutHorizontalSide(bottomPins, bottomStartX);

  // Find topmost label Y for top margin
  const allLabels = [...leftLayout.labels, ...rightLayout.labels];
  const topLabelY = allLabels.length > 0 ? Math.max(...allLabels.map(l => l.y)) : 0;

  // Extra clearance when top/bottom pins exist — their names and group labels
  // render vertically inside the body, so we need room at those edges.
  let topPinClearance = 0;
  let bottomPinClearance = 0;

  if (topLayout.pins.length > 0) {
    const longestTopName = topLayout.pins.reduce((max, p) => Math.max(max, p.name.length), 0);
    const longestTopLabel = topLayout.labels.reduce((max, l) => Math.max(max, l.name.length), 0);
    topPinClearance = Math.max(longestTopName * FONT_SIZES.pinName * 0.7, longestTopLabel * FONT_SIZES.groupLabel * 0.7) + 2;
  }
  if (bottomLayout.pins.length > 0) {
    const longestBottomName = bottomLayout.pins.reduce((max, p) => Math.max(max, p.name.length), 0);
    const longestBottomLabel = bottomLayout.labels.reduce((max, l) => Math.max(max, l.name.length), 0);
    // Extra 0.5mm for KiCad pin name text overshoot at body edge
    bottomPinClearance = Math.max(longestBottomName * FONT_SIZES.pinName * 0.7, longestBottomLabel * FONT_SIZES.groupLabel * 0.7) + 2.5;
  }

  const bodyTop = topLabelY + BODY_TOP_MARGIN + topPinClearance;

  // Find lowest pin Y for bottom margin
  const allPinYs = [...leftLayout.pins, ...rightLayout.pins].map(p => p.y);
  const lowestPinY = allPinYs.length > 0 ? Math.min(...allPinYs) : -10;
  const bodyBottom = lowestPinY - BODY_BOTTOM_MARGIN - bottomPinClearance;

  // Build the .kicad_sym content
  const lines = [];
  lines.push('(kicad_symbol_lib');
  lines.push('  (version 20231120)');
  lines.push('  (generator "adom")');
  lines.push(`  (symbol "${symbolName}"`);
  lines.push('    (pin_names (offset 1.016))');
  lines.push('    (exclude_from_sim no)');
  lines.push('    (in_bom yes)');
  lines.push('    (on_board yes)');

  // Properties
  lines.push(`    (property "Reference" "${referencePrefix}"`);
  lines.push(`      (at 0 ${(bodyTop + 2.54).toFixed(2)} 0)`);
  lines.push('      (effects (font (size 1.27 1.27)))');
  lines.push('    )');

  lines.push(`    (property "Value" "${symbolName}"`);
  lines.push(`      (at 0 ${(bodyBottom - 2.54).toFixed(2)} 0)`);
  lines.push('      (effects (font (size 1.27 1.27)))');
  lines.push('    )');

  lines.push(`    (property "Footprint" "${pkg}"`);
  lines.push('      (at 0 0 0)');
  lines.push('      (effects (font (size 1.27 1.27)) (hide yes))');
  lines.push('    )');

  lines.push(`    (property "Datasheet" "${datasheetUrl}"`);
  lines.push('      (at 0 0 0)');
  lines.push('      (effects (font (size 1.27 1.27)) (hide yes))');
  lines.push('    )');

  lines.push(`    (property "Description" "${description}"`);
  lines.push('      (at 0 0 0)');
  lines.push('      (effects (font (size 1.27 1.27)) (hide yes))');
  lines.push('    )');

  lines.push(`    (property "Manufacturer" "${manufacturer}"`);
  lines.push('      (at 0 0 0)');
  lines.push('      (effects (font (size 1.27 1.27)) (hide yes))');
  lines.push('    )');

  // Distributor part numbers
  if (preferences.distributorPn) {
    for (const [dist, pn] of Object.entries(preferences.distributorPn)) {
      const label = dist.charAt(0).toUpperCase() + dist.slice(1);
      lines.push(`    (property "${label}" "${pn}"`);
      lines.push('      (at 0 0 0)');
      lines.push('      (effects (font (size 1.27 1.27)) (hide yes))');
      lines.push('    )');
    }
  }

  // Graphics sub-symbol (_0_1)
  lines.push(`    (symbol "${symbolName}_0_1"`);

  // Body rectangle
  lines.push(`      (rectangle (start ${bodyLeft.toFixed(2)} ${bodyTop.toFixed(2)}) (end ${bodyRight.toFixed(2)} ${bodyBottom.toFixed(2)})`);
  lines.push('        (stroke (width 0.254) (type default))');
  lines.push('        (fill (type background))');
  lines.push('      )');

  // Group labels (left side)
  for (const label of leftLayout.labels) {
    lines.push(`      (text "${label.name}" (at ${(bodyLeft + 1.02).toFixed(2)} ${label.y.toFixed(2)} 0)`);
    lines.push('        (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify left))');
    lines.push('      )');
  }

  // Group labels (right side)
  for (const label of rightLayout.labels) {
    lines.push(`      (text "${label.name}" (at ${(bodyRight - 1.02).toFixed(2)} ${label.y.toFixed(2)} 0)`);
    lines.push('        (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify right))');
    lines.push('      )');
  }

  // Group labels (top side) — placed just inside the top edge, text extends downward into body
  // At 90° rotation, "justify right" makes text extend in -Y direction (into body)
  for (const label of topLayout.labels) {
    lines.push(`      (text "${label.name}" (at ${label.x.toFixed(2)} ${(bodyTop - 1.02).toFixed(2)} 900)`);
    lines.push('        (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify right))');
    lines.push('      )');
  }

  // Group labels (bottom side) — placed just inside the bottom edge, text extends upward into body
  // At 90° rotation, "justify left" makes text extend in +Y direction (into body)
  for (const label of bottomLayout.labels) {
    lines.push(`      (text "${label.name}" (at ${label.x.toFixed(2)} ${(bodyBottom + 1.02).toFixed(2)} 900)`);
    lines.push('        (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify left))');
    lines.push('      )');
  }

  // Centered chip name (if preference enabled)
  if (chipNameCentered) {
    const centerY = ((bodyTop + bodyBottom) / 2).toFixed(2);
    lines.push(`      (text "${symbolName}" (at 0 ${centerY} 0)`);
    lines.push('        (effects (font (size 1.27 1.27) (color 100 100 100 1)) (justify center))');
    lines.push('      )');
  }

  lines.push('    )');

  // Pins sub-symbol (_1_1)
  lines.push(`    (symbol "${symbolName}_1_1"`);

  // Left-side pins
  for (const pin of leftLayout.pins) {
    const tipX = bodyLeft - PIN_LENGTH;
    lines.push(`      (pin ${pin.type} line (at ${tipX.toFixed(2)} ${pin.y.toFixed(2)} ${ANGLE_LEFT_SIDE}) (length ${PIN_LENGTH}) (name "${pin.name}") (number "${pin.number}"))`);
  }

  // Right-side pins
  for (const pin of rightLayout.pins) {
    const tipX = bodyRight + PIN_LENGTH;
    lines.push(`      (pin ${pin.type} line (at ${tipX.toFixed(2)} ${pin.y.toFixed(2)} ${ANGLE_RIGHT_SIDE}) (length ${PIN_LENGTH}) (name "${pin.name}") (number "${pin.number}"))`);
  }

  // Top-side pins — enter body 0.1mm inside top edge to prevent name text overshoot
  if (topLayout.pins.length > 0) {
    const topPinEntry = bodyTop - 0.1;
    for (const pin of topLayout.pins) {
      const tipY = topPinEntry + PIN_LENGTH;
      lines.push(`      (pin ${pin.type} line (at ${pin.x.toFixed(2)} ${tipY.toFixed(2)} ${ANGLE_TOP_SIDE}) (length ${PIN_LENGTH}) (name "${pin.name}") (number "${pin.number}"))`);
    }
  }

  // Bottom-side pins — enter body 0.1mm inside bottom edge to prevent name text overshoot
  if (bottomLayout.pins.length > 0) {
    const bottomPinEntry = bodyBottom + 0.1;
    for (const pin of bottomLayout.pins) {
      const tipY = bottomPinEntry - PIN_LENGTH;
      lines.push(`      (pin ${pin.type} line (at ${pin.x.toFixed(2)} ${tipY.toFixed(2)} ${ANGLE_BOTTOM_SIDE}) (length ${PIN_LENGTH}) (name "${pin.name}") (number "${pin.number}"))`);
    }
  }

  lines.push('    )');
  lines.push('  )');
  lines.push(')');
  lines.push('');

  return lines.join('\n');
}

/**
 * Generate a .kicad_sym file for a Category B (discrete) component.
 *
 * Handles the `extends` mechanism by building from scratch:
 * takes parent's graphics + pins, applies child's properties.
 *
 * @param {object} input
 * @param {string} input.symbolName
 * @param {string} input.manufacturer
 * @param {string} input.package
 * @param {string} input.description
 * @param {string} input.datasheetUrl
 * @param {string} [input.referencePrefix]
 * @param {object} input.discreteBaseline - { library, symbol }
 * @param {Array} [input.pinOverrides] - Override pin numbers/names
 * @returns {Promise<string>} Complete .kicad_sym file content
 */
export async function generateDiscreteSym(input) {
  const {
    symbolName,
    manufacturer = '',
    package: pkg = '',
    description = '',
    datasheetUrl = '',
    referencePrefix,
    discreteBaseline,
    pinOverrides = [],
  } = input;

  // Fetch baseline from KiCad service (cached locally after first fetch)
  let baseline = await cachedSymGet(discreteBaseline.library, discreteBaseline.symbol);

  // Check if it uses extends — need to fetch parent too
  const extendsMatch = baseline.match(/\(extends "([^"]+)"\)/);
  let parentContent = null;
  let parentName = null;
  if (extendsMatch) {
    parentName = extendsMatch[1];
    parentContent = await cachedSymGet(discreteBaseline.library, parentName);
  }

  // The source of graphics and pins: parent (if extends) or baseline itself
  const graphicsSource = parentContent || baseline;
  const graphicsSourceName = parentName || discreteBaseline.symbol;

  // Extract sub-symbol blocks from the graphics source
  const graphicsBlock = extractSubSymbol(graphicsSource, graphicsSourceName, '_0_1');
  const pinsBlock = extractSubSymbol(graphicsSource, graphicsSourceName, '_1_1');

  // Extract header settings from the graphics source
  const header = extractSymbolHeader(graphicsSource);

  // Get reference prefix from the graphics source or override
  let refPrefix = referencePrefix;
  if (!refPrefix) {
    const refMatch = graphicsSource.match(/\(property "Reference" "([^"]+)"/);
    refPrefix = refMatch ? refMatch[1] : 'U';
  }

  // Get property values — child properties override parent
  const propValue = symbolName;
  const propFootprint = pkg || extractProperty(baseline, 'Footprint') || extractProperty(graphicsSource, 'Footprint') || '';
  const propDatasheet = datasheetUrl || extractProperty(baseline, 'Datasheet') || '';
  const propDescription = description || extractProperty(baseline, 'Description') || '';
  const propManufacturer = manufacturer || extractProperty(baseline, 'Manufacturer') || '';

  // Get position hints from baseline for Reference/Value property placement
  const refAt = extractPropertyAt(graphicsSource, 'Reference') || '0 2.54 0';
  const valAt = extractPropertyAt(graphicsSource, 'Value') || '0 -2.54 0';
  const refEffects = extractPropertyEffects(graphicsSource, 'Reference') || '(effects (font (size 1.27 1.27)))';
  const valEffects = extractPropertyEffects(graphicsSource, 'Value') || '(effects (font (size 1.27 1.27)))';

  // Rename sub-symbol blocks to the new symbol name
  let renamedGraphics = graphicsBlock
    .replace(`(symbol "${graphicsSourceName}_0_1"`, `(symbol "${symbolName}_0_1"`);
  let renamedPins = pinsBlock
    .replace(`(symbol "${graphicsSourceName}_1_1"`, `(symbol "${symbolName}_1_1"`);

  // Apply pin overrides
  for (const override of pinOverrides) {
    if (override.oldNumber && override.newNumber) {
      renamedPins = renamedPins.replace(
        new RegExp(`\\(number "${escRegex(override.oldNumber)}"`, 'g'),
        `(number "${override.newNumber}"`
      );
    }
    if (override.oldName && override.newName) {
      renamedPins = renamedPins.replace(
        new RegExp(`\\(name "${escRegex(override.oldName)}"`, 'g'),
        `(name "${override.newName}"`
      );
    }
  }

  // Build the complete file from scratch
  const lines = [];
  lines.push('(kicad_symbol_lib');
  lines.push('  (version 20231120)');
  lines.push('  (generator "adom")');
  lines.push(`  (symbol "${symbolName}"`);

  // Header settings
  if (header.hidePinNames) {
    lines.push(`    (pin_numbers (hide yes))`);
    lines.push(`    (pin_names (offset ${header.pinNameOffset}) (hide yes))`);
  } else {
    lines.push(`    (pin_names (offset ${header.pinNameOffset}))`);
  }
  lines.push('    (exclude_from_sim no)');
  lines.push('    (in_bom yes)');
  lines.push('    (on_board yes)');

  // Properties (clean, single-line format)
  lines.push(`    (property "Reference" "${refPrefix}"`);
  lines.push(`      (at ${refAt})`);
  lines.push(`      ${refEffects}`);
  lines.push('    )');
  lines.push(`    (property "Value" "${propValue}"`);
  lines.push(`      (at ${valAt})`);
  lines.push(`      ${valEffects}`);
  lines.push('    )');
  lines.push(`    (property "Footprint" "${propFootprint}"`);
  lines.push('      (at 0 0 0)');
  lines.push('      (effects (font (size 1.27 1.27)) (hide yes))');
  lines.push('    )');
  lines.push(`    (property "Datasheet" "${propDatasheet}"`);
  lines.push('      (at 0 0 0)');
  lines.push('      (effects (font (size 1.27 1.27)) (hide yes))');
  lines.push('    )');
  lines.push(`    (property "Description" "${propDescription}"`);
  lines.push('      (at 0 0 0)');
  lines.push('      (effects (font (size 1.27 1.27)) (hide yes))');
  lines.push('    )');
  if (propManufacturer) {
    lines.push(`    (property "Manufacturer" "${propManufacturer}"`);
    lines.push('      (at 0 0 0)');
    lines.push('      (effects (font (size 1.27 1.27)) (hide yes))');
    lines.push('    )');
  }

  // Graphics sub-symbol
  lines.push(renamedGraphics);

  // Pins sub-symbol
  lines.push(renamedPins);

  lines.push('  )');
  lines.push(')');
  lines.push('');

  return lines.join('\n');
}

// ── Discrete helpers ──

function extractSubSymbol(content, symbolName, suffix) {
  const target = `(symbol "${symbolName}${suffix}"`;
  const start = content.indexOf(target);
  if (start === -1) return '';

  let depth = 0;
  let i = start;
  for (; i < content.length; i++) {
    if (content[i] === '(') depth++;
    else if (content[i] === ')') {
      depth--;
      if (depth === 0) break;
    }
  }

  const lineStart = content.lastIndexOf('\n', start) + 1;
  return content.slice(lineStart, i + 1);
}

function extractSymbolHeader(content) {
  const header = { hidePinNames: false, hidePinNumbers: false, pinNameOffset: 1.016 };
  if (/\(pin_names[\s\S]*?\(hide\s+yes\)/.test(content)) header.hidePinNames = true;
  if (/\(pin_numbers[\s\S]*?\(hide\s+yes\)/.test(content)) header.hidePinNumbers = true;
  const offsetMatch = content.match(/\(pin_names\s*\n?\s*\(offset\s+([\d.]+)\)/);
  if (offsetMatch) header.pinNameOffset = parseFloat(offsetMatch[1]);
  return header;
}

function extractProperty(content, propName) {
  const re = new RegExp(`\\(property "${propName}" "([^"]*)"`);
  const m = content.match(re);
  return m ? m[1] : '';
}

function extractPropertyAt(content, propName) {
  // Find (property "NAME" "..." \n (at X Y Z) and extract "X Y Z"
  const re = new RegExp(`\\(property "${propName}" "[^"]*"\\s*\\n\\s*\\(at\\s+([^)]+)\\)`);
  const m = content.match(re);
  return m ? m[1].trim() : null;
}

function extractPropertyEffects(content, propName) {
  // Find the effects block for a property
  const propStart = content.indexOf(`(property "${propName}"`);
  if (propStart === -1) return null;

  // Find the (effects ...) after this property
  const effectsStart = content.indexOf('(effects', propStart);
  if (effectsStart === -1) return null;

  // Make sure it's within the property block
  const propEnd = findMatchingParen(content, propStart);
  if (effectsStart > propEnd) return null;

  const effectsEnd = findMatchingParen(content, effectsStart);
  return content.slice(effectsStart, effectsEnd + 1);
}

function findMatchingParen(content, start) {
  let depth = 0;
  for (let i = start; i < content.length; i++) {
    if (content[i] === '(') depth++;
    else if (content[i] === ')') {
      depth--;
      if (depth === 0) return i;
    }
  }
  return content.length;
}

// ── Helpers ──

function replaceProperty(content, propName, newValue) {
  const re = new RegExp(`(\\(property "${propName}" )"[^"]*"`, 'g');
  return content.replace(re, `$1"${newValue}"`);
}

/**
 * Safe property replacement that handles multi-line KiCad format.
 * Finds `(property "NAME" "OLD_VALUE"` and replaces just the value string.
 */
function replacePropertySafe(content, propName, newValue) {
  // Match (property "PropName" "anything" — just replace the value part
  const re = new RegExp(`(\\(property "${escRegex(propName)}" )"[^"]*"`, 'g');
  return content.replace(re, `$1"${newValue}"`);
}

/**
 * Remove an entire property block from the content.
 * Handles multi-line property blocks with nested parentheses.
 */
function removeProperty(content, propName) {
  const marker = `(property "${propName}"`;
  let idx = content.indexOf(marker);
  while (idx !== -1) {
    // Find the start of the line
    const lineStart = content.lastIndexOf('\n', idx);
    // Find the end of the property block (matched parentheses)
    let depth = 0;
    let end = idx;
    for (; end < content.length; end++) {
      if (content[end] === '(') depth++;
      else if (content[end] === ')') {
        depth--;
        if (depth === 0) break;
      }
    }
    // Remove from line start to end of property (including trailing newline)
    const nextNewline = content.indexOf('\n', end);
    const cutEnd = nextNewline !== -1 ? nextNewline + 1 : end + 1;
    content = content.slice(0, lineStart === -1 ? idx : lineStart) + content.slice(cutEnd);
    idx = content.indexOf(marker);
  }
  return content;
}

function escRegex(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}