/*
 * Copyright (C) 2016 Kovid Goyal <kovid at kovidgoyal.net>
 *
 * Distributed under terms of the GPL3 license.
 */

// Need _POSIX_C_SOURCE for strtok_r
#define _POSIX_C_SOURCE 200809L

#include "data-types.h"
#include "control-codes.h"
#include "screen.h"
#include "graphics.h"
#include "charsets.h"
#include "monotonic.h"
#include <time.h>

extern PyTypeObject Screen_Type;
#define EXTENDED_OSC_SENTINEL 0x1bu
#define PENDING_BUF_INCREMENT (16u * 1024u)

// utils {{{
static const uint64_t pow_10_array[] = {
    1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000
};

static int64_t
utoi(const uint32_t *buf, unsigned int sz) {
    int64_t ans = 0;
    const uint32_t *p = buf;
    int mult = 1;
    if (sz && *p == '-') {
        mult = -1; p++; sz--;
    }
    // Ignore leading zeros
    while(sz > 0) {
        if (*p == '0') { p++; sz--; }
        else break;
    }
    if (sz < sizeof(pow_10_array)/sizeof(pow_10_array[0])) {
        for (int i = sz-1, j=0; i >= 0; i--, j++) {
            ans += (p[i] - '0') * pow_10_array[j];
        }
    }
    return ans * mult;
}


static const char*
utf8(char_type codepoint) {
    if (!codepoint) return "";
    static char buf[8];
    int n = encode_utf8(codepoint, buf);
    buf[n] = 0;
    return buf;
}

// }}}

// Macros {{{
#define IS_DIGIT \
    case '0': \
    case '1': \
    case '2': \
    case '3': \
    case '4': \
    case '5': \
    case '6': \
    case '7': \
    case '8': \
    case '9':


#ifdef DUMP_COMMANDS
static void
_report_error(PyObject *dump_callback, const char *fmt, ...) {
    va_list argptr;
    va_start(argptr, fmt);
    PyObject *temp = PyUnicode_FromFormatV(fmt, argptr);
    va_end(argptr);
    if (temp != NULL) {
        Py_XDECREF(PyObject_CallFunctionObjArgs(dump_callback, temp, NULL)); PyErr_Clear();
        Py_CLEAR(temp);
    }
}

static void
_report_params(PyObject *dump_callback, const char *name, int *params, unsigned int count, Region *r) {
    static char buf[MAX_PARAMS*3] = {0};
    unsigned int i, p=0;
    if (r) p += snprintf(buf + p, sizeof(buf) - 2, "%u %u %u %u ", r->top, r->left, r->bottom, r->right);
    for(i = 0; i < count && p < MAX_PARAMS*3-20; i++) {
        int n = snprintf(buf + p, MAX_PARAMS*3 - p, "%i ", params[i]);
        if (n < 0) break;
        p += n;
    }
    buf[p] = 0;
    Py_XDECREF(PyObject_CallFunction(dump_callback, "ss", name, buf)); PyErr_Clear();
}

#define DUMP_UNUSED

#define REPORT_ERROR(...) _report_error(dump_callback, __VA_ARGS__);

#define REPORT_COMMAND1(name) \
        Py_XDECREF(PyObject_CallFunction(dump_callback, "s", #name)); PyErr_Clear();

#define REPORT_COMMAND2(name, x) \
        Py_XDECREF(PyObject_CallFunction(dump_callback, "si", #name, (int)x)); PyErr_Clear();

#define REPORT_COMMAND3(name, x, y) \
        Py_XDECREF(PyObject_CallFunction(dump_callback, "sii", #name, (int)x, (int)y)); PyErr_Clear();

#define GET_MACRO(_1,_2,_3,NAME,...) NAME
#define REPORT_COMMAND(...) GET_MACRO(__VA_ARGS__, REPORT_COMMAND3, REPORT_COMMAND2, REPORT_COMMAND1, SENTINEL)(__VA_ARGS__)
#define REPORT_VA_COMMAND(...) Py_XDECREF(PyObject_CallFunction(dump_callback, __VA_ARGS__)); PyErr_Clear();

#define REPORT_DRAW(ch) \
    Py_XDECREF(PyObject_CallFunction(dump_callback, "sC", "draw", ch)); PyErr_Clear();

#define REPORT_PARAMS(name, params, num, region) _report_params(dump_callback, name, params, num_params, region)

#define FLUSH_DRAW \
    Py_XDECREF(PyObject_CallFunction(dump_callback, "sO", "draw", Py_None)); PyErr_Clear();

#define REPORT_OSC(name, string) \
    Py_XDECREF(PyObject_CallFunction(dump_callback, "sO", #name, string)); PyErr_Clear();

#define REPORT_OSC2(name, code, string) \
    Py_XDECREF(PyObject_CallFunction(dump_callback, "siO", #name, code, string)); PyErr_Clear();

#define REPORT_HYPERLINK(id, url) \
    Py_XDECREF(PyObject_CallFunction(dump_callback, "szz", "set_active_hyperlink", id, url)); PyErr_Clear();

#else

#define DUMP_UNUSED UNUSED

#define REPORT_ERROR(...) log_error(ERROR_PREFIX " " __VA_ARGS__);

#define REPORT_COMMAND(...)
#define REPORT_VA_COMMAND(...)
#define REPORT_DRAW(ch)
#define REPORT_PARAMS(...)
#define FLUSH_DRAW
#define REPORT_OSC(name, string)
#define REPORT_OSC2(name, code, string)
#define REPORT_HYPERLINK(id, url)

#endif

#define SET_STATE(state) screen->parser_state = state; screen->parser_buf_pos = 0;
// }}}

// Normal mode {{{
static void
screen_nel(Screen *screen) { screen_carriage_return(screen); screen_linefeed(screen); }

static void
dispatch_normal_mode_char(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_callback) {
#define CALL_SCREEN_HANDLER(name) REPORT_COMMAND(name); name(screen); break;
    switch(ch) {
        case BEL:
            CALL_SCREEN_HANDLER(screen_bell);
        case BS:
            CALL_SCREEN_HANDLER(screen_backspace);
        case HT:
            CALL_SCREEN_HANDLER(screen_tab);
        case NEL:
            CALL_SCREEN_HANDLER(screen_nel);
        case LF:
        case VT:
        case FF:
            CALL_SCREEN_HANDLER(screen_linefeed);
        case CR:
            CALL_SCREEN_HANDLER(screen_carriage_return);
        case SI:
            REPORT_COMMAND(screen_change_charset, 0);
            screen_change_charset(screen, 0); break;
        case SO:
            REPORT_COMMAND(screen_change_charset, 1);
            screen_change_charset(screen, 1); break;
        case IND:
            CALL_SCREEN_HANDLER(screen_index);
        case RI:
            CALL_SCREEN_HANDLER(screen_reverse_index);
        case HTS:
            CALL_SCREEN_HANDLER(screen_set_tab_stop);
        case ESC:
        case CSI:
        case OSC:
        case DCS:
        case APC:
        case PM:
            SET_STATE(ch); break;
        case NUL:
        case DEL:
            break;  // no-op
        default:
            REPORT_DRAW(ch);
            screen_draw(screen, ch, true);
            break;
    }
#undef CALL_SCREEN_HANDLER
} // }}}

// Esc mode {{{
static void
dispatch_esc_mode_char(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_callback) {
#define CALL_ED(name) REPORT_COMMAND(name); name(screen); SET_STATE(0);
#define CALL_ED1(name, ch) REPORT_COMMAND(name, ch); name(screen, ch); SET_STATE(0);
#define CALL_ED2(name, a, b) REPORT_COMMAND(name, a, b); name(screen, a, b); SET_STATE(0);
    switch(screen->parser_buf_pos) {
        case 0:
            switch (ch) {
                case ESC_DCS:
                    SET_STATE(DCS); break;
                case ESC_OSC:
                    SET_STATE(OSC); break;
                case ESC_CSI:
                    SET_STATE(CSI); break;
                case ESC_APC:
                    SET_STATE(APC); break;
                case ESC_PM:
                    SET_STATE(PM); break;
                case ESC_RIS:
                    CALL_ED(screen_reset); break;
                case ESC_IND:
                    CALL_ED(screen_index); break;
                case ESC_NEL:
                    CALL_ED(screen_nel); break;
                case ESC_RI:
                    CALL_ED(screen_reverse_index); break;
                case ESC_HTS:
                    CALL_ED(screen_set_tab_stop); break;
                case ESC_DECSC:
                    CALL_ED(screen_save_cursor); break;
                case ESC_DECRC:
                    CALL_ED(screen_restore_cursor); break;
                case ESC_DECKPNM:
                    CALL_ED(screen_normal_keypad_mode); break;
                case ESC_DECKPAM:
                    CALL_ED(screen_alternate_keypad_mode); break;
                case '%':
                case '(':
                case ')':
                case '*':
                case '+':
                case '-':
                case '.':
                case '/':
                case ' ':
                case '#':
                    screen->parser_buf[screen->parser_buf_pos++] = ch;
                    break;
                default:
                    REPORT_ERROR("%s0x%x", "Unknown char after ESC: ", ch);
                    SET_STATE(0); break;
            }
            break;
        default:
            switch(screen->parser_buf[0]) {
                case '%':
                    switch(ch) {
                        case '@':
                            REPORT_COMMAND(screen_use_latin1, 1);
                            screen_use_latin1(screen, true);
                            break;
                        case 'G':
                            REPORT_COMMAND(screen_use_latin1, 0);
                            screen_use_latin1(screen, false);
                            break;
                        default:
                            REPORT_ERROR("Unhandled Esc %% code: 0x%x", ch);  break;
                    }
                    break;
                case '#':
                    if (ch == '8') { CALL_ED(screen_align); }
                    else { REPORT_ERROR("Unhandled Esc # code: 0x%x", ch); }
                    break;
                case '(':
                case ')':
                    switch(ch) {
                        case 'A':
                        case 'B':
                        case '0':
                        case 'U':
                        case 'V':
                            CALL_ED2(screen_designate_charset, screen->parser_buf[0] - '(', ch); break;
                        default:
                            REPORT_ERROR("Unknown charset: 0x%x", ch); break;
                    }
                    break;
                case ' ':
                    switch(ch) {
                        case 'F':
                        case 'G':
                            REPORT_COMMAND(screen_set_8bit_controls, ch == 'G');
                            screen_set_8bit_controls(screen, ch == 'G');
                            break;
                        default:
                            REPORT_ERROR("Unhandled ESC SP escape code: 0x%x", ch); break;
                    }
                    break;
                default:
                    REPORT_ERROR("Unhandled charset related escape code: 0x%x 0x%x", screen->parser_buf[0], ch); break;
            }
            SET_STATE(0);
            break;
    }
#undef CALL_ED
#undef CALL_ED1
} // }}}

// OSC mode {{{

static bool
parse_osc_8(char *buf, char **id, char **url) {
    char *boundary = strstr(buf, ";");
    if (boundary == NULL) return false;
    *boundary = 0;
    if (*(boundary + 1)) *url = boundary + 1;
    char *save = NULL, *token = strtok_r(buf, ":", &save);
    while (token != NULL) {
        size_t len = strlen(token);
        if (len > 3 && token[0] == 'i' && token[1] == 'd' && token[2] == '=' && token[3]) {
            *id = token + 3;
            break;
        }
        token = strtok_r(NULL, ":", &save);
    }
    return true;
}

static void
dispatch_hyperlink(Screen *screen, size_t pos, size_t size, PyObject DUMP_UNUSED *dump_callback) {
    // since the spec says only ASCII printable chars are allowed in OSC 8, we
    // can just convert to char* directly
    if (!size) return;  // ignore empty OSC 8 since it must have two semi-colons to be valid, which means one semi-colon here
    char *id = NULL, *url = NULL;
    char *data = malloc(size + 1);
    if (!data) fatal("Out of memory");
    for (size_t i = 0; i < size; i++) {
        data[i] = screen->parser_buf[i + pos] & 0x7f;
        if (data[i] < 32 || data[i] > 126) data[i] = '_';
    }
    data[size] = 0;

    if (parse_osc_8(data, &id, &url)) {
        REPORT_HYPERLINK(id, url);
        set_active_hyperlink(screen, id, url);
    } else {
        REPORT_ERROR("Ignoring malformed OSC 8 code");
    }

    free(data);
}

static void
continue_osc_52(Screen *screen) {
    screen->parser_buf[0] = '5'; screen->parser_buf[1] = '2'; screen->parser_buf[2] = ';';
    screen->parser_buf[3] = ';'; screen->parser_buf_pos = 4;
}

static bool
is_extended_osc(const Screen *screen) {
    return screen->parser_buf_pos > 2 && screen->parser_buf[0] == EXTENDED_OSC_SENTINEL && screen->parser_buf[1] == 1 && screen->parser_buf[2] == ';';
}

static void
dispatch_osc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
#define DISPATCH_OSC_WITH_CODE(name) REPORT_OSC2(name, code, string); name(screen, code, string);
#define DISPATCH_OSC(name) REPORT_OSC(name, string); name(screen, string);
#define START_DISPATCH {\
    PyObject *string = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + i, limit - i); \
    if (string) {
#define END_DISPATCH Py_CLEAR(string); } PyErr_Clear(); break; }

    const unsigned int limit = screen->parser_buf_pos;
    int code=0;
    unsigned int i;
    for (i = 0; i < MIN(limit, 5u); i++) {
        if (screen->parser_buf[i] < '0' || screen->parser_buf[i] > '9') break;
    }
    if (i > 0) {
        code = utoi(screen->parser_buf, i);
        if (i < limit && screen->parser_buf[i] == ';') i++;
    } else {
        if (is_extended_osc(screen)) {
            // partial OSC 52
            i = 3;
            code = -52;
        }
    }
    switch(code) {
        case 0:
            START_DISPATCH
            DISPATCH_OSC(set_title);
            DISPATCH_OSC(set_icon);
            END_DISPATCH
        case 1:
            START_DISPATCH
            DISPATCH_OSC(set_icon);
            END_DISPATCH
        case 2:
            START_DISPATCH
            DISPATCH_OSC(set_title);
            END_DISPATCH
        case 4:
        case 104:
            START_DISPATCH
            DISPATCH_OSC_WITH_CODE(set_color_table_color);
            END_DISPATCH
        case 6:
        case 7:
            START_DISPATCH
            DISPATCH_OSC_WITH_CODE(process_cwd_notification);
            END_DISPATCH
        case 8:
            dispatch_hyperlink(screen, i, limit-i, dump_callback);
            break;
        case 9:
        case 99:
        case 777:
            START_DISPATCH
            DISPATCH_OSC_WITH_CODE(desktop_notify)
            END_DISPATCH
        case 10:
        case 11:
        case 12:
        case 17:
        case 19:
        case 110:
        case 111:
        case 112:
        case 117:
        case 119:
            START_DISPATCH
            DISPATCH_OSC_WITH_CODE(set_dynamic_color);
            END_DISPATCH
        case 52: case -52: case 5522:
            START_DISPATCH
            DISPATCH_OSC_WITH_CODE(clipboard_control);
            if (code == -52) continue_osc_52(screen);
            END_DISPATCH
        case 133:
            START_DISPATCH
            DISPATCH_OSC(shell_prompt_marking);
            END_DISPATCH
        case FILE_TRANSFER_CODE:
            START_DISPATCH
            DISPATCH_OSC(file_transmission);
            END_DISPATCH
        case 30001:
            REPORT_COMMAND(screen_push_dynamic_colors);
            screen_push_colors(screen, 0);
            break;
        case 30101:
            REPORT_COMMAND(screen_pop_dynamic_colors);
            screen_pop_colors(screen, 0);
            break;
        case 697:
            REPORT_ERROR("Ignoring OSC 697, typically used by Fig for shell integration");
            break;
        default:
            REPORT_ERROR("Unknown OSC code: %u", code);
            break;
    }
#undef DISPATCH_OSC
#undef DISPATCH_OSC_WITH_CODE
#undef START_DISPATCH
#undef END_DISPATCH
}
// }}}

// CSI mode {{{
// As per ECMA 48 section 5.4 secondary byte is column 02 of the 7-bit ascii table
#define CSI_SECONDARY \
        case ' ': \
        case '!': \
        case '"': \
        case '#': \
        case '$': \
        case '%': \
        case '&': \
        case '\'': \
        case '(': \
        case ')': \
        case '*': \
        case '+': \
        case ',': \
        case '-': \
        case '.': \
        case '/':


static void
screen_cursor_up2(Screen *s, unsigned int count) { screen_cursor_up(s, count, false, -1); }
static void
screen_cursor_back1(Screen *s, unsigned int count) { screen_cursor_back(s, count, -1); }
static void
screen_tabn(Screen *s, unsigned int count) { for (index_type i=0; i < MAX(1u, count); i++) screen_tab(s); }

static const char*
repr_csi_params(int *params, unsigned int num_params) {
    if (!num_params) return "";
    static char buf[256];
    unsigned int pos = 0, i = 0;
    while (pos < 200 && i++ < num_params && sizeof(buf) > pos + 1) {
        const char *fmt = i < num_params ? "%i, " : "%i";
        int ret = snprintf(buf + pos, sizeof(buf) - pos - 1, fmt, params[i-1]);
        if (ret < 0) return "An error occurred formatting the params array";
        pos += ret;
    }
    buf[pos] = 0;
    return buf;
}

#ifdef DUMP_COMMANDS
static
#endif
void
parse_sgr(Screen *screen, uint32_t *buf, unsigned int num, int *params, PyObject DUMP_UNUSED *dump_callback, const char *report_name DUMP_UNUSED, Region *region) {
    enum State { START, NORMAL, MULTIPLE, COLOR, COLOR1, COLOR3 };
    enum State state = START;
    unsigned int num_params, num_start, i;

#define READ_PARAM { params[num_params++] = utoi(buf + num_start, i - num_start); }
#define SEND_SGR { REPORT_PARAMS(report_name, params, num_params, region); select_graphic_rendition(screen, params, num_params, region); state = START; num_params = 0; }

    for (i=0, num_start=0, num_params=0; i < num && num_params < MAX_PARAMS; i++) {
        switch(buf[i]) {
            IS_DIGIT
                switch(state) {
                    case START:
                        num_start = i;
                        state = NORMAL;
                        num_params = 0;
                        break;
                    default:
                        break;
                }
                break;
            case ';':
                switch(state) {
                    case START:
                        params[num_params++] = 0;
                        SEND_SGR;
                        break;
                    case NORMAL:
                        READ_PARAM;
                        switch(params[0]) {
                            case 38:
                            case 48:
                            case 58:
                                state = COLOR;
                                num_start = i + 1;
                                break;
                            default:
                                SEND_SGR;
                                break;
                        }
                        break;
                    case MULTIPLE:
                        READ_PARAM;
                        SEND_SGR;
                        break;
                    case COLOR:
                        READ_PARAM;
                        switch(params[1]) {
                            case 2:
                                state = COLOR3;
                                break;
                            case 5:
                                state = COLOR1;
                                break;
                            default:
                                REPORT_ERROR("Invalid SGR color code with unknown color type: %u", params[1]);
                                return;
                        }
                        num_start = i + 1;
                        break;
                    case COLOR1:
                        READ_PARAM;
                        SEND_SGR;
                        break;
                    case COLOR3:
                        READ_PARAM;
                        if (num_params == 5) { SEND_SGR; }
                        else num_start = i + 1;
                        break;
                }
                break;
            case ':':
                switch(state) {
                    case START:
                        REPORT_ERROR("Invalid SGR code containing ':' at an invalid location: %u", i);
                        return;
                    case NORMAL:
                        READ_PARAM;
                        state = MULTIPLE;
                        num_start = i + 1;
                        break;
                    case MULTIPLE:
                        READ_PARAM;
                        num_start = i + 1;
                        break;
                    case COLOR:
                    case COLOR1:
                    case COLOR3:
                        REPORT_ERROR("Invalid SGR code containing disallowed character: %s (U+%x)", utf8(buf[i]), buf[i]);
                        return;
                }
                break;
            default:
                REPORT_ERROR("Invalid SGR code containing disallowed character: %s (U+%x)", utf8(buf[i]), buf[i]);
                return;
        }
    }
    switch(state) {
        case START:
            if (num_params < MAX_PARAMS) params[num_params++] = 0;
            SEND_SGR;
            break;
        case COLOR1:
        case NORMAL:
        case MULTIPLE:
            if (i > num_start && num_params < MAX_PARAMS) { READ_PARAM; }
            if (num_params) { SEND_SGR; }
            else { REPORT_ERROR("Incomplete SGR code"); }
            break;
        case COLOR:
            REPORT_ERROR("Invalid SGR code containing incomplete semi-colon separated color sequence");
            break;
        case COLOR3:
            if (i > num_start && num_params < MAX_PARAMS) READ_PARAM;
            if (num_params != 5) {
                REPORT_ERROR("Invalid SGR code containing incomplete semi-colon separated color sequence");
                break;
            }
            if (num_params) { SEND_SGR; }
            else { REPORT_ERROR("Incomplete SGR code"); }
            break;
    }
#undef READ_PARAM
#undef SEND_SGR
}

static unsigned int
parse_region(Region *r, uint32_t *buf, unsigned int num) {
    unsigned int i, start, num_params = 0;
    int params[8] = {0};
    for (i=0, start=0; i < num && num_params < 4; i++) {
        switch(buf[i]) {
            IS_DIGIT
                break;
            default:
                if (i > start) params[num_params++] = utoi(buf + start, i - start);
                else if (i == start && buf[i] == ';') params[num_params++] = 0;
                start = i + 1;
                break;
        }
    }

    switch(num_params) {
        case 0:
            break;
        case 1:
            r->top = params[0];
            break;
        case 2:
            r->top = params[0]; r->left = params[1];
            break;
        case 3:
            r->top = params[0]; r->left = params[1]; r->bottom = params[2];
            break;
        default:
            r->top = params[0]; r->left = params[1]; r->bottom = params[2]; r->right = params[3];
            break;
    }
    return i;
}

static const char*
csi_letter(unsigned code) {
    static char buf[8];
    if (33 <= code && code <= 126) snprintf(buf, sizeof(buf), "%c", code);
    else snprintf(buf, sizeof(buf), "0x%x", code);
    return buf;
}

static void
dispatch_csi(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
#define AT_MOST_ONE_PARAMETER { \
    if (num_params > 1) { \
        REPORT_ERROR("CSI code %s has %u > 1 parameters", csi_letter(code), num_params); \
        break; \
    } \
}
#define NON_NEGATIVE_PARAM(x) { \
    if (x < 0) { \
        REPORT_ERROR("CSI code %s is not allowed to have negative parameter (%d)", csi_letter(code), x); \
        break; \
    } \
}

#define CALL_CSI_HANDLER1(name, defval) \
    AT_MOST_ONE_PARAMETER; \
    p1 = num_params > 0 ? params[0] : defval; \
    NON_NEGATIVE_PARAM(p1); \
    REPORT_COMMAND(name, p1); \
    name(screen, p1); \
    break;

#define CALL_CSI_HANDLER1P(name, defval, qch) \
    AT_MOST_ONE_PARAMETER; \
    p1 = num_params > 0 ? params[0] : defval; \
    NON_NEGATIVE_PARAM(p1); \
    private = start_modifier == qch; \
    REPORT_COMMAND(name, p1, private); \
    name(screen, p1, private); \
    break;

#define CALL_CSI_HANDLER1S(name, defval) \
    AT_MOST_ONE_PARAMETER; \
    p1 = num_params > 0 ? params[0] : defval; \
    NON_NEGATIVE_PARAM(p1); \
    REPORT_COMMAND(name, p1, start_modifier); \
    name(screen, p1, start_modifier); \
    break;

#define CALL_CSI_HANDLER1M(name, defval) \
    AT_MOST_ONE_PARAMETER; \
    p1 = num_params > 0 ? params[0] : defval; \
    NON_NEGATIVE_PARAM(p1); \
    REPORT_COMMAND(name, p1, end_modifier); \
    name(screen, p1, end_modifier); \
    break;

#define CALL_CSI_HANDLER2(name, defval1, defval2) \
    if (num_params > 2) { \
        REPORT_ERROR("CSI code %s has %u > 2 parameters", csi_letter(code), num_params); \
        break; \
    } \
    p1 = num_params > 0 ? params[0] : defval1; \
    p2 = num_params > 1 ? params[1] : defval2; \
    NON_NEGATIVE_PARAM(p1); \
    NON_NEGATIVE_PARAM(p2); \
    REPORT_COMMAND(name, p1, p2); \
    name(screen, p1, p2); \
    break;

#define SET_MODE(func) \
    p1 = start_modifier == '?' ? 5 : 0; \
    for (i = 0; i < num_params; i++) { \
        NON_NEGATIVE_PARAM(params[i]); \
        REPORT_COMMAND(func, params[i], start_modifier == '?'); \
        func(screen, params[i] << p1); \
    } \
    break;

#define NO_MODIFIERS(modifier, special, special_msg) { \
    if (start_modifier || end_modifier) { \
        if (special && modifier == special) { REPORT_ERROR(special_msg); } \
        else { REPORT_ERROR("CSI code %s has unsupported start modifier: %s or end modifier: %s", csi_letter(code), csi_letter(start_modifier), csi_letter(end_modifier));} \
        break; \
    } \
}

    char start_modifier = 0, end_modifier = 0;
    uint32_t *buf = screen->parser_buf, code = screen->parser_buf[screen->parser_buf_pos];
    unsigned int num = screen->parser_buf_pos, start, i, num_params=0;
    static int params[MAX_PARAMS] = {0}, p1, p2;
    bool private;
    if (buf[0] == '>' || buf[0] == '<' || buf[0] == '?' || buf[0] == '!' || buf[0] == '=') {
        start_modifier = (char)screen->parser_buf[0];
        buf++; num--;
    }
    if (num > 0) {
        switch(buf[num-1]) {
            CSI_SECONDARY
                end_modifier = (char)buf[--num];
                break;
        }
    }
    if (code == SGR && !start_modifier && !end_modifier) {
        parse_sgr(screen, buf, num, params, dump_callback, "select_graphic_rendition", NULL);
        return;
    }
    if (code == 'r' && !start_modifier && end_modifier == '$') {
        // DECCARA
        Region r = {0};
        unsigned int consumed = parse_region(&r, buf, num);
        num -= consumed; buf += consumed;
        parse_sgr(screen, buf, num, params, dump_callback, "deccara", &r);
        return;
    }

    for (i=0, start=0; i < num; i++) {
        switch(buf[i]) {
            IS_DIGIT
                break;
            case '-':
                if (i > start) {
                    REPORT_ERROR("CSI code can contain hyphens only at the start of numbers");
                    return;
                }
                break;
            default:
                if (i > start) params[num_params++] = utoi(buf + start, i - start);
                else if (i == start && buf[i] == ';') params[num_params++] = 0;
                if (num_params >= MAX_PARAMS) { i = num; start = num + 1; }
                else { start = i + 1; break; }
        }
    }
    if (i > start) params[num_params++] = utoi(buf + start, i - start);
    switch(code) {
        case ICH:
            NO_MODIFIERS(end_modifier, ' ', "Shift left escape code not implemented");
            CALL_CSI_HANDLER1(screen_insert_characters, 1);
        case REP:
            CALL_CSI_HANDLER1(screen_repeat_character, 1);
        case CUU:
            NO_MODIFIERS(end_modifier, ' ', "Shift right escape code not implemented");
            CALL_CSI_HANDLER1(screen_cursor_up2, 1);
        case CUD:
        case VPR:
            CALL_CSI_HANDLER1(screen_cursor_down, 1);
        case CUF:
        case HPR:
            CALL_CSI_HANDLER1(screen_cursor_forward, 1);
        case CUB:
            CALL_CSI_HANDLER1(screen_cursor_back1, 1);
        case CNL:
            CALL_CSI_HANDLER1(screen_cursor_down1, 1);
        case CPL:
            CALL_CSI_HANDLER1(screen_cursor_up1, 1);
        case CHA:
        case HPA:
            CALL_CSI_HANDLER1(screen_cursor_to_column, 1);
        case VPA:
            CALL_CSI_HANDLER1(screen_cursor_to_line, 1);
        case CBT:
            CALL_CSI_HANDLER1(screen_backtab, 1);
        case CHT:
            CALL_CSI_HANDLER1(screen_tabn, 1);
        case CUP:
        case HVP:
            CALL_CSI_HANDLER2(screen_cursor_position, 1, 1);
        case ED:
            CALL_CSI_HANDLER1P(screen_erase_in_display, 0, '?');
        case EL:
            CALL_CSI_HANDLER1P(screen_erase_in_line, 0, '?');
        case IL:
            CALL_CSI_HANDLER1(screen_insert_lines, 1);
        case DL:
            CALL_CSI_HANDLER1(screen_delete_lines, 1);
        case DCH:
            if (end_modifier == '#' && !start_modifier) {
                CALL_CSI_HANDLER1(screen_push_colors, 0);
            } else {
                CALL_CSI_HANDLER1(screen_delete_characters, 1);
            }
        case 'Q':
            if (end_modifier == '#' && !start_modifier) { CALL_CSI_HANDLER1(screen_pop_colors, 0); }
            REPORT_ERROR("Unknown CSI Q sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params);
            break;
        case 'R':
            if (end_modifier == '#' && !start_modifier) {
                REPORT_COMMAND(screen_report_color_stack);
                screen_report_color_stack(screen);
                break;
            }
            REPORT_ERROR("Unknown CSI R sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params);
            break;
        case ECH:
            CALL_CSI_HANDLER1(screen_erase_characters, 1);
        case DA:
            CALL_CSI_HANDLER1S(report_device_attributes, 0);
        case TBC:
            CALL_CSI_HANDLER1(screen_clear_tab_stop, 0);
        case SM:
            SET_MODE(screen_set_mode);
        case RM:
            SET_MODE(screen_reset_mode);
        case DSR:
            CALL_CSI_HANDLER1P(report_device_status, 0, '?');
        case 's':
            if (!start_modifier && !end_modifier && !num_params) {
                REPORT_COMMAND(screen_save_cursor);
                screen_save_cursor(screen);
                break;
            } else if (start_modifier == '?' && !end_modifier) {
                if (!num_params) {
                    REPORT_COMMAND(screen_save_modes);
                    screen_save_modes(screen);
                } else { SET_MODE(screen_save_mode); }
                break;
            }
            REPORT_ERROR("Unknown CSI s sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params);
            break;
        case 't':
            if (!num_params) {
                REPORT_ERROR("Unknown CSI t sequence with start and end modifiers: '%c' '%c' and no parameters", start_modifier, end_modifier);
                break;
            }
            if (start_modifier || end_modifier) {
                REPORT_ERROR("Unknown CSI t sequence with start and end modifiers: '%c' '%c', %u parameters and first parameter: %d", start_modifier, end_modifier, num_params, params[0]);
                break;
            }
            switch(params[0]) {
                case 4:
                case 8:
                    log_error("Escape codes to resize text area are not supported");
                    break;
                case 14:
                case 16:
                case 18:
                    CALL_CSI_HANDLER1(screen_report_size, 0);
                    break;
                case 22:
                case 23:
                    if (num_params == 3 && !params[2]) num_params = 2; // ignore extra 0, generated by weechat or ncurses
                    CALL_CSI_HANDLER2(screen_manipulate_title_stack, 22, 0);
                    break;
                default:
                    REPORT_ERROR("Unknown CSI t window manipulation sequence with %u parameters and first parameter: %d", num_params, params[0]);
                    break;
            }
            break;
        case 'u':
            if (!start_modifier && !end_modifier && !num_params) {
                REPORT_COMMAND(screen_restore_cursor);
                screen_restore_cursor(screen);
                break;
            }
            if (!end_modifier && start_modifier == '?') {
                REPORT_COMMAND(screen_report_key_encoding_flags);
                screen_report_key_encoding_flags(screen);
                break;
            }
            if (!end_modifier && start_modifier == '=') {
                CALL_CSI_HANDLER2(screen_set_key_encoding_flags, 0, 1);
                break;
            }
            if (!end_modifier && start_modifier == '>') {
                CALL_CSI_HANDLER1(screen_push_key_encoding_flags, 0);
                break;
            }
            if (!end_modifier && start_modifier == '<') {
                CALL_CSI_HANDLER1(screen_pop_key_encoding_flags, 1);
                break;
            }
            REPORT_ERROR("Unknown CSI u sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params);
            break;
        case 'r':
            if (!start_modifier && !end_modifier) {
                // DECSTBM
                CALL_CSI_HANDLER2(screen_set_margins, 0, 0);
            } else if (start_modifier == '?' && !end_modifier) {
                if (!num_params) {
                    REPORT_COMMAND(screen_restore_modes);
                    screen_restore_modes(screen);
                } else { SET_MODE(screen_restore_mode); }
                break;
            }
            REPORT_ERROR("Unknown CSI r sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params);
            break;
        case 'x':
            if (!start_modifier && end_modifier == '*') {
                CALL_CSI_HANDLER1(screen_decsace, 0);
            }
            REPORT_ERROR("Unknown CSI x sequence with start and end modifiers: '%c' '%c'", start_modifier, end_modifier);
            break;
        case DECSCUSR:
            if (!start_modifier && end_modifier == ' ') {
                CALL_CSI_HANDLER1M(screen_set_cursor, 1);
            }
            if (start_modifier == '>' && !end_modifier) {
                CALL_CSI_HANDLER1(screen_xtversion, 0);
            }
            REPORT_ERROR("Unknown CSI q sequence with start and end modifiers: '%c' '%c'", start_modifier, end_modifier);
            break;
        case SU:
            NO_MODIFIERS(end_modifier, ' ', "Select presentation directions escape code not implemented");
            CALL_CSI_HANDLER1(screen_scroll, 1);
        case SD:
            if (!start_modifier && end_modifier == '+') {
                CALL_CSI_HANDLER1(screen_reverse_scroll_and_fill_from_scrollback, 1);
            } else {
                NO_MODIFIERS(start_modifier, 0, "");
                CALL_CSI_HANDLER1(screen_reverse_scroll, 1);
            }
            break;
        case DECSTR:
            if (end_modifier == '$') {
                // DECRQM
                CALL_CSI_HANDLER1P(report_mode_status, 0, '?');
            } else {
                REPORT_ERROR("Unknown DECSTR CSI sequence with start and end modifiers: '%c' '%c'", start_modifier, end_modifier);
            }
            break;
        case 'm':
            if (start_modifier == '>' && !end_modifier) {
                REPORT_ERROR(
                    "The application is trying to use xterm's modifyOtherKeys."
                    " This is superseded by the kitty keyboard protocol: https://sw.kovidgoyal.net/kitty/keyboard-protocol/"
                    " the application should be updated to use that"
                );
                break;
            }
            /* fallthrough */
        default:
            REPORT_ERROR("Unknown CSI code: '%s' with start_modifier: '%c' and end_modifier: '%c' and parameters: '%s'", utf8(code), start_modifier, end_modifier, repr_csi_params(params, num_params));
    }
}
// }}}

// DCS mode {{{

static bool
startswith(const uint32_t *string, ssize_t sz, const char *prefix, ssize_t l) {
    if (sz < l) return false;
    for (ssize_t i = 0; i < l; i++) {
        if (string[i] != (unsigned char)prefix[i]) return false;
    }
    return true;
}

#define PENDING_MODE_CHAR '='

static void
dispatch_dcs(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
    if (screen->parser_buf_pos < 2) return;
    switch(screen->parser_buf[0]) {
        case '+':
        case '$':
            if (screen->parser_buf[1] == 'q') {
                PyObject *string = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + 2, screen->parser_buf_pos - 2);
                if (string != NULL) {
                    REPORT_OSC2(screen_request_capabilities, (char)screen->parser_buf[0], string);
                    screen_request_capabilities(screen, (char)screen->parser_buf[0], string);
                    Py_DECREF(string);
                } else PyErr_Clear();
            } else {
                REPORT_ERROR("Unrecognized DCS %c code: 0x%x", (char)screen->parser_buf[0], screen->parser_buf[1]);
            }
            break;
        case PENDING_MODE_CHAR:
            if (screen->parser_buf_pos > 2 && (screen->parser_buf[1] == '1' || screen->parser_buf[1] == '2') && screen->parser_buf[2] == 's') {
                if (screen->parser_buf[1] == '1') {
                    screen->pending_mode.activated_at = monotonic();
                    REPORT_COMMAND(screen_start_pending_mode);
                } else {
                    // ignore stop without matching start, see queue_pending_bytes()
                    // for how stop is detected while in pending mode.
                    REPORT_ERROR("Pending mode stop command issued while not in pending mode, this can"
                        " be either a bug in the terminal application or caused by a timeout with no data"
                        " received for too long or by too much data in pending mode");
                    REPORT_COMMAND(screen_stop_pending_mode);
                }
            } else {
                REPORT_ERROR("Unrecognized DCS %c code: 0x%x", (char)screen->parser_buf[0], screen->parser_buf[1]);
            }
            break;
        case '@':
            if (startswith(screen->parser_buf + 1, screen->parser_buf_pos - 2, "kitty-", sizeof("kitty-") - 1)) {
                if (startswith(screen->parser_buf + 7, screen->parser_buf_pos - 2, "cmd{", sizeof("cmd{") - 1)) {
                    PyObject *cmd = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + 10, screen->parser_buf_pos - 10);
                    if (cmd != NULL) {
                        REPORT_OSC2(screen_handle_cmd, (char)screen->parser_buf[0], cmd);
                        screen_handle_cmd(screen, cmd);
                        Py_DECREF(cmd);
                    } else PyErr_Clear();
#define IF_SIMPLE_PREFIX(prefix, func) \
        if (startswith(screen->parser_buf + 7, screen->parser_buf_pos - 1, prefix, sizeof(prefix) - 1)) { \
            const size_t pp_size = sizeof("kitty") + sizeof(prefix); \
            PyObject *msg = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf + pp_size, screen->parser_buf_pos - pp_size); \
            if (msg != NULL) { \
                REPORT_OSC2(func, (char)screen->parser_buf[0], msg); \
                screen_handle_kitty_dcs(screen, #func, msg); \
                Py_DECREF(msg); \
            } else PyErr_Clear();

                } else IF_SIMPLE_PREFIX("overlay-ready|", handle_overlay_ready)
                } else IF_SIMPLE_PREFIX("kitten-result|", handle_kitten_result)
                } else IF_SIMPLE_PREFIX("print|", handle_remote_print)
                } else IF_SIMPLE_PREFIX("echo|", handle_remote_echo)
                } else IF_SIMPLE_PREFIX("ssh|", handle_remote_ssh)
                } else IF_SIMPLE_PREFIX("ask|", handle_remote_askpass)
                } else IF_SIMPLE_PREFIX("clone|", handle_remote_clone)
                } else IF_SIMPLE_PREFIX("edit|", handle_remote_edit)
#undef IF_SIMPLE_PREFIX
                } else {
                    PyObject *tp = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, screen->parser_buf, screen->parser_buf_pos);
                    REPORT_ERROR("Unrecognized DCS @ code: %s", tp ? PyUnicode_AsUTF8(tp) : "could not read");
                    Py_XDECREF(tp);
                }
            }
            break;
        default:
            REPORT_ERROR("Unrecognized DCS code: 0x%x", screen->parser_buf[0]);
            break;
    }
}
// }}}

// APC mode {{{

#include "parse-graphics-command.h"

static void
dispatch_apc(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
    if (screen->parser_buf_pos < 2) return;
    switch(screen->parser_buf[0]) {
        case 'G':
            parse_graphics_code(screen, dump_callback);
            break;
        default:
            REPORT_ERROR("Unrecognized APC code: 0x%x", screen->parser_buf[0]);
            break;
    }
}

// }}}

// PM mode {{{
static void
dispatch_pm(Screen *screen, PyObject DUMP_UNUSED *dump_callback) {
    if (screen->parser_buf_pos < 2) return;
    switch(screen->parser_buf[0]) {
        default:
            REPORT_ERROR("Unrecognized PM code: 0x%x", screen->parser_buf[0]);
            break;
    }
}


// }}}

// Parse loop {{{

static bool
handle_extended_osc_code(Screen *screen) {
    // Handle extra long OSC 52 codes
    if (screen->parser_buf[0] != '5' || screen->parser_buf[1] != '2' || screen->parser_buf[2] != ';') return false;
    screen->parser_buf[0] = EXTENDED_OSC_SENTINEL; screen->parser_buf[1] = 1;
    return true;
}

static bool
accumulate_osc(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_callback, bool *extended_osc_code) {
    switch(ch) {
        case ST:
            return true;
        case BEL:
            return true;
        case NUL:
        case DEL:
            break;
        case ESC_ST:
            if (screen->parser_buf_pos > 0 && screen->parser_buf[screen->parser_buf_pos - 1] == ESC) {
                screen->parser_buf_pos--;
                return true;
            }
            /* fallthrough */
        default:
            if (screen->parser_buf_pos >= PARSER_BUF_SZ - 1) {
                if (handle_extended_osc_code(screen)) *extended_osc_code = true;
                else REPORT_ERROR("OSC sequence too long, truncating.");
                return true;
            }
            screen->parser_buf[screen->parser_buf_pos++] = ch;
            break;
    }
    return false;
}

static bool
accumulate_dcs(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_callback) {
    switch(ch) {
        case ST:
            return true;
        case NUL:
        case DEL:
            break;
        case ESC:
START_ALLOW_CASE_RANGE
        case 32 ... 126:
END_ALLOW_CASE_RANGE
            if (screen->parser_buf_pos > 0 && screen->parser_buf[screen->parser_buf_pos-1] == ESC) {
                if (ch == '\\') { screen->parser_buf_pos--; return true; }
                REPORT_ERROR("DCS sequence contained ESC without trailing \\ at pos: %u ignoring the sequence", screen->parser_buf_pos);
                SET_STATE(ESC); return false;
            }
            if (screen->parser_buf_pos >= PARSER_BUF_SZ - 1) {
                REPORT_ERROR("DCS sequence too long, truncating.");
                return true;
            }
            screen->parser_buf[screen->parser_buf_pos++] = ch;
            break;
        default:
            REPORT_ERROR("DCS sequence contained non-printable character: 0x%x ignoring the sequence", ch);
    }
    return false;
}


static bool
accumulate_oth(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_callback) {
    switch(ch) {
        case ST:
            return true;
        case DEL:
        case NUL:
            break;
        case ESC_ST:
            if (screen->parser_buf_pos > 0 && screen->parser_buf[screen->parser_buf_pos - 1] == ESC) {
                screen->parser_buf_pos--;
                return true;
            }
            /* fallthrough */
        default:
            if (screen->parser_buf_pos >= PARSER_BUF_SZ - 1) {
                REPORT_ERROR("OTH sequence too long, truncating.");
                return true;
            }
            screen->parser_buf[screen->parser_buf_pos++] = ch;
            break;
    }
    return false;
}


static bool
accumulate_csi(Screen *screen, uint32_t ch, PyObject DUMP_UNUSED *dump_callback) {
#define ENSURE_SPACE \
    if (screen->parser_buf_pos > PARSER_BUF_SZ - 1) { \
        REPORT_ERROR("CSI sequence too long, ignoring"); \
        SET_STATE(0); \
        return false; \
    }

    switch(ch) {
        IS_DIGIT
        CSI_SECONDARY
        case ':':
        case ';':
            ENSURE_SPACE;
            screen->parser_buf[screen->parser_buf_pos++] = ch;
            break;
        case '?':
        case '>':
        case '<':
        case '=':
            if (screen->parser_buf_pos != 0) {
                REPORT_ERROR("Invalid character in CSI: 0x%x, ignoring the sequence", ch);
                SET_STATE(0);
                return false;
            }
            ENSURE_SPACE;
            screen->parser_buf[screen->parser_buf_pos++] = ch;
            break;
START_ALLOW_CASE_RANGE
        case 'a' ... 'z':
        case 'A' ... 'Z':
END_ALLOW_CASE_RANGE
        case '@':
        case '`':
        case '{':
        case '|':
        case '}':
        case '~':
            screen->parser_buf[screen->parser_buf_pos] = ch;
            return true;
        case BEL:
        case BS:
        case HT:
        case LF:
        case VT:
        case FF:
        case NEL:
        case CR:
        case SO:
        case SI:
        case IND:
        case RI:
        case HTS:
            dispatch_normal_mode_char(screen, ch, dump_callback);
            break;
        case NUL:
        case DEL:
            SET_STATE(0);
            break;  // no-op
        default:
            REPORT_ERROR("Invalid character in CSI: 0x%x, ignoring the sequence", ch);
            SET_STATE(0);
            return false;

    }
    return false;
#undef ENSURE_SPACE
}

#define dispatch_unicode_char(codepoint, dispatch, watch_for_pending) { \
    switch(screen->parser_state) { \
        case ESC: \
            dispatch##_esc_mode_char(screen, codepoint, dump_callback); \
            break; \
        case CSI: \
            if (accumulate_csi(screen, codepoint, dump_callback)) { dispatch##_csi(screen, dump_callback); SET_STATE(0); watch_for_pending; } \
            break; \
        case OSC: \
            { \
                bool extended_osc_code = false; \
                if (accumulate_osc(screen, codepoint, dump_callback, &extended_osc_code)) {  \
                    dispatch##_osc(screen, dump_callback); \
                    if (extended_osc_code) { \
                        if (accumulate_osc(screen, codepoint, dump_callback, &extended_osc_code)) { dispatch##_osc(screen, dump_callback); SET_STATE(0); } \
                    } else { SET_STATE(0); } \
                } \
            } \
            break; \
        case APC: \
            if (accumulate_oth(screen, codepoint, dump_callback)) { dispatch##_apc(screen, dump_callback); SET_STATE(0); } \
            break; \
        case PM: \
            if (accumulate_oth(screen, codepoint, dump_callback)) { dispatch##_pm(screen, dump_callback); SET_STATE(0); } \
            break; \
        case DCS: \
            if (accumulate_dcs(screen, codepoint, dump_callback)) { dispatch##_dcs(screen, dump_callback); SET_STATE(0); watch_for_pending; } \
            if (screen->parser_state == ESC) { dispatch##_esc_mode_char(screen, codepoint, dump_callback); } \
            break; \
        default: \
            dispatch##_normal_mode_char(screen, codepoint, dump_callback); \
            break; \
    } \
} \

extern uint32_t *latin1_charset;

#define decode_loop(dispatch, watch_for_pending) { \
    i = 0; \
    uint32_t prev = screen->utf8_state; \
    while(i < (size_t)len) { \
        uint8_t ch = buf[i++]; \
        if (screen->use_latin1) { \
            dispatch_unicode_char(latin1_charset[ch], dispatch, watch_for_pending); \
        } else { \
            switch (decode_utf8(&screen->utf8_state, &screen->utf8_codepoint, ch)) { \
                case UTF8_ACCEPT: \
                    dispatch_unicode_char(screen->utf8_codepoint, dispatch, watch_for_pending); \
                    break; \
                case UTF8_REJECT: \
                    screen->utf8_state = UTF8_ACCEPT; \
                    if (prev != UTF8_ACCEPT && i > 0) i--; \
                    break; \
            } \
            prev = screen->utf8_state; \
        } \
    }  \
}

static void
_parse_bytes(Screen *screen, const uint8_t *buf, Py_ssize_t len, PyObject DUMP_UNUSED *dump_callback) {
    unsigned int i;
    decode_loop(dispatch, ;);
FLUSH_DRAW;
}

static size_t
_parse_bytes_watching_for_pending(Screen *screen, const uint8_t *buf, Py_ssize_t len, PyObject DUMP_UNUSED *dump_callback) {
    unsigned int i;
    decode_loop(dispatch, if (screen->pending_mode.activated_at) goto end);
end:
FLUSH_DRAW;
    return i;
}

static void
write_pending_char(Screen *screen, uint32_t ch) {
    if (screen->pending_mode.capacity < screen->pending_mode.used + 8) {
        if (screen->pending_mode.capacity) {
            screen->pending_mode.capacity += screen->pending_mode.capacity >= READ_BUF_SZ ? PENDING_BUF_INCREMENT : screen->pending_mode.capacity;
        } else screen->pending_mode.capacity = PENDING_BUF_INCREMENT;
        screen->pending_mode.buf = realloc(screen->pending_mode.buf, screen->pending_mode.capacity);
        if (!screen->pending_mode.buf) fatal("Out of memory");
    }
    screen->pending_mode.used += encode_utf8(ch, (char*)screen->pending_mode.buf + screen->pending_mode.used);
}

static void
pending_normal_mode_char(Screen *screen, uint32_t ch, PyObject *dump_callback UNUSED) {
    switch(ch) {
        case ESC: case CSI: case OSC: case DCS: case APC: case PM:
            SET_STATE(ch); break;
        default:
            write_pending_char(screen, ch); break;
    }
}

static void
pending_esc_mode_char(Screen *screen, uint32_t ch, PyObject *dump_callback UNUSED) {
    if (screen->parser_buf_pos > 0) {
        write_pending_char(screen, ESC);
        write_pending_char(screen, screen->parser_buf[screen->parser_buf_pos - 1]);
        write_pending_char(screen, ch);
        SET_STATE(0);
        return;
    }
    switch (ch) {
        case ESC_DCS:
            SET_STATE(DCS); break;
        case ESC_OSC:
            SET_STATE(OSC); break;
        case ESC_CSI:
            SET_STATE(CSI); break;
        case ESC_APC:
            SET_STATE(APC); break;
        case ESC_PM:
            SET_STATE(PM); break;
        case '%':
        case '(':
        case ')':
        case '*':
        case '+':
        case '-':
        case '.':
        case '/':
        case ' ':
        case '#':
            screen->parser_buf[screen->parser_buf_pos++] = ch;
            break;
        default:
            write_pending_char(screen, ESC); write_pending_char(screen, ch);
            SET_STATE(0); break;
    }
}

#define pb(i) screen->parser_buf[i]
static void
pending_escape_code(Screen *screen, char_type start_ch, char_type end_ch) {
    write_pending_char(screen, start_ch);
    for (unsigned i = 0; i < screen->parser_buf_pos; i++) write_pending_char(screen, pb(i));
    write_pending_char(screen, end_ch);
}

static void pending_pm(Screen *screen, PyObject *dump_callback UNUSED) { pending_escape_code(screen, PM, ST); }
static void pending_apc(Screen *screen, PyObject *dump_callback UNUSED) { pending_escape_code(screen, APC, ST); }

static void
pending_osc(Screen *screen, PyObject *dump_callback UNUSED) {
    const bool extended = is_extended_osc(screen);
    pending_escape_code(screen, OSC, ST);
    if (extended) continue_osc_52(screen);
}


static void
pending_dcs(Screen *screen, PyObject *dump_callback DUMP_UNUSED) {
    if (screen->parser_buf_pos >= 3 && pb(0) == '=' && (pb(1) == '1' || pb(1) == '2') && pb(2) == 's') {
        screen->pending_mode.activated_at = pb(1) == '1' ? monotonic() : 0;
        if (pb(1) == '1') {
            REPORT_COMMAND(screen_start_pending_mode);
            screen->pending_mode.activated_at = monotonic();
        } else {
            screen->pending_mode.stop_escape_code_type = DCS;
            screen->pending_mode.activated_at = 0;
        }
    } else pending_escape_code(screen, DCS, ST);
}

static void
pending_csi(Screen *screen, PyObject *dump_callback DUMP_UNUSED) {
    if (screen->parser_buf_pos == 5 && pb(0) == '?' && pb(1) == '2' && pb(2) == '0' && pb(3) == '2' && pb(4) == '6' && (pb(5) == 'h' || pb(5) == 'l')) {
        if (pb(5) == 'h') {
            REPORT_COMMAND(screen_set_mode, 2026, 1);
            screen->pending_mode.activated_at = monotonic();
        } else {
            screen->pending_mode.activated_at = 0;
            screen->pending_mode.stop_escape_code_type = CSI;
        }
    } else pending_escape_code(screen, CSI, pb(screen->parser_buf_pos));
}

#undef pb

static size_t
queue_pending_bytes(Screen *screen, const uint8_t *buf, size_t len, PyObject *dump_callback DUMP_UNUSED) {
    unsigned int i;
    decode_loop(pending, if (!screen->pending_mode.activated_at) goto end);
end:
    return i;
}

static void
dump_partial_escape_code_to_pending(Screen *screen) {
    if (screen->parser_buf_pos) {
        write_pending_char(screen, screen->parser_state);
        for (unsigned i = 0; i < screen->parser_buf_pos; i++) write_pending_char(screen, screen->parser_buf[i]);
    }
}

static void
do_parse_bytes(Screen *screen, const uint8_t *read_buf, const size_t read_buf_sz, monotonic_t now, PyObject *dump_callback DUMP_UNUSED) {
    enum STATE {START, PARSE_PENDING, PARSE_READ_BUF, QUEUE_PENDING};
    enum STATE state = START;
    size_t read_buf_pos = 0;
    unsigned int parser_state_at_start_of_pending = 0;

    do {
        switch(state) {
            case START:
                if (screen->pending_mode.activated_at) {
                    if (screen->pending_mode.activated_at + screen->pending_mode.wait_time < now) {
                        dump_partial_escape_code_to_pending(screen);
                        screen->pending_mode.activated_at = 0;
                        state = START;
                    } else state = QUEUE_PENDING;
                } else {
                    state = screen->pending_mode.used ? PARSE_PENDING : PARSE_READ_BUF;
                }
                break;

            case PARSE_PENDING:
                screen->parser_state = parser_state_at_start_of_pending;
                parser_state_at_start_of_pending = 0;
                _parse_bytes(screen, screen->pending_mode.buf, screen->pending_mode.used, dump_callback);
                screen->pending_mode.used = 0;
                screen->pending_mode.activated_at = 0;  // ignore any pending starts in the pending bytes
                if (screen->pending_mode.capacity > READ_BUF_SZ + PENDING_BUF_INCREMENT) {
                    screen->pending_mode.capacity = READ_BUF_SZ;
                    screen->pending_mode.buf = realloc(screen->pending_mode.buf, screen->pending_mode.capacity);
                    if (!screen->pending_mode.buf) fatal("Out of memory");
                }
                if (screen->pending_mode.stop_escape_code_type) {
                    if (screen->pending_mode.stop_escape_code_type == DCS) { REPORT_COMMAND(screen_stop_pending_mode); }
                    else if (screen->pending_mode.stop_escape_code_type == CSI) { REPORT_COMMAND(screen_reset_mode, 2026, 1); }
                    screen->pending_mode.stop_escape_code_type = 0;
                }
                state = START;
                break;

            case PARSE_READ_BUF:
                screen->pending_mode.activated_at = 0;
                read_buf_pos += _parse_bytes_watching_for_pending(screen, read_buf + read_buf_pos, read_buf_sz - read_buf_pos, dump_callback);
                state = START;
                break;

            case QUEUE_PENDING: {
                screen->pending_mode.stop_escape_code_type = 0;
                if (screen->pending_mode.used >= READ_BUF_SZ) {
                    dump_partial_escape_code_to_pending(screen);
                    screen->pending_mode.activated_at = 0;
                    state = START;
                    break;
                }
                if (!screen->pending_mode.used) parser_state_at_start_of_pending = screen->parser_state;
                read_buf_pos += queue_pending_bytes(screen, read_buf + read_buf_pos, read_buf_sz - read_buf_pos, dump_callback);
                state = START;
            }   break;
        }
    } while(read_buf_pos < read_buf_sz || (!screen->pending_mode.activated_at && screen->pending_mode.used));

}

// }}}

// Entry points {{{
#ifdef DUMP_COMMANDS
#define FNAME(x) x##_dump
#else
#define FNAME(x) x
#endif

PyObject*
FNAME(parse_bytes)(PyObject UNUSED *self, PyObject *args) {
    PyObject *dump_callback = NULL;
    Py_buffer pybuf;
    Screen *screen;
#ifdef DUMP_COMMANDS
    if (!PyArg_ParseTuple(args, "OO!y*", &dump_callback, &Screen_Type, &screen, &pybuf)) return NULL;
#else
    if (!PyArg_ParseTuple(args, "O!y*", &Screen_Type, &screen, &pybuf)) return NULL;
#endif
    do_parse_bytes(screen, pybuf.buf, pybuf.len, monotonic(), dump_callback);
    Py_RETURN_NONE;
}


void
FNAME(parse_worker)(Screen *screen, PyObject *dump_callback, monotonic_t now) {
#ifdef DUMP_COMMANDS
    if (screen->read_buf_sz) {
        Py_XDECREF(PyObject_CallFunction(dump_callback, "sy#", "bytes", screen->read_buf, screen->read_buf_sz)); PyErr_Clear();
    }
#endif
    do_parse_bytes(screen, screen->read_buf, screen->read_buf_sz, now, dump_callback);
    screen->read_buf_sz = 0;
}
#undef FNAME
// }}}
