/*
 * OpenComal -- a free Comal implementation
 *
 * This file is part of the OpenComal package.
 * (c) Copyright 1992-2024 Jos Visser <josvisser66@gmail.com>
 *
 * The OpenComal package is covered by the GNU General Public
 * License. See doc/LICENSE for more information.
 */

/* This file contains the system dependent routines for Win32 */

#include "pdcglob.h"
#include "pdcmisc.h"
#include "pdcext.h"
#include "pdcexec.h"

#include <windows.h>
#include <errno.h>
#include <time.h>

PRIVATE HANDLE cstdin;
PRIVATE HANDLE cstdout;
PRIVATE HANDLE cstderr;
PRIVATE int paged;
DWORD fdwSaveOldMode;
int escape;
int lines_printed;

/* key definitions for line editing */
#define KEY_LEFT 257
#define KEY_RIGHT 258
#define KEY_INSERT 259
#define KEY_UP 260
#define KEY_DOWN 261
#define KEY_HOME 262
#define KEY_END 263

/* Forward definition */
void do_put(int, char *, long);

/* Stack of previous lines */
#define STACKSIZE 256

struct stack_rec {
    struct stack_rec *prev;
    struct stack_rec *next;
    char *line;
};

PRIVATE struct stack_rec *tail;
PRIVATE struct stack_rec *head;
PRIVATE int stack_size;

PRIVATE BOOL WINAPI
consoleHandler(DWORD
signal) {
if (signal == CTRL_C_EVENT)
escape = 1;
return
TRUE;
}

PUBLIC void sys_init() {
    cstdin = GetStdHandle(STD_INPUT_HANDLE);
    GetConsoleMode(cstdin, &fdwSaveOldMode);
    SetConsoleMode(cstdin, ENABLE_WINDOW_INPUT | ENABLE_PROCESSED_INPUT);

    cstdout = GetStdHandle(STD_OUTPUT_HANDLE);
    cstderr = GetStdHandle(STD_ERROR_HANDLE);

    SetConsoleTitle("OpenComal on Win32");

    SetConsoleCtrlHandler(consoleHandler, TRUE);
}

PUBLIC void sys_tini() {
    SetConsoleMode(cstdin, fdwSaveOldMode);
}

PUBLIC void sys_setpaged(int flag) {
    paged = flag;
    lines_printed = 0;
}

PUBLIC int sys_system(char *cmd) {
    return system(cmd);
}

PUBLIC char *sys_unit_string() {
    run_error(SYS_ERR, "Not implemented");
}

PUBLIC void sys_unit(char *drive) {
    run_error(SYS_ERR, "Not implemented");
}

PUBLIC char *sys_dir_string() {
    static int buf_size = 1024;
    static char *buf = 0;

    while (1 == 1) {
        if (!buf) buf = malloc(buf_size);

        if (getcwd(buf, buf_size) != NULL) return buf;

        if (errno == ERANGE) { /* buffer too small */
            buf_size += 1024;
            free(buf);
            buf = 0;
        } else
            run_error(DIRS_ERR, strerror(errno));
    }
}

PUBLIC void sys_dir(char *pattern) {
    const char cur[2] = ".";
    char *fname, *envar, *buf;
    FILE *fdir;
    long l;

    /* Check environment variable for temp directory, otherwise use
current dir. */
    if (!(envar = getenv("TEMP")))
        envar = (char *) cur;

    fname = (char *) malloc(strlen(envar) + 16);
    sprintf(fname, "%s\\%s", envar, "_oc_dir.txt");

    /* Call the "dir" command rather than writing a custom directory
printing routine. */
    buf = (char *) malloc(strlen(pattern) + strlen(fname) + 16);
    sprintf(buf, "dir %s > \"%s\"", pattern, fname);
    system(buf);

    /* Read directory output from re-directed file. */
    fdir = fopen(fname, "rb");
    fseek(fdir, 0, SEEK_END);
    l = ftell(fdir);
    buf = (char *) realloc(buf, (l + 1));
    fseek(fdir, 0, SEEK_SET);
    fread(buf, 1, l, fdir);

    /* Display contents of directory to console. */
    *(buf + l) = 0;
    sys_setpaged(TRUE);
    do_put(MSG_PROGRAM, buf, -1L);
    sys_setpaged(FALSE);

    /* Clean up. */
    fclose(fdir);
    remove(fname);
    free(buf);
    free(fname);
}

PUBLIC void sys_rand(long *result, long *max) {
    *result = rand();
    *max = RAND_MAX;
}

PUBLIC int sys_escape() {
    /* Somebody pressed control-C */
    if (escape) {
        escape = 0;
        return 1;
    }

    INPUT_RECORD peek[256];
    DWORD numread;

    PeekConsoleInput(cstdin, peek, 256, &numread);

    int i;

    for (i = 0; i < numread; i++) {
        if (peek[i].EventType == KEY_EVENT && peek[i].Event.KeyEvent.uChar.AsciiChar == 27)
            return 1;
    }

    return 0;
}

PRIVATE void gotoxy(int x, int y) {
    CONSOLE_SCREEN_BUFFER_INFO info;

    GetConsoleScreenBufferInfo(cstdout, &info);

    if (x < 0) x = 0;
    if (y < 0) y = 0;
    if (x >= info.dwSize.X) x = info.dwSize.X;
    if (y >= info.dwSize.Y) y = info.dwSize.Y;

    COORD new;
    new.X = x;
    new.Y = y;
    SetConsoleCursorPosition(cstdout, new);
}

PRIVATE void getxy(int *x, int *y) {
    CONSOLE_SCREEN_BUFFER_INFO info;

    GetConsoleScreenBufferInfo(cstdout, &info);

    *y = info.dwCursorPosition.Y;
    *x = info.dwCursorPosition.X;
}

PRIVATE void getscreensize(int *w, int *d) {
    CONSOLE_SCREEN_BUFFER_INFO info;

    GetConsoleScreenBufferInfo(cstdout, &info);

    *w = info.dwSize.X;
    *d = info.srWindow.Bottom - info.srWindow.Top + 1;
}

PRIVATE void page() {
    CONSOLE_SCREEN_BUFFER_INFO csbi;
    SMALL_RECT scrollRect;
    COORD scrollTarget;
    CHAR_INFO fill;

    // Get the number of character cells in the current buffer.
    GetConsoleScreenBufferInfo(cstdout, &csbi);

    // Scroll the rectangle of the entire buffer.
    scrollRect.Left = 0;
    scrollRect.Top = 0;
    scrollRect.Right = csbi.dwSize.X;
    scrollRect.Bottom = csbi.dwSize.Y;

    // Scroll it upwards off the top of the buffer with a magnitude of the entire height.
    scrollTarget.X = 0;
    scrollTarget.Y = (SHORT)(0 - csbi.dwSize.Y);

    // Fill with empty spaces with the buffer's default text attribute.
    fill.Char.UnicodeChar = TEXT(' ');
    fill.Attributes = csbi.wAttributes;

    // Do the scroll
    ScrollConsoleScreenBuffer(cstdout, &scrollRect, NULL, scrollTarget, &fill);

    // Move the cursor to the top left corner too.
    csbi.dwCursorPosition.X = 0;
    csbi.dwCursorPosition.Y = 0;

    SetConsoleCursorPosition(cstdout, csbi.dwCursorPosition);
}

PRIVATE void clearline() {
    CONSOLE_SCREEN_BUFFER_INFO info;

    GetConsoleScreenBufferInfo(cstdout, &info);

    info.dwCursorPosition.X = 0;
    DWORD dummy;
    FillConsoleOutputCharacter(cstdout, ' ', info.dwSize.X, info.dwCursorPosition, &dummy);
    SetConsoleCursorPosition(cstdout, info.dwCursorPosition);
}

PRIVATE void do_put(int stream, char *buf, long len) {
    if (len < 0) len = strlen(buf);

    if (!paged) {
        WriteConsole(cstdout, buf, len, NULL, NULL);
        return;
    }

    int w, d;

    getscreensize(&w, &d);

    char *s = buf;
    char *t;

    while (*s) {
        if (lines_printed < d) {
            for (t = s; *t && *t != '\n'; ++t);

            WriteConsole(cstdout, s, t - s + 1, NULL, NULL);
            lines_printed += (t - s + 1) / w;
            if (*t == '\n') lines_printed++;
            if (!*t) {
                return;
            }
            s = t + 1;
        }

        if (lines_printed >= d - 1) {
            int c = get_key();

            switch (c) {
                case 32:
                    lines_printed = 1;
                    /* fallthrough */
                case 13:
                    lines_printed--;
                    break;
                default:
                    Beep(440, 500);
            }
        }
    }
}

PUBLIC void sys_put(int stream, char *buf, long len) {
    ext_put(stream, buf, len);
    do_put(stream, buf, len);
}

PUBLIC void sys_page(FILE *f) {
    if (f)
        ext_page();
    else
        page();
}

PUBLIC void sys_cursor(FILE *f, long x, long y) {
    if (f)
        return;

    gotoxy(x, y);
}

PUBLIC void sys_nl(int stream) {
    ext_nl();
    do_put(stream, "\n", 1L);
}

PUBLIC void sys_screen_readjust() {
}

PRIVATE int get_key() {
    INPUT_RECORD irInBuf[1];
    DWORD numread;
    PKEY_EVENT_RECORD event;

    while (1) {
        ReadConsoleInput(
                cstdin,     // input buffer handle
                irInBuf,    // buffer to read into
                1,          // size of read buffer
                &numread); // number of records read

        if (numread == 0) return -1;
        if (escape == 1) {
            escape = 0;
            return -1;
        }

        if (irInBuf[0].EventType != KEY_EVENT) continue;

        event = &irInBuf[0].Event.KeyEvent;
        if (!event->bKeyDown) continue;

        switch (event->wVirtualKeyCode) {
            case VK_LEFT:
                return KEY_LEFT;
            case VK_RIGHT:
                return KEY_RIGHT;
            case VK_INSERT:
                return KEY_INSERT;
            case VK_HOME:
                return KEY_HOME;
            case VK_END:
                return KEY_END;
            case VK_UP:
                return KEY_UP;
            case VK_DOWN:
                return KEY_DOWN;
            default:
                if (event->uChar.AsciiChar != 0) return event->uChar.AsciiChar;
        }
    }
}

PUBLIC int sys_yn(int stream, char *s) {
    char c;

    my_printf(MSG_DIALOG, 0, s);

    c = get_key();

    if (c == 'y' || c == 'Y') {
        do_put(stream, "yes\n", 4L);
        return 1;
    }

    do_put(stream, "no\n", 3L);
    return 0;
}

PRIVATE void push_line() {
    struct stack_rec *work = malloc(sizeof(struct stack_rec));

    work->line = NULL;
    work->next = NULL;

    if (stack_size == 0) {
        work->next = NULL;
        work->prev = NULL;
        head = tail = work;
    } else {
        head->next = work;
        work->prev = head;
        head = work;
    }

    stack_size++;
}

PRIVATE void curtail_stack() {
    while (stack_size > STACKSIZE) {
        free(tail->line);
        tail = tail->next;
        free(tail->prev);
        tail->prev = NULL;
        stack_size--;
    }
}

PRIVATE void swap(struct stack_rec *work, char *line) {
    if (work->line) free(work->line);
    work->line = strdup(line);
}

PRIVATE int do_get(int stream, char *line, int maxlen, char *prompt, int
cursor) {
    int len = strlen(line);
    int x, y, w, d, i;
    struct stack_rec *work = tail;

    /*
    while (work) {
        char buf[1024];
        sprintf(buf, "%d) %s\n", i, work->line);
        do_put(stream, buf, -1);
        i++;
        work = work->next;
    }
    */

    push_line();
    getscreensize(&w, &d);
    do_put(stream, prompt, strlen(prompt));
    getxy(&x, &y);
    do_put(stream, line, len);

    int curlen = len;
    int oldlen;
    int idx = curlen;
    char *pos = line + idx;
    int insert_mode = 1;
    struct stack_rec *sp = head;
    int changes_made = 0;

    gotoxy((x + idx) % w, y + (idx / w));

    /* When we start editing sp needs to point to a valid location*/

    do {
        int c = get_key();

        switch (c) {
            case -1:
            case 27:
                *line = 0;
                /* Escape */
                return 1;

            case 13:
                /* CR */
                do_put(stream, "\n", 1);

                /* Do not save empty line in the stack. */
                if (curlen > 0 && (!sp->line || strcmp(line, sp->line) != 0)) {
                    swap(head, line);
                } else {
                    work = head;
                    head = head->prev;

                    if (head) {
                        head->next = NULL;
                    } else {
                        tail = NULL;
                    }

                    free(work);
                    stack_size--;
                }
                curtail_stack();
                return 0;

            case 8:
                /* Backspace */
                if (idx == 0) {
                    /* We are at the start of the line. */
                    Beep(440, 500);
                    continue;
                }
                memcpy(pos - 1, pos, curlen - idx + 1);
                line[curlen - 1] = 0;
                curlen--;
                idx--;
                pos--;
                gotoxy(x, y);
                do_put(stream, line, curlen);
                do_put(stream, " ", 1L);
                gotoxy((x + idx) % w, y + (idx / w));
                changes_made = 1;
                continue;

            case KEY_HOME:
                pos = line;
                idx = 0;
                gotoxy((x + idx) % w, y + (idx / w));
                break;

            case KEY_END:
                idx = curlen;
                pos = line + idx;
                gotoxy((x + idx) % w, y + (idx / w));
                break;

            case KEY_LEFT:
                if (idx == 0) {
                    Beep(440, 500);
                    break;
                }
                idx--;
                pos--;
                do_put(stream, "\b", 1L);
                break;

            case KEY_RIGHT:
                if (idx >= curlen) {
                    Beep(440, 500);
                    break;
                }
                idx++;
                pos++;
                do_put(stream, pos - 1, 1L);
                break;

            case KEY_INSERT:
                insert_mode = !insert_mode;
                break;

            case KEY_UP:
                if (!sp->prev) {
                    Beep(440, 500);
                    continue;
                }
                if (changes_made) swap(head, line);
                sp = sp->prev;
                oldlen = curlen;
                if (sp->line) {
                    strncpy(line, sp->line, maxlen);
                    line[maxlen - 1] = 0;
                } else {
                    line[0] = 0;
                }
                idx = curlen = strlen(line);
                changes_made = 0;
                pos = line + idx;
                gotoxy(x, y);
                do_put(stream, line, curlen);
                for (i = curlen; i < oldlen; i++) do_put(stream, " ", 1);
                gotoxy((x + idx) % w, y + (idx / w));
                break;

            case KEY_DOWN:
                if (!sp->next) {
                    Beep(440, 500);
                    continue;
                }
                if (changes_made) swap(head, line);
                sp = sp->next;
                oldlen = curlen;
                if (sp->line) {
                    strncpy(line, sp->line, maxlen);
                    line[maxlen - 1] = 0;
                } else {
                    line[0] = 0;
                }
                idx = curlen = strlen(line);
                changes_made = 0;
                idx = curlen;
                pos = line + idx;
                gotoxy(x, y);
                do_put(stream, line, curlen);
                for (i = curlen; i < oldlen; i++) do_put(stream, " ", 1);
                gotoxy((x + idx) % w, y + (idx / w));
                break;

            default:
                if (curlen == maxlen - 1) {
                    Beep(440, 500);
                    continue;
                }

                changes_made = 1;

                if (!insert_mode) {
                    do_put(stream, &c, 1);
                    *pos = c;
                    pos++;
                    idx++;

                    if (idx > curlen) {
                        curlen++;
                        *pos = 0;
                    }
                } else {
                    if (curlen == maxlen) {
                        Beep(440, 500);
                        continue;
                    } else {
                        memcpy(pos + 1, pos, curlen - idx + 1);
                        *pos = c;
                        do_put(stream, pos, curlen - idx + 1);
                        pos++;
                        idx++;
                        curlen++;
                        gotoxy((x + idx) % w, y + (idx / w));
                    }
                }
        }
    } while (1);
}

PUBLIC int sys_get(int stream, char *line, int maxlen, char *prompt) {
    if (ext_get(stream, line, maxlen, prompt))
        return 0;
    else {
        line[0] = 0;
        return do_get(stream, line, maxlen, prompt, 1);
    }
}

PUBLIC int sys_edit(int stream, char *line, int maxlen, int cursor) {
    return do_get(stream, line, maxlen, "", cursor);
}

PUBLIC void *sys_alloc(long size) {
    if (size > MAXUNSIGNED)
        return NULL;
    return calloc(1, (unsigned) size);
}

PUBLIC void *sys_realloc(void *block, long newsize) {
    if (newsize > MAXUNSIGNED)
        return NULL;
    return realloc(block, (unsigned) newsize);
}

PUBLIC void sys_free(void *p) {
    free(p);
}

PUBLIC void sys_chdir(char *dir) {
    if (chdir(dir) < 0)
        run_error(DIR_ERR, strerror(errno));
}

PUBLIC void sys_mkdir(char *dir) {
    if (!mkdir(dir) < 0)
        run_error(DIR_ERR, strerror(errno));
}

PUBLIC void sys_rmdir(char *dir) {
    if (rmdir(dir) < 0)
        run_error(DIR_ERR, strerror(errno));
}

PUBLIC char *sys_key(long delay) {
    static char result[2] = {0, 0};

    result[0] = get_key();
    return result;
}

PUBLIC int sys_call_scan(struct id_rec *id, struct exp_list *exproot,
                         char *errtext) {
    int rc = ext_call_scan(id, exproot, errtext);
    if (rc == -1) {
        sprintf(errtext, "PROCedure %s not found", id->id_name);
        return 0;
    }
    return rc;
}

PUBLIC int sys_call(struct id_rec *id, struct exp_list *exproot,
                    int calltype, void **result, enum VAL_TYPE *type) {
    return ext_call(id, exproot, calltype, result, type);
}

PUBLIC void sys_sys_exp(struct exp_list *exproot, void **result,
                        enum VAL_TYPE *type) {
    if (ext_sys_exp(exproot, result, type) == -1)
        run_error(SYS_ERR, "Unknown SYS() command");
}

PUBLIC void sys_syss_exp(struct exp_list *exproot, struct string **result,
                         enum VAL_TYPE *type) {
    if (ext_syss_exp(exproot, result, type) == -1)
        run_error(SYS_ERR, "Unknown SYS$() command");
}

PUBLIC int sys_sys_stat(struct exp_list *exproot) {
    int rc = ext_sys_stat(exproot);
    if (rc == -1)
        run_error(SYS_ERR, "Unknown SYS statement command");
    return rc;
}

PUBLIC void sys_opencomalrc() {
    const char *homedrive = getenv("HOMEDRIVE");
    const char *homedir = getenv("HOMEPATH");

    if (!homedir || !homedrive)
        return;

    char filename[PATH_MAX];

    strcpy(filename, homedrive);
    strcpy(filename, homedir);
    strcat(filename, "\\.opencomalrc");

    int attr = GetFileAttributes(filename);

    if (attr == FILE_ATTRIBUTE_NORMAL) do_enter(filename);
}

PUBLIC int sys_quit_allowed() {
    return 1;
}