/**********
ngspice-jit: parasitic-network reduction at parse time.

Two brutal-but-effective simplifications for very large extracted RC
netlists (millions of R and C), selected from the deck via:

    .option MINRES=<val>    short (merge terminals of) every plain
                            numeric resistor whose value < <val>
    .option MINCAP=<val>    delete every plain numeric capacitor whose
                            value < <val>

Both default to 0 (feature off) so a deck without these options is
untouched and pays zero cost.

Mechanism.  This pass runs on the flattened, subckt-expanded deck (so
node names are already global and R/C values numeric).  For MINRES it
builds a union-find over node NAMES: each sub-threshold resistor unions
its two nodes and its card is commented out.  Ground ("0") is forced to
be the class representative so a small R to ground merges the node into
ground.  The resulting name->representative map is consulted by
INPtermInsert() (inpsymt.c) when every later device resolves its
terminal names to nodes, so all devices on a merged class collapse onto
a single CKTnode -- no fragile text rewriting, node positions are left
to each device's own parser.  For MINCAP the sub-threshold cap card is
simply commented out.

Author: ngspice-jit fork, 2026
**********/

#include "ngspice/ngspice.h"
#include "ngspice/inpdefs.h"
#include "ngspice/hash.h"
#include "ngspice/stringutil.h"
#include "inpxx.h"

/* ------------------------------------------------------------------ */
/* union-find over node-name strings                                  */
/* ------------------------------------------------------------------ */

typedef struct sUFent {
    char *name;             /* canonical node name (owned)            */
    struct sUFent *parent;  /* NULL => this entry is a class root     */
    int pinned;             /* node is a source/ground terminal: it   */
                            /* must survive and never be merged into  */
                            /* a *different* pinned net (would short a */
                            /* source). Pinned nodes stay class roots. */
} UFent;

static NGHASHPTR uf_tab = NULL;   /* node name -> UFent*              */

static void free_ufent(void *p)
{
    UFent *e = (UFent *) p;
    tfree(e->name);
    tfree(e);
}

/* Get (creating if needed) the union-find entry for a node name. */
static UFent *uf_get(const char *name)
{
    UFent *e = (UFent *) nghash_find(uf_tab, (void *) name);
    if (e)
        return e;
    e = TMALLOC(UFent, 1);
    e->name = copy(name);
    e->parent = NULL;
    e->pinned = 0;
    nghash_insert(uf_tab, (void *) name, e);
    return e;
}

/* Mark a node as pinned (a source or ground terminal). */
static void uf_pin(const char *name)
{
    uf_get(name)->pinned = 1;
}

/* Find the class root, with path compression. */
static UFent *uf_root(UFent *e)
{
    UFent *r = e, *n;
    while (r->parent)
        r = r->parent;
    while (e->parent) {        /* path compression */
        n = e->parent;
        e->parent = r;
        e = n;
    }
    return r;
}

/* Union the classes of two node names.  A pinned net (source/ground)
   always wins the root, so its name survives.  Merging two DIFFERENT
   pinned nets would short two sources (or a source to ground), so it is
   refused.  Returns:
     UF_MERGED (2) -- two distinct nets joined: one node is eliminated
     UF_SAME   (1) -- already the same net (redundant short, no node lost)
     UF_REFUSED(0) -- would short two driven nets: refused */
#define UF_REFUSED 0
#define UF_SAME    1
#define UF_MERGED  2
static int uf_union(const char *a, const char *b)
{
    UFent *ra = uf_root(uf_get(a));
    UFent *rb = uf_root(uf_get(b));
    if (ra == rb)
        return UF_SAME;              /* already the same node */
    if (ra->pinned && rb->pinned)
        return UF_REFUSED;           /* two driven nets: refuse the short */
    if (rb->pinned) {                /* keep the pinned net as the root */
        UFent *t = ra; ra = rb; rb = t;
    }
    rb->parent = ra;
    return UF_MERGED;                /* one node collapsed away */
}

/* ------------------------------------------------------------------ */
/* public: representative-name lookup used by INPtermInsert           */
/* ------------------------------------------------------------------ */

/* Return the representative node name for 'name' if it was merged into
   a different node, else NULL (not merged / is its own root / feature
   inactive).  The returned string is owned by this module. */
char *INPparasiticAlias(char *name)
{
    UFent *e, *r;
    if (!uf_tab)
        return NULL;
    e = (UFent *) nghash_find(uf_tab, name);
    if (!e)
        return NULL;
    r = uf_root(e);
    if (r == e)
        return NULL;
    return r->name;
}

/* ------------------------------------------------------------------ */
/* option scanning                                                    */
/* ------------------------------------------------------------------ */

/* case-insensitive strstr */
static char *ci_strstr(char *hay, const char *needle)
{
    size_t nlen = strlen(needle);
    for (; *hay; hay++)
        if (cieqn(hay, (char *) needle, nlen))
            return hay;
    return NULL;
}

/* Extract '.option KEY=<val>' (also 'KEY <val>' / 'KEY = <val>') from a
   line.  Returns 1 and stores the value if found. */
static int scan_opt_key(char *line, const char *key, double *out)
{
    size_t klen = strlen(key);
    char *p = line;
    int found = 0;

    while ((p = ci_strstr(p, key)) != NULL) {
        char *q;
        int err;
        double v;
        /* require a token boundary before the key */
        if (p > line && (isalnum_c(p[-1]) || p[-1] == '_')) {
            p += klen;
            continue;
        }
        q = p + klen;
        while (*q == ' ' || *q == '\t')
            q++;
        if (*q == '=')
            q++;
        while (*q == ' ' || *q == '\t')
            q++;
        v = INPevaluate(&q, &err, 1);
        if (!err) {
            *out = v;
            found = 1;              /* last occurrence wins */
        }
        p += klen;
    }
    return found;
}

/* ------------------------------------------------------------------ */
/* the pass                                                           */
/* ------------------------------------------------------------------ */

/* Parse "<name> <n1> <n2> <value>" from a device card.  On success
   returns 1 and hands back copies of the two node names plus the
   numeric value.  Returns 0 if the card is not a plain 2-terminal
   device with a leading numeric value (model resistor, expression,
   malformed, ...), so such devices are never reduced. */
static int parse_rc_card(char *cardline, char **n1, char **n2, double *val)
{
    char *line = cardline;
    char *name = NULL, *a = NULL, *b = NULL;
    int err;
    double v;

    *n1 = *n2 = NULL;

    INPgetNetTok(&line, &name, 1);          /* device name  */
    if (name == NULL || *line == '\0') { tfree(name); return 0; }
    INPgetNetTok(&line, &a, 1);             /* node 1       */
    if (*line == '\0') { tfree(name); tfree(a); return 0; }
    INPgetNetTok(&line, &b, 1);             /* node 2       */
    tfree(name);
    if (a == NULL || b == NULL) { tfree(a); tfree(b); return 0; }

    v = INPevaluate(&line, &err, 1);        /* value        */
    if (err) { tfree(a); tfree(b); return 0; }   /* model/expr: skip */

    *n1 = a;
    *n2 = b;
    *val = v;
    return 1;
}

/* Pin the two output/terminal nodes of every source in the deck so they
   survive reduction and are never merged into a different pinned net.
   Covers the sources whose first two nodes are the driven terminals:
   V, I (independent), E, F, G, H (controlled) and B (behavioral).  The
   sense nodes of E/G (tokens 4,5) are intentionally left unpinned. */
static void pin_source_nodes(struct card *deck)
{
    struct card *card;
    for (card = deck; card; card = card->nextcard) {
        char *ln = card->line;
        char c;
        char *line, *name = NULL, *a = NULL, *b = NULL;

        if (ln == NULL || *ln == '\0')
            continue;
        c = *ln;
        if (islower_c(c)) c = toupper_c(c);
        if (c != 'V' && c != 'I' && c != 'E' && c != 'F' &&
            c != 'G' && c != 'H' && c != 'B')
            continue;

        line = ln;
        INPgetNetTok(&line, &name, 1);          /* source name */
        tfree(name);
        if (*line == '\0') continue;
        INPgetNetTok(&line, &a, 1);             /* n+ */
        if (*line == '\0') { tfree(a); continue; }
        INPgetNetTok(&line, &b, 1);             /* n- */
        if (a) uf_pin(a);
        if (b) uf_pin(b);
        tfree(a);
        tfree(b);
    }
}

/* Scan a card list for minres/mincap tokens.  'options' cards have had
   their leading ".option " stripped by inp_getopts, so accept both the
   raw ".option ..." form (deck) and bare "minres=..." form (options). */
static void scan_option_list(struct card *list, int on_option_deck,
        double *minres, int *have_minres, double *mincap, int *have_mincap)
{
    struct card *card;
    for (card = list; card; card = card->nextcard) {
        char *ln = card->line;
        if (ln == NULL || *ln == '*')
            continue;
        if (!on_option_deck && !ciprefix(".opt", ln))
            continue;
        if (scan_opt_key(ln, "minres", minres)) *have_minres = 1;
        if (scan_opt_key(ln, "mincap", mincap)) *have_mincap = 1;
    }
}

void INPparasiticReduce(struct card *deck, struct card *options)
{
    struct card *card;
    double minres = 0.0, mincap = 0.0;
    int have_minres = 0, have_mincap = 0;
    long n_short = 0, n_capdel = 0, n_conflict = 0;
    long n_res_total = 0, n_cap_total = 0, n_nodes = 0;

    /* fresh state for this deck (also clears any prior deck's map) */
    if (uf_tab) {
        nghash_free(uf_tab, free_ufent, NULL);
        uf_tab = NULL;
    }

    /* 1. collect thresholds: from the extracted .option list, and from
       the deck itself (options carrying params are left in the deck). */
    scan_option_list(options, 1, &minres, &have_minres, &mincap, &have_mincap);
    scan_option_list(deck,    0, &minres, &have_minres, &mincap, &have_mincap);

    if (!(have_minres && minres > 0.0) && !(have_mincap && mincap > 0.0))
        return;                              /* nothing to do */

    if (have_minres && minres > 0.0) {
        uf_tab = nghash_init(1024);
        uf_pin("0");                         /* ground is a pinned net */
        pin_source_nodes(deck);              /* so are all source terminals */
    }

    /* 2. walk the deck: short small R (union + comment), drop small C */
    for (card = deck; card; card = card->nextcard) {
        char *ln = card->line;
        char c;
        char *n1, *n2;
        double val;

        if (ln == NULL || *ln == '\0')
            continue;
        c = *ln;
        if (islower_c(c)) c = toupper_c(c);

        if (c == 'R' && uf_tab) {
            n_res_total++;
            if (parse_rc_card(ln, &n1, &n2, &val)) {
                if (val >= 0.0 && val < minres) {
                    int r = uf_union(n1, n2);
                    if (r != UF_REFUSED) {
                        *ln = '*';           /* comment out the R */
                        n_short++;
                        if (r == UF_MERGED)
                            n_nodes++;       /* a node was eliminated */
                    } else {
                        n_conflict++;        /* would short two nets: keep */
                    }
                }
                tfree(n1);
                tfree(n2);
            }
        } else if (c == 'C' && have_mincap && mincap > 0.0) {
            n_cap_total++;
            if (parse_rc_card(ln, &n1, &n2, &val)) {
                if (val >= 0.0 && val < mincap) {
                    *ln = '*';               /* comment out the C */
                    n_capdel++;
                }
                tfree(n1);
                tfree(n2);
            }
        }
    }

    /* 3. report the reduction (counts, fractions, nodes eliminated) */
    if (n_short || n_capdel || n_conflict) {
        fprintf(stdout, "Parasitic reduction:\n");
        if (have_minres && minres > 0.0)
            fprintf(stdout,
                "    resistors: shorted %ld of %ld (%.1f%%) < %g ohm; "
                "%ld node(s) eliminated\n",
                n_short, n_res_total,
                n_res_total ? 100.0 * (double) n_short / (double) n_res_total : 0.0,
                minres, n_nodes);
        if (n_conflict)
            fprintf(stdout,
                "    kept %ld sub-threshold resistor(s) that would short two "
                "driven nets (source/ground)\n", n_conflict);
        if (have_mincap && mincap > 0.0)
            fprintf(stdout,
                "    capacitors: removed %ld of %ld (%.1f%%) < %g F\n",
                n_capdel, n_cap_total,
                n_cap_total ? 100.0 * (double) n_capdel / (double) n_cap_total : 0.0,
                mincap);
    }
}
