/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-â
âvi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8                                :viâ
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¡
â Copyright 2020 Justine Alexandra Roberts Tunney                              â
â                                                                              â
â Permission to use, copy, modify, and/or distribute this software for         â
â any purpose with or without fee is hereby granted, provided that the         â
â above copyright notice and this permission notice appear in all copies.      â
â                                                                              â
â THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL                â
â WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED                â
â WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE             â
â AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL         â
â DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR        â
â PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER               â
â TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR             â
â PERFORMANCE OF THIS SOFTWARE.                                                â
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ*/
#include "libc/fmt/bing.internal.h"
#include "libc/fmt/itoa.h"
#include "libc/intrin/bits.h"
#include "libc/intrin/directmap.internal.h"
#include "libc/intrin/safemacros.internal.h"
#include "libc/runtime/pc.internal.h"
#include "libc/str/str.h"
#include "libc/str/thompike.h"
#include "libc/str/unicode.h"
#include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/prot.h"
#include "libc/sysv/errfuns.h"
#include "libc/vga/vga.internal.h"

#ifdef __x86_64__

/**
 * @fileoverview ECMA-48 / VT100 video terminal implementation for bare
 * metal VGA.
 *
 * The code is mainly a pared-down version of the implementation of a
 * pseudo-tty in tool/build/lib/pty.c.
 *
 * @see tool/build/lib/pty.c
 */

#define DEFAULT_FG \
  ((TtyCanvasColor){.bgr.r = 0xaa, .bgr.g = 0xaa, .bgr.b = 0xaa, .bgr.x = 0xff})
#define DEFAULT_BG \
  ((TtyCanvasColor){.bgr.r = 0, .bgr.g = 0, .bgr.b = 0, .bgr.x = 0xff})
#define CRTPORT 0x3d4

static void SetYn(struct Tty *tty, unsigned short yn) {
  tty->yn = yn;
}

static void SetXn(struct Tty *tty, unsigned short xn) {
  tty->xn = xn;
}

static void SetYp(struct Tty *tty, unsigned short yp) {
  tty->yp = yp;
}

static void SetXp(struct Tty *tty, unsigned short xp) {
  tty->xp = xp;
}

static void SetXsFb(struct Tty *tty, unsigned short xsfb) {
  tty->xsfb = xsfb;
}

static bool SetWcs(struct Tty *tty, unsigned init_flags) {
#ifdef VGA_USE_WCS
  struct DirectMap dm;
  if (!(init_flags & kTtyAllocWcs)) return false;
  dm = sys_mmap_metal(NULL, Yn(tty) * Xn(tty) * sizeof(wchar_t),
                      PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1,
                      0);
  if (dm.addr == (void *)-1) {
    tty->wcs = NULL;
    return false;
  }
  tty->wcs = dm.addr;
  return true;
#else
  return false;
#endif
}

static size_t Yn(struct Tty *tty) {
  return tty->yn;
}

static size_t Xn(struct Tty *tty) {
  return tty->xn;
}

static wchar_t *Wcs(struct Tty *tty) {
#ifdef VGA_USE_WCS
  return tty->wcs;
#else
  return NULL;
#endif
}

static void TtyNoopUpdate(struct Tty *);
static void TtyTextDrawChar(struct Tty *, size_t, size_t, wchar_t);
static void TtyTextEraseLineCells(struct Tty *, size_t, size_t, size_t);
static void TtyTextMoveLineCells(struct Tty *, size_t, size_t, size_t, size_t,
                                 size_t);

static void TtySetType(struct Tty *tty, unsigned char type,
                       unsigned init_flags) {
  tty->type = type;
  if ((init_flags & kTtyKlog) == 0) {
    switch (type) {
      case PC_VIDEO_BGR565:
        tty->update = _TtyBgr565Update;
        goto graphics_mode;
      case PC_VIDEO_BGR555:
        tty->update = _TtyBgr555Update;
        goto graphics_mode;
      case PC_VIDEO_BGRX8888:
        tty->update = _TtyBgrxUpdate;
        goto graphics_mode;
      case PC_VIDEO_RGBX8888:
        tty->update = _TtyRgbxUpdate;
      graphics_mode:
        tty->drawchar = _TtyGraphDrawChar;
        tty->eraselinecells = _TtyGraphEraseLineCells;
        tty->movelinecells = _TtyGraphMoveLineCells;
        break;
      default:
        tty->update = TtyNoopUpdate;
        tty->drawchar = TtyTextDrawChar;
        tty->eraselinecells = TtyTextEraseLineCells;
        tty->movelinecells = TtyTextMoveLineCells;
    }
  } else {
    switch (type) {
      case PC_VIDEO_BGR565:
      case PC_VIDEO_BGR555:
        tty->update = _TtyKlog16Update;
        tty->drawchar = _TtyKlog16DrawChar;
        tty->eraselinecells = _TtyKlog16EraseLineCells;
        tty->movelinecells = _TtyKlog16MoveLineCells;
        break;
      case PC_VIDEO_BGRX8888:
      case PC_VIDEO_RGBX8888:
        tty->update = _TtyKlog32Update;
        tty->drawchar = _TtyKlog32DrawChar;
        tty->eraselinecells = _TtyKlog32EraseLineCells;
        tty->movelinecells = _TtyKlog32MoveLineCells;
        break;
      default:
        tty->update = TtyNoopUpdate;
        tty->drawchar = TtyTextDrawChar;
        tty->eraselinecells = TtyTextEraseLineCells;
        tty->movelinecells = TtyTextMoveLineCells;
    }
  }
}

void _StartTty(struct Tty *tty, unsigned char type, unsigned short yp,
               unsigned short xp, unsigned short xsfb, unsigned short starty,
               unsigned short startx, unsigned char yc, unsigned char xc,
               void *fb, unsigned init_flags) {
  unsigned short yn, xn, xs = xp * sizeof(TtyCanvasColor);
  struct DirectMap dm;
  memset(tty, 0, sizeof(struct Tty));
  SetYp(tty, yp);
  SetXp(tty, xp);
  SetXsFb(tty, xsfb);
  if (type == PC_VIDEO_TEXT) {
    yn = yp;
    xn = xp;
    tty->canvas = fb;
  } else {
    yn = yp / yc;
    xn = xp / xc;
    if ((init_flags & kTtyKlog) != 0) {
      tty->canvas = fb;
      xs = xsfb;
    } else {
      dm = sys_mmap_metal(NULL, (size_t)yp * xs, PROT_READ | PROT_WRITE,
                          MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
      if (dm.addr == (void *)-1) {
        /*
         * We are a bit low on memory.  Try to go on anyway, & initialize
         * our tty as an emergency console.
         */
        init_flags |= kTtyKlog;
        tty->canvas = fb;
        xs = xsfb;
      } else
        tty->canvas = dm.addr;
    }
  }
  SetYn(tty, yn);
  SetXn(tty, xn);
  tty->yc = yc;
  tty->xc = xc;
  tty->fb = fb;
  if (starty >= yn) starty = yn - 1;
  if (startx >= xn) startx = xn - 1;
  tty->y = starty;
  tty->x = startx;
  if ((init_flags & kTtyKlog) != 0) init_flags &= ~kTtyAllocWcs;
  tty->xs = xs;
  TtySetType(tty, type, init_flags);
  if (SetWcs(tty, init_flags)) {
    wchar_t *wcs = Wcs(tty);
    size_t n = (size_t)yn * xn, i;
    if (type == PC_VIDEO_TEXT) {
      struct VgaTextCharCell *ccs = fb;
      for (i = 0; i < n; ++i) wcs[i] = bing(ccs[i].ch, 0);
    } else
      wmemset(wcs, L' ', n);
  }
  _TtyResetOutputMode(tty);
}

static wchar_t *GetXlatAscii(void) {
  unsigned i;
  static bool once;
  static wchar_t xlat[128];
  if (!once) {
    for (i = 0; i < 128; ++i) {
      xlat[i] = i;
    }
    once = true;
  }
  return xlat;
}

static wchar_t *GetXlatLineDrawing(void) {
  unsigned i;
  static bool once;
  static wchar_t xlat[128];
  if (!once) {
    for (i = 0; i < 128; ++i) {
      if (0x5F <= i && i <= 0x7E) {
        xlat[i] = u"Â ââââââÂ°Â±â¤ââââââ¼âºâ»ââ¼â½ââ¤â´â¬ââ¤â¥Ïâ Â£Â·"[i - 0x5F];
      } else {
        xlat[i] = i;
      }
    }
    once = true;
  }
  return xlat;
}

static wchar_t *GetXlatItalic(void) {
  /* Unimplemented.  Simply output normal non-italicized characters for now. */
  return GetXlatAscii();
}

static wchar_t *GetXlatBoldItalic(void) {
  /*
   * Unimplemented.  Simply output high-intensity non-italicized characters
   * for now.
   */
  return GetXlatAscii();
}

static wchar_t *GetXlatBoldFraktur(void) {
  /* Unimplemented. */
  return GetXlatAscii();
}

static wchar_t *GetXlatFraktur(void) {
  /* Unimplemented. */
  return GetXlatAscii();
}

static wchar_t *GetXlatDoubleWidth(void) {
  unsigned i;
  static bool once;
  static wchar_t xlat[128];
  if (!once) {
    for (i = 0; i < ARRAYLEN(xlat); ++i) {
      if ('!' <= i && i <= '~') {
        xlat[i] = -(i - '!' + L'ï¼');
      } else {
        xlat[i] = i;
      }
    }
    once = true;
  }
  return xlat;
}

static wchar_t *GetXlatSgr(struct Tty *tty) {
  switch (!!(tty->pr & kTtyFraktur) << 2 | !!(tty->pr & kTtyItalic) << 1 |
          !!(tty->pr & kTtyBold) << 0) {
    case 0b100:
    case 0b110:
      return GetXlatFraktur();
    case 0b101:
    case 0b111:
      return GetXlatBoldFraktur();
    case 0b011:
      return GetXlatBoldItalic();
    case 0b010:
      return GetXlatItalic();
    default:
      return GetXlatAscii();
  }
}

static void TtySetXlat(struct Tty *tty, wchar_t *xlat) {
  tty->xlat = xlat;
  tty->pr &= ~(kTtyItalic | kTtyFraktur);
}

static void TtySetCodepage(struct Tty *tty, char id) {
  unsigned i;
  switch (id) {
    default:
    case 'B':
      TtySetXlat(tty, GetXlatAscii());
      break;
    case '0':
      TtySetXlat(tty, GetXlatLineDrawing());
      break;
  }
}

/**
 * Map the given direct color value to one of the 16 basic foreground colors
 * or one of the 16 background colors.
 *
 * @see drivers/tty/vt/vt.c in Linux 5.9.14 source code
 */
static uint8_t TtyGetTextColor(TtyCanvasColor color) {
  uint8_t r = color.bgr.r, g = color.bgr.g, b = color.bgr.b;
  uint8_t hue = 0, max = MAX(MAX(r, g), b);
  if (r > max / 2) hue |= 4;
  if (g > max / 2) hue |= 2;
  if (b > max / 2) hue |= 1;
  if (hue == 7 && max <= 0x55)
    hue = 8;
  else if (max > 0xaa)
    hue |= 8;
  return hue;
}

/**
 * Map the currently active foreground & background colors & terminal
 * configuration to a VGA text character attribute byte.
 *
 * @see VGA_USE_BLINK macro (libc/vga/vga.internal.h)
 * @see drivers/tty/vt/vt.c in Linux 5.9.14 source code
 */
static uint8_t TtyGetTextAttr(struct Tty *tty) {
  uint8_t fg = TtyGetTextColor(tty->fg), bg = TtyGetTextColor(tty->bg);
  uint8_t attr;
  if ((tty->pr & kTtyFlip) == 0)
    attr = fg | bg << 4;
  else
    attr = bg | fg << 4;
#ifdef VGA_USE_BLINK
  /*
   * If blinking is enabled, we can only have the 8 dark background color
   * codes (0 to 7).  Simply map any bright background color (8 to 15) to
   * its corresponding dark color, & call it a day.  This is a bit more
   * simplistic than what Linux does, but should be enough.
   */
  attr &= ~0x80;
  if ((tty->pr & kTtyBlink) != 0) attr |= 0x80;
#endif
  if ((tty->pr & kTtyBold) != 0) attr |= 0x08;
  return attr;
}

static void TtyNoopUpdate(struct Tty *tty) {
}

static void TtyTextDrawChar(struct Tty *tty, size_t y, size_t x, wchar_t wc) {
  uint8_t attr = TtyGetTextAttr(tty);
  struct VgaTextCharCell *ccs = (struct VgaTextCharCell *)tty->canvas;
  size_t i = tty->y * Xn(tty) + tty->x;
  int c = unbing(wc);
  if (c == -1) c = 0xFE;
  ccs[i] = (struct VgaTextCharCell){c, attr};
}

static void TtyTextEraseLineCells(struct Tty *tty, size_t dsty, size_t dstx,
                                  size_t n) {
  uint8_t attr = TtyGetTextAttr(tty);
  struct VgaTextCharCell *ccs = (struct VgaTextCharCell *)tty->canvas;
  size_t dst = dsty * Xn(tty) + dstx, i;
  for (i = 0; i < n; ++i) ccs[dst + i] = (struct VgaTextCharCell){' ', attr};
}

void _TtyEraseLineCells(struct Tty *tty, size_t dsty, size_t dstx, size_t n) {
  tty->eraselinecells(tty, dsty, dstx, n);
  if (Wcs(tty)) {
    size_t i = dsty * Xn(tty) + dstx;
    wmemset(Wcs(tty) + i, L' ', n);
  }
}

void _TtyEraseLines(struct Tty *tty, size_t dsty, size_t n) {
  size_t xn = Xn(tty), y;
  y = dsty;
  while (n-- != 0) {
    _TtyEraseLineCells(tty, y, 0, xn);
    ++y;
  }
}

static void TtyTextMoveLineCells(struct Tty *tty, size_t dsty, size_t dstx,
                                 size_t srcy, size_t srcx, size_t n) {
  size_t xn = Xn(tty);
  size_t dst = dsty * xn + dstx, src = srcy * xn + srcx;
  struct VgaTextCharCell *ccs = (struct VgaTextCharCell *)tty->canvas;
  memmove(ccs + dst, ccs + src, n * sizeof(struct VgaTextCharCell));
}

void _TtyMoveLineCells(struct Tty *tty, size_t dsty, size_t dstx, size_t srcy,
                       size_t srcx, size_t n) {
  size_t xn = Xn(tty);
  size_t dst = dsty * xn + dstx, src = srcy * xn + srcx;
  tty->movelinecells(tty, dsty, dstx, srcy, srcx, n);
  if (Wcs(tty)) wmemmove(Wcs(tty) + dst, Wcs(tty) + src, n);
}

void _TtyMoveLines(struct Tty *tty, size_t dsty, size_t srcy, size_t n) {
  size_t xn = Xn(tty);
  if (dsty < srcy) {
    while (n-- != 0) {
      _TtyMoveLineCells(tty, dsty, 0, srcy, 0, xn);
      ++dsty;
      ++srcy;
    }
  } else if (dsty > srcy) {
    while (n-- != 0) _TtyMoveLineCells(tty, dsty + n, 0, srcy + n, 0, xn);
  }
}

void _TtyResetOutputMode(struct Tty *tty) {
  tty->updy1 = tty->updx1 = tty->updy2 = tty->updx2 = 0;
  tty->pr = 0;
  tty->fg = DEFAULT_FG;
  tty->bg = DEFAULT_BG;
  tty->u8 = 0;
  tty->n8 = 0;
  tty->conf = 0;
  tty->savey = 0;
  tty->savex = 0;
  tty->state = 0;
  tty->esc.i = 0;
  tty->input.i = 0;
  tty->xlat = GetXlatAscii();
}

void _TtyFullReset(struct Tty *tty) {
  _TtyResetOutputMode(tty);
  tty->y = 0;
  tty->x = 0;
  _TtyEraseLines(tty, 0, Yn(tty));
}

void _TtySetY(struct Tty *tty, unsigned short y) {
  tty->conf &= ~kTtyRedzone;
  tty->y = MAX(0, MIN(Yn(tty) - 1, y));
}

void _TtySetX(struct Tty *tty, unsigned short x) {
  tty->conf &= ~kTtyRedzone;
  tty->x = MAX(0, MIN(Xn(tty) - 1, x));
}

static void TtyScroll(struct Tty *tty) {
  _TtyMoveLines(tty, 0, 1, Yn(tty) - 1);
  _TtyEraseLines(tty, Yn(tty) - 1, 1);
}

static void TtyReverse(struct Tty *tty) {
  _TtyMoveLines(tty, 1, 0, Yn(tty) - 1);
  _TtyEraseLines(tty, 0, 1);
}

static void TtyIndex(struct Tty *tty) {
  if (tty->y < Yn(tty) - 1) {
    ++tty->y;
  } else {
    TtyScroll(tty);
  }
}

static void TtyReverseIndex(struct Tty *tty) {
  if (tty->y) {
    --tty->y;
  } else {
    TtyReverse(tty);
  }
}

static void TtyCarriageReturn(struct Tty *tty) {
  _TtySetX(tty, 0);
}

static void TtyNewline(struct Tty *tty) {
  TtyIndex(tty);
  if (!(tty->conf & kTtyNoopost)) {
    TtyCarriageReturn(tty);
  }
}

static void TtyAdvance(struct Tty *tty) {
  tty->conf &= ~kTtyRedzone;
  tty->x = 0;
  if (tty->y < Yn(tty) - 1) {
    ++tty->y;
  } else {
    TtyScroll(tty);
  }
}

static void TtyWriteGlyph(struct Tty *tty, wint_t wc, int w) {
  if (w < 1) wc = L' ', w = 1;
  if ((tty->conf & kTtyRedzone) || tty->x + w > Xn(tty)) {
    TtyAdvance(tty);
  }
  tty->drawchar(tty, tty->y, tty->x, wc);
  if (Wcs(tty)) Wcs(tty)[tty->y * Xn(tty) + tty->x] = wc;
  if ((tty->x += w) >= Xn(tty)) {
    tty->x = Xn(tty) - 1;
    tty->conf |= kTtyRedzone;
  }
}

static void TtyWriteTab(struct Tty *tty) {
  unsigned x, x2;
  if (tty->conf & kTtyRedzone) {
    TtyAdvance(tty);
  }
  x2 = MIN(Xn(tty), ROUNDUP(tty->x + 1, 8));
  for (x = tty->x; x < x2; ++x) {
    tty->eraselinecells(tty, tty->y, x, 1);
  }
  if (Wcs(tty)) {
    for (x = tty->x; x < x2; ++x) {
      Wcs(tty)[tty->y * Xn(tty) + x] = L' ';
    }
  }
  if (x2 < Xn(tty)) {
    tty->x = x2;
  } else {
    tty->x = Xn(tty) - 1;
    tty->conf |= kTtyRedzone;
  }
}

int TtyAtoi(const char *s, const char **e) {
  int i;
  for (i = 0; isdigit(*s); ++s) i *= 10, i += *s - '0';
  if (e) *e = s;
  return i;
}

static int TtyGetMoveParam(struct Tty *tty) {
  int x = TtyAtoi(tty->esc.s, NULL);
  if (x < 1) x = 1;
  return x;
}

static void TtySetCursorPosition(struct Tty *tty) {
  int row, col;
  const char *s = tty->esc.s;
  row = max(1, TtyAtoi(s, &s));
  if (*s == ';') ++s;
  col = max(1, TtyAtoi(s, &s));
  _TtySetY(tty, row - 1);
  _TtySetX(tty, col - 1);
}

static void TtySetCursorRow(struct Tty *tty) {
  _TtySetY(tty, TtyGetMoveParam(tty) - 1);
}

static void TtySetCursorColumn(struct Tty *tty) {
  _TtySetX(tty, TtyGetMoveParam(tty) - 1);
}

static void TtyMoveCursor(struct Tty *tty, int dy, int dx) {
  int n = TtyGetMoveParam(tty);
  _TtySetY(tty, tty->y + dy * n);
  _TtySetX(tty, tty->x + dx * n);
}

static void TtyScrollUp(struct Tty *tty) {
  int n = TtyGetMoveParam(tty);
  while (n--) TtyScroll(tty);
}

static void TtyScrollDown(struct Tty *tty) {
  int n = TtyGetMoveParam(tty);
  while (n--) TtyReverse(tty);
}

static void TtySetCursorStatus(struct Tty *tty, bool status) {
  if (status) {
    tty->conf &= ~kTtyNocursor;
  } else {
    tty->conf |= kTtyNocursor;
  }
}

static void TtySetMode(struct Tty *tty, bool status) {
  const char *p = tty->esc.s;
  switch (*p++) {
    case '?':
      while (isdigit(*p)) {
        switch (TtyAtoi(p, &p)) {
          case 25:
            TtySetCursorStatus(tty, status);
            break;
          default:
            break;
        }
        if (*p == ';') {
          ++p;
        }
      }
      break;
    default:
      break;
  }
}

static void TtySaveCursorPosition(struct Tty *tty) {
  tty->savey = tty->y;
  tty->savex = tty->x;
}

static void TtyRestoreCursorPosition(struct Tty *tty) {
  _TtySetY(tty, tty->savey);
  _TtySetX(tty, tty->savex);
}

static void TtyEraseDisplay(struct Tty *tty) {
  switch (TtyAtoi(tty->esc.s, NULL)) {
    case 0:
      _TtyEraseLineCells(tty, tty->y, tty->x, Xn(tty) - tty->x);
      _TtyEraseLines(tty, tty->y + 1, Yn(tty) - tty->y - 1);
      break;
    case 1:
      _TtyEraseLines(tty, 0, tty->y);
      _TtyEraseLineCells(tty, tty->y, 0, tty->x);
      break;
    case 2:
    case 3:
      _TtyEraseLines(tty, 0, Yn(tty));
      break;
    default:
      break;
  }
}

static void TtyEraseLine(struct Tty *tty) {
  switch (TtyAtoi(tty->esc.s, NULL)) {
    case 0:
      _TtyEraseLineCells(tty, tty->y, tty->x, Xn(tty) - tty->x);
      break;
    case 1:
      _TtyEraseLineCells(tty, tty->y, 0, tty->x);
      break;
    case 2:
      _TtyEraseLines(tty, tty->y, 1);
      break;
    default:
      break;
  }
}

static void TtyEraseCells(struct Tty *tty) {
  int yn, xn, yi, xi, n, left;
  yn = Yn(tty);
  xn = Xn(tty);
  yi = tty->y;
  xi = tty->x;
  left = min(max(TtyAtoi(tty->esc.s, NULL), 1), yn * xn - (yi * xn + xi));
  while (left) {
    if (left >= xn - xi) {
      _TtyEraseLineCells(tty, yi, xi, xn - xi);
      left -= xn - xi;
      ++yi;
      xi = 0;
    } else {
      _TtyEraseLineCells(tty, yi, xi, left);
      left = 0;
    }
  }
}

static int TtyArg1(struct Tty *tty) {
  return max(1, TtyAtoi(tty->esc.s, NULL));
}

static void TtyInsertCells(struct Tty *tty) {
  int n = min(Xn(tty) - tty->x, TtyArg1(tty));
  _TtyMoveLineCells(tty, tty->y, tty->x + n, tty->y, tty->x,
                    Xn(tty) - (tty->x + n));
  _TtyEraseLineCells(tty, tty->y, tty->x, n);
}

static void TtyInsertLines(struct Tty *tty) {
  int n = min(Yn(tty) - tty->y, TtyArg1(tty));
  _TtyMoveLines(tty, tty->y + n, tty->y, Yn(tty) - tty->y - n);
  _TtyEraseLines(tty, tty->y, n);
}

static void TtyDeleteCells(struct Tty *tty) {
  int n = min(Xn(tty) - tty->x, TtyArg1(tty));
  _TtyMoveLineCells(tty, tty->y, tty->x, tty->y, tty->x + n,
                    Xn(tty) - (tty->x + n));
  _TtyEraseLineCells(tty, tty->y, tty->x, n);
}

static void TtyDeleteLines(struct Tty *tty) {
  int n = min(Yn(tty) - tty->y, TtyArg1(tty));
  _TtyMoveLines(tty, tty->y, tty->y + n, Yn(tty) - tty->y - n);
  _TtyEraseLines(tty, tty->y + n, n);
}

static void TtyReportDeviceStatus(struct Tty *tty) {
  static const char report[] = "\e[0n";
  _TtyWriteInput(tty, report, strlen(report));
}

static void TtyReportPreferredVtType(struct Tty *tty) {
  static const char report[] = "\e[?1;0c";
  _TtyWriteInput(tty, report, strlen(report));
}

static void TtyReportPreferredVtIdentity(struct Tty *tty) {
  static const char report[] = "\e/Z";
  _TtyWriteInput(tty, report, strlen(report));
}

static void TtyBell(struct Tty *tty) {
  tty->conf |= kTtyBell;
}

static void TtyLed(struct Tty *tty) {
  switch (TtyAtoi(tty->esc.s, NULL)) {
    case 0:
      tty->conf &= ~kTtyLed1;
      tty->conf &= ~kTtyLed2;
      tty->conf &= ~kTtyLed3;
      tty->conf &= ~kTtyLed4;
      break;
    case 1:
      tty->conf |= kTtyLed1;
      break;
    case 2:
      tty->conf |= kTtyLed2;
      break;
    case 3:
      tty->conf |= kTtyLed3;
      break;
    case 4:
      tty->conf |= kTtyLed4;
      break;
    default:
      break;
  }
}

static void TtyReportCursorPosition(struct Tty *tty) {
  char *p;
  char buf[2 + 10 + 1 + 10 + 1];
  p = buf;
  *p++ = '\e';
  *p++ = '[';
  p = FormatInt32(p, (tty->y + 1) & 0x7fff);
  *p++ = ';';
  p = FormatInt32(p, (tty->x + 1) & 0x7fff);
  *p++ = 'R';
  _TtyWriteInput(tty, buf, p - buf);
}

static void TtyCsiN(struct Tty *tty) {
  switch (TtyAtoi(tty->esc.s, NULL)) {
    case 5:
      TtyReportDeviceStatus(tty);
      break;
    case 6:
      TtyReportCursorPosition(tty);
      break;
    default:
      break;
  }
}

/** Create a direct color pixel value. */
static TtyCanvasColor TtyMapTrueColor(uint8_t r, uint8_t g, uint8_t b) {
  return (TtyCanvasColor){.bgr.r = r, .bgr.g = g, .bgr.b = b, .bgr.x = 0xff};
}

/** Map the given 256-color code to a direct color. */
static TtyCanvasColor TtyMapXtermColor(uint8_t color) {
  uint8_t r, g, b;
  if (color < 8) {
    r = (color & 1) ? 0xaa : 0;
    g = (color & 2) ? 0xaa : 0;
    b = (color & 4) ? 0xaa : 0;
  } else if (color < 16) {
    r = (color & 1) ? 0xff : 0x55;
    g = (color & 2) ? 0xff : 0x55;
    b = (color & 4) ? 0xff : 0x55;
  } else if (color < 232) {
    color -= 16;
    b = color % 6 * 0x55 / 2;
    color /= 6;
    g = color % 6 * 0x55 / 2;
    color /= 6;
    r = color * 0x55 / 2;
  } else
    r = g = b = (unsigned)color * 10 - 2312;
  return TtyMapTrueColor(r, g, b);
}

/** Map the given ECMA-48 / VT100 SGR color code to a direct color. */
static TtyCanvasColor TtyMapEcma48Color(uint8_t color) {
  return TtyMapXtermColor(color % 16);
}

static void TtySelectGraphicsRendition(struct Tty *tty) {
  char *p, c;
  unsigned x;
  uint8_t code[4];
  enum {
    kSgr,
    kSgrFg = 010,
    kSgrFgTrue = 012,
    kSgrFgXterm = 015,
    kSgrBg = 020,
    kSgrBgTrue = 022,
    kSgrBgXterm = 025,
  } t;
  x = 0;
  t = kSgr;
  p = tty->esc.s;
  bzero(code, sizeof(code));
  for (;;) {
    c = *p++;
    switch (c) {
      case '\0':
        return;
      case '0' ... '9':
        x *= 10;
        x += c - '0';
        break;
      case ';':
      case 'm':
        code[code[3]] = x;
        x = 0;
        switch (t) {
          case kSgr:
            switch (code[0]) {
              case 38:
                t = kSgrFg;
                break;
              case 48:
                t = kSgrBg;
                break;
              case 0:
                tty->pr = 0;
                tty->fg = DEFAULT_FG;
                tty->bg = DEFAULT_BG;
                tty->xlat = GetXlatSgr(tty);
                break;
              case 1:
                tty->pr |= kTtyBold;
                tty->xlat = GetXlatSgr(tty);
                break;
              case 2:
                tty->pr |= kTtyFaint;
                break;
              case 3:
                tty->pr |= kTtyItalic;
                tty->xlat = GetXlatSgr(tty);
                break;
              case 4:
                tty->pr |= kTtyUnder;
                break;
              case 5:
                tty->pr |= kTtyBlink;
                break;
              case 7:
                tty->pr |= kTtyFlip;
                break;
              case 8:
                tty->pr |= kTtyConceal;
                break;
              case 9:
                tty->pr |= kTtyStrike;
                break;
              case 20:
                tty->pr |= kTtyFraktur;
                tty->xlat = GetXlatSgr(tty);
                break;
              case 21:
                tty->pr |= kTtyUnder | kTtyDunder;
                break;
              case 22:
                tty->pr &= ~(kTtyFaint | kTtyBold);
                tty->xlat = GetXlatSgr(tty);
                break;
              case 23:
                tty->pr &= ~kTtyItalic;
                tty->xlat = GetXlatSgr(tty);
                break;
              case 24:
                tty->pr &= ~(kTtyUnder | kTtyDunder);
                break;
              case 25:
                tty->pr &= ~kTtyBlink;
                break;
              case 27:
                tty->pr &= ~kTtyFlip;
                break;
              case 28:
                tty->pr &= ~kTtyConceal;
                break;
              case 29:
                tty->pr &= ~kTtyStrike;
                break;
              case 39:
                tty->fg = DEFAULT_FG;
                tty->pr &= ~kTtyFg;
                break;
              case 49:
                tty->bg = DEFAULT_BG;
                tty->pr &= ~kTtyBg;
                break;
              case 90 ... 97:
                code[0] -= 90 - 30;
                code[0] += 8;
                /* fallthrough */
              case 30 ... 37:
                tty->fg = TtyMapEcma48Color(code[0] - 30);
                tty->pr |= kTtyFg;
                tty->pr &= ~kTtyTrue;
                break;
              case 100 ... 107:
                code[0] -= 100 - 40;
                code[0] += 8;
                /* fallthrough */
              case 40 ... 47:
                tty->bg = TtyMapEcma48Color(code[0] - 40);
                tty->pr |= kTtyBg;
                tty->pr &= ~kTtyTrue;
                break;
              default:
                break;
            }
            break;
          case kSgrFg:
          case kSgrBg:
            switch (code[0]) {
              case 2:
              case 5:
                t += code[0];
                break;
              default:
                t = kSgr;
                break;
            }
            break;
          case kSgrFgTrue:
            if (++code[3] == 3) {
              code[3] = 0;
              t = kSgr;
              tty->fg = TtyMapTrueColor(code[0], code[1], code[2]);
              tty->pr |= kTtyFg;
              tty->pr |= kTtyTrue;
            }
            break;
          case kSgrBgTrue:
            if (++code[3] == 3) {
              code[3] = 0;
              t = kSgr;
              tty->bg = TtyMapTrueColor(code[0], code[1], code[2]);
              tty->pr |= kTtyBg;
              tty->pr |= kTtyTrue;
            }
            break;
          case kSgrFgXterm:
            t = kSgr;
            tty->fg = TtyMapXtermColor(code[0]);
            tty->pr |= kTtyFg;
            tty->pr &= ~kTtyTrue;
            break;
          case kSgrBgXterm:
            t = kSgr;
            tty->bg = TtyMapXtermColor(code[0]);
            tty->pr |= kTtyBg;
            tty->pr &= ~kTtyTrue;
            break;
          default:
            abort();
        }
        break;
      default:
        break;
    }
  }
}

static void TtyCsi(struct Tty *tty) {
  switch (tty->esc.s[tty->esc.i - 1]) {
    case 'f':
    case 'H':
      TtySetCursorPosition(tty);
      break;
    case 'G':
      TtySetCursorColumn(tty);
      break;
    case 'd':
      TtySetCursorRow(tty);
      break;
    case 'F':
      tty->x = 0;
      /* fallthrough */
    case 'A':
      TtyMoveCursor(tty, -1, +0);
      break;
    case 'E':
      tty->x = 0;
      /* fallthrough */
    case 'B':
      TtyMoveCursor(tty, +1, +0);
      break;
    case 'C':
      TtyMoveCursor(tty, +0, +1);
      break;
    case 'D':
      TtyMoveCursor(tty, +0, -1);
      break;
    case 'S':
      TtyScrollUp(tty);
      break;
    case 'T':
      TtyScrollDown(tty);
      break;
    case '@':
      TtyInsertCells(tty);
      break;
    case 'P':
      TtyDeleteCells(tty);
      break;
    case 'L':
      TtyInsertLines(tty);
      break;
    case 'M':
      TtyDeleteLines(tty);
      break;
    case 'J':
      TtyEraseDisplay(tty);
      break;
    case 'K':
      TtyEraseLine(tty);
      break;
    case 'X':
      TtyEraseCells(tty);
      break;
    case 's':
      TtySaveCursorPosition(tty);
      break;
    case 'u':
      TtyRestoreCursorPosition(tty);
      break;
    case 'n':
      TtyCsiN(tty);
      break;
    case 'm':
      TtySelectGraphicsRendition(tty);
      break;
    case 'h':
      TtySetMode(tty, true);
      break;
    case 'l':
      TtySetMode(tty, false);
      break;
    case 'c':
      TtyReportPreferredVtType(tty);
      break;
    case 'q':
      TtyLed(tty);
      break;
    default:
      break;
  }
}

static void TtyScreenAlignmentDisplay(struct Tty *tty) {
  size_t yn = Yn(tty), xn = Xn(tty), y, x;
  for (y = 0; y < yn; ++y)
    for (x = 0; x < xn; ++x) tty->drawchar(tty, y, x, 'E');
  if (Wcs(tty)) wmemset(Wcs(tty), L'E', yn * xn);
}

static void TtyEscHash(struct Tty *tty) {
  switch (tty->esc.s[1]) {
    case '5':
      TtySetXlat(tty, GetXlatAscii());
      break;
    case '6':
      TtySetXlat(tty, GetXlatDoubleWidth());
      break;
    case '8':
      TtyScreenAlignmentDisplay(tty);
      break;
    default:
      break;
  }
}

static void TtyEsc(struct Tty *tty) {
  switch (tty->esc.s[0]) {
    case 'c':
      _TtyFullReset(tty);
      break;
    case '7':
      TtySaveCursorPosition(tty);
      break;
    case '8':
      TtyRestoreCursorPosition(tty);
      break;
    case 'E':
      tty->x = 0;
    case 'D':
      TtyIndex(tty);
      break;
    case 'M':
      TtyReverseIndex(tty);
      break;
    case 'Z':
      TtyReportPreferredVtIdentity(tty);
      break;
    case '(':
      TtySetCodepage(tty, tty->esc.s[1]);
      break;
    case '#':
      TtyEscHash(tty);
      break;
    default:
      break;
  }
}

static void TtyCntrl(struct Tty *tty, int c01) {
  switch (c01) {
    case '\a':
      TtyBell(tty);
      break;
    case 0x85:
    case '\f':
    case '\v':
    case '\n':
      TtyNewline(tty);
      break;
    case '\r':
      TtyCarriageReturn(tty);
      break;
    case '\e':
      tty->state = kTtyEsc;
      tty->esc.i = 0;
      break;
    case '\t':
      TtyWriteTab(tty);
      break;
    case 0x7F:
    case '\b':
      tty->x = MAX(0, tty->x - 1);
      break;
    case 0x84:
      TtyIndex(tty);
      break;
    case 0x8D:
      TtyReverseIndex(tty);
      break;
    case 0x9B:
      tty->state = kTtyCsi;
      break;
    default:
      break;
  }
}

static void TtyEscAppend(struct Tty *tty, char c) {
  tty->esc.i = MIN(tty->esc.i + 1, ARRAYLEN(tty->esc.s) - 1);
  tty->esc.s[tty->esc.i - 1] = c;
  tty->esc.s[tty->esc.i - 0] = 0;
}

static void TtyUpdate(struct Tty *tty) {
  if (tty->type == PC_VIDEO_TEXT) {
    unsigned char start = tty->yc - 2, end = tty->yc - 1;
    unsigned short pos = tty->y * Xn(tty) + tty->x;
    if ((tty->conf & kTtyNocursor)) start |= 1 << 5;
    outb(CRTPORT, 0x0A);
    outb(CRTPORT + 1, start);
    outb(CRTPORT, 0x0B);
    outb(CRTPORT + 1, end);
    outb(CRTPORT, 0x0E);
    outb(CRTPORT + 1, (unsigned char)(pos >> 8));
    outb(CRTPORT, 0x0F);
    outb(CRTPORT + 1, (unsigned char)pos);
  } else
    tty->update(tty);
}

ssize_t _TtyWrite(struct Tty *tty, const void *data, size_t n) {
  int i;
  wchar_t wc;
  const uint8_t *p;
  for (p = data, i = 0; i < n; ++i) {
    switch (tty->state) {
      case kTtyAscii:
        if (0x00 <= p[i] && p[i] <= 0x7F) {
          if (0x20 <= p[i] && p[i] <= 0x7E) {
            if ((wc = tty->xlat[p[i]]) >= 0) {
              TtyWriteGlyph(tty, wc, 1);
            } else {
              TtyWriteGlyph(tty, -wc, 2);
            }
          } else {
            TtyCntrl(tty, p[i]);
          }
        } else if (!ThomPikeCont(p[i])) {
          tty->state = kTtyUtf8;
          tty->u8 = ThomPikeByte(p[i]);
          tty->n8 = ThomPikeLen(p[i]) - 1;
        }
        break;
      case kTtyUtf8:
        if (ThomPikeCont(p[i])) {
          tty->u8 = ThomPikeMerge(tty->u8, p[i]);
          if (--tty->n8) break;
        }
        wc = tty->u8;
        if ((0x00 <= wc && wc <= 0x1F) || (0x7F <= wc && wc <= 0x9F)) {
          TtyCntrl(tty, wc);
        } else {
          TtyWriteGlyph(tty, wc, wcwidth(wc));
        }
        tty->state = kTtyAscii;
        tty->u8 = 0;
        --i;
        break;
      case kTtyEsc:
        if (p[i] == '[') {
          tty->state = kTtyCsi;
        } else if (0x30 <= p[i] && p[i] <= 0x7E) {
          TtyEscAppend(tty, p[i]);
          TtyEsc(tty);
          tty->state = kTtyAscii;
        } else if (0x20 <= p[i] && p[i] <= 0x2F) {
          TtyEscAppend(tty, p[i]);
        } else {
          tty->state = kTtyAscii;
        }
        break;
      case kTtyCsi:
        TtyEscAppend(tty, p[i]);
        switch (p[i]) {
          case ':':
          case ';':
          case '<':
          case '=':
          case '>':
          case '?':
          case '0' ... '9':
            break;
          case '`':
          case '~':
          case '^':
          case '@':
          case '[':
          case ']':
          case '{':
          case '}':
          case '_':
          case '|':
          case '\\':
          case 'A' ... 'Z':
          case 'a' ... 'z':
            TtyCsi(tty);
            tty->state = kTtyAscii;
            break;
          default:
            tty->state = kTtyAscii;
            continue;
        }
        break;
      default:
        unreachable;
    }
  }
  TtyUpdate(tty);
  return n;
}

ssize_t _TtyWriteInput(struct Tty *tty, const void *data, size_t n) {
  int c;
  bool cr;
  char *p;
  const char *q;
  size_t i, j, m;
  q = data;
  p = tty->input.p;
  i = tty->input.i;
  cr = i && p[i - 1] == '\r';
  for (j = 0; j < n && i < sizeof tty->input.p; ++j) {
    c = q[j] & 255;
    if (c == '\r') {
      cr = true;
    } else if (cr) {
      if (c != '\n') {
        p[i++] = '\n';
      }
      cr = false;
    }
    p[i++] = c;
  }
  if (cr && i < sizeof tty->input.p) {
    p[i++] = '\n';
  }
#ifdef VGA_PERSNICKETY_STATUS
  if (!(tty->conf & kTtyNoecho)) {
    _TtyWrite(tty, p + tty->input.i, i - tty->input.i);
  }
#endif
  tty->input.i = i;
  return n;
}

ssize_t _TtyRead(struct Tty *tty, void *buf, size_t size) {
  char *p;
  size_t n;
  n = MIN(size, tty->input.i);
#ifdef VGA_PERSNICKETY_STATUS
  if (!(tty->conf & kTtyNocanon)) {
    if ((p = memchr(tty->input.p, '\n', n))) {
      n = MIN(n, tty->input.p - p + 1);
    } else {
      n = 0;
    }
  }
#endif
  memcpy(buf, tty->input.p, n);
  memcpy(tty->input.p, tty->input.p + n, tty->input.i - n);
  tty->input.i -= n;
  return n;
}

#endif /* __x86_64__ */
