#!/bin/bash
w# Build ngspice for WebAssembly: sharedspice STATIC library + statically
# registered XSPICE code models. Consumed by the ngspice_service worker that
# backs KiCad eeschema's simulator.
#
# Deviations from a stock ngspice build, all load-bearing for wasm:
#  - --with-ngshared + --disable-shared: libtool builds libngspice.a with the
#    sharedspice API (ngSpice_Init & co.) compiled in and no CLI programs
#    (bin_PROGRAMS is gated !SHARED_MODULE upstream, so no duplicate main()).
#  - XSPICE code models (.cm) are dlopen'd at runtime natively, which a static
#    wasm module cannot do. Under NGCM_STATIC the icm build archives each code
#    model instead of linking a shared object (per-cm renamed table symbols via
#    ngcm_dlmain_static.c), and a registry appended to dev.c resolves the seven
#    bundled .cm basenames without dlopen. dlopen remains the fallback for
#    unknown paths so user code models fail with ngspice's normal error text.
#    Sources in scripts/deps/ngspice-wasm/; edits to the ngspice tree are
#    idempotent (marker-guarded) since the tree is an extracted tarball.
#  - /proc/meminfo header check is forced off: configure runs on the build
#    host (Linux in docker), the browser runtime has no procfs, and ngspice's
#    memory guard treats "0 bytes available" as out-of-memory.
#  - -pthread everywhere: sharedspice's bg_run/bg_halt background thread is
#    gated on HAVE_LIBPTHREAD; without it bg_run silently degrades to a
#    synchronous blocking call.
#  - Exception model must match the rest of the tree (DEPS_EH_FLAGS).
#  - XSPICE + CIDER enabled: parity with native KiCad's bundled ngspice.
#  - cmpp (XSPICE preprocessor) runs on the build host; ngspice's configure
#    handles that itself when cross_compiling=yes (src/xspice/cmpp/build/).

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/../common/env.sh"
source "${SCRIPT_DIR}/../common/versions.sh"
source "${SCRIPT_DIR}/../common/functions.sh"

NGSPICE_DIR="${DEPS_ROOT}/ngspice-${NGSPICE_VERSION}"
NGSPICE_BUILD="${BUILD_ROOT}/deps/ngspice"
NGSPICE_STAMP="${BUILD_ROOT}/stamps/ngspice.stamp"
NGCM_SRC_DIR="${SCRIPT_DIR}/ngspice-wasm"

# Parse arguments
CLEAN=0
for arg in "$@"; do
    case $arg in
        --clean)
            CLEAN=1
            shift
            ;;
    esac
done

if [ $CLEAN -eq 1 ]; then
    log_info "Cleaning ngspice build..."
    rm -rf "${NGSPICE_BUILD}" "${NGSPICE_STAMP}"
fi

# Check if already built
if check_stamp "${NGSPICE_STAMP}"; then
    log_info "ngspice already built, skipping..."
    exit 0
fi

# Download if needed
if [ ! -d "${NGSPICE_DIR}" ]; then
    log_info "Downloading ngspice ${NGSPICE_VERSION}..."
    mkdir -p "${DEPS_ROOT}"
    cd "${DEPS_ROOT}"

    download_file "${NGSPICE_URL}" "ngspice-${NGSPICE_VERSION}.tar.gz"
    tar -xzf "ngspice-${NGSPICE_VERSION}.tar.gz"
    rm "ngspice-${NGSPICE_VERSION}.tar.gz"
fi

# ---------------------------------------------------------------------------
# Static code-model support (NGCM_STATIC) - idempotent source edits
# ---------------------------------------------------------------------------

DEV_C="${NGSPICE_DIR}/src/spicelib/devices/dev.c"
ICM_MK="${NGSPICE_DIR}/src/xspice/icm/GNUmakefile.in"

# 1. Hook load_opus(): consult the registry before attempting dlopen.
#    Must run BEFORE the registry append: the registry body contains the
#    ngcm_static_load definition, so a plain grep for the name would mask the
#    hook forever. The guard matches the call site only.
if ! grep -q "ngcm_static_load(name)" "${DEV_C}"; then
    log_info "Inserting registry hook into load_opus()"
    python3 - "${DEV_C}" <<'EOF'
import sys

path = sys.argv[1]
src = open(path).read()
anchor = "    lib = dlopen(name, RTLD_NOW);"
hook = """#ifdef NGCM_STATIC
    {
        extern int ngcm_static_load(const char *path);
        int ngcm_ret = ngcm_static_load(name);
        if (ngcm_ret >= 0)
            return ngcm_ret;
        /* not a bundled code model: fall through to dlopen */
    }
#endif
"""
assert src.count(anchor) == 1, "load_opus dlopen anchor not unique"
open(path, "w").write(src.replace(anchor, hook + anchor))
EOF
fi

# 2. Append the code-model registry to dev.c (marker-guarded).
if ! grep -q "NGCM_REGISTRY_MARKER" "${DEV_C}"; then
    log_info "Appending static code-model registry to dev.c"
    cat "${NGCM_SRC_DIR}/ngcm_registry.c" >> "${DEV_C}"
fi

# 3. Fix an upstream 32-bit union-punning bug in the CIDER card parser
#    (found by the Gate-1 smoke, ngspice 46). The cleanup tests use
#    `dataType & IF_REALVEC` (0x8004): every scalar IF_SET|IF_REAL parameter
#    (0x2004) matches through the shared 0x4 bit, and the code then frees the
#    vec-pointer union member overlaying the parsed scalar double. On 64-bit
#    hosts that misread lands in zeroed padding after the double
#    (free(NULL) no-op), which is why upstream never noticed; on wasm32 the
#    pointer member overlays the HIGH HALF of the double and free() faults.
#    TODO: upstream this to ngspice.
python3 - "${NGSPICE_DIR}/src/spicelib/parser/inpgmod.c" <<'EOF'
import sys

path = sys.argv[1]
src = open(path).read()
old = """                if (info->cardParms[idx].dataType & IF_STRING) {
                    FREE(value->sValue);
                } else if (info->cardParms[idx].dataType & IF_REALVEC) {
                    FREE(value->v.vec.rVec);
                } else if (info->cardParms[idx].dataType & IF_INTVEC) {
                    FREE(value->v.vec.iVec);
                }"""
new = """                /* kicad-wasm: exact variant-type tests. The original
                 * `& IF_REALVEC` composite masks also match scalar
                 * IF_SET|IF_REAL parameters (shared 0x4 bit) and free the
                 * vec-pointer union member overlaying the scalar double -
                 * benign on 64-bit (lands in zero padding), heap fault on
                 * wasm32. */
                int ngcm_vt = info->cardParms[idx].dataType & IF_VARTYPES;
                if (ngcm_vt == IF_STRING) {
                    FREE(value->sValue);
                } else if (ngcm_vt == IF_REALVEC) {
                    FREE(value->v.vec.rVec);
                } else if (ngcm_vt == IF_INTVEC) {
                    FREE(value->v.vec.iVec);
                }"""
if new in src:
    pass  # already applied
else:
    assert src.count(old) == 1, "inpgmod.c cleanup-tests anchor not found"
    open(path, "w").write(src.replace(old, new))
    print("patched inpgmod.c IF_VARTYPES cleanup tests")
EOF

# 4. Redirect the icm build: archive code models instead of shared-linking,
#    compile our renamed-tables TU instead of dlmain.c, and pass the cm name.
#    All three edits are exact-string replacements, applied once.
python3 - "${ICM_MK}" <<'EOF'
import sys

path = sys.argv[1]
src = open(path).read()

edits = [
    # .cm link recipe -> archive under NGCM_STATIC. The three common objects
    # are excluded: dstring.o duplicates the core's, and the tline commons are
    # shipped once via ngcm_common.a (they are prerequisites of every cm here
    # but only the tlines models reference them).
    ("\t$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(VIS_CFLAGS) $(LDFLAGS) $^ $(LIBS) -o $@",
     "\t$(if $(NGCM_STATIC),emar rcs $@ $(filter-out dstring.o msline_common.o tline_common.o,$^),$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(VIS_CFLAGS) $(LDFLAGS) $^ $(LIBS) -o $@)"),
    # dlmain.o compiles our static-tables TU under NGCM_STATIC ($< follows the
    # first prerequisite).
    ("$(cm)/dlmain.o : $(srcdir)/dlmain.c $(cm-descr)",
     "$(cm)/dlmain.o : $(if $(NGCM_STATIC),$(NGCM_DLMAIN),$(srcdir)/dlmain.c) $(cm-descr)"),
    # Per-cm symbol prefix for the tables TU (harmless for the other objects).
    ("COMPILE = $(CC) $(INCLUDES) -I$(cm) -I$(srcdir)/$(cm) $(CFLAGS) $(EXTRA_CFLAGS) $(VIS_CFLAGS)",
     "COMPILE = $(CC) $(INCLUDES) -I$(cm) -I$(srcdir)/$(cm) $(CFLAGS) $(EXTRA_CFLAGS) $(VIS_CFLAGS) $(if $(NGCM_STATIC),-DNGCM_NAME=$(cm))"),
    # The $(shell cmpp -p) model-list calls hardcode the in-tree cmpp, which
    # is the CROSS-compiled (wasm) binary the build host cannot execute -
    # the lists come back empty and the code models silently lose all their
    # cfunc/ifspec objects. makedefs' CMPP is the host-built one under
    # cross-compilation (configure.ac:1470-1475).
    ("""ifeq ($(OS),Windows_NT)
    cmpp = ../cmpp/cmpp.exe
else
    cmpp = ../cmpp/cmpp
endif""",
     "cmpp = $(CMPP)"),
]

changed = False
for old, new in edits:
    if new in src:
        continue  # already applied
    assert old in src, f"icm GNUmakefile.in anchor not found: {old!r}"
    src = src.replace(old, new)
    changed = True

if changed:
    open(path, "w").write(src)
    print("patched icm GNUmakefile.in")
EOF

log_info "Building ngspice ${NGSPICE_VERSION} for WASM..."

mkdir -p "${NGSPICE_BUILD}"
cd "${NGSPICE_BUILD}"

# ngspice must run at real speed even in debug builds of the rest of the tree:
# the simulator is compute-bound and its own module is finalized separately.
if [ "${DEBUG_BUILD:-1}" = "1" ]; then
    NGSPICE_OPT="-O2 -g"
else
    NGSPICE_OPT="-O2"
fi

# -Wno-error guards: CIDER and parts of XSPICE are legacy C that modern clang
# (emcc >= llvm 16) rejects by default.
export CFLAGS="${NGSPICE_OPT} -pthread ${DEPS_EH_FLAGS} -DNGCM_STATIC \
-Wno-error=implicit-function-declaration -Wno-error=implicit-int"
export CXXFLAGS="${NGSPICE_OPT} -pthread ${DEPS_EH_FLAGS}"
export LDFLAGS="-pthread ${DEPS_EH_FLAGS}"

# Makefile-level knobs for the icm (code model) build, read from the
# environment by our GNUmakefile.in edits above.
export NGCM_STATIC=1
export NGCM_DLMAIN="${NGCM_SRC_DIR}/ngcm_dlmain_static.c"

emconfigure "${NGSPICE_DIR}/configure" \
    --prefix="${SYSROOT}" \
    --host=wasm32-unknown-emscripten \
    --build="$("${NGSPICE_DIR}/config.guess")" \
    --with-ngshared \
    --disable-shared \
    --enable-static \
    --enable-xspice \
    --enable-cider \
    --disable-openmp \
    --disable-debug \
    --without-x \
    --with-readline=no \
    --without-editline \
    ac_cv_header__proc_meminfo=no

# ngspice hardwires libtool's -shared mode for the ngshared build: configure
# sets STATIC=-shared (consumed as AM_CFLAGS by every convenience lib) and
# src/Makefile.am gives libngspice_la_{CFLAGS,LDFLAGS} a literal -shared.
# libtool refuses -shared outright on a target without shared-library support
# ("Fatal configuration error"), so force static mode: the make command line
# overrides $(STATIC) everywhere, and the two hardwired lines are rewritten in
# the generated Makefile (regenerated by configure on every build, so this
# stays idempotent).
sed -i.ngcm.bak \
    -e 's/^\(libngspice_la_[A-Z]*FLAGS *=.*\)-shared/\1-static/' \
    src/Makefile

# The XSPICE verilog/vhdl subdirs build VPI co-simulation shims (ivlng.la,
# ivlngvpi.la) that are inherently SHARED objects plugging into an external
# Icarus/GHDL process - impossible in wasm and unbuildable without shared-lib
# support. Skipping them matches native behaviour when no cosimulator is
# installed: the d_cosim code model fails at runtime with ngspice's normal
# error message.
sed -i.ngcm.bak \
    -e 's/^\(SUBDIRS = mif cm enh evt idn cmpp icm\) verilog vhdl$/\1/' \
    src/xspice/Makefile

emmake make -j${JOBS} STATIC=-static
emmake make install STATIC=-static

# dlmain.c's tail (fopen_with_path, cm_message_printf, cm_is_inertial) is
# utility code the cfunc objects call but that exists nowhere in the core -
# natively each .cm DLL carries its own copy. Extract it once from the
# pristine dlmain.c (BSD-3) so the seven archives stay collision-free; the
# coreitf wrapper section above the marker must NOT come along (it would
# shadow real core functions with calls through a never-initialized coreitf).
python3 - "${NGSPICE_DIR}/src/xspice/icm/dlmain.c" "${NGSPICE_BUILD}/ngcm_cmutil.c" <<'EOF'
import sys

src_path, out_path = sys.argv[1], sys.argv[2]
src = open(src_path).read()
marker = "#define DFLT_BUF_SIZE   256"
assert src.count(marker) == 1, "dlmain.c utility-tail marker not unique"
tail = src[src.index(marker):]
preamble = """/* Generated by build-ngspice.sh: utility tail of ngspice's
 * src/xspice/icm/dlmain.c (BSD-3-Clause, Copyright 2000 The ngspice team),
 * shared once across the statically linked code models. */
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "ngspice/config.h"
#include "ngspice/cpextern.h"
#include "ngspice/devdefs.h"
#include "ngspice/dstring.h"
#include "ngspice/dllitf.h"
#include "ngspice/evtudn.h"
#include "ngspice/inpdefs.h"
#include "ngspice/inertial.h"
#include "ngspice/cmproto.h"

/* In the DLL world cm_getvar is a dlmain.c wrapper that reaches the core's
 * cp_getvar through coreitf (see cmexport.c binding dllitf_cm_getvar to
 * cp_getvar). Statically there is no coreitf; bind it directly. */
bool cm_getvar(char *name, enum cp_types type, void *retval, size_t rsize)
{
    return cp_getvar(name, type, retval, rsize);
}

"""
open(out_path, "w").write(preamble + tail)
EOF

emcc -c "${NGSPICE_BUILD}/ngcm_cmutil.c" -o "${NGSPICE_BUILD}/ngcm_cmutil.o" \
    ${CFLAGS} \
    -I"${NGSPICE_DIR}/src/include" \
    -I"${NGSPICE_BUILD}/src/include"

# The tline/msline common objects every cm build compiles but only the tlines
# models reference; shipped once so whole-archiving the .cm archives stays
# duplicate-free.
emar rcs "${SYSROOT}/lib/ngspice/ngcm_common.a" \
    src/xspice/icm/msline_common.o \
    src/xspice/icm/tline_common.o \
    src/xspice/icm/dstring.o \
    "${NGSPICE_BUILD}/ngcm_cmutil.o"

# Sanity: everything the ngspice_service link needs must exist.
for f in \
    "${SYSROOT}/lib/libngspice.a" \
    "${SYSROOT}/include/ngspice/sharedspice.h" \
    "${SYSROOT}/lib/ngspice/analog.cm" \
    "${SYSROOT}/lib/ngspice/digital.cm" \
    "${SYSROOT}/lib/ngspice/spice2poly.cm" \
    "${SYSROOT}/lib/ngspice/table.cm" \
    "${SYSROOT}/lib/ngspice/tlines.cm" \
    "${SYSROOT}/lib/ngspice/xtradev.cm" \
    "${SYSROOT}/lib/ngspice/xtraevt.cm" \
    "${SYSROOT}/share/ngspice/scripts/spinit"; do
    if [ ! -f "$f" ]; then
        log_error "ngspice install incomplete: missing $f"
        exit 1
    fi
done

create_stamp "${NGSPICE_STAMP}"
log_info "ngspice build complete!"
