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

#include "parameters.h"
#include "embed.h"
#include "log.h"
#include "report.h"
#include "strfmt.h"
#include "pgmprivs.h"
#include "lock.h"
#include "activity.h"
#include "update.h"
#include "cmd.h"
#include "cmd_navigation.h"
#include "brl.h"
#include "brl_utils.h"
#include "spk.h"
#include "spk_input.h"
#include "scr.h"
#include "scr_special.h"
#include "status.h"
#include "blink.h"
#include "variables.h"
#include "datafile.h"
#include "ttb.h"
#include "ctb.h"
#include "atb.h"
#include "ktb.h"
#include "ktb_keyboard.h"
#include "kbd.h"
#include "alert.h"
#include "bell.h"
#include "leds.h"
#include "tune.h"
#include "notes.h"
#include "message.h"
#include "file.h"
#include "parse.h"
#include "dynld.h"
#include "async_handle.h"
#include "async_alarm.h"
#include "program.h"
#include "messages.h"
#include "revision.h"
#include "service.h"
#include "cmdline.h"
#include "options.h"
#include "profile_types.h"
#include "brl_input.h"
#include "cmd_queue.h"
#include "core.h"
#include "api_control.h"
#include "prefs.h"
#include "utf8.h"

#include "io_generic.h"
#include "io_usb.h"
#include "io_bluetooth.h"

#ifdef __MINGW32__
int isWindowsService = 0;
#endif /* __MINGW32__ */

#ifdef __MSDOS__
#include "system_msdos.h"
#endif /* __MSDOS__ */

static void
makeProgramBanner (char *buffer, size_t size, int includeRevision) {
  const char *revision = includeRevision? getRevisionIdentifier(): "";
  snprintf(buffer, size, "%s %s%s%s",
           PACKAGE_NAME, PACKAGE_VERSION,
           (*revision? " rev ": ""), revision);
}

static void
logProgramBanner (void) {
  char banner[0X100];
  makeProgramBanner(banner, sizeof(banner), 1);

  {
    int pushed = pushLogPrefix("");
    logMessage(LOG_NOTICE, "%s [%s]", banner, PACKAGE_URL);
    if (pushed) popLogPrefix();
  }
}

static const char optionValue_off[] = "off";

static void
onSettingChange (char **setting, const char *newValue, const char *label) {
  const char *oldValue = *setting;

  if (!*oldValue) oldValue = optionValue_off;
  if (!*newValue) newValue = optionValue_off;

  logMessage(LOG_DEBUG,
    "%s changed: %s -> %s",
    label, oldValue, newValue
  );

  changeStringSetting(setting, newValue);
}

static void
logProperty (const char *value, const char *variable, const char *label) {
  if (value && *value) {
    if (variable) setGlobalVariable(variable, value);
  } else {
    value = optionValue_off;
  }

  logMessage(LOG_INFO, "%s: %s", label, value);
}

static const char optionOperand_none[] = "no";
static const char optionOperand_autodetect[] = "auto";
static const char optionOperand_off[] = "";

static const char *const *const fallbackBrailleDrivers =
  NULL_TERMINATED_STRING_ARRAY(
    optionOperand_none
  );

static const char *const *const autodetectableBrailleDrivers_serial =
  NULL_TERMINATED_STRING_ARRAY(
    "md", "pm", "ts", "ht", "bn", "al", "bm", "pg", "sk"
  );

static const char *const *const autodetectableBrailleDrivers_USB =
  NULL_TERMINATED_STRING_ARRAY(
    "al", "bm", "bn", "cn", "dp", "eu", "fs", "hd", "hm", "ht", "hw", "ic", "mt", "pg", "pm", "sk", "vo"
  );

static const char *const *const autodetectableBrailleDrivers_Bluetooth =
  NULL_TERMINATED_STRING_ARRAY(
    "np", "ht", "al", "bm"
  );

#define SERVICE_NAME "BrlAPI"
#define SERVICE_DESCRIPTION "Braille Devices API"

static
STR_BEGIN_FORMATTER(formatLogLevelString, unsigned int index)
  switch (index) {
    case 0:
      STR_PRINTF("0-%u", logLevelCount-1);
      break;

    case 1: {
      for (unsigned int level=0; level<logLevelCount; level+=1) {
        if (level) STR_PRINTF(" ");
        STR_PRINTF("%s", logLevelNames[level]);
      }

      break;
    }

    case 2: {
      LogCategoryIndex category;

      STR_PRINTF("%s", logCategoryName_all);

      for (category=0; category<LOG_CATEGORY_COUNT; category+=1) {
        const char *name = getLogCategoryName(category);

        if (name && *name) {
          STR_PRINTF(" %s", name);
        }
      }

      break;
    }

    case 3:
      STR_PRINTF("%c", logCategoryPrefix_disable);
      break;

    default:
      break;
  }
STR_END_FORMATTER

static const char *const *const screenContentQualityChoices =
  NULL_TERMINATED_STRING_ARRAY(
    "none", "low", "poor", "fair", "good", "high"
  );

STR_BEGIN_FORMATTER(formatScreenContentQualityChoices, unsigned int index)
  switch (index) {
    case 0: {
      const char *const *choices = screenContentQualityChoices;
      const char *const *choice = choices;

      while (*choice) {
        if (choice != choices) STR_PRINTF(" ");
        STR_PRINTF("%s", *choice);
        choice += 1;
      }

      break;
    }

    default:
      break;
  }
STR_END_FORMATTER

static int opt_installService;
static const char *const optionStrings_InstallService[] = {
  SERVICE_NAME,
  NULL
};

static int opt_removeService;
static const char *const optionStrings_RemoveService[] = {
  SERVICE_NAME,
  NULL
};

char *opt_startMessage;
char *opt_stopMessage;
char *opt_localeDirectory;

static int opt_version;
static int opt_verify;
static int opt_quiet;
int opt_noDaemon;
int opt_logToStandardError;
static char *opt_logLevel;
char *opt_logFile;
int opt_environmentVariables;
int opt_bootParameters = 1;
static char *opt_messageTime;

static int opt_cancelExecution;
static const char *const optionStrings_CancelExecution[] = {
  PACKAGE_TARNAME,
  NULL
};

char *opt_promptPatterns;

int opt_stayPrivileged;
char *opt_privilegeParameters;

char *opt_pidFile;
char *opt_configurationFile;

char *opt_driversDirectory;

char *opt_brailleDevice;
static char **brailleDevices = NULL;
static const char *brailleDevice = NULL;
int opt_releaseDevice;

char *opt_brailleDriver;
static char **brailleDrivers = NULL;
static const BrailleDriver *brailleDriver = NULL;
static void *brailleObject = NULL;
static int brailleDriverConstructed;

char *opt_brailleParameters;
static char *brailleParameters = NULL;
static char **brailleDriverParameters = NULL;

char *opt_preferencesFile;
char *opt_overridePreferences;

static char *oldPreferencesFile = NULL;
static int oldPreferencesEnabled = 1;

char *opt_tablesDirectory;
char *opt_textTable;
char *opt_contractionTable;
char *opt_attributesTable;

char *opt_keyboardTable;
KeyTable *keyboardTable = NULL;
static KeyboardMonitorObject *keyboardMonitor = NULL;

char *opt_keyboardProperties;
static KeyboardProperties keyboardProperties;

int opt_guiKeyboardEnabled;
char *opt_guiKeyboardTable;
static KeyTable *guiKeyboardTable = NULL;

#ifdef ENABLE_API
int opt_noApi;
char *opt_apiParameters;
static char **apiParameters = NULL;
#endif /* ENABLE_API */

#ifdef ENABLE_SPEECH_SUPPORT
char *opt_speechDriver;
static char **speechDrivers = NULL;
static const SpeechDriver *speechDriver = NULL;
static void *speechObject = NULL;

char *opt_speechParameters;
static char *speechParameters = NULL;
static char **speechDriverParameters = NULL;

char *opt_speechInput;
static SpeechInputObject *speechInputObject;

int opt_quietIfNoBraille;
static char *opt_autospeakThreshold;
unsigned int autospeakMinimumScreenContentQuality;

static void
setAutospeakThreshold (void) {
  const char *choice = opt_autospeakThreshold;

  int ok = validateChoice(
    &autospeakMinimumScreenContentQuality,
    choice, screenContentQualityChoices
  );

  if (!ok) {
    logMessage(LOG_ERR, "%s: %s",
      gettext("unknown screen content quality"),
      choice
    );
  }

  logProperty(
    screenContentQualityChoices[autospeakMinimumScreenContentQuality],
    "autospeakThreshold", "Autospeak Threshold"
  );
}
#endif /* ENABLE_SPEECH_SUPPORT */

char *opt_screenDriver;
static char **screenDrivers = NULL;
static const ScreenDriver *screenDriver = NULL;
static void *screenObject = NULL;
char *opt_screenParameters;
static char *screenParameters = NULL;
static char **screenDriverParameters = NULL;

static const char *const optionStrings_TextTable[] = {
  optionOperand_autodetect,
  NULL
};

static const char *const optionStrings_BrailleDriver[] = {
  optionOperand_autodetect,
  optionOperand_none,
  BRAILLE_DRIVER_CODES,
  NULL
};

static const char *const optionStrings_ScreenDriver[] = {
  optionOperand_autodetect,
  optionOperand_none,
  SCREEN_DRIVER_CODES,
  NULL
};

#ifdef ENABLE_SPEECH_SUPPORT
static const char *const optionStrings_SpeechDriver[] = {
  optionOperand_autodetect,
  optionOperand_none,
  SPEECH_DRIVER_CODES,
  NULL
};
#endif /* ENABLE_SPEECH_SUPPORT */

BEGIN_COMMAND_LINE_OPTIONS(programOptions)
  { .word = "version",
    .letter = 'V',
    .setting.flag = &opt_version,
    .description = strtext("Log the versions of the core, API, and built-in drivers, and then exit.")
  },

  { .word = "environment-variables",
    .letter = 'E',
    .setting.flag = &opt_environmentVariables,
    .description = strtext("Recognize environment variables.")
  },

  { .word = "configuration-file",
    .letter = 'f',
    .flags = OPT_EnvVar,
    .argument = strtext("file"),
    .setting.string = &opt_configurationFile,
    .internal.setting = CONFIGURATION_DIRECTORY "/" CONFIGURATION_FILE,
    .internal.adjust = toAbsoluteInstallPath,
    .description = strtext("Path to default settings file.")
  },

  { .word = "braille-driver",
    .letter = 'b',
    .bootParameter = 1,
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("driver,..."),
    .setting.string = &opt_brailleDriver,
    .internal.setting = optionOperand_autodetect,
    .description = strtext("Braille driver code (%s, %s, or one of {%s})."),
    .strings.array = optionStrings_BrailleDriver
  },

  { .word = "braille-parameters",
    .letter = 'B',
    .bootParameter = 4,
    .flags = OPT_Extend | OPT_Config | OPT_EnvVar,
    .argument = strtext("name=value,..."),
    .setting.string = &opt_brailleParameters,
    .internal.setting = BRAILLE_PARAMETERS,
    .description = strtext("Parameters for the braille driver.")
  },

  { .word = "braille-device",
    .letter = 'd',
    .bootParameter = 2,
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("identifier,..."),
    .setting.string = &opt_brailleDevice,
    .internal.setting = BRAILLE_DEVICE,
    .description = strtext("Device for accessing braille display.")
  },

  { .word = "release-device",
    .letter = 'r',
    .flags = OPT_Config | OPT_EnvVar,
    .setting.flag = &opt_releaseDevice,
#ifdef WINDOWS
    .internal.setting = OPT_WORD_TRUE,
#else /* WINDOWS */
    .internal.setting = OPT_WORD_FALSE,
#endif /* WINDOWS */
    .description = strtext("Release braille device when screen or window is unreadable.")
  },

  { .word = "text-table",
    .letter = 't',
    .bootParameter = 3,
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("file"),
    .setting.string = &opt_textTable,
    .internal.setting = optionOperand_autodetect,
    .description = strtext("Name of or path to text table (or %s)."),
    .strings.array = optionStrings_TextTable
  },

  { .word = "contraction-table",
    .letter = 'c',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("file"),
    .setting.string = &opt_contractionTable,
    .internal.setting = optionOperand_autodetect,
    .description = strtext("Name of or path to contraction table.")
  },

  { .word = "attributes-table",
    .letter = 'a',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("file"),
    .setting.string = &opt_attributesTable,
    .description = strtext("Name of or path to attributes table.")
  },

#ifdef ENABLE_SPEECH_SUPPORT
  { .word = "speech-driver",
    .letter = 's',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("driver,..."),
    .setting.string = &opt_speechDriver,
    .internal.setting = optionOperand_autodetect,
    .description = strtext("Speech driver code (%s, %s, or one of {%s})."),
    .strings.array = optionStrings_SpeechDriver
  },

  { .word = "speech-parameters",
    .letter = 'S',
    .flags = OPT_Extend | OPT_Config | OPT_EnvVar,
    .argument = strtext("name=value,..."),
    .setting.string = &opt_speechParameters,
    .internal.setting = SPEECH_PARAMETERS,
    .description = strtext("Parameters for the speech driver.")
  },

  { .word = "quiet-if-no-braille",
    .letter = 'Q',
    .flags = OPT_Config | OPT_EnvVar,
    .setting.flag = &opt_quietIfNoBraille,
    .description = strtext("Do not autospeak when braille is not being used.")
  },

  { .word = "speech-input",
    .letter = 'i',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("file"),
    .setting.string = &opt_speechInput,
    .description = strtext("Name of or path to speech input object.")
  },

  { .word = "autospeak-threshold",
    .flags = OPT_Config | OPT_EnvVar | OPT_Format,
    .argument = strtext("quality"),
    .setting.string = &opt_autospeakThreshold,
    .description = strtext("Minimum screen content quality to autospeak (one of {%s})."),
    .strings.format = formatScreenContentQualityChoices
  },
#endif /* ENABLE_SPEECH_SUPPORT */

  { .word = "screen-driver",
    .letter = 'x',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("driver,..."),
    .setting.string = &opt_screenDriver,
    .internal.setting = DEFAULT_SCREEN_DRIVER,
    .description = strtext("Screen driver code (%s, %s, or one of {%s})."),
    .strings.array = optionStrings_ScreenDriver
  },

  { .word = "screen-parameters",
    .letter = 'X',
    .flags = OPT_Extend | OPT_Config | OPT_EnvVar,
    .argument = strtext("name=value,..."),
    .setting.string = &opt_screenParameters,
    .internal.setting = SCREEN_PARAMETERS,
    .description = strtext("Parameters for the screen driver.")
  },

  { .word = "keyboard-table",
    .letter = 'k',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("file"),
    .setting.string = &opt_keyboardTable,
    .internal.setting = optionOperand_off,
    .description = strtext("Name of or path to keyboard table.")
  },

  { .word = "keyboard-properties",
    .letter = 'K',
    .flags = OPT_Extend | OPT_Config | OPT_EnvVar,
    .argument = strtext("name=value,..."),
    .setting.string = &opt_keyboardProperties,
    .description = strtext("Properties of eligible keyboards.")
  },

  { .word = "gui-keyboard-enabled",
    .letter = 'g',
    .flags = OPT_Config | OPT_EnvVar,
    .setting.flag = &opt_guiKeyboardEnabled,
    .description = strtext("Handle keyboard key events when using a Linux tty in graphics mode.")
  },

  { .word = "gui-keyboard-table",
    .letter = 'G',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("file"),
    .setting.string = &opt_guiKeyboardTable,
    .internal.setting = optionOperand_off,
    .description = strtext("Name of or path to GUI keyboard table.")
  },

  { .word = "preferences-file",
    .letter = 'F',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("file"),
    .setting.string = &opt_preferencesFile,
    .internal.setting = PREFERENCES_FILE,
    .description = strtext("Name of or path to default preferences file.")
  },

  { .word = "override-preferences",
    .letter = 'o',
    .flags = OPT_Extend | OPT_Config | OPT_EnvVar,
    .argument = strtext("name=value,..."),
    .setting.string = &opt_overridePreferences,
    .description = strtext("Explicit preference settings.")
  },

#ifdef ENABLE_API
  { .word = "no-api",
    .letter = 'N',
    .flags = OPT_Config | OPT_EnvVar,
    .setting.flag = &opt_noApi,
    .description = strtext("Disable the application programming interface.")
  },

  { .word = "api-parameters",
    .letter = 'A',
    .flags = OPT_Extend | OPT_Config | OPT_EnvVar,
    .argument = strtext("name=value,..."),
    .setting.string = &opt_apiParameters,
    .internal.setting = API_PARAMETERS,
    .description = strtext("Parameters for the application programming interface.")
  },
#endif /* ENABLE_API */

  { .word = "quiet",
    .letter = 'q',
    .flags = OPT_Config | OPT_EnvVar,
    .setting.flag = &opt_quiet,
    .description = strtext("Suppress start-up messages.")
  },

  { .word = "log-level",
    .letter = 'l',
    .flags = OPT_Extend | OPT_Config | OPT_EnvVar | OPT_Format,
    .argument = strtext("lvl|cat,..."),
    .setting.string = &opt_logLevel,
    .description = strtext("Logging level (%s or one of {%s}) and/or log categories to enable (any combination of {%s}, each optionally prefixed by %s to disable)."),
    .strings.format = formatLogLevelString
  },

  { .word = "log-file",
    .letter = 'L',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("file"),
    .setting.string = &opt_logFile,
    .description = strtext("Path to log file.")
  },

  { .word = "standard-error",
    .letter = 'e',
    .setting.flag = &opt_logToStandardError,
    .description = strtext("Log to standard error rather than to the system log.")
  },

  { .word = "no-daemon",
    .letter = 'n',
    .setting.flag = &opt_noDaemon,
    .description = strtext("Remain a foreground process.")
  },

  { .word = "stay-privileged",
    .letter = 'z',
    .flags = OPT_Config | OPT_EnvVar,
    .setting.flag = &opt_stayPrivileged,
    .description = strtext("Don't switch to an unprivileged user or relinquish any privileges (group memberships, capabilities, etc).")
  },

  { .word = "privilege-parameters",
    .letter = 'Z',
    .flags = OPT_Extend | OPT_Config | OPT_EnvVar,
    .argument = strtext("name=value,..."),
    .setting.string = &opt_privilegeParameters,
    .internal.setting = PRIVILEGE_PARAMETERS,
    .description = strtext("Parameters for the privilege establishment stage.")
  },

  { .word = "message-time",
    .letter = 'M',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("csecs"),
    .setting.string = &opt_messageTime,
    .description = strtext("Message hold timeout (in 10ms units).")
  },

  { .word = "start-message",
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("text"),
    .setting.string = &opt_startMessage,
    .description = strtext("The text to be shown when the braille driver starts and to be spoken when the speech driver starts.")
  },

  { .word = "stop-message",
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("text"),
    .setting.string = &opt_stopMessage,
    .description = strtext("The text to be shown when the braille driver stops.")
  },

  { .word = "prompt-patterns",
    .flags = OPT_Extend | OPT_Config | OPT_EnvVar,
    .argument = strtext("regexp,..."),
    .setting.string = &opt_promptPatterns,
    .description = strtext("Patterns that match command prompts.")
  },

#ifdef HAVE_PCM_SUPPORT
  { .word = "pcm-device",
    .letter = 'p',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("device"),
    .setting.string = &opt_pcmDevice,
    .description = strtext("PCM (soundcard digital audio) device specifier.")
  },
#endif /* HAVE_PCM_SUPPORT */

#ifdef HAVE_MIDI_SUPPORT
  { .word = "midi-device",
    .letter = 'm',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("device"),
    .setting.string = &opt_midiDevice,
    .description = strtext("MIDI (Musical Instrument Digital Interface) device specifier.")
  },
#endif /* HAVE_MIDI_SUPPORT */

  { .word = "tables-directory",
    .letter = 'T',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("directory"),
    .setting.string = &opt_tablesDirectory,
    .internal.setting = TABLES_DIRECTORY,
    .internal.adjust = toAbsoluteInstallPath,
    .description = strtext("Path to directory containing tables.")
  },

  { .word = "drivers-directory",
    .letter = 'D',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("directory"),
    .setting.string = &opt_driversDirectory,
    .internal.setting = DRIVERS_DIRECTORY,
    .internal.adjust = toAbsoluteInstallPath,
    .description = strtext("Path to directory containing drivers.")
  },

  { .word = "helpers-directory",
    .letter = 'H',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("directory"),
    .setting.string = &opt_helpersDirectory,
    .internal.setting = HELPERS_DIRECTORY,
    .internal.adjust = toAbsoluteInstallPath,
    .description = strtext("Path to directory containing helper commands.")
  },

  { .word = "updatable-directory",
    .letter = 'U',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("directory"),
    .setting.string = &opt_updatableDirectory,
    .internal.setting = UPDATABLE_DIRECTORY,
    .internal.adjust = toAbsoluteInstallPath,
    .description = strtext("Path to directory which contains files that can be updated.")
  },

  { .word = "writable-directory",
    .letter = 'W',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("directory"),
    .setting.string = &opt_writableDirectory,
    .internal.setting = WRITABLE_DIRECTORY,
    .internal.adjust = toAbsoluteInstallPath,
    .description = strtext("Path to directory which can be written to.")
  },

  { .word = "locale-directory",
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("directory"),
    .setting.string = &opt_localeDirectory,
    .internal.setting = LOCALE_DIRECTORY,
    .internal.adjust = toAbsoluteInstallPath,
    .description = strtext("Path to directory which contains message localizations.")
  },

  { .word = "pid-file",
    .letter = 'P',
    .flags = OPT_Config | OPT_EnvVar,
    .argument = strtext("file"),
    .setting.string = &opt_pidFile,
    .internal.adjust = toAbsoluteInstallPath,
    .description = strtext("Path to process identifier file.")
  },

  { .word = "cancel-execution",
    .letter = 'C',
    .setting.flag = &opt_cancelExecution,
    .description = strtext("Stop an existing instance of %s, and then exit."),
    .strings.array = optionStrings_CancelExecution
  },

  { .word = "install-service",
    .letter = 'I',
    .setting.flag = &opt_installService,
    .description = strtext("Install the %s service, and then exit."),
    .strings.array = optionStrings_InstallService
  },

  { .word = "remove-service",
    .letter = 'R',
    .setting.flag = &opt_removeService,
    .description = strtext("Remove the %s service, and then exit."),
    .strings.array = optionStrings_RemoveService
  },

  { .word = "verify",
    .letter = 'v',
    .setting.flag = &opt_verify,
    .description = strtext("Write the start-up logs, and then exit.")
  },
END_COMMAND_LINE_OPTIONS(programOptions)

BEGIN_COMMAND_LINE_PARAMETERS(programParameters)
END_COMMAND_LINE_PARAMETERS(programParameters)

BEGIN_COMMAND_LINE_NOTES(programNotes)
END_COMMAND_LINE_NOTES

BEGIN_COMMAND_LINE_DESCRIPTOR(programDescriptor)
  .name = "brltty",
  .purpose = strtext("Screen reader for those who use a braille device."),

  .options = &programOptions,
  .parameters = &programParameters,
  .notes = COMMAND_LINE_NOTES(programNotes),

  .configurationFile = &opt_configurationFile,
  .doEnvironmentVariables = &opt_environmentVariables,
  .doBootParameters = &opt_bootParameters,
END_COMMAND_LINE_DESCRIPTOR

int
changeLogLevel (const char *operand) {
  int ok = 1;
  char **strings = splitString(operand, ',', NULL);

  if (strings) {
    char **string = strings;

    while (*string) {
      unsigned int level;

      if (isLogLevel(&level, *string)) {
        systemLogLevel = level;
      } else if (!setLogCategory(*string)) {
        logMessage(LOG_ERR, "%s: %s", gettext("unknown log level or category"), *string);
        ok = 0;
      }

      string += 1;
    }

    deallocateStrings(strings);
  }

  return ok;
}

int
changeLogCategories (const char *operand) {
  disableAllLogCategories();
  return changeLogLevel(operand);
}

static void
exitLog (void *data) {
  closeSystemLog();
  closeLogFile();
}

static void
setLogLevels (void) {
  systemLogLevel = LOG_NOTICE;
  disableAllLogCategories();
  changeLogLevel(opt_logLevel);

  {
    unsigned char level;

    if (opt_logToStandardError) {
      level = systemLogLevel;
    } else {
      level = LOG_NOTICE;
      if (opt_version || opt_verify) level += 1;
      if (opt_quiet) level -= 1;
    }

    stderrLogLevel = level;
  }
}

static void
establishPrivileges (void) {
  const char *platform = getPrivilegeParametersPlatform();
  const char *const *names = getPrivilegeParameterNames();
  char **parameters = getParameters(names, platform, opt_privilegeParameters);

  if (parameters) {
    logParameters(names, parameters, "Privilege Parameter");
    establishProgramPrivileges(parameters, opt_stayPrivileged);
    deallocateStrings(parameters);
  }
}

ProgramExitStatus
brlttyPrepare (int argc, char *argv[]) {
  {
    ProgramExitStatus exitStatus = processCommandLine(&programDescriptor, &argc, &argv);

    switch (exitStatus) {
      case PROG_EXIT_SYNTAX:
      case PROG_EXIT_SUCCESS:
        break;

      default:
        return exitStatus;
    }
  }

  setMessagesDirectory(opt_localeDirectory);
  anchorRelativePath(&opt_preferencesFile, getUpdatableDirectory());

  setLogLevels();
  onProgramExit("log", exitLog, NULL);

  {
    const char *logFile;

    if (*opt_logFile) {
      logFile = opt_logFile;
      openLogFile(logFile);
    } else {
      logFile = "<system>";
      openSystemLog();
    }

    logProgramBanner();
    logProperty(logFile, "logFile", "Log File");
    logProperty(opt_logLevel, "logLevel", "Log Level");
  }

  logProperty(getMessagesLocale(), "messagesLocale", "Messages Locale");
  logProperty(getMessagesDomain(), "messagesDomain", "Messages Domain");
  logProperty(getMessagesDirectory(), "messagesDirectory", "Messages Directory");

#ifdef ENABLE_SPEECH_SUPPORT
  setAutospeakThreshold();
#endif /* ENABLE_SPEECH_SUPPORT */

  establishPrivileges();
  return PROG_EXIT_SUCCESS;
}

static int
setTextTable (const char *name) {
  if (!name) name = "";
  if (!replaceTextTable(opt_tablesDirectory, name)) return 0;

  if (!*name) name = TEXT_TABLE;
  changeStringSetting(&opt_textTable, name);

  api.updateParameter(BRLAPI_PARAM_COMPUTER_BRAILLE_TABLE, 0);
  return 1;
}

static int
setTextTableForLocale (void) {
  changeStringSetting(&opt_textTable, "");
  char *name = getTextTableForLocale(opt_tablesDirectory);

  if (name) {
    logMessage(LOG_DEBUG, "using autoselected text table: %s", name);
    int ok = setTextTable(name);
    free(name);
    if (ok) return 1;
  }

  return 0;
}

int
changeTextTable (const char *name) {
  if (strcmp(name, optionOperand_autodetect) == 0) {
    return setTextTableForLocale();
  }

  return setTextTable(name);
}

static void
exitTextTable (void *data) {
  setTextTable(NULL);
}

static int
setContractionTable (const char *name) {
  if (!name) name = "";
  if (!replaceContractionTable(opt_tablesDirectory, name)) return 0;

  if (!*name) name = CONTRACTION_TABLE;
  changeStringSetting(&opt_contractionTable, name);

  api.updateParameter(BRLAPI_PARAM_LITERARY_BRAILLE_TABLE, 0);
  return 1;
}

static int
setContractionTableForLocale (void) {
  changeStringSetting(&opt_contractionTable, "");
  char *name = getContractionTableForLocale(opt_tablesDirectory);

  if (name) {
    logMessage(LOG_DEBUG, "using autoselected contraction table: %s", name);
    int ok = setContractionTable(name);
    free(name);
    if (ok) return 1;
  }

  return 0;
}

int
changeContractionTable (const char *name) {
  if (strcmp(name, optionOperand_autodetect) == 0) {
    return setContractionTableForLocale();
  }

  return setContractionTable(name);
}

static void
exitContractionTable (void *data) {
  setContractionTable(NULL);
}

static void
setTextAndContractionTables (void) {
  int usingInternalTextTable = 0;

  if (*opt_textTable) {
    if (strcmp(opt_textTable, optionOperand_autodetect) == 0) {
      setTextTableForLocale();
    } else if (!setTextTable(opt_textTable)) {
      changeStringSetting(&opt_textTable, "");
    }
  }

  if (!*opt_textTable) {
    logMessage(LOG_DEBUG, "using internal text table: %s", TEXT_TABLE);
    changeStringSetting(&opt_textTable, TEXT_TABLE);
    usingInternalTextTable = 1;
  }

  logProperty(opt_textTable, "textTable", "Text Table");
  onProgramExit("text-table", exitTextTable, NULL);

  if (*opt_contractionTable) {
    if (strcmp(opt_contractionTable, optionOperand_autodetect) == 0) {
      if (setContractionTableForLocale()) {
        if (usingInternalTextTable) {
          if (!isContractedBraille()) {
            setContractedBraille(1);
            logMessage(LOG_DEBUG, "contracted braille has been enabled");
          }
        }
      }
    } else if (!setContractionTable(opt_contractionTable)) {
      changeStringSetting(&opt_contractionTable, "");
    }
  }

  if (!*opt_contractionTable) {
    if (setContractionTable(NULL)) {
      logMessage(LOG_DEBUG, "using internal contraction table: %s", CONTRACTION_TABLE);
    }
  }

  logProperty(opt_contractionTable, "contractionTable", "Contraction Table");
  onProgramExit("contraction-table", exitContractionTable, NULL);
}

int
changeAttributesTable (const char *name) {
  if (!name) name = "";
  if (!replaceAttributesTable(opt_tablesDirectory, name)) return 0;

  if (!*name) name = ATTRIBUTES_TABLE;
  changeStringSetting(&opt_attributesTable, name);

  return 1;
}

static void
exitAttributesTable (void *data) {
  changeAttributesTable(NULL);
}

static void
setAttributesTable (void) {
  if (*opt_attributesTable) {
    if (!changeAttributesTable(opt_attributesTable)) {
      changeStringSetting(&opt_attributesTable, "");
    }
  }

  if (!*opt_attributesTable) {
    changeStringSetting(&opt_attributesTable, ATTRIBUTES_TABLE);
  }

  logProperty(opt_attributesTable, "attributesTable", "Attributes Table");
  onProgramExit("attributes-table", exitAttributesTable, NULL);
}

static KeyTableState
handleKeyboardEvent (KeyGroup group, KeyNumber number, int press) {
  int gui = !!scr.unreadable;

  if (gui && guiKeyboardTable) {
    if (keyboardTable) resetKeyTable(keyboardTable);
    return processKeyEvent(guiKeyboardTable, getCurrentCommandContext(), group, number, press);
  }

  if (keyboardTable) {
    if (guiKeyboardTable) resetKeyTable(guiKeyboardTable);

    if (!gui || opt_guiKeyboardEnabled) {
      return processKeyEvent(keyboardTable, getCurrentCommandContext(), group, number, press);
    }

    resetKeyTable(keyboardTable);
  }

  return KTS_UNBOUND;
}

static int
startKeyboardMonitor (void) {
  return !!(keyboardMonitor = newKeyboardMonitorObject(&keyboardProperties, handleKeyboardEvent));
}

static void
stopKeyboardMonitor (void) {
  if (keyboardMonitor) {
    destroyKeyboardMonitorObject(keyboardMonitor);
    keyboardMonitor = NULL;
  }
}

static int
prepareKeyboardMonitorActivity (void *data) {
  return 1;
}

static int
startKeyboardMonitorActivity (void *data) {
  return startKeyboardMonitor();
}

static void
stopKeyboardMonitorActivity (void *data) {
  stopKeyboardMonitor();
}

static const ActivityMethods keyboardMonitorActivityMethods = {
  .activityName = "keyboard-monitor",
  .retryInterval = KEYBOARD_MONITOR_START_RETRY_INTERVAL,

  .prepare = prepareKeyboardMonitorActivity,
  .start = startKeyboardMonitorActivity,
  .stop = stopKeyboardMonitorActivity
};

static ActivityObject *keyboardMonitorActivity = NULL;

static void
exitKeyboardMonitor (void *data) {
  if (keyboardMonitorActivity) {
    destroyActivity(keyboardMonitorActivity);
    keyboardMonitorActivity = NULL;
  }
}

static ActivityObject *
getKeyboardMonitorActivity (int allocate) {
  if (!keyboardMonitorActivity) {
    if (allocate) {
      if (!(keyboardMonitorActivity = newActivity(&keyboardMonitorActivityMethods, NULL))) {
        return NULL;
      }

      onProgramExit("keyboard-monitor", exitKeyboardMonitor, NULL);
    }
  }

  return keyboardMonitorActivity;
}

static void
enableKeyboardMonitor (void) {
  ActivityObject *activity = getKeyboardMonitorActivity(1);
  if (activity) startActivity(activity);
}

static void
disableKeyboardMonitor (void) {
  ActivityObject *activity = getKeyboardMonitorActivity(0);
  if (activity) stopActivity(activity);
}

static unsigned int brailleHelpPageNumber = 0;
static unsigned int keyboardHelpPageNumber = 0;

static int
enableHelpPage (unsigned int *pageNumber) {
  if (!*pageNumber) {
    if (!constructHelpScreen()) return 0;
    if (!(*pageNumber = addHelpPage())) return 0;
  }

  return setHelpPageNumber(*pageNumber);
}

static int
enableBrailleHelpPage (void) {
  return enableHelpPage(&brailleHelpPageNumber);
}

static int
enableKeyboardHelpPage (void) {
  return enableHelpPage(&keyboardHelpPageNumber);
}

static void
disableHelpPage (unsigned int pageNumber) {
  if (pageNumber) {
    if (setHelpPageNumber(pageNumber)) {
      clearHelpPage();
    }
  }
}

static void
disableBrailleHelpPage (void) {
  disableHelpPage(brailleHelpPageNumber);
}

static void
disableKeyboardHelpPage (void) {
  disableHelpPage(keyboardHelpPageNumber);
}

static int
handleWcharHelpLine (const wchar_t *line, void *data UNUSED) {
  return addHelpLine(line);
}

static int
handleUtf8HelpLine (const LineHandlerParameters *parameters) {
  const char *utf8 = parameters->line.text;
  size_t size = parameters->line.length + 1;
  wchar_t characters[size];
  wchar_t *character = characters;

  convertUtf8ToWchars(&utf8, &character, size);
  return handleWcharHelpLine(characters, parameters->data);
}

static int
loadHelpFile (const char *file) {
  int loaded = 0;
  FILE *stream;

  if ((stream = openDataFile(file, "r", 0))) {
    if (processLines(stream, handleUtf8HelpLine, NULL)) loaded = 1;

    fclose(stream);
  }

  return loaded;
}

static char *
makeBrailleKeyTablePath (void) {
  return makeInputTablePath(opt_tablesDirectory, braille->definition.code, brl.keyBindings);
}

static void
makeBrailleHelpPage (const char *keyTablePath) {
  if (enableBrailleHelpPage()) {
    if (brl.keyTable) {
      listKeyTable(brl.keyTable, NULL, handleWcharHelpLine, NULL);
    } else {
      char *keyHelpPath = replaceFileExtension(keyTablePath, KEY_HELP_EXTENSION);

      if (keyHelpPath) {
        if (loadHelpFile(keyHelpPath)) {
          logMessage(LOG_INFO, "%s: %s", gettext("Key Help"), keyHelpPath);
        } else {
          logMessage(LOG_WARNING, "%s: %s", gettext("cannot open key help"), keyHelpPath);
        }

        free(keyHelpPath);
      }
    }

    if (!getHelpLineCount()) {
      addHelpLine(WS_C("help not available"));
      message("warn", gettext("no key bindings"), 0);
    }
  }
}

static void
makeKeyboardHelpPage (void) {
  if (enableKeyboardHelpPage()) {
    listKeyTable(keyboardTable, NULL, handleWcharHelpLine, NULL);
  }
}

static void
exitKeyboardTable (void *data) {
  if (keyboardTable) {
    destroyKeyTable(keyboardTable);
    keyboardTable = NULL;
  }

  disableKeyboardHelpPage();
}

int
changeKeyboardTable (const char *name) {
  KeyTable *table = NULL;

  if (*name) {
    char *path = makeKeyboardTablePath(opt_tablesDirectory, name);

    if (path) {
      logMessage(LOG_DEBUG, "compiling keyboard table: %s", path);

      if (!(table = compileKeyTable(path, KEY_NAME_TABLES(keyboard)))) {
        logMessage(LOG_ERR, "cannot compile keyboard table: %s", path);
      }

      free(path);
    }

    if (!table) return 0;
  }

  if (keyboardTable) {
    disableKeyboardHelpPage();
    disableKeyboardMonitor();

    destroyKeyTable(keyboardTable);
    keyboardTable = NULL;
  }

  if (table) {
    setKeyTableLogLabel(table, "kbd");
    setLogKeyEventsFlag(table, &LOG_CATEGORY_FLAG(KEYBOARD_KEYS));

    keyboardTable = table;
    enableKeyboardMonitor();
    makeKeyboardHelpPage();
  }

  onSettingChange(&opt_keyboardTable, name, "keyboard table");
  return 1;
}

static void
setKeyboardTable (void) {
  parseKeyboardProperties(&keyboardProperties, opt_keyboardProperties);
  onProgramExit("keyboard-table", exitKeyboardTable, NULL);
  changeKeyboardTable(opt_keyboardTable);
  logProperty(opt_keyboardTable, "keyboardTable", "Keyboard Table");
}

static void
exitGuiKeyboardTable (void *data) {
  if (guiKeyboardTable) {
    destroyKeyTable(guiKeyboardTable);
    guiKeyboardTable = NULL;
  }
}

int
changeGuiKeyboardTable (const char *name) {
  KeyTable *table = NULL;

  if (*name) {
    char *path = makeKeyboardTablePath(opt_tablesDirectory, name);

    if (path) {
      logMessage(LOG_DEBUG, "compiling GUI keyboard table: %s", path);

      if (!(table = compileKeyTable(path, KEY_NAME_TABLES(keyboard)))) {
        logMessage(LOG_ERR, "cannot compile GUI keyboard table: %s", path);
      }

      free(path);
    }

    if (!table) return 0;
  }

  if (guiKeyboardTable) {
    destroyKeyTable(guiKeyboardTable);
    guiKeyboardTable = NULL;
  }

  if (table) {
    setKeyTableLogLabel(table, "gkb");
    setLogKeyEventsFlag(table, &LOG_CATEGORY_FLAG(KEYBOARD_KEYS));
    guiKeyboardTable = table;
  }

  onSettingChange(&opt_guiKeyboardTable, name, "GUI keyboard table");
  return 1;
}

static void
setGuiKeyboardTable (void) {
  onProgramExit("gui-keyboard-table", exitGuiKeyboardTable, NULL);
  changeGuiKeyboardTable(opt_guiKeyboardTable);
  logProperty(opt_guiKeyboardTable, "guiKeyboardTable", "GUI Keyboard Table");
}

int
haveStatusCells (void) {
  return brl.statusColumns > 0;
}

static void
brailleWindowReconfigured (unsigned int rows, unsigned int columns) {
  textStart = 0;
  textCount = columns;
  statusStart = 0;
  statusCount = 0;

  if (!(textMaximized || haveStatusCells())) {
    unsigned int separatorWidth = (prefs.statusSeparator == ssNone)? 0: 1;
    unsigned int reserved = 1 + separatorWidth;

    if (brl.textColumns > reserved) {
      unsigned int statusWidth = prefs.statusCount;
      if (!statusWidth) statusWidth = getStatusFieldsLength(prefs.statusFields);
      statusWidth += brl.textRows - 1;
      statusWidth /= brl.textRows;
      statusWidth = MIN(statusWidth, brl.textColumns-reserved);

      if (statusWidth > 0) {
        switch (prefs.statusPosition) {
          case spLeft:
            statusStart = 0;
            statusCount = statusWidth;
            textStart = statusCount + separatorWidth;
            textCount = columns - textStart;
            break;

          case spRight:
            statusCount = statusWidth;
            statusStart = columns - statusCount;
            textCount = statusStart - separatorWidth;
            textStart = 0;
            break;
        }
      }
    }
  }

  logMessage(LOG_DEBUG,
    "regions: text=%u.%u status=%u.%u",
    textStart, textCount,
    statusStart, statusCount
  );

  fullWindowShift = MAX(textCount-prefs.brailleWindowOverlap, 1);
  halfWindowShift = textCount / 2;
  verticalWindowShift = (rows > 1)? rows: 5;

  logMessage(LOG_DEBUG,
    "shifts: full=%u half=%u vertical=%u",
    fullWindowShift, halfWindowShift, verticalWindowShift
  );
}

void
reconfigureBrailleWindow (void) {
  brailleWindowReconfigured(brl.textRows, brl.textColumns);
}

static void
applyBraillePreferences (void) {
  reconfigureBrailleWindow();

  setBrailleFirmness(&brl, prefs.brailleFirmness);
  setTouchSensitivity(&brl, prefs.touchSensitivity);

  setAutorepeatProperties(&brl, prefs.autorepeatEnabled,
                          PREFS2MSECS(prefs.longPressTime),
                          PREFS2MSECS(prefs.autorepeatInterval));

  if (brl.keyTable) {
    setKeyAutoreleaseTime(brl.keyTable, prefs.autoreleaseTime);
  }
}

#ifdef ENABLE_SPEECH_SUPPORT
static void
applySpeechPreferences (void) {
  setSpeechVolume(&spk, prefs.speechVolume, 0);
  setSpeechRate(&spk, prefs.speechRate, 0);
  setSpeechPitch(&spk, prefs.speechPitch, 0);
  setSpeechPunctuation(&spk, prefs.speechPunctuation, 0);
}
#endif /* ENABLE_SPEECH_SUPPORT */

static void
applyAllPreferences (void) {
  setConsoleBellMonitoring(prefs.consoleBellAlert);
  setLedMonitoring(prefs.keyboardLedAlerts);
  tuneSetDevice(prefs.tuneDevice);
  applyBraillePreferences();

#ifdef ENABLE_SPEECH_SUPPORT
  applySpeechPreferences();
#endif /* ENABLE_SPEECH_SUPPORT */
}

void
setPreferences (const PreferenceSettings *newPreferences) {
  prefs = *newPreferences;
  applyAllPreferences();
}

static void
ensureStatusFields (void) {
  const unsigned char *fields = braille->statusFields;
  unsigned int count = brl.statusColumns * brl.statusRows;

  if (!fields && count) {
    static const unsigned char fields1[] = {
      sfWindowRow, sfEnd
    };

    static const unsigned char fields2[] = {
      sfWindowRow, sfCursorRow, sfEnd
    };

    static const unsigned char fields3[] = {
      sfWindowRow, sfCursorRow, sfCursorColumn, sfEnd
    };

    static const unsigned char fields4[] = {
      sfWindowCoordinates2, sfCursorCoordinates2, sfEnd
    };

    static const unsigned char fields5[] = {
      sfWindowCoordinates2, sfCursorCoordinates2, sfStateDots, sfEnd
    };

    static const unsigned char fields6[] = {
      sfWindowCoordinates2, sfCursorCoordinates2, sfStateDots, sfScreenNumber,
      sfEnd
    };

    static const unsigned char fields7[] = {
      sfWindowCoordinates2, sfCursorCoordinates2, sfStateDots, sfTime,
      sfEnd
    };

    static const unsigned char *const fieldsTable[] = {
      fields1, fields2, fields3, fields4, fields5, fields6, fields7
    };

    static const unsigned char fieldsCount = ARRAY_COUNT(fieldsTable);
    if (count > fieldsCount) count = fieldsCount;
    fields = fieldsTable[count - 1];
  }

  setStatusFields(fields);
}

static void
setPreferenceOverrides (void) {
  int count;
  char **settings = splitString(opt_overridePreferences, PARAMETER_SEPARATOR_CHARACTER, &count);

  if (settings) {
    char **setting = settings;
    char **end = setting + count;

    while (setting < end) {
      setPreference(*setting);
      setting += 1;
    }

    deallocateStrings(settings);
  }
}

static void
finishPreferencesLoad (void) {
  setPreferenceOverrides();
  applyAllPreferences();
}

int
loadPreferences (int reset) {
  int ok = 0;
  int found = 0;

  if (reset) {
    resetPreferences();
  } else {
    {
      char *path = makePreferencesFilePath(opt_preferencesFile);

      if (path) {
        if (testFilePath(path)) {
          found = 1;
          if (loadPreferencesFile(path)) ok = 1;
          oldPreferencesEnabled = 0;
        } else {
          logMessage(LOG_DEBUG, "preferences file not found: %s", path);
        }

        free(path);
      }
    }

    if (oldPreferencesEnabled) {
      const char *path = oldPreferencesFile;

      if (path) {
        if (testFilePath(path)) {
          found = 1;
          if (loadPreferencesFile(path)) ok = 1;
        } else {
          logMessage(LOG_DEBUG, "old preferences file not found: %s", path);
        }
      }
    }
  }

  if (!found) {
    char *path = makePath(opt_tablesDirectory, "default.prefs");

    if (path) {
      if (loadPreferencesFile(path)) ok = 1;
      free(path);
    }
  }

  finishPreferencesLoad();
  return ok;
}

int 
savePreferences (void) {
  int ok = 0;
  char *path = makePreferencesFilePath(opt_preferencesFile);

  if (path) {
    static const char suffix[] = ".new";
    char newPath[strlen(path) + strlen(suffix) + 1];
    snprintf(newPath, sizeof(newPath), "%s%s", path, suffix);

    if (savePreferencesFile(newPath)) {
      logMessage(LOG_DEBUG,
        "renaming new preferences file: %s -> %s",
        newPath, path
      );

      if (rename(newPath, path) != -1) {
        ok = 1;
        oldPreferencesEnabled = 0;
      } else {
        logSystemError("rename");
      }
    }

    free(path);
  }

  return ok;
}

#ifdef ENABLE_API
static void
exitApiServer (void *data) {
  if (api.isServerLinked()) api.unlinkServer();
  if (api.isServerRunning()) api.stopServer();

  if (apiParameters) {
    deallocateStrings(apiParameters);
    apiParameters = NULL;
  }
}
#endif /* ENABLE_API */

static void
startApiServer (void) {
#ifdef ENABLE_API
  if (!(opt_noApi || api.isServerRunning())) {
    const char *const *parameters = api.getServerParameters();

    apiParameters = getParameters(parameters,
                                  NULL,
                                  opt_apiParameters);

    if (apiParameters) {
      api.logServerIdentity(0);
      logParameters(parameters, apiParameters, "API Parameter");

      if (!opt_verify) {
        if (api.startServer(apiParameters)) {
          onProgramExit("api-server", exitApiServer, NULL);
        }
      }
    }
  }
#endif /* ENABLE_API */
}

typedef struct {
  const char *driverType;
  const char *const *requestedDrivers;
  const char *const *autodetectableDrivers;
  const char * (*getDefaultDriver) (void);
  int (*haveDriver) (const char *code);
  int (*initializeDriver) (const char *code, int verify);
} DriverActivationData;

static int
activateDriver (const DriverActivationData *data, int verify) {
  int oneDriver = data->requestedDrivers[0] && !data->requestedDrivers[1];
  int autodetect = oneDriver && (strcmp(data->requestedDrivers[0], optionOperand_autodetect) == 0);
  const char *const defaultDrivers[] = {data->getDefaultDriver(), NULL};
  const char *const *driver;

  if (!oneDriver || autodetect) verify = 0;

  if (!autodetect) {
    driver = data->requestedDrivers;
  } else if (defaultDrivers[0]) {
    driver = defaultDrivers;
  } else if (*(driver = data->autodetectableDrivers)) {
    logMessage(LOG_DEBUG, "performing %s driver autodetection", data->driverType);
  } else {
    logMessage(LOG_DEBUG, "no autodetectable %s drivers", data->driverType);
  }

  if (!*driver) {
    driver = fallbackBrailleDrivers;
    autodetect = 0;
  }

  while (*driver) {
    if (!autodetect || data->haveDriver(*driver)) {
      logMessage(LOG_DEBUG, "checking for %s driver: %s", data->driverType, *driver);
      if (data->initializeDriver(*driver, verify)) return 1;
    }

    ++driver;
  }

  logMessage(LOG_DEBUG, "%s driver not found", data->driverType);
  return 0;
}

static void
unloadDriverObject (void **object) {
#ifdef ENABLE_SHARED_OBJECTS
  if (*object) {
    unloadSharedObject(*object);
    *object = NULL;
  }
#endif /* ENABLE_SHARED_OBJECTS */
}

void
forgetDevices (void) {
  usbForgetDevices();
  bthForgetDevices();
}

static void
initializeBrailleDisplay (void) {
  constructBrailleDisplay(&brl);
  brl.bufferResized = &brailleWindowReconfigured;
}

static LockDescriptor *
getBrailleDriverLock (void) {
  static LockDescriptor *lock = NULL;
  return getLockDescriptor(&lock, "braille-driver");
}

void
lockBrailleDriver (void) {
  obtainExclusiveLock(getBrailleDriverLock());
}

void
unlockBrailleDriver (void) {
  releaseLock(getBrailleDriverLock());
}

int
isBrailleDriverConstructed (void) {
  return brailleDriverConstructed;
}

static void
setBrailleDriverConstructed (int yes) {
  lockBrailleDriver();
  brailleDriverConstructed = yes;
  unlockBrailleDriver();

  if (brailleDriverConstructed) {
    announceBrailleOnline();
  } else {
    announceBrailleOffline();
  }

  static const brlapi_param_t parameters[] = {
    BRLAPI_PARAM_DRIVER_CODE,
    BRLAPI_PARAM_DRIVER_NAME,
    BRLAPI_PARAM_DRIVER_VERSION,
    BRLAPI_PARAM_DEVICE_MODEL,
    BRLAPI_PARAM_DEVICE_CELL_SIZE,
    BRLAPI_PARAM_DISPLAY_SIZE,
    BRLAPI_PARAM_DEVICE_IDENTIFIER,
    BRLAPI_PARAM_DEVICE_SPEED,
    BRLAPI_PARAM_DEVICE_KEY_CODES,
    BRLAPI_PARAM_BOUND_COMMAND_CODES,
  };

  const brlapi_param_t *parameter = parameters;
  const brlapi_param_t *end = parameter + ARRAY_COUNT(parameters);

  while (parameter < end) {
    api.updateParameter(*parameter++, 0);
  }
}

int
constructBrailleDriver (void) {
  initializeBrailleDisplay();

  if (braille->construct(&brl, brailleDriverParameters, brailleDevice)) {
    if (ensureBrailleBuffer(&brl, LOG_INFO)) {
      if (brl.keyBindings) {
        char *keyTablePath = makeBrailleKeyTablePath();

        logMessage(LOG_INFO, "%s: %s", gettext("Key Bindings"), brl.keyBindings);

        if (keyTablePath) {
          if (brl.keyNames) {
            if ((brl.keyTable = compileKeyTable(keyTablePath, brl.keyNames))) {
              logMessage(LOG_INFO, "%s: %s", gettext("Key Table"), keyTablePath);

              setKeyTableLogLabel(brl.keyTable, "brl");
              setLogKeyEventsFlag(brl.keyTable, &LOG_CATEGORY_FLAG(BRAILLE_KEYS));
              setKeyboardEnabledFlag(brl.keyTable, &prefs.brailleKeyboardEnabled);
            } else {
              logMessage(LOG_WARNING, "%s: %s", gettext("cannot compile key table"), keyTablePath);
            }
          }

          if (haveBrailleDisplay()) makeBrailleHelpPage(keyTablePath);
          free(keyTablePath);
        }
      }

      setBrailleDriverConstructed(1);
      startBrailleInput();
      return 1;
    }

    braille->destruct(&brl);
  } else {
    logMessage(LOG_DEBUG, "%s: %s -> %s",
               "braille driver initialization failed",
               braille->definition.code, brailleDevice);
  }

  return 0;
}

void
destructBrailleDriver (void) {
  stopBrailleInput();
  drainBrailleOutput(&brl, 0);

  setBrailleDriverConstructed(0);
  braille->destruct(&brl);

  disableBrailleHelpPage();
  destructBrailleDisplay(&brl);
}

int
isBrailleOnline (void) {
  return isBrailleDriverConstructed() && !brl.isOffline;
}

static int
initializeBrailleDriver (const char *code, int verify) {
  if ((braille = loadBrailleDriver(code, &brailleObject, opt_driversDirectory))) {
    brailleDriverParameters = getParameters(braille->parameters,
                                            braille->definition.code,
                                            brailleParameters);

    if (brailleDriverParameters) {
      int constructed = verify;

      if (!constructed) {
        logMessage(LOG_DEBUG, "initializing braille driver: %s -> %s",
                   braille->definition.code, brailleDevice);

        if (constructBrailleDriver()) {
          brailleDriver = braille;
          constructed = 1;
        }
      }

      if (constructed) {
        identifyBrailleDriver(braille, 0);
        logMessage(LOG_INFO, "%s: %s", gettext("Braille Device"), brailleDevice);

        logParameters(
          braille->parameters,
          brailleDriverParameters,
          "Braille Parameter"
        );

        {
          const char *strings[] = {
            CONFIGURATION_DIRECTORY, "/",
            PACKAGE_TARNAME, "-",
            braille->definition.code, ".prefs"
          };

          oldPreferencesFile = joinStrings(strings, ARRAY_COUNT(strings));
        }

        if (oldPreferencesFile) {
          logMessage(LOG_INFO, "%s: %s", gettext("Old Preferences File"), oldPreferencesFile);

          api.linkServer();

          return 1;
        } else {
          logMallocError();
        }
      }

      deallocateStrings(brailleDriverParameters);
      brailleDriverParameters = NULL;
    }

    unloadDriverObject(&brailleObject);
  } else {
    logMessage(LOG_ERR, "%s: %s", gettext("braille driver not loadable"), code);
  }

  braille = &noBraille;
  return 0;
}

static int
activateBrailleDriver (int verify) {
  int oneDevice = brailleDevices[0] && !brailleDevices[1];
  const char *const *device = (const char *const *)brailleDevices;

  if (!oneDevice) verify = 0;

  while (*device) {
    const char *const *autodetectableDrivers = NULL;

    brailleDevice = *device;
    logMessage(LOG_DEBUG, "checking braille device: %s", brailleDevice);

    {
      const char *dev = brailleDevice;
      const GioPublicProperties *properties = gioGetPublicProperties(&dev);

      if (properties) {
        logMessage(LOG_DEBUG, "braille device type: %s", properties->type.name);

        switch (properties->type.identifier) {
          case GIO_TYPE_SERIAL: {
            autodetectableDrivers = autodetectableBrailleDrivers_serial;
            break;
          }

          case GIO_TYPE_USB: {
            autodetectableDrivers = autodetectableBrailleDrivers_USB;
            break;
          }

          case GIO_TYPE_BLUETOOTH: {
            if (!(autodetectableDrivers = bthGetDriverCodes(dev, BLUETOOTH_DEVICE_NAME_OBTAIN_TIMEOUT))) {
              autodetectableDrivers = autodetectableBrailleDrivers_Bluetooth;
            }

            break;
          }

          default:
            break;
        }
      } else {
        logMessage(LOG_DEBUG, "unrecognized braille device type");
      }
    }

    if (!autodetectableDrivers) {
      static const char *noDrivers[] = {NULL};
      autodetectableDrivers = noDrivers;
    }

    {
      const DriverActivationData data = {
        .driverType = "braille",
        .requestedDrivers = (const char *const *)brailleDrivers,
        .autodetectableDrivers = autodetectableDrivers,
        .getDefaultDriver = getDefaultBrailleDriver,
        .haveDriver = haveBrailleDriver,
        .initializeDriver = initializeBrailleDriver
      };
      if (activateDriver(&data, verify)) return 1;
    }

    device += 1;
  }

  brailleDevice = NULL;
  return 0;
}

static void
deactivateBrailleDriver (void) {
  if (brailleDriver) {
    api.unlinkServer();
    if (brailleDriverConstructed) destructBrailleDriver();
    braille = &noBraille;
    brailleDevice = NULL;
    brailleDriver = NULL;
  }

  unloadDriverObject(&brailleObject);
  stopAllBlinkDescriptors();

  if (brailleDriverParameters) {
    deallocateStrings(brailleDriverParameters);
    brailleDriverParameters = NULL;
  }

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

static int
startBrailleDriver (void) {
  forgetDevices();

  if (activateBrailleDriver(0)) {
    if (oldPreferencesEnabled) {
      loadPreferencesFile(oldPreferencesFile);
      finishPreferencesLoad();
    } else {
      applyBraillePreferences();
    }

    ensureStatusFields();
    alert(ALERT_BRAILLE_ON);

    ses->winx = 0;
    trackScreenCursor(1);

    if (clearStatusCells(&brl)) {
      if (opt_quiet) {
        scheduleUpdate("braille driver start");
        return 1;
      }

      {
        char banner[0X100];
        const char *text = opt_startMessage;

        if (*text) {
          text = gettext(text);
        } else {
          makeProgramBanner(banner, sizeof(banner), 0);
          text = banner;
        }

        if (message("start", text, MSG_SILENT)) return 1;
      }
    }

    deactivateBrailleDriver();
  }

  return 0;
}

static void
stopBrailleDriver (void) {
  deactivateBrailleDriver();
  alert(ALERT_BRAILLE_OFF);
}

static int
prepareBrailleDriverActivity (void *data) {
  initializeBrailleDisplay();
  ensureBrailleBuffer(&brl, LOG_DEBUG);
  return 1;
}

static int
startBrailleDriverActivity (void *data) {
  return startBrailleDriver();
}

static void
stopBrailleDriverActivity (void *data) {
  stopBrailleDriver();
}

static const ActivityMethods brailleDriverActivityMethods = {
  .activityName = "braille-driver",
  .retryInterval = BRAILLE_DRIVER_START_RETRY_INTERVAL,

  .prepare = prepareBrailleDriverActivity,
  .start = startBrailleDriverActivity,
  .stop = stopBrailleDriverActivity
};

static ActivityObject *brailleDriverActivity = NULL;

static void
writeBrailleMessage (const char *label, const char *text) {
  clearStatusCells(&brl);
  message(label, text, (MSG_NODELAY | MSG_SILENT | MSG_SYNC));
  brl.noDisplay = 1;
}

static void
exitBrailleDriver (void *data) {
  if (brailleDriverConstructed) {
    const char *text = opt_stopMessage;

    if (*text) {
      text = gettext(text);
    } else {
      text = gettext("BRLTTY stopped");
    }

    writeBrailleMessage("exit", text);
  }

  if (brailleDriverActivity) {
    destroyActivity(brailleDriverActivity);
    brailleDriverActivity = NULL;
  }

  forgetDevices();
}

static ActivityObject *
getBrailleDriverActivity (int allocate) {
  if (!brailleDriverActivity) {
    if (allocate) {
      if (!(brailleDriverActivity = newActivity(&brailleDriverActivityMethods, NULL))) {
        return NULL;
      }

      onProgramExit("braille-driver", exitBrailleDriver, NULL);
    }
  }

  return brailleDriverActivity;
}

static int canEnableBrailleDriver = 1;

void
enableBrailleDriver (void) {
  if (canEnableBrailleDriver) {
    ActivityObject *activity = getBrailleDriverActivity(1);
    if (activity) startActivity(activity);
  }
}

void
disableBrailleDriver (const char *reason) {
  ActivityObject *activity = getBrailleDriverActivity(0);

  if (activity) {
    if (reason) writeBrailleMessage("stop", reason);
    stopActivity(activity);
  }
}

void
setBrailleOn (void) {
  if (!canEnableBrailleDriver) {
    canEnableBrailleDriver = 1;
    enableBrailleDriver();
  }
}

void
setBrailleOff (const char *reason) {
  canEnableBrailleDriver = 0;
  disableBrailleDriver(reason);
}

void
restartBrailleDriver (void) {
  disableBrailleDriver(gettext("braille driver restarting"));
  awaitActivityStopped(brailleDriverActivity);
  brl.hasFailed = 0;

  logMessage(LOG_INFO, gettext("reinitializing braille driver"));
  enableBrailleDriver();
}

static void
exitBrailleData (void *data) {
  if (brailleDrivers) {
    deallocateStrings(brailleDrivers);
    brailleDrivers = NULL;
  }

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

  if (brailleDevices) {
    deallocateStrings(brailleDevices);
    brailleDevices = NULL;
  }
}

int
changeBrailleDriver (const char *driver) {
  return changeListSetting(&brailleDrivers, &opt_brailleDriver, driver);
}

int
changeBrailleParameters (const char *parameters) {
  if (!parameters) parameters = "";
  return changeStringSetting(&brailleParameters, parameters);
}

int
changeBrailleDevice (const char *device) {
  return changeListSetting(&brailleDevices, &opt_brailleDevice, device);
}

#ifdef ENABLE_SPEECH_SUPPORT
static AsyncHandle autospeakDelayAlarm = NULL;

static void
cancelAutospeakDelayAlarm (void) {
  if (autospeakDelayAlarm) {
    asyncCancelRequest(autospeakDelayAlarm);
    autospeakDelayAlarm = NULL;
  }
}

static void
endAutospeakDelay (SpeechSynthesizer *spk) {
  cancelAutospeakDelayAlarm();

  if (!spk->canAutospeak) {
    spk->canAutospeak = 1;
    scheduleUpdate("banner spoken");
  }
}

ASYNC_ALARM_CALLBACK(handleAutospeakDelayAlarm) {
  asyncDiscardHandle(autospeakDelayAlarm);
  autospeakDelayAlarm = NULL;

  endAutospeakDelay(&spk);
}

static void
beginAutospeakDelay (int duration) {
  if (asyncNewRelativeAlarm(&autospeakDelayAlarm, duration,
                            handleAutospeakDelayAlarm, NULL)) {
    spk.canAutospeak = 0;
  }
}

static void
setSpeechFinished (SpeechSynthesizer *spk) {
  spk->track.isActive = 0;
  spk->track.speechLocation = SPK_LOC_NONE;

  endAutospeakDelay(spk);
}

static void
setSpeechLocation (SpeechSynthesizer *spk, int location) {
  if (spk->track.isActive) {
    if (scr.number == spk->track.screenNumber) {
      if (location != spk->track.speechLocation) {
        spk->track.speechLocation = location;
        if (ses->trackScreenCursor) trackSpeech();
      }

      return;
    }

    setSpeechFinished(spk);
  }
}

static void
initializeSpeechSynthesizer (void) {
  constructSpeechSynthesizer(&spk);
  spk.setFinished = setSpeechFinished;
  spk.setLocation = setSpeechLocation;
}

int
constructSpeechDriver (void) {
  initializeSpeechSynthesizer();

  if (startSpeechDriverThread(&spk, speechDriverParameters)) {
    return 1;
  } else {
    logMessage(LOG_DEBUG, "speech driver initialization failed: %s",
               speech->definition.code);
  }

  return 0;
}

void
destructSpeechDriver (void) {
  stopSpeechDriverThread(&spk);
  destructSpeechSynthesizer(&spk);
}

static int
initializeSpeechDriver (const char *code, int verify) {
  if ((speech = loadSpeechDriver(code, &speechObject, opt_driversDirectory))) {
    speechDriverParameters = getParameters(speech->parameters,
                                           speech->definition.code,
                                           speechParameters);

    if (speechDriverParameters) {
      int constructed = verify;

      if (!constructed) {
        logMessage(LOG_DEBUG, "initializing speech driver: %s",
                   speech->definition.code);

        if (constructSpeechDriver()) {
          constructed = 1;
          speechDriver = speech;
        }
      }

      if (constructed) {
        identifySpeechDriver(speech, 0);

        logParameters(
          speech->parameters,
          speechDriverParameters,
          "Speech Parameter"
        );

        return 1;
      }

      deallocateStrings(speechDriverParameters);
      speechDriverParameters = NULL;
    }

    unloadDriverObject(&speechObject);
  } else {
    logMessage(LOG_ERR, "%s: %s", gettext("speech driver not loadable"), code);
  }

  speech = &noSpeech;
  return 0;
}

static int
activateSpeechDriver (int verify) {
  static const char *const autodetectableDrivers[] = {
    NULL
  };

  const DriverActivationData data = {
    .driverType = "speech",
    .requestedDrivers = (const char *const *)speechDrivers,
    .autodetectableDrivers = autodetectableDrivers,
    .getDefaultDriver = getDefaultSpeechDriver,
    .haveDriver = haveSpeechDriver,
    .initializeDriver = initializeSpeechDriver
  };

  return activateDriver(&data, verify);
}

static void
deactivateSpeechDriver (void) {
  if (speechDriver) {
    destructSpeechDriver();

    speech = &noSpeech;
    speechDriver = NULL;
  }

  unloadDriverObject(&speechObject);

  if (speechDriverParameters) {
    deallocateStrings(speechDriverParameters);
    speechDriverParameters = NULL;
  }
}

static int
startSpeechDriver (void) {
  if (!activateSpeechDriver(0)) return 0;
  applySpeechPreferences();

  if (!opt_quiet && spk.sayBanner) {
    char banner[0X100];
    const char *text = opt_startMessage;

    if (*text) {
      text = gettext(text);
    } else {
      makeProgramBanner(banner, sizeof(banner), 0);
      text = banner;
    }

    sayString(&spk, text, SAY_OPT_MUTE_FIRST);
    beginAutospeakDelay(SPEECH_DRIVER_START_AUTOSPEAK_DELAY);
  } else if (isAutospeakActive()) {
    autospeak(AUTOSPEAK_FORCE);
  }

  return 1;
}

static void
stopSpeechDriver (void) {
  cancelAutospeakDelayAlarm();

  muteSpeech(&spk, "driver stop");
  deactivateSpeechDriver();
}

static int
prepareSpeechDriverActivity (void *data) {
  initializeSpeechSynthesizer();
  return 1;
}

static int
startSpeechDriverActivity (void *data) {
  return startSpeechDriver();
}

static void
stopSpeechDriverActivity (void *data) {
  stopSpeechDriver();
}

static const ActivityMethods speechDriverActivityMethods = {
  .activityName = "speech-driver",
  .retryInterval = SPEECH_DRIVER_START_RETRY_INTERVAL,

  .prepare = prepareSpeechDriverActivity,
  .start = startSpeechDriverActivity,
  .stop = stopSpeechDriverActivity
};

static ActivityObject *speechDriverActivity = NULL;

static void
exitSpeechDriver (void *data) {
  if (speechDriverActivity) {
    destroyActivity(speechDriverActivity);
    speechDriverActivity = NULL;
  }
}

static ActivityObject *
getSpeechDriverActivity (int allocate) {
  if (!speechDriverActivity) {
    if (allocate) {
      if (!(speechDriverActivity = newActivity(&speechDriverActivityMethods, NULL))) {
        return NULL;
      }

      onProgramExit("speech-driver", exitSpeechDriver, NULL);
    }
  }

  return speechDriverActivity;
}

void
enableSpeechDriver (int sayBanner) {
  ActivityObject *activity = getSpeechDriverActivity(1);

  spk.sayBanner = sayBanner;
  if (activity) startActivity(activity);
}

void
disableSpeechDriver (const char *reason) {
  ActivityObject *activity = getSpeechDriverActivity(0);

  if (activity) {
    if (reason) {
      sayString(&spk, reason, SAY_OPT_MUTE_FIRST);
      drainSpeech(&spk);
    }

    stopActivity(activity);
  }
}

void
restartSpeechDriver (void) {
  disableSpeechDriver(gettext("speech driver restarting"));
  awaitActivityStopped(speechDriverActivity);

  logMessage(LOG_INFO, gettext("reinitializing speech driver"));
  enableSpeechDriver(1);
}

static void
exitSpeechData (void *data) {
  if (speechDrivers) {
    deallocateStrings(speechDrivers);
    speechDrivers = NULL;
  }

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

static void
exitSpeechInput (void *data) {
  if (speechInputObject) {
    destroySpeechInputObject(speechInputObject);
    speechInputObject = NULL;
  }
}

int
changeSpeechDriver (const char *driver) {
  return changeListSetting(&speechDrivers, &opt_speechDriver, driver);
}

int
changeSpeechParameters (const char *parameters) {
  if (!parameters) parameters = "";
  return changeStringSetting(&speechParameters, parameters);
}
#endif /* ENABLE_SPEECH_SUPPORT */

static int
initializeScreenDriver (const char *code, int verify) {
  if ((screen = loadScreenDriver(code, &screenObject, opt_driversDirectory))) {
    screenDriverParameters = getParameters(
      getScreenParameters(screen),
      screen->definition.code,
      screenParameters
    );

    if (screenDriverParameters) {
      int constructed = verify;

      if (!constructed) {
        logMessage(LOG_DEBUG,
          "initializing screen driver: %s",
          screen->definition.code
        );

        if (constructScreenDriver(screenDriverParameters)) {
          constructed = 1;
          screenDriver = screen;
        }
      }

      if (constructed) {
        identifyScreenDriver(screen, 0);

        logParameters(
          getScreenParameters(screen),
          screenDriverParameters,
          "Screen Parameter"
        );

        return 1;
      }

      deallocateStrings(screenDriverParameters);
      screenDriverParameters = NULL;
    }

    unloadDriverObject(&screenObject);
  } else {
    logMessage(LOG_ERR, "%s: %s", gettext("screen driver not loadable"), code);
  }

  setNoScreen();
  return 0;
}

static int
activateScreenDriver (int verify) {
  static const char *const autodetectableDrivers[] = {
    NULL
  };

  const DriverActivationData data = {
    .driverType = "screen",
    .requestedDrivers = (const char *const *)screenDrivers,
    .autodetectableDrivers = autodetectableDrivers,
    .getDefaultDriver = getDefaultScreenDriver,
    .haveDriver = haveScreenDriver,
    .initializeDriver = initializeScreenDriver
  };

  return activateDriver(&data, verify);
}

static void
deactivateScreenDriver (void) {
  if (screenDriver) {
    destructScreenDriver();

    setNoScreen();
    screenDriver = NULL;
  }

  unloadDriverObject(&screenObject);

  if (screenDriverParameters) {
    deallocateStrings(screenDriverParameters);
    screenDriverParameters = NULL;
  }
}

static int
startScreenDriver (void) {
  if (!activateScreenDriver(0)) return 0;
  if (isMainScreen()) scheduleUpdate("main screen started");
  return 1;
}

static void
stopScreenDriver (void) {
  deactivateScreenDriver();
}

static int
prepareScreenDriverActivity (void *data) {
  return 1;
}

static int
startScreenDriverActivity (void *data) {
  return startScreenDriver();
}

static void
stopScreenDriverActivity (void *data) {
  stopScreenDriver();
}

static const ActivityMethods screenDriverActivityMethods = {
  .activityName = "screen-driver",
  .retryInterval = SCREEN_DRIVER_START_RETRY_INTERVAL,

  .prepare = prepareScreenDriverActivity,
  .start = startScreenDriverActivity,
  .stop = stopScreenDriverActivity
};

static ActivityObject *screenDriverActivity = NULL;

static void
exitScreenDriver (void *data) {
  if (screenDriverActivity) {
    destroyActivity(screenDriverActivity);
    screenDriverActivity = NULL;
  }
}

static ActivityObject *
getScreenDriverActivity (int allocate) {
  if (!screenDriverActivity) {
    if (allocate) {
      if (!(screenDriverActivity = newActivity(&screenDriverActivityMethods, NULL))) {
        return NULL;
      }

      onProgramExit("screen-driver", exitScreenDriver, NULL);
    }
  }

  return screenDriverActivity;
}

void
enableScreenDriver (void) {
  ActivityObject *activity = getScreenDriverActivity(1);

  setNoScreenDriverReason(NULL);
  if (activity) startActivity(activity);
}

void
disableScreenDriver (const char *reason) {
  ActivityObject *activity = getScreenDriverActivity(0);

  setNoScreenDriverReason(reason);
  if (activity) stopActivity(activity);
}

void
restartScreenDriver (void) {
  disableScreenDriver(gettext("screen driver restarting"));
  awaitActivityStopped(screenDriverActivity);

  logMessage(LOG_INFO, gettext("reinitializing screen driver"));
  enableScreenDriver();
}

static void
exitScreenData (void *data) {
  endSpecialScreens();

  if (screenDrivers) {
    deallocateStrings(screenDrivers);
    screenDrivers = NULL;
  }

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

int
changeScreenDriver (const char *driver) {
  return changeListSetting(&screenDrivers, &opt_screenDriver, driver);
}

int
changeScreenParameters (const char *parameters) {
  if (!parameters) parameters = "";
  return changeStringSetting(&screenParameters, parameters);
}

int
changeMessageLocale (const char *locale) {
  int changed = !!setlocale(LC_ALL, locale);

  if (changed) {
    api.updateParameter(BRLAPI_PARAM_MESSAGE_LOCALE, 0);
  } else {
    logMessage(LOG_WARNING, "message locale change failed: %s", locale);
  }

  return changed;
}

static void
exitPidFile (void *data) {
#if defined(GRUB_RUNTIME)

#else /* remove pid file */
  unlink(opt_pidFile);
#endif /* remove pid file */
}

static int
makePidFile (ProcessIdentifier pid) {
  return createPidFile(opt_pidFile, pid);
}

static int tryPidFile (void);

ASYNC_ALARM_CALLBACK(retryPidFile) {
  tryPidFile();
}

static int
tryPidFile (void) {
  if (makePidFile(0)) {
    onProgramExit("pid-file", exitPidFile, NULL);
  } else if (errno == EEXIST) {
    return 0;
  } else {
    asyncNewRelativeAlarm(NULL, PID_FILE_CREATE_RETRY_INTERVAL, retryPidFile, NULL);
  }

  return 1;
}

#if defined(__MINGW32__)
static void
background (void) {
  char *variableName;

  {
    const char *strings[] = {programName, "_DAEMON"};
    variableName = joinStrings(strings, ARRAY_COUNT(strings));
  }

  {
    int i;
    for (i=0; variableName[i]; i+=1) {
      char c = variableName[i];

      if (c == '_') continue;
      if (isdigit((unsigned char)c) && (i > 0)) continue;

      if (isalpha((unsigned char)c)) {
        if (islower((unsigned char)c)) variableName[i] = toupper((unsigned char)c);
        continue;
      }

      variableName[i] = '_';
    }
  }

  if (!getenv(variableName)) {
    LPTSTR commandLine = GetCommandLine();
    STARTUPINFO startupInfo;
    PROCESS_INFORMATION processInfo;
    
    memset(&startupInfo, 0, sizeof(startupInfo));
    startupInfo.cb = sizeof(startupInfo);

    if (!SetEnvironmentVariable(variableName, "BACKGROUND")) {
      logWindowsSystemError("SetEnvironmentVariable");
      exit(PROG_EXIT_FATAL);
    }

    if (!CreateProcess(NULL, commandLine, NULL, NULL, TRUE,
                       CREATE_NEW_PROCESS_GROUP | CREATE_SUSPENDED,
                       NULL, NULL, &startupInfo, &processInfo)) {
      logWindowsSystemError("CreateProcess");
      exit(PROG_EXIT_FATAL);
    }

    {
      int created = makePidFile(processInfo.dwProcessId);
      int resumed = ResumeThread(processInfo.hThread) != -1;

      if (!created) {
        if (errno == EEXIST) {
          ExitProcess(PROG_EXIT_FATAL);
        }
      }

      if (!resumed) {
        logWindowsSystemError("ResumeThread");
        ExitProcess(PROG_EXIT_FATAL);
      }
    }

    ExitProcess(PROG_EXIT_SUCCESS);
  }

  free(variableName);
}

#elif defined(__MSDOS__)
static void
background (void) {
  msdosBackground();
}

#elif defined(GRUB_RUNTIME)
static void
background (void) {
}

#else /* Unix */
static void
background (void) {
  int fds[2];

  if (pipe(fds) == -1) {
    logSystemError("pipe");
    exit(PROG_EXIT_FATAL);
  }

  fflush(stdout);
  fflush(stderr);

  {
    pid_t child = fork();

    if (child == -1) {
      logSystemError("fork");
      exit(PROG_EXIT_FATAL);
    }

    if (child) {
      ProgramExitStatus exitStatus = PROG_EXIT_SUCCESS;

      if (close(fds[0]) == -1) logSystemError("close");

      if (!makePidFile(child)) {
        if (errno == EEXIST) {
          exitStatus = PROG_EXIT_SEMANTIC;
        }
      }

      if (close(fds[1]) == -1) logSystemError("close");
      _exit(exitStatus);
    }
  }

  if (close(fds[1]) == -1) logSystemError("close");

  {
    unsigned char buffer[1];

    if (read(fds[0], buffer, sizeof(buffer)) == -1) logSystemError("read");
    if (close(fds[0]) == -1) logSystemError("close");
  }

  if (setsid() == -1) {                        
    logSystemError("setsid");
    exit(PROG_EXIT_FATAL);
  }
}
#endif /* background() */

static int
validateInterval (int *value, const char *string) {
  if (!*string) return 1;

  {
    static const int minimum = 1;
    int ok = validateInteger(value, string, &minimum, NULL);
    if (ok) *value *= 10;
    return ok;
  }
}

ProgramExitStatus
brlttyStart (void) {
  if (opt_cancelExecution) {
    ProgramExitStatus exitStatus;

    if (!*opt_pidFile) {
      exitStatus = PROG_EXIT_SEMANTIC;
      logMessage(LOG_ERR, "%s", gettext("pid file not specified"));
    } else if (cancelProgram(opt_pidFile)) {
      exitStatus = PROG_EXIT_FORCE;
    } else {
      exitStatus = PROG_EXIT_FATAL;
    }

    return exitStatus;
  }

  {
    int stop = 0;

    if (opt_removeService) {
      removeService(SERVICE_NAME);
      stop = 1;
    }

    if (opt_installService) {
      installService(SERVICE_NAME, SERVICE_DESCRIPTION, opt_configurationFile);
      stop = 1;
    }

    if (stop) return PROG_EXIT_FORCE;
  }

  if (!validateInterval(&messageHoldTimeout, opt_messageTime)) {
    logMessage(LOG_ERR, "%s: %s", gettext("invalid message hold timeout"), opt_messageTime);
  }

  if (opt_version) {
    logMessage(LOG_INFO, "Copyright %s", PACKAGE_COPYRIGHT);
    identifyScreenDrivers(1);

#ifdef ENABLE_API
    api.logServerIdentity(1);
#endif /* ENABLE_API */

    identifyBrailleDrivers(1);

#ifdef ENABLE_SPEECH_SUPPORT
    identifySpeechDrivers(1);
#endif /* ENABLE_SPEECH_SUPPORT */

    return PROG_EXIT_FORCE;
  }

  if (opt_verify) opt_noDaemon = 1;
  if (!opt_noDaemon
#ifdef __MINGW32__
      && !isWindowsService
#endif
     ) {
    background();
  }

  if (*opt_pidFile) {
    if (!tryPidFile()) {
      return PROG_EXIT_SEMANTIC;
    }
  }

  if (!opt_noDaemon) {
    stderrLogLevel = 0;

    detachStandardInput();
    detachStandardOutput();
    if (!opt_logToStandardError) detachStandardError();

#ifdef __MINGW32__
    {
      HANDLE h = CreateFile("NUL", GENERIC_READ|GENERIC_WRITE,
                            FILE_SHARE_READ|FILE_SHARE_WRITE,
                            NULL, OPEN_EXISTING, 0, NULL);

      if (!h) {
        logWindowsSystemError("CreateFile[NUL]");
      } else {
        SetStdHandle(STD_INPUT_HANDLE, h);
        SetStdHandle(STD_OUTPUT_HANDLE, h);

        if (!opt_logToStandardError) {
          SetStdHandle(STD_ERROR_HANDLE, h);
        }
      }
    }
#endif /* __MINGW32__ */
  }

  /*
   * From this point, all IO functions as printf, puts, perror, etc. can't be
   * used anymore since we are a daemon.  The logMessage() facility should 
   * be used instead.
   */

  changeScreenDriver(opt_screenDriver);
  changeScreenParameters(opt_screenParameters);
  beginSpecialScreens();
  onProgramExit("screen-data", exitScreenData, NULL);

  suppressTuneDeviceOpenErrors();

  {
    char *directory;

    if ((directory = getWorkingDirectory())) {
      logProperty(directory, "workingDirectory", "Working Directory");
      free(directory);
    } else {
      logMessage(LOG_WARNING, "%s: %s", gettext("cannot determine working directory"), strerror(errno));
    }
  }

  logProperty(opt_configurationFile, "configurationFile", "Configuration File");
  logProperty(opt_tablesDirectory, "tablesDirectory", "Tables Directory");
  logProperty(opt_driversDirectory, "driversDirectory", "Drivers Directory");
  logProperty(opt_helpersDirectory, "helpersDirectory", "Helpers Directory");
  logProperty(opt_writableDirectory, "writableDirectory", "Writable Directory");
  logProperty(opt_updatableDirectory, "updatableDirectory", "Updatable Directory");
  logProperty(opt_preferencesFile, "preferencesFile", "Preferences File");

  resetPreferences();
  loadPreferences(0);

  if (opt_promptPatterns && *opt_promptPatterns) {
    int count;
    char **patterns = splitString(opt_promptPatterns, PARAMETER_SEPARATOR_CHARACTER, &count);

    if (patterns) {
      for (int index=0; index<count; index+=1) {
        if (!addPromptPattern(patterns[index])) break;
      }

      deallocateStrings(patterns);
    }
  }

  setTextAndContractionTables();
  setAttributesTable();
  setKeyboardTable();
  setGuiKeyboardTable();

  /* initialize screen driver */
  if (opt_verify) {
    if (activateScreenDriver(1)) deactivateScreenDriver();
  } else {
    setNoScreen();
    enableScreenDriver();
  }
  
  /* The device(s) the braille display might be connected to. */
  if (!*opt_brailleDevice) {
    logMessage(LOG_ERR, gettext("braille device not specified"));
    return PROG_EXIT_SYNTAX;
  }

  constructBrailleDisplay(&brl);
  changeBrailleDriver(opt_brailleDriver);
  changeBrailleParameters(opt_brailleParameters);
  changeBrailleDevice(opt_brailleDevice);
  brailleDriverConstructed = 0;
  onProgramExit("braille-data", exitBrailleData, NULL);

  if (opt_verify) {
    if (activateBrailleDriver(1)) deactivateBrailleDriver();
  } else {
    enableBrailleDriver();
  }

#ifdef ENABLE_SPEECH_SUPPORT
  constructSpeechSynthesizer(&spk);
  changeSpeechDriver(opt_speechDriver);
  changeSpeechParameters(opt_speechParameters);
  onProgramExit("speech-data", exitSpeechData, NULL);

  if (opt_verify) {
    if (activateSpeechDriver(1)) deactivateSpeechDriver();
  } else {
    enableSpeechDriver(1);
  }

  /* Create the file system object for speech input. */
  logProperty(opt_speechInput, "speechInput", "Speech Input");
  if (!opt_verify) {
    if (*opt_speechInput) {
      speechInputObject = newSpeechInputObject(opt_speechInput);
      onProgramExit("speech-input", exitSpeechInput, NULL);
    }
  }
#endif /* ENABLE_SPEECH_SUPPORT */

  startApiServer();

  if (!opt_verify) notifyServiceReady();

  return opt_verify? PROG_EXIT_FORCE: PROG_EXIT_SUCCESS;
}

static char *configuredLocale = "";

static int
changeLocale (const char *locale) {
  if (changeMessageLocale(locale)) return 1;
  setlocale(LC_ALL, configuredLocale);
  return 0;
}

static const ProfileProperty languageProfileProperties[] = {
  { .name = WS_C("locale"),
    .defaultValue = &configuredLocale,
    .change = changeLocale
  },

#ifdef ENABLE_SPEECH_SUPPORT
  { .name = WS_C("speech-driver"),
    .defaultValue = &opt_speechDriver,
    .change = changeSpeechDriver
  },

  { .name = WS_C("speech-parameters"),
    .defaultValue = &opt_speechParameters,
    .change = changeSpeechParameters
  },
#endif /* ENABLE_SPEECH_SUPPORT */

  { .name = WS_C("text-table"),
    .defaultValue = &opt_textTable,
    .change = changeTextTable
  },

  { .name = WS_C("contraction-table"),
    .defaultValue = &opt_contractionTable,
    .change = changeContractionTable
  },
};

static int
beginLanguageProfile (void) {
#ifdef ENABLE_SPEECH_SUPPORT
  disableSpeechDriver(NULL);
  awaitActivityStopped(speechDriverActivity);
#endif /* ENABLE_SPEECH_SUPPORT */

  return 1;
}

static int
endLanguageProfile (void) {
#ifdef ENABLE_SPEECH_SUPPORT
  enableSpeechDriver(0);
#endif /* ENABLE_SPEECH_SUPPORT */

  if (brl.keyTable) {
    char *path = makeBrailleKeyTablePath();

    if (path) {
      disableBrailleHelpPage();
      makeBrailleHelpPage(path);
      free(path);
    }
  }

  if (keyboardTable) {
    disableKeyboardHelpPage();
    makeKeyboardHelpPage();
  }

  return 1;
}

const ProfileDescriptor languageProfile = {
  .category = strtext("Language"),
  .extension = LANGUAGE_PROFILE_EXTENSION,

  .begin = beginLanguageProfile,
  .end = endLanguageProfile,

  .properties = {
    .array = languageProfileProperties,
    .count = ARRAY_COUNT(languageProfileProperties)
  }
};
