/* SPDX-FileCopyrightText: Yorhel <projects@yorhel.nl>
 * SPDX-License-Identifier: MIT */

#define _GNU_SOURCE
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <spawn.h>
#include <regex.h>
#include <getopt.h>


/* TODO: There's no need for random access while processing a file. The
 * parsing/processing/formatting steps are currently separated, but could
 * really be done in a single step in a streaming fashion as well. That may or
 * may not simplify the code, I haven't really investigated yet.
 * (Output will have to be buffered either way, we wouldn't want to write
 * anything if there's an error during processing)
 *
 * Also, the current implementation leaks memory everywhere.
 * That's by design.
 * Kind of.
 */


struct ctx {
    char *fn;
    int line, offset;
    struct ctx *parent;
};


struct cfg_arg {
    struct ctx ctx;
    char *data;
    struct cfg_arg *next;
};

struct cfg_directive {
    struct ctx ctx;
    char *name;
    struct cfg_arg *args;
    struct cfg_directive *body, *next;
};
static const struct cfg_directive _empty_body = {};
static struct cfg_directive *empty_body = (struct cfg_directive *)&_empty_body;


#define isAlpha(c) (((unsigned)(c)|32)-'a' < 26)
#define isNum(c) (((unsigned)(c))-'0' < 10)


#ifdef __GNUC__
__attribute__((noreturn, format(printf, 2, 3)))
#endif
static void die(const struct ctx *ctx, const char *fmt, ...) {
    va_list arg;
    va_start(arg, fmt);
    vfprintf(stderr, fmt, arg);
    va_end(arg);
    fprintf(stderr, "\n  in %s", ctx->fn);
    if(ctx->line)
        fprintf(stderr, " line %d:%d", ctx->line+1, ctx->offset+1);
    putc('\n', stderr);

    while((ctx = ctx->parent))
        fprintf(stderr, "  included by %s line %d:%d\n", ctx->fn, ctx->line, ctx->offset);
    exit(1);
}


/* Read a file descriptor to EOF. Buffer will be zero-terminated. */
static size_t slurp_fd(int fd, char **buf) {
    size_t bufsize = 4096, buflen = 0;
    ssize_t r;
    *buf = malloc(bufsize);
    while((r = read(fd, *buf+buflen, bufsize-buflen-1)) > 0) {
        buflen += r;
        if(bufsize-buflen < 4096) {
            bufsize *= 2;
            *buf = realloc(*buf, bufsize);
        }
    }
    if(r < 0) {
        free(*buf);
        *buf = NULL;
        return -1;
    }
    (*buf)[buflen] = 0;
    return buflen;
}


static struct cfg_arg *cfg_arg_copy(struct cfg_arg *a) {
    struct cfg_arg *head = NULL, **t = &head;
    while(a) {
        *t = malloc(sizeof(struct cfg_arg));
        memcpy(*t, a, sizeof(struct cfg_arg));
        (*t)->data = strdup(a->data);
        t = &(*t)->next;
        a = a->next;
    }
    return head;
}


static struct cfg_directive *cfg_directive_copy(struct cfg_directive *d) {
    struct cfg_directive *head = NULL, **t = &head;
    if(d == empty_body)
        return d;
    while(d) {
        *t = malloc(sizeof(struct cfg_directive));
        memcpy(*t, d, sizeof(struct cfg_directive));
        (*t)->name = strdup(d->name);
        (*t)->args = cfg_arg_copy(d->args);
        (*t)->body = cfg_directive_copy(d->body);
        t = &(*t)->next;
        d = d->next;
    }
    return head;
}




/***********
 *   PARSING
 */

struct parse_ctx {
    struct ctx *ctx;
    size_t pos, len;
    char *buf, c;
};


static char parse_take(struct parse_ctx *ctx) {
    char c = ctx->c;
    if(c == 0)
        die(ctx->ctx, "Invalid 0 byte");
    if(c == '\n') {
        ctx->ctx->line++;
        ctx->ctx->offset = 0;
    } else
        ctx->ctx->offset++;
    ctx->pos++;
    ctx->c = ctx->buf[ctx->pos];
    return c;
}


// Consumes any number of whirespace characters and comments.
static void parse_ws(struct parse_ctx *ctx) {
    while(1) {
        switch(ctx->c) {
            case ' ':
            case '\t':
            case '\r':
            case '\n':
                parse_take(ctx);
                break;
            case '#':
                while(ctx->c != 0 && ctx->c != '\n')
                    parse_take(ctx);
                break;
            default:
                return;
        }
    }
}


static void parse_unqarg(struct parse_ctx *ctx) {
    while(1) {
        switch(ctx->c) {
            case '\\':
                parse_take(ctx);
                if(ctx->c == 0)
                    die(ctx->ctx, "Unexpected EOF");
                break;
            case '$':
                /* $#{ should not be taken to start a block */
                if(ctx->buf[ctx->pos+1] == '#' && ctx->buf[ctx->pos+2] == '{') {
                    parse_take(ctx);
                    parse_take(ctx);
                /* nor should ${ */
                } else if(ctx->buf[ctx->pos+1] == '{')
                    parse_take(ctx);
                break;
            case 0:
            case ' ':
            case '\t':
            case '\r':
            case '\n':
            case '{':
            case ';':
                return;
        }
        parse_take(ctx);
    }
}


static void parse_qarg(struct parse_ctx *ctx) {
    char c, q = parse_take(ctx);
    while(1) {
        if(ctx->c == 0)
            die(ctx->ctx, "Unexpected EOF");
        c = parse_take(ctx);
        if(c == q)
            return;
        if(c == '\\') {
            if(ctx->c == 0)
                die(ctx->ctx, "Unexpected EOF");
            parse_take(ctx);
        }
    }
}


// Parse either a quoted or unquoted string
static char *parse_arg(struct parse_ctx *ctx) {
    size_t start = ctx->pos;
    (ctx->c == '"' || ctx->c == '\'' ? parse_qarg : parse_unqarg)(ctx);
    char *str = malloc(ctx->pos-start+1);
    memcpy(str, ctx->buf+start, ctx->pos-start);
    str[ctx->pos-start] = 0;
    return str;
}


static struct cfg_directive *parse_block(struct parse_ctx *ctx) {
    struct cfg_directive *head = NULL, *cur = NULL, *tmp;
    struct cfg_arg **arg;

directive_start:
    parse_ws(ctx);
    if(ctx->c == 0 || ctx->c == '}')
        return head ? head : empty_body;

    tmp = calloc(1, sizeof(struct cfg_directive));
    *(head ? &cur->next : &head) = tmp;
    cur = tmp;

    cur->name = parse_arg(ctx);
    cur->ctx = *ctx->ctx;
    arg = &cur->args;

directive_arg:
    parse_ws(ctx);
    switch(ctx->c) {
        case 0:
            die(ctx->ctx, "Unexpected EOF");
        case ';':
            parse_take(ctx);
            goto directive_start;
        case '{':
            parse_take(ctx);
            cur->body = parse_block(ctx);
            if(ctx->c != '}')
                die(ctx->ctx, "Unexpected EOF");
            parse_take(ctx);
            goto directive_start;
        default:
            *arg = calloc(1, sizeof(struct cfg_arg));
            (*arg)->ctx = *ctx->ctx;
            (*arg)->data = parse_arg(ctx);
            arg = &(*arg)->next;
            goto directive_arg;
    }

    return head; /* unreachable */
}


static struct cfg_directive *parse_file(struct ctx *ctx) {
    size_t len;
    char *buf;
    int fd = strcmp(ctx->fn, "-") == 0 ? 0 : open(ctx->fn, 0);
    if(fd < 0)
        return NULL;

    len = slurp_fd(fd, &buf);
    if(len < 0)
        return NULL;
    close(fd);

    struct parse_ctx pc = { ctx, 0, len, buf, *buf };
    struct cfg_directive *ret = parse_block(&pc);
    if(pc.c == '}')
        die(pc.ctx, "Unexpected '}'");
    free(buf);
    return ret;
}




/**************
 *   FORMATTING
 */

static void write_str(FILE *f, const char *s) {
    if(fputs(s, f) < 0) {
        fprintf(stderr, "Error writing to output: %s", strerror(errno));
        exit(1);
    }
}

static void write_indent(FILE *f, int lvl) {
    int i;
    for(i=0; i<lvl; i++)
        write_str(f, "    ");
}


static void write_directive(FILE *f, int lvl, struct cfg_directive *d) {
    struct cfg_arg *arg = d->args;
    struct cfg_directive *body = d->body;

    if(d == empty_body)
        return;
    write_indent(f, lvl);
    write_str(f, d->name);
    for(; arg; arg=arg->next) {
        write_str(f, " ");
        write_str(f, arg->data);
    }

    if(body) {
        write_str(f, " {\n");
        for(; body && body != empty_body; body=body->next)
            write_directive(f, lvl+1, body);
        write_indent(f, lvl);
        write_str(f, "}\n");
    } else
        write_str(f, ";\n");
}




/**************
 *   PROCESSING
 */

struct proc_macro;

struct proc_data {
    /* Array of macros */
    size_t macrolen, macrosize;
    struct proc_macro **macros;
    /* Array of variables, each key/value pair is encoded as "key\0value\0"; values are already expanded and unquoted */
    size_t varlen, varsize;
    char **vars;
};

struct proc_macro {
    char *name;
    char **vars; /* Names of positional scalar arguments */
    char *array; /* Name of the @array argument */
    char *block; /* Name of the &block argument */
    struct proc_data data[1]; /* Variables and macros that were available when this macro was defined */
    struct cfg_directive *body;
};

struct proc_ctx {
    struct proc_data data[1];
    char **search_path;
    /* When expanding a macro: */
    struct proc_macro *macro;
    struct cfg_arg *macro_array;
    struct cfg_directive *macro_block;
};


/* Turns a quoted argument into an unquoted string. Returns a newly allocated string. */
static char *str_unquote(const char *str) {
    char quote = *str == '\'' || *str == '"' ? *(str++) : 0;
    char *ret = malloc(strlen(str)+1); /* Unquoted string will never be larger than the quoted version */
    char *cur = ret;
    while(*str != quote) {
        if(*str == '\\')
            str++;
        *(cur++) = *(str++);
    }
    *cur = 0;
    return ret;
}


/* Turns an unquoted string into a quoted argument using the given quoting style (", ' or 0). Returns a newly allocated string. Does not actually include the quotes. */
static char *str_quote(char quote, const char *strstart) {
    const char *str = strstart;
    char *ret = malloc(strlen(str)*2+1); /* Overly conservative, but w/e */
    char *cur = ret;
    while(*str) {
        if(*str == quote || (quote == 0 &&
                (*str == ' ' || *str == '\t' || *str == '\r' || *str == '\n' || *str == '\\' || *str == ';' || *str == '{' || (strstart == str && (*str == '"' || *str == '\'')))))
            *(cur++) = '\\';
        *(cur++) = *(str++);
    }
    *cur = 0;
    return ret;
}


/* Returns the length of the variable name starting at str, or 0 if there's no valid variable name */
static size_t str_varname(const char *str) {
    size_t len = 1;
    if(!isAlpha(*str) && !isNum(*str))
        return 0;
    while(isAlpha(str[len]) || isNum(str[len]) || str[len] == '_')
        len++;
    return len;
}


/* Makes a shallow copy of a proc_data struct. Copies the arrays but not the values, as these can be re-used accross multiple contexts */
static struct proc_data proc_data_copy(struct proc_data d) {
    struct proc_data r = d;
    r.vars = malloc(sizeof(*r.vars)*r.varsize);
    memcpy(r.vars, d.vars, sizeof(*r.vars)*r.varlen);
    r.macros = malloc(sizeof(*r.macros)*r.macrosize);
    memcpy(r.macros, d.macros, sizeof(*r.macros)*r.macrolen);
    return r;
}


/* Lookup a variable in the current context, returns NULL if not found. */
static char *proc_var_get(struct proc_ctx *ctx, const char *varname, size_t varlen) {
    size_t i;
    if(varlen == 0)
        return NULL;
    for(i=0; i<ctx->data->varlen; i++)
        if(strlen(ctx->data->vars[i]) == varlen && memcmp(ctx->data->vars[i], varname, varlen) == 0)
            return ctx->data->vars[i]+varlen+1;
    return NULL;
}


static void proc_var_set(struct proc_ctx *ctx, const char *name, const char *val) {
    size_t i;
    char *buf = malloc(strlen(name)+strlen(val)+2);
    strcpy(buf, name);
    strcpy(buf+strlen(name)+1, val);
    for(i=0; i<ctx->data->varlen; i++)
        if(strcmp(name, ctx->data->vars[i]) == 0) {
            /* Do not free the old buffer here, it may still be referenced by macros */
            ctx->data->vars[i] = buf;
            return;
        }
    if(ctx->data->varlen == ctx->data->varsize) {
        ctx->data->varsize *= 2;
        ctx->data->vars = realloc(ctx->data->vars, ctx->data->varsize*sizeof(*ctx->data->vars));
    }
    ctx->data->vars[ctx->data->varlen++] = buf;
}


/* Delete $0..$9 variables */
static void proc_var_del_num(struct proc_ctx *ctx) {
    size_t i = 0;
    while(i < ctx->data->varlen)
        if(isNum(*ctx->data->vars[i]) && !ctx->data->vars[i][1]) {
            ctx->data->varlen--;
            ctx->data->vars[i] = ctx->data->vars[ctx->data->varlen];
            ctx->data->vars[ctx->data->varlen] = NULL;
        } else
            i++;
}


/* Substitute variables in a string. Returns a newly allocated string.
 *
 * Interpolated variables will be converted to the quoting style of the containing string.
 * E.g.
 *   a = "abc{";  b = "x\yz";  c = "$something"
 *   $a       -> abc\{
 *   $a$b;    -> abc\{xyz
 *   "$a$b";  -> "abc{xyz"
 *   $c$a     -> $somethingabc\{   <- Or should this be ${something}abc\{? Current solution is simpler, but not sure what the expected behavior is here.
 */
static char *proc_subst_vars(struct proc_ctx *ctx, const char *str) {
    size_t len = 0, size = strlen(str)+1, varlen;
    const char *cur;
    char *var, *ret = malloc(size);
    char quote = *str == '\'' || *str == '"' ? *str : 0;
    struct cfg_arg *arg;

#define addc(c) do { if(len == size) { size *= 2; ret = realloc(ret, size); } ret[len++] = c; } while(0)

    while(*str) {
        if(*str == '\\') {
            addc(*(str++));
            addc(*(str++));
            continue;
        }
        if(ctx->macro && ctx->macro->array && *str == '$' && *(str+1) == '#') {
            cur = str + 2 + (*(str+2) == '{');
            varlen = str_varname(cur);
            if((*(str+2) != '{' || cur[varlen] == '}') &&
                    strlen(ctx->macro->array) == varlen &&
                    memcmp(ctx->macro->array, cur, varlen) == 0) {
                str += (*(str+2) == '{' ? 2 : 0) + 2 + varlen;
                varlen = 0;
                for(arg = ctx->macro_array; arg; arg = arg->next)
                    varlen++;
                if(varlen < 10) {
                    addc(varlen + '0');
                } else if(varlen < 100) {
                    addc((varlen / 10) + '0');
                    addc((varlen % 10) + '0');
                } else {
                    fprintf(stderr, "Unable to expand $#{%s}: too many arguments (%d)\n", ctx->macro->array, (int)varlen);
                    exit(1);
                }
                continue;
            }
        }
        if(*str == '$') {
            cur = str + 1 + (*(str+1) == '{');
            varlen = str_varname(cur);
            if((*(str+1) != '{' || cur[varlen] == '}') && (var = proc_var_get(ctx, cur, varlen)) != NULL) {
                str += (*(str+1) == '{' ? 2 : 0) + 1 + varlen;
                cur = var = str_quote(quote, var);
                while(*cur)
                    addc(*(cur++));
                free(var);
                continue;
            }
        }
        addc(*(str++));
    }
    addc(0);

#undef addc
    return ret;
}


static void proc_args(struct proc_ctx *ctx, struct cfg_arg **arg) {
    struct cfg_arg **a = arg, *next;
    char *tmp;
    while(*a) {
        if(ctx->macro && ctx->macro->array && *(*a)->data == '@' && strcmp((*a)->data+1, ctx->macro->array) == 0) {
            next = (*a)->next;
            free((*a)->data);
            free(*a);
            if(ctx->macro_array) {
                *a = cfg_arg_copy(ctx->macro_array);
                while((*a)->next)
                    a = &(*a)->next;
                (*a)->next = next;
                a = &(*a)->next;
            } else
                *a = next;
        } else {
            tmp = proc_subst_vars(ctx, (*a)->data);
            free((*a)->data);
            (*a)->data = tmp;
            a = &(*a)->next;
        }
    }
}


static void proc_block(struct proc_ctx *, struct cfg_directive **);


static void proc_include(struct proc_ctx *ctx, struct cfg_directive **block) {
    struct ctx parser = {};
    char *arg, **inc = ctx->search_path;
    struct cfg_directive *c = *block, *new;
    size_t l;

    if(!c->args)
        die(&c->ctx, "'pre_include' needs a filename argument");
    if(c->args->next)
        die(&c->ctx, "Too many arguments to 'pre_include'");

    arg = str_unquote(proc_subst_vars(ctx, c->args->data));
    if(strcmp(arg, "-") == 0)
        die(&c->ctx, "Can't include files from standard input");
    parser.parent = &c->ctx;

    parser.fn = arg;
    new = parse_file(&parser);
    while(*arg != '/' && !new && inc && *inc) {
        l = strlen(*inc) + strlen(arg) + 2;
        parser.fn = malloc(l);
        snprintf(parser.fn, l, "%s/%s", *inc, arg);
        new = parse_file(&parser);
        if(!new)
            free(parser.fn);
        inc++;
    }
    if(!new)
        die(&c->ctx, "Unable to include file '%s': %s", arg, strerror(errno));

    if(!new || new == empty_body)
        *block = c->next;
    else {
        *block = new;
        while(new && new->next)
            new = new->next;
        new->next = c->next;
    }
}


static void proc_set(struct proc_ctx *ctx, struct cfg_directive *c) {
    if(!c->args)
        die(&c->ctx, "'pre_set' requires two arguments, but found none");
    if(!c->args->next)
        die(&c->ctx, "'pre_set' requires two arguments, but found only one");
    if(c->args->next->next)
        die(&c->ctx, "Too many arguments to 'pre_set'");
    if(*c->args->data != '$' || str_varname(c->args->data+1) != strlen(c->args->data+1))
        die(&c->args->ctx, "Invalid variable name '%s'", c->args->data);
    proc_var_set(ctx, c->args->data+1, str_unquote(proc_subst_vars(ctx, c->args->next->data)));
}


static void proc_exec(struct proc_ctx *ctx, struct cfg_directive *c) {
    char *buf, *argv[4];
    size_t len;
    posix_spawn_file_actions_t fact;
    int status, fd[2];
    pid_t pid;

    if(!c->args)
        die(&c->ctx, "'pre_exec' requires two arguments, but found none");
    if(!c->args->next)
        die(&c->ctx, "'pre_exec' requires two arguments, but found only one");
    if(c->args->next->next)
        die(&c->ctx, "Too many arguments to 'pre_exec'");
    if(*c->args->data != '$' || str_varname(c->args->data+1) != strlen(c->args->data+1))
        die(&c->args->ctx, "Invalid variable name '%s'", c->args->data);

    argv[0] = "/bin/sh";
    argv[1] = "-c";
    argv[2] = str_unquote(proc_subst_vars(ctx, c->args->next->data));
    argv[3] = NULL;

    if(pipe(fd) < 0
        || posix_spawn_file_actions_init(&fact) < 0
        || posix_spawn_file_actions_addclose(&fact, fd[0]) < 0
        || posix_spawn_file_actions_adddup2(&fact, fd[1], 1) < 0
        || posix_spawn(&pid, "/bin/sh", &fact, NULL, argv, NULL) < 0
        || posix_spawn_file_actions_destroy(&fact) < 0)
        die(&c->ctx, "Error spawning process: %s", strerror(errno));

    close(fd[1]);
    len = slurp_fd(fd[0], &buf);
    if(len < 0)
        die(&c->ctx, "Error reading data from process: %s", strerror(errno));
    if(strlen(buf) != len)
        die(&c->ctx, "Invalid 0-byte in process output");
    close(fd[0]);

    waitpid(pid, &status, 0);
    if(WIFEXITED(status) && WEXITSTATUS(status) != 0)
        die(&c->ctx, "Process exited with error status %d", WEXITSTATUS(status));
    if(WIFSIGNALED(status))
        die(&c->ctx, "Process was killed by signal %d", WTERMSIG(status));

    if(len > 0 && buf[len-1] == '\n') /* Strip trailing newline */
        buf[len-1] = 0;
    proc_var_set(ctx, c->args->data+1, buf);
}


/* pre_if arguments are kind of annoying:
 *   Single arg:  x     ; (x)     ; ( x )     ; (x )     ; ( x)
 *   Two args:    x y   ; (x y)   ; ( x y )   ; (x y )   ; ( x y)
 *   Three args:  x y z ; (x y z) ; ( x y z ) ; (x y z ) ; ( x y z)
 *
 * This function normalizes to the first variant. */
static void proc_if_parse_args(struct ctx *ctx, struct cfg_arg *arg, char **out) {
    size_t i = 0;
    int braces = 0;
    out[0] = out[1] = out[2] = NULL;

    if(arg && *arg->data == '(') {
        braces = 1;
        if(!arg->data[1])
            arg = arg->next;
        else
            arg->data = arg->data+1;
    }

    while(arg) {
        if(braces && !arg->next) {
            if(arg->data[strlen(arg->data)-1] != ')')
                die(ctx, "Missing )");
            arg->data[strlen(arg->data)-1] = 0;
            if(!*arg->data)
                arg = arg->next;
            if(!arg)
                return;
        }
        if(i >= 3)
            die(ctx, "Too many arguments to 'pre_if'");
        out[i++] = arg->data;
        arg = arg->next;
    }
}


static int proc_if_infix(struct proc_ctx *ctx, struct ctx *if_ctx, char *a, char *op, char *b) {
    regex_t reg;
    regmatch_t match[9];
    size_t i, len;
    int r;
    char *tmp, buf[1024];

    if(strcmp(op, "==") == 0)
        return strcmp(a, b) == 0;
    if(strcmp(op, "!=") == 0)
        return strcmp(a, b) != 0;

    if(strcmp(op, "~") != 0 && strcmp(op, "~*") != 0 && strcmp(op, "!~") != 0 && strcmp(op, "!~*") != 0)
        die(if_ctx, "Unknown comparison operator '%s'", op);

    r = regcomp(&reg, b, REG_EXTENDED | (op[strlen(op)-1] == '*' ? REG_ICASE : 0));
    if(r != 0) {
        regerror(r, &reg, buf, sizeof(buf));
        die(if_ctx, "Invalid regular expression: %s", buf);
    }

    r = regexec(&reg, a, sizeof(match)/sizeof(*match), match, 0);
    if(r != 0 && r != REG_NOMATCH) {
        regerror(r, &reg, buf, sizeof(buf));
        die(if_ctx, "Error executing regular expression: %s", buf);
    }

    r = !r;
    if(r)
        proc_var_del_num(ctx);
    for(i=0; r && match[i].rm_so != -1; i++) {
        buf[0] = '0' + (char)i;
        buf[1] = '\0';
        len = match[i].rm_eo - match[i].rm_so;
        tmp = malloc(len + 1);
        memcpy(tmp, a+match[i].rm_so, len);
        tmp[len] = 0;
        proc_var_set(ctx, buf, tmp);
        r = 2;
    }

    regfree(&reg);
    return *op == '!' ? !r : r;
}


static int proc_if_cond(struct proc_ctx *ctx, struct ctx *if_ctx, char **arg) {
    int negate = 0, r;
    struct stat st;
    char *a, *b;

    /* Single argument: test if true/false */
    if(!arg[1]) {
        a = str_unquote(proc_subst_vars(ctx, arg[0]));
        return *a && strcmp(a, "0") != 0;
    }

    /* Two arguments: file tests */
    if(!arg[2]) {
        a = arg[0];
        b = str_unquote(proc_subst_vars(ctx, arg[1]));
        if(*a == '!') {
            a++;
            negate = 1;
        }
        if(*a != '-' || !a[1] || a[2])
            die(if_ctx, "Unknown argument to pre_if");

        if(stat(b, &st) < 0) {
            if(errno != ENOENT)
                die(if_ctx, "Unable to fetch file information for '%s': %s\n", b, strerror(errno));
            st.st_mode = 0;
        }

        switch(a[1]) {
            case 'f': r = S_ISREG(st.st_mode); break;
            case 'd': r = S_ISDIR(st.st_mode); break;
            case 'e': r = st.st_mode != 0; break;
            case 'x': r = st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_mode & S_IXOTH; break; /* Maybe use access() instead? */
            default: die(if_ctx, "Unknown file test flag '%s'", a);
        }
        return negate ? !r : r;
    }

    /* Three arguments: infix comparison operators */
    a = str_unquote(proc_subst_vars(ctx, arg[0]));
    b = str_unquote(proc_subst_vars(ctx, arg[2]));
    return proc_if_infix(ctx, if_ctx, a, arg[1], b);
}


static void proc_if(struct proc_ctx *ctx, struct cfg_directive **cur) {
    struct cfg_directive *tmp, *c = *cur;
    char *arg[3];
    int cond;

    proc_if_parse_args(&c->ctx, c->args, arg);
    if(!arg[0])
        die(&c->ctx, "No condition provided to 'pre_if'");
    if(!c->body)
        die(&c->ctx, "No block argument provided to 'pre_if'");

    cond = proc_if_cond(ctx, &c->ctx, arg);

    if(cond && c->body != empty_body) {
        proc_block(ctx, &c->body);
        if(c->body != empty_body) {
            tmp = *cur = c->body;
            while(tmp->next)
                tmp = tmp->next;
            tmp->next = c->next;
        }
    } else
        *cur = c->next;

    /* If we've set any $0 vars, make sure to delete them again */
    if(cond == 2)
        proc_var_del_num(ctx);
}


static void proc_macro(struct proc_ctx *ctx, struct cfg_directive *c) {
    struct proc_macro *m;
    struct cfg_arg *a;
    size_t i, varlen = 0, varsize = 0;

    if(!c->args)
        die(&c->ctx, "No macro name provided");
    if(!c->body)
        die(&c->ctx, "Macro definition without a body");
    if(strlen(c->args->data) != str_varname(c->args->data))
        die(&c->ctx, "Invalid macro name '%s'", c->args->data);

    m = calloc(1, sizeof(struct proc_macro));
    m->name = c->args->data;
    *m->data = proc_data_copy(*ctx->data);
    m->body = c->body;

    for(a=c->args->next; a; a=a->next) {
        if(!a->data[1] || strlen(a->data+1) != str_varname(a->data+1))
            die(&a->ctx, "Invalid variable name '%s'", a->data);

        if(*a->data == '$') {
            if(m->array || m->block)
                die(&a->ctx, "Invalid $scalar macro argument after @array or &block");
            if(varsize == varlen) {
                varsize = varsize ? varsize*2 : 8;
                m->vars = realloc(m->vars, sizeof(*m->vars)*(varsize+1));
            }
            m->vars[varlen++] = a->data+1;
            m->vars[varlen] = NULL;
        } else if(*a->data == '@') {
            if(m->array || m->block)
                die(&a->ctx, "Invalid @array macro argument after @array or &block");
            m->array = a->data+1;
        } else if(*a->data == '&') {
            if(m->block)
                die(&a->ctx, "Invalid duplicate &block macro argument");
            m->block = a->data+1;
        } else
            die(&a->ctx, "Invalid variable argument '%s'", a->data);
    }

    for(i=0; i<ctx->data->macrolen; i++)
        if(strcmp(ctx->data->macros[i]->name, m->name) == 0) {
            ctx->data->macros[i] = m;
            return;
        }
    if(ctx->data->macrolen == ctx->data->macrosize) {
        ctx->data->macrosize *= 2;
        ctx->data->macros = realloc(ctx->data->macros, sizeof(*ctx->data->macros)*ctx->data->macrosize);
    }
    ctx->data->macros[ctx->data->macrolen++] = m;
}


static struct cfg_directive **proc_directive(struct proc_ctx *ctx, struct cfg_directive **cur) {
    size_t i;
    struct proc_macro *m, *old_macro;
    struct proc_data old_data;
    struct cfg_arg *old_array;
    struct cfg_directive *old_block, *c = *cur;

    proc_args(ctx, &c->args);
    if(c->body)
        proc_block(ctx, &c->body);

    for(i=0; i<ctx->data->macrolen; i++)
        if(strcmp(ctx->data->macros[i]->name, c->name) == 0)
            break;
    if(i == ctx->data->macrolen)
        return &c->next;

    m = ctx->data->macros[i];
    old_data = proc_data_copy(*ctx->data);
    old_macro = ctx->macro;
    old_array = ctx->macro_array;
    old_block = ctx->macro_block;

    free(ctx->data->vars);
    free(ctx->data->macros);
    *ctx->data = proc_data_copy(*m->data);

    if(m->block && !c->body)
        die(&c->ctx, "Macro '%s' requires a block argument, none given", m->name);
    if(!m->block && c->body)
        die(&c->ctx, "Macro '%s' does not accept a block argument", m->name);
    for(i=0; m->vars && m->vars[i]; i++) {
        if(!c->args)
            die(&c->ctx, "Not enough arguments given to macro '%s'", m->name);
        proc_var_set(ctx, m->vars[i], str_unquote(c->args->data));
        c->args = c->args->next;
    }
    if(c->args && !m->array)
        die(&c->ctx, "Too many arguments given to macro '%s'", m->name);
    ctx->macro = m;
    ctx->macro_array = c->args;
    ctx->macro_block = c->body;

    /* TODO: Errors reported in the macro expansion will be given with the
     * context of the macro itself, but it would be nice to include the context
     * of the line that invoked the macro. */
    *cur = cfg_directive_copy(m->body);
    proc_block(ctx, cur);
    if(*cur == empty_body)
        *cur = c->next;
    else {
        while((*cur)->next)
            cur = &(*cur)->next;
        (*cur)->next = c->next;
        cur = &(*cur)->next;
    }

    free(ctx->data->vars);
    free(ctx->data->macros);
    *ctx->data = old_data;
    ctx->macro = old_macro;
    ctx->macro_array = old_array;
    ctx->macro_block = old_block;
    return cur;
}


static void proc_block(struct proc_ctx *ctx, struct cfg_directive **block) {
    struct cfg_directive **cur = block, *c = *block;
    struct cfg_arg *arg;

    while(*cur && *cur != empty_body) {
        c = *cur;
        if(strcmp(c->name, "pre_warn") == 0) {
            proc_args(ctx, &c->args);
            fputs("[warn]", stderr);
            for(arg=c->args; arg; arg=arg->next) {
                fputc(' ', stderr);
                fputs(str_unquote(arg->data), stderr);
            };
            fputc('\n', stderr);
            *cur = c->next;

        } else if(strcmp(c->name, "pre_include") == 0) {
            proc_include(ctx, cur);

        } else if(strcmp(c->name, "pre_set") == 0) {
            proc_set(ctx, *cur);
            *cur = c->next;

        } else if(strcmp(c->name, "pre_exec") == 0) {
            proc_exec(ctx, *cur);
            *cur = c->next;

        } else if(strcmp(c->name, "pre_if") == 0) {
            proc_if(ctx, cur);

        } else if(strcmp(c->name, "macro") == 0) {
            proc_macro(ctx, *cur);
            *cur = c->next;

        } else if(ctx->macro && ctx->macro->block && *c->name == '&' && strcmp(c->name+1, ctx->macro->block) == 0) {
            if(c->args)
                die(&c->ctx, "&block variable should not be given any arguments");
            if(c->body)
                die(&c->ctx, "Unexpected block after &block variable");
            if(ctx->macro_block && ctx->macro_block != empty_body) {
                *cur = cfg_directive_copy(ctx->macro_block);
                while((*cur)->next)
                    cur = &(*cur)->next;
                (*cur)->next = c->next;
                cur = &(*cur)->next;
            } else
                *cur = c->next;

        } else
            cur = proc_directive(ctx, cur);
    }
    if(!*block)
        *block = empty_body;
}




int main(int argc, char **argv) {
    struct cfg_directive *d;
    struct ctx toplevel = { "-", 0, 0, NULL };
    struct proc_ctx proc = {};
    char *output = "-";
    FILE *out_fh;
    int c;
    size_t searchlen = 0, searchsize = 0;
    static const struct option long_options[] = {
        {"help",    no_argument, 0, 'h' },
        {"version", no_argument, 0, 'V' },
        {0,         0,           0, 0   }
    };

    while((c = getopt_long(argc, argv, "hVi:o:I:", long_options, NULL)) >= 0) {
        switch(c) {
            case 'h':
                puts("Usage: nginx-confgen <options>\n");
                puts("  -h,--help       This help message");
                puts("  -V,--version    Print version");
                puts("  -i <FILE>       Read input from file, \"-\" for standard input");
                puts("  -o <FILE>       Write output to file, \"-\" for standard output");
                puts("  -I <DIR>        Add directory to the search path for 'pre_include'");
                exit(0);
            case 'V':
                printf("nginx-confgen %s\n", NGCFG_VERSION);
                exit(0);
            case 'i':
                toplevel.fn = optarg;
                break;
            case 'o':
                output = optarg;
                break;
            case 'I':
                if(searchlen == searchsize) {
                    searchsize = searchsize ? searchsize*2 : 8;
                    proc.search_path = realloc(proc.search_path, sizeof(*proc.search_path)*(1+searchsize));
                }
                proc.search_path[searchlen++] = optarg;
                proc.search_path[searchlen] = NULL;
                break;
            case '?':
                exit(1);
        }
    }
    if(optind < argc) {
        fprintf(stderr, "Unrecognized option: %s\n", argv[optind]);
        exit(1);
    }

    d = parse_file(&toplevel);
    if(!d)
        die(&toplevel, "Error reading input: %s", strerror(errno));

    proc.data->varsize = 32;
    proc.data->vars = malloc(proc.data->varsize*sizeof(*proc.data->vars));
    proc.data->macrosize = 32;
    proc.data->macros = malloc(proc.data->macrosize*sizeof(*proc.data->macros));
    proc_block(&proc, &d);

    if(strcmp(output, "-") == 0)
        out_fh = stdout;
    else if((out_fh = fopen(output, "w")) == NULL) {
        fprintf(stderr, "Error writing to '%s': %s\n", output, strerror(errno));
        exit(1);
    }
    for(; d; d=d->next)
        write_directive(out_fh, 0, d);
    return 0;
}
