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

#include "prologue.h"

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <time.h>

#ifdef HAVE_LANGINFO_H
#include <langinfo.h>
#endif /* HAVE_LANGINFO_H */

#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif /* HAVE_SYS_WAIT_H */

#include "parameters.h"
#include "embed.h"
#include "log.h"
#include "alert.h"
#include "strfmt.h"

#include "cmd_queue.h"
#include "cmd_clipboard.h"
#include "cmd_custom.h"
#include "cmd_input.h"
#include "cmd_keycodes.h"
#include "cmd_learn.h"
#include "cmd_miscellaneous.h"
#include "cmd_navigation.h"
#include "cmd_override.h"
#include "cmd_preferences.h"
#include "cmd_speech.h"
#include "cmd_toggle.h"
#include "cmd_touch.h"

#include "async_handle.h"
#include "async_wait.h"
#include "async_alarm.h"
#include "async_event.h"
#include "async_signal.h"
#include "async_task.h"

#include "brl_cmds.h"
#include "timing.h"
#include "ctb.h"
#include "routing.h"
#include "utf8.h"
#include "unicode.h"
#include "scr.h"
#include "update.h"
#include "ses.h"
#include "brl.h"
#include "brl_utils.h"
#include "prefs.h"
#include "api_control.h"
#include "options.h"
#include "program.h"
#include "core.h"

#ifdef ENABLE_SPEECH_SUPPORT
#include "spk.h"
#endif /* ENABLE_SPEECH_SUPPORT */

BrailleDisplay brl;                        /* For the Braille routines */

int
haveBrailleDisplay (void) {
  return braille->definition.code != noBraille.definition.code;
}

ScreenDescription scr;
SessionEntry *ses = NULL;

unsigned char infoMode = 0;

unsigned int textStart;
unsigned int textCount;
unsigned char textMaximized = 0;

unsigned int statusStart;
unsigned int statusCount;

unsigned int fullWindowShift;                /* Full window horizontal distance */
unsigned int halfWindowShift;                /* Half window horizontal distance */
unsigned int verticalWindowShift;                /* Window vertical distance */

int
isContractedBraille (void) {
  return (prefs.brailleVariant == bvContracted6)
      || (prefs.brailleVariant == bvContracted8)
      ;
}

int
isSixDotComputerBraille (void) {
  return (prefs.brailleVariant == bvComputer6)
      || (prefs.brailleVariant == bvContracted6)
      ;
}

static void
setBrailleVariant (int contracted, int sixDot) {
  prefs.brailleVariant = contracted?
                         (sixDot? bvContracted6: bvContracted8):
                         (sixDot? bvComputer6: bvComputer8);
}

void
setContractedBraille (int contracted) {
  setBrailleVariant(contracted, isSixDotComputerBraille());
  api.updateParameter(BRLAPI_PARAM_LITERARY_BRAILLE, 0);
}

void
setSixDotComputerBraille (int sixDot) {
  setBrailleVariant(isContractedBraille(), sixDot);
  api.updateParameter(BRLAPI_PARAM_COMPUTER_BRAILLE_CELL_SIZE, 0);
}

void
onBrailleVariantUpdated (void) {
  api.updateParameter(BRLAPI_PARAM_COMPUTER_BRAILLE_CELL_SIZE, 0);
  api.updateParameter(BRLAPI_PARAM_LITERARY_BRAILLE, 0);
}

int
startScreenCursorRouting (int column, int row) {
  if (!routeScreenCursor(column, row, scr.number)) return 0;
  if (isRouting()) alert(ALERT_ROUTING_STARTED);
  return 1;
}

int
bringScreenCursor (int column, int row) {
  if (!startScreenCursorRouting(column, row)) return 0;
  RoutingStatus status = getRoutingStatus(1);

  if (status != ROUTING_STATUS_NONE) {
    alert(
      (status > ROUTING_STATUS_COLUMN)? ALERT_ROUTING_FAILED:
      ALERT_ROUTING_SUCCEEDED
    );

    ses->spkx = scr.posx;
    ses->spky = scr.posy;
  }

  return 1;
}

typedef struct {
  int motionColumn;
  int motionRow;

  int speechColumn;
  int speechRow;
} PrecommandState;

static void *
preprocessCommand (void) {
  PrecommandState *pre;

  if ((pre = malloc(sizeof(*pre)))) {
    memset(pre, 0, sizeof(*pre));

    pre->motionColumn = ses->winx;
    pre->motionRow = ses->winy;

    pre->speechColumn = ses->spkx;
    pre->speechRow = ses->spky;

    suspendUpdates();
    return pre;
  } else {
    logMallocError();
  }

  return NULL;
}

static void
postprocessCommand (void *state, int command, const CommandEntry *cmd, int handled) {
  PrecommandState *pre = state;

  if (pre) {
    resumeUpdates(0);
    if (handled) scheduleUpdate("command executed");

    if ((ses->winx != pre->motionColumn) || (ses->winy != pre->motionRow)) {
      /* The braille window has been manually moved. */
      reportBrailleWindowMoved();

      ses->motx = ses->winx;
      ses->moty = ses->winy;

      isContracted = 0;
    }

    if (cmd) {
      if (cmd->isMotion) {
        if (command & BRL_FLG_MOTION_ROUTE) {
          if ((ses->spkx != pre->speechColumn) || (ses->spky != pre->speechRow)) {
            /* The speech cursor has moved. */
            bringScreenCursor(ses->spkx, ses->spky);
          } else if (command & BRL_MSK_BLK) {
            bringScreenCursor((command & BRL_MSK_ARG), ses->winy);
          } else {
            int left = ses->winx;
            int right = MIN(left+textCount, scr.cols) - 1;

            int top = ses->winy;
            int bottom = MIN(top+brl.textRows, scr.rows) - 1;

            if ((scr.posx < left) || (scr.posx > right) ||
                (scr.posy < top) || (scr.posy > bottom)) {
              bringScreenCursor(left, top);
            }
          }
        }
      }
    }

    free(pre);
  }
}

static int
handleUnhandledCommands (int command, void *data) {
  switch (command & BRL_MSK_CMD) {
    case BRL_CMD_NOOP:        /* do nothing but loop */
      break;

    default:
      alert(ALERT_COMMAND_REJECTED);
      return 0;
  }

  return 1;
}

static int
handleApiCommands (int command, void *data) {
  if (isIneligibleApiCommand(command)) return 0;
  return api.handleCommand(command);
}

static int
addScreenCommands (void) {
  return pushCommandHandler("screen", KTB_CTX_DEFAULT,
                            handleScreenCommands, NULL, NULL);
}

static int
addCommands (void) {
  if (!pushCommandEnvironment("main", preprocessCommand, postprocessCommand)) return 0;

  pushCommandHandler("unhandled", KTB_CTX_DEFAULT,
                     handleUnhandledCommands, NULL, NULL);

  addMiscellaneousCommands();
  addLearnCommands();
  addSpeechCommands();
  addClipboardCommands();
  addPreferencesCommands();
  addToggleCommands();
  addTouchCommands();
  addKeycodeCommands();
  addInputCommands();
  addNavigationCommands();

  addOverrideCommands();
  addScreenCommands();
  addCustomCommands();

  pushCommandHandler("API", KTB_CTX_DEFAULT,
                     handleApiCommands, NULL, NULL);

  return 1;
}

static AsyncHandle delayedCursorTrackingAlarm;

ASYNC_ALARM_CALLBACK(handleDelayedCursorTrackingAlarm) {
  asyncDiscardHandle(delayedCursorTrackingAlarm);
  delayedCursorTrackingAlarm = NULL;

  ses->trkx = ses->dctx;
  ses->trky = ses->dcty;

  ses->dctx = -1;
  ses->dcty = -1;

  scheduleUpdate("delayed cursor tracking");
}

void
cancelDelayedCursorTrackingAlarm (void) {
  if (delayedCursorTrackingAlarm) {
    asyncCancelRequest(delayedCursorTrackingAlarm);
    delayedCursorTrackingAlarm = NULL;
  }
}

static void
setSessionEntry (void) {
  describeScreen(&scr);
  if (scr.number == -1) scr.number = userVirtualTerminal(0);

  {
    typedef enum {SAME, DIFFERENT, FIRST} State;
    State state = (!ses)? FIRST:
                  (scr.number == ses->number)? SAME:
                  DIFFERENT;

    if (state != SAME) {
      cancelDelayedCursorTrackingAlarm();
      ses = getSessionEntry(scr.number);

      if (state == FIRST) {
        addCommands();
      }
    }
  }
}

void
updateSessionAttributes (void) {
  setSessionEntry();

  {
    int maximum = MAX(scr.rows-1, 0);
    int *table[] = {&ses->winy, &ses->moty, NULL};
    int **value = table;

    while (*value) {
      if (**value > maximum) **value = maximum;
      value += 1;
    }
  }

  {
    int maximum = MAX(scr.cols-1, 0);
    int *table[] = {&ses->winx, &ses->motx, NULL};
    int **value = table;

    while (*value) {
      if (**value > maximum) **value = maximum;
      value += 1;
    }
  }

  // Notify routing system that screen was updated
  if (isRouting()) {
    onCursorPositionChanged();
  }
}

void
fillStatusSeparator (wchar_t *text, unsigned char *dots) {
  if ((prefs.statusSeparator != ssNone) && (statusCount > 0)) {
    int onRight = statusStart > 0;
    unsigned int column = (onRight? statusStart: textStart) - 1;

    wchar_t textSeparator;
#ifdef HAVE_WCHAR_H 
    const wchar_t textSeparator_left  = 0X23B8; /* LEFT VERTICAL BOX LINE */
    const wchar_t textSeparator_right = 0X23B9; /* RIGHT VERTICAL BOX LINE */
    const wchar_t textSeparator_block = 0X2503; /* BOX DRAWINGS HEAVY VERTICAL */
#else /* HAVE_WCHAR_H */
    const wchar_t textSeparator_left  = 0X5B; /* LEFT SQUARE BRACKET */
    const wchar_t textSeparator_right = 0X5D; /* RIGHT SQUARE BRACKET */
    const wchar_t textSeparator_block = 0X7C; /* VERTICAL LINE */
#endif /* HAVE_WCHAR_H */

    unsigned char dotsSeparator;
    const unsigned char dotsSeparator_left = BRL_DOT1 | BRL_DOT2 | BRL_DOT3 | BRL_DOT7;
    const unsigned char dotsSeparator_right = BRL_DOT4 | BRL_DOT5 | BRL_DOT6 | BRL_DOT8;
    const unsigned char dotsSeparator_block = dotsSeparator_left | dotsSeparator_right;

    text += column;
    dots += column;

    switch (prefs.statusSeparator) {
      case ssBlock:
        textSeparator = textSeparator_block;
        dotsSeparator = dotsSeparator_block;
        break;

      case ssStatusSide:
        textSeparator = onRight? textSeparator_right: textSeparator_left;
        dotsSeparator = onRight? dotsSeparator_right: dotsSeparator_left;
        break;

      case ssTextSide:
        textSeparator = onRight? textSeparator_left: textSeparator_right;
        dotsSeparator = onRight? dotsSeparator_left: dotsSeparator_right;
        break;

      default:
        textSeparator = WC_C(' ');
        dotsSeparator = 0;
        break;
    }

    {
      for (unsigned int row=0; row<brl.textRows; row+=1) {
        *text = textSeparator;
        text += brl.textColumns;

        *dots = dotsSeparator;
        dots += brl.textColumns;
      }
    }
  }
}

int
writeBrailleCharacters (const char *label, const wchar_t *characters, size_t length) {
  wchar_t textBuffer[brl.textColumns * brl.textRows];

  fillTextRegion(
    textBuffer, brl.buffer, textStart, textCount,
    brl.textColumns, brl.textRows, characters, length
  );

  {
    size_t labelLength = label? countUtf8Characters(label): 0;
    wchar_t labelCharacters[labelLength + 1];
    makeWcharsFromUtf8(label, labelCharacters, ARRAY_COUNT(labelCharacters));

    fillTextRegion(
      textBuffer, brl.buffer, statusStart, statusCount,
      brl.textColumns, brl.textRows, labelCharacters, labelLength
    );
  }

  fillStatusSeparator(textBuffer, brl.buffer);

  return writeBrailleWindow(&brl, textBuffer, SCQ_GOOD);
}

int
writeBrailleText (const char *mode, const char *text) {
  size_t count = countUtf8Characters(text) + 1;
  wchar_t characters[count];
  size_t length = makeWcharsFromUtf8(text, characters, count);
  return writeBrailleCharacters(mode, characters, length);
}

int
showBrailleText (const char *mode, const char *text, int minimumDelay) {
  int ok = writeBrailleText(mode, text);
  drainBrailleOutput(&brl, minimumDelay);
  return ok;
}

static inline const char *
getMeridianString_am (void) {
#ifdef HAVE_NL_LANGINFO
  return nl_langinfo(AM_STR);
#else /* HAVE_NL_LANGINFO */
  return "am";
#endif /* HAVE_NL_LANGINFO */
}

static inline const char *
getMeridianString_pm (void) {
#ifdef HAVE_NL_LANGINFO
  return nl_langinfo(PM_STR);
#else /* HAVE_NL_LANGINFO */
  return "pm";
#endif /* HAVE_NL_LANGINFO */
}

static const char *
getMeridianString (uint8_t *hour) {
  const char *string = NULL;

  switch (prefs.timeFormat) {
    case tf12Hour: {
      const uint8_t twelve = 12;

      string = (*hour < twelve)? getMeridianString_am(): getMeridianString_pm();
      *hour %= twelve;
      if (!*hour) *hour = twelve;
      break;
    }

    default:
      break;
  }

  return string;
}

STR_BEGIN_FORMATTER(formatBrailleTime, const TimeFormattingData *fmt)
  char time[0X40];

  {
    const char *hourFormat = "%02" PRIu8;
    const char *minuteFormat = "%02" PRIu8;
    const char *secondFormat = "%02" PRIu8;
    char separator;

    switch (prefs.timeSeparator) {
      default:
      case tsColon:
        separator = ':';
        break;

      case tsDot:
        separator = '.';
        break;
    }

    switch (prefs.timeFormat) {
      default:
      case tf24Hour:
        break;

      case tf12Hour:
        hourFormat = "%" PRIu8;
        break;
    }

    STR_BEGIN(time, sizeof(time));
    STR_PRINTF(hourFormat, fmt->components.hour);
    STR_PRINTF("%c", separator);
    STR_PRINTF(minuteFormat, fmt->components.minute);

    if (prefs.showSeconds) {
      STR_PRINTF("%c", separator);
      STR_PRINTF(secondFormat, fmt->components.second);
    }

    if (fmt->meridian) STR_PRINTF("%s", fmt->meridian);
    STR_END;
  }

  if (prefs.datePosition == dpNone) {
    STR_PRINTF("%s", time);
  } else {
    char date[0X40];

    {
      const char *yearFormat = "%04" PRIu16;
      const char *monthFormat = "%02" PRIu8;
      const char *dayFormat = "%02" PRIu8;

      uint16_t year = fmt->components.year;
      uint8_t month = fmt->components.month + 1;
      uint8_t day = fmt->components.day + 1;

      char separator;

      switch (prefs.dateSeparator) {
        default:
        case dsDash:
          separator = '-';
          break;

        case dsSlash:
          separator = '/';
          break;

        case dsDot:
          separator = '.';
          break;
      }

      STR_BEGIN(date, sizeof(date));
      switch (prefs.dateFormat) {
        default:
        case dfYearMonthDay:
          STR_PRINTF(yearFormat, year);
          STR_PRINTF("%c", separator);
          STR_PRINTF(monthFormat, month);
          STR_PRINTF("%c", separator);
          STR_PRINTF(dayFormat, day);
          break;

        case dfMonthDayYear:
          STR_PRINTF(monthFormat, month);
          STR_PRINTF("%c", separator);
          STR_PRINTF(dayFormat, day);
          STR_PRINTF("%c", separator);
          STR_PRINTF(yearFormat, year);
          break;

        case dfDayMonthYear:
          STR_PRINTF(dayFormat, day);
          STR_PRINTF("%c", separator);
          STR_PRINTF(monthFormat, month);
          STR_PRINTF("%c", separator);
          STR_PRINTF(yearFormat, year);
          break;
      }
      STR_END;

      switch (prefs.datePosition) {
        case dpBeforeTime:
          STR_PRINTF("%s %s", date, time);
          break;

        case dpAfterTime:
          STR_PRINTF("%s %s", time, date);
          break;

        default:
          STR_PRINTF("%s", date);
          break;
      }
    }
  }
STR_END_FORMATTER

void
getTimeFormattingData (TimeFormattingData *fmt) {
  getCurrentTime(&fmt->value);
  expandTimeValue(&fmt->value, &fmt->components);
  fmt->meridian = getMeridianString(&fmt->components.hour);
}

int
isCursorPosition (int x) {
  return (x == scr.posx) && (ses->winy == scr.posy) && showScreenCursor();
}

int
isWordBreak (const ScreenCharacter *characters, int x) {
  if (!iswspace(characters[x].text)) return 0;
  return !isCursorPosition(x);
}

int
getWordWrapLength (int row, int from, int count) {
  int width = scr.cols;
  if (from >= width) return 0;

  int end = from + count;
  if (end >= width) return width - from;

  ScreenCharacter characters[width];
  readScreenRow(row, width, characters);

  int to = end;
  int onWordBreak = iswspace(characters[to].text);

  if (!onWordBreak) {
    int index = to;

    while (index > from) {
      if (iswspace(characters[--index].text)) {
        to = index;
        onWordBreak = 1;
        break;
      }
    }
  }

  if (onWordBreak) {
    while (to < width) {
      if (!iswspace(characters[to].text)) break;
      if ((to >= end) && isCursorPosition(to)) break;
      to += 1;
    }
  }

  return to - from;
}

void
setWordWrapStart (int start) {
  if (start < 0) start = 0;
  ses->winx = start;

  if (start > 0) {
    int end = start + textCount;
    if (end > scr.cols) end = scr.cols;

    ScreenCharacter characters[end];
    readScreenRow(ses->winy, end, characters);

    while (end > 0) {
      if (!isWordBreak(characters, --end)) {
        end += 1;
        break;
      }
    }

    start = end - textCount;
    if (start < 0) start = 0;

    if (start > 0) {
      if (!isWordBreak(characters, start-1)) {
        while (start < end) {
          if (isWordBreak(characters, start)) break;
          start += 1;
        }
      }

      while (start < end) {
        if (!isWordBreak(characters, start)) break;
        start += 1;
      }
    }

    if (start < end) ses->winx = start;
  }
}

void 
placeBrailleWindowHorizontally (int x) {
  if (prefs.slidingBrailleWindow) {
    ses->winx = MAX(0, (x - (int)(textCount / 2)));
  } else {
    ses->winx = x / textCount * textCount;
  }
}

void
placeRightEdge (int column) {
  if (isContracting()) {
    ses->winx = 0;

    while (1) {
      int length = getContractedLength(textCount);
      int end = ses->winx + length;

      if (end > column) break;
      if (end == ses->winx) break;
      ses->winx = end;
    }
  } else {
    ses->winx = column / textCount * textCount;
  }
}

void
placeBrailleWindowRight (void) {
  placeRightEdge(scr.cols-1);
}

int
moveBrailleWindowLeft (unsigned int amount) {
  if (ses->winx < 1) return 0;
  if (amount < 1) return 0;

  ses->winx -= MIN(ses->winx, amount);
  return 1;
}

int
moveBrailleWindowRight (unsigned int amount) {
  if (amount < 1) return 0;
  int newx = ses->winx + amount;
  if (newx >= scr.cols) return 0;

  ses->winx = newx;
  return 1;
}

int
shiftBrailleWindowLeft (unsigned int amount) {
  if (isContracting()) {
    int reference = ses->winx;
    if (!reference) return 0;

    {
      int from = 0;
      int to = ses->winx;

      while (from < to) {
        int end = (ses->winx = ((from + to) / 2)) + getContractedLength(amount);

        if (end < reference) {
          from = ses->winx + 1;
        } else {
          to = ses->winx;
        }
      }

      if (!(ses->winx = from)) return 1;
    }

    ScreenCharacter characters[reference];
    readScreenRow(ses->winy, reference, characters);
    int x = ses->winx;

    if (!isWordBreak(characters, x-1)) {
      int wasIdeographic = isIdeographicCharacter(characters[x-1].text);

      for (int i=x; i<reference; i+=1) {
        int isIdeographic = isIdeographicCharacter(characters[i].text);

        if (!(isIdeographic && wasIdeographic)) {
          if (!isWordBreak(characters, i)) {
            wasIdeographic = isIdeographic;
            continue;
          }
        }

        x = i;
        break;
      }
    }

    while (x < reference) {
      if (!isWordBreak(characters, x)) break;
      x += 1;
    }

    if (x < reference) ses->winx = x;
    return 1;
  }

  if (prefs.wordWrap) {
    if (ses->winx < 1) return 0;
    setWordWrapStart(ses->winx - amount);
    return 1;
  }

  return moveBrailleWindowLeft(amount);
}

int
shiftBrailleWindowRight (unsigned int amount) {
  if (isContracting()) {
    amount = getContractedLength(amount);
  } else if (prefs.wordWrap) {
    amount = getWordWrapLength(ses->winy, ses->winx, amount);
  }

  return moveBrailleWindowRight(amount);
}

void
slideBrailleWindowVertically (int y) {
  if ((y < ses->winy) || (y >= (int)(ses->winy + brl.textRows))) {
    y -= brl.textRows / 2;

    {
      int maxy = scr.rows - brl.textRows;
      if (y > maxy) y = maxy;
    }

    if (y < 0) y = 0;
    ses->winy = y;
  }
}

static int
isWithinBrailleWindow (int x, int y) {
  return (x >= ses->winx)
      && (x < (int)(ses->winx + textCount))
      && (y >= ses->winy)
      && (y < (int)(ses->winy + brl.textRows))
      ;
}

int
trackScreenCursor (int place) {
  if (!SCR_CURSOR_OK()) return 0;

  if (place) {
    cancelDelayedCursorTrackingAlarm();
  } else if (delayedCursorTrackingAlarm) {
    /* A cursor tracking motion has been delayed. If the cursor returned
     * to its initial location in the mean time then we discard and ignore
     * the previous motion. Otherwise we wait for the timer to expire.
     */
    if ((ses->dctx == scr.posx) && (ses->dcty == scr.posy)) {
      cancelDelayedCursorTrackingAlarm();
    }

    return 1;
  } else if ((prefs.cursorTrackingDelay > 0) && (ses->dctx != -1) &&
             !isWithinBrailleWindow(ses->trkx, ses->trky)) {
    /* The cursor may move spuriously while a program updates information
     * on a status bar. If cursor tracking is on and the cursor was
     * outside the braille window before it moved, we delay the tracking
     * motion for a while so as not to obnoxiously move the braille window
     * in case the cursor will eventually return to its initial location
     * within a short time.
     */
    ses->dctx = ses->trkx;
    ses->dcty = ses->trky;

    int delay = 250 << (prefs.cursorTrackingDelay - 1);
    asyncNewRelativeAlarm(&delayedCursorTrackingAlarm, delay,
                          handleDelayedCursorTrackingAlarm, NULL);

    return 1;
  }

  /* anything but -1 */
  ses->dctx = 0;
  ses->dcty = 0;

  if (isContracted) {
    slideBrailleWindowVertically(scr.posy);
    contractedTrack = 1;

    if (scr.posx > ses->winx) {
      if (scr.posx < (ses->winx + getContractedLength(textCount))) {
        return 1;
      }
    }

    ses->winx = scr.posx;
    shiftBrailleWindowLeft(halfWindowShift);
    return 1;
  }

  if (place && !isWithinBrailleWindow(scr.posx, scr.posy)) {
    placeBrailleWindowHorizontally(scr.posx);
  }

  if (prefs.slidingBrailleWindow) {
    {
      int width = scr.cols;
      ScreenCharacter characters[width];
      readScreenRow(scr.posy, width, characters);

      int column = findLastNonSpaceCharacter(characters, width);
      if (column < 0) column = 0;
      if (column < textCount) ses->winx = 0;
    }

    int reset = textCount * 3 / 10;
    int trigger = prefs.eagerSlidingBrailleWindow? textCount*3/20: 0;
    if (scr.posx == ses->winx) trigger = 1;

    if (scr.posx < (ses->winx + trigger)) {
      ses->winx = MAX(scr.posx-reset, 0);
    } else if (scr.posx >= (int)(ses->winx + textCount - trigger)) {
      ses->winx = MAX(MIN(scr.posx+reset+1, scr.cols)-(int)textCount, 0);
    }
  } else if (scr.posx < ses->winx) {
    ses->winx -= ((ses->winx - scr.posx - 1) / textCount + 1) * textCount;
    if (ses->winx < 0) ses->winx = 0;
  } else {
    ses->winx += (scr.posx - ses->winx) / textCount * textCount;
  }

  if (prefs.wordWrap) {
    int length = getWordWrapLength(ses->winy, ses->winx, textCount);
    int next = ses->winx + length;
    if (scr.posx >= next) ses->winx = next;
  }

  slideBrailleWindowVertically(scr.posy);
  return 1;
}

int
findFirstNonSpaceCharacter (const ScreenCharacter *characters, int count) {
  int index = 0;

  while (index < count) {
    if (!iswspace(characters[index].text)) return index;
    index += 1;
  }

  return -1;
}

int
findLastNonSpaceCharacter (const ScreenCharacter *characters, int count) {
  int index = count;

  while (index > 0)
    if (!iswspace(characters[--index].text))
      return index;

  return -1;
}

int
isAllSpaceCharacters (const ScreenCharacter *characters, int count) {
  return findFirstNonSpaceCharacter(characters, count) < 0;
}

#ifdef ENABLE_SPEECH_SUPPORT
SpeechSynthesizer spk;

int
haveSpeechSynthesizer (void) {
  return speech->definition.code != noSpeech.definition.code;
}

void
trackSpeech (void) {
  int location = spk.track.speechLocation;

  if (location != SPK_LOC_NONE) {
    placeBrailleWindowHorizontally(location % scr.cols);
    slideBrailleWindowVertically(spk.track.firstLine + (location / scr.cols));
    scheduleUpdate("speech tracked");
  }
}

int
isAutospeakActive (void) {
  if (!haveSpeechSynthesizer()) return 0;
  if (prefs.autospeak) return 1;
  if (haveBrailleDisplay()) return 0;
  return !opt_quietIfNoBraille;
}

void
sayScreenCharacters (const ScreenCharacter *characters, size_t count, SayOptions options) {
  wchar_t *textBuffer;

  if ((textBuffer = malloc(ARRAY_SIZE(textBuffer, count)))) {
    unsigned char *attributesBuffer;

    if ((attributesBuffer = malloc(ARRAY_SIZE(attributesBuffer, count)))) {
      wchar_t *text = textBuffer;
      unsigned char *attributes = attributesBuffer;

      const ScreenCharacter *character = characters;
      const ScreenCharacter *end = character + count;

      while (character < end) {
        *text++ = character->text;
        *attributes++ = getScreenColorAttributes(&character->color);
        character += 1;
      }

      sayWideCharacters(&spk, textBuffer, attributesBuffer, count, options);
      free(attributesBuffer);
    } else {
      logMallocError();
    }

    free(textBuffer);
  } else {
    logMallocError();
  }
}

void
speakCharacters (const ScreenCharacter *characters, size_t count, int spell, int interrupt) {
  SayOptions sayOptions = 0;
  if (interrupt) sayOptions |= SAY_OPT_MUTE_FIRST;

  if (isAllSpaceCharacters(characters, count)) {
    switch (prefs.speechWhitespaceIndicator) {
      default:
      case swsNone:
        break;

      case swsSaySpace: {
        wchar_t buffer[0X100];
        size_t length = makeWcharsFromUtf8(gettext("space"), buffer, ARRAY_COUNT(buffer));

        sayWideCharacters(&spk, buffer, NULL, length, sayOptions);
        break;
      }
    }
  } else if (count == 1) {
    wchar_t character = characters[0].text;
    unsigned char attributes = getScreenColorAttributes(&characters[0].color);
    const char *prefix = NULL;

    if (iswupper(character)) {
      switch (prefs.speechUppercaseIndicator) {
        default:
        case sucNone:
          break;

        case sucSayCap:
          // "cap" here, used during speech output, is short for "capital".
          // It is spoken just before an uppercase letter, e.g. "cap A".
          prefix = gettext("cap");
          break;

        case sucRaisePitch:
          sayOptions |= SAY_OPT_HIGHER_PITCH;
          break;
      }
    } else if (iswpunct(character)) {
      sayOptions |= SAY_OPT_ALL_PUNCTUATION;
    }

    if (prefix) {
      wchar_t textBuffer[0X100];
      size_t length = makeWcharsFromUtf8(prefix, textBuffer, ARRAY_COUNT(textBuffer));

      textBuffer[length++] = WC_C(' ');
      textBuffer[length++] = character;

      unsigned char attributesBuffer[length];
      memset(attributesBuffer, VGA_COLOR_DEFAULT, length);
      attributesBuffer[length-1] = attributes;

      sayWideCharacters(&spk, textBuffer, attributesBuffer, length, sayOptions);
    } else {
      sayWideCharacters(&spk, &character, &attributes, 1, sayOptions);
    }
  } else if (spell) {
    size_t length = count * 2;
    wchar_t textBuffer[length];
    unsigned char attributesBuffer[length];

    wchar_t *text = textBuffer;
    unsigned char *attributes = attributesBuffer;

    const ScreenCharacter *character = characters;
    const ScreenCharacter *end = character + count;

    while (character < end) {
      *text++ = character->text;
      *attributes++ = getScreenColorAttributes(&character->color);

      *text++ = WC_C(' ');
      *attributes++ = VGA_COLOR_DEFAULT;

      character += 1;
    }

    sayOptions |= SAY_OPT_ALL_PUNCTUATION;
    sayWideCharacters(&spk, textBuffer, attributesBuffer, length-1, sayOptions);
  } else {
    sayScreenCharacters(characters, count, sayOptions);
  }
}

int
speakIndent (const ScreenCharacter *characters, int count, int evenIfNoIndent) {
  int length = scr.cols;
  ScreenCharacter buffer[length];

  if (!characters) {
    readScreenRow(ses->spky, length, buffer);
    characters = buffer;
    count = length;
  }

  int indent = findFirstNonSpaceCharacter(characters, count);
  if ((indent < 1) && !evenIfNoIndent) return 0;

  char message[50];
  const char *text = message;

  if (indent < 0) {
    text = gettext("blank line");
  } else {
    snprintf(message, sizeof(message),
             "%s %d", gettext("indent"), indent);
  }

  logMessage(LOG_CATEGORY(SPEECH_EVENTS),
             "line indent: %d", indent);

  sayString(&spk, text, SAY_OPT_MUTE_FIRST);
  return 1;
}
#endif /* ENABLE_SPEECH_SUPPORT */

int
isContracting (void) {
  return isContractedBraille() && contractionTable;
}

int
getContractedLength (unsigned int outputLimit) {
  int inputLength = scr.cols - ses->winx;
  wchar_t inputBuffer[inputLength];
  readScreenText(ses->winx, ses->winy, inputLength, 1, inputBuffer);

  int outputLength = outputLimit;
  unsigned char outputBuffer[outputLength];

  int offsetCount = inputLength;
  int outputOffsets[offsetCount + 1];

  contractText(
    contractionTable, NULL,
    inputBuffer, &inputLength,
    outputBuffer, &outputLength,
    outputOffsets, getCursorOffsetForContracting()
  );

  for (int length=0; length<inputLength; length+=1) {
    int offset = outputOffsets[length];

    if (offset != CTB_NO_OFFSET) {
      if (offset >= outputLimit) {
        return length;
      }
    }
  }

  return inputLength;
}

int
showScreenCursor (void) {
  return scr.hasCursor
      && prefs.showScreenCursor
      && !(ses->hideScreenCursor || brl.hideCursor)
      ;
}

int
isSameText (
  const ScreenCharacter *character1,
  const ScreenCharacter *character2
) {
  return character1->text == character2->text;
}

int
isSameScreenColor (
  const ScreenCharacter *character1,
  const ScreenCharacter *character2
) {
  return sameScreenColors(&character1->color, &character2->color);
}

int
isSameCharacter (
  const ScreenCharacter *character1,
  const ScreenCharacter *character2
) {
  return isSameText(character1, character2) && isSameScreenColor(character1, character2);
}

int
isSameRow (
  const ScreenCharacter *characters1,
  const ScreenCharacter *characters2,
  int count,
  IsSameCharacter isSameCharacter
) {
  int i;
  for (i=0; i<count; ++i)
    if (!isSameCharacter(&characters1[i], &characters2[i]))
      return 0;

  return 1;
}

int
canBraille (void) {
  return braille && brl.buffer && !brl.noDisplay && !brl.isSuspended;
}

static unsigned int interruptEnabledCount;
static AsyncEvent *interruptEvent;
static int interruptPending;
static WaitResult waitResult;

typedef struct {
  WaitResult waitResult;
} InterruptEventParameters;

int
brlttyInterrupt (WaitResult waitResult) {
  if (interruptEvent) {
    InterruptEventParameters *iep;

    if ((iep = malloc(sizeof(*iep)))) {
      memset(iep, 0, sizeof(*iep));
      iep->waitResult = waitResult;

      if (asyncSignalEvent(interruptEvent, iep)) {
        return 1;
      }

      free(iep);
    } else {
      logMallocError();
    }
  }

  return 0;
}

ASYNC_EVENT_CALLBACK(handleCoreInterrupt) {
  InterruptEventParameters *iep = parameters->signalData;

  if (iep) {
    interruptPending = 1;
    waitResult = iep->waitResult;
    free(iep);
  }
}

int
brlttyEnableInterrupt (void) {
  if (!interruptEnabledCount) {
    if (!(interruptEvent = asyncNewEvent(handleCoreInterrupt, NULL))) {
      return 0;
    }
  }

  interruptEnabledCount += 1;
  return 1;
}

int
brlttyDisableInterrupt (void) {
  if (!interruptEnabledCount) return 0;

  if (!--interruptEnabledCount) {
    asyncDiscardEvent(interruptEvent);
    interruptEvent = NULL;
  }

  return 1;
}

typedef void UnmonitoredConditionHandler (const void *data);

static void
handleRoutingDone (const void *data) {
  const RoutingStatus *status = data;

  alert(
    (*status > ROUTING_STATUS_SUCCEESS)? ALERT_ROUTING_FAILED:
    ALERT_ROUTING_SUCCEEDED
  );

  ses->spkx = scr.posx;
  ses->spky = scr.posy;
}

static void
handleBrailleDriverFailed (const void *data) {
  restartBrailleDriver();
}

static time_t programTerminationRequestTime;
static int programTerminationRequestSignal;
static volatile sig_atomic_t programTerminationRequestCount;

typedef struct {
  UnmonitoredConditionHandler *handler;
  const void *data;
} UnmonitoredConditionDescriptor;

ASYNC_CONDITION_TESTER(checkUnmonitoredConditions) {
  UnmonitoredConditionDescriptor *ucd = data;

  if (interruptPending) {
    logMessage(LOG_CATEGORY(ASYNC_EVENTS), "interrupt pending");
    ucd->data = &waitResult;
    interruptPending = 0;
    return 1;
  }

  if (programTerminationRequestCount) {
    // This is a memory read barrier to ensure that the most recent
    // time and number for the program termination signal are seen.
    __sync_synchronize();

    logMessage(LOG_CATEGORY(ASYNC_EVENTS),
      "program termination requested: Count=%ld Signal=%d",
      (long)programTerminationRequestCount, programTerminationRequestSignal
    );

    static const WaitResult result = WAIT_STOP;
    ucd->data = &result;
    return 1;
  }

  {
    static RoutingStatus status;

    if ((status = getRoutingStatus(0)) != ROUTING_STATUS_NONE) {
      logMessage(LOG_CATEGORY(ASYNC_EVENTS), "routing status: %u", status);
      ucd->handler = handleRoutingDone;
      ucd->data = &status;
      return 1;
    }
  }

  if (brl.hasFailed) {
    logMessage(LOG_CATEGORY(ASYNC_EVENTS), "braille driver failed");
    ucd->handler = handleBrailleDriverFailed;
    return 1;
  }

  return 0;
}

WaitResult
brlttyWait (int duration) {
  UnmonitoredConditionDescriptor ucd = {
    .handler = NULL,
    .data = NULL
  };

  if (asyncAwaitCondition(duration, checkUnmonitoredConditions, &ucd)) {
    if (!ucd.handler) {
      const WaitResult *result = ucd.data;
      return *result;
    }

    ucd.handler(ucd.data);
  }

  return 1;
}

int
showDotPattern (unsigned char dots, unsigned char duration) {
  if (braille->writeStatus && (brl.statusColumns > 0)) {
    unsigned int length = brl.statusColumns * brl.statusRows;
    unsigned char cells[length];        /* status cell buffer */
    memset(cells, dots, length);
    if (!braille->writeStatus(&brl, cells)) return 0;
  }

  memset(brl.buffer, dots, brl.textColumns*brl.textRows);
  if (!writeBrailleWindow(&brl, NULL, SCQ_GOOD)) return 0;

  drainBrailleOutput(&brl, duration);
  return 1;
}

static void
exitSessions (void *data) {
  cancelDelayedCursorTrackingAlarm();

  if (ses) {
    popCommandEnvironment();
    ses = NULL;
  }

  deallocateSessionEntries();
}

static AsyncEvent *addCoreTaskEvent = NULL;

static int
startCoreTasks (void) {
  if (!addCoreTaskEvent) {
    if (!(addCoreTaskEvent = asyncNewAddTaskEvent())) {
      return 0;
    }
  }

  return 1;
}

static void
stopCoreTasks (void) {
  if (addCoreTaskEvent) {
    asyncDiscardEvent(addCoreTaskEvent);
    addCoreTaskEvent = NULL;
  }
}

static void
logCoreTaskAction (CoreTaskCallback *callback, const char *action) {
  logSymbol(LOG_DEBUG, callback, "%s core task", action);
}

typedef struct {
  struct {
    CoreTaskCallback *callback;
    void *data;
  } run;

  struct {
    AsyncEvent *event;
    unsigned char finished:1;
  } wait;
} CoreTaskData;

ASYNC_TASK_CALLBACK(handleCoreTask) {
  CoreTaskData *ctd = data;

  {
    CoreTaskCallback *callback = ctd->run.callback;
    logCoreTaskAction(callback, "starting");
    callback(ctd->run.data);
    logCoreTaskAction(callback, "finished");
  }

  {
    AsyncEvent *event = ctd->wait.event;
    if (event) asyncSignalEvent(event, ctd);
  }
}

ASYNC_CONDITION_TESTER(testCoreTaskFinished) {
  CoreTaskData *ctd = data;
  return ctd->wait.finished;
}

ASYNC_EVENT_CALLBACK(setCoreTaskFinished) {
  CoreTaskData *ctd = parameters->signalData;
  ctd->wait.finished = 1;
}

int
runCoreTask (CoreTaskCallback *callback, void *data, int wait) {
  int wasScheduled = 0;

  if (addCoreTaskEvent) {
    CoreTaskData *ctd;

    if ((ctd = malloc(sizeof(*ctd)))) {
      memset(ctd, 0, sizeof(*ctd));

      ctd->run.callback = callback;
      ctd->run.data = data;

      ctd->wait.event = NULL;
      ctd->wait.finished = 0;

      if (!wait || (ctd->wait.event = asyncNewEvent(setCoreTaskFinished, NULL))) {
        logCoreTaskAction(callback, "scheduling");

        if (asyncAddTask(addCoreTaskEvent, handleCoreTask, ctd)) {
          wasScheduled = 1;

          if (wait) {
            logCoreTaskAction(callback, "awaiting");
            asyncWaitFor(testCoreTaskFinished, ctd);
            logCoreTaskAction(callback, "completed");
          }
        }

        {
          AsyncEvent *event = ctd->wait.event;
          if (event) asyncDiscardEvent(event);
        }
      }

      free(ctd);
    } else {
      logMallocError();
    }
  } else {
    logMessage(LOG_ERR, "core tasks not started");
  }

  return wasScheduled;
}

#ifdef ASYNC_CAN_HANDLE_SIGNALS
ASYNC_SIGNAL_HANDLER(handleProgramTerminationRequest) {
  time_t now = time(NULL);

  int reset = difftime(now, programTerminationRequestTime)
            > PROGRAM_TERMINATION_REQUEST_RESET_SECONDS;

  int count = reset? 0: programTerminationRequestCount;
  if (++count > PROGRAM_TERMINATION_REQUEST_COUNT_THRESHOLD) exit(1);

  programTerminationRequestTime = now;
  programTerminationRequestSignal = signalNumber;

  // This is a memory write barrier to ensure that the time and number
  // for this signal will be visible before its count is adjusted.
  __sync_synchronize();

  programTerminationRequestCount = count;
}

#ifdef SIGCHLD
ASYNC_SIGNAL_HANDLER(handleChildDeath) {
}
#endif /* SIGCHLD */
#endif /* ASYNC_CAN_HANDLE_SIGNALS */

ProgramExitStatus
brlttyConstruct (int argc, char *argv[]) {
  {
    TimeValue now;
    getMonotonicTime(&now);
    srand(now.seconds ^ now.nanoseconds);
  }

  {
    ProgramExitStatus exitStatus = brlttyPrepare(argc, argv);
    if (exitStatus != PROG_EXIT_SUCCESS) return exitStatus;
  }

  programTerminationRequestTime = time(NULL);
  programTerminationRequestSignal = 0;
  programTerminationRequestCount = 0;

#ifdef ASYNC_CAN_BLOCK_SIGNALS
  asyncBlockObtainableSignals();
#endif /* ASYNC_CAN_BLOCK_SIGNALS */

#ifdef ASYNC_CAN_HANDLE_SIGNALS
#ifdef SIGPIPE
  /* We ignore SIGPIPE before calling brlttyStart() so that a driver
   * which uses a broken pipe won't abort program execution.
   */
  asyncIgnoreSignal(SIGPIPE, NULL);
#endif /* SIGPIPE */

#ifdef SIGTERM
  asyncHandleSignal(SIGTERM, handleProgramTerminationRequest, NULL);
#endif /* SIGTERM */

#ifdef SIGINT
  asyncHandleSignal(SIGINT, handleProgramTerminationRequest, NULL);
#endif /* SIGINT */

#ifdef SIGCHLD
  asyncHandleSignal(SIGCHLD, handleChildDeath, NULL);
#endif /* SIGCHLD */
#endif /* ASYNC_CAN_HANDLE_SIGNALS */

  interruptEnabledCount = 0;
  interruptEvent = NULL;
  interruptPending = 0;

  delayedCursorTrackingAlarm = NULL;

  startCoreTasks();
  beginCommandQueue();
  beginUpdates();
  suspendUpdates();

  {
    ProgramExitStatus exitStatus = brlttyStart();
    if (exitStatus != PROG_EXIT_SUCCESS) return exitStatus;
  }

  onProgramExit("sessions", exitSessions, NULL);
  setSessionEntry();
  ses->trkx = scr.posx; ses->trky = scr.posy;
  if (!trackScreenCursor(1)) ses->winx = ses->winy = 0;
  ses->motx = ses->winx; ses->moty = ses->winy;
  ses->spkx = ses->winx; ses->spky = ses->winy;

  resumeUpdates(1);
  return PROG_EXIT_SUCCESS;
}

int
brlttyDestruct (void) {
  if (prefs.saveOnExit) savePreferences();

  suspendUpdates();
  stopCoreTasks();

  endProgram();
  endCommandQueue();
  return 1;
}
