/*
 * 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 "embed.h"
#include "strfmt.h"
#include "cmd_queue.h"
#include "cmd_speech.h"
#include "cmd_utils.h"
#include "brl_cmds.h"
#include "prefs.h"
#include "alert.h"
#include "spk.h"
#include "scr.h"
#include "update.h"
#include "unicode.h"
#include "ascii.h"
#include "core.h"

#ifdef ENABLE_SPEECH_SUPPORT
static void
sayScreenRegion (int left, int top, int width, int height, int track, SayMode mode) {
  if (mode == sayImmediate) muteSpeech(&spk, __func__);

  size_t count = width * height;
  ScreenCharacter characters[count];
  readScreen(left, top, width, height, characters);

  {
    ScreenCharacter *character = characters;
    ScreenCharacter *end = character + count;
    character += width - 1;

    while (character < end) {
      if (iswspace(character->text)) character->text = WC_C('\n');
      character += width;
    }
  }

  spk.track.isActive = track;
  spk.track.screenNumber = scr.number;
  spk.track.firstLine = top;
  spk.track.speechLocation = SPK_LOC_NONE;

  sayScreenCharacters(characters, count, 0);
}

static void
sayScreenLines (int line, int count, int track, SayMode mode) {
  sayScreenRegion(0, line, scr.cols, count, track, mode);
}

static void
speakDone (const ScreenCharacter *line, int column, int count, int spell) {
  ScreenCharacter internalBuffer[count];

  if (line) {
    line = &line[column];
  } else {
    readScreen(column, ses->spky, count, 1, internalBuffer);
    line = internalBuffer;
  }

  speakCharacters(line, count, spell, 1);
  placeBrailleWindowHorizontally(ses->spkx);
  slideBrailleWindowVertically(ses->spky);
  suppressAutospeak();
}

static void
speakCurrentCharacter (void) {
  speakDone(NULL, ses->spkx, 1, 0);
}

static void
speakCurrentLine (int spell) {
  speakDone(NULL, 0, scr.cols, spell);
}

static const char *const phoneticWords[] = {
  [' '] = "space",

  ['a'] = "alpha",
  ['b'] = "bravo",
  ['c'] = "charlie",
  ['d'] = "delta",
  ['e'] = "echo",
  ['f'] = "foxtrot",
  ['g'] = "golf",
  ['h'] = "hotel",
  ['i'] = "india",
  ['j'] = "juliet",
  ['k'] = "kilo",
  ['l'] = "lima",
  ['m'] = "mike",
  ['n'] = "november",
  ['o'] = "oscar",
  ['p'] = "papa",
  ['q'] = "quebec",
  ['r'] = "romeo",
  ['s'] = "sierra",
  ['t'] = "tango",
  ['u'] = "uniform",
  ['v'] = "victor",
  ['w'] = "whiskey",
  ['x'] = "x-ray",
  ['y'] = "yankee",
  ['z'] = "zulu",

  ['0'] = "zero",
  ['1'] = "one",
  ['2'] = "two",
  ['3'] = "three",
  ['4'] = "four",
  ['5'] = "five",
  ['6'] = "six",
  ['7'] = "seven",
  ['8'] = "eight",
  ['9'] = "nine",

  ['+'] = "plus",
  ['='] = "equals",
  ['<'] = "less than",
  ['>'] = "greater than",

  ['('] = "left parenthesis",
  [')'] = "right parenthesis",
  ['['] = "left bracket",
  [']'] = "right bracket",
  ['{'] = "left brace",
  ['}'] = "right brace",

  ['"'] = "quote",
  ['\''] = "apostrophe",
  [','] = "comma",
  [';'] = "semicolon",
  [':'] = "colon",
  ['.'] = "period",
  ['!'] = "exclamation",
  ['?'] = "question",

  ['`'] = "grave",
  ['~'] = "tilde",
  ['@'] = "at",
  ['#'] = "number",
  ['$'] = "dollar",
  ['%'] = "percent",
  ['^'] = "circumflex",
  ['&'] = "ampersand",
  ['*'] = "asterisk",
  ['-'] = "dash",
  ['_'] = "underscore",

  ['/'] = "slash",
  ['\\'] = "backslash",
  ['|'] = "vertical bar",

  [ASCII_NUL] = "null",
  [ASCII_SOH] = "start of header",
  [ASCII_STX] = "start of text",
  [ASCII_ETX] = "end of text",
  [ASCII_EOT] = "end of transmission",
  [ASCII_ENQ] = "enquiry",
  [ASCII_ACK] = "acknowledgement",
  [ASCII_BEL] = "bell",
  [ASCII_BS] = "backspace",
  [ASCII_HT] = "horizontal tab",
  [ASCII_LF] = "line feed",
  [ASCII_VT] = "vertical tab",
  [ASCII_FF] = "form feed",
  [ASCII_CR] = "carriage return",
  [ASCII_SO] = "shift out",
  [ASCII_SI] = "shift in",
  [ASCII_DLE] = "data link escape",
  [ASCII_DC1] = "device control one",
  [ASCII_DC2] = "device control two",
  [ASCII_DC3] = "device control three",
  [ASCII_DC4] = "device control four",
  [ASCII_NAK] = "negative acknowledgement",
  [ASCII_SYN] = "synchronous idle",
  [ASCII_ETB] = "end of transmission block",
  [ASCII_CAN] = "cancel",
  [ASCII_EM] = "end of medium",
  [ASCII_SUB] = "substitute",
  [ASCII_ESC] = "escape",
  [ASCII_FS] = "file separator",
  [ASCII_GS] = "group separator",
  [ASCII_RS] = "record separator",
  [ASCII_US] = "unit separator",
  [ASCII_DEL] = "delete",
};

static const char *
getPhoneticWord (wchar_t character) {
  if (character >= ARRAY_COUNT(phoneticWords)) return NULL;
  return phoneticWords[character];
}

static void
sayPhoneticPhrase (int column, int row) {
  ScreenCharacter character;
  readScreenCharacter(column, row, &character);
  wchar_t text = character.text;

  char description[0X50];
  STR_BEGIN(description, sizeof(description));

  wchar_t characters[0X10];
  size_t characterCount = decomposeCharacter(text, characters, ARRAY_COUNT(characters));

  if (!characterCount) {
    characters[0] = text;
    characterCount = 1;
  }

  for (unsigned int characterIndex=0; characterIndex<characterCount; characterIndex+=1) {
    if (characterIndex > 0) {
      STR_PRINTF("%s ", ((characterIndex == 1)? " with": ","));
    }

    text = characters[characterIndex];
    const char *word = getPhoneticWord(text);
    char nameBuffer[0X40];

    if (!word) {
      if (iswupper(text)) {
        wchar_t lowercase = towlower(text);
        word = getPhoneticWord(lowercase);

        if (word) {
          STR_PRINTF("cap ");
          text = lowercase;
        }
      }
    }

    if (!word) {
      if (getCharacterName(text, nameBuffer, sizeof(nameBuffer))) {
        word = nameBuffer;

        {
          char *byte = nameBuffer;

          while (*byte) {
            *byte = tolower((unsigned char)*byte);
            byte += 1;
          }
        }

        {
          const char *space = strchr(word, ' ');

          if (space) {
            size_t length = space - word + 1;
            if (memcmp(word,  "combining ", length) == 0) word += length;
          }
        }
      }
    }

    if (word) {
      if (STR_LENGTH > 0) STR_PRINTF(" ");
      STR_PRINTF("%s", word);
    }
  }

  STR_END;
  sayString(&spk, description, SAY_OPT_MUTE_FIRST);
}

typedef enum {
  SCT_SPACE,
  SCT_WORD,
  SCT_SYMBOL,
} ScreenCharacterType;

static ScreenCharacterType
getScreenCharacterType (const ScreenCharacter *character, int partialWord) {
  if (iswspace(character->text)) return SCT_SPACE;
  if (!partialWord) return SCT_WORD;
  if (character->text == WC_C('_')) return SCT_WORD;
  if (iswalnum(character->text)) return SCT_WORD;
  return SCT_SYMBOL;
}

static int
handleSpeechCommands (int command, void *data) {
  switch (command & BRL_MSK_CMD) {
    case BRL_CMD_RESTARTSPEECH:
      restartSpeechDriver();
      break;

    case BRL_CMD_SPK_STOP:
      disableSpeechDriver(gettext("speech driver stopped"));
      break;

    case BRL_CMD_SPK_START:
      enableSpeechDriver(1);
      break;

    case BRL_CMD_SPKHOME: {
      if (scr.number == spk.track.screenNumber) {
        if (startScreenCursorRouting(ses->spkx, ses->spky)) {
          break;
        }
      }

      alert(ALERT_COMMAND_REJECTED);
      break;
    }

    case BRL_CMD_MUTE:
      muteSpeech(&spk, "command");
      break;

    case BRL_CMD_SAY_LINE:
      sayScreenLines(ses->winy, 1, 0, prefs.sayLineMode);
      break;

    case BRL_CMD_SAY_ALL:
      sayScreenLines(0, scr.rows, 1, sayImmediate);
      break;

    case BRL_CMD_SAY_ABOVE:
      sayScreenLines(0, ses->winy+1, 1, sayImmediate);
      break;

    case BRL_CMD_SAY_BELOW:
      sayScreenLines(ses->winy, scr.rows-ses->winy, 1, sayImmediate);
      break;

    case BRL_CMD_SAY_SOFTER:
      if (!canSetSpeechVolume(&spk)) {
        alert(ALERT_COMMAND_REJECTED);
      } else if (prefs.speechVolume > 0) {
        setSpeechVolume(&spk, --prefs.speechVolume, 1);
      } else {
        alert(ALERT_NO_CHANGE);
      }
      break;

    case BRL_CMD_SAY_LOUDER:
      if (!canSetSpeechVolume(&spk)) {
        alert(ALERT_COMMAND_REJECTED);
      } else if (prefs.speechVolume < SPK_VOLUME_MAXIMUM) {
        setSpeechVolume(&spk, ++prefs.speechVolume, 1);
      } else {
        alert(ALERT_NO_CHANGE);
      }
      break;

    case BRL_CMD_SAY_SLOWER:
      if (!canSetSpeechRate(&spk)) {
        alert(ALERT_COMMAND_REJECTED);
      } else if (prefs.speechRate > 0) {
        setSpeechRate(&spk, --prefs.speechRate, 1);
      } else {
        alert(ALERT_NO_CHANGE);
      }
      break;

    case BRL_CMD_SAY_FASTER:
      if (!canSetSpeechRate(&spk)) {
        alert(ALERT_COMMAND_REJECTED);
      } else if (prefs.speechRate < SPK_RATE_MAXIMUM) {
        setSpeechRate(&spk, ++prefs.speechRate, 1);
      } else {
        alert(ALERT_NO_CHANGE);
      }
      break;

    case BRL_CMD_SAY_LOWER:
      if (!canSetSpeechPitch(&spk)) {
        alert(ALERT_COMMAND_REJECTED);
      } else if (prefs.speechPitch > 0) {
        setSpeechPitch(&spk, --prefs.speechPitch, 1);
      } else {
        alert(ALERT_NO_CHANGE);
      }
      break;

    case BRL_CMD_SAY_HIGHER:
      if (!canSetSpeechPitch(&spk)) {
        alert(ALERT_COMMAND_REJECTED);
      } else if (prefs.speechPitch < SPK_PITCH_MAXIMUM) {
        setSpeechPitch(&spk, ++prefs.speechPitch, 1);
      } else {
        alert(ALERT_NO_CHANGE);
      }
      break;

    case BRL_CMD_SPK_PUNCT_LEVEL:
      if (canSetSpeechPunctuation(&spk)) {
        unsigned char newLevel = prefs.speechPunctuation + 1;
        if (newLevel > SPK_PUNCTUATION_ALL) newLevel = 0;

        if (setSpeechPunctuation(&spk, newLevel, 1)) {
          prefs.speechPunctuation = newLevel;
          break;
        }
      }

      alert(ALERT_COMMAND_REJECTED);
      break;

    case BRL_CMD_SPEAK_CURR_CHAR:
      speakCurrentCharacter();
      break;

    case BRL_CMD_SPEAK_PREV_CHAR:
      if (ses->spkx > 0) {
        ses->spkx -= 1;
        speakCurrentCharacter();
      } else if (ses->spky > 0) {
        ses->spky -= 1;
        ses->spkx = scr.cols - 1;
        alert(ALERT_WRAP_UP);
        speakCurrentCharacter();
      } else {
        alert(ALERT_BOUNCE);
      }
      break;

    case BRL_CMD_SPEAK_NEXT_CHAR:
      if (ses->spkx < (scr.cols - 1)) {
        ses->spkx += 1;
        speakCurrentCharacter();
      } else if (ses->spky < (scr.rows - 1)) {
        ses->spky += 1;
        ses->spkx = 0;
        alert(ALERT_WRAP_DOWN);
        speakCurrentCharacter();
      } else {
        alert(ALERT_BOUNCE);
      }
      break;

    case BRL_CMD_SPEAK_FRST_CHAR: {
      ScreenCharacter characters[scr.cols];
      int column;

      readScreenRow(ses->spky, scr.cols, characters);
      if ((column = findFirstNonSpaceCharacter(characters, scr.cols)) >= 0) {
        ses->spkx = column;
        speakDone(characters, column, 1, 0);
      } else {
        alert(ALERT_COMMAND_REJECTED);
      }

      break;
    }

    case BRL_CMD_SPEAK_LAST_CHAR: {
      ScreenCharacter characters[scr.cols];
      int column;

      readScreenRow(ses->spky, scr.cols, characters);
      if ((column = findLastNonSpaceCharacter(characters, scr.cols)) >= 0) {
        ses->spkx = column;
        speakDone(characters, column, 1, 0);
      } else {
        alert(ALERT_COMMAND_REJECTED);
      }

      break;
    }

    {
      int direction;
      int partial;
      int spell;

    case BRL_CMD_SPEAK_PREV_WORD:
      direction = -1;
      partial = 0;
      spell = 0;
      goto speakWord;

    case BRL_CMD_SPEAK_NEXT_WORD:
      direction = 1;
      partial = 0;
      spell = 0;
      goto speakWord;

    case BRL_CMD_SPEAK_CURR_WORD:
      direction = 0;
      partial = 0;
      spell = 0;
      goto speakWord;

    case BRL_CMD_SPELL_CURR_WORD:
      direction = 0;
      partial = 0;
      spell = 1;
      goto speakWord;

    case BRL_CMD_SPEAK_PREV_PWRD:
      direction = -1;
      partial = 1;
      spell = 0;
      goto speakWord;

    case BRL_CMD_SPEAK_NEXT_PWRD:
      direction = 1;
      partial = 1;
      spell = 0;
      goto speakWord;

    case BRL_CMD_SPEAK_CURR_PWRD:
      direction = 0;
      partial = 1;
      spell = 0;
      goto speakWord;

    case BRL_CMD_SPELL_CURR_PWRD:
      direction = 0;
      partial = 1;
      spell = 1;
      goto speakWord;

    speakWord:
      {
        int row = ses->spky;
        int column = ses->spkx;

        ScreenCharacter characters[scr.cols];
        ScreenCharacterType type;
        int onCurrentWord;

        int from = column;
        int to = from + 1;

      findWord:
        readScreenRow(row, scr.cols, characters);
        type = (row == ses->spky)? getScreenCharacterType(&characters[column], partial): SCT_SPACE;
        onCurrentWord = type != SCT_SPACE;

        if (direction < 0) {
          while (1) {
            if (column == 0) {
              if ((type != SCT_SPACE) && !onCurrentWord) {
                ses->spkx = from = column;
                ses->spky = row;
                break;
              }

              if (row == 0) goto noWord;
              if (row-- == ses->spky) alert(ALERT_WRAP_UP);
              column = scr.cols;
              goto findWord;
            }

            {
              ScreenCharacterType newType = getScreenCharacterType(&characters[--column], partial);

              if (newType != type) {
                if (onCurrentWord) {
                  onCurrentWord = 0;
                } else if (type != SCT_SPACE) {
                  ses->spkx = from = column + 1;
                  ses->spky = row;
                  break;
                }

                if (newType != SCT_SPACE) to = column + 1;
                type = newType;
              }
            }
          }
        } else if (direction > 0) {
          while (1) {
            if (++column == scr.cols) {
              if ((type != SCT_SPACE) && !onCurrentWord) {
                to = column;
                ses->spkx = from;
                ses->spky = row;
                break;
              }

              if (row == (scr.rows - 1)) goto noWord;
              if (row++ == ses->spky) alert(ALERT_WRAP_DOWN);
              column = -1;
              goto findWord;
            }

            {
              ScreenCharacterType newType = getScreenCharacterType(&characters[column], partial);

              if (newType != type) {
                if (onCurrentWord) {
                  onCurrentWord = 0;
                } else if (type != SCT_SPACE) {
                  to = column;
                  ses->spkx = from;
                  ses->spky = row;
                  break;
                }

                if (newType != SCT_SPACE) from = column;
                type = newType;
              }
            }
          }
        } else if (type != SCT_SPACE) {
          while (from > 0) {
            if (getScreenCharacterType(&characters[--from], partial) != type) {
              from += 1;
              break;
            }
          }

          while (to < scr.cols) {
            if (getScreenCharacterType(&characters[to], partial) != type) break;
            to += 1;
          }
        }

        speakDone(characters, from, to-from, spell);
        break;
      }

    noWord:
      alert(ALERT_BOUNCE);
      break;
    }

    case BRL_CMD_SPEAK_CURR_LINE:
      speakCurrentLine(0);
      break;

    case BRL_CMD_SPELL_CURR_LINE:
      speakCurrentLine(1);
      break;

    {
      int increment;
      int limit;

    case BRL_CMD_SPEAK_PREV_LINE:
      increment = -1;
      limit = 0;
      goto speakLine;

    case BRL_CMD_SPEAK_NEXT_LINE:
      increment = 1;
      limit = scr.rows - 1;
      goto speakLine;

    speakLine:
      if (ses->spky == limit) {
        alert(ALERT_BOUNCE);
      } else {
        if (prefs.skipIdenticalLines) {
          ScreenCharacter original[scr.cols];
          ScreenCharacter current[scr.cols];
          unsigned int count = 0;

          readScreenRow(ses->spky, scr.cols, original);

          do {
            readScreenRow(ses->spky+=increment, scr.cols, current);
            if (!isSameRow(original, current, scr.cols, isSameText)) break;
            alertLineSkipped(&count);
          } while (ses->spky != limit);
        } else {
          ses->spky += increment;
        }

        speakCurrentLine(0);
      }

      break;
    }

    case BRL_CMD_SPEAK_FRST_LINE: {
      ScreenCharacter characters[scr.cols];
      int row = 0;

      while (row < scr.rows) {
        readScreenRow(row, scr.cols, characters);
        if (!isAllSpaceCharacters(characters, scr.cols)) break;
        row += 1;
      }

      if (row < scr.rows) {
        ses->spky = row;
        ses->spkx = 0;
        speakCurrentLine(0);
      } else {
        alert(ALERT_COMMAND_REJECTED);
      }

      break;
    }

    case BRL_CMD_SPEAK_LAST_LINE: {
      ScreenCharacter characters[scr.cols];
      int row = scr.rows - 1;

      while (row >= 0) {
        readScreenRow(row, scr.cols, characters);
        if (!isAllSpaceCharacters(characters, scr.cols)) break;
        row -= 1;
      }

      if (row >= 0) {
        ses->spky = row;
        ses->spkx = 0;
        speakCurrentLine(0);
      } else {
        alert(ALERT_COMMAND_REJECTED);
      }

      break;
    }

    case BRL_CMD_DESC_CURR_CHAR: {
      sayPhoneticPhrase(ses->spkx, ses->spky);
      break;
    }

    case BRL_CMD_ROUTE_CURR_LOCN: {
      if (!startScreenCursorRouting(ses->spkx, ses->spky)) {
        alert(ALERT_COMMAND_REJECTED);
      }

      break;
    }

    case BRL_CMD_SPEAK_CURR_LOCN: {
      char buffer[0X50];
      snprintf(buffer, sizeof(buffer), "%s %d, %s %d",
               gettext("line"), ses->spky+1,
               gettext("column"), ses->spkx+1);
      sayString(&spk, buffer, SAY_OPT_MUTE_FIRST);
      break;
    }

    case BRL_CMD_SPEAK_INDENT:
      speakIndent(NULL, 0, 1);
      break;

    default: {
      int arg = command & BRL_MSK_ARG;

      switch (command & BRL_MSK_BLK) {
        case BRL_CMD_BLK(ROUTE_SPEECH): {
          int column, row;

          if (getCharacterCoordinates(arg, &row, &column, NULL, 0)) {
            ses->spkx = column;
            ses->spky = row;
          } else {
            alert(ALERT_COMMAND_REJECTED);
          }

          break;
        }

        default:
          return 0;
      }
    }
  }

  return 1;
}
#endif /* ENABLE_SPEECH_SUPPORT */

int
addSpeechCommands (void) {
#ifdef ENABLE_SPEECH_SUPPORT
  return pushCommandHandler("speech", KTB_CTX_DEFAULT,
                            handleSpeechCommands, NULL, NULL);
#else /* ENABLE_SPEECH_SUPPORT */
  return 0;
#endif /* ENABLE_SPEECH_SUPPORT */
}
