/*
 * BRLTTY - A background process providing access to the console screen (when in
 *          text mode) for a blind person using a refreshable braille display.
 *
 * Copyright (C) 1995-2025 by The BRLTTY Developers.
 *
 * BRLTTY comes with ABSOLUTELY NO WARRANTY.
 *
 * This is free software, placed under the terms of the
 * GNU Lesser General Public License, as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any
 * later version. Please see the file LICENSE-LGPL for details.
 *
 * Web Page: http://brltty.app/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

/* unimplemented output actions
 * enacs=\E(B\E)0 - enable alternate charset mode
 * hts=\EH - set tab
 * kmous=\E[M - mouse event
 * tbc=\E[3g - clear all tabs
 * u6=\E[%i%d;%dR - user string 6
 * u7=\E[6n - user string 7
 * u8=\E[?1;2c - user string 8
 * u9=\E[c - user string 9
 */

#include "prologue.h"

#include <string.h>

#include "log.h"
#include "strfmt.h"
#include "pty_terminal.h"
#include "pty_screen.h"
#include "scr_types.h"
#include "color.h"
#include "get_term.h"
#include "ascii.h"

static unsigned char terminalLogLevel = LOG_DEBUG;
static unsigned char logInput = 0;
static unsigned char logOutput = 0;
static unsigned char logSequences = 0;
static unsigned char logUnexpected = 0;

void
ptySetTerminalLogLevel (unsigned char level) {
  terminalLogLevel = level;
  ptySetScreenLogLevel(level);
}

void
ptySetLogTerminalInput (int yes) {
  logInput = yes;
}

void
ptySetLogTerminalOutput (int yes) {
  logOutput = yes;
}

void
ptySetLogTerminalSequences (int yes) {
  logSequences = yes;
}

void
ptySetLogUnexpectedTerminalIO (int yes) {
  logUnexpected = yes;
}

static int
testTerminalType (const char *type) {
  int status;
  if (setupterm(type, STDOUT_FILENO, &status) == OK) return 1;

  switch (status) {
    case 1:
      logMessage(LOG_WARNING, "hardcopy terminal: %s", type);
      break;

    case 0:
      logMessage(LOG_WARNING, "unrecognized terminal type: %s", type);
      break;

    case -1:
      logMessage(LOG_WARNING, "terminfo database not installed");
      break;

    default:
      logMessage(LOG_WARNING, "unexpected setupterm error status: %d", status);
      break;
  }

  return 0;
}

const char *
ptyGetTerminalType (void) {
  static const char *terminalType = NULL;

  if (!terminalType) {
    static const char *const terminalTypes[] = {
      "tmux-256color",
      "screen-256color",
      "tmux",
      "screen",
      NULL
    };

    for (const char *const *type=terminalTypes; *type; type+=1) {
      if (testTerminalType(*type)) {
        terminalType = *type;
        break;
      }
    }

    if (terminalType) {
      logMessage(LOG_INFO, "selected terminal type: %s", terminalType);
    } else {
      logMessage(LOG_ERR, "can't use any eligible terminal type");
    }
  }

  return terminalType;
}

static unsigned char insertMode = 0;
static unsigned char alternateCharsetMode = 0;
static unsigned char keypadTransmitMode = 0;
static unsigned char bracketedPasteMode = 0;
static unsigned char absoluteCursorAddressingMode = 0;

int
ptyBeginTerminal (PtyObject *pty, int driverDirectives) {
  insertMode = 0;
  alternateCharsetMode = 0;
  keypadTransmitMode = 0;
  bracketedPasteMode = 0;
  absoluteCursorAddressingMode = 0;

  return ptyBeginScreen(pty, driverDirectives);
}

void
ptyEndTerminal (void) {
  ptyEndScreen();
}

void
ptyResizeTerminal (unsigned int height, unsigned int width) {
  ptyResizeScreen(height, width);
}

static void
soundAlert (void) {
  beep();
}

static void
showAlert (void) {
  flash();
}

int
ptyProcessTerminalInput (PtyObject *pty) {
  int character = getch();

  if (logInput) {
    const char *name = keyname(character);
    if (!name) name = "unknown";
    logMessage(terminalLogLevel, "input: 0X%02X (%s)", character, name);
  }

  if (character > UINT8_MAX) {
    ScreenKey key = 0;

    #define KEY(from, to) case KEY_##from: key = SCR_KEY_##to; break;
    switch (character) {
      KEY(ENTER    , ENTER)
      KEY(BACKSPACE, BACKSPACE)

      KEY(LEFT     , CURSOR_LEFT)
      KEY(RIGHT    , CURSOR_RIGHT)
      KEY(UP       , CURSOR_UP)
      KEY(DOWN     , CURSOR_DOWN)

      KEY(PPAGE    , PAGE_UP)
      KEY(NPAGE    , PAGE_DOWN)
      KEY(HOME     , HOME)
      KEY(END      , END)
      KEY(IC       , INSERT)
      KEY(DC       , DELETE)

      KEY(F( 1)    , F1)
      KEY(F( 2)    , F2)
      KEY(F( 3)    , F3)
      KEY(F( 4)    , F4)
      KEY(F( 5)    , F5)
      KEY(F( 6)    , F6)
      KEY(F( 7)    , F7)
      KEY(F( 8)    , F8)
      KEY(F( 9)    , F9)
      KEY(F(10)    , F10)
      KEY(F(11)    , F11)
      KEY(F(12)    , F12)
      KEY(F(13)    , F13)
      KEY(F(14)    , F14)
      KEY(F(15)    , F15)
      KEY(F(16)    , F16)
      KEY(F(17)    , F17)
      KEY(F(18)    , F18)
      KEY(F(19)    , F19)
      KEY(F(20)    , F20)
      KEY(F(21)    , F21)
      KEY(F(22)    , F22)
      KEY(F(23)    , F23)
      KEY(F(24)    , F24)
      KEY(F(25)    , F25)
      KEY(F(26)    , F26)
      KEY(F(27)    , F27)
      KEY(F(28)    , F28)
      KEY(F(29)    , F29)
      KEY(F(30)    , F30)
      KEY(F(31)    , F31)
      KEY(F(32)    , F32)
      KEY(F(33)    , F33)
      KEY(F(34)    , F34)
      KEY(F(35)    , F35)
      KEY(F(36)    , F36)
      KEY(F(37)    , F37)
      KEY(F(38)    , F38)
      KEY(F(39)    , F39)
      KEY(F(40)    , F40)
      KEY(F(41)    , F41)
      KEY(F(42)    , F42)
      KEY(F(43)    , F43)
      KEY(F(44)    , F44)
      KEY(F(45)    , F45)
      KEY(F(46)    , F46)
      KEY(F(47)    , F47)
      KEY(F(48)    , F48)
      KEY(F(49)    , F49)
      KEY(F(50)    , F50)
      KEY(F(51)    , F51)
      KEY(F(52)    , F52)
      KEY(F(53)    , F53)
      KEY(F(54)    , F54)
      KEY(F(55)    , F55)
      KEY(F(56)    , F56)
      KEY(F(57)    , F57)
      KEY(F(58)    , F58)
      KEY(F(59)    , F59)
      KEY(F(60)    , F60)
      KEY(F(61)    , F61)
      KEY(F(62)    , F62)
      KEY(F(63)    , F63)
    }
    #undef KEY

    if (key) {
      if (!ptyWriteInputCharacter(pty, key, keypadTransmitMode)) {
        return 0;
      }
    } else if (logUnexpected) {
      const char *name = keyname(character);
      if (!name) name = "unknown";
      logMessage(terminalLogLevel, "unexpected input: 0X%02X (%s)", character, name);
    }

    return 1;
  }

  char byte = character;
  return ptyWriteInputData(pty, &byte,1);
}

static unsigned char outputByteBuffer[0X40];
static unsigned char outputByteCount;

static void
logUnexpectedSequence (void) {
  if (logUnexpected) {
    logBytes(
      terminalLogLevel, "unexpected sequence",
      outputByteBuffer, outputByteCount
    );
  }
}

typedef enum {
  OPS_BASIC,
  OPS_ESCAPE,
  OPS_BRACKET,
  OPS_NUMBER,
  OPS_DIGIT,
  OPS_ACTION,
} OutputParserState;

static OutputParserState outputParserState;
static unsigned char outputParserQuestionMark;

static unsigned int outputParserNumber;
static unsigned int outputParserNumberArray[9];
static unsigned char outputParserNumberCount;

static void
addOutputParserNumber (unsigned int number) {
  if (outputParserNumberCount < ARRAY_COUNT(outputParserNumberArray)) {
    outputParserNumberArray[outputParserNumberCount++] = number;
  }
}

static unsigned int
getOutputActionCount (void) {
  if (outputParserNumberCount == 0) return 1;
  return outputParserNumberArray[0];
}

static void
logOutputAction (const char *name, const char *description) {
  if (logSequences) {
    char prefix[0X100];
    STR_BEGIN(prefix, sizeof(prefix));
    STR_PRINTF("action: %s", name);

    for (unsigned int i=0; i<outputParserNumberCount; i+=1) {
      STR_PRINTF(" %u", outputParserNumberArray[i]);
    }

    if (description && *description) STR_PRINTF(" (%s)", description);
    STR_END;
    logBytes(terminalLogLevel, "%s", outputByteBuffer, outputByteCount, prefix);
  }
}

typedef enum {
  OBP_DONE,
  OBP_CONTINUE,
  OBP_REPROCESS,
  OBP_UNEXPECTED,
} OutputByteParserResult;

typedef OutputByteParserResult OutputByteParser (unsigned char byte);

static OutputByteParserResult
parseOutputByte_BASIC (unsigned char byte) {
  outputParserQuestionMark = 0;
  outputParserNumberCount = 0;

  switch (byte) {
    case ASCII_ESC:
      outputParserState = OPS_ESCAPE;
      return OBP_CONTINUE;

    case ASCII_BEL:
      logOutputAction("bel", "audible alert");
      soundAlert();
      return OBP_DONE;

    case ASCII_BS:
      logOutputAction("cub1", "cursor left 1");
      ptyMoveCursorLeft(1);
      return OBP_DONE;

    case ASCII_HT:
      logOutputAction("ht", "tab forward");
      ptyTabForward();
      return OBP_DONE;

    case ASCII_LF:
      if (ptyAmWithinScrollRegion()) {
        logOutputAction("ind", "move down 1");
        ptyMoveDown1();
      } else {
        logOutputAction("cud1", "cursor down 1");
        ptyMoveCursorDown(1);
      }
      return OBP_DONE;

    case ASCII_CR:
      logOutputAction("cr", "carriage return");
      ptySetCursorColumn(0);
      return OBP_DONE;

    case ASCII_SO:
      logOutputAction("smacs", "alternate charset on");
      alternateCharsetMode = 1;
      return OBP_DONE;

    case ASCII_SI:
      logOutputAction("rmacs", "alternate charset off");
      alternateCharsetMode = 0;
      return OBP_DONE;

    default: {
      if (logOutput) {
        logMessage(terminalLogLevel, "output: 0X%02X", byte);
      }

      if (insertMode) ptyInsertCharacters(1);
      ptyAddCharacter(byte);
      return OBP_DONE;
    }
  }
}

static OutputByteParserResult
parseOutputByte_ESCAPE (unsigned char byte) {
  switch (byte) {
    case '[':
      outputParserState = OPS_BRACKET;
      return OBP_CONTINUE;

    case '=':
      logOutputAction("smkx", "keypad transmit on");
      keypadTransmitMode = 1;
      return OBP_DONE;

    case '>':
      logOutputAction("rmkx", "keypad transmit off");
      keypadTransmitMode = 0;
      return OBP_DONE;

    case 'E':
      logOutputAction("nel", "new line");
      ptySetCursorColumn(0);
      ptyMoveDown1();
      return OBP_DONE;

    case 'M':
      if (ptyAmWithinScrollRegion()) {
        logOutputAction("ri", "move up 1");
        ptyMoveUp1();
      } else {
        logOutputAction("cuu1", "cursor up 1");
        ptyMoveCursorUp(1);
      }
      return OBP_DONE;

    case 'c':
      logOutputAction("clear", "clear screen");
      ptySetCursorPosition(0, 0);
      ptyClearToEndOfDisplay();
      return OBP_DONE;

    case 'g':
      logOutputAction("flash", "visual alert");
      showAlert();
      return OBP_DONE;

    case '7':
      logOutputAction("sc", "save cursor position");
      ptySaveCursorPosition();
      return OBP_DONE;

    case '8':
      logOutputAction("rc", "restore cursor position");
      ptyRestoreCursorPosition();
      return OBP_DONE;
  }

  return OBP_UNEXPECTED;
}

static OutputByteParserResult
parseOutputByte_BRACKET (unsigned char byte) {
  outputParserState = OPS_NUMBER;
  if (outputParserQuestionMark) return OBP_REPROCESS;
  if (byte != '?') return OBP_REPROCESS;

  outputParserQuestionMark = 1;
  outputParserState = OPS_BRACKET;
  return OBP_CONTINUE;
}

static OutputByteParserResult
parseOutputByte_NUMBER (unsigned char byte) {
  if (iswdigit(byte)) {
    outputParserNumber = 0;
    outputParserState = OPS_DIGIT;
  } else {
    outputParserState = OPS_ACTION;
  }

  return OBP_REPROCESS;
}

static OutputByteParserResult
parseOutputByte_DIGIT (unsigned char byte) {
  if (iswdigit(byte)) {
    outputParserNumber *= 10;
    outputParserNumber += byte - '0';
    return OBP_CONTINUE;
  }

  addOutputParserNumber(outputParserNumber);
  outputParserNumber = 0;
  if (byte == ';') return OBP_CONTINUE;

  outputParserState = OPS_ACTION;
  return OBP_REPROCESS;
}

static OutputByteParserResult
performBracketAction_h (unsigned char byte) {
  if (outputParserNumberCount == 1) {
    switch (outputParserNumberArray[0]) {
      case 4:
        logOutputAction("smir", "insert on");
        insertMode = 1;
        return OBP_DONE;

      case 34:
        logOutputAction("cnorm", "cursor normal visibility");
        ptySetCursorVisibility(1);
        return OBP_DONE;
    }
  }

  return OBP_UNEXPECTED;
}

static OutputByteParserResult
performBracketAction_l (unsigned char byte) {
  if (outputParserNumberCount == 1) {
    switch (outputParserNumberArray[0]) {
      case 4:
        logOutputAction("rmir", "insert off");
        insertMode = 0;
        return OBP_DONE;

      case 34:
        logOutputAction("cvvis", "cursor very visile");
        ptySetCursorVisibility(2);
        return OBP_DONE;
    }
  }

  return OBP_UNEXPECTED;
}

static OutputByteParserResult
performBracketAction_m (unsigned char byte) {
  if (outputParserNumberCount == 0) addOutputParserNumber(0);

  for (unsigned int index=0; index<outputParserNumberCount; index+=1) {
    unsigned int number = outputParserNumberArray[index];
    unsigned int count = outputParserNumberCount - index;

    switch (number / 10) {
      {
        int bright;

        case 3:
          bright = 0;
          goto DO_FOREGROUND;

        case 4:
          bright = 0;
          goto DO_BACKGROUND;

        case 9:
          bright = 1;
          goto DO_FOREGROUND;

        case 10:
          bright = 1;
          goto DO_BACKGROUND;

        const char *name;
        const char *description;
        void (*setColor) (int color);
        int color;

      DO_FOREGROUND:
        name = "setaf";
        description = "foreground color";
        setColor = ptySetForegroundColor;
        goto DO_COLOR;

      DO_BACKGROUND:
        name = "setab";
        description = "background color";
        setColor = ptySetBackgroundColor;
        goto DO_COLOR;

      DO_COLOR:
        color = number % 10;

        if (color == 8) {
          if (!bright) {
            if (count >= 2) {
              switch (outputParserNumberArray[++index]) {
                case 2: {
                  if (count >= 5) {
                    unsigned int red = outputParserNumberArray[++index];
                    unsigned int green = outputParserNumberArray[++index];
                    unsigned int blue = outputParserNumberArray[++index];

                    color = rgbToVga(red, green, blue, 0);
                    goto SET_COLOR;
                  }

                  break;
                }

                case 5: {
                  if (count >= 3) {
                    color = outputParserNumberArray[++index];
                    if (color <= 0XF) goto SET_COLOR;
                  }

                  break;
                }
              }
            }
          }

          return OBP_UNEXPECTED;
        }

        if (color == 9) {
          color = -1;
        } else if (bright) {
          color |= 0X8;
        }

      SET_COLOR:
        logOutputAction(name, description);
        setColor(color);
        continue;
      }
    }

    switch (number) {
      case 0:
        logOutputAction("sgr0", "all attributes off");
        ptySetAttributes(0);
        continue;

      case 1:
        logOutputAction("bold", "bold on");
        ptyAddAttributes(A_BOLD);
        continue;

      case 2:
        logOutputAction("dim", "dim on");
        ptyAddAttributes(A_DIM);
        continue;

      case 3:
        logOutputAction("sitm", "italic on");
        ptyAddAttributes(A_ITALIC);
        continue;

      case 4:
        logOutputAction("smul", "underline on");
        ptyAddAttributes(A_UNDERLINE);
        continue;

      case 5:
        logOutputAction("smbl", "slow blink on");
        ptyAddAttributes(A_BLINK);
        continue;

      case 6:
        logOutputAction("smfbl", "fast blink on");
        ptyAddAttributes(A_BLINK);
        continue;

      case 7:
        logOutputAction("rev", "reverse video on");
        ptyAddAttributes(A_REVERSE);
        continue;

      case 8:
        logOutputAction("smxon", "conceal on");
        continue;

      case 9:
        logOutputAction("smstrike", "strikethrough on");
        continue;

      case 22:
        logOutputAction("normal", "bold/dim off");
        ptyRemoveAttributes(A_BOLD | A_DIM);
        continue;

      case 23:
        logOutputAction("ritm", "italic off");
        ptyRemoveAttributes(A_ITALIC);
        continue;

      case 24:
        logOutputAction("rmul", "underline off");
        ptyRemoveAttributes(A_UNDERLINE);
        continue;

      case 25:
        logOutputAction("rmbl", "slow/fast blink off");
        ptyRemoveAttributes(A_BLINK);
        continue;

      case 27:
        logOutputAction("unrev", "reverse video off");
        ptyRemoveAttributes(A_REVERSE);
        continue;

      case 28:
        logOutputAction("rmxon", "conceal off");
        continue;

      case 29:
        logOutputAction("rmstrike", "strikethrough off");
        continue;
    }

    return OBP_UNEXPECTED;
  }

  return OBP_DONE;
}

static OutputByteParserResult
performBracketAction (unsigned char byte) {
  switch (byte) {
    case 'A':
      logOutputAction("cuu", "cursor up");
      ptyMoveCursorUp(getOutputActionCount());
      return OBP_DONE;

    case 'B':
      logOutputAction("cud", "cursor down");
      ptyMoveCursorDown(getOutputActionCount());
      return OBP_DONE;

    case 'C':
      logOutputAction("cuf", "cursor right");
      ptyMoveCursorRight(getOutputActionCount());
      return OBP_DONE;

    case 'D':
      logOutputAction("cub", "cursor left");
      ptyMoveCursorLeft(getOutputActionCount());
      return OBP_DONE;

    case 'G': {
      if (outputParserNumberCount != 1) return OBP_UNEXPECTED   ;
      unsigned int *column = &outputParserNumberArray[0];
      if (!(*column)--) return OBP_UNEXPECTED;

      logOutputAction("hpa", "set cursor column");
      ptySetCursorColumn(*column);
      return OBP_DONE;
    }

    {
      const char *action;
      int allowZero;

    case 'f':
      action = "hvp";
      allowZero = 1;
      goto MOVE_CURSOR;

    case 'H':
      action = "cup";
      allowZero = 0;
      goto MOVE_CURSOR;

    MOVE_CURSOR:
      if (outputParserNumberCount == 0) {
        addOutputParserNumber(1);
        addOutputParserNumber(1);
      } else if (outputParserNumberCount != 2) {
        return OBP_UNEXPECTED;
      }

      unsigned int *row = &outputParserNumberArray[0];
      unsigned int *column = &outputParserNumberArray[1];

      if (allowZero) {
        if (*row) *row -= 1;
        if (*column) *column -= 1;
      } else {
        if (!(*row)--) return OBP_UNEXPECTED;
        if (!(*column)--) return OBP_UNEXPECTED;
      }

      logOutputAction(action, "set cursor position");
      ptySetCursorPosition(*row, *column);
      return OBP_DONE;
    }

    case 'J':
      if (outputParserNumberCount != 0) return OBP_UNEXPECTED;
      logOutputAction("ed", "clear to end of display");
      ptyClearToEndOfDisplay();
      return OBP_DONE;

    case 'K': {
      if (outputParserNumberCount == 0) addOutputParserNumber(0);
      if (outputParserNumberCount != 1) return OBP_UNEXPECTED;

      switch (outputParserNumberArray[0]) {
        case 0:
          logOutputAction("el", "clear to end of line");
          ptyClearToEndOfLine();
          return OBP_DONE;

        case 1:
          logOutputAction("el1", "clear to beginning of line");
          ptyClearToBeginningOfLine();
          return OBP_DONE;
      }

      break;
    }

    case 'L':
      logOutputAction("il", "insert lines");
      ptyInsertLines(getOutputActionCount());
      return OBP_DONE;

    case 'M':
      logOutputAction("dl", "delete lines");
      ptyDeleteLines(getOutputActionCount());
      return OBP_DONE;

    case 'P':
      logOutputAction("dch", "delete characters");
      ptyDeleteCharacters(getOutputActionCount());
      return OBP_DONE;

    case 'S':
      logOutputAction("indn", "scroll forward");
      ptyScrollUp(getOutputActionCount());
      return OBP_DONE;

    case 'T':
      logOutputAction("rin", "scroll backward");
      ptyScrollDown(getOutputActionCount());
      return OBP_DONE;

    case 'Z':
      logOutputAction("cbt", "tab backward");
      ptyTabBackward();
      return OBP_DONE;

    case 'd': {
      if (outputParserNumberCount != 1) return OBP_UNEXPECTED   ;
      unsigned int *row = &outputParserNumberArray[0];
      if (!(*row)--) return OBP_UNEXPECTED;

      logOutputAction("vpa", "set cursor row");
      ptySetCursorRow(*row);
      return OBP_DONE;
    }

    case 'h':
      return performBracketAction_h(byte);

    case 'l':
      return performBracketAction_l(byte);

    case 'm':
      return performBracketAction_m(byte);

    case 'r': {
      if (outputParserNumberCount != 2) return OBP_UNEXPECTED;

      unsigned int *top = &outputParserNumberArray[0];
      unsigned int *bottom = &outputParserNumberArray[1];

      if (!(*top)--) return OBP_UNEXPECTED;
      if (!(*bottom)--) return OBP_UNEXPECTED;

      logOutputAction("csr", "set scroll region");
      ptySetScrollRegion(*top, *bottom);
      return OBP_DONE;
    }

    case '@':
      logOutputAction("ic", "insert characters");
      ptyInsertCharacters(getOutputActionCount());
      return OBP_DONE;
  }

  return OBP_UNEXPECTED;
}

static OutputByteParserResult
performQuestionMarkAction_h (unsigned char byte) {
  if (outputParserNumberCount == 1) {
    switch (outputParserNumberArray[0]) {
      case 1:
        logOutputAction("smkx", "keypad transmit on");
        keypadTransmitMode = 1;
        return OBP_DONE;

      case 25:
        logOutputAction("cnorm", "cursor normal visibility");
        ptySetCursorVisibility(1);
        return OBP_DONE;

      case 1049:
        logOutputAction("smcup", "absolute cursor addressing on");
        absoluteCursorAddressingMode = 1;
        return OBP_DONE;

      case 2004:
        logOutputAction("smbp", "bracketed paste on");
        bracketedPasteMode = 1;
        return OBP_DONE;
    }
  }

  return OBP_UNEXPECTED;
}

static OutputByteParserResult
performQuestionMarkAction_l (unsigned char byte) {
  if (outputParserNumberCount == 1) {
    switch (outputParserNumberArray[0]) {
      case 1:
        logOutputAction("rmkx", "keypad transmit off");
        keypadTransmitMode = 0;
        return OBP_DONE;

      case 25:
        logOutputAction("civis", "cursor invisible");
        ptySetCursorVisibility(0);
        return OBP_DONE;

      case 1049:
        logOutputAction("rmcup", "absolute cursor addressing off");
        absoluteCursorAddressingMode = 0;
        return OBP_DONE;

      case 2004:
        logOutputAction("rmbp", "bracketed paste off");
        bracketedPasteMode = 0;
        return OBP_DONE;
    }
  }

  return OBP_UNEXPECTED;
}

static OutputByteParserResult
performQuestionMarkAction (unsigned char byte) {
  switch (byte) {
    case 'h':
      return performQuestionMarkAction_h(byte);

    case 'l':
      return performQuestionMarkAction_l(byte);
  }

  return OBP_UNEXPECTED;
}

static OutputByteParserResult
parseOutputByte_ACTION (unsigned char byte) {
  if (outputParserQuestionMark) {
    return performQuestionMarkAction(byte);
  } else {
    return performBracketAction(byte);
  }
}

typedef struct {
  OutputByteParser *parseOutputByte;
  const char *name;
} OutputParserStateEntry;

#define OPS(state) \
[OPS_##state] = { \
  .parseOutputByte = parseOutputByte_##state, \
  .name = #state, \
}

static const OutputParserStateEntry outputParserStateTable[] = {
  OPS(BASIC),
  OPS(ESCAPE),
  OPS(BRACKET),
  OPS(NUMBER),
  OPS(DIGIT),
  OPS(ACTION),
};
#undef OPS

static int
parseOutputByte (unsigned char byte) {
  if (outputParserState == OPS_BASIC) {
    outputByteCount = 0;
  }

  if (outputByteCount < ARRAY_COUNT(outputByteBuffer)) {
    outputByteBuffer[outputByteCount++] = byte;
  }

  while (1) {
    OutputByteParserResult result = outputParserStateTable[outputParserState].parseOutputByte(byte);

    switch (result) {
      case OBP_REPROCESS:
        continue;

      case OBP_UNEXPECTED:
        logUnexpectedSequence();
        /* fall through */

      case OBP_DONE:
        outputParserState = OPS_BASIC;
        return 1;

      case OBP_CONTINUE:
        return 0;
    }
  }
}

int
ptyProcessTerminalOutput (const unsigned char *bytes, size_t count) {
  int wantRefresh = 0;

  const unsigned char *byte = bytes;
  const unsigned char *end = byte + count;

  while (byte < end) {
    wantRefresh = parseOutputByte(*byte++);
  }

  if (wantRefresh) {
    ptyRefreshScreen();
  }

  return 1;
}
