// SPDX-License-Identifier: GPL-2.0-only
// Copyright (c) 1997-2007 Ralf S. Engelschall.

#include "config.h"
#include "iselect.h"
#include "enter.h"

#include <stdio.h>
#include <errno.h>
#include <wctype.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <curses.h>

#define max(a, b) ((a) > (b) ? (a) : (b))


Line *spaLines;  /* filled by main(), corrected by iSelect() */
size_t  nLines;


/*
 *  Strip leading and trailing blanks
 *  from a string buffer in ln->cpResult and ln->nResult
 */
static void strip(Line *ln)
{
    mbstate_t state = {0};
    wchar_t c;
    size_t r = 0;
    while (ln->nResult)
        switch (r = mbrtowc(&c, ln->cpResult, ln->nResult, &state)) {
            case (size_t)-2:  // too short
                return;
            case (size_t)-1:  // EILSEQ
                state = (mbstate_t){0};
                // FALLTHROUGH
            case 0:
                r = 1;
                c = -1;
                // FALLTHROUGH
            default:
                if (c == (wchar_t)-1 || !iswspace(c))
                    goto break2;

                ln->cpResult += r;
                ln->nResult  -= r;
        }
break2:;

    size_t oklen = r, spacelen = 0;
    while (oklen + spacelen != ln->nResult)
        switch (r = mbrtowc(&c, ln->cpResult + oklen + spacelen, ln->nResult - (oklen + spacelen), &state)) {
            case (size_t)-2:  // too short
                return;
            case (size_t)-1:  // EILSEQ
                state = (mbstate_t){0};
                // FALLTHROUGH
            case 0:
                r = 1;
                c = -1;
                // FALLTHROUGH
            default:
                if (c == (wchar_t)-1 || !iswspace(c)) {
                    oklen += spacelen + r;
                    spacelen = 0;
                }
                else
                    spacelen += r;
        }
    ln->nResult -= spacelen;
}

/*
 *  Die gracefully...
 */
static void diehard(int sig)
{
    (void) sig;
    endwin();
    exit(0);
}

char  *boldbegin,    *boldend;
size_t boldbegin_len, boldend_len;
static void iSelect_PrepTagStrs(const char *tagbegin, const char *tagend)
{
    boldbegin_len = asprintf(&boldbegin, "%sb%s",  tagbegin, tagend);
    boldend_len   = asprintf(&boldend,   "%s/b%s", tagbegin, tagend);
}

/*
 *  Function to draw a complete screen
 */
static void iSelect_Draw(const Line *spaLines,
                  WINDOW *wField,
                  int wXSize,
                  int nAbsFirstLine,
                  size_t nRelMarked, bool bRelMarked,
                  size_t nRelFirstDraw, size_t nRelLastDraw,
                  size_t nLines,
                  WINDOW *sField, const char *title, const char *name,
                  WINDOW *mField, const char *msg)
{
    /*
     *  draw browser window
     */
    for (size_t i = nRelFirstDraw; i <= nRelLastDraw && nAbsFirstLine+nRelFirstDraw+i < nLines; ++i) {
        const Line *l = &spaLines[nAbsFirstLine+nRelFirstDraw+i];
        wmove(wField, i, 0);
        wclrtoeol(wField);
        int mode = A_NORMAL;
        if (l->fSelectable)
            mode |= A_BOLD;
        if (bRelMarked && i == nRelMarked)
            mode |= A_REVERSE;
        wattrset(wField, mode);
        waddch(wField, l->fSelected ? '*' : ' ');  // cursor pos == first blank

        int curx = 0;
        size_t collectbeg = 0, collectend = 0, boldlen;
        for (size_t j = 0; curx <= wXSize-1 && j < l->nLine;) {
            if (l->nLine - j >= boldbegin_len && !strncasecmp(&l->cpLine[j], boldbegin, boldlen = boldbegin_len)) {
                mode |= A_BOLD;
                goto flush;
            }
            if (l->nLine - j >= boldend_len   && !strncasecmp(&l->cpLine[j], boldend,   boldlen = boldend_len)) {
                mode &= ~A_BOLD;
                goto flush;
            }
            if (!l->cpLine[j] && (boldlen = 1)) {
            flush:
                waddnstr(wField, l->cpLine + collectbeg, collectend - collectbeg);
                curx = getcurx(wField);
                wattrset(wField, mode);
                j += boldlen;
                collectbeg = collectend = j;
                continue;
            }
            ++j;
            ++collectend;
        }
        waddnstr(wField, l->cpLine + collectbeg, collectend - collectbeg);
        curx = getcurx(wField);

        while (curx <= (wXSize-1)-1)
            waddch(wField, ' '), ++curx;
        wattrset(wField, A_NORMAL);
    }
    if (wmove(wField, nRelLastDraw + 1, 0) != ERR)
        wclrtobot(wField);
    wmove(wField, nRelMarked, wXSize-1);

    /*
     *  draw status bar
     */
    werase(sField);
    mvwaddstr(sField, 0, 0, title);
    int title_width = getcurx(sField);
    wmove(sField, 0, 0);
    while (waddch(sField, '\t') != ERR)
        ;

    mvwaddstr(sField, 0, 1, name);

    int percent = nLines ? (int)(((nAbsFirstLine+nRelMarked+1)*100)/nLines) : 100;
    char ca[80];
    int ca_width = snprintf(ca, sizeof(ca), "%4zu,%3d%%", 1+nAbsFirstLine+nRelMarked, percent > 100 ? 100 : percent);
    mvwaddnstr(sField, 0, COLS - ca_width - 1, ca, ca_width);

    mvwaddstr(sField, 0, (COLS-1)/2-(title_width/2), title);

    wrefresh(sField);

    /*
     *  draw message field
     */
    mvwaddstr(mField, 0, 0, msg);
    wclrtoeol(mField);
    wrefresh(mField);
}

/*
 *  Function to do a complete selection screen
 */
static ssize_t iSelect_Browser(size_t wYSize, size_t wXSize, int wYPos, int wXPos, size_t selectpos, bool multiselect,
                    int sYSize, int sXSize, int sYPos, int sXPos, const char *title, const char *name,
                    int mYSize, int mXSize, int mYPos, int mXPos,
                    enum iSelect_key *unmapped_key)
{
    size_t nAbsFirstLine, nAbsLastLine; /* first & last line of output buffer */
    size_t nRelMarked;                  /* relative line inside output buffer of marked line */
    size_t nRelFirstDraw, nRelLastDraw; /* relative first & last line inside output buffer */
    enum iSelect_key c;
    bool bEOI = false;
    bool bQuit = false;
    const char *msg = "";

    /*
     *  Browser field
     */
    WINDOW *wField = newwin(wYSize, wXSize, wYPos, wXPos);
    werase(wField);
    cbreak();
    noecho();
    keypad(wField, true);

    /*
     *  Status field
     */
    WINDOW *sField = newwin(sYSize, sXSize, sYPos, sXPos);
    werase(sField);
    wattrset(sField, A_REVERSE);

    /*
     *  Message field
     */
    WINDOW *mField = newwin(mYSize, mXSize, mYPos, mXPos);
    werase(mField);
    keypad(mField, true);

    /* first & last line in buffer */
    const size_t nFirstLine = 0;
    const size_t nLastLine  = nLines ? nLines-1 : 0;

    /* determine curses select position */
    selectpos = max(selectpos, 1) - 1;
    if (selectpos > nLastLine)
        selectpos = nLastLine;

    /* calculate browser view borders */
    if (nLastLine < (wYSize-1)) {
        /* buffer has fewer lines then our browser window */

        nAbsFirstLine = nFirstLine;
        nAbsLastLine  = nLastLine;
        nRelFirstDraw = 0;
        nRelLastDraw  = nLastLine-nFirstLine;
        nRelMarked    = selectpos;
    }
    else {
        /* browser window is smaller then file */

        /* find top view position, so adjust the
           cursor into the middle of the browser window */
        ssize_t y = selectpos - (int)((wYSize-1)/2);
        if (y <= 0)
            y = 0;
        if (y+(wYSize-1) > nLastLine)
            y = nLastLine-(wYSize-1);

        nAbsFirstLine = y;
        nAbsLastLine  = y+(wYSize-1);
        nRelFirstDraw = 0;
        nRelLastDraw  = (wYSize-1);
        nRelMarked    = selectpos-y;
    }


    bool ok = false;
    for (size_t i = nFirstLine; i < nLines; ++i) {
        if (spaLines[i].fSelectable) {
            ok = true;
            break;
        }
    }
    if (!ok)
        msg = "WARNING! No lines selectable.";


    while (!bEOI) {
         iSelect_Draw(spaLines,
                      wField,
                      wXSize,
                      nAbsFirstLine,
                      nRelMarked, true,
                      nRelFirstDraw, nRelLastDraw,
                      nLines,
                      sField, title, name,
                      mField, msg);
        msg = "";
        switch (c = mapped_key(wField, unmapped_key)) {
            case iSelect_NextLine:
                if (nAbsFirstLine+nRelMarked < nAbsLastLine) {
                    nRelMarked++;
                    /* nRelFirstDraw=nRelMarked-1; !!OPTIMIZE!! */
                    /* nRelLastDraw=nRelMarked;    !!OPTIMIZE!! */
                }
                else {
                    if (nAbsLastLine < nLastLine) {
                        wscrl(wField, 1);
                        nAbsFirstLine++;
                        nAbsLastLine++;
                        /* nRelFirstDraw=(wYSize-1); !!OPTIMIZE!! */
                        /* nRelLastDraw=(wYSize-1);  !!OPTIMIZE!!*/
                    }
                    else {
                        msg = "Already at End.";
                    }
                }
                break;
            case iSelect_PrevLine:
                if (nRelMarked > 0) {
                    nRelMarked--;
                    /* nRelLastDraw=nRelMarked;    !!OPTIMIZE!! */
                    /* nRelFirstDraw=nRelMarked+1; !!OPTIMIZE!! */
                }
                else {
                    if (nAbsFirstLine > nFirstLine) {
                        wscrl(wField, -1);
                        nAbsFirstLine--;
                        nAbsLastLine--;
                        /* nRelFirstDraw=0 !!OPTIMIZE!! */
                        /* nRelLastDraw=0; !!OPTIMIZE!! */
                    }
                    else {
                        msg = "Already at Begin.";
                    }
                }
                break;
            case iSelect_NextPage:
                if (nAbsFirstLine+nRelMarked == nLastLine) {
                    msg = "Already at End.";
                }
                else {
                    for (size_t i = 0; i < (wYSize-1); ++i) {
                        if (nAbsFirstLine+nRelMarked < nAbsLastLine)
                            nRelMarked++;
                        else {
                            if (nAbsLastLine < nLastLine) {
                                wscrl(wField, 1);
                                nAbsFirstLine++;
                                nAbsLastLine++;
                            }
                        }
                    }
                }
                break;
            case iSelect_PrevPage:
                if (nAbsFirstLine+nRelMarked == nFirstLine) {
                    msg = "Already at Begin.";
                }
                else {
                    for (size_t i = 0; i < (wYSize-1); ++i) {
                        if (nRelMarked > 0)
                            nRelMarked--;
                        else {
                            if (nAbsFirstLine > nFirstLine) {
                                wscrl(wField, -1);
                                nAbsFirstLine--;
                                nAbsLastLine--;
                            }
                        }
                    }
                }
                break;
            case L'\n':  // RETURN
                    if (spaLines[nAbsFirstLine+nRelMarked].fSelectable) {
                        spaLines[nAbsFirstLine+nRelMarked].fSelected = true;
                        bEOI = true;
                    }
                    else {
                        if (multiselect) {
                            for (size_t i = 0; i < nLines; ++i) {
                                if (spaLines[i].fSelected) {
                                    bEOI = true;
                                    break;
                                }
                            }
                            if (!bEOI)
                                msg = "Line not selectable and still no others selected.";
                        }
                        else {
                            msg = "Line not selectable.";
                        }
                    }

                    /* additionally ask for query strings */
                    size_t first_visible = nAbsFirstLine;
                    for (size_t i = 0; bEOI && i < nLines; ++i) {
                        if (!spaLines[i].fSelected)
                            continue;

                        Line *l = &spaLines[i];
                        char *sub_start = memmem(l->cpResult, l->nResult, "%[", 2);
                        if (sub_start) {
                            if (i < first_visible || i >= first_visible + wYSize)
                                first_visible = i;
                            if (first_visible + wYSize > nLines)
                                first_visible = nLines - wYSize;
                            iSelect_Draw(spaLines, wField, wXSize,
                                         first_visible,
                                         i - first_visible, true,
                                         nRelFirstDraw, nRelLastDraw,
                                         nLines, sField, title, name, mField, msg);
                            wrefresh(wField);

                            free(l->cpFmtResult);
                            FILE *res = open_memstream(&l->cpFmtResult, &l->nFmtResult);

                            fwrite(l->cpResult, 1, sub_start - l->cpResult, res);
                            char *window  = l->cpResult + (sub_start - l->cpResult);
                            size_t winlen = l->nResult  - (sub_start - l->cpResult);
                            for (;;) {
                                char *sub_end = memchr(window + 2, ']', winlen - 2);
                                if (sub_end && !(sub_end[1] == 's' || sub_end[1] == 'S'))
                                    sub_end = NULL;
                                if (!sub_end) {
                                nosubs:
                                    fwrite(window, 1, winlen, res);
                                    break;
                                }

                                window += 2, winlen -= 2;
                                size_t sublen = sub_end - window;
                                wmove(mField, 0, 0);
                                while(sublen) {
                                    waddnstr(mField, window, sublen);
                                    size_t bit = strnlen(window, sublen);
                                    window += bit, winlen -= bit, sublen -= bit;
                                    while(sublen && !*window)
                                        ++window, --winlen, --sublen;
                                }
                                waddstr(mField, ": ");
                                window += 2, winlen -= 2;

                                if (!enter_string(mField, res, sub_end[1] == 's')) {
                                    bEOI = false;
                                    l->fSelected = false;
                                    msg = "Selection cancelled.";
                                    break;
                                }
                                msg = "";

                                sub_start = memmem(window, winlen, "%[", 2);
                                if(!sub_start)
                                    goto nosubs;
                                size_t constlen = sub_start - window;
                                fwrite(window, 1, constlen, res);
                                window += constlen, winlen -= constlen;
                            }

                            fclose(res);
                        }
                    }
                break;
            case L' ':
                if (multiselect) {
                    if (spaLines[nAbsFirstLine+nRelMarked].fSelectable) {
                        spaLines[nAbsFirstLine+nRelMarked].fSelected ^= 1;
                    }
                    else {
                        msg = "Line not selectable.";
                    }
                }
                else
                nomultiselect:
                    msg = "No multi-line selection allowed.";
                break;
            case L'C':
                if (multiselect) {
                    for (size_t i = 0; i < nLines; ++i)
                        spaLines[i].fSelected = false;
                }
                else
                    goto nomultiselect;
                break;
            case L'q':
                bEOI = true;
                bQuit = true;
                break;
            case L'g':
                if (nAbsFirstLine+nRelMarked == nFirstLine)
                    msg = "Already at Begin.";
                else {
                    if (nLastLine < (wYSize-1)) {
                        nAbsFirstLine = nFirstLine;
                        nAbsLastLine  = nLastLine;
                        nRelFirstDraw = 0;
                        nRelLastDraw  = nLastLine-nFirstLine;
                        nRelMarked    = 0;
                    }
                    else {
                        nAbsFirstLine = nFirstLine;
                        nAbsLastLine  = nFirstLine+(wYSize-1);
                        nRelFirstDraw = 0;
                        nRelLastDraw  = (wYSize-1);
                        nRelMarked    = 0;
                    }
                }
                break;
            case L'G':
                if (nAbsFirstLine+nRelMarked == nLastLine)
                    msg = "Already at End.";
                else {
                    if (nLastLine < (wYSize-1)) {
                        nAbsFirstLine = nFirstLine;
                        nAbsLastLine  = nLastLine;
                        nRelFirstDraw = 0;
                        nRelLastDraw  = nLastLine-nFirstLine;
                        nRelMarked    = nLastLine-nFirstLine;
                    }
                    else {
                        nAbsFirstLine = nLastLine-(wYSize-1);
                        nAbsLastLine  = nLastLine;
                        nRelFirstDraw = 0;
                        nRelLastDraw  = (wYSize-1);
                        nRelMarked    = (wYSize-1);
                    }
                }
                break;
            case L'h':
            case L'v':
            help_version:
                if (c == 'h')
                    msg = "Help Page: Press 'q' to exit";
                else
                    msg = "Version Page: Press 'q' to exit";
                const Line *cpp = (c == 'h') ? iSelect_Help : iSelect_README;
                size_t cpp_len = 0;
                for (const Line *c = cpp; c->cpLine; ++c)
                   ++cpp_len;
                size_t first = 0;

                curs_set(0);
                while (c != 'q') {
                    iSelect_Draw(cpp, wField, wXSize, first, wYSize - 1, false, 0, wYSize - 1, cpp_len, sField, title, name, mField, msg);

                    switch (c = mapped_key(wField, unmapped_key)) {
                        case iSelect_NextLine:
                            if (first + wYSize < cpp_len)
                                ++first;
                            break;
                        case iSelect_PrevLine:
                            if (first > 0)
                                --first;
                            break;
                        case iSelect_NextPage:
                            if (first + wYSize + wYSize - 1 < cpp_len)
                                first += wYSize;
                            else
                                first = cpp_len - wYSize;
                            break;
                        case iSelect_PrevPage:
                            first = max(first, wYSize) - wYSize;
                            break;
                        case L'\n':  // ignored
                        case L' ':   // ignored
                        case L'C':   // ignored
                        case L'q':   // loop condition
                            break;
                        case L'g':
                            first = 0;
                            break;
                        case L'G':
                            first = max(cpp_len - 1, wYSize) - wYSize;
                            break;
                        case L'h':
                        case L'v':
                            goto help_version;
                    }
                }
                curs_set(1);

                nRelFirstDraw = 0;
                nRelLastDraw = nAbsLastLine-nAbsFirstLine;
                msg = "";
                iSelect_Draw(spaLines, wField, wXSize, nAbsFirstLine, nRelMarked, true, nRelFirstDraw, nRelLastDraw, nLines, sField, title, name, mField, msg);
                wrefresh(wField);
                break;
            default:
                msg = "Invalid key. Press 'h' for Help Page!";
                break;
        }
    }

    echo();
    nocbreak();
    delwin(wField);

    if (bQuit)
        return(-1);
    else
        return(nAbsFirstLine+nRelMarked);
}

/*
 *  The iSelect function...
 */
ssize_t iSelect(size_t pos, const char *title, const char *name,
            const char *tagbegin, const char *tagend, bool stripws,
            bool browsealways, bool allselectable,
            bool multiselect, bool exitnoselect,
            enum iSelect_key *unmapped_key)
{
    size_t tagbegin_len = strlen(tagbegin);
    size_t tagend_len   = strlen(tagend);

    // parse input lines in spaLines into an array of browsable strings:
    // only cpLine and nLine start filled in
    for (size_t i = 0; i < nLines; ++i) {
        size_t tagbeg_off = 0;
        for (;;) {
            char *found = memmem(spaLines[i].cpLine + tagbeg_off, spaLines[i].nLine - tagbeg_off, tagbegin, tagbegin_len);
            if (!found) {
                tagbeg_off = -1;
                break;
            }
            tagbeg_off = found - spaLines[i].cpLine;
            if (found[tagbegin_len] == 's' || found[tagbegin_len] == 'S')
                break;
            tagbeg_off += tagbegin_len;
        }

        if (tagbeg_off != (size_t)-1) {
            spaLines[i].fSelectable = true;

            char *itr = spaLines[i].cpLine + tagbeg_off + tagbegin_len + 1;
            bool have_name = *itr == ':';  // <s:result text>
            if(have_name)
                ++itr;
            char *end = memmem(itr, spaLines[i].cpLine + spaLines[i].nLine - itr, tagend, tagend_len);
            if(!end || (itr != end && !have_name))  // reject "<s  "  and "<s asdf>"
                goto normal;

            if(have_name) {
                spaLines[i].nResult  = end - itr;
                spaLines[i].cpResult = malloc(spaLines[i].nResult);
                if (!spaLines[i].cpResult)
                    fprintf(stderr, "iSelect: %s\n", strerror(errno)), exit(1);
                memcpy(spaLines[i].cpResult, itr, spaLines[i].nResult);
            }

            // delete the entire tag, which occupies [spaLines[i].cpLine + tagbeg_off, end + tagend_len)
            size_t delete_len = (end + tagend_len) - (spaLines[i].cpLine + tagbeg_off);
            if (tagbeg_off == 0)  // common case: tag at start of string
                spaLines[i].cpLine += delete_len;
            else
                memmove(spaLines[i].cpLine + tagbeg_off, end + tagend_len, (spaLines[i].cpLine + spaLines[i].nLine) - (end + tagend_len));
            spaLines[i].nLine  -= delete_len;
        }
        else {
        normal:
            spaLines[i].fSelectable = allselectable;
        }

        if (!spaLines[i].cpResult) {
            spaLines[i].cpResult    = spaLines[i].cpLine;
            spaLines[i].nResult     = spaLines[i].nLine;
        }

        if (stripws)
            strip(&spaLines[i]);
    }
#if 0
    for (size_t i = 0; i < nLines; ++i) {
        fprintf(stderr, "spaLines[%zu] = {\n", i);
        fprintf(stderr, "    cpLine      = \""); fwrite(spaLines[i].cpLine,   1, spaLines[i].nLine,   stderr); fprintf(stderr, "\"\n");
        fprintf(stderr, "     nLine      = %zu\n", spaLines[i].nLine);
        fprintf(stderr, "    fSelectable = %d\n", spaLines[i].fSelectable);
        fprintf(stderr, "    cpResult    = \""); fwrite(spaLines[i].cpResult, 1, spaLines[i].nResult, stderr); fprintf(stderr, "\"\n");
        fprintf(stderr, "     nResult    = %zu\n", spaLines[i].nResult);
        fprintf(stderr, "}\n");
    }
#endif

    wchar_t unget = -1;
    if (!browsealways && nLines == 0)
        return -1;
    if (!browsealways && nLines == 1) {
        char *sub_start;
        spaLines[0].fSelected = true;
        if (spaLines[0].fSelectable
            && (sub_start = memmem(spaLines[0].cpResult, spaLines[0].nResult, "%[", 2))
            && (memmem(sub_start + 2, spaLines[0].nResult - (sub_start + 2 - spaLines[0].cpResult), "]s", 2)
            ||  memmem(sub_start + 2, spaLines[0].nResult - (sub_start + 2 - spaLines[0].cpResult), "]S", 2))) {
            unget = L'\n';
            goto show;
        }
        return -!spaLines[0].fSelectable;
    }

    if (exitnoselect) {
        for (size_t i = 0; i < nLines; ++i)
            if (spaLines[i].fSelectable)
                goto show;
        return -1;
    }
show:

    /*
     *  setup Curses package and
     *  open our own first window which holds the complete screen
     */
    signal(SIGINT,  diehard); /* die gracefully on interrupt signal */
    signal(SIGTERM, diehard); /* die gracefully on terminate signal */
    initscr();                /* initialize Curses package */
    if(unget != (wchar_t)-1)
        unget_wch(unget);

    /*
     *  Now run the browser...
     */
    iSelect_PrepTagStrs(tagbegin, tagend);
    ssize_t rc = iSelect_Browser(/* Browser: */ LINES-2, COLS-3, 0, 1, pos, multiselect,
                            /* Status:  */ 1, COLS, LINES-2, 0, title, name,
                            /* Message: */ 1, COLS-1, LINES-1, 0,
                            /* Result:  */ unmapped_key);

    endwin();
    return rc;
}
