/*
 * 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-2022 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 <strings.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>

#ifdef __MINGW32__
#include <ws2tcpip.h>
#include "system_windows.h"
#else /* __MINGW32__ */
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#endif /* __MINGW32__ */
#include "get_select.h"

#if !defined(AF_LOCAL) && defined(AF_UNIX)
#define AF_LOCAL AF_UNIX
#endif /* !defined(AF_LOCAL) && defined(AF_UNIX) */
 
#if !defined(PF_LOCAL) && defined(PF_UNIX)
#define PF_LOCAL PF_UNIX
#endif /* !defined(PF_LOCAL) && defined(PF_UNIX) */
 
#ifdef WINDOWS
#undef AF_LOCAL
#endif /* WINDOWS */

#ifdef __MINGW32__
#define close(fd) CloseHandle((HANDLE)(fd))
#define LogSocketError(msg) logWindowsSocketError(msg)
#else /* __MINGW32__ */
#define LogSocketError(msg) logSystemError(msg)
#endif /* __MINGW32__ */

#include "log.h"
#include "io_misc.h"
#include "parse.h"
#include "async_wait.h"
#include "charset.h"
#include "cmd.h"

#define BRL_STATUS_FIELDS sfGeneric
#define BRL_HAVE_STATUS_CELLS
#include "brl_driver.h"
#include "braille.h"

static int fileDescriptor = -1;

#define INPUT_SIZE 0X200
static char inputBuffer[INPUT_SIZE];
static size_t inputLength;
static size_t inputStart;
static int inputEnd;
static int inputCarriageReturn;
static const char *inputDelimiters = " ";

#define OUTPUT_SIZE 0X200
static char outputBuffer[OUTPUT_SIZE];
static size_t outputLength;

typedef struct {
  const CommandEntry *entry;
  unsigned int count;
} CommandDescriptor;
static CommandDescriptor *commandDescriptors = NULL;
static const size_t commandSize = sizeof(*commandDescriptors);
static size_t commandCount;

static int brailleColumns;
static int brailleRows;
static int brailleCount;
static unsigned char *brailleCells = NULL;
static wchar_t *textCharacters = NULL;

static int statusColumns;
static int statusRows;
static int statusCount;
static unsigned char *statusCells = NULL;
static unsigned char genericCells[GSC_COUNT];

typedef struct {
#ifdef AF_LOCAL
  int (*getLocalConnection) (const struct sockaddr_un *address);
#endif /* AF_LOCAL */

#ifdef __MINGW32__
  int (*getNamedPipeConnection) (const char *path);
#endif /* __MINGW32__ */

  int (*getInetConnection) (const struct sockaddr_in *address);
} ModeEntry;
static const ModeEntry *mode;

typedef struct {
  int (*read) (int descriptor, void *buffer, int size);
} OperationsEntry;
static const OperationsEntry *operations;

static int
readNetworkSocket (int descriptor, void *buffer, int size) {
  if (awaitSocketInput(descriptor, 0)) {
    int count = recv(descriptor, buffer, size, 0);
    if (count != -1) return count;
    LogSocketError("recv");
  }

  return -1;
}

static const OperationsEntry socketOperationsEntry = {
  readNetworkSocket
};

static char *
formatSocketAddress (const struct sockaddr *address) {
  char *string;

  switch (address->sa_family) {
#ifdef AF_LOCAL
    case AF_LOCAL: {
      const struct sockaddr_un *localAddress = (const struct sockaddr_un *)address;

      string = strdup(localAddress->sun_path);
      break;
    }
#endif /* AF_LOCAL */

    case AF_INET: {
      const struct sockaddr_in *inetAddress = (const struct sockaddr_in *)address;
      const char *host = inet_ntoa(inetAddress->sin_addr);
      unsigned short port = ntohs(inetAddress->sin_port);
      char buffer[strlen(host) + 7];

      snprintf(buffer, sizeof(buffer), "%s:%u", host, port);
      string = strdup(buffer);
      break;
    }

    default:
      string = strdup("");
      break;
  }

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

static int
acceptSocketConnection (
  int (*getSocket) (void),
  int (*prepareQueue) (int socket),
  void (*unbindAddress) (const struct sockaddr *address),
  const struct sockaddr *localAddress, socklen_t localSize,
  struct sockaddr *remoteAddress, socklen_t *remoteSize
) {
  int serverSocket = -1;
  int queueSocket;

  if ((queueSocket = getSocket()) != -1) {
    if (!prepareQueue || prepareQueue(queueSocket)) {
      if (bind(queueSocket, localAddress, localSize) != -1) {
        if (listen(queueSocket, 1) != -1) {
          int attempts = 0;

          {
            char *address = formatSocketAddress(localAddress);

            if (address) {
              logMessage(LOG_NOTICE, "listening on: %s", address);
              free(address);
            }
          }

          while (1) {
            fd_set readMask;
            struct timeval timeout;

            FD_ZERO(&readMask);
            FD_SET(queueSocket, &readMask);

            memset(&timeout, 0, sizeof(timeout));
            timeout.tv_sec = 10;

            ++attempts;
            switch (select(queueSocket+1, &readMask, NULL, NULL, &timeout)) {
              case -1:
                if (errno == EINTR) continue;
                LogSocketError("select");
                break;

              case 0:
                logMessage(LOG_DEBUG, "no connection yet, still waiting (%d).", attempts);
                continue;

              default: {
                if (!FD_ISSET(queueSocket, &readMask)) continue;

                if ((serverSocket = accept(queueSocket, remoteAddress, remoteSize)) != -1) {
                  char *address = formatSocketAddress(remoteAddress);

                  if (address) {
                    logMessage(LOG_NOTICE, "client is: %s", address);
                    free(address);
                  }
                } else {
                  LogSocketError("accept");
                }
              }
            }
            break;
          }
        } else {
          LogSocketError("listen");
        }

        if (unbindAddress) unbindAddress(localAddress);
      } else {
        LogSocketError("bind");
      }
    }

    close(queueSocket);
  } else {
    LogSocketError("socket");
  }

  operations = &socketOperationsEntry;
  return serverSocket;
}

static int
requestConnection (
  int (*getSocket) (void),
  const struct sockaddr *remoteAddress, socklen_t remoteSize
) {
  int clientSocket;

  {
    char *address = formatSocketAddress(remoteAddress);

    if (address) {
      logMessage(LOG_DEBUG, "connecting to: %s", address);
      free(address);
    }
  }

  if ((clientSocket = getSocket()) != -1) {
    if (connect(clientSocket, remoteAddress, remoteSize) != -1) {
      {
        char *address = formatSocketAddress(remoteAddress);

        if (address) {
          logMessage(LOG_NOTICE, "connected to: %s", address);
          free(address);
        }
      }

      operations = &socketOperationsEntry;
      return clientSocket;
    } else {
      logMessage(LOG_WARNING, "connect error: %s", strerror(errno));
    }

    close(clientSocket);
  } else {
    LogSocketError("socket");
  }

  return -1;
}

static int
setSocketReuseAddress (int socket) {
  int yes = 1;

  if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, (void *)&yes, sizeof(yes)) != -1) {
    return 1;
  } else {
    LogSocketError("setsockopt REUSEADDR");
  }
  return 0;
}

#ifdef AF_LOCAL
static int
setLocalAddress (const char *string, struct sockaddr_un *address) {
  int ok = 1;

  memset(address, 0, sizeof(*address));
  address->sun_family = AF_LOCAL;

  if (strlen(string) < sizeof(address->sun_path)) {
    strncpy(address->sun_path, string, sizeof(address->sun_path)-1);
  } else {
    ok = 0;
    logMessage(LOG_WARNING, "Local socket path too long: %s", string);
  }

  return ok;
}

static int
getLocalSocket (void) {
  return socket(PF_LOCAL, SOCK_STREAM, 0);
}

static void
unbindLocalAddress (const struct sockaddr *address) {
  const struct sockaddr_un *localAddress = (const struct sockaddr_un *)address;
  if (unlink(localAddress->sun_path) == -1) {
    logSystemError("unlink");
  }
}

static int
acceptLocalConnection (const struct sockaddr_un *localAddress) {
  struct sockaddr_un remoteAddress;
  socklen_t remoteSize = sizeof(remoteAddress);

  return acceptSocketConnection(getLocalSocket, NULL, unbindLocalAddress,
                                (const struct sockaddr *)localAddress, sizeof(*localAddress),
                                (struct sockaddr *)&remoteAddress, &remoteSize);
}

static int
requestLocalConnection (const struct sockaddr_un *remoteAddress) {
  return requestConnection(getLocalSocket,
                           (const struct sockaddr *)remoteAddress, sizeof(*remoteAddress));
}
#endif /* AF_LOCAL */

#ifdef __MINGW32__
static int
readNamedPipe (int descriptor, void *buffer, int size) {
  {
    DWORD available;

    if (!PeekNamedPipe((HANDLE)descriptor, NULL, 0, NULL, &available, NULL)) {
      logWindowsSystemError("PeekNamedPipe");
      return 0;
    }

    if (!available) {
      errno = EAGAIN;
      return -1;
    }

    if (available < size) size = available;
  }

  {
    DWORD received;
    OVERLAPPED overl = {0, 0, {{0, 0}}, NULL};
    overl.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

    if (!ReadFile((HANDLE)descriptor, buffer, size, &received, &overl)) {
      if (GetLastError() != ERROR_IO_PENDING) {
        logWindowsSystemError("ReadPipe");
        received = 0;
      } else if (!GetOverlappedResult((HANDLE)descriptor, &overl, &received, TRUE)) {
        logWindowsSystemError("GetOverlappedResult");
        received = 0;
      }
    }

    CloseHandle(overl.hEvent);
    return received;
  }
}

static const OperationsEntry namedPipeOperationsEntry = {
  readNamedPipe
};

static int
acceptNamedPipeConnection (const char *path) {
  HANDLE h;
  OVERLAPPED overl = {0, 0, {{0, 0}}, NULL};
  DWORD res;
  int attempts = 0;

  if ((h = CreateNamedPipe(path, 
                                PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
                                PIPE_TYPE_BYTE | PIPE_READMODE_BYTE,
                                1, 0, 0, 0, NULL)) == INVALID_HANDLE_VALUE) {
    logWindowsSystemError("CreateNamedPipe");
    return -1;
  }

  overl.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
  if (!ConnectNamedPipe(h, &overl)) {
    switch (GetLastError()) {
      case ERROR_IO_PENDING:
        while ((res = WaitForSingleObject(overl.hEvent, 10000)) != WAIT_OBJECT_0) {
          if (res == WAIT_TIMEOUT) {
            ++attempts;
            logMessage(LOG_DEBUG, "no connection yet, still waiting (%d).", attempts);
          } else {
            logWindowsSystemError("ConnectNamedPipe");
            CloseHandle(h);
            h = (HANDLE) -1;
            break;
          }
        }

      case ERROR_PIPE_CONNECTED:
        break;

      default:
        logWindowsSystemError("ConnectNamedPipe");
        CloseHandle(h);
        h = (HANDLE) -1;
        break;
    }
  }

  CloseHandle(overl.hEvent);
  operations = &namedPipeOperationsEntry;
  return (int)h;
}

static int
requestNamedPipeConnection (const char *path) {
  HANDLE h;

  if ((h = CreateFile(path,
                      GENERIC_READ|GENERIC_WRITE,
                      FILE_SHARE_READ|FILE_SHARE_WRITE,
                      NULL, OPEN_EXISTING, 0, NULL)) == INVALID_HANDLE_VALUE) {
    logWindowsSystemError("Connect to named pipe");
    return -1;
  }

  operations = &namedPipeOperationsEntry;
  return (int)h;
}
#endif /* __MINGW32__ */

static int
setInetAddress (const char *string, struct sockaddr_in *address) {
  int ok = 1;
  char *hostName = strdup(string);

  if (hostName) {
    char *portNumber = strchr(hostName, ':');

    if (portNumber) {
      *portNumber++ = 0;
      if (!*portNumber) portNumber = NULL;
    }

    memset(address, 0, sizeof(*address));
    address->sin_family = AF_INET;

    if (*hostName) {
      const struct hostent *host = gethostbyname(hostName);
      if (host && (host->h_addrtype == AF_INET) && (host->h_length == sizeof(address->sin_addr))) {
        memcpy(&address->sin_addr, host->h_addr, sizeof(address->sin_addr));
      } else {
        ok = 0;
        logMessage(LOG_WARNING, "Unknown host name: %s", hostName);
      }
    } else {
      address->sin_addr.s_addr = INADDR_ANY;
    }

    if (portNumber) {
      int port;

      if (isInteger(&port, portNumber)) {
        if ((port > 0) && (port <= 0XFFFF)) {
          address->sin_port = htons(port);
        } else {
          ok = 0;
          logMessage(LOG_WARNING, "Invalid port number: %s", portNumber);
        }
      } else {
        const struct servent *service = getservbyname(portNumber, "tcp");

        if (service) {
          address->sin_port = service->s_port;
        } else {
          ok = 0;
          logMessage(LOG_WARNING, "Unknown service: %s", portNumber);
        }
      }
    } else {
      address->sin_port = htons(VR_DEFAULT_PORT);
    }

    free(hostName);
  } else {
    ok = 0;
    logMallocError();
  }

  return ok;
}

static int
getInetSocket (void) {
  return socket(PF_INET, SOCK_STREAM, 0);
}

static int
prepareInetQueue (int socket) {
  if (setSocketReuseAddress(socket)) return 1;
  return 0;
}

static int
acceptInetConnection (const struct sockaddr_in *localAddress) {
  struct sockaddr_in remoteAddress;
  socklen_t remoteSize = sizeof(remoteAddress);

  return acceptSocketConnection(getInetSocket, prepareInetQueue, NULL,
                                (const struct sockaddr *)localAddress, sizeof(*localAddress),
                                (struct sockaddr *)&remoteAddress, &remoteSize);
}

static int
requestInetConnection (const struct sockaddr_in *remoteAddress) {
  return requestConnection(getInetSocket,
                           (const struct sockaddr *)remoteAddress, sizeof(*remoteAddress));
}

static char *
makeString (const char *characters, int count) {
  char *string = malloc(count+1);

  if (string) {
    memcpy(string, characters, count);
    string[count] = 0;
  } else {
    logMallocError();
  }

  return string;
}

static char *
copyString (const char *string) {
  return makeString(string, strlen(string));
}

static int
fillInputBuffer (void) {
  if ((inputLength < INPUT_SIZE) && !inputEnd) {
    int count = operations->read(fileDescriptor, &inputBuffer[inputLength], INPUT_SIZE-inputLength);
    if (!count) {
      inputEnd = 1;
    } else if (count != -1) {
      inputLength += count;
    } else if (errno != EAGAIN) {
      return 0;
    }
  }
  return 1;
}

static char *
readCommandLine (void) {
  if (fillInputBuffer()) {
    if (inputStart < inputLength) {
      const char *newline = memchr(&inputBuffer[inputStart], '\n', inputLength-inputStart);

      if (newline) {
        char *string;
        int stringLength = newline - inputBuffer;
        inputCarriageReturn = 0;

        if ((newline != inputBuffer) && (*(newline-1) == '\r')) {
          inputCarriageReturn = 1;
          stringLength -= 1;
        }

        string = makeString(inputBuffer, stringLength);
        inputLength -= ++newline - inputBuffer;
        memmove(inputBuffer, newline, inputLength);
        inputStart = 0;
        return string;
      } else {
        inputStart = inputLength;
      }
    } else if (inputEnd) {
      char *string;

      if (inputLength) {
        string = makeString(inputBuffer, inputLength);
        inputLength = 0;
        inputStart = 0;
      } else {
        string = copyString("quit");
      }

      return string;
    }
  }

  return NULL;
}

static const char *
nextWord (void) {
  return strtok(NULL, inputDelimiters);
}

static int
compareWords (const char *word1, const char *word2) {
  return strcasecmp(word1, word2);
}

static int
testWord (const char *suppliedWord, const char *desiredWord) {
  return compareWords(suppliedWord, desiredWord) == 0;
}

static int
flushOutput (void) {
  const char *buffer = outputBuffer;
  size_t length = outputLength;

  while (length) {
#ifdef __MINGW32__
    DWORD sent;
    OVERLAPPED overl = {0, 0, {{0, 0}}, CreateEvent(NULL, TRUE, FALSE, NULL)};
    if ((!WriteFile((HANDLE) fileDescriptor, buffer, length, &sent, &overl)
      && GetLastError() != ERROR_IO_PENDING) ||
      !GetOverlappedResult((HANDLE) fileDescriptor, &overl, &sent, TRUE)) {
        LogSocketError("WriteFile");
        CloseHandle(overl.hEvent);
        memmove(outputBuffer, buffer, (outputLength = length));
        return 0;
      }
    CloseHandle(overl.hEvent);
#else /* __MINGW32__ */
    int sent;
    sent = send(fileDescriptor, buffer, length, 0);

    if (sent == -1) {
      if (errno == EINTR) continue;
      LogSocketError("send");
      memmove(outputBuffer, buffer, (outputLength = length));
      return 0;
    }
#endif /* __MINGW32__ */

    buffer += sent;
    length -= sent;
  }

  outputLength = 0;
  return 1;
}

static int
writeBytes (const char *bytes, size_t length) {
  while (length) {
    size_t count = OUTPUT_SIZE - outputLength;
    if (length < count) count = length;
    memcpy(&outputBuffer[outputLength], bytes, count);
    bytes += count;
    length -= count;
    if ((outputLength += count) == OUTPUT_SIZE)
      if (!flushOutput())
        return 0;
  }

  return 1;
}

static int
writeByte (char byte) {
  return writeBytes(&byte, 1);
}

static int
writeString (const char *string) {
  return writeBytes(string, strlen(string));
}

static int
writeCharacter (wchar_t character) {
  Utf8Buffer buffer;
  size_t count = convertWcharToUtf8(character, buffer);
  return writeBytes(buffer, count);
}

static int
writeDots (const unsigned char *cells, int count) {
  const unsigned char *cell = cells;

  while (count-- > 0) {
    char dots[9];
    char *d = dots;

    if (cell != cells) *d++ = '|';
    if (*cell) {
      if (*cell & BRL_DOT1) *d++ = '1';
      if (*cell & BRL_DOT2) *d++ = '2';
      if (*cell & BRL_DOT3) *d++ = '3';
      if (*cell & BRL_DOT4) *d++ = '4';
      if (*cell & BRL_DOT5) *d++ = '5';
      if (*cell & BRL_DOT6) *d++ = '6';
      if (*cell & BRL_DOT7) *d++ = '7';
      if (*cell & BRL_DOT8) *d++ = '8';
    } else {
      *d++ = ' ';
    }
    ++cell;

    if (!writeBytes(dots, d-dots)) return 0;
  }

  return 1;
}

static int
writeLine (void) {
  if (inputCarriageReturn)
    if (!writeByte('\r'))
      return 0;

  if (writeByte('\n'))
    if (flushOutput())
      return 1;

  return 0;
}

static void
sortCommands (int (*compareCommands) (const void *item1, const void *item2)) {
  qsort(commandDescriptors, commandCount, commandSize, compareCommands);
}

static int
compareCommandCodes (const void *item1, const void *item2) {
  const CommandDescriptor *descriptor1 = item1;
  const CommandDescriptor *descriptor2 = item2;

  int code1 = descriptor1->entry->code;
  int code2 = descriptor2->entry->code;

  if (code1 < code2) return -1;
  if (code1 > code2) return 1;
  return 0;
}

static void
sortCommandsByCode (void) {
  sortCommands(compareCommandCodes);
}

static int
compareCommandNames (const void *item1, const void *item2) {
  const CommandDescriptor *descriptor1 = item1;
  const CommandDescriptor *descriptor2 = item2;

  return strcmp(descriptor1->entry->name, descriptor2->entry->name);
}

static void
sortCommandsByName (void) {
  sortCommands(compareCommandNames);
}

static int
allocateCommandDescriptors (void) {
  if (!commandDescriptors) {
    commandCount = getCommandCount();
    commandDescriptors = malloc(commandCount * commandSize);

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

    {
      CommandDescriptor *descriptor = commandDescriptors;
      const CommandEntry *entry = commandTable;
      while (entry->name) {
        descriptor->entry = entry++;
        descriptor->count = 0;
        ++descriptor;
      }
    }

    sortCommandsByCode();
    {
      CommandDescriptor *descriptor = commandDescriptors + commandCount;
      int previousBlock = -1;

      while (descriptor-- != commandDescriptors) {
        int code = descriptor->entry->code;
        int currentBlock = code & BRL_MSK_BLK;

        if (currentBlock != previousBlock) {
          if (currentBlock) {
            descriptor->count = (BRL_MSK_ARG + 1) - (code & BRL_MSK_ARG);
          }
          previousBlock = currentBlock;
        }
      }
    }

    sortCommandsByName();
  }

  return 1;
}

static void
deallocateCommandDescriptors (void) {
  if (commandDescriptors) {
    free(commandDescriptors);
    commandDescriptors = NULL;
  }
}

static int
compareCommandName (const void *key, const void *item) {
  const char *name = key;
  const CommandDescriptor *descriptor = item;
  return compareWords(name, descriptor->entry->name);
}

static const CommandDescriptor *
findCommand (const char *name) {
  return bsearch(name, commandDescriptors, commandCount, commandSize, compareCommandName);
}

static int
dimensionsChanged (BrailleDisplay *brl) {
  int ok = 1;
  const char *word;

  int columns1;
  int rows1;

  int columns2 = 0;
  int rows2 = 0;

  if ((word = nextWord())) {
    if (isInteger(&columns1, word) && (columns1 > 0)) {
      rows1 = 1;

      if ((word = nextWord())) {
        if (isInteger(&rows1, word) && (rows1 > 0)) {
          if ((word = nextWord())) {
            if (isInteger(&columns2, word) && (columns2 > 0)) {
              rows2 = 0;

              if ((word = nextWord())) {
                if (isInteger(&rows2, word) && (rows2 > 0)) {
                } else {
                  logMessage(LOG_WARNING, "invalid status row count: %s", word);
                  ok = 0;
                }
              }
            } else {
              logMessage(LOG_WARNING, "invalid status column count: %s", word);
              ok = 0;
            }
          }
        } else {
          logMessage(LOG_WARNING, "invalid text row count: %s", word);
          ok = 0;
        }
      }
    } else {
      logMessage(LOG_WARNING, "invalid text column count: %s", word);
      ok = 0;
    }
  } else {
    logMessage(LOG_WARNING, "missing text column count");
    ok = 0;
  }

  if (ok) {
    int count1 = columns1 * rows1;
    int count2 = columns2 * rows2;
    unsigned char *braille;
    wchar_t *text;
    unsigned char *status;

    if ((braille = calloc(count1, sizeof(*braille)))) {
      if ((text = calloc(count1, sizeof(*text)))) {
        if ((status = calloc(count2, sizeof(*status)))) {
          brailleColumns = columns1;
          brailleRows = rows1;
          brailleCount = count1;

          statusColumns = columns2;
          statusRows = rows2;
          statusCount = count2;

          if (brailleCells) free(brailleCells);
          brailleCells = braille;
          memset(brailleCells, 0, count1);

          if (textCharacters) free(textCharacters);
          textCharacters = text;
          wmemset(textCharacters, WC_C(' '), count1);

          if (statusCells) free(statusCells);
          statusCells = status;
          memset(statusCells, 0, count2);
          memset(genericCells, 0, GSC_COUNT);

          brl->textColumns = brailleColumns;
          brl->textRows = brailleRows;
          brl->statusColumns = statusColumns;
          brl->statusRows = statusRows;
          return 1;
        }

        free(text);
      }

      free(braille);
    }
  }

  return 0;
}

static int
brl_construct (BrailleDisplay *brl, char **parameters, const char *device) {
  if (!allocateCommandDescriptors()) return 0;

  inputLength = 0;
  inputStart = 0;
  inputEnd = 0;
  outputLength = 0;

  if (hasQualifier(&device, "client")) {
    static const ModeEntry clientModeEntry = {
#ifdef AF_LOCAL
      requestLocalConnection,
#endif /* AF_LOCAL */

#ifdef __MINGW32__
      requestNamedPipeConnection,
#endif /* __MINGW32__ */

      requestInetConnection
    };
    mode = &clientModeEntry;
  } else if (hasQualifier(&device, "server")) {
    static const ModeEntry serverModeEntry = {
#ifdef AF_LOCAL
      acceptLocalConnection,
#endif /* AF_LOCAL */

#ifdef __MINGW32__
      acceptNamedPipeConnection,
#endif /* __MINGW32__ */

      acceptInetConnection
    };
    mode = &serverModeEntry;
  } else {
    unsupportedDeviceIdentifier(device);
    goto failed;
  }
  if (!*device) device = VR_DEFAULT_SOCKET;

#ifdef AF_LOCAL
  if (device[0] == '/') {
    struct sockaddr_un address;
    if (setLocalAddress(device, &address)) {
      fileDescriptor = mode->getLocalConnection(&address);
    }
  } else
#endif /* AF_LOCAL */

#ifdef __MINGW32__
  if (device[0] == '\\') {
    fileDescriptor = mode->getNamedPipeConnection(device);
  } else {
    static WSADATA wsadata;
    if (WSAStartup(MAKEWORD(1, 1), &wsadata)) {
      logWindowsSystemError("socket library start");
      goto failed;
    }
  }
#endif /* __MINGW32__ */

  {
    struct sockaddr_in address;
    if (setInetAddress(device, &address)) {
      fileDescriptor = mode->getInetConnection(&address);
    }
  }

  if (fileDescriptor != -1) {
    char *line = NULL;

    while (1) {
      if (line) free(line);
      if ((line = readCommandLine())) {
        const char *word;
        logMessage(LOG_DEBUG, "command received: %s", line);

        if ((word = strtok(line, inputDelimiters))) {
          if (testWord(word, "cells")) {
            if (dimensionsChanged(brl)) {
              free(line);
              return 1;
            }
          } else if (testWord(word, "quit")) {
            break;
          } else {
            logMessage(LOG_WARNING, "unexpected command: %s", word);
          }
        }
      } else {
        asyncWait(1000);
      }
    }
    if (line) free(line);

    close(fileDescriptor);
    fileDescriptor = -1;
  }

failed:
  deallocateCommandDescriptors();
  return 0;
}

static void
brl_destruct (BrailleDisplay *brl) {
  if (statusCells) {
    free(statusCells);
    statusCells = NULL;
  }

  if (textCharacters) {
    free(textCharacters);
    textCharacters = NULL;
  }

  if (brailleCells) {
    free(brailleCells);
    brailleCells = NULL;
  }

  if (fileDescriptor != -1) {
    close(fileDescriptor);
    fileDescriptor = -1;
  }

  deallocateCommandDescriptors();
}

static int
brl_writeWindow (BrailleDisplay *brl, const wchar_t *text) {
  if (text) {
    if (wmemcmp(text, textCharacters, brailleCount) != 0) {
      const wchar_t *address = text;
      int count = brailleCount;

      writeString("Visual \"");

      while (count-- > 0) {
        wchar_t character = *address++;

        switch (character) {
          case WC_C('"'):
          case WC_C('\\'):
            writeCharacter(WC_C('\\'));
            /* fall through */
          default:
            writeCharacter(character);
            break;
        }
      }

      writeString("\"");
      writeLine();

      wmemcpy(textCharacters, text, brailleCount);
    }
  }

  if (cellsHaveChanged(brailleCells, brl->buffer, brailleCount, NULL, NULL, NULL)) {
    writeString("Braille \"");
    writeDots(brl->buffer, brailleCount);
    writeString("\"");
    writeLine();
  }

  return 1;
}

static int
brl_writeStatus (BrailleDisplay *brl, const unsigned char *status) {
  int generic = status[GSC_FIRST] == GSC_MARKER;
  unsigned char *cells;
  int count;

  if (generic) {
    cells = genericCells;
    count = GSC_COUNT;
  } else {
    cells = statusCells;
    count = statusCount;
  }

  if (cellsHaveChanged(cells, status, count, NULL, NULL, NULL)) {
    if (generic) {
      int all = cells[GSC_FIRST] != GSC_MARKER;
      int i;

      for (i=1; i<count; ++i) {
        unsigned char value = status[i];
        if (all || (value != cells[i])) {
          static const char *const names[] = {
            [GSC_FIRST] = NULL,
            [gscBrailleWindowColumn] = "BRLCOL",
            [gscBrailleWindowRow] = "BRLROW",
            [gscScreenCursorColumn] = "CSRCOL",
            [gscScreenCursorRow] = "CSRROW",
            [gscScreenNumber] = "SCRNUM",
            [gscFrozenScreen] = "FREEZE",
            [gscDisplayMode] = "DISPMD",
            [gscSixDotComputerBraille] = "SIXDOTS",
            [gscContractedBraille] = "CONTRACTED",
            [gscSlidingBrailleWindow] = "SLIDEWIN",
            [gscSkipIdenticalLines] = "SKPIDLNS",
            [gscSkipBlankBrailleWindows] = "SKPBLNKWINS",
            [gscShowScreenCursor] = "CSRVIS",
            [gscHideScreenCursor] = "CSRHIDE",
            [gscTrackScreenCursor] = "CSRTRK",
            [gscScreenCursorStyle] = "CSRSIZE",
            [gscBlinkingScreenCursor] = "CSRBLINK",
            [gscShowAttributes] = "ATTRVIS",
            [gscBlinkingAttributes] = "ATTRBLINK",
            [gscBlinkingCapitals] = "CAPBLINK",
            [gscAlertTunes] = "TUNES",
            [gscAutorepeat] = "AUTOREPEAT",
            [gscAutospeak] = "AUTOSPEAK",
            [gscBrailleTypingMode] = "BRLUCDOTS"
          };
          const int nameCount = ARRAY_COUNT(names);

          if (i < nameCount) {
            const char *name = names[i];
            if (name) {
              char buffer[0X40];
              snprintf(buffer, sizeof(buffer), "%s %d", name, value);
              writeString(buffer);
              writeLine();
            }
          }
        }
      }
    } else {
      writeString("Status \"");
      writeDots(cells, count);
      writeString("\"");
      writeLine();
    }
  }

  return 1;
}

static int
brl_readCommand (BrailleDisplay *brl, KeyTableCommandContext context) {
  int command = EOF;
  char *line = readCommandLine();

  if (line) {
    const char *word;
    logMessage(LOG_DEBUG, "Command received: %s", line);

    if ((word = strtok(line, inputDelimiters))) {
      if (testWord(word, "cells")) {
        if (dimensionsChanged(brl)) brl->resizeRequired = 1;
      } else if (testWord(word, "quit")) {
        command = BRL_CMD_RESTARTBRL;
      } else {
        const CommandDescriptor *descriptor = findCommand(word);
        if (descriptor) {
          int needsNumber = descriptor->count > 0;
          int numberSpecified = 0;
          int switchSpecified = 0;
          int block;

          command = descriptor->entry->code;
          block = command & BRL_MSK_BLK;

          while ((word = nextWord())) {
            if (block == 0) {
              if (!switchSpecified) {
                if (testWord(word, "on")) {
                  switchSpecified = 1;
                  command |= BRL_FLG_TOGGLE_ON;
                  continue;
                }

                if (testWord(word, "off")) {
                  switchSpecified = 1;
                  command |= BRL_FLG_TOGGLE_OFF;
                  continue;
                }
              }
            }

            if (needsNumber && !numberSpecified) {
              int number;
              if (isInteger(&number, word)) {
                if ((number > 0) && (number <= descriptor->count)) {
                  numberSpecified = 1;
                  command += number;
                  continue;
                } else {
                  logMessage(LOG_WARNING, "Number out of range.");
                }
              }
            }

            logMessage(LOG_WARNING, "unknown option: %s", word);
          }

          if (needsNumber && !numberSpecified) {
            logMessage(LOG_WARNING, "Number not specified.");
            command = EOF;
          }
        } else {
          logMessage(LOG_WARNING, "unknown command: %s", word);
        }
      }
    }

    free(line);
  }

  return command;
}
