/* Netlist text preprocessor.
 *
 * Implements the cpp-style directives that Eldo decks use to select stimuli:
 *
 *   #define name [value]      #undef name
 *   #ifdef name               #ifndef name
 *   #if expr                  #elif expr        #else       #endif
 *   #error message            #warning message
 *   #include "file"           (a plain synonym for .include)
 *
 * It runs inside inp_read(), on each line as it comes off readline() and
 * before anything else touches it -- before the .control counter, before
 * .include recursion and before case folding. Two consequences that are the
 * whole point of doing it here rather than by piping the deck through cpp:
 *
 *   - A .include inside a false branch is never opened, because this filter
 *     sits above the .include handler in the same loop. The ordinary
 *     .include keeps working unchanged; there is no need for a separate
 *     '#include' spelling the way Spectre demands.
 *   - ngspice's own file resolution, search paths, line numbering and error
 *     provenance are all preserved.
 *
 * Deliberate differences from C's preprocessor, forced by netlist syntax:
 *
 *   - Macro names are matched case-insensitively, like the rest of a netlist.
 *   - Names are netlist identifiers, not C identifiers, so '#ifdef 2x' and
 *     '#ifdef sig<2>' are legal.
 *   - A macro with an empty body is a FLAG and is never substituted. In C,
 *     '#define powerdown' deletes every later occurrence of the token powerdown --
 *     which in a netlist is a node name, not a macro use. Only
 *     '#define name value' substitutes, and then only whole tokens outside
 *     quoted strings.
 *
 * '#ifdef name' is true if name was #define'd, or if a variable of that name
 * exists -- so 'ngspice -D fast' and 'set fast' in .spiceinit both select
 * branches without any new command line option.
 *
 * A line beginning with '#' has always been a comment in ngspice, so nothing
 * can have depended on these spellings doing something else. To keep prose
 * comments such as '# if you change this ...' comments, the keyword must
 * follow the '#' with no space between; a '#' followed by anything else is
 * left alone and commented out as before.
 */

#include "ngspice/ngspice.h"

#include "ngspice/cpdefs.h"
#include "ngspice/dstring.h"
#include "ngspice/stringskip.h"
#include "ngspice/stringutil.h"

#include "inppp.h"
#include "variable.h"

#define PP_MAXNAME 256

struct pp_macro {
    char *name;
    char *value; /* NULL for a flag: never substituted */
    struct pp_macro *next;
};

struct pp_cond {
    struct pp_cond *next;
    bool emit;       /* emit the lines of the branch we are in */
    bool any_taken;  /* some branch of this chain has been taken */
    bool else_seen;  /* #else has been seen, no more branches allowed */
    bool outer_emit; /* emit state of the enclosing context */
    int line;
    char *file;
};

static struct pp_macro *pp_macros = NULL;
static int pp_n_valued = 0; /* macros that can substitute; usually 0 */
static struct pp_cond *pp_stack = NULL;
static int pp_stack_depth = 0;


/* Characters that may appear in a netlist identifier. Node names such as
   sig<2> or sig<7> have to come out as a single token, so that a macro
   named 'sig' cannot be substituted into the middle of one. */
static inline int pp_namechar(char c)
{
    return isalnum_c(c) || c == '_' || c == '<' || c == '>' || c == '.' ||
           c == '[' || c == ']' || c == '!' || c == '%';
}


static bool pp_emitting(void)
{
    return pp_stack ? pp_stack->emit : TRUE;
}


static bool pp_verbose(void)
{
    return cp_getvar("cpp_verbose", CP_BOOL, NULL, 0);
}


/* Report a directive and the branch it selected. Called after the
   conditional stack has been updated, so 'taken'/'skipped' describes the
   branch the following lines belong to. */
static void pp_trace(const char *file_name, int line_number, const char *key,
        const char *arg)
{
    if (!pp_verbose())
        return;

    fprintf(cp_err, "cpp: %s:%d: #%s%s%s -> %s\n",
            file_name ? file_name : "<input>", line_number, key,
            (arg && *arg) ? " " : "", (arg && *arg) ? arg : "",
            pp_emitting() ? "taken" : "skipped");
}


/* Copy the identifier at *ps into name, advancing *ps. Returns FALSE if
   there is no identifier there. */
static bool pp_get_name(const char **ps, char *name)
{
    const char *s = skip_ws(*ps);
    int i = 0;

    while (pp_namechar(*s) && i < PP_MAXNAME - 1)
        name[i++] = *s++;
    name[i] = '\0';
    *ps = s;

    return i > 0;
}


static struct pp_macro *pp_find(const char *name)
{
    struct pp_macro *m;

    for (m = pp_macros; m; m = m->next)
        if (cieq(m->name, name))
            return m;

    return NULL;
}


/* A name is 'defined' if it was #define'd, or if a variable of that name
   exists -- which is how -D and 'set' reach the preprocessor. */
static bool pp_defined(const char *name)
{
    struct variable *v;

    if (pp_find(name))
        return TRUE;

    for (v = variables; v; v = v->va_next)
        if (cieq(name, v->va_name))
            return TRUE;

    return FALSE;
}


static void pp_define(const char *name, const char *value)
{
    struct pp_macro *m = pp_find(name);

    if (!m) {
        m = TMALLOC(struct pp_macro, 1);
        m->name = copy(name);
        m->value = NULL;
        m->next = pp_macros;
        pp_macros = m;
    }

    if (m->value) {
        tfree(m->value);
        pp_n_valued--;
    }

    if (value && *value) {
        m->value = copy(value);
        pp_n_valued++;
    }
}


static void pp_undef(const char *name)
{
    struct pp_macro *m, **pm;

    for (pm = &pp_macros; (m = *pm) != NULL; pm = &m->next)
        if (cieq(m->name, name)) {
            *pm = m->next;
            if (m->value) {
                tfree(m->value);
                pp_n_valued--;
            }
            tfree(m->name);
            tfree(m);
            return;
        }
}


/* ---- #if / #elif expression --------------------------------------------
   defined(name), defined name, !, &&, ||, parentheses, integer literals,
   == != < > <= >=, and bare macro names (a valued macro contributes its
   value read as an integer, anything else 0 or 1 by existence). */

static const char *pp_ep;
static bool pp_eperr;

static long pp_or(void);


static long pp_primary(void)
{
    char name[PP_MAXNAME];
    struct pp_macro *m;

    pp_ep = skip_ws(pp_ep);

    if (pp_ep[0] == '!' && pp_ep[1] != '=') {
        pp_ep++;
        return !pp_primary();
    }

    if (*pp_ep == '(') {
        long v;
        pp_ep++;
        v = pp_or();
        pp_ep = skip_ws(pp_ep);
        if (*pp_ep == ')')
            pp_ep++;
        else
            pp_eperr = TRUE;
        return v;
    }

    if (isdigit_c(*pp_ep)) {
        char *end;
        long v = strtol(pp_ep, &end, 0);
        pp_ep = end;
        return v;
    }

    if (!pp_get_name(&pp_ep, name)) {
        pp_eperr = TRUE;
        return 0;
    }

    if (cieq(name, "defined")) {
        bool paren = FALSE;
        pp_ep = skip_ws(pp_ep);
        if (*pp_ep == '(') {
            paren = TRUE;
            pp_ep++;
        }
        if (!pp_get_name(&pp_ep, name)) {
            pp_eperr = TRUE;
            return 0;
        }
        if (paren) {
            pp_ep = skip_ws(pp_ep);
            if (*pp_ep == ')')
                pp_ep++;
            else
                pp_eperr = TRUE;
        }
        return pp_defined(name) ? 1 : 0;
    }

    m = pp_find(name);
    if (m && m->value)
        return strtol(m->value, NULL, 0);

    return pp_defined(name) ? 1 : 0;
}


static long pp_cmp(void)
{
    long v = pp_primary();

    for (;;) {
        pp_ep = skip_ws(pp_ep);
        if (pp_ep[0] == '=' && pp_ep[1] == '=') {
            pp_ep += 2;
            v = (v == pp_primary());
        }
        else if (pp_ep[0] == '!' && pp_ep[1] == '=') {
            pp_ep += 2;
            v = (v != pp_primary());
        }
        else if (pp_ep[0] == '<' && pp_ep[1] == '=') {
            pp_ep += 2;
            v = (v <= pp_primary());
        }
        else if (pp_ep[0] == '>' && pp_ep[1] == '=') {
            pp_ep += 2;
            v = (v >= pp_primary());
        }
        else if (pp_ep[0] == '<') {
            pp_ep++;
            v = (v < pp_primary());
        }
        else if (pp_ep[0] == '>') {
            pp_ep++;
            v = (v > pp_primary());
        }
        else {
            return v;
        }
    }
}


static long pp_and(void)
{
    long v = pp_cmp();

    for (;;) {
        pp_ep = skip_ws(pp_ep);
        if (pp_ep[0] == '&' && pp_ep[1] == '&') {
            long r;
            pp_ep += 2;
            r = pp_cmp();
            v = (v && r);
        }
        else {
            return v;
        }
    }
}


static long pp_or(void)
{
    long v = pp_and();

    for (;;) {
        pp_ep = skip_ws(pp_ep);
        if (pp_ep[0] == '|' && pp_ep[1] == '|') {
            long r;
            pp_ep += 2;
            r = pp_and();
            v = (v || r);
        }
        else {
            return v;
        }
    }
}


static bool pp_eval(const char *expr, const char *file_name, int line_number)
{
    long v;

    pp_ep = expr;
    pp_eperr = FALSE;

    v = pp_or();

    pp_ep = skip_ws(pp_ep);
    if (*pp_ep || pp_eperr) {
        fprintf(cp_err,
                "Error: cannot evaluate preprocessor expression '%s'\n"
                "    file %s, line %d\n",
                expr, file_name ? file_name : "<input>", line_number);
        controlled_exit(EXIT_FAILURE);
    }

    return v != 0;
}


/* ---- conditional stack ------------------------------------------------- */

static void pp_push(bool cond, const char *file_name, int line_number)
{
    struct pp_cond *c = TMALLOC(struct pp_cond, 1);

    c->outer_emit = pp_emitting();
    c->any_taken = c->outer_emit && cond;
    c->emit = c->any_taken;
    c->else_seen = FALSE;
    c->line = line_number;
    c->file = copy(file_name ? file_name : "<input>");
    c->next = pp_stack;

    pp_stack = c;
    pp_stack_depth++;
}


static void pp_pop(void)
{
    struct pp_cond *c = pp_stack;

    if (!c)
        return;

    pp_stack = c->next;
    pp_stack_depth--;
    tfree(c->file);
    tfree(c);
}


int inppp_depth(void)
{
    return pp_stack_depth;
}


void inppp_check_balance(int depth_on_entry, const char *file_name)
{
    if (pp_stack_depth <= depth_on_entry)
        return;

    fprintf(cp_err, "Error: unterminated #if/#ifdef, missing #endif\n");
    while (pp_stack_depth > depth_on_entry) {
        fprintf(cp_err, "    opened at %s, line %d\n", pp_stack->file,
                pp_stack->line);
        pp_pop();
    }
    fprintf(cp_err, "    while reading %s\n",
            file_name ? file_name : "<input>");
    controlled_exit(EXIT_FAILURE);
}


void inppp_deck_reset(void)
{
    while (pp_macros) {
        struct pp_macro *m = pp_macros;
        pp_macros = m->next;
        tfree(m->name);
        tfree(m->value);
        tfree(m);
    }
    pp_n_valued = 0;

    while (pp_stack)
        pp_pop();
}


/* ---- substitution of valued macros -------------------------------------- */

/* Replace whole tokens that name a macro with a value. Quoted strings are
   left alone, so that file names in .include never change. Flags have no
   value and are skipped, so a '#define powerdown' cannot delete the node
   powerdown. */
static char *pp_substitute(char *buf)
{
    DS_CREATE(ds, 256);
    const char *s = buf;
    bool changed = FALSE;

    while (*s) {
        if (*s == '"' || *s == '\'') {
            char q = *s;
            ds_cat_char(&ds, *s++);
            while (*s && *s != q)
                ds_cat_char(&ds, *s++);
            if (*s)
                ds_cat_char(&ds, *s++);
            continue;
        }

        if (pp_namechar(*s)) {
            char name[PP_MAXNAME];
            struct pp_macro *m;
            int i = 0;

            while (pp_namechar(*s)) {
                if (i < PP_MAXNAME - 1)
                    name[i++] = *s;
                s++;
            }
            name[i] = '\0';

            m = pp_find(name);
            if (m && m->value) {
                ds_cat_str(&ds, m->value);
                changed = TRUE;
            }
            else {
                ds_cat_str(&ds, name);
            }
            continue;
        }

        ds_cat_char(&ds, *s++);
    }

    if (!changed) {
        ds_free(&ds);
        return buf;
    }

    {
        char *out = copy(ds_get_buf(&ds));
        ds_free(&ds);
        tfree(buf);
        return out;
    }
}


/* ---- the line filter ---------------------------------------------------- */

/* Cut a trailing newline and any end-of-line comment off a directive's
   argument text, and trim the white space around it. Operates on a copy. */
static char *pp_tail(const char *s)
{
    char *t = copy(skip_ws(s));
    char *p;
    size_t n;

    for (p = t; *p; p++) {
        if (*p == '\n' || *p == '\r' || *p == ';')
            break;
        if (p[0] == '/' && p[1] == '/')
            break;
        if (*p == '$' && p > t && (p[-1] == ' ' || p[-1] == '\t')) {
            p--;
            break;
        }
    }
    *p = '\0';

    n = strlen(t);
    while (n > 0 && (t[n - 1] == ' ' || t[n - 1] == '\t'))
        t[--n] = '\0';

    return t;
}


int inppp_line(char **pbuffer, const char *file_name, int line_number)
{
    char *buf = *pbuffer;
    const char *s = skip_ws(buf);
    char key[PP_MAXNAME];
    int i = 0;

    if (*s != '#')
        goto plain_line;

    /* The keyword must follow '#' immediately. C allows '# ifdef', but a
       netlist comment may legitimately begin '# if you change this ...',
       and such lines have always been comments here. Requiring no space
       keeps them comments. */
    s++;
    while (isalpha_c(*s) && i < PP_MAXNAME - 1)
        key[i++] = tolower_c(*s++);
    key[i] = '\0';

    /* Anything else beginning with '#' keeps whatever meaning it had --
       ngspice already treats such a line as a comment. */
    if (!cieq(key, "define") && !cieq(key, "undef") && !cieq(key, "ifdef") &&
            !cieq(key, "ifndef") && !cieq(key, "if") && !cieq(key, "elif") &&
            !cieq(key, "else") && !cieq(key, "endif") &&
            !cieq(key, "error") && !cieq(key, "warning") &&
            !cieq(key, "include"))
        goto plain_line;

    /* Conditionals are acted on whether or not we are emitting, so that
       nesting inside a false branch is tracked correctly. */

    if (cieq(key, "ifdef") || cieq(key, "ifndef")) {
        char name[PP_MAXNAME];
        bool cond;
        if (!pp_get_name(&s, name)) {
            fprintf(cp_err, "Error: #%s without a name\n    file %s, line %d\n",
                    key, file_name ? file_name : "<input>", line_number);
            controlled_exit(EXIT_FAILURE);
        }
        /* Do not consult the tables at all inside a dead branch: a name
           may be #define'd by a line we are skipping. */
        cond = pp_emitting() ? pp_defined(name) : FALSE;
        if (cieq(key, "ifndef"))
            cond = pp_emitting() ? !cond : FALSE;
        pp_push(cond, file_name, line_number);
        pp_trace(file_name, line_number, key, name);
        goto swallow;
    }

    if (cieq(key, "if")) {
        char *expr = pp_tail(s);
        bool cond = pp_emitting() ? pp_eval(expr, file_name, line_number)
                                  : FALSE;
        pp_push(cond, file_name, line_number);
        pp_trace(file_name, line_number, key, expr);
        tfree(expr);
        goto swallow;
    }

    if (cieq(key, "elif") || cieq(key, "else")) {
        if (!pp_stack) {
            fprintf(cp_err, "Error: #%s without #if\n    file %s, line %d\n",
                    key, file_name ? file_name : "<input>", line_number);
            controlled_exit(EXIT_FAILURE);
        }
        if (pp_stack->else_seen) {
            fprintf(cp_err, "Error: #%s after #else\n    file %s, line %d\n",
                    key, file_name ? file_name : "<input>", line_number);
            controlled_exit(EXIT_FAILURE);
        }
        if (cieq(key, "else")) {
            pp_stack->else_seen = TRUE;
            pp_stack->emit = pp_stack->outer_emit && !pp_stack->any_taken;
            if (pp_stack->emit)
                pp_stack->any_taken = TRUE;
            pp_trace(file_name, line_number, key, NULL);
        }
        else {
            char *expr = pp_tail(s);
            bool cond = FALSE;
            if (pp_stack->outer_emit && !pp_stack->any_taken)
                cond = pp_eval(expr, file_name, line_number);
            pp_stack->emit = cond;
            if (pp_stack->emit)
                pp_stack->any_taken = TRUE;
            pp_trace(file_name, line_number, key, expr);
            tfree(expr);
        }
        goto swallow;
    }

    if (cieq(key, "endif")) {
        if (!pp_stack) {
            fprintf(cp_err, "Error: #endif without #if\n    file %s, line %d\n",
                    file_name ? file_name : "<input>", line_number);
            controlled_exit(EXIT_FAILURE);
        }
        pp_pop();
        pp_trace(file_name, line_number, key, NULL);
        goto swallow;
    }

    /* The rest do nothing at all inside a false branch. */

    if (!pp_emitting())
        goto swallow;

    if (cieq(key, "define")) {
        char name[PP_MAXNAME];
        char *value;
        if (!pp_get_name(&s, name)) {
            fprintf(cp_err, "Error: #define without a name\n"
                            "    file %s, line %d\n",
                    file_name ? file_name : "<input>", line_number);
            controlled_exit(EXIT_FAILURE);
        }
        value = pp_tail(s);
        pp_define(name, value);
        if (pp_verbose())
            fprintf(cp_err, "cpp: %s:%d: #define %s%s%s\n",
                    file_name ? file_name : "<input>", line_number, name,
                    *value ? " = " : " (flag)", value);
        tfree(value);
        goto swallow;
    }

    if (cieq(key, "undef")) {
        char name[PP_MAXNAME];
        if (pp_get_name(&s, name)) {
            pp_undef(name);
            if (pp_verbose())
                fprintf(cp_err, "cpp: %s:%d: #undef %s\n",
                        file_name ? file_name : "<input>", line_number, name);
        }
        goto swallow;
    }

    if (cieq(key, "error")) {
        char *msg = pp_tail(s);
        fprintf(cp_err, "Error: %s\n    file %s, line %d\n", msg,
                file_name ? file_name : "<input>", line_number);
        tfree(msg);
        controlled_exit(EXIT_FAILURE);
    }

    if (cieq(key, "warning")) {
        char *msg = pp_tail(s);
        fprintf(cp_err, "Warning: %s\n    file %s, line %d\n", msg,
                file_name ? file_name : "<input>", line_number);
        tfree(msg);
        goto swallow;
    }

    if (cieq(key, "include")) {
        /* Hand it to the ordinary .include machinery: same search path,
           same recursion, same error messages. */
        char *rest = pp_tail(s);
        char *out = tprintf(".include %s\n", rest);
        tfree(rest);
        tfree(buf);
        *pbuffer = out;
        return PP_PASS;
    }

swallow:
    return PP_DROP;

plain_line:
    if (!pp_emitting())
        return PP_DROP;

    if (pp_n_valued > 0 && *skip_ws(buf) != '*')
        *pbuffer = pp_substitute(buf);

    return PP_PASS;
}
