/*
 * hist - interactive readline module
 *
 * Copyright (C) 1999-2007,2021-2023,2026  David I. Bell
 *
 * Calc is open software; you can redistribute it and/or modify it under
 * the terms of the version 2.1 of the GNU Lesser General Public License
 * as published by the Free Software Foundation.
 *
 * Calc is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
 * Public License for more details.
 *
 * A copy of version 2.1 of the GNU Lesser General Public License is
 * distributed with calc under the filename COPYING-LGPL.  You should have
 * received a copy with calc; if not, write to Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Under source code control:   1993/05/02 20:09:19
 * File existed as early as:    1993
 *
 * Share and enjoy!  :-)        http://www.isthe.com/chongo/tech/comp/calc/
 */

/*
 * Adapted from code written by Stephen Rothwell.
 *
 * GNU readline support added by Martin Buck <mbuck at debian dot org>
 *
 * Interactive readline module.  This is called to read lines of input,
 * while using emacs-like editing commands within a command stack.
 * The key bindings for the editing commands are (slightly) configurable.
 */

/*
 * important <system> header includes
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <setjmp.h>
#include <stdint.h>
#include <stdbool.h>
#include <sys/stat.h>

/*
 * conditional <system> head includes
 */
#if !defined(_WIN32) && !defined(_WIN64)
#  include <pwd.h>
#endif

/*
 * calc local src includes
 */
#include "value.h"
#include "calc.h"
#include "lib_calc.h"
#include "hist.h"
#include "strl.h"
#include "have_unused.h"
#include "attribute.h"
#include "errtbl.h"

#include "banned.h" /* include after all other includes */

#define MIN(a, b) (((a) <= (b)) ? (a) : (b))

#if !defined(USE_READLINE)

extern FILE *curstream(void);

#  define STDIN 0
#  define SAVE_SIZE 256 /* size of save buffer */
#  define MAX_KEYS 60   /* number of key bindings */

#  define CONTROL(x) ((char)(((int)(x)) & 0x1f))

static struct {
    char *prompt;
    char *buf;
    char *pos;
    char *end;
    char *mark;
    int bufsize;
    int linelen;
    int histcount; /* valid history entries */
    int curhist;
    bool virgin_line; /* 1 => never typed chars, 0 => chars typed */
} HS;

typedef void (*FUNCPTR)(int);

typedef struct {
    char *name;
    FUNCPTR func;
} FUNC;

/* declare binding functions */
static void flush_input(int key);
static void start_of_line(int key);
static void end_of_line(int key);
static void forward_char(int key);
static void backward_char(int key);
static void forward_word(int key);
static void backward_word(int key);
static void delete_char(int key);
static void forward_kill_char(int key);
static void backward_kill_char(int key);
static void forward_kill_word(int key);
static void kill_line(int key);
static void new_line(int key);
static void save_line(int key);
static void forward_history(int key);
static void backward_history(int key);
static void insert_char(int key);
static void goto_line(int key);
static void list_history(int key);
static void refresh_line(int key);
static void swap_chars(int key);
static void set_mark(int key);
static void yank(int key);
static void save_region(int key);
static void kill_region(int key);
static void reverse_search(int key);
static void quote_char(int key);
static void uppercase_word(int key);
static void lowercase_word(int key);
static void ignore_char(int key);
static void arrow_key(int key);
static void quit_calc(int key) __attribute__((noreturn));
static void hist_mkdir_parent(void);

static FUNC funcs[] = {{"ignore-char", ignore_char},
                       {"flush-input", flush_input},
                       {"start-of-line", start_of_line},
                       {"end-of-line", end_of_line},
                       {"forward-char", forward_char},
                       {"backward-char", backward_char},
                       {"forward-word", forward_word},
                       {"backward-word", backward_word},
                       {"delete-char", delete_char},
                       {"forward-kill-char", forward_kill_char},
                       {"backward-kill-char", backward_kill_char},
                       {"forward-kill-word", forward_kill_word},
                       {"uppercase-word", uppercase_word},
                       {"lowercase-word", lowercase_word},
                       {"kill-line", kill_line},
                       {"goto-line", goto_line},
                       {"new-line", new_line},
                       {"save-line", save_line},
                       {"forward-history", forward_history},
                       {"backward-history", backward_history},
                       {"insert-char", insert_char},
                       {"list-history", list_history},
                       {"refresh-line", refresh_line},
                       {"swap-chars", swap_chars},
                       {"set-mark", set_mark},
                       {"yank", yank},
                       {"save-region", save_region},
                       {"kill-region", kill_region},
                       {"reverse-search", reverse_search},
                       {"quote-char", quote_char},
                       {"arrow-key", arrow_key},
                       {"quit", quit_calc},
                       {NULL, NULL}};

typedef struct key_ent KEY_ENT;
typedef struct key_map KEY_MAP;

struct key_ent {
    FUNCPTR func;
    KEY_MAP *next;
};

struct key_map {
    char *name;
    KEY_ENT default_ent;
    KEY_ENT *map[256];
};

static char base_map_name[] = "base-map";
static char esc_map_name[] = "esc-map";

static KEY_MAP maps[] = {
    {base_map_name, {NULL, NULL}, {NULL, NULL}},
    {esc_map_name, {NULL, NULL}, {NULL, NULL}},
};

typedef struct history_entry {
    int len;    /* length of data */
    char *data; /* varying length data */
    struct history_entry *prev;
    struct history_entry *next;
} HIST;

static int inited;
static int canedit;
static int key_count;
static int save_len;
static KEY_MAP *cur_map;
static KEY_MAP *base_map;
static KEY_ENT key_table[MAX_KEYS];
static HIST *hist_first = NULL;
static HIST *hist_last = NULL;
static char save_buffer[SAVE_SIZE];

/* declare other static functions */
static FUNCPTR find_func(char *name);
static HIST *get_event(int n);
static HIST *find_event(char *pat, int len);
static void read_key(void);
static void erasechar(void);
static void newline(void);
static void backspace(void);
static void beep(void);
static void echo_char(int ch);
static void echo_string(char *str, int len);
static void savetext(char *str, int len);
static void memrcpy(char *dest, char *src, int len);
static int read_bindings(FILE *fp);
static int in_word(int ch);
static KEY_MAP *find_map(char *map);
static void unbind_key(KEY_MAP *map, int key);
static void raw_bind_key(KEY_MAP *map, int key, FUNCPTR func, KEY_MAP *next_map);
static KEY_MAP *do_map_line(char *line);
static void do_default_line(KEY_MAP *map, char *line);
static void do_bind_line(KEY_MAP *map, char *line);
static void back_over_char(int ch);
static void echo_rest_of_line(void);
static void goto_start_of_line(void);
static void goto_end_of_line(void);
static void remove_char(int ch);
static void decrement_end(int n);
static void insert_string(char *str, int len);

/*
 * Read a line into the specified buffer.  The line ends in a newline,
 * and is NULL terminated.  Returns the number of characters read, or
 * zero on an end of file or error.  The prompt is printed before reading
 * the line.
 */
size_t
hist_getline(char *prompt, char *buf, size_t len)
{
    /*
     * initialize if we have not already done so
     */
    if (!inited) {
        (void)hist_init(calcbindings);
    }

    /*
     * establish the beginning of a line condition
     */
    HS.prompt = prompt;
    HS.bufsize = len - 2;
    HS.buf = buf;
    HS.pos = buf;
    HS.end = buf;
    HS.mark = NULL;
    HS.linelen = -1;
    HS.virgin_line = true;

    /*
     * prep the I/O
     */
    fputs(prompt, stdout);
    fflush(stdout);

    /*
     * special case: non-interactive editing
     */
    if (!canedit) {
        if (fgets(buf, len, stdin) == NULL) {
            return 0;
        }
        return strlen(buf);
    }

    /*
     * get the line
     */
    while (HS.linelen < 0) {

        /* get the next char */
        read_key();

        /* chars typed, no longer virgin */
        HS.virgin_line = false;
    }

    /*
     * return the line
     */
    return HS.linelen;
}

/*
 * Initialize the module by reading in the key bindings from the specified
 * filename, and then setting the terminal modes for noecho and cbreak mode.
 * If the supplied filename is NULL, then a default filename will be used.
 * We will search the CALCPATH for the file.
 *
 * Returns zero if successful, or a nonzero error code if unsuccessful.
 * If this routine fails, hist_getline, hist_saveline, and hist_term can
 * still be called but all fancy editing is disabled.
 */
int
hist_init(char *filename)
{
    /*
     * prevent multiple initializations
     */
    if (inited) {
        if (conf->calc_debug & CALCDBG_TTY) {
            printf("hist_init: inited already set\n");
        }
        return HIST_INITED;
    }

    /*
     * setup
     */
    inited = 1;
    canedit = 0;
    if (conf->calc_debug & CALCDBG_TTY) {
        printf("hist_init: Set inited, cleared canedit\n");
    }

    /*
     * open the bindings file
     */
    if (filename == NULL) {
        filename = HIST_BINDING_FILE;
    }
    if (opensearchfile(filename, calcpath, NULL, false) > 0) {
        return HIST_NOFILE;
    }

    /*
     * load the bindings
     */
    if (read_bindings(curstream())) {
        return HIST_NOFILE;
    }

    /*
     * close the bindings
     */
    closeinput();

    /*
     * setup the calc TTY on STDIN
     */
    if (!calc_tty(STDIN)) {
        return HIST_NOTTY;
    }

    canedit = 1;
    if (conf->calc_debug & CALCDBG_TTY) {
        printf("hist_init: Set canedit\n");
    }

    return HIST_SUCCESS;
}

/*
 * Reset the terminal modes just before exiting.
 */
void
hist_term(void)
{
    if (!inited || !canedit) {
        if (conf->calc_debug & CALCDBG_TTY) {
            if (!inited) {
                printf("hist_term: inited already cleared\n");
            }
            if (!canedit) {
                printf("hist_term: canedit already cleared\n");
            }
        }
        inited = 0;
        if (conf->calc_debug & CALCDBG_TTY) {
            printf("hist_term: Cleared inited\n");
        }
        return;
    }

    if (hist_first) {
        HIST *hp = hist_first;
        HIST *next;
        while (hp) {
            next = hp->next;
            free(hp->data);
            free(hp);
            hp = next;
        }
    }
    hist_first = NULL;
    hist_last = NULL;

    /*
     * restore original tty state
     */
    (void)orig_tty(STDIN);
}

static KEY_MAP *
find_map(char *map)
{
    unsigned int i;

    for (i = 0; i < sizeof(maps) / sizeof(maps[0]); i++) {
        if (strcmp(map, maps[i].name) == 0) {
            return &maps[i];
        }
    }
    return NULL;
}

static void
unbind_key(KEY_MAP *map, int key)
{
    map->map[key] = NULL;
}

static void
raw_bind_key(KEY_MAP *map, int key, FUNCPTR func, KEY_MAP *next_map)
{
    if (map->map[key] == NULL) {
        if (key_count >= MAX_KEYS) {
            return;
        }
        map->map[key] = &key_table[key_count++];
    }
    map->map[key]->func = func;
    map->map[key]->next = next_map;
}

static KEY_MAP *
do_map_line(char *line)
{
    char *cp;
    char *map_name;

    cp = line;
    while (isspace((int)*cp)) {
        cp++;
    }
    if (*cp == '\0') {
        return NULL;
    }
    map_name = cp;
    while ((*cp != '\0') && !isspace((int)*cp)) {
        cp++;
    }
    *cp = '\0';
    return find_map(map_name);
}

static void
do_bind_line(KEY_MAP *map, char *line)
{
    char *cp;
    char key;
    char *func_name;
    char *next_name;
    KEY_MAP *next;
    FUNCPTR func;

    if (map == NULL) {
        return;
    }
    cp = line;
    key = *cp++;
    if (*cp == '\0') {
        unbind_key(map, key);
        return;
    }
    if (key == '^') {
        if (*cp == '?') {
            key = 0177;
            cp++;
        } else {
            key = CONTROL(*cp++);
        }
    } else if (key == '\\') {
        key = *cp++;
    }

    while (isspace((int)*cp)) {
        cp++;
    }
    if (*cp == '\0') {
        unbind_key(map, key);
        return;
    }

    func_name = cp;
    while ((*cp != '\0') && !isspace((int)*cp)) {
        cp++;
    }
    if (*cp) {
        *cp++ = '\0';
        while (isspace((int)*cp)) {
            cp++;
        }
    }
    func = find_func(func_name);
    if (func == NULL) {
        fprintf(stderr, "Unknown function \"%s\"\n", func_name);
        return;
    }

    if (*cp == '\0') {
        next = map->default_ent.next;
        if (next == NULL) {
            next = base_map;
        }
    } else {
        next_name = cp;
        while ((*cp != '\0') && !isspace((int)*cp)) {
            cp++;
        }
        if (*cp) {
            *cp++ = '\0';
            while (isspace((int)*cp)) {
                cp++;
            }
        }
        next = find_map(next_name);
        if (next == NULL) {
            return;
        }
    }
    raw_bind_key(map, key, func, next);
}

static void
do_default_line(KEY_MAP *map, char *line)
{
    char *cp;
    char *func_name;
    char *next_name;
    KEY_MAP *next;
    FUNCPTR func;

    if (map == NULL) {
        return;
    }
    cp = line;
    while (isspace((int)*cp)) {
        cp++;
    }
    if (*cp == '\0') {
        return;
    }

    func_name = cp;
    while ((*cp != '\0') && !isspace((int)*cp)) {
        cp++;
    }
    if (*cp != '\0') {
        *cp++ = '\0';
        while (isspace((int)*cp)) {
            cp++;
        }
    }
    func = find_func(func_name);
    if (func == NULL) {
        return;
    }

    if (*cp == '\0') {
        next = map;
    } else {
        next_name = cp;
        while ((*cp != '\0') && !isspace((int)*cp)) {
            cp++;
        }
        if (*cp != '\0') {
            *cp++ = '\0';
            while (isspace((int)*cp)) {
                cp++;
            }
        }
        next = find_map(next_name);
        if (next == NULL) {
            return;
        }
    }

    map->default_ent.func = func;
    map->default_ent.next = next;
}

/*
 * Read bindings from specified open file.
 *
 * Returns nonzero on error.
 */
static int
read_bindings(FILE *fp)
{
    char *cp;
    KEY_MAP *input_map;
    char line[BUFSIZ + 1];

    base_map = find_map(base_map_name);
    cur_map = base_map;
    input_map = base_map;

    if (fp == NULL) {
        return 1;
    }

    while (fgets(line, sizeof(line) - 1, fp)) {
        line[BUFSIZ] = '\0';
        cp = line;
        while (isspace((int)*cp)) {
            cp++;
        }

        if ((*cp == '\0') || (*cp == '#') || (*cp == '\n')) {
            continue;
        }

        if (cp[strlen(cp) - 1] == '\n') {
            cp[strlen(cp) - 1] = '\0';
        }

        if (memcmp(cp, "map", 3) == 0) {
            input_map = do_map_line(&cp[3]);
        } else if (memcmp(cp, "default", 7) == 0) {
            do_default_line(input_map, &cp[7]);
        } else {
            do_bind_line(input_map, cp);
        }
    }
    return 0;
}

static void
read_key(void)
{
    KEY_ENT *ent;
    int key;

    fflush(stdout);
    key = fgetc(stdin);
    if (key == EOF) {
        HS.linelen = 0;
        HS.buf[0] = '\0';
        return;
    }

    ent = cur_map->map[key];
    if (ent == NULL) {
        ent = &cur_map->default_ent;
    }
    if (ent->next) {
        cur_map = ent->next;
    }
    if (ent->func != NULL) {
        (*ent->func)(key);
    } else {
        insert_char(key);
    }
}

/*
 * Return the Nth history event, indexed from zero.
 * Earlier history events are lower in number.
 */
static HIST *
get_event(int n)
{
    register HIST *hp = hist_first;
    int i = 0;

    do {
        if (!hp) {
            break;
        }
        if (i == n) {
            return hp;
        }
        ++i;
        hp = hp->next;
    } while (hp);
    return NULL;
}

/*
 * Search the history list for the specified pattern.
 * Returns the found history, or NULL.
 */
static HIST *
find_event(char *pat, int len)
{
    register HIST *hp = hist_first;

    for (hp = hist_first; hp != NULL; hp = hp->next) {
        if ((hp->len == len) && (memcmp(hp->data, pat, len) == 0)) {
            return hp;
        }
    }
    return NULL;
}

/*
 * Insert a line into the end of the history table.
 * If the line already appears in the table, then it is moved to the end.
 * If the table is full, then the earliest commands are deleted as necessary.
 * Warning: the incoming line cannot point into the history table.
 */
void
hist_saveline(char *line, int len)
{
    HIST *hp;

    if ((len > 0) && (line[len - 1] == '\n')) {
        len--;
    }
    if (len <= 0) {
        return;
    }

    /*
     * See if the line is already present in the history table.
     * If so, and it is already at the end, then we are all done.
     * Otherwise delete it since we will reinsert it at the end.
     */
    hp = find_event(line, len);
    if (hp) {
        if (hp == hist_last) {
            return;
        }
        if (hp->prev) {
            hp->prev->next = hp->next;
        }
        hp->next->prev = hp->prev;

        hist_last->next = hp;
        hp->next = NULL;
        hp->prev = hist_last;
        hist_last = hp;
        return;
    }

    /*
     * If there is not enough room left in the history buffer to add
     * the new command, then repeatedly delete the earliest command
     * as many times as necessary in order to make enough room.
     */
    if (HS.histcount >= HIST_SIZE) {
        HIST *new_first = hist_first->next;
        free(hist_first->data);
        free(hist_first);
        new_first->prev = NULL;
        hist_first = new_first;
        HS.histcount--;
    }

    /*
     * Add the line to the end of the history table.
     */
    hp = calloc(1, sizeof(HIST));
    if (hp == NULL) {
        fprintf(stderr, "Out of memory adding line to the history table #0\n");
        return;
    }
    hp->next = NULL;
    hp->prev = NULL;
    hp->len = len;
    hp->data = calloc(len, 1);
    if (hp->data == NULL) {
        fprintf(stderr, "Out of memory adding line to the history table #1\n");
        return;
    }
    memcpy(hp->data, line, len);
    HS.curhist = ++HS.histcount;
    if (!hist_first) {
        hist_first = hp;
    }
    if (!hist_last) {
        hist_last = hp;
    } else {
        hist_last->next = hp;
        hp->prev = hist_last;
        hist_last = hp;
    }
}

/*
 * Find the function for a specified name.
 */
static FUNCPTR
find_func(char *name)
{
    FUNC *fp;

    for (fp = funcs; fp->name; fp++) {
        if (strcmp(fp->name, name) == 0) {
            return fp->func;
        }
    }
    return NULL;
}

static void
arrow_key(int UNUSED(key))
{
    switch (fgetc(stdin)) {
    case 'A':
        backward_history(0);
        break;
    case 'B':
        forward_history(0);
        break;
    case 'C':
        forward_char(0);
        break;
    case 'D':
        backward_char(0);
        break;
    }
}

static void
back_over_char(int ch)
{
    backspace();
    if (!isprint(ch)) {
        backspace();
    }
}

static void
remove_char(int ch)
{
    erasechar();
    if (!isprint(ch)) {
        erasechar();
    }
}

static void
echo_rest_of_line(void)
{
    echo_string(HS.pos, HS.end - HS.pos);
}

static void
goto_start_of_line(void)
{
    while (HS.pos > HS.buf) {
        back_over_char((int)(*--HS.pos));
    }
}

static void
goto_end_of_line(void)
{
    echo_rest_of_line();
    HS.pos = HS.end;
}

static void
decrement_end(int n)
{
    HS.end -= n;
    if (HS.mark && (HS.mark > HS.end)) {
        HS.mark = NULL;
    }
}

static void
ignore_char(int UNUSED(key))
{
}

static void
flush_input(int UNUSED(key))
{
    echo_rest_of_line();
    while (HS.end > HS.buf) {
        remove_char((int)(*--HS.end));
    }
    HS.pos = HS.buf;
    HS.mark = NULL;
}

static void
start_of_line(int UNUSED(key))
{
    goto_start_of_line();
}

static void
end_of_line(int UNUSED(key))
{
    goto_end_of_line();
}

static void
forward_char(int UNUSED(key))
{
    if (HS.pos < HS.end) {
        echo_char(*HS.pos++);
    }
}

static void
backward_char(int UNUSED(key))
{
    if (HS.pos > HS.buf) {
        back_over_char((int)(*--HS.pos));
    }
}

static void
uppercase_word(int UNUSED(key))
{
    while ((HS.pos < HS.end) && !in_word((int)(*HS.pos))) {
        echo_char(*HS.pos++);
    }
    while ((HS.pos < HS.end) && in_word((int)(*HS.pos))) {
        /* convert ASCII lower case to UPPER CASE */
        if (isascii((int)(*HS.pos)) && islower((int)(*HS.pos))) {
            *HS.pos = toupper((int)(*HS.pos));
        }
        echo_char(*HS.pos++);
    }
}

static void
lowercase_word(int UNUSED(key))
{
    while ((HS.pos < HS.end) && !in_word((int)(*HS.pos))) {
        echo_char(*HS.pos++);
    }
    while ((HS.pos < HS.end) && in_word((int)(*HS.pos))) {
        /* convert ASCII UPPER CASE to lower case */
        if (isascii((int)(*HS.pos)) && isupper((int)(*HS.pos))) {
            *HS.pos = tolower((int)(*HS.pos));
        }
        echo_char(*HS.pos++);
    }
}

static void
forward_word(int UNUSED(key))
{
    while ((HS.pos < HS.end) && !in_word((int)(*HS.pos))) {
        echo_char(*HS.pos++);
    }
    while ((HS.pos < HS.end) && in_word((int)(*HS.pos))) {
        echo_char(*HS.pos++);
    }
}

static void
backward_word(int UNUSED(key))
{
    if ((HS.pos > HS.buf) && in_word((int)(*HS.pos))) {
        back_over_char((int)(*--HS.pos));
    }
    while ((HS.pos > HS.buf) && !in_word((int)(*HS.pos))) {
        back_over_char((int)(*--HS.pos));
    }
    while ((HS.pos > HS.buf) && in_word((int)(*HS.pos))) {
        back_over_char((int)(*--HS.pos));
    }
    if ((HS.pos < HS.end) && !in_word((int)(*HS.pos))) {
        echo_char(*HS.pos++);
    }
}

static void
forward_kill_char(int UNUSED(key))
{
    int rest;
    char ch;

    rest = HS.end - HS.pos;
    if (rest-- <= 0) {
        return;
    }
    ch = *HS.pos;
    if (rest > 0) {
        memcpy(HS.pos, HS.pos + 1, rest);
        *(HS.end - 1) = ch;
    }
    echo_rest_of_line();
    remove_char((int)ch);
    decrement_end(1);
    while (rest > 0) {
        back_over_char((int)(HS.pos[--rest]));
    }
}

static void
delete_char(int UNUSED(key))
{
    /*
     * quit delete_char (usually ^D) is at start of line and we are allowed
     *
     * We exit of start of line and config("ctrl_d", "empty") or
     * if config("ctrl_d", "virgin") and we have never typed on the line.
     */
    if ((HS.end == HS.buf) &&
        (conf->ctrl_d == CTRL_D_EMPTY_EOF || (conf->ctrl_d == CTRL_D_VIRGIN_EOF && HS.virgin_line == true))) {
        quit_calc(0);
    }

    /*
     * normal case: just forward_kill_char
     */
    if (HS.end > HS.buf) {
        forward_kill_char(0);
    }
}

static void
backward_kill_char(int UNUSED(key))
{
    if (HS.pos > HS.buf) {
        HS.pos--;
        back_over_char((int)(*HS.pos));
        forward_kill_char(0);
    }
}

static void
forward_kill_word(int UNUSED(key))
{
    char *cp;

    if (HS.pos >= HS.end) {
        return;
    }
    echo_rest_of_line();
    for (cp = HS.end; cp > HS.pos;) {
        remove_char((int)(*--cp));
    }
    cp = HS.pos;
    while ((cp < HS.end) && !in_word((int)(*cp))) {
        cp++;
    }
    while ((cp < HS.end) && in_word((int)(*cp))) {
        cp++;
    }
    savetext(HS.pos, cp - HS.pos);
    memcpy(HS.pos, cp, HS.end - cp);
    decrement_end(cp - HS.pos);
    echo_rest_of_line();
    for (cp = HS.end; cp > HS.pos;) {
        back_over_char((int)(*--cp));
    }
}

static void
kill_line(int UNUSED(key))
{
    if (HS.end <= HS.pos) {
        return;
    }
    savetext(HS.pos, HS.end - HS.pos);
    echo_rest_of_line();
    while (HS.end > HS.pos) {
        remove_char((int)(*--HS.end));
    }
    decrement_end(0);
}

/*
 * This is the function which completes a command line editing session.
 * The final line length is returned in the HS.linelen variable.
 * The line is NOT put into the edit history, so that the caller can
 * decide whether or not this should be done.
 */
static void
new_line(int UNUSED(key))
{
    int len;

    newline();
    fflush(stdout);

    HS.mark = NULL;
    HS.end[0] = '\n';
    HS.end[1] = '\0';
    len = HS.end - HS.buf + 1;
    if (len <= 1) {
        HS.curhist = HS.histcount;
        HS.linelen = 1;
        return;
    }
    HS.curhist = HS.histcount;
    HS.pos = HS.buf;
    HS.end = HS.buf;
    HS.linelen = len;
}

static void
save_line(int UNUSED(key))
{
    int len;

    len = HS.end - HS.buf;
    if (len > 0) {
        hist_saveline(HS.buf, len);
        flush_input(0);
    }
    HS.curhist = HS.histcount;
}

static void
goto_line(int UNUSED(key))
{
    int num;
    char *cp;
    HIST *hp;

    num = 0;
    cp = HS.buf;
    while ((*cp >= '0') && (*cp <= '9') && (cp < HS.pos)) {
        num = num * 10 + (*cp++ - '0');
    }
    if ((num <= 0) || (num > HS.histcount) || (cp != HS.pos)) {
        beep();
        return;
    }
    flush_input(0);
    HS.curhist = HS.histcount - num;
    hp = get_event(HS.curhist);
    memcpy(HS.buf, hp->data, hp->len);
    HS.end = &HS.buf[hp->len];
    goto_end_of_line();
}

static void
forward_history(int UNUSED(key))
{
    HIST *hp;

    flush_input(0);
    if (++HS.curhist >= HS.histcount) {
        HS.curhist = 0;
    }
    hp = get_event(HS.curhist);
    if (hp) {
        memcpy(HS.buf, hp->data, hp->len);
        HS.end = &HS.buf[hp->len];
    }
    goto_end_of_line();
}

static void
backward_history(int UNUSED(key))
{
    HIST *hp;

    flush_input(0);
    if (--HS.curhist < 0) {
        HS.curhist = HS.histcount - 1;
    }
    hp = get_event(HS.curhist);
    if (hp) {
        memcpy(HS.buf, hp->data, hp->len);
        HS.end = &HS.buf[hp->len];
    }
    goto_end_of_line();
}

static void
insert_char(int key)
{
    int len;
    int rest;

    len = HS.end - HS.buf;
    if (len >= HS.bufsize) {
        beep();
        return;
    }
    rest = HS.end - HS.pos;
    if (rest > 0) {
        memrcpy(HS.pos + 1, HS.pos, rest);
    }
    HS.end++;
    *HS.pos++ = key;
    echo_char(key);
    echo_rest_of_line();
    while (rest > 0) {
        back_over_char((int)(HS.pos[--rest]));
    }
}

static void
insert_string(char *str, int len)
{
    int rest;
    int totallen;

    if (len <= 0) {
        return;
    }
    totallen = (HS.end - HS.buf) + len;
    if (totallen > HS.bufsize) {
        beep();
        return;
    }
    rest = HS.end - HS.pos;
    if (rest > 0) {
        memrcpy(HS.pos + len, HS.pos, rest);
    }
    HS.end += len;
    memcpy(HS.pos, str, len);
    HS.pos += len;
    echo_string(str, len);
    echo_rest_of_line();
    while (rest > 0) {
        back_over_char((int)(HS.pos[--rest]));
    }
}

static void
list_history(int UNUSED(key))
{
    HIST *hp;
    int hnum;

    for (hnum = 0; hnum < HS.histcount; hnum++) {
        hp = get_event(hnum);
        printf("\n%3d: ", HS.histcount - hnum);
        echo_string(hp->data, hp->len);
    }
    refresh_line(0);
}

static void
refresh_line(int UNUSED(key))
{
    char *cp;

    newline();
    fputs(HS.prompt, stdout);
    if (HS.end > HS.buf) {
        echo_string(HS.buf, HS.end - HS.buf);
        cp = HS.end;
        while (cp > HS.pos) {
            back_over_char((int)(*--cp));
        }
    }
}

static void
swap_chars(int UNUSED(key))
{
    char ch1;
    char ch2;

    if ((HS.pos <= HS.buf) || (HS.pos >= HS.end)) {
        return;
    }
    ch1 = *HS.pos--;
    ch2 = *HS.pos;
    *HS.pos++ = ch1;
    *HS.pos = ch2;
    back_over_char((int)ch2);
    echo_char(ch1);
    echo_char(ch2);
    back_over_char((int)ch2);
}

static void
set_mark(int UNUSED(key))
{
    HS.mark = HS.pos;
}

static void
save_region(int UNUSED(key))
{
    int len;

    if (HS.mark == NULL) {
        return;
    }
    len = HS.mark - HS.pos;
    if (len > 0) {
        savetext(HS.pos, len);
    }
    if (len < 0) {
        savetext(HS.mark, -len);
    }
}

static void
kill_region(int UNUSED(key))
{
    char *cp;
    char *left;
    char *right;

    if ((HS.mark == NULL) || (HS.mark == HS.pos)) {
        return;
    }

    echo_rest_of_line();
    if (HS.mark < HS.pos) {
        left = HS.mark;
        right = HS.pos;
        HS.pos = HS.mark;
    } else {
        left = HS.pos;
        right = HS.mark;
        HS.mark = HS.pos;
    }
    savetext(left, right - left);
    for (cp = HS.end; cp > left;) {
        remove_char((int)(*--cp));
    }
    if (right < HS.end) {
        memcpy(left, right, HS.end - right);
    }
    decrement_end(right - left);
    echo_rest_of_line();
    for (cp = HS.end; cp > HS.pos;) {
        back_over_char((int)(*--cp));
    }
}

static void
yank(int UNUSED(key))
{
    insert_string(save_buffer, save_len);
}

static void
reverse_search(int UNUSED(key))
{
    int len;
    int count;
    int testhist;
    HIST *hp;
    char *save_pos;

    count = HS.histcount;
    len = HS.pos - HS.buf;
    if (len <= 0) {
        count = 0;
    }
    testhist = HS.curhist;
    do {
        if (--count < 0) {
            beep();
            return;
        }
        if (--testhist < 0) {
            testhist = HS.histcount - 1;
        }
        hp = get_event(testhist);
    } while ((hp == NULL) || (hp->len < len) || memcmp(hp->data, HS.buf, len));

    HS.curhist = testhist;
    save_pos = HS.pos;
    flush_input(0);
    memcpy(HS.buf, hp->data, hp->len);
    HS.end = &HS.buf[hp->len];
    goto_end_of_line();
    while (HS.pos > save_pos) {
        back_over_char((int)(*--HS.pos));
    }
}

static void
quote_char(int UNUSED(key))
{
    int ch;

    ch = fgetc(stdin);
    if (ch != EOF) {
        insert_char(ch);
    }
}

/*
 * Save data in the save buffer.
 */
static void
savetext(char *str, int len)
{
    save_len = 0;
    if (len <= 0) {
        return;
    }
    if (len > SAVE_SIZE) {
        len = SAVE_SIZE;
    }
    memcpy(save_buffer, str, len);
    save_len = len;
}

/*
 * Test whether a character is part of a word.
 */
static int
in_word(int ch)
{
    return (isalnum(ch) || (ch == '_'));
}

static void
erasechar(void)
{
    fputs("\b \b", stdout);
}

static void
newline(void)
{
    fputc('\n', stdout);
}

static void
backspace(void)
{
    fputc('\b', stdout);
}

static void
beep(void)
{
    fputc('\007', stdout);
}

static void
echo_char(int ch)
{
    if (isprint(ch)) {
        putchar(ch);
    } else {
        putchar('^');
        putchar((ch + '@') & 0x7f);
    }
}

static void
echo_string(char *str, int len)
{
    while (len-- > 0) {
        echo_char(*str++);
    }
}

static void
memrcpy(char *dest, char *src, int len)
{
    dest += len - 1;
    src += len - 1;
    while (len-- > 0) {
        *dest-- = *src--;
    }
}

#endif

static void
quit_calc(int UNUSED(ch))
{
    hist_term();
    putchar('\n');
    libcalc_call_me_last();
    exit(0);
}

#if defined(USE_READLINE)

#  define HISTORY_LEN (4096) /* number of entries to save */

#  include <readline/readline.h>
#  include <readline/history.h>

static void
hist_mkdir_parent(void)
{
    char *dir;
    char *slash;

    if (calc_history == NULL) {
        return;
    }

    slash = strrchr(calc_history, PATHCHAR);
    if (slash == NULL || slash == calc_history) {
        return;
    }

    dir = strdup(calc_history);
    if (dir == NULL) {
        return;
    }
    dir[slash - calc_history] = '\0';

    if (access(dir, F_OK) != 0) {
        (void)mkdir(dir, 0700);
    }

    free(dir);
}

/*
 * The readline/history libs do most of the dirty work for us, so we can
 * replace hist_init() and hist_term() with dummies when using readline.
 * For hist_getline() we have to add a newline that readline removed but
 * calc expects. For hist_saveline(), we have to undo this. hist_getline()
 * also has to cope with the different memory management schemes of calc and
 * readline (pointer to target buffer passed to hist_getline() vs. returned
 * calloc()ed buffer from readline()). While doing that, we also split
 * multi-line strings potentially returned by readline() in case of
 * bracketed paste mode even though its documentation promises to only return
 * single lines. For details, see https://github.com/lcn2/calc/issues/138
 * and https://lists.gnu.org/archive/html/bug-readline/2024-01/msg00000.html
 */

size_t
hist_getline(char *prompt, char *buf, size_t len)
{
    static char *rlbuf, *rlcur;

    if (!rlbuf) {
        rlbuf = rlcur = readline(prompt);
        if (!rlbuf) {
            buf[0] = '\0';
            switch (conf->ctrl_d) {
            case CTRL_D_NEVER_EOF:
                return 0;
            case CTRL_D_VIRGIN_EOF:
            case CTRL_D_EMPTY_EOF:
            default:
                quit_calc(0);
                not_reached();
            }
        }
    }

    /* eol: pointer to trailing newline (if there is one) or \0 */
    char *eol = strchr(rlcur, '\n');
    if (!eol) {
        eol = rlcur + strlen(rlcur);
    }
    /* len: length of line in target buffer including (possibly added)
     * newline, truncated if buffer is too small. Note that we reduce
     * the available buffer size by 1 so that we can safely add the
     * newline below.
     */
    len = MIN(len - 1, (size_t)(eol - rlcur + 1));
    strlcpy(buf, rlcur, len);
    /* make sure we have a newline and NUL */
    buf[len - 1] = '\n';
    buf[len] = '\0';

    /* skip over newline in readline buffer */
    if (*eol) {
        eol++;
    }
    /* prepare for next invocation: point to next line or free readline
     * buffer if we've reached EOL
     */
    if (*eol) {
        rlcur = eol;
    } else {
        free(rlbuf);
        rlbuf = rlcur = NULL;
    }

    return len;
}

void
hist_term(void)
{
}

static void
my_stifle_history(void)
{
    /* only save last number of entries */
    stifle_history(HISTORY_LEN);

    if (calc_history) {
        write_history(calc_history);
    }
}

/*
 * close down and complete history and free the calc_history value allocated by initenv().
 */
static void
hist_finish(void)
{
    /* only save last number of entries */
    my_stifle_history();
    if (calc_history != NULL) {
        free(calc_history);
        calc_history = NULL;
    }
}

int
hist_init(char *UNUSED(filename))
{
    char *expanded = NULL; /* tilde_expand of ~/.calc_history */

    /* used when parsing conditionals in ~/.inputrc */
    rl_readline_name = "calc";

    /* initialize interactive variables */
    using_history();

    /* name of history file */
    if (calc_history == NULL) {
        /* for info in tilde_expand see the readine(3) man page */
        expanded = tilde_expand("~/.calc_history");
        if (expanded == NULL) {
            /* tilde_expand failed, default to just .calc_history */
            calc_history = strdup(".calc_history");
        } else {
            calc_history = strdup(expanded);
        }
    }

    /* paranoia */
    if (calc_history == NULL) {
        return HIST_NULL_HIST;
    }

    /* create parent directory if necessary */
    hist_mkdir_parent();

    /* read previous history */
    read_history(calc_history);

    /* setup how history is closed down and synced when exiting */
    atexit(hist_finish);

    return HIST_SUCCESS;
}

void
hist_saveline(char *line, int len)
{
    static char *prev = NULL;

    if (len <= 1) {
        return;
    }

    /* ignore if identical with previous line */
    if (prev != NULL && strcmp(prev, line) == 0) {
        return;
    }

    free(prev);

    /* fail silently */
    prev = strdup(line);

    line[len - 1] = '\0';
    add_history(line);
    line[len - 1] = '\n';
}

#endif

#if defined(HIST_TEST)

/*
 * Main routine to test history.
 */
int
main(int argc, char **argv)
{
    char *filename;
    int len;
    char buf[BUFSIZ + 1];

    filename = NULL;
    if (argc > 1) {
        filename = argv[1];
    }

    switch (hist_init(filename)) {
    case HIST_SUCCESS:
        break;
    case HIST_NOFILE:
        fprintf(stderr, "Binding file was not found\n");
        break;
    case HIST_NOTTY:
        fprintf(stderr, "Cannot set terminal parameters\n");
        break;
    case HIST_INITED:
        fprintf(stderr, "Hist is already inited\n");
        break;
    default:
        fprintf(stderr, "Unknown error from hist_init\n");
        break;
    }

    do {
        len = hist_getline("HIST> ", buf, sizeof(buf));
        hist_saveline(buf, len);
    } while (len && (buf[0] != 'q'));

    hist_term();
    exit(0);
}

#endif
