/*
 * 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-2021 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>.
 */

#include "prologue.h"

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <linux/tty.h>
#include <linux/vt.h>
#include <linux/kd.h>
#include <linux/tiocl.h>
#include <linux/major.h>

#ifndef VT_GETHIFONTMASK
#define VT_GETHIFONTMASK 0X560D
#endif /* VT_GETHIFONTMASK */

#include "log.h"
#include "report.h"
#include "async_io.h"
#include "device.h"
#include "io_misc.h"
#include "timing.h"
#include "parse.h"
#include "brl_cmds.h"
#include "kbd_keycodes.h"
#include "ascii.h"
#include "unicode.h"
#include "charset.h"
#include "scr_gpm.h"
#include "system_linux.h"

typedef enum {
  PARM_CHARSET,
  PARM_FALLBACK_TEXT,
  PARM_HIGH_FONT_BIT,
  PARM_LOG_SCREEN_FONT_MAP,
  PARM_UNICODE,
  PARM_VIRTUAL_TERMINAL_NUMBER,
} ScreenParameters;
#define SCRPARMS "charset", "fallbacktext", "hfb", "logsfm", "unicode", "vt"

#include "scr_driver.h"
#include "screen.h"

static const char *problemText;
static const char *fallbackText;

static unsigned int logScreenFontMap;
static unsigned int unicodeEnabled;
static int virtualTerminalNumber;

#define UNICODE_ROW_DIRECT 0XF000

typedef enum {
  CONV_OK,
  CONV_ILLEGAL,
  CONV_SHORT,
  CONV_OVERFLOW,
  CONV_ERROR
} CharacterConversionResult;

#if defined(HAVE_ICONV_H)
#include <iconv.h>

typedef struct {
  iconv_t iconvHandle;
} CharsetConverter;

#define ICONV_NULL ((iconv_t)-1)
#define CHARSET_CONVERTER_INITIALIZER {.iconvHandle = ICONV_NULL}

static int
allocateCharsetConverter (CharsetConverter *converter, const char *sourceCharset, const char *targetCharset) {
  if (converter->iconvHandle == ICONV_NULL) {
    if ((converter->iconvHandle = iconv_open(targetCharset, sourceCharset)) == ICONV_NULL) {
      logSystemError("iconv_open");
      return 0;
    }
  }
  return 1;
}

static void
deallocateCharsetConverter (CharsetConverter *converter) {
  if (converter->iconvHandle != ICONV_NULL) {
    iconv_close(converter->iconvHandle);
    converter->iconvHandle = ICONV_NULL;
  }
}

static CharacterConversionResult
convertCharacters (
  CharsetConverter *converter,
  const char **inputAddress, size_t *inputLength,
  char **outputAddress, size_t *outputLength
) {
  ssize_t result = iconv(converter->iconvHandle, (char **)inputAddress, inputLength, outputAddress, outputLength);
  if (result != -1) return CONV_OK;
  if (errno == EILSEQ) return CONV_ILLEGAL;
  if (errno == EINVAL) return CONV_SHORT;
  if (errno == E2BIG) return CONV_OVERFLOW;
  logSystemError("iconv");
  return CONV_ERROR;
}
#else /* charset conversion definitions */
typedef struct {
  char aStructNeedsAtLeastOneField;
} CharsetConverter;

#define CHARSET_CONVERTER_INITIALIZER {0}

static int
allocateCharsetConverter (CharsetConverter *converter, const char *sourceCharset, const char *targetCharset) {
  return 1;
}

static void
deallocateCharsetConverter (CharsetConverter *converter) {
}

static CharacterConversionResult
convertCharacters (
  CharsetConverter *converter,
  const char **inputAddress, size_t *inputLength,
  char **outputAddress, size_t *outputLength
) {
  *(*outputAddress)++ = *(*inputAddress)++;
  *inputLength -= 1;
  *outputLength -= 1;
  return CONV_OK;
}
#endif /* charset conversion definitions */

typedef struct {
  char *name;
  unsigned isMultiByte:1;
  CharsetConverter charsetToWchar;
  CharsetConverter wcharToCharset;
} CharsetEntry;

static CharsetEntry *charsetEntries = NULL;
static unsigned int charsetCount = 0;
static unsigned int charsetIndex = 0;

static inline CharsetEntry *
getCharsetEntry (void) {
  return &charsetEntries[charsetIndex];
}

static void
deallocateCharsetEntries (void) {
  if (charsetEntries) {
    while (charsetCount) {
      CharsetEntry *charset = &charsetEntries[--charsetCount];
      free(charset->name);
      deallocateCharsetConverter(&charset->charsetToWchar);
      deallocateCharsetConverter(&charset->wcharToCharset);
    }

    free(charsetEntries);
    charsetEntries = NULL;
  }
}

static int
allocateCharsetEntries (const char *names) {
  int ok = 0;
  int count;
  char **namesArray = splitString(names, '+', &count);

  if (namesArray) {
    CharsetEntry *entries = calloc(count, sizeof(*entries));

    if (entries) {
      charsetEntries = entries;
      charsetCount = 0;
      charsetIndex = 0;
      ok = 1;

      while (charsetCount < count) {
        CharsetEntry *charset = &charsetEntries[charsetCount];

        if (!(charset->name = strdup(namesArray[charsetCount]))) {
          logMallocError();
          ok = 0;
          deallocateCharsetEntries();
          break;
        }

        charset->isMultiByte = 0;

        {
          static const CharsetConverter nullCharsetConverter = CHARSET_CONVERTER_INITIALIZER;
          charset->charsetToWchar = nullCharsetConverter;
          charset->wcharToCharset = nullCharsetConverter;
        }

        charsetCount += 1;
      }
    }

    deallocateStrings(namesArray);
  }

  return ok;
}

static CharacterConversionResult
convertCharsToWchar (const char *chars, size_t length, wchar_t *character, size_t *size) {
  unsigned int count = charsetCount;

  while (count--) {
    CharsetEntry *charset = getCharsetEntry();
    CharsetConverter *converter = &charset->charsetToWchar;
    CharacterConversionResult result = CONV_ERROR;

    if (allocateCharsetConverter(converter, charset->name, getWcharCharset())) {
      const char *inptr = chars;
      size_t inlen = length;
      char *outptr = (char *)character;
      size_t outlen = sizeof(*character);

      if ((result = convertCharacters(converter, &inptr, &inlen, &outptr, &outlen)) == CONV_OK)
        if (size)
          *size = inptr - chars;
    }

    if (result == CONV_SHORT) charset->isMultiByte = 1;
    if (result != CONV_ILLEGAL) return result;
    if (++charsetIndex == charsetCount) charsetIndex = 0;
  }

  return CONV_ILLEGAL;
}

static CharacterConversionResult
convertWcharToChars (wchar_t character, char *chars, size_t length, size_t *size) {
  CharsetEntry *charset = getCharsetEntry();
  CharsetConverter *converter = &charset->wcharToCharset;
  CharacterConversionResult result = CONV_ERROR;

  if (allocateCharsetConverter(converter, getWcharCharset(), charset->name)) {
    const char *inptr = (char *)&character;
    size_t inlen = sizeof(character);
    char *outptr = chars;
    size_t outlen = length;

    if ((result = convertCharacters(converter, &inptr, &inlen, &outptr, &outlen)) == CONV_OK) {
      size_t count = outptr - chars;
      if (size) *size = count;
      if (count > 1) charset->isMultiByte = 1;
    } else if ((result == CONV_OVERFLOW) && length) {
      charset->isMultiByte = 1;
    }
  }

  return result;
}

static wint_t
convertCharacter (const wchar_t *character) {
  static unsigned char spaces = 0;
  static unsigned char length = 0;
  static char buffer[MB_LEN_MAX];
  const wchar_t cellMask = 0XFF;

  if (!character) {
    length = 0;
    if (!spaces) return WEOF;
    spaces -= 1;
    return WC_C(' ');
  }

  if ((*character & ~cellMask) != UNICODE_ROW_DIRECT) {
    length = 0;
    return *character;
  }

  if (length < sizeof(buffer)) {
    buffer[length++] = *character & cellMask;

    while (1) {
      wchar_t wc;
      CharacterConversionResult result = convertCharsToWchar(buffer, length, &wc, NULL);

      if (result == CONV_OK) {
        length = 0;
        return wc;
      }

      if (result == CONV_SHORT) break;
      if (result != CONV_ILLEGAL) break;

      if (!--length) break;
      memmove(buffer, buffer+1, length);
    }
  }

  spaces += 1;
  return WEOF;
}

static int
setDeviceName (const char **name, const char *const *names, int strict, const char *description) {
  return (*name = resolveDeviceName(names, strict, description)) != NULL;
}

static char *
vtName (const char *name, unsigned char vt) {
  char *string;

  if (vt) {
    int length = strlen(name);
    if (name[length-1] == '0') length -= 1;

    char buffer[length+4];
    snprintf(buffer, sizeof(buffer), "%.*s%u", length, name, vt);

    string = strdup(buffer);
  } else {
    string = strdup(name);
  }

  if (!string) logMallocError();
  return string;
}

static const char *consoleName = NULL;

static int
setConsoleName (void) {
  static const char *const names[] = {"tty0", "vc/0", NULL};
  return setDeviceName(&consoleName, names, 0, "console");
}

static void
closeConsole (int *fd) {
  if (*fd != -1) {
    logMessage(LOG_CATEGORY(SCREEN_DRIVER), "closing console: fd=%d", *fd);
    if (close(*fd) == -1) logSystemError("close[console]");
    *fd = -1;
  }
}

static int
openConsole (int *fd, int vt) {
  int opened = 0;
  char *name = vtName(consoleName, vt);

  if (name) {
    int console = openCharacterDevice(name, O_WRONLY|O_NOCTTY, TTY_MAJOR, vt);

    if (console != -1) {
      logMessage(LOG_CATEGORY(SCREEN_DRIVER),
                 "console opened: %s: fd=%d", name, console);

      closeConsole(fd);
      *fd = console;
      opened = 1;
    }

    free(name);
  }

  return opened;
}

static int
controlConsole (int *fd, int vt, int operation, void *argument) {
  int result = ioctl(*fd, operation, argument);

  if (result == -1) {
    if (errno == EIO) {
      logMessage(LOG_ERR,
                 "console control error %d: fd=%d vt=%d op=0X%04X: %s",
                 errno, *fd, vt, operation, strerror(errno));

      if (openConsole(fd, vt)) {
        result = ioctl(*fd, operation, argument);
      }
    }
  }

  return result;
}

static int consoleDescriptor;

static void
closeCurrentConsole (void) {
  closeConsole(&consoleDescriptor);
}

static int
openCurrentConsole (void) {
  return openConsole(&consoleDescriptor, virtualTerminalNumber);
}

static int
controlCurrentConsole (int operation, void *argument) {
  if (consoleDescriptor != -1) {
    return controlConsole(&consoleDescriptor, virtualTerminalNumber, operation, argument);
  }

  switch (operation) {
    case GIO_UNIMAP: {
      struct unimapdesc *sfm = argument;
      memset(sfm, 0, sizeof(*sfm));
      sfm->entries = NULL;
      sfm->entry_ct = 0;
      return 0;
    }

    case KDFONTOP: {
      struct console_font_op *cfo = argument;

      if (cfo->op == KD_FONT_OP_GET) {
        cfo->charcount = 0;
        cfo->width = 8;
        cfo->height = 16;
        return 0;
      }

      break;
    }

    case VT_GETHIFONTMASK: {
      unsigned short *mask = argument;
      *mask = 0;
      return 0;
    }

    case KDGETMODE: {
      int *mode = argument;
      *mode = KD_TEXT;
      return 0;
    }

    default:
      break;
  }

  errno = EAGAIN;
  return -1;
}

static const int NO_CONSOLE = 0;
static const int MAIN_CONSOLE = 0;
static int mainConsoleDescriptor;

static void
closeMainConsole (void) {
  closeConsole(&mainConsoleDescriptor);
}

static int
openMainConsole (void) {
  return openConsole(&mainConsoleDescriptor, MAIN_CONSOLE);
}

static int
controlMainConsole (int operation, void *argument) {
  return controlConsole(&mainConsoleDescriptor, MAIN_CONSOLE, operation, argument);
}

static const char *unicodeName = NULL;

static int
setUnicodeName (void) {
  static const char *const names[] = {"vcsu", "vcsu0", NULL};
  return setDeviceName(&unicodeName, names, 1, "unicode");
}

static void
closeUnicode (int *fd) {
  if (*fd != -1) {
    logMessage(LOG_CATEGORY(SCREEN_DRIVER), "closing unicode: fd=%d", *fd);
    if (close(*fd) == -1) logSystemError("close[unicode]");
    *fd = -1;
  }
}

static int
openUnicode (int *fd, int vt) {
  if (!unicodeName) return 0;
  if (*fd != -1) return 1;

  int opened = 0;
  char *name = vtName(unicodeName, vt);

  if (name) {
    int unicode = openCharacterDevice(name, O_RDWR, VCS_MAJOR, 0X40|vt);

    if (unicode != -1) {
      logMessage(LOG_CATEGORY(SCREEN_DRIVER),
                 "unicode opened: %s: fd=%d", name, unicode);

      closeUnicode(fd);
      *fd = unicode;
      opened = 1;
    } else {
      unicodeName = NULL;
    }

    free(name);
  }

  return opened;
}

static int unicodeDescriptor;

static void
closeCurrentUnicode (void) {
  closeUnicode(&unicodeDescriptor);
}

static int
openCurrentUnicode (void) {
  if (!unicodeEnabled) return 0;
  return openUnicode(&unicodeDescriptor, virtualTerminalNumber);
}

static size_t
readUnicodeDevice (off_t offset, void *buffer, size_t size) {
  if (openCurrentUnicode()) {
    const ssize_t count = pread(unicodeDescriptor, buffer, size, offset);
    if (count != -1) return count;
    if (errno != ENODATA) logSystemError("unicode read");
  }

  return 0;
}

static unsigned char *unicodeCacheBuffer;
static size_t unicodeCacheSize;
static size_t unicodeCacheUsed;

static size_t
readUnicodeCache (off_t offset, void *buffer, size_t size) {
  if (offset <= unicodeCacheSize) {
    size_t left = unicodeCacheSize - offset;

    if (size > left) size = left;
    memcpy(buffer, &unicodeCacheBuffer[offset], size);
    return size;
  } else {
    logMessage(LOG_ERR, "invalid unicode cache offset: %u", (unsigned int)offset);
  }

  return 0;
}

static int
readUnicodeData (off_t offset, void *buffer, size_t size) {
  size_t count = (unicodeCacheBuffer? readUnicodeCache: readUnicodeDevice)(offset, buffer, size);
  if (count == size) return 1;

  logMessage(LOG_ERR,
             "truncated unicode data: expected %zu bytes but read %zu",
             size, count);

  return 0;
}

static int
readUnicodeContent (off_t offset, uint32_t *buffer, size_t count) {
  count *= sizeof(*buffer);
  offset *= sizeof(*buffer);
  return readUnicodeData(offset, buffer, count);
}

static int
refreshUnicodeCache (size_t size) {
  size *= 4;

  if (size > unicodeCacheSize) {
    const unsigned int bits = 10;
    const unsigned int mask = (1 << bits) - 1;

    size |= mask;
    size += 1;
    unsigned char *buffer = malloc(size);

    if (!buffer) {
      logMallocError();
      return 0;
    }

    if (unicodeCacheBuffer) free(unicodeCacheBuffer);
    unicodeCacheBuffer = buffer;
    unicodeCacheSize = size;
  }

  unicodeCacheUsed = readUnicodeDevice(0, unicodeCacheBuffer, unicodeCacheSize);
  return 1;
}

static const char *screenName = NULL;
static int screenDescriptor;

static int isMonitorable;
static THREAD_LOCAL AsyncHandle screenMonitor = NULL;

static int screenUpdated;

static int currentConsoleNumber;
static int inTextMode;
static TimePeriod mappingRecalculationTimer;

typedef struct {
  unsigned char rows;
  unsigned char columns;
} ScreenSize;

typedef struct {
  unsigned char column;
  unsigned char row;
} ScreenLocation;

typedef struct {
  ScreenSize size;
  ScreenLocation location;
} ScreenHeader;

#ifdef HAVE_SYS_POLL_H
#include <poll.h>

static int
canMonitorScreen (void) {
  struct pollfd pollDescriptor = {
    .fd = screenDescriptor,
    .events = POLLPRI
  };

  return poll(&pollDescriptor, 1, 0) == 1;
}

#else /* can poll */
static int
canMonitorScreen (void) {
  return 0;
}
#endif /* can poll */

static int
setScreenName (void) {
  static const char *const names[] = {"vcsa", "vcsa0", "vcc/a", NULL};
  return setDeviceName(&screenName, names, 0, "screen");
}

static int
openScreenDevice (int *fd, int vt) {
  int opened = 0;
  char *name = vtName(screenName, vt);

  if (name) {
    int screen = openCharacterDevice(name, O_RDWR, VCS_MAJOR, 0X80|vt);

    if (screen != -1) {
      logMessage(LOG_CATEGORY(SCREEN_DRIVER),
                 "screen opened: %s: fd=%d", name, screen);

      *fd = screen;
      opened = 1;
    }

    free(name);
  }

  return opened;
}

static void
closeCurrentScreen (void) {
  if (screenMonitor) {
    asyncCancelRequest(screenMonitor);
    screenMonitor = NULL;
  }

  if (screenDescriptor != -1) {
    logMessage(LOG_CATEGORY(SCREEN_DRIVER),
               "closing screen: fd=%d", screenDescriptor);

    if (close(screenDescriptor) == -1) logSystemError("close[screen]");
    screenDescriptor = -1;
  }
}

static int
setCurrentScreen (unsigned char vt) {
  int screen;
  if (!openScreenDevice(&screen, vt)) return 0;

  closeCurrentConsole();
  closeCurrentUnicode();
  closeCurrentScreen();
  screenDescriptor = screen;
  virtualTerminalNumber = vt;

  isMonitorable = canMonitorScreen();
  logMessage(LOG_CATEGORY(SCREEN_DRIVER),
             "screen is monitorable: %s",
             (isMonitorable? "yes": "no"));

  screenMonitor = NULL;
  screenUpdated = 1;
  return 1;
}

static size_t
readScreenDevice (off_t offset, void *buffer, size_t size) {
  const ssize_t count = pread(screenDescriptor, buffer, size, offset);
  if (count != -1) return count;

  logSystemError("screen read");
  return 0;
}

static unsigned char *screenCacheBuffer;
static size_t screenCacheSize;

static size_t
readScreenCache (off_t offset, void *buffer, size_t size) {
  if (offset <= screenCacheSize) {
    size_t left = screenCacheSize - offset;

    if (size > left) size = left;
    memcpy(buffer, &screenCacheBuffer[offset], size);
    return size;
  } else {
    logMessage(LOG_ERR, "invalid screen cache offset: %u", (unsigned int)offset);
  }

  return 0;
}

static int
readScreenData (off_t offset, void *buffer, size_t size) {
  size_t count = (screenCacheBuffer? readScreenCache: readScreenDevice)(offset, buffer, size);
  if (count == size) return 1;

  logMessage(LOG_ERR,
             "truncated screen data: expected %zu bytes but read %zu",
             size, count);

  return 0;
}

static int
readScreenHeader (ScreenHeader *header) {
  return readScreenData(0, header, sizeof(*header));
}

static int
readScreenSize (ScreenSize *size) {
  return readScreenData(0, size, sizeof(*size));
}

static int
readScreenContent (off_t offset, uint16_t *buffer, size_t count) {
  count *= sizeof(*buffer);
  offset *= sizeof(*buffer);
  offset += sizeof(ScreenHeader);
  return readScreenData(offset, buffer, count);
}

static size_t
getScreenBufferSize (const ScreenSize *screenSize) {
  return (screenSize->columns * screenSize->rows * 2) + sizeof(ScreenHeader);
}

static size_t
refreshScreenBuffer (unsigned char **screenBuffer, size_t *screenSize) {
  if (!*screenBuffer) {
    ScreenHeader header;

    {
      size_t size = sizeof(header);
      size_t count = readScreenDevice(0, &header, size);
      if (!count) return 0;

      if (count < size) {
        logBytes(LOG_ERR, "truncated screen header", &header, count);
        return 0;
      }
    }

    {
      size_t size = getScreenBufferSize(&header.size);
      unsigned char *buffer = malloc(size);

      if (!buffer) {
        logMallocError();
        return 0;
      }

      *screenBuffer = buffer;
      *screenSize = size;
    }
  }

  while (1) {
    size_t count = readScreenDevice(0, *screenBuffer, *screenSize);
    if (!count) return 0;

    if (count < sizeof(ScreenHeader)) {
      logBytes(LOG_ERR, "truncated screen header", *screenBuffer, count);
      return 0;
    }

    {
      ScreenHeader *header = (void *)*screenBuffer;
      size_t size = getScreenBufferSize(&header->size);
      if (count >= size) return header->size.columns * header->size.rows;

      {
        unsigned char *buffer = realloc(*screenBuffer, size);

        if (!buffer) {
          logMallocError();
          return 0;
        }

        *screenBuffer = buffer;
        *screenSize = size;
      }
    }
  }
}

static struct unipair *screenFontMapTable = NULL;
static unsigned short screenFontMapSize = 0;
static unsigned short screenFontMapCount;

static int
setScreenFontMap (int force) {
  struct unimapdesc sfm;
  unsigned short size = force? 0: screenFontMapCount;

  if (!size) size = 0X100;

  while (1) {
    sfm.entry_ct = size;

    if (!(sfm.entries = malloc(sfm.entry_ct * sizeof(*sfm.entries)))) {
      logMallocError();
      return 0;
    }

    if (controlCurrentConsole(GIO_UNIMAP, &sfm) != -1) break;
    free(sfm.entries);

    if (errno != ENOMEM) {
      logSystemError("ioctl[GIO_UNIMAP]");
      return 0;
    }

    if (!(size <<= 1)) {
      logMessage(LOG_ERR, "screen font map too big");
      return 0;
    }
  }

  if (!force) {
    if (sfm.entry_ct == screenFontMapCount) {
      if (memcmp(sfm.entries, screenFontMapTable, sfm.entry_ct*sizeof(sfm.entries[0])) == 0) {
        free(sfm.entries);
        return 0;
      }
    }
  }

  if (screenFontMapTable) free(screenFontMapTable);
  screenFontMapTable = sfm.entries;
  screenFontMapCount = sfm.entry_ct;
  screenFontMapSize = size;
  logMessage(LOG_CATEGORY(SCREEN_DRIVER),
             "Font Map Size: %d", screenFontMapCount);

  if (logScreenFontMap) {
    for (unsigned int i=0; i<screenFontMapCount; i+=1) {
      const struct unipair *map = &screenFontMapTable[i];

      logMessage(LOG_CATEGORY(SCREEN_DRIVER),
                 "SFM[%03u]: U+%04X@%04X",
                 i, map->unicode, map->fontpos);
    }
  }

  return 1;
}

static int vgaCharacterCount;
static int vgaLargeTable;

static int
setVgaCharacterCount (int force) {
  int oldCount = vgaCharacterCount;

  {
    struct console_font_op cfo = {
      .width = UINT_MAX,
      .height = UINT_MAX,
      .op = KD_FONT_OP_GET
    };

    vgaCharacterCount = 0;
    {
      static unsigned char isNotImplemented = 0;

      if (!isNotImplemented) {
        if (controlCurrentConsole(KDFONTOP, &cfo) != -1) {
          logMessage(LOG_CATEGORY(SCREEN_DRIVER),
                     "Font Properties: %ux%u*%u",
                     cfo.width, cfo.height, cfo.charcount);
          vgaCharacterCount = cfo.charcount;
        } else {
          if (errno == ENOSYS) isNotImplemented = 1;

          if (errno != EINVAL) {
            logMessage(LOG_WARNING, "ioctl[KDFONTOP[GET]]: %s", strerror(errno));
          }
        }
      }
    }
  }

  if (!vgaCharacterCount) {
    unsigned int index;

    for (index=0; index<screenFontMapCount; ++index) {
      const struct unipair *map = &screenFontMapTable[index];

      if (vgaCharacterCount <= map->fontpos) vgaCharacterCount = map->fontpos + 1;
    }
  }

  vgaCharacterCount = ((vgaCharacterCount - 1) | 0XFF) + 1;
  vgaLargeTable = vgaCharacterCount > 0X100;

  if (!force) {
    if (vgaCharacterCount == oldCount) {
      return 0;
    }
  }

  logMessage(LOG_CATEGORY(SCREEN_DRIVER),
             "VGA Character Count: %d(%s)",
             vgaCharacterCount,
             vgaLargeTable? "large": "small");

  return 1;
}

static unsigned short highFontBit;
static unsigned short fontAttributesMask;
static unsigned short unshiftedAttributesMask;
static unsigned short shiftedAttributesMask;

static void
setAttributesMasks (unsigned short bit) {
  fontAttributesMask = bit;
  unshiftedAttributesMask = bit - 1;
  shiftedAttributesMask = ~unshiftedAttributesMask & ~bit;
  unshiftedAttributesMask &= 0XFF00;
  logMessage(LOG_CATEGORY(SCREEN_DRIVER),
             "Attributes Masks: Font:%04X Unshifted:%04X Shifted:%04X",
             fontAttributesMask, unshiftedAttributesMask, shiftedAttributesMask);
}

static int
determineAttributesMasks (void) {
  if (!vgaLargeTable) {
    setAttributesMasks(0);
  } else if (highFontBit) {
    setAttributesMasks(highFontBit);
  } else {
    {
      unsigned short mask;

      if (controlCurrentConsole(VT_GETHIFONTMASK, &mask) == -1) {
        if (errno != EINVAL) logSystemError("ioctl[VT_GETHIFONTMASK]");
      } else if (mask & 0XFF) {
        logMessage(LOG_ERR, "high font mask has bit set in low-order byte: %04X", mask);
      } else {
        setAttributesMasks(mask);
        return 1;
      }
    }

    {
      ScreenSize size;

      if (readScreenSize(&size)) {
        const size_t count = size.columns * size.rows;
        unsigned short buffer[count];

        if (readScreenContent(0, buffer, ARRAY_COUNT(buffer))) {
          unsigned int counts[0X10];
          memset(counts, 0, sizeof(counts));

          for (unsigned int index=0; index<count; index+=1) {
            counts[(buffer[index] & 0X0F00) >> 8] += 1;
          }

          setAttributesMasks((counts[0XE] > counts[0X7])? 0X0100: 0X0800);
          return 1;
        }
      }
    }
  }

  return 0;
}

static wchar_t translationTable[0X200];

static int
setTranslationTable (int force) {
  int mappingChanged = 0;
  int sfmChanged = setScreenFontMap(force);
  int vccChanged = (sfmChanged || force)? setVgaCharacterCount(force): 0;

  if (vccChanged || force) determineAttributesMasks();

  if (sfmChanged || vccChanged) {
    unsigned int count = ARRAY_COUNT(translationTable);

    for (unsigned int i=0; i<count; i+=1) {
      translationTable[i] = UNICODE_ROW_DIRECT | i;
    }

    {
      unsigned int screenFontMapIndex = screenFontMapCount;

      while (screenFontMapIndex > 0) {
        const struct unipair *sfm = &screenFontMapTable[--screenFontMapIndex];

        if (sfm->fontpos < count) {
          wchar_t *character = &translationTable[sfm->fontpos];
          if (*character == 0X20) continue;
          *character = sfm->unicode;
        }
      }
    }

    mappingChanged = 1;
  }

  if (mappingChanged) {
    logMessage(LOG_CATEGORY(SCREEN_DRIVER), "character mapping changed");
  }

  restartTimePeriod(&mappingRecalculationTimer);
  return mappingChanged;
}

static int
readScreenRow (int row, size_t size, ScreenCharacter *characters, int *offsets) {
  off_t offset = row * size;

  uint16_t vgaBuffer[size];
  if (!readScreenContent(offset, vgaBuffer, size)) return 0;

  uint32_t unicodeBuffer[size];
  const uint32_t *unicode = NULL;

  if (unicodeEnabled) {
    if (readUnicodeContent(offset, unicodeBuffer, size)) {
      unicode = unicodeBuffer;
    }
  }

  ScreenCharacter *character = characters;
  int column = 0;

  {
    const uint16_t *vga = vgaBuffer;
    const uint16_t *end = vga + size;
    int blanks = 0;

    while (vga < end) {
      wint_t wc;

      if (unicode) {
        wc = *unicode++;

        if ((blanks > 0) && (wc == WC_C(' '))) {
          blanks -= 1;
          wc = WEOF;
        } else {
          blanks = getCharacterWidth(wc) - 1;
        }
      } else {
        uint16_t position = *vga & 0XFF;
        if (*vga & fontAttributesMask) position |= 0X100;
        wc = convertCharacter(&translationTable[position]);
      }

      if (wc != WEOF) {
        if (character) {
          character->attributes = ((*vga & unshiftedAttributesMask) |
                                   ((*vga & shiftedAttributesMask) >> 1)) >> 8;

          character->text = wc;
          character += 1;
        }

        if (offsets) offsets[column] = vga - vgaBuffer;
        column += 1;
      }

      vga += 1;
    }
  }

  if (!unicode) {
    wint_t wc;

    while ((wc = convertCharacter(NULL)) != WEOF) {
      if (column < size) {
        if (character) {
          character->text = wc;
          character->attributes = SCR_COLOUR_DEFAULT;
          character += 1;
        }

        if (offsets) offsets[column] = size - 1;
        column += 1;
      }
    }
  }

  while (column < size) {
    if (character) {
      static const ScreenCharacter pad = {
        .text = WC_C(' '),
        .attributes = SCR_COLOUR_DEFAULT
      };

      *character++ = pad;
    }

    if (offsets) offsets[column] = size - 1;
    column += 1;
  }

  return 1;
}

static void
adjustCursorColumn (short *column, short row, short columns) {
  int offsets[columns];

  if (readScreenRow(row, columns, NULL, offsets)) {
    int first = 0;
    int last = columns - 1;

    while (first <= last) {
      int current = (first + last) / 2;

      if (offsets[current] < *column) {
        first = current + 1;
      } else {
        last = current - 1;
      }
    }

    if (first == columns) first -= 1;
    *column = first;
  }
}

#ifdef HAVE_LINUX_INPUT_H
#include <linux/input.h>

static const LinuxKeyCode *xtKeys;
static const LinuxKeyCode *atKeys;
static int atKeyPressed;
static int ps2KeyPressed;
#endif /* HAVE_LINUX_INPUT_H */

static UinputObject *uinputKeyboard = NULL;
static ReportListenerInstance *brailleDeviceOfflineListener;

static void
closeKeyboard (void) {
  if (uinputKeyboard) {
    destroyUinputObject(uinputKeyboard);
    uinputKeyboard = NULL;
  }
}

static int
openKeyboard (void) {
  if (!uinputKeyboard) {
    if (!(uinputKeyboard = newUinputKeyboard("Linux Screen Driver Keyboard"))) {
      return 0;
    }

    atexit(closeKeyboard);
  }

  return 1;
}

static void
resetKeyboard (void) {
  if (uinputKeyboard) {
    releasePressedKeys(uinputKeyboard);
  }
}

static int
processParameters_LinuxScreen (char **parameters) {
  fallbackText = parameters[PARM_FALLBACK_TEXT];

  {
    const char *names = parameters[PARM_CHARSET];

    if (!names || !*names) names = getLocaleCharset();
    if (!allocateCharsetEntries(names)) return 0;
  }

  highFontBit = 0;
  {
    const char *parameter = parameters[PARM_HIGH_FONT_BIT];

    if (parameter && *parameter) {
      int bit = 0;

      static const int minimum = 0;
      static const int maximum = 7;

      static const char *choices[] = {"auto", "vga", "fb", NULL};
      unsigned int choice;

      if (validateInteger(&bit, parameter, &minimum, &maximum)) {
        highFontBit = 1 << (bit + 8);
      } else if (!validateChoice(&choice, parameter, choices)) {
        logMessage(LOG_WARNING, "%s: %s", "invalid high font bit", parameter);
      } else if (choice) {
        static const unsigned short bits[] = {0X0800, 0X0100};
        highFontBit = bits[choice-1];
      }
    }
  }

  logScreenFontMap = 0;
  {
    const char *parameter = parameters[PARM_LOG_SCREEN_FONT_MAP];

    if (parameter && *parameter) {
      if (!validateYesNo(&logScreenFontMap, parameter)) {
        logMessage(LOG_WARNING, "%s: %s", "invalid log screen font map setting", parameter);
      }
    }
  }

  unicodeEnabled = 1;
  {
    const char *parameter = parameters[PARM_UNICODE];

    if (parameter && *parameter) {
      if (!validateYesNo(&unicodeEnabled, parameter)) {
        logMessage(LOG_WARNING, "%s: %s", "invalid direct unicode setting", parameter);
      }
    }
  }

  virtualTerminalNumber = 0;
  {
    const char *parameter = parameters[PARM_VIRTUAL_TERMINAL_NUMBER];

    if (parameter && *parameter) {
      static const int minimum = 0;
      static const int maximum = MAX_NR_CONSOLES;

      if (!validateInteger(&virtualTerminalNumber, parameter, &minimum, &maximum)) {
        logMessage(LOG_WARNING, "%s: %s", "invalid virtual terminal number", parameter);
      }
    }
  }

  return 1;
}

static void
releaseParameters_LinuxScreen (void) {
  deallocateCharsetEntries();
}

REPORT_LISTENER(lxBrailleDeviceOfflineListener) {
  resetKeyboard();
}

static int
construct_LinuxScreen (void) {
  mainConsoleDescriptor = -1;
  screenDescriptor = -1;
  consoleDescriptor = -1;
  unicodeDescriptor = -1;

  screenUpdated = 0;
  screenCacheBuffer = NULL;
  screenCacheSize = 0;

  unicodeCacheBuffer = NULL;
  unicodeCacheSize = 0;
  unicodeCacheUsed = 0;

  currentConsoleNumber = 0;
  inTextMode = 1;
  startTimePeriod(&mappingRecalculationTimer, 4000);

  brailleDeviceOfflineListener = NULL;

#ifdef HAVE_LINUX_INPUT_H
  xtKeys = linuxKeyMap_xt00;
  atKeys = linuxKeyMap_at00;
  atKeyPressed = 1;
  ps2KeyPressed = 1;
#endif /* HAVE_LINUX_INPUT_H */

  if (setScreenName()) {
    if (setConsoleName()) {
      if (unicodeEnabled) {
        if (!setUnicodeName()) {
          unicodeEnabled = 0;
        }
      }

      if (openMainConsole()) {
        if (setCurrentScreen(virtualTerminalNumber)) {
          openKeyboard();
          brailleDeviceOfflineListener = registerReportListener(REPORT_BRAILLE_DEVICE_OFFLINE, lxBrailleDeviceOfflineListener, NULL);
          return 1;
        }
      }
    }
  }

  closeCurrentConsole();
  closeCurrentScreen();
  closeMainConsole();
  return 0;
}

static void
destruct_LinuxScreen (void) {
  if (brailleDeviceOfflineListener) {
    unregisterReportListener(brailleDeviceOfflineListener);
    brailleDeviceOfflineListener = NULL;
  }

  closeCurrentConsole();
  consoleName = NULL;

  closeCurrentScreen();
  screenName = NULL;

  if (screenFontMapTable) {
    free(screenFontMapTable);
    screenFontMapTable = NULL;
  }
  screenFontMapSize = 0;
  screenFontMapCount = 0;

  if (screenCacheBuffer) {
    free(screenCacheBuffer);
    screenCacheBuffer = NULL;
  }
  screenCacheSize = 0;

  if (unicodeCacheBuffer) {
    free(unicodeCacheBuffer);
    unicodeCacheBuffer = NULL;
  }
  unicodeCacheSize = 0;
  unicodeCacheUsed = 0;

  closeMainConsole();
}

ASYNC_MONITOR_CALLBACK(lxScreenUpdated) {
  asyncDiscardHandle(screenMonitor);
  screenMonitor = NULL;

  screenUpdated = 1;
  mainScreenUpdated();

  return 0;
}

static int
poll_LinuxScreen (void) {
  int poll = !isMonitorable? 1:
             screenMonitor? 0:
             !asyncMonitorFileAlert(&screenMonitor, screenDescriptor,
                                    lxScreenUpdated, NULL);

  if (poll) screenUpdated = 1;
  return poll;
}

static int
getConsoleState (struct vt_stat *state) {
  if (controlMainConsole(VT_GETSTATE, state) != -1) return 1;
  logSystemError("ioctl[VT_GETSTATE]");
  problemText = gettext("can't get console state");
  return 0;
}

static int
isUnusedConsole (int vt) {
  int isUnused = 1;
  unsigned char *buffer = NULL;
  size_t size = 0;

  if (refreshScreenBuffer(&buffer, &size)) {
    const ScreenHeader *header = (void *)buffer;
    const uint16_t *from = (void *)(buffer + sizeof(*header));
    const uint16_t *to = (void *)(buffer + getScreenBufferSize(&header->size));

    if (from < to) {
      const uint16_t vga = *from++;

      while (from < to) {
        if (*from++ != vga) {
          isUnused = 0;
          break;
        }
      }
    }
  }

  if (buffer) free(buffer);
  return isUnused;
}

static int
canOpenCurrentConsole (void) {
  typedef uint16_t OpenableConsoles;
  static OpenableConsoles openableConsoles = 0;

  struct vt_stat state;
  if (!getConsoleState(&state)) return 0;

  int console = virtualTerminalNumber;
  if (!console) console = state.v_active;
  OpenableConsoles bit = 1 << console;

  if (bit && !(openableConsoles & bit)) {
    if (console != MAIN_CONSOLE) {
      if (!(state.v_state & bit)) {
        if (isUnusedConsole(console)) {
          return 0;
        }
      }
    }

    openableConsoles |= bit;
  }

  return 1;
}

static int
getConsoleNumber (void) {
  int console;

  if (virtualTerminalNumber) {
    console = virtualTerminalNumber;
  } else {
    struct vt_stat state;
    if (!getConsoleState(&state)) return NO_CONSOLE;
    console = state.v_active;
  }

  if (console != currentConsoleNumber) {
    closeCurrentConsole();
  }

  if (consoleDescriptor == -1) {
    if (!canOpenCurrentConsole()) {
      problemText = gettext("console not in use");
    } else if (!openCurrentConsole()) {
      problemText = gettext("can't open console");
    }

    setTranslationTable(1);
  }

  return console;
}

static int
testTextMode (void) {
  if (problemText) return 0;
  int mode;

  if (controlCurrentConsole(KDGETMODE, &mode) == -1) {
    logSystemError("ioctl[KDGETMODE]");
  } else if (mode == KD_TEXT) {
    if (afterTimePeriod(&mappingRecalculationTimer, NULL)) setTranslationTable(0);
    return 1;
  }

  problemText = gettext("screen not in text mode");
  return 0;
}

static int
refreshCache (void) {
  size_t size = refreshScreenBuffer(&screenCacheBuffer, &screenCacheSize);
  if (!size) return 0;

  if (unicodeEnabled) {
    if (!refreshUnicodeCache(size)) {
      return 0;
    }
  }

  return 1;
}

static int
refresh_LinuxScreen (void) {
  if (screenUpdated) {
    while (1) {
      problemText = NULL;

      if (!refreshCache()) {
        problemText = gettext("can't read screen content");
        goto done;
      }

      {
        int consoleNumber = getConsoleNumber();
        if (consoleNumber == currentConsoleNumber) break;

        logMessage(LOG_CATEGORY(SCREEN_DRIVER),
                   "console number changed: %u -> %u",
                   currentConsoleNumber, consoleNumber);

        currentConsoleNumber = consoleNumber;
      }
    }

    inTextMode = testTextMode();
    screenUpdated = 0;

  done:
    if (problemText) {
      if (*fallbackText) {
        problemText = gettext(fallbackText);
      }
    }
  }

  return 1;
}

static int
getScreenDescription (ScreenDescription *description) {
  ScreenHeader header;

  if (readScreenHeader(&header)) {
    description->cols = header.size.columns;
    description->rows = header.size.rows;

    description->posx = header.location.column;
    description->posy = header.location.row;

    adjustCursorColumn(&description->posx, description->posy, description->cols);
    return 1;
  }

  problemText = gettext("can't read screen header");
  return 0;
}

static void
describe_LinuxScreen (ScreenDescription *description) {
  if (!screenCacheBuffer) {
    problemText = NULL;
    currentConsoleNumber = getConsoleNumber();
    inTextMode = testTextMode();
  }

  if ((description->number = currentConsoleNumber)) {
    if (inTextMode) {
      if (getScreenDescription(description)) {
      }
    }
  }

  if ((description->unreadable = problemText)) {
    description->cols = strlen(problemText);
    description->rows = 1;

    description->posx = 0;
    description->posy = 0;
  }
}

static int
readCharacters_LinuxScreen (const ScreenBox *box, ScreenCharacter *buffer) {
  ScreenSize size;

  if (readScreenSize(&size)) {
    if (validateScreenBox(box, size.columns, size.rows)) {
      if (problemText) {
        setScreenMessage(box, buffer, problemText);
        return 1;
      }

      for (unsigned int row=0; row<box->height; row+=1) {
        ScreenCharacter characters[size.columns];
        if (!readScreenRow(box->top+row, size.columns, characters, NULL)) return 0;

        memcpy(buffer, &characters[box->left],
               (box->width * sizeof(characters[0])));

        buffer += box->width;
      }

      return 1;
    }
  }

  return 0;
}

static int
getCapsLockState (void) {
  char leds;
  if (controlCurrentConsole(KDGETLED, &leds) != -1)
    if (leds & LED_CAP)
      return 1;
  return 0;
}

static inline int
hasModUpper (ScreenKey key) {
  return (key & SCR_KEY_UPPER) && !getCapsLockState();
}

static inline int
hasModShift (ScreenKey key) {
  return !!(key & SCR_KEY_SHIFT);
}

static inline int
hasModControl (ScreenKey key) {
  return !!(key & SCR_KEY_CONTROL);
}

static inline int
hasModAltLeft (ScreenKey key) {
  return !!(key & SCR_KEY_ALT_LEFT);
}

static inline int
hasModAltRight (ScreenKey key) {
  return !!(key & SCR_KEY_ALT_RIGHT);
}

static inline int
hasModGui (ScreenKey key) {
  return !!(key & SCR_KEY_GUI);
}

static int
injectKeyEvent (int key, int press) {
  if (!openKeyboard()) return 0;
  return writeKeyEvent(uinputKeyboard, key, press);
}

static int
insertUinput (
  LinuxKeyCode code,
  int modUpper, int modShift, int modControl,
  int modAltLeft, int modAltRight
) {
#ifdef HAVE_LINUX_INPUT_H
  if (code) {
#define KEY_EVENT(KEY, PRESS) { if (!injectKeyEvent((KEY), (PRESS))) return 0; }
    if (modUpper) {
      KEY_EVENT(KEY_CAPSLOCK, 1);
      KEY_EVENT(KEY_CAPSLOCK, 0);
    }

    if (modShift) KEY_EVENT(KEY_LEFTSHIFT, 1);
    if (modControl) KEY_EVENT(KEY_LEFTCTRL, 1);
    if (modAltLeft) KEY_EVENT(KEY_LEFTALT, 1);
    if (modAltRight) KEY_EVENT(KEY_RIGHTALT, 1);

    KEY_EVENT(code, 1);
    KEY_EVENT(code, 0);

    if (modAltRight) KEY_EVENT(KEY_RIGHTALT, 0);
    if (modAltLeft) KEY_EVENT(KEY_LEFTALT, 0);
    if (modControl) KEY_EVENT(KEY_LEFTCTRL, 0);
    if (modShift) KEY_EVENT(KEY_LEFTSHIFT, 0);

    if (modUpper) {
      KEY_EVENT(KEY_CAPSLOCK, 1);
      KEY_EVENT(KEY_CAPSLOCK, 0);
    }
#undef KEY_EVENT

    return 1;
  }
#endif /* HAVE_LINUX_INPUT_H */

  return 0;
}

static int
insertByte (char byte) {
  if (controlCurrentConsole(TIOCSTI, &byte) != -1) return 1;
  logSystemError("ioctl[TIOCSTI]");
  return 0;
}

static int
insertBytes (const char *byte, size_t count) {
  while (count) {
    if (!insertByte(*byte++)) return 0;
    count -= 1;
  }
  return 1;
}

static int
insertXlate (wchar_t character) {
  char bytes[MB_LEN_MAX];
  size_t count;
  CharacterConversionResult result = convertWcharToChars(character, bytes, sizeof(bytes), &count);

  if (result != CONV_OK) {
    uint32_t value = character;

    logMessage(LOG_WARNING, "character not supported in xlate mode: 0X%02"PRIX32, value);
    return 0;
  }

  return insertBytes(bytes, count);
}

static int
insertUnicode (wchar_t character) {
  {
    Utf8Buffer utf8;
    size_t utfs = convertWcharToUtf8(character, utf8);

    if (utfs) return insertBytes(utf8, utfs);
  }

  {
    uint32_t value = character;

    logMessage(LOG_WARNING, "character not supported in unicode keyboard mode: 0X%02"PRIX32, value);
  }

  return 0;
}

static int
insertCode (ScreenKey key, int raw) {
  const LinuxKeyCode *map;
  unsigned char code;
  unsigned char escape;

  setScreenKeyModifiers(&key, SCR_KEY_SHIFT | SCR_KEY_CONTROL);

#define KEY_TO_XT(KEY, ESCAPE, CODE) \
  case (KEY): \
  map = linuxKeyMap_xt ## ESCAPE; \
  code = XT_KEY_ ## ESCAPE ## _ ## CODE; \
  escape = XT_MOD_ ## ESCAPE; \
  break;

  switch (key & SCR_KEY_CHAR_MASK) {
    KEY_TO_XT(SCR_KEY_ESCAPE, 00, Escape)
    KEY_TO_XT(SCR_KEY_F1, 00, F1)
    KEY_TO_XT(SCR_KEY_F2, 00, F2)
    KEY_TO_XT(SCR_KEY_F3, 00, F3)
    KEY_TO_XT(SCR_KEY_F4, 00, F4)
    KEY_TO_XT(SCR_KEY_F5, 00, F5)
    KEY_TO_XT(SCR_KEY_F6, 00, F6)
    KEY_TO_XT(SCR_KEY_F7, 00, F7)
    KEY_TO_XT(SCR_KEY_F8, 00, F8)
    KEY_TO_XT(SCR_KEY_F9, 00, F9)
    KEY_TO_XT(SCR_KEY_F10, 00, F10)
    KEY_TO_XT(SCR_KEY_F11, 00, F11)
    KEY_TO_XT(SCR_KEY_F12, 00, F12)

    KEY_TO_XT(SCR_KEY_F13, 00, F13)
    KEY_TO_XT(SCR_KEY_F14, 00, F14)
    KEY_TO_XT(SCR_KEY_F15, 00, F15)
    KEY_TO_XT(SCR_KEY_F16, 00, F16)
    KEY_TO_XT(SCR_KEY_F17, 00, F17)
    KEY_TO_XT(SCR_KEY_F18, 00, F18)
    KEY_TO_XT(SCR_KEY_F19, 00, F19)
    KEY_TO_XT(SCR_KEY_F20, 00, F20)
    KEY_TO_XT(SCR_KEY_F21, 00, F21)
    KEY_TO_XT(SCR_KEY_F22, 00, F22)
    KEY_TO_XT(SCR_KEY_F23, 00, F23)
    KEY_TO_XT(SCR_KEY_F24, 00, F24)

    KEY_TO_XT('`', 00, Grave)
    KEY_TO_XT('1', 00, 1)
    KEY_TO_XT('2', 00, 2)
    KEY_TO_XT('3', 00, 3)
    KEY_TO_XT('4', 00, 4)
    KEY_TO_XT('5', 00, 5)
    KEY_TO_XT('6', 00, 6)
    KEY_TO_XT('7', 00, 7)
    KEY_TO_XT('8', 00, 8)
    KEY_TO_XT('9', 00, 9)
    KEY_TO_XT('0', 00, 0)
    KEY_TO_XT('-', 00, Minus)
    KEY_TO_XT('=', 00, Equal)
    KEY_TO_XT(SCR_KEY_BACKSPACE, 00, Backspace)

    KEY_TO_XT(SCR_KEY_TAB, 00, Tab)
    KEY_TO_XT('q', 00, Q)
    KEY_TO_XT('w', 00, W)
    KEY_TO_XT('e', 00, E)
    KEY_TO_XT('r', 00, R)
    KEY_TO_XT('t', 00, T)
    KEY_TO_XT('y', 00, Y)
    KEY_TO_XT('u', 00, U)
    KEY_TO_XT('i', 00, I)
    KEY_TO_XT('o', 00, O)
    KEY_TO_XT('p', 00, P)
    KEY_TO_XT('[', 00, LeftBracket)
    KEY_TO_XT(']', 00, RightBracket)
    KEY_TO_XT('\\', 00, Backslash)

    KEY_TO_XT('a', 00, A)
    KEY_TO_XT('s', 00, S)
    KEY_TO_XT('d', 00, D)
    KEY_TO_XT('f', 00, F)
    KEY_TO_XT('g', 00, G)
    KEY_TO_XT('h', 00, H)
    KEY_TO_XT('j', 00, J)
    KEY_TO_XT('k', 00, K)
    KEY_TO_XT('l', 00, L)
    KEY_TO_XT(';', 00, Semicolon)
    KEY_TO_XT('\'', 00, Apostrophe)
    KEY_TO_XT(SCR_KEY_ENTER, 00, Enter)

    KEY_TO_XT('z', 00, Z)
    KEY_TO_XT('x', 00, X)
    KEY_TO_XT('c', 00, C)
    KEY_TO_XT('v', 00, V)
    KEY_TO_XT('b', 00, B)
    KEY_TO_XT('n', 00, N)
    KEY_TO_XT('m', 00, M)
    KEY_TO_XT(',', 00, Comma)
    KEY_TO_XT('.', 00, Period)
    KEY_TO_XT('/', 00, Slash)

    KEY_TO_XT(' ', 00, Space)

    KEY_TO_XT(SCR_KEY_INSERT, E0, Insert)
    KEY_TO_XT(SCR_KEY_DELETE, E0, Delete)
    KEY_TO_XT(SCR_KEY_HOME, E0, Home)
    KEY_TO_XT(SCR_KEY_END, E0, End)
    KEY_TO_XT(SCR_KEY_PAGE_UP, E0, PageUp)
    KEY_TO_XT(SCR_KEY_PAGE_DOWN, E0, PageDown)

    KEY_TO_XT(SCR_KEY_CURSOR_UP, E0, ArrowUp)
    KEY_TO_XT(SCR_KEY_CURSOR_LEFT, E0, ArrowLeft)
    KEY_TO_XT(SCR_KEY_CURSOR_DOWN, E0, ArrowDown)
    KEY_TO_XT(SCR_KEY_CURSOR_RIGHT, E0, ArrowRight)

    default:
      logMessage(LOG_WARNING, "key not supported in raw keyboard mode: %04X", key);
      return 0;
  }
#undef KEY_TO_XT

  {
    const int modUpper = hasModUpper(key);
    const int modShift = hasModShift(key);
    const int modControl = hasModControl(key);
    const int modAltLeft = hasModAltLeft(key);
    const int modAltRight = hasModAltRight(key);
    const int modGui = hasModGui(key);

    if (raw) {
      char codes[22];
      unsigned int count = 0;

      if (modUpper) {
        codes[count++] = XT_KEY_00_CapsLock;
        codes[count++] = XT_KEY_00_CapsLock | XT_BIT_RELEASE;
      }

      if (modShift) codes[count++] = XT_KEY_00_LeftShift;
      if (modControl) codes[count++] = XT_KEY_00_LeftControl;
      if (modAltLeft) codes[count++] = XT_KEY_00_LeftAlt;

      if (modAltRight) {
        codes[count++] = XT_MOD_E0;
        codes[count++] = XT_KEY_E0_RightAlt;
      }

      if (modGui) {
        codes[count++] = XT_MOD_E0;
        codes[count++] = XT_KEY_E0_LeftGUI;
      }

      if (escape) codes[count++] = escape;
      codes[count++] = code;

      if (escape) codes[count++] = escape;
      codes[count++] = code | XT_BIT_RELEASE;

      if (modGui) {
        codes[count++] = XT_MOD_E0;
        codes[count++] = XT_KEY_E0_LeftGUI | XT_BIT_RELEASE;
      }

      if (modAltRight) {
        codes[count++] = XT_MOD_E0;
        codes[count++] = XT_KEY_E0_RightAlt | XT_BIT_RELEASE;
      }

      if (modAltLeft) codes[count++] = XT_KEY_00_LeftAlt | XT_BIT_RELEASE;
      if (modControl) codes[count++] = XT_KEY_00_LeftControl | XT_BIT_RELEASE;
      if (modShift) codes[count++] = XT_KEY_00_LeftShift | XT_BIT_RELEASE;

      if (modUpper) {
        codes[count++] = XT_KEY_00_CapsLock;
        codes[count++] = XT_KEY_00_CapsLock | XT_BIT_RELEASE;
      }

      return insertBytes(codes, count);
    } else {
      LinuxKeyCode mapped = map[code];

      if (!mapped) {
        logMessage(LOG_WARNING, "key not supported in medium raw keyboard mode: %04X", key);
        return 0;
      }

      return insertUinput(mapped, modUpper, modShift, modControl, modAltLeft, modAltRight);
    }
  }
}

static int
insertTranslated (ScreenKey key, int (*insertCharacter)(wchar_t character)) {
  wchar_t buffer[2];
  const wchar_t *sequence;
  const wchar_t *end;

  setScreenKeyModifiers(&key, 0);

  if (isSpecialKey(key)) {
    switch (key) {
      case SCR_KEY_ENTER:
        sequence = WS_C("\r");
        break;

      case SCR_KEY_TAB:
        sequence = WS_C("\t");
        break;

      case SCR_KEY_BACKSPACE:
        sequence = WS_C("\x7f");
        break;

      case SCR_KEY_ESCAPE:
        sequence = WS_C("\x1b");
        break;

      case SCR_KEY_CURSOR_LEFT:
        sequence = WS_C("\x1b[D");
        break;

      case SCR_KEY_CURSOR_RIGHT:
        sequence = WS_C("\x1b[C");
        break;

      case SCR_KEY_CURSOR_UP:
        sequence = WS_C("\x1b[A");
        break;

      case SCR_KEY_CURSOR_DOWN:
        sequence = WS_C("\x1b[B");
        break;

      case SCR_KEY_PAGE_UP:
        sequence = WS_C("\x1b[5~");
        break;

      case SCR_KEY_PAGE_DOWN:
        sequence = WS_C("\x1b[6~");
        break;

      case SCR_KEY_HOME:
        sequence = WS_C("\x1b[1~");
        break;

      case SCR_KEY_END:
        sequence = WS_C("\x1b[4~");
        break;

      case SCR_KEY_INSERT:
        sequence = WS_C("\x1b[2~");
        break;

      case SCR_KEY_DELETE:
        sequence = WS_C("\x1b[3~");
        break;

      case SCR_KEY_F1:
        sequence = WS_C("\x1b[[A");
        break;

      case SCR_KEY_F2:
        sequence = WS_C("\x1b[[B");
        break;

      case SCR_KEY_F3:
        sequence = WS_C("\x1b[[C");
        break;

      case SCR_KEY_F4:
        sequence = WS_C("\x1b[[D");
        break;

      case SCR_KEY_F5:
        sequence = WS_C("\x1b[[E");
        break;

      case SCR_KEY_F6:
        sequence = WS_C("\x1b[17~");
        break;

      case SCR_KEY_F7:
        sequence = WS_C("\x1b[18~");
        break;

      case SCR_KEY_F8:
        sequence = WS_C("\x1b[19~");
        break;

      case SCR_KEY_F9:
        sequence = WS_C("\x1b[20~");
        break;

      case SCR_KEY_F10:
        sequence = WS_C("\x1b[21~");
        break;

      case SCR_KEY_F11:
        sequence = WS_C("\x1b[23~");
        break;

      case SCR_KEY_F12:
        sequence = WS_C("\x1b[24~");
        break;

      case SCR_KEY_F13:
        sequence = WS_C("\x1b[25~");
        break;

      case SCR_KEY_F14:
        sequence = WS_C("\x1b[26~");
        break;

      case SCR_KEY_F15:
        sequence = WS_C("\x1b[28~");
        break;

      case SCR_KEY_F16:
        sequence = WS_C("\x1b[29~");
        break;

      case SCR_KEY_F17:
        sequence = WS_C("\x1b[31~");
        break;

      case SCR_KEY_F18:
        sequence = WS_C("\x1b[32~");
        break;

      case SCR_KEY_F19:
        sequence = WS_C("\x1b[33~");
        break;

      case SCR_KEY_F20:
        sequence = WS_C("\x1b[34~");
        break;

      default:
        if (insertCode(key, 0)) return 1;
        logMessage(LOG_WARNING, "key not supported in xlate keyboard mode: %04X", key);
        return 0;
    }

    end = sequence + wcslen(sequence);
  } else {
    wchar_t *character = buffer + ARRAY_COUNT(buffer);

    end = character;
    *--character = key & SCR_KEY_CHAR_MASK;

    if (hasModAltLeft(key)) {
      int meta;

      if (controlCurrentConsole(KDGKBMETA, &meta) == -1) return 0;

      switch (meta) {
        case K_ESCPREFIX:
          *--character = ESC;
          break;

        case K_METABIT:
          if (*character >= 0X80) {
            logMessage(LOG_WARNING, "can't add meta bit to character: U+%04X", *character);
            return 0;
          }

          *character |= 0X80;
          break;

        default:
          logMessage(LOG_WARNING, "unsupported keyboard meta mode: %d", meta);
          return 0;
      }
    }

    sequence = character;
  }

  while (sequence != end) {
    if (!insertCharacter(*sequence)) return 0;
    sequence += 1;
  }

  return 1;
}

static int
insertKey_LinuxScreen (ScreenKey key) {
  int ok = 0;
  int mode;

  if (controlCurrentConsole(KDGKBMODE, &mode) != -1) {
    switch (mode) {
      case K_RAW:
        if (insertCode(key, 1)) ok = 1;
        break;

      case K_MEDIUMRAW:
        if (insertCode(key, 0)) ok = 1;
        break;

      case K_XLATE:
        if (insertTranslated(key, insertXlate)) ok = 1;
        break;

      case K_UNICODE:
        if (insertTranslated(key, insertUnicode)) ok = 1;
        break;

#ifdef K_OFF
      case K_OFF:
        ok = 1;
        break;
#endif /* K_OFF */

      default:
        logMessage(LOG_WARNING, "unsupported keyboard mode: %d", mode);
        break;
    }
  } else {
    logSystemError("ioctl[KDGKBMODE]");
  }

  return ok;
}

typedef struct {
  char subcode;
  struct tiocl_selection selection;
} PACKED RegionSelectionArgument;

static int
selectRegion (RegionSelectionArgument *argument) {
  if (controlCurrentConsole(TIOCLINUX, argument) != -1) return 1;
  if (errno != EINVAL) logSystemError("ioctl[TIOCLINUX]");
  return 0;
}

static int
highlightRegion_LinuxScreen (int left, int right, int top, int bottom) {
  RegionSelectionArgument argument = {
    .subcode = TIOCL_SETSEL,

    .selection = {
      .xs = left + 1,
      .ys = top + 1,
      .xe = right + 1,
      .ye = bottom + 1,
      .sel_mode = TIOCL_SELCHAR
    }
  };

  return selectRegion(&argument);
}

static int
unhighlightRegion_LinuxScreen (void) {
  RegionSelectionArgument argument = {
    .subcode = TIOCL_SETSEL,

    .selection = {
      .xs = 0,
      .ys = 0,
      .xe = 0,
      .ye = 0,
      .sel_mode = TIOCL_SELCLEAR
    }
  };

  return selectRegion(&argument);
}

static int
validateVt (int vt) {
  if ((vt >= 1) && (vt <= MAX_NR_CONSOLES)) return 1;
  logMessage(LOG_WARNING, "virtual terminal out of range: %d", vt);
  return 0;
}

static int
selectVirtualTerminal_LinuxScreen (int vt) {
  if (vt == virtualTerminalNumber) return 1;
  if (vt && !validateVt(vt)) return 0;
  return setCurrentScreen(vt);
}

static int
switchVirtualTerminal_LinuxScreen (int vt) {
  if (validateVt(vt)) {
    if (selectVirtualTerminal_LinuxScreen(0)) {
      if (controlMainConsole(VT_ACTIVATE, (void *)(intptr_t)vt) != -1) {
        logMessage(LOG_CATEGORY(SCREEN_DRIVER),
                   "switched to virtual tertminal %d", vt);
        return 1;
      } else {
        logSystemError("ioctl[VT_ACTIVATE]");
      }
    }
  }
  return 0;
}

static int
currentVirtualTerminal_LinuxScreen (void) {
  return currentConsoleNumber;
}

static int
userVirtualTerminal_LinuxScreen (int number) {
  return MAX_NR_CONSOLES + 1 + number;
}

static int
handleCommand_LinuxScreen (int command) {
  int blk = command & BRL_MSK_BLK;
  int arg UNUSED = command & BRL_MSK_ARG;
  int cmd = blk | arg;

  switch (cmd) {
    default:
#ifdef HAVE_LINUX_INPUT_H
      switch (blk) {
        case BRL_CMD_BLK(PASSXT):
          if (command & BRL_FLG_KBD_RELEASE) arg |= XT_BIT_RELEASE;

          {
            int handled = 0;

            if (command & BRL_FLG_KBD_EMUL0) {
              xtKeys = linuxKeyMap_xtE0;
            } else if (arg == XT_MOD_E0) {
              xtKeys = linuxKeyMap_xtE0;
              handled = 1;
	    } else if (command & BRL_FLG_KBD_EMUL1) {
              xtKeys = linuxKeyMap_xtE1;
            } else if (arg == XT_MOD_E1) {
              xtKeys = linuxKeyMap_xtE1;
              handled = 1;
            }

            if (handled) return 1;
          }

	  {
            LinuxKeyCode key = xtKeys[arg & ~XT_BIT_RELEASE];
            int press = !(arg & XT_BIT_RELEASE);

            xtKeys = linuxKeyMap_xt00;

            if (key) return injectKeyEvent(key, press);
	  }
          break;

	case BRL_CMD_BLK(PASSAT):
          {
            int handled = 0;

            if (command & BRL_FLG_KBD_RELEASE) {
              atKeyPressed = 0;
            } else if (arg == AT_MOD_RELEASE) {
              atKeyPressed = 0;
              handled = 1;
            }

            if (command & BRL_FLG_KBD_EMUL0) {
              atKeys = linuxKeyMap_atE0;
            } else if (arg == AT_MOD_E0) {
              atKeys = linuxKeyMap_atE0;
              handled = 1;
	    } else if (command & BRL_FLG_KBD_EMUL1) {
              atKeys = linuxKeyMap_atE1;
            } else if (arg == AT_MOD_E1) {
              atKeys = linuxKeyMap_atE1;
              handled = 1;
            }

            if (handled) return 1;
          }

          {
            LinuxKeyCode key = atKeys[arg];
            int press = atKeyPressed;

            atKeys = linuxKeyMap_at00;
            atKeyPressed = 1;

            if (key) return injectKeyEvent(key, press);
          }
          break;

	case BRL_CMD_BLK(PASSPS2):
          {
            int handled = 0;

            if (command & BRL_FLG_KBD_RELEASE) {
              ps2KeyPressed = 0;
            } else if (arg == PS2_MOD_RELEASE) {
              ps2KeyPressed = 0;
              handled = 1;
            }

            if (handled) return 1;
          }

          {
            LinuxKeyCode key = linuxKeyMap_ps2[arg];
            int press = ps2KeyPressed;

            ps2KeyPressed = 1;

            if (key) return injectKeyEvent(key, press);
          }
          break;

        default:
          break;
      }
#endif /* HAVE_LINUX_INPUT_H */
      break;
  }

  return 0;
}

static void
scr_initialize (MainScreen *main) {
  initializeRealScreen(main);
  gpmIncludeScreenHandlers(main);

  main->base.poll = poll_LinuxScreen;
  main->base.refresh = refresh_LinuxScreen;
  main->base.describe = describe_LinuxScreen;
  main->base.readCharacters = readCharacters_LinuxScreen;
  main->base.insertKey = insertKey_LinuxScreen;
  main->base.highlightRegion = highlightRegion_LinuxScreen;
  main->base.unhighlightRegion = unhighlightRegion_LinuxScreen;
  main->base.selectVirtualTerminal = selectVirtualTerminal_LinuxScreen;
  main->base.switchVirtualTerminal = switchVirtualTerminal_LinuxScreen;
  main->base.currentVirtualTerminal = currentVirtualTerminal_LinuxScreen;
  main->base.handleCommand = handleCommand_LinuxScreen;

  main->processParameters = processParameters_LinuxScreen;
  main->releaseParameters = releaseParameters_LinuxScreen;
  main->construct = construct_LinuxScreen;
  main->destruct = destruct_LinuxScreen;
  main->userVirtualTerminal = userVirtualTerminal_LinuxScreen;
}
