/***********************************************************************
 *                                                                      *
 *               This software is part of the ast package               *
 *          Copyright (c) 1982-2012 AT&T Intellectual Property          *
 *                      and is licensed under the                       *
 *                 Eclipse Public License, Version 1.0                  *
 *                    by AT&T Intellectual Property                     *
 *                                                                      *
 *                A copy of the License is available at                 *
 *          http://www.eclipse.org/org/documents/epl-v10.html           *
 *         (with md5 checksum b35adb5213ca9657e911e9befb180842)         *
 *                                                                      *
 *              Information and Software Systems Research               *
 *                            AT&T Research                             *
 *                           Florham Park NJ                            *
 *                                                                      *
 *                    David Korn <dgkorn@gmail.com>                     *
 *                                                                      *
 ***********************************************************************/
//
// Bash style history expansion.
//
// Author:
// Karsten Fleischer
// Omnium Software Engineering
// An der Luisenburg 7
// D-51379 Leverkusen
// Germany
//
// <K.Fleischer@omnium.de>
//
#include "config_ast.h"  // IWYU pragma: keep

#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

#include "defs.h"
#include "edit.h"
#include "error.h"
#include "history.h"
#include "name.h"
#include "sfio.h"
#include "stk.h"

static char *modifiers = "htrepqxs&";
static int mod_flags[] = {0, 0, 0, 0, HIST_PRINT, HIST_QUOTE, HIST_QUOTE | HIST_QUOTE_BR, 0, 0};

#define DONE()            \
    flag |= HIST_ERROR;   \
    cp = NULL;            \
    stkseek(shp->stk, 0); \
    goto done

struct subst {
    char *str[2];  // [0] is "old", [1] is "new" string
};

//
// Parse an /old/new/ string, delimiter expected as first char.
// If "old" not specified, keep sb->str[0].
// If "new" not specified, set sb->str[1] to empty string.
// Read up to third delimeter char, \n or \0, whichever comes first.
// Return adress is one past the last valid char in s:
// - the address containing \n or \0 or
// - one char beyond the third delimiter
//
static char *parse_subst(Shell_t *shp, const char *s, struct subst *sb) {
    char *cp, del;
    int off, n = 0;

    // Build the strings on the stack, mainly for '&' substition in "new".
    off = stktell(shp->stk);

    // Init "new" with empty string
    if (sb->str[1]) free(sb->str[1]);
    sb->str[1] = strdup("");

    del = *s;  // get delimiter
    cp = (char *)s + 1;

    while (n < 2) {
        if (*cp == del || *cp == '\n' || *cp == '\0') {
            // Delimiter or EOL.
            if (stktell(shp->stk) != off) {
                // Dup string on stack and rewind stack.
                sfputc(shp->stk, '\0');
                if (sb->str[n]) free(sb->str[n]);
                sb->str[n] = strdup(stkptr(shp->stk, off));
                stkseek(shp->stk, off);
            }
            n++;

            // If not delimiter, we've reached EOL. Get outta here.
            if (*cp != del) break;
        } else if (*cp == '\\') {
            if (*(cp + 1) == del) {  // quote delimiter
                sfputc(shp->stk, del);
                cp++;
            } else if (*(cp + 1) == '&' && n == 1) {  // quote '&' only in "new"
                sfputc(shp->stk, '&');
                cp++;
            } else {
                sfputc(shp->stk, '\\');
            }
        } else if (*cp == '&' && n == 1 && sb->str[0]) {
            // Substitute '&' with "old" in "new".
            sfputr(shp->stk, sb->str[0], -1);
        } else {
            sfputc(shp->stk, *cp);
        }
        cp++;
    }

    stkseek(shp->stk, off);  // rewind stack

    return cp;
}

//
// History expansion main routine.
//
int hist_expand(Shell_t *shp, const char *ln, char **xp) {
    int off;                                  // stack offset
    int q;                                    // quotation flags
    int p;                                    // flag
    int c;                                    // current char
    int flag = 0;                             // HIST_* flags
    Sfoff_t n;                                // history line number, counter, etc.
    Sfoff_t i;                                // counter
    Sfoff_t w[2];                             // word range
    char *sp;                                 // stack pointer
    char *cp;                                 // current char in ln
    char *str;                                // search string
    char *evp;                                // event/word designator string, for error msgs
    char *cc = NULL;                          // copy of current line up to cp; temp ptr
    char hc[3];                               // default histchars
    char *qc = "\'\"`";                       // quote characters
    Sfio_t *ref = NULL;                       // line referenced by event designator
    Sfio_t *tmp = NULL;                       // temporary line buffer
    Sfio_t *tmp2 = NULL;                      // temporary line buffer
    Histloc_t hl;                             // history location
    static Namval_t *np = NULL;               // histchars variable
    static struct subst sb = {{NULL, NULL}};  // substition strings
    static Sfio_t *wm;                        // word match from !?string? event designator

    wm = sfopen(NULL, NULL, "swr");
    hc[0] = '!';
    hc[1] = '^';
    hc[2] = 0;
    np = nv_open("histchars", shp->var_tree, 0);
    if (np) {
        cp = nv_getval(np);
        if (cp && cp[0]) {
            hc[0] = cp[0];
            if (cp[1]) {
                hc[1] = cp[1];
                if (cp[2]) hc[2] = cp[2];
            }
        }
    }

    // Save shell stack.
    off = stktell(shp->stk);
    if (off) sp = stkfreeze(shp->stk, 0);

    cp = (char *)ln;

    while (cp && *cp) {
        // Read until event/quick substitution/comment designator.
        if ((*cp != hc[0] && *cp != hc[1] && *cp != hc[2]) || (*cp == hc[1] && cp != ln)) {
            if (*cp == '\\') {  // skip escaped designators
                sfputc(shp->stk, *cp++);
            } else if (*cp == '\'') {  // skip quoted designators
                do {
                    sfputc(shp->stk, *cp);
                } while (*++cp && *cp != '\'');
            }
            sfputc(shp->stk, *cp++);
            continue;
        }

        if (hc[2] && *cp == hc[2]) {  // history comment designator, skip rest of line
            sfputc(shp->stk, *cp++);
            sfputr(shp->stk, cp, -1);
            DONE();
        }

        n = -1;
        str = 0;
        flag &= HIST_EVENT;  // save event flag for returning later
        evp = cp;
        ref = 0;

        if (*cp == hc[1]) {  // shortcut substitution
            flag |= HIST_QUICKSUBST;
            goto getline;
        }

        if (*cp == hc[0] && *(cp + 1) == hc[0]) {  // refer to line -1
            cp += 2;
            goto getline;
        }

        switch (c = *++cp) {
            case ' ':
            case '\t':
            case '\n':
            case '\0':
            case '=':
            case '(': {
                sfputc(shp->stk, hc[0]);
                continue;
            }
            case '#': {  // the line up to current position
                flag |= HIST_HASH;
                cp++;
                n = stktell(shp->stk);  // terminate string and dup
                sfputc(shp->stk, '\0');
                cc = strdup(stkptr(shp->stk, 0));
                stkseek(shp->stk, n);        // remove null byte again
                ref = sfopen(ref, cc, "s");  // open as file
                n = 0;                       // skip history file referencing
                break;
            }
            case '-': {  // back reference by number
                if (!isdigit(*(cp + 1))) goto string_event;
                cp++;
            }
            // FALLTHRU
            case '0':  // reference by number
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9': {
                n = 0;
                while (isdigit(*cp)) n = n * 10 + (*cp++) - '0';
                if (c == '-') n = -n;
                break;
            }
            case '$': {
                n = -1;
            }
            case ':': {
                break;
            }
            case '?': {
                cp++;
                flag |= HIST_QUESTION;
            }
            // FALLTHRU
            string_event:
            default: {
                // Read until end of string or word designator/modifier.
                str = cp;
                while (*cp) {
                    cp++;
                    if ((!(flag & HIST_QUESTION) &&
                         (*cp == ':' || isspace(*cp) || *cp == '^' || *cp == '$' || *cp == '*' ||
                          *cp == '-' || *cp == '%')) ||
                        ((flag & HIST_QUESTION) && (*cp == '?' || *cp == '\n'))) {
                        c = *cp;
                        *cp = '\0';
                    }
                }
                break;
            }
        }

    getline:
        flag |= HIST_EVENT;
        if (str) {  // !string or !?string? event designator
            // Search history for string.
            hl = hist_find(shgd->hist_ptr, str, shgd->hist_ptr->histind, flag & HIST_QUESTION, -1);
            if ((n = hl.hist_command) == -1) n = 0;  // not found
        }
        if (n) {
            if (n < 0) {  // determine index for backref
                n = shgd->hist_ptr->histind + n;
            }
            // Search and use history file if found.
            if (n > 0 && hist_seek(shgd->hist_ptr, n) != -1) ref = shgd->hist_ptr->histfp;
        }
        if (!ref) {
            // String not found or command # out of range.
            c = *cp;
            *cp = '\0';
            errormsg(SH_DICT, ERROR_ERROR, "%s: event not found", evp);
            *cp = c;
            DONE();
        }

        if (str) {  // string search: restore orig. line
            if (flag & HIST_QUESTION) {
                *cp++ = c;  // skip second question mark
            } else {
                *cp = c;
            }
        }

        // Colon introduces either word designators or modifiers.
        if (*(evp = cp) == ':') cp++;

        w[0] = 0;   // -1 means last word, -2 means match from !?string?
        w[1] = -1;  // -1 means last word, -2 means suppress last word

        if (flag & HIST_QUICKSUBST) {  // shortcut substitution
            goto getsel;
        }

        n = 0;
        while (n < 2) {
            switch (c = *cp++) {
                case '^': {  // first word
                    if (n == 0) {
                        w[0] = w[1] = 1;
                        goto skip;
                    } else {
                        goto skip2;
                    }
                }
                case '$': {  // last word
                    w[n] = -1;
                    goto skip;
                }
                case '%': {  // match from !?string? event designator
                    if (n == 0) {
                        if (!str) {
                            w[0] = 0;
                            w[1] = -1;
                            ref = wm;
                        } else {
                            w[0] = -2;
                            w[1] = sftell(ref) + hl.hist_char;
                        }
                        sfseek(wm, 0, SEEK_SET);
                        goto skip;
                    }
                    goto skip2;
                }
                case '*': {  // until last word
                    if (n == 0) w[0] = 1;
                    w[1] = -1;
                skip:
                    flag |= HIST_WORDDSGN;
                    n = 2;
                    break;
                }
                case '-': {  // until last word or specified index
                    w[1] = -2;
                    flag |= HIST_WORDDSGN;
                    n = 1;
                    break;
                }
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9': {  // specify index
                    if ((*evp == ':') || w[1] == -2) {
                        w[n] = c - '0';
                        while (isdigit(c = *cp++)) w[n] = w[n] * 10 + c - '0';
                        flag |= HIST_WORDDSGN;
                        if (n == 0) w[1] = w[0];
                        n++;
                    } else {
                        n = 2;
                    }
                    cp--;
                    break;
                }
                default: {
                skip2:
                    cp--;
                    n = 2;
                    break;
                }
            }
        }

        if (w[0] != -2 && w[1] > 0 && w[0] > w[1]) {
            c = *cp;
            *cp = '\0';
            errormsg(SH_DICT, ERROR_ERROR, "%s: bad word specifier", evp);
            *cp = c;
            DONE();
        }

        // No valid word designator after colon, rewind.
        if (!(flag & HIST_WORDDSGN) && (*evp == ':')) cp = evp;

    getsel:
        // Open temp buffer, let sfio do the (re)allocation.
        tmp = sfopen(NULL, NULL, "swr");

        // Push selected words into buffer, squash whitespace into single blank or a newline.
        n = i = q = 0;

        while ((c = sfgetc(ref)) > 0) {
            if (isspace(c)) {
                flag |= (c == '\n' ? HIST_NEWLINE : 0);
                continue;
            }

            if (n >= w[0] && ((w[0] != -2) ? (w[1] < 0 || n <= w[1]) : 1)) {
                if (w[0] < 0) {
                    sfseek(tmp, 0, SEEK_SET);
                } else {
                    i = sftell(tmp);
                }

                if (i > 0) sfputc(tmp, flag & HIST_NEWLINE ? '\n' : ' ');
                flag &= ~HIST_NEWLINE;
                p = 1;
            } else {
                p = 0;
            }

            do {
                cc = strchr(qc, c);
                q ^= cc ? 1 << (int)(cc - qc) : 0;
                if (p) sfputc(tmp, c);
            } while ((c = sfgetc(ref)) > 0 && (!isspace(c) || q));

            if (w[0] == -2 && sftell(ref) > w[1]) break;
            flag |= (c == '\n' ? HIST_NEWLINE : 0);
            n++;
        }
        if (w[0] != -2 && w[1] >= 0 && w[1] >= n) {
            c = *cp;
            *cp = '\0';
            errormsg(SH_DICT, ERROR_ERROR, "%s: bad word specifier", evp);
            *cp = c;
            DONE();
        } else if (w[1] == -2) {  // skip last word
            sfseek(tmp, i, SEEK_SET);
        }

        // Remove trailing newline.
        if (sftell(tmp)) {
            sfseek(tmp, -1, SEEK_CUR);
            if (sfgetc(tmp) == '\n') sfungetc(tmp, '\n');
        }

        sfputc(tmp, '\0');

        if (str) {
            if (wm) sfclose(wm);
            wm = tmp;
        }

        if (cc && (flag & HIST_HASH)) {
            // Close !# temp file.
            sfclose(ref);
            flag &= ~HIST_HASH;
            free(cc);
            cc = 0;
        }

        evp = cp;

        // Selected line/words are now in buffer, now go for the modifiers.
        while (*cp == ':' || (flag & HIST_QUICKSUBST)) {
            if (flag & HIST_QUICKSUBST) {
                flag &= ~HIST_QUICKSUBST;
                c = 's';
                cp--;
            } else {
                c = *++cp;
            }

            sfseek(tmp, 0, SEEK_SET);
            tmp2 = sfopen(tmp2, NULL, "swr");

            if (c == 'g') {  // global substitution
                flag |= HIST_GLOBALSUBST;
                c = *++cp;
            }

            cc = strchr(modifiers, c);
            if (cc) {
                flag |= mod_flags[cc - modifiers];
            } else {
                errormsg(SH_DICT, ERROR_ERROR, "%c: unrecognized history modifier", c);
                DONE();
            }

            if (c == 'h' || c == 'r') {  // head or base
                n = -1;
                while ((c = sfgetc(tmp)) > 0) {  // remember position of / or .
                    if ((c == '/' && *cp == 'h') || (c == '.' && *cp == 'r')) n = sftell(tmp2);
                    sfputc(tmp2, c);
                }
                if (n > 0) {  // rewind to last / or .
                    sfseek(tmp2, n, SEEK_SET);
                    // End string there.
                    sfputc(tmp2, '\0');
                }
            } else if (c == 't' || c == 'e') {  // tail or suffix
                n = 0;
                while ((c = sfgetc(tmp)) > 0) { /* remember position of / or . */
                    if ((c == '/' && *cp == 't') || (c == '.' && *cp == 'e')) n = sftell(tmp);
                }
                // Rewind to last / or .
                sfseek(tmp, n, SEEK_SET);
                // Copy from there on.
                while ((c = sfgetc(tmp)) > 0) sfputc(tmp2, c);
            } else if (c == 's' || c == '&') {
                cp++;

                if (c == 's') {
                    // Preset old with match from !?string?.
                    if (!sb.str[0] && wm) {
                        char *sbuf = sfgetbuf(wm);
                        int n = sftell(wm);
                        sb.str[0] = malloc(n + 1);
                        sb.str[0][n] = '\0';
                        memcpy(sb.str[0], sbuf, n);
                    }
                    cp = parse_subst(shp, cp, &sb);
                }

                if (!sb.str[0] || !sb.str[1]) {
                    c = *cp;
                    *cp = '\0';
                    errormsg(SH_DICT, ERROR_ERROR, "%s%s: no previous substitution",
                             (flag & HIST_QUICKSUBST) ? ":s" : "", evp);
                    *cp = c;
                    DONE();
                }

                str = sfgetbuf(tmp);  // need pointer for strstr()
                flag |= HIST_SUBSTITUTE;
                while (flag & HIST_SUBSTITUTE) {
                    // Find string.
                    cc = strstr(str, sb.str[0]);
                    if (cc) {  // replace it
                        c = *cc;
                        *cc = '\0';
                        sfputr(tmp2, str, -1);
                        sfputr(tmp2, sb.str[1], -1);
                        *cc = c;
                        str = cc + strlen(sb.str[0]);
                    } else if (!sftell(tmp2)) {  // not successfull
                        c = *cp;
                        *cp = '\0';
                        errormsg(SH_DICT, ERROR_ERROR, "%s%s: substitution failed",
                                 (flag & HIST_QUICKSUBST) ? ":s" : "", evp);
                        *cp = c;
                        DONE();
                    }
                    // Loop if g modifier specified.
                    if (!cc || !(flag & HIST_GLOBALSUBST)) flag &= ~HIST_SUBSTITUTE;
                }
                // Output rest of line.
                sfputr(tmp2, str, -1);
                if (*cp) cp--;
            }

            if (sftell(tmp2)) {  // if any substitions done, swap buffers
                if (wm != tmp) sfclose(tmp);
                tmp = tmp2;
                tmp2 = 0;
            }
            cc = 0;
            if (*cp) cp++;
        }

        // Flush temporary buffer to stack.
        if (!tmp) continue;
        sfseek(tmp, 0, SEEK_SET);
        if (flag & HIST_QUOTE) sfputc(shp->stk, '\'');

        while ((c = sfgetc(tmp)) > 0) {
            if (isspace(c)) {
                flag = flag & ~HIST_NEWLINE;

                // Squash white space to either a blank or a newline.
                do {
                    flag |= (c == '\n' ? HIST_NEWLINE : 0);
                } while ((c = sfgetc(tmp)) > 0 && isspace(c));

                sfungetc(tmp, c);

                c = (flag & HIST_NEWLINE) ? '\n' : ' ';

                if (flag & HIST_QUOTE_BR) {
                    sfputc(shp->stk, '\'');
                    sfputc(shp->stk, c);
                    sfputc(shp->stk, '\'');
                } else {
                    sfputc(shp->stk, c);
                }
            } else if ((c == '\'') && (flag & HIST_QUOTE)) {
                sfputc(shp->stk, '\'');
                sfputc(shp->stk, '\\');
                sfputc(shp->stk, c);
                sfputc(shp->stk, '\'');
            } else {
                sfputc(shp->stk, c);
            }
        }
        if (flag & HIST_QUOTE) sfputc(shp->stk, '\'');
    }

    sfputc(shp->stk, '\0');

done:
    if (cc && (flag & HIST_HASH)) {  // close !# temp file
        sfclose(ref);
        free(cc);
        cc = 0;
    }

    // Error?
    if (stktell(shp->stk) && !(flag & HIST_ERROR)) *xp = strdup(stkfreeze(shp->stk, 1));

    // Restore shell stack.
    if (off) {
        stkset(shp->stk, sp, off);
    } else {
        stkseek(shp->stk, 0);
    }

    // Drop temporary files.
    if (tmp && tmp != wm) sfclose(tmp);
    if (tmp2) sfclose(tmp2);

    return flag & HIST_ERROR ? HIST_ERROR : flag & HIST_FLAG_RETURN_MASK;
}
