/*
 * QuickJS Read Eval Print Loop
 *
 * Copyright (c) 2017-2020 Fabrice Bellard
 * Copyright (c) 2017-2020 Charlie Gordon
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
import * as std from "qjs:std";
import * as os from "qjs:os";
import * as bjson from "qjs:bjson";

(function(g) {
    /* add 'bjson', 'os' and 'std' bindings */
    g.bjson = bjson;
    g.os = os;
    g.std = std;

    /* close global objects */
    var Object = g.Object;
    var String = g.String;
    var Number = g.Number;
    var Boolean = g.Boolean;
    var BigInt = g.BigInt;
    var Uint8Array = g.Uint8Array;
    var Array = g.Array;
    var Date = g.Date;
    var RegExp = g.RegExp;
    var Error = g.Error;
    var Symbol = g.Symbol;
    var Math = g.Math;
    var JSON = g.JSON;
    var isFinite = g.isFinite;
    var isNaN = g.isNaN;
    var Infinity = g.Infinity;
    var console = g.console;

    // auto-completion keywords
    var keywords = [
        "await ", "catch (", "class ", "const ", "else ", "export ", "for ",
        "function ", "if (", "import ", "instanceof ", "let ", "new ",
        "return", "super ", "this", "try {", "typeof ", "var ", "while (",
        "yield ",
    ];

    var colors = {
        none:    "\x1b[0m",
        black:   "\x1b[30m",
        red:     "\x1b[31m",
        green:   "\x1b[32m",
        yellow:  "\x1b[33m",
        blue:    "\x1b[34m",
        magenta: "\x1b[35m",
        cyan:    "\x1b[36m",
        white:   "\x1b[37m",
        gray:    "\x1b[30;1m",
        grey:    "\x1b[30;1m",
        bright_red:     "\x1b[31;1m",
        bright_green:   "\x1b[32;1m",
        bright_yellow:  "\x1b[33;1m",
        bright_blue:    "\x1b[34;1m",
        bright_magenta: "\x1b[35;1m",
        bright_cyan:    "\x1b[36;1m",
        bright_white:   "\x1b[37;1m",
    };

    var themes = {
        dark: {
            'annotation': 'cyan',
            'boolean':    'bright_white',
            'comment':    'white',
            'date':       'magenta',
            'default':    'bright_green',
            'error':      'bright_red',
            'function':   'bright_yellow',
            'identifier': 'bright_green',
            'keyword':    'bright_white',
            'null':       'bright_white',
            'number':     'green',
            'other':      'white',
            'propname':   'white',
            'regexp':     'cyan',
            'string':     'bright_cyan',
            'symbol':     'bright_white',
            'type':       'bright_magenta',
            'undefined':  'bright_white',
        },
        light: {
            'annotation': 'cyan',
            'boolean':    'bright_magenta',
            'comment':    'grey',
            'date':       'magenta',
            'default':    'black',
            'error':      'red',
            'function':   'bright_yellow',
            'identifier': 'black',
            'keyword':    'bright_magenta',
            'null':       'bright_magenta',
            'number':     'green',
            'other':      'black',
            'propname':   'black',
            'regexp':     'cyan',
            'string':     'bright_cyan',
            'symbol':     'grey',
            'type':       'bright_magenta',
            'undefined':  'bright_magenta',
        },
    };
    var styles = themes.dark;
    var utf8 = true;
    var show_time = false;
    var show_colors = true;
    var show_hidden = false;
    var show_depth = 2;
    var hex_mode = false;
    var use_strict = false;

    var history = [];
    var history_index;
    var clip_board = "";
    var pstate = "";
    var prompt = "";
    var plen = 0;
    var ps1 = "qjs > ";
    var ps2 = "  ... ";
    var eval_start_time;
    var eval_time = 0;
    var mexpr = "";
    var level = 0;
    var cmd = "";
    var cursor_pos = 0;
    var last_cmd = "";
    var last_cursor_pos = 0;
    var this_fun, last_fun;
    var quote_flag = false;

    var utf8_state = 0;
    var utf8_val = 0;

    var term_fd;
    var term_read_buf;
    var term_width;
    /* current X position of the cursor in the terminal */
    var term_cursor_x = 0;

    function termInit() {
        var tab;
        term_fd = std.in.fileno();

        /* get the terminal size */
        term_width = 80;
        if (os.isatty(term_fd)) {
            if (os.ttyGetWinSize) {
                tab = os.ttyGetWinSize(term_fd);
                if (tab)
                    term_width = tab[0];
            }
            if (os.ttySetRaw) {
                /* set the TTY to raw mode */
                os.ttySetRaw(term_fd);
            }
        }

        /* install a Ctrl-C signal handler */
        os.signal(os.SIGINT, sigint_handler);

        /* install a handler to read stdin */
        term_read_buf = new Uint8Array(64);
        os.setReadHandler(term_fd, term_read_handler);
    }

    function sigint_handler() {
        /* send Ctrl-C to readline */
        handle_byte(3);
    }

    function term_read_handler() {
        var l, i;
        l = os.read(term_fd, term_read_buf.buffer, 0, term_read_buf.length);
        for(i = 0; i < l; i++)
            handle_byte(term_read_buf[i]);
    }

    function handle_byte(c) {
        if (!utf8) {
            handle_char(c);
        } else if (utf8_state !== 0 && (c >= 0x80 && c < 0xc0)) {
            utf8_val = (utf8_val << 6) | (c & 0x3F);
            utf8_state--;
            if (utf8_state === 0) {
                handle_char(utf8_val);
            }
        } else if (c >= 0xc0 && c < 0xf8) {
            utf8_state = 1 + (c >= 0xe0) + (c >= 0xf0);
            utf8_val = c & ((1 << (6 - utf8_state)) - 1);
        } else {
            utf8_state = 0;
            handle_char(c);
        }
    }

    function is_alpha(c) {
        return typeof c === "string" &&
            ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'));
    }

    function is_digit(c) {
        return typeof c === "string" && (c >= '0' && c <= '9');
    }

    function is_word(c) {
        return typeof c === "string" &&
            (is_alpha(c) || is_digit(c) || c == '_' || c == '$');
    }

    function is_blank(c) {
        return typeof c === "string" && "\t\r\n\f\v".includes(c[0])
    }

    function ucs_length(str) {
        var len, c, i, str_len = str.length;
        len = 0;
        /* we never count the trailing surrogate to have the
         following property: ucs_length(str) =
         ucs_length(str.substring(0, a)) + ucs_length(str.substring(a,
         str.length)) for 0 <= a <= str.length */
        for(i = 0; i < str_len; i++) {
            c = str.charCodeAt(i);
            if (c < 0xdc00 || c >= 0xe000)
                len++;
        }
        return len;
    }

    function is_trailing_surrogate(c)  {
        var d;
        if (typeof c !== "string")
            return false;
        d = c.codePointAt(0); /* can be NaN if empty string */
        return d >= 0xdc00 && d < 0xe000;
    }

    function is_balanced(a, b) {
        switch (a + b) {
        case "()":
        case "[]":
        case "{}":
            return true;
        }
        return false;
    }

    function print_color_text(str, start, style_names) {
        var i, j;
        for (j = start; j < str.length;) {
            var style = style_names[i = j];
            while (++j < str.length && style_names[j] == style)
                continue;
            std.puts(colors[styles[style] || 'none']);
            std.puts(str.substring(i, j));
            std.puts(colors['none']);
        }
    }

    function print_csi(n, code) {
        std.puts("\x1b[" + ((n != 1) ? n : "") + code);
    }

    /* XXX: handle double-width characters */
    function move_cursor(delta) {
        var i, l;
        if (delta > 0) {
            while (delta != 0) {
                if (term_cursor_x == (term_width - 1)) {
                    std.puts("\n"); /* translated to CRLF */
                    term_cursor_x = 0;
                    delta--;
                } else {
                    l = Math.min(term_width - 1 - term_cursor_x, delta);
                    print_csi(l, "C"); /* right */
                    delta -= l;
                    term_cursor_x += l;
                }
            }
        } else {
            delta = -delta;
            while (delta != 0) {
                if (term_cursor_x == 0) {
                    print_csi(1, "A"); /* up */
                    print_csi(term_width - 1, "C"); /* right */
                    delta--;
                    term_cursor_x = term_width - 1;
                } else {
                    l = Math.min(delta, term_cursor_x);
                    print_csi(l, "D"); /* left */
                    delta -= l;
                    term_cursor_x -= l;
                }
            }
        }
    }

    function update() {
        var i, cmd_len;
        /* cursor_pos is the position in 16 bit characters inside the
           UTF-16 string 'cmd' */
        if (cmd != last_cmd) {
            if (!show_colors && last_cmd.substring(0, last_cursor_pos) == cmd.substring(0, last_cursor_pos)) {
                /* optimize common case */
                std.puts(cmd.substring(last_cursor_pos));
            } else {
                /* goto the start of the line */
                move_cursor(-ucs_length(last_cmd.substring(0, last_cursor_pos)));
                if (show_colors) {
                    var str = mexpr ? mexpr + '\n' + cmd : cmd;
                    var start = str.length - cmd.length;
                    var colorstate = colorize_js(str);
                    print_color_text(str, start, colorstate[2]);
                } else {
                    std.puts(cmd);
                }
            }
            term_cursor_x = (term_cursor_x + ucs_length(cmd)) % term_width;
            if (term_cursor_x == 0) {
                /* show the cursor on the next line */
                std.puts(" \x08");
            }
            /* remove the trailing characters */
            std.puts("\x1b[J");
            last_cmd = cmd;
            last_cursor_pos = cmd.length;
        }
        if (cursor_pos > last_cursor_pos) {
            move_cursor(ucs_length(cmd.substring(last_cursor_pos, cursor_pos)));
        } else if (cursor_pos < last_cursor_pos) {
            move_cursor(-ucs_length(cmd.substring(cursor_pos, last_cursor_pos)));
        }
        last_cursor_pos = cursor_pos;
        std.out.flush();
    }

    /* editing commands */
    function insert(str) {
        if (str) {
            cmd = cmd.substring(0, cursor_pos) + str + cmd.substring(cursor_pos);
            cursor_pos += str.length;
        }
    }

    function quoted_insert() {
        quote_flag = true;
    }

    function abort() {
        cmd = "";
        cursor_pos = 0;
        return -2;
    }

    function alert() {
    }

    function beginning_of_line() {
        cursor_pos = 0;
    }

    function end_of_line() {
        cursor_pos = cmd.length;
    }

    function forward_char() {
        if (cursor_pos < cmd.length) {
            cursor_pos++;
            while (is_trailing_surrogate(cmd.charAt(cursor_pos)))
                cursor_pos++;
        }
    }

    function backward_char() {
        if (cursor_pos > 0) {
            cursor_pos--;
            while (is_trailing_surrogate(cmd.charAt(cursor_pos)))
                cursor_pos--;
        }
    }

    function skip_word_forward(pos) {
        while (pos < cmd.length && !is_word(cmd.charAt(pos)))
            pos++;
        while (pos < cmd.length && is_word(cmd.charAt(pos)))
            pos++;
        return pos;
    }

    function skip_word_backward(pos) {
        while (pos > 0 && !is_word(cmd.charAt(pos - 1)))
            pos--;
        while (pos > 0 && is_word(cmd.charAt(pos - 1)))
            pos--;
        return pos;
    }

    function forward_word() {
        cursor_pos = skip_word_forward(cursor_pos);
    }

    function backward_word() {
        cursor_pos = skip_word_backward(cursor_pos);
    }

    function clear_screen() {
        directives["clear"]();
        return -2;
    }

    function accept_line() {
        std.puts("\n");
        history_add(cmd);
        return -1;
    }

    function history_add(str) {
        str = str.trimRight();
        if (str) {
            while (history.length && !history[history.length - 1])
                history.length--;
            history.push(str);
        }
        history_index = history.length;
    }

    function previous_history() {
        if (history_index > 0) {
            if (history_index == history.length) {
                history.push(cmd);
            }
            history_index--;
            cmd = history[history_index];
            cursor_pos = cmd.length;
        }
    }

    function next_history() {
        if (history_index < history.length - 1) {
            history_index++;
            cmd = history[history_index];
            cursor_pos = cmd.length;
        }
    }

    function history_search(dir) {
        var pos = cursor_pos;
        for (var i = 1; i <= history.length; i++) {
            var index = (history.length + i * dir + history_index) % history.length;
            if (history[index].substring(0, pos) == cmd.substring(0, pos)) {
                history_index = index;
                cmd = history[index];
                return;
            }
        }
    }

    function history_search_backward() {
        return history_search(-1);
    }

    function history_search_forward() {
        return history_search(1);
    }

    function delete_char_dir(dir) {
        var start, end;

        start = cursor_pos;
        if (dir < 0) {
            start--;
            while (is_trailing_surrogate(cmd.charAt(start)))
                start--;
        }
        end = start + 1;
        while (is_trailing_surrogate(cmd.charAt(end)))
            end++;

        if (start >= 0 && start < cmd.length) {
            if (last_fun === kill_region) {
                kill_region(start, end, dir);
            } else {
                cmd = cmd.substring(0, start) + cmd.substring(end);
                cursor_pos = start;
            }
        }
    }

    function delete_char() {
        delete_char_dir(1);
    }

    function control_d() {
        if (cmd.length == 0) {
            std.puts("\n");
            return -3; /* exit read eval print loop */
        } else {
            delete_char_dir(1);
        }
    }

    function backward_delete_char() {
        delete_char_dir(-1);
    }

    function transpose_chars() {
        var pos = cursor_pos;
        if (cmd.length > 1 && pos > 0) {
            if (pos == cmd.length)
                pos--;
            cmd = cmd.substring(0, pos - 1) + cmd.substring(pos, pos + 1) +
                cmd.substring(pos - 1, pos) + cmd.substring(pos + 1);
            cursor_pos = pos + 1;
        }
    }

    function transpose_words() {
        var p1 = skip_word_backward(cursor_pos);
        var p2 = skip_word_forward(p1);
        var p4 = skip_word_forward(cursor_pos);
        var p3 = skip_word_backward(p4);

        if (p1 < p2 && p2 <= cursor_pos && cursor_pos <= p3 && p3 < p4) {
            cmd = cmd.substring(0, p1) + cmd.substring(p3, p4) +
            cmd.substring(p2, p3) + cmd.substring(p1, p2);
            cursor_pos = p4;
        }
    }

    function upcase_word() {
        var end = skip_word_forward(cursor_pos);
        cmd = cmd.substring(0, cursor_pos) +
            cmd.substring(cursor_pos, end).toUpperCase() +
            cmd.substring(end);
    }

    function downcase_word() {
        var end = skip_word_forward(cursor_pos);
        cmd = cmd.substring(0, cursor_pos) +
            cmd.substring(cursor_pos, end).toLowerCase() +
            cmd.substring(end);
    }

    function kill_region(start, end, dir) {
        var s = cmd.substring(start, end);
        if (last_fun !== kill_region)
            clip_board = s;
        else if (dir < 0)
            clip_board = s + clip_board;
        else
            clip_board = clip_board + s;

        cmd = cmd.substring(0, start) + cmd.substring(end);
        if (cursor_pos > end)
            cursor_pos -= end - start;
        else if (cursor_pos > start)
            cursor_pos = start;
        this_fun = kill_region;
    }

    function kill_line() {
        kill_region(cursor_pos, cmd.length, 1);
    }

    function backward_kill_line() {
        kill_region(0, cursor_pos, -1);
    }

    function kill_word() {
        kill_region(cursor_pos, skip_word_forward(cursor_pos), 1);
    }

    function backward_kill_word() {
        kill_region(skip_word_backward(cursor_pos), cursor_pos, -1);
    }

    function yank() {
        insert(clip_board);
    }

    function control_c() {
        if (last_fun === control_c) {
            std.puts("\n");
            exit(0);
        } else {
            std.puts("\n(Press Ctrl-C again to quit)\n");
            readline_print_prompt();
        }
    }

    function reset() {
        cmd = "";
        cursor_pos = 0;
    }

    // returns true if |line| looks something like "object.prop"
    function is_named_property(line, end) {
        var pos = end;
        while (pos > 0 && is_word(line[pos - 1]))
            pos--;
        while (pos > 0 && is_blank(line[pos - 1]))
            pos--;
        return pos > 0 && line[pos - 1] === ".";
    }

    function get_context_word(line, end) {
        var pos = end;
        while (pos > 0 && is_word(line[pos - 1]))
            pos--;
        return line.slice(pos, end);
    }

    function get_context_object(line, pos) {
        if (pos <= 0)
            return g;
        var c = line[pos - 1];
        if (pos === 1 && (c === '\\' || c === '.'))
            return directives;
        if ("'\"`@#)]}\\".indexOf(c) >= 0)
            return void 0;
        if (c === ".") {
            pos--;
            switch (c = line[pos - 1]) {
            case '\'':
            case '\"':
            case '`':
                return "a";
            case ']':
                return [];  // incorrect for a[b].<TAB>
            case '/':
                return / /;
            default:
                if (is_word(c)) {
                    var base = get_context_word(line, pos);
                    var base_pos = pos - base.length;
                    if (base === 'true' || base === 'false')
                        return true;
                    if (base === 'null')
                        return null;
                    if (base === 'this')
                        return g;
                    if (!isNaN(+base))  // number literal, incorrect for 1.<TAB>
                        return 0;
                    var obj = get_context_object(line, base_pos);
                    if (obj === null || obj === void 0)
                        return obj;
                    if (typeof obj[base] !== 'undefined')
                        return obj[base];
                    // Check if `base` is a set of regexp flags
                    // TODO(chqrlie): this is incorrect for a/i<TAB>...
                    // Should use colorizer to determine the token type
                    if (base_pos >= 3 && line[base_pos - 1] === '/' && base.match(/^[dgimsuvy]+$/))
                        return RegExp();
                    // base is a local identifier, complete as generic object
                }
                break;
            }
            return {};
        }
        return g;
    }

    function get_completions(line, pos) {
        var s, obj, ctx_obj, r, i, j, paren;

        s = get_context_word(line, pos);
        ctx_obj = get_context_object(line, pos - s.length);
        r = [];
        if (!is_named_property(line, pos)) {
            for (const kw of keywords) {
                if (kw.startsWith(s))
                    r.push(kw);
            }
        }
        /* enumerate properties from object and its prototype chain,
           add non-numeric regular properties with s as e prefix
         */
        for (i = 0, obj = ctx_obj; i < 10 && obj !== null && obj !== void 0; i++) {
            var props = Object.getOwnPropertyNames(obj);
            /* add non-numeric regular properties */
            for (j = 0; j < props.length; j++) {
                var prop = props[j];
                if (typeof prop == "string" && ""+(+prop) != prop && prop.startsWith(s))
                    r.push(prop);
            }
            obj = Object.getPrototypeOf(obj);
        }
        if (r.length > 1) {
            /* sort list with internal names last and remove duplicates */
            function symcmp(a, b) {
                if (a[0] != b[0]) {
                    if (a[0] == '_')
                        return 1;
                    if (b[0] == '_')
                        return -1;
                }
                if (a < b)
                    return -1;
                if (a > b)
                    return +1;
                return 0;
            }
            r.sort(symcmp);
            for(i = j = 1; i < r.length; i++) {
                if (r[i] != r[i - 1])
                    r[j++] = r[i];
            }
            r.length = j;
        }
        /* 'tab' = list of completions, 'pos' = cursor position inside
           the completions */
        return { tab: r, pos: s.length, ctx: ctx_obj };
    }

    function completion() {
        var tab, res, s, i, j, len, t, max_width, col, n_cols, row, n_rows;
        res = get_completions(cmd, cursor_pos);
        tab = res.tab;
        if (tab.length === 0)
            return;
        s = tab[0];
        len = s.length;
        /* add the chars which are identical in all the completions */
        for(i = 1; i < tab.length; i++) {
            t = tab[i];
            for(j = 0; j < len; j++) {
                if (t[j] !== s[j]) {
                    len = j;
                    break;
                }
            }
        }
        for(i = res.pos; i < len; i++) {
            insert(s[i]);
        }
        if (last_fun === completion && tab.length == 1) {
            /* append parentheses to function names */
            s = tab[0];
            var m = res.ctx[s];
            if (typeof m == "function") {
                insert('(');
                if (m.length == 0)
                    insert(')');
            } else if (typeof m == "object" ||
                       typeof m == "undefined" && s == "this") {
                insert('.');
            }
        }
        /* show the possible completions */
        if (last_fun === completion && tab.length >= 2) {
            max_width = 0;
            for(i = 0; i < tab.length; i++)
                max_width = Math.max(max_width, tab[i].length);
            max_width += 2;
            n_cols = Math.max(1, Math.floor((term_width + 1) / max_width));
            n_rows = Math.ceil(tab.length / n_cols);
            std.puts("\n");
            /* display the sorted list column-wise */
            for (row = 0; row < n_rows; row++) {
                for (col = 0; col < n_cols; col++) {
                    i = col * n_rows + row;
                    if (i >= tab.length)
                        break;
                    s = tab[i];
                    if (col != n_cols - 1)
                        s = s.padEnd(max_width);
                    std.puts(s);
                }
                std.puts("\n");
            }
            /* show a new prompt */
            readline_print_prompt();
        }
    }

    var commands = {        /* command table */
        "\x01":     beginning_of_line,      /* ^A - bol */
        "\x02":     backward_char,          /* ^B - backward-char */
        "\x03":     control_c,              /* ^C - abort */
        "\x04":     control_d,              /* ^D - delete-char or exit */
        "\x05":     end_of_line,            /* ^E - eol */
        "\x06":     forward_char,           /* ^F - forward-char */
        "\x07":     abort,                  /* ^G - bell */
        "\x08":     backward_delete_char,   /* ^H - backspace */
        "\x09":     completion,             /* ^I - history-search-backward */
        "\x0a":     accept_line,            /* ^J - newline */
        "\x0b":     kill_line,              /* ^K - delete to end of line */
        "\x0c":     clear_screen,           /* ^L - clear screen */
        "\x0d":     accept_line,            /* ^M - enter */
        "\x0e":     next_history,           /* ^N - down */
        "\x10":     previous_history,       /* ^P - up */
        "\x11":     quoted_insert,          /* ^Q - quoted-insert */
        "\x12":     alert,                  /* ^R - reverse-search */
        "\x13":     alert,                  /* ^S - search */
        "\x14":     transpose_chars,        /* ^T - transpose */
        "\x17":     backward_kill_word,     /* ^W - backward_kill_word */
        "\x18":     reset,                  /* ^X - cancel */
        "\x19":     yank,                   /* ^Y - yank */
        "\x1bOA":   previous_history,       /* ^[OA - up */
        "\x1bOB":   next_history,           /* ^[OB - down */
        "\x1bOC":   forward_char,           /* ^[OC - right */
        "\x1bOD":   backward_char,          /* ^[OD - left */
        "\x1bOF":   forward_word,           /* ^[OF - ctrl-right */
        "\x1bOH":   backward_word,          /* ^[OH - ctrl-left */
        "\x1b[1;5C": forward_word,          /* ^[[1;5C - ctrl-right */
        "\x1b[1;5D": backward_word,         /* ^[[1;5D - ctrl-left */
        "\x1b[1~":  beginning_of_line,      /* ^[[1~ - bol */
        "\x1b[3~":  delete_char,            /* ^[[3~ - delete */
        "\x1b[4~":  end_of_line,            /* ^[[4~ - eol */
        "\x1b[5~":  history_search_backward,/* ^[[5~ - page up */
        "\x1b[6~":  history_search_forward, /* ^[[5~ - page down */
        "\x1b[A":   previous_history,       /* ^[[A - up */
        "\x1b[B":   next_history,           /* ^[[B - down */
        "\x1b[C":   forward_char,           /* ^[[C - right */
        "\x1b[D":   backward_char,          /* ^[[D - left */
        "\x1b[F":   end_of_line,            /* ^[[F - end */
        "\x1b[H":   beginning_of_line,      /* ^[[H - home */
        "\x1b\x7f": backward_kill_word,     /* M-C-? - backward_kill_word */
        "\x1bb":    backward_word,          /* M-b - backward_word */
        "\x1bd":    kill_word,              /* M-d - kill_word */
        "\x1bf":    forward_word,           /* M-f - backward_word */
        "\x1bk":    backward_kill_line,     /* M-k - backward_kill_line */
        "\x1bl":    downcase_word,          /* M-l - downcase_word */
        "\x1bt":    transpose_words,        /* M-t - transpose_words */
        "\x1bu":    upcase_word,            /* M-u - upcase_word */
        "\x7f":     backward_delete_char,   /* ^? - delete */
    };

    function dupstr(str, count) {
        var res = "";
        while (count-- > 0)
            res += str;
        return res;
    }

    var readline_keys;
    var readline_state;
    var readline_cb;

    function readline_print_prompt()
    {
        std.puts(prompt);
        term_cursor_x = ucs_length(prompt) % term_width;
        last_cmd = "";
        last_cursor_pos = 0;
    }

    function readline_start(defstr, cb) {
        cmd = defstr || "";
        cursor_pos = cmd.length;
        history_index = history.length;
        readline_cb = cb;

        prompt = pstate;

        if (mexpr) {
            prompt += dupstr(" ", plen - prompt.length);
            prompt += ps2;
        } else {
            if (show_time) {
                var t = eval_time / 1000;
                prompt += t.toFixed(6) + " ";
            }
            plen = prompt.length;
            prompt += ps1;
        }
        readline_print_prompt();
        update();
        readline_state = 0;
    }

    function handle_char(c1) {
        var c;
        c = String.fromCodePoint(c1);
        switch(readline_state) {
        case 0:
            if (c == '\x1b') {  /* '^[' - ESC */
                readline_keys = c;
                readline_state = 1;
            } else {
                handle_key(c);
            }
            break;
        case 1: /* '^[ */
            readline_keys += c;
            if (c == '[') {
                readline_state = 2;
            } else if (c == 'O') {
                readline_state = 3;
            } else {
                handle_key(readline_keys);
                readline_state = 0;
            }
            break;
        case 2: /* '^[[' - CSI */
            readline_keys += c;
            if (!(c == ';' || (c >= '0' && c <= '9'))) {
                handle_key(readline_keys);
                readline_state = 0;
            }
            break;
        case 3: /* '^[O' - ESC2 */
            readline_keys += c;
            handle_key(readline_keys);
            readline_state = 0;
            break;
        }
    }

    function handle_key(keys) {
        var fun;

        if (quote_flag) {
            if (ucs_length(keys) === 1)
                insert(keys);
            quote_flag = false;
        } else if (fun = commands[keys]) {
            this_fun = fun;
            switch (fun(keys)) {
            case -1:
                readline_cb(cmd);
                return;
            case -2:
                readline_cb(null);
                return;
            case -3:
                /* uninstall a Ctrl-C signal handler */
                os.signal(os.SIGINT, null);
                /* uninstall the stdin read handler */
                os.setReadHandler(term_fd, null);
                save_history();
                return;
            }
            last_fun = this_fun;
        } else if (ucs_length(keys) === 1 && keys >= ' ') {
            insert(keys);
            last_fun = insert;
        } else {
            alert(); /* beep! */
        }

        cursor_pos = (cursor_pos < 0) ? 0 :
            (cursor_pos > cmd.length) ? cmd.length : cursor_pos;
        update();
    }

    function number_to_string(a, radix) {
        var s;
        if (!isFinite(a)) {
            /* NaN, Infinite */
            return a.toString();
        } else {
            if (a == 0) {
                if (1 / a < 0)
                    s = "-0";
                else
                    s = "0";
            } else {
                if (radix == 16 && a === Math.floor(a)) {
                    var s;
                    if (a < 0) {
                        a = -a;
                        s = "-";
                    } else {
                        s = "";
                    }
                    s += "0x" + a.toString(16);
                } else {
                    s = a.toString();
                }
            }
            return s;
        }
    }

    function bigint_to_string(a, radix) {
        var s;
        if (radix == 16) {
            var s;
            if (a < 0) {
                a = -a;
                s = "-";
            } else {
                s = "";
            }
            s += "0x" + a.toString(16);
        } else {
            s = a.toString();
        }
        return s + "n";
    }

    var util = {};
    util.inspect = function(val, show_hidden, max_depth, use_colors) {
        var options = {};
        if (typeof show_hidden === 'object' && show_hidden !== null) {
            options = show_hidden;
            show_hidden = options.showHidden;
            max_depth = options.depth;
            use_colors = options.colors;
        }
        function set(opt, def) {
            return (typeof opt === 'undefined') ? def : (opt === null) ? Infinity : opt;
        }
        if (typeof show_hidden !== 'boolean')
            show_hidden = false;
        max_depth = set(max_depth, 2);
        use_colors = set(use_colors, true);
        var breakLength = set(options.breakLength, Math.min(term_width, 80));
        var maxArrayLength = set(options.maxArrayLength, 100);
        var maxObjectLength = set(options.maxObjectLength, maxArrayLength + 10);
        var maxStringLength = set(options.maxStringLength, 78);
        var refs = [{}];    /* list of circular references */
        var stack = [];     /* stack of pending objects */
        var tokens = [];    /* list of generated tokens */
        var output = [];    /* list of output fragments */
        var last_style = 'none';

        function quote_str(s) {
            if (s.includes("'"))
                return JSON.stringify(s);
            s = JSON.stringify(s).slice(1, -1).replaceAll('\\"', '"');
            return `'${s}'`;
        }
        function push_token(s) {
            tokens.push("" + s);
        }
        function append_token(s) {
            tokens[tokens.length - 1] += s;
        }
        function class_tag(o) {
            // get the class id of an object
            // works for boxed objects, Math, JSON, globalThis...
            return Object.prototype.toString.call(o).slice(8, -1);
        }

        function print_rec(a, level) {
            var n, n0, i, k, keys, key, type, isarray, noindex, nokeys, brace, sep;

            switch (type = typeof(a)) {
            case "undefined":
            case "boolean":
                push_token(a);
                break;
            case "number":
                push_token(number_to_string(a, hex_mode ? 16 : 10));
                break;
            case "bigint":
                push_token(bigint_to_string(a, hex_mode ? 16 : 10));
                break;
            case "string":
                if (a.length > maxStringLength)
                    a = a.substring(0, maxStringLength) + "...";
                push_token(quote_str(a));
                break;
            case "symbol":
                push_token(String(a));
                break;
            case "object":
            case "function":
                if (a === null) {
                    push_token(a);
                    break;
                }
                if ((n = refs.indexOf(a)) >= 0) {
                    push_token(`[Circular *${n}]`);
                    break;
                }
                if ((n = stack.indexOf(a)) >= 0) {
                    push_token(`[Circular *${refs.length}]`);
                    refs.push(stack[n]);
                    break;
                }
                var obj_index = tokens.length;
                var tag = class_tag(a);
                stack.push(a);
                // XXX: should have Proxy instances
                if (a instanceof Date) {
                    push_token(`Date ${JSON.stringify(a.toGMTString())}`);
                } else if (a instanceof RegExp) {
                    push_token(a.toString());
                } else if (a instanceof Boolean || a instanceof Number || a instanceof BigInt) {
                    push_token(`[${tag}: ${a}]`);
                } else if (a instanceof String) {
                    push_token(`[${tag}: ${quote_str(a)}]`);
                    len = a.length;
                    noindex = 1;
                } else if (Array.isArray(a)) {
                    push_token("[");
                    isarray = 1;
                } else if (tag.includes('Array') && a instanceof Uint8Array.__proto__) {
                    push_token(`${tag}(${a.length}) [`);
                    isarray = 1;
                } else if (type === 'function') {
                    if (a.name)
                        push_token(`[Function: ${a.name}]`);
                    else
                        push_token(`[Function (anonymous)]`);
                } else {
                    var cons = (a.constructor && a.constructor.name) || 'Object';
                    if (tag !== 'Object') {
                        push_token(`${cons} [${tag}] {`);
                    } else if (a.__proto__ === null) {
                        push_token(`[${cons}: null prototype] {`);
                    } else if (cons !== 'Object') {
                        push_token(`${cons} {`);
                    } else {
                        push_token("{");
                    }
                    brace = "}";
                }
                keys = null;
                n = 0;
                n0 = 0;
                k = 0;
                if (isarray) {
                    brace = "]";
                    var len = a.length;
                    if (level > max_depth && len) {
                        push_token("...");
                        push_token(brace);
                        return;
                    }
                    for (i = 0; i < len; i++) {
                        k++;
                        if (i in a) {
                            print_rec(a[i], level + 1);
                        } else {
                            var start = i;
                            while (i + 1 < len && !((i + 1) in a))
                                i++;
                            if (i > start)
                                push_token(`<${i - start + 1} empty items>`);
                            else
                                push_token("<empty>");
                        }
                        if (k >= maxArrayLength && len - k > 5) {
                            push_token(`... ${len - k} more items`);
                            break;
                        }
                    }
                    noindex = 1;
                    /* avoid using Object.keys for large arrays */
                    if (i !== len && len > 1000)
                        nokeys = 1;
                }
                if (!nokeys) {
                    keys = show_hidden ? Object.getOwnPropertyNames(a) : Object.keys(a);
                    n = keys.length;
                }
                if (noindex) {
                    /* skip all index properties */
                    for (; n0 < n; n0++) {
                        i = +keys[n0];
                        if (i !== (i >>> 0) || i >= len)
                            break;
                    }
                }
                if (n0 < n) {
                    if (!brace) {
                        append_token(" {");
                        brace = "}";
                    }
                    if (level > max_depth && n0 < n) {
                        push_token("...");
                        push_token(brace);
                        return;
                    }
                    for(i = n0; i < n; i++) {
                        var key = keys[i];
                        var desc = Object.getOwnPropertyDescriptor(a, key);
                        if (!desc)
                            continue;
                        if (!desc.enumerable)
                            push_token(`[${String(key)}]`);
                        else
                        if (+key === (key >>> 0) || key.match(/^[a-zA-Z_$][0-9a-zA-Z_$]*/))
                            push_token(key);
                        else
                            push_token(quote_str(key));
                        push_token(":");
                        if ('value' in desc) {
                            print_rec(desc.value, level + 1);
                        } else {
                            var fields = [];
                            if (desc.get)
                                fields.push("Getter");
                            if (desc.set)
                                fields.push("Setter");
                            push_token(`[${fields.join('/')}]`);
                        }
                        k++;
                        if (k > maxObjectLength && n - k > 5) {
                            push_token(`... ${n - k} more properties`);
                            break;
                        }
                    }
                }
                if (brace)
                    push_token(brace);
                stack.pop(a);
                if ((i = refs.indexOf(a)) > 0)
                    tokens[obj_index] = `<ref *${i}> ${tokens[obj_index]}`;
                break;
            default:
                push_token(String(a));
                break;
            }
        };
        function output_str(s, style) {
            if (use_colors) {
                if (last_style !== style) {
                    output.push(colors.none);
                    last_style = style;
                }
                if (style) {
                    var color = colors[styles[style]];
                    if (color)
                        output.push(color);
                }
            }
            output.push(s);
        }
        function output_propname(s) {
            if (s[0] >= '0' && s[0] <= '9')
                output_str(s, 'number');
            else
                output_str(s, 'propname');
            output_str(": ");
        }
        function output_pretty(s) {
            if (!use_colors) {
                output_str(s);
                return;
            }
            while (s.length > 0) {
                var style = 'none';
                var chunk = s;
                var len = 0;
                var m = null;
                switch (s[0]) {
                case '"':
                    style = 'string';
                    m = s.match(/^"([^\\"]|\\.)*"/);
                    break;
                case '\'':
                    style = 'string';
                    m = s.match(/^'([^\\']|\\.)*'/);
                    break;
                case '/':
                    style = 'regexp';
                    break;
                case '<':
                    m = s.match(/^\<[^\>]+\>/);
                    if (m)
                        style = 'annotation';
                    break;
                case '[':
                    m = s.match(/^\[[^\]]+\]/);
                    if (m) {
                        style = 'annotation';
                        break;
                    }
                    /* fall thru */
                case ']':
                case '}':
                case ',':
                case ' ':
                    style = 'other';
                    len = 1;
                    break;
                case '.':
                    style = 'annotation';
                    break;
                case '0': case '1': case '2': case '3': case '4':
                case '5': case '6': case '7': case '8': case '9':
                    style = 'number';
                    m = s.match(/^[0-9a-z_]+[.]?[0-9a-z_]*[eEpP]?[+-]?[0-9]*/);
                    break;
                case '-':
                    len = 1;
                    break;
                default:
                    if (is_block(s))
                        len = s.length - 1;
                    if (s.startsWith('Date'))
                        style = 'date';
                    else if (s.startsWith('Symbol'))
                        style = 'symbol';
                    else if (s === 'Infinity' || s === 'NaN')
                        style = 'keyword';
                    else if (s === 'true' || s === 'false')
                        style = 'boolean';
                    else if (s === 'null')
                        style = 'null';
                    else if (s === 'undefined')
                        style = 'undefined';
                    break;
                }
                if (m)
                    len = m[0].length;
                if (len > 0)
                    chunk = s.slice(0, len);
                output_str(chunk, style);
                s = s.slice(chunk.length);
            }
        }
        function is_block(s) {
            var c = s[s.length - 1];
            return c === '[' || c === '{';
        }
        function block_width(i) {
            var w = tokens[i].length;
            if (tokens[i + 1] === ":") {
                i += 2;
                w += 2 + tokens[i].length;
            }
            var width = w;
            if (is_block(tokens[i])) {
                var seplen = 1;
                while (++i < tokens.length) {
                    width += seplen;
                    var s = tokens[i];
                    if (s === ']' || s === '}')
                        break;
                    [ i, w ] = block_width(i);
                    width += w;
                    seplen = 2;
                }
            }
            return [ i, width ];
        }
        function output_single(i, last) {
            var sep = "";
            while (i <= last) {
                var s = tokens[i++];
                if (s === ']' || s === '}') {
                    if (sep.length > 1)
                        output_str(" ");
                } else {
                    output_str(sep);
                    if (tokens[i] === ":") {
                        output_propname(s);
                        i++;
                        s = tokens[i++];
                    }
                }
                output_pretty(s);
                sep = is_block(s) ? " " : ", ";
            }
        }
        function output_spaces(s, count) {
            if (count > 0)
                s += " ".repeat(count);
            output_str(s);
        }
        function output_indent(indent, from) {
            var avail_width = breakLength - indent - 2;
            var [ last, width ] = block_width(from);
            if (width <= avail_width) {
                output_single(from, last);
                return [ last, width ];
            }
            if (tokens[from + 1] === ":") {
                output_propname(tokens[from]);
                from += 2;
            }
            output_pretty(tokens[from]);
            if (!is_block(tokens[from])) {
                return [ from, width ];
            }
            indent += 2;
            avail_width -= 2;
            var sep = "";
            var first = from + 1;
            var i, w;
            if (tokens[from].endsWith('[')) {
                /* array: try multiple columns for indexed values */
                var k = 0, col, cols;
                var tab = [];
                for (i = first; i < last; i++) {
                    if (tokens[i][0] === '.' || tokens[i + 1] === ':')
                        break;
                    [ i, w ] = block_width(i);
                    tab[k++] = w;
                }
                var colwidth;
                for (cols = Math.min(avail_width / 3, tab.length, 16); cols > 1; cols--) {
                    colwidth = [];
                    col = 0;
                    for (k = 0; k < tab.length; k++) {
                        colwidth[col] = Math.max(colwidth[col] || 0, tab[k] + 2);
                        col = (col + 1) % cols;
                    }
                    w = 0;
                    for (col = 0; col < cols; col++) {
                        w += colwidth[col];
                    }
                    if (w <= avail_width)
                        break;
                }
                if (cols > 1) {
                    w = 0;
                    col = cols - 1;
                    for (i = first; i < last; i++) {
                        if (tokens[i][0] === '.' || tokens[i + 1] === ':')
                            break;
                        w += sep.length;
                        output_str(sep);
                        sep = ",";
                        if (col === cols - 1) {
                            output_spaces("\n", indent);
                            col = 0;
                        } else {
                            output_spaces("", colwidth[col++] - w);
                        }
                        [i, w] = output_indent(indent, i);
                    }
                    first = i;
                }
            }
            for (i = first; i < last; i++) {
                output_str(sep);
                sep = ",";
                output_spaces("\n", indent);
                [i, w] = output_indent(indent, i);
            }
            output_spaces("\n", indent -= 2);
            output_pretty(tokens[last]);
            return [last, breakLength];
        }
        print_rec(val, 0);
        output_indent(0, 0);
        output_str("");
        return output.join("");
    };

    function print(val) {
        std.puts(util.inspect(val, { depth: show_depth, colors: show_colors, showHidden: show_hidden }));
        std.puts("\n");
    }

    /* return true if the string was a directive */
    function handle_directive(a) {
        if (a === "?") {
            help();
            return true;
        }
        if (a[0] !== '\\' && a[0] !== '.')
            return false;
        var pos = 1;
        while (pos < a.length && a[pos] !== ' ') {
            pos++;
        }
        var cmd = a.substring(1, pos);
        var partial = 0;
        var fun;
        for (var p in directives) {
            if (p.startsWith(cmd)) {
                fun = directives[p];
                partial++;
                if (p === cmd) {
                    partial = 0;
                    break;
                }
            }
        }
        if (fun && partial < 2) {
            fun(a.substring(pos).trim());
        } else {
            std.puts(`Unknown directive: ${cmd}\n`);
        }
        return true;
    }

    function help() {
        var sel = (n) => n ? "*": " ";
        std.puts(".help    print this help\n" +
                 ".x      " + sel(hex_mode) + "hexadecimal number display\n" +
                 ".dec    " + sel(!hex_mode) + "decimal number display\n" +
                 ".time   " + sel(show_time) + "toggle timing display\n" +
                 ".strict " + sel(use_strict) + "toggle strict mode evaluation\n" +
                 `.depth   set object depth (current: ${show_depth})\n` +
                 ".hidden " + sel(show_hidden) + "toggle hidden properties display\n" +
                 ".color  " + sel(show_colors) + "toggle colored output\n" +
                 ".dark   " + sel(styles == themes.dark) + "select dark color theme\n" +
                 ".light  " + sel(styles == themes.light) + "select light color theme\n" +
                 ".clear   clear the terminal\n" +
                 ".load    load source code from a file\n" +
                 ".quit    exit\n");
    }

    function load(s) {
        if (s.lastIndexOf(".") <= s.lastIndexOf("/"))
            s += ".js";
        try {
            std.loadScript(s);
        } catch (e) {
            std.puts(`${e}\n`);
        }
    }

    function exit(e) {
        save_history();
        std.exit(e);
    }

    function to_bool(s, def) {
        return s ? "1 true yes Yes".includes(s) : def;
    }

    var directives = Object.setPrototypeOf({
        "help":   help,
        "load":   load,
        "x":      (s) => { hex_mode = to_bool(s, true); },
        "dec":    (s) => { hex_mode = !to_bool(s, true); },
        "time":   (s) => { show_time = to_bool(s, !show_time); },
        "strict": (s) => { use_strict = to_bool(s, !use_strict); },
        "depth":  (s) => { show_depth = +s || 2; },
        "hidden": (s) => { show_hidden = to_bool(s, !show_hidden); },
        "color":  (s) => { show_colors = to_bool(s, !show_colors); },
        "dark":   () => { styles = themes.dark; },
        "light":  () => { styles = themes.light; },
        "clear":  () => { std.puts("\x1b[H\x1b[J") },
        "quit":   () => { exit(0); },
    }, null);

    function cmd_start() {
        std.puts('QuickJS-ng - Type ".help" for help\n');
        cmd_readline_start();
    }

    function cmd_readline_start() {
        readline_start(dupstr("    ", level), readline_handle_cmd);
    }

    function readline_handle_cmd(expr) {
        if (!handle_cmd(expr)) {
            cmd_readline_start();
        }
    }

    /* return true if async termination */
    function handle_cmd(expr) {
        if (!expr)
            return false;
        if (mexpr) {
            expr = mexpr + '\n' + expr;
        } else {
            if (handle_directive(expr))
                return false;
        }
        var colorstate = colorize_js(expr);
        pstate = colorstate[0];
        level = colorstate[1];
        if (pstate) {
            mexpr = expr;
            return false;
        }
        mexpr = "";

        eval_and_print(expr);

        return true;
    }

    function eval_and_print(expr) {
        var result;

        if (use_strict)
            expr = '"use strict"; void 0;' + expr;
        eval_start_time = os.now();
        /* eval as a script */
        result = Promise.try(std.evalScript, expr, { backtrace_barrier: true, async: true });
        result.then(print_eval_result, print_eval_error);
    }

    function print_eval_result(result) {
        result = result.value;
        eval_time = os.now() - eval_start_time;
        print(result);
        /* set the last result */
        g._ = result;

        handle_cmd_end();
    }

    function print_eval_error(error) {
        if (show_colors) {
            std.puts(colors[styles.error]);
        }
        if (error instanceof Error) {
            std.puts(error);
            std.puts('\n');
            if (error.stack) {
                std.puts(error.stack);
            }
        } else {
            std.puts("Throw: ");
            std.puts(error);
            std.puts('\n');
        }

        if (show_colors) {
            std.puts(colors.none);
        }

        handle_cmd_end();
    }

    function handle_cmd_end() {
        level = 0;

        /* run the garbage collector after each command */
        std.gc();

        cmd_readline_start();
    }

    function colorize_js(str) {
        var i, c, start, n = str.length;
        var style, state = "", level = 0;
        var primary, can_regex = 1;
        var r = [];

        function push_state(c) { state += c; }
        function last_state(c) { return state.substring(state.length - 1); }
        function pop_state(c) {
            var c = last_state();
            state = state.substring(0, state.length - 1);
            return c;
        }

        function parse_block_comment() {
            style = 'comment';
            push_state('/');
            for (i++; i < n - 1; i++) {
                if (str[i] == '*' && str[i + 1] == '/') {
                    i += 2;
                    pop_state('/');
                    break;
                }
            }
        }

        function parse_line_comment() {
            style = 'comment';
            for (i++; i < n; i++) {
                if (str[i] == '\n') {
                    break;
                }
            }
        }

        function parse_string(delim) {
            style = 'string';
            push_state(delim);
            while (i < n) {
                c = str[i++];
                if (c == '\n') {
                    style = 'error';
                    continue;
                }
                if (c == '\\') {
                    if (i >= n)
                        break;
                    i++;
                } else
                if (c == delim) {
                    pop_state();
                    break;
                }
            }
        }

        function parse_regex() {
            style = 'regexp';
            push_state('/');
            while (i < n) {
                c = str[i++];
                if (c == '\n') {
                    style = 'error';
                    continue;
                }
                if (c == '\\') {
                    if (i < n) {
                        i++;
                    }
                    continue;
                }
                if (last_state() == '[') {
                    if (c == ']') {
                        pop_state()
                    }
                    // ECMA 5: ignore '/' inside char classes
                    continue;
                }
                if (c == '[') {
                    push_state('[');
                    if (str[i] == '[' || str[i] == ']')
                        i++;
                    continue;
                }
                if (c == '/') {
                    pop_state();
                    while (i < n && is_word(str[i]))
                        i++;
                    break;
                }
            }
        }

        function parse_number() {
            style = 'number';
            // TODO(chqrlie) parse partial number syntax
            // TODO(chqrlie) special case bignum
            while (i < n && (is_word(str[i]) || (str[i] == '.' && (i == n - 1 || str[i + 1] != '.')))) {
                i++;
            }
        }

        var js_keywords = "|" +
            "break|case|catch|continue|debugger|default|delete|do|" +
            "else|finally|for|function|if|in|instanceof|new|" +
            "return|switch|this|throw|try|typeof|while|with|" +
            "class|const|enum|import|export|extends|super|" +
            "implements|interface|let|package|private|protected|" +
            "public|static|yield|" +
            "undefined|null|true|false|Infinity|NaN|" +
            "eval|arguments|" +
            "await|";

        var js_no_regex = "|this|super|undefined|null|true|false|Infinity|NaN|arguments|";
        var js_types = "|void|var|";

        function parse_identifier() {
            can_regex = 1;

            while (i < n && is_word(str[i]))
                i++;

            var s = str.substring(start, i);
            var w = '|' + s + '|';

            if (js_keywords.indexOf(w) >= 0) {
                style = 'keyword';
                if (s === 'true' || s === 'false')
                    style = 'boolean';
                else if (s === 'true' || s === 'false')
                    style = 'boolean';
                else if (s === 'null')
                    style = 'null';
                else if (s === 'undefined')
                    style = 'undefined';
                if (js_no_regex.indexOf(w) >= 0)
                    can_regex = 0;
                return;
            }

            var i1 = i;
            while (i1 < n && str[i1] == ' ')
                i1++;

            if (i1 < n && str[i1] == '(') {
                style = 'function';
                return;
            }

            if (js_types.indexOf(w) >= 0) {
                style = 'type';
                return;
            }

            style = 'identifier';
            can_regex = 0;
        }

        function set_style(from, to) {
            while (r.length < from)
                r.push('default');
            while (r.length < to)
                r.push(style);
        }

        for (i = 0; i < n;) {
            style = null;
            start = i;
            switch (c = str[i++]) {
            case ' ':
            case '\t':
            case '\r':
            case '\n':
                continue;
            case '+':
            case '-':
                if (i < n && str[i] == c) {
                    i++;
                    continue;
                }
                can_regex = 1;
                continue;
            case '/':
                if (i < n && str[i] == '*') { // block comment
                    parse_block_comment();
                    break;
                }
                if (i < n && str[i] == '/') { // line comment
                    parse_line_comment();
                    break;
                }
                if (can_regex) {
                    parse_regex();
                    can_regex = 0;
                    break;
                }
                can_regex = 1;
                continue;
            case '\'':
            case '\"':
            case '`':
                parse_string(c);
                can_regex = 0;
                break;
            case '(':
            case '[':
            case '{':
                can_regex = 1;
                level++;
                push_state(c);
                continue;
            case ')':
            case ']':
            case '}':
                can_regex = 0;
                if (level > 0 && is_balanced(last_state(), c)) {
                    level--;
                    pop_state();
                    continue;
                }
                style = 'error';
                break;
            default:
                if (is_digit(c)) {
                    parse_number();
                    can_regex = 0;
                    break;
                }
                if (is_word(c)) {
                    parse_identifier();
                    break;
                }
                can_regex = 1;
                continue;
            }
            if (style)
                set_style(start, i);
        }
        set_style(n, n);
        return [ state, level, r ];
    }

    function config_file(s) {
        var xdg_state_home = std.getenv("XDG_STATE_HOME");
        if (xdg_state_home && xdg_state_home[0] === "/") return xdg_state_home + "/" + s;
        var home = std.getenv("HOME");
        if (os.platform === "linux" && home) xdg_state_home = home + "/.local/state";
        if (os.stat(xdg_state_home + "/")[1] === 0) return xdg_state_home + "/" + s;
        return (home || std.getenv("USERPROFILE") || ".") + "/." + s;
    }
    function save_history() {
        var s = history.slice(-1000).join('\n').trim();
        if (s) {
            try {
                var f = std.open(config_file("qjs_history"), "w");
                f.puts(s + '\n');
                f.close();
            } catch (e) {
            }
        }
    }
    function load_history() {
        var a = std.loadFile(config_file("qjs_history"));
        if (a) {
            history = a.trim().split('\n');
            history_index = history.length;
        }
    }
    function load_config() {
        var m, s = std.getenv("COLORFGBG");
        if (s && (m = s.match(/(\d+);(\d+)/))) {
            if (+m[2] !== 0) { // light background
                styles = themes.light;
            }
        }
        s = std.getenv("NO_COLOR"); // https://no-color.org/
        if (s && +s[0] !== 0) {
            show_colors = false;
        }
    }

    load_config();
    load_history();
    termInit();
    cmd_start();

})(globalThis);
