/*
 * 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-2024 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 "log.h"
#include "alert.h"
#include "parameters.h"
#include "program.h"
#include "cmd_queue.h"
#include "cmd_navigation.h"
#include "cmd_utils.h"
#include "brl_cmds.h"
#include "parse.h"
#include "rgx.h"
#include "prefs.h"
#include "routing.h"
#include "scr.h"
#include "core.h"

static int
getWindowLength (void) {
  if (isContracting()) return getContractedLength(textCount);
  return textCount;
}

typedef int (*CanMoveWindow) (void);

static int
canMoveUp (void) {
  return ses->winy > 0;
}

static int
canMoveDown (void) {
  return (ses->winy + brl.textRows) < scr.rows;
}

static int
toDifferentLine (
  IsSameCharacter isSameCharacter,
  CanMoveWindow canMoveWindow,
  int amount, int from, int width
) {
  if (canMoveWindow()) {
    ScreenCharacter characters1[width];
    unsigned int skipped = 0;

    if ((isSameCharacter == isSameText) && ses->displayMode) isSameCharacter = isSameAttributes;
    readScreen(from, ses->winy, width, 1, characters1);

    do {
      ScreenCharacter characters2[width];
      readScreen(from, ses->winy+=amount, width, 1, characters2);

      if (!isSameRow(characters1, characters2, width, isSameCharacter) ||
          (showScreenCursor() && (scr.posy == ses->winy) &&
           (scr.posx >= from) && (scr.posx < (from + width)))) {
        return 1;
      }

      /* lines are identical */
      alertLineSkipped(&skipped);
    } while (canMoveWindow());
  }

  alert(ALERT_BOUNCE);
  return 0;
}

static int
upDifferentLine (IsSameCharacter isSameCharacter) {
  return toDifferentLine(isSameCharacter, canMoveUp, -1, 0, scr.cols);
}

static int
downDifferentLine (IsSameCharacter isSameCharacter) {
  return toDifferentLine(isSameCharacter, canMoveDown, 1, 0, scr.cols);
}

static int
upDifferentCharacter (IsSameCharacter isSameCharacter, int column) {
  return toDifferentLine(isSameCharacter, canMoveUp, -1, column, 1);
}

static int
downDifferentCharacter (IsSameCharacter isSameCharacter, int column) {
  return toDifferentLine(isSameCharacter, canMoveDown, 1, column, 1);
}

static void
upOneLine (void) {
  if (canMoveUp()) {
    ses->winy--;
  } else {
    alert(ALERT_BOUNCE);
  }
}

static void
downOneLine (void) {
  if (canMoveDown()) {
    ses->winy++;
  } else {
    alert(ALERT_BOUNCE);
  }
}

static void
upLine (IsSameCharacter isSameCharacter) {
  if (prefs.skipIdenticalLines) {
    upDifferentLine(isSameCharacter);
  } else {
    upOneLine();
  }
}

static void
downLine (IsSameCharacter isSameCharacter) {
  if (prefs.skipIdenticalLines) {
    downDifferentLine(isSameCharacter);
  } else {
    downOneLine();
  }
}

typedef int (*RowTester) (int column, int row, void *data);

static void
findRow (int column, int increment, RowTester test, void *data) {
  int row = ses->winy;

  while (1) {
    row += increment;
    if (row < 0) break;
    if ((row + brl.textRows) > scr.rows) break;

    if (test(column, row, data)) {
      ses->winy = row;
      return;
    }
  }

  alert(ALERT_BOUNCE);
}

static int
testIndent (int column, int row, void *data UNUSED) {
  int count = column+1;
  ScreenCharacter characters[count];
  readScreenRow(row, count, characters);

  while (column >= 0) {
    wchar_t text = characters[column].text;
    if (text != WC_C(' ')) return 1;
    column -= 1;
  }

  return 0;
}

static RGX_Object *promptPatterns = NULL;

static void
exitPromptPatterns (void *data) {
  if (promptPatterns) {
    rgxDestroyObject(promptPatterns);
    promptPatterns = NULL;
  }
}

int
addPromptPattern (const char *string) {
  if (!promptPatterns) {
    if (!(promptPatterns = rgxNewObject(NULL))) return 0;
    onProgramExit("prompt-patterns", exitPromptPatterns, NULL);
    rgxCompileOption(promptPatterns, RGX_OPTION_SET, RGX_COMPILE_ANCHOR_START);
  }

  RGX_Matcher *matcher = rgxAddPatternUTF8(
    promptPatterns, string, NULL, NULL
  );

  if (!matcher) return 0;
  return 1;
}

static int
testPromptOriginal (int column, int row, void *data) {
  if (!column) return 0;

  int length = column + 1;
  ScreenCharacter characters[length];
  readScreenRow(row, length, characters);

  const ScreenCharacter *prompt = data;
  return isSameRow(characters, prompt, length, isSameText);
}

static int
testPromptPatterns (int column, int row, void *data) {
  int length = scr.cols;
  wchar_t text[length];

  {
    ScreenCharacter characters[length];
    readScreenRow(row, length, characters);

    const ScreenCharacter *from = characters;
    const ScreenCharacter *end = from + length;

    wchar_t *to = text;
    while (from < end) *to++ = from++->text;
  }

  return !!rgxMatchTextCharacters(promptPatterns, text, length, NULL, NULL);
}

static void
toPreviousNonblankWindow (void) {
  int oldX = ses->winx;
  int oldY = ses->winy;
  int tuneLimit = 3;
  ScreenCharacter characters[scr.cols];

  while (1) {
    int charCount;
    int charIndex;

    if (!shiftBrailleWindowLeft(fullWindowShift)) {
      if (ses->winy == 0) {
        ses->winx = oldX;
        ses->winy = oldY;

        alert(ALERT_BOUNCE);
        break;
      }

      if (tuneLimit-- > 0) alert(ALERT_WRAP_UP);
      upLine(isSameText);
      placeBrailleWindowRight();
    }

    charCount = getWindowLength();
    charCount = MIN(charCount, scr.cols-ses->winx);
    readScreen(ses->winx, ses->winy, charCount, 1, characters);

    for (charIndex=charCount-1; charIndex>=0; charIndex-=1) {
      wchar_t text = characters[charIndex].text;

      if (text != WC_C(' ')) break;
    }

    if (showScreenCursor() &&
        (scr.posy == ses->winy) &&
        (scr.posx >= 0) &&
        (scr.posx < (ses->winx + charCount))) {
      charIndex = MAX(charIndex, scr.posx-ses->winx);
    }

    if (charIndex >= 0) break;
  }
}

static void
toNextNonblankWindow (void) {
  int oldX = ses->winx;
  int oldY = ses->winy;
  int tuneLimit = 3;
  ScreenCharacter characters[scr.cols];

  while (1) {
    int charCount;
    int charIndex;

    if (!shiftBrailleWindowRight(fullWindowShift)) {
      if (ses->winy >= (scr.rows - brl.textRows)) {
        ses->winx = oldX;
        ses->winy = oldY;

        alert(ALERT_BOUNCE);
        break;
      }

      if (tuneLimit-- > 0) alert(ALERT_WRAP_DOWN);
      downLine(isSameText);
      ses->winx = 0;
    }

    charCount = getWindowLength();
    charCount = MIN(charCount, scr.cols-ses->winx);
    readScreen(ses->winx, ses->winy, charCount, 1, characters);

    for (charIndex=0; charIndex<charCount; charIndex+=1) {
      wchar_t text = characters[charIndex].text;

      if (text != WC_C(' ')) break;
    }

    if (showScreenCursor() &&
        (scr.posy == ses->winy) &&
        (scr.posx < scr.cols) &&
        (scr.posx >= ses->winx)) {
      charIndex = MIN(charIndex, scr.posx-ses->winx);
    }

    if (charIndex < charCount) break;
  }
}

static int
handleNavigationCommands (int command, void *data) {
  int oldwiny = ses->winy;

  switch (command & BRL_MSK_CMD) {
    case BRL_CMD_TOP_LEFT:
      ses->winx = 0;
      /* fall through */
    case BRL_CMD_TOP:
      ses->winy = 0;
      break;

    case BRL_CMD_BOT_LEFT:
      ses->winx = 0;
      /* fall through */
    case BRL_CMD_BOT:
      ses->winy = MAX(scr.rows, brl.textRows) - brl.textRows;
      break;

    case BRL_CMD_WINUP:
      if (canMoveUp()) {
        ses->winy -= MIN(verticalWindowShift, ses->winy);
      } else {
        alert(ALERT_BOUNCE);
      }
      break;
    case BRL_CMD_WINDN:
      if (canMoveDown()) {
        ses->winy += MIN(verticalWindowShift, (scr.rows - brl.textRows - ses->winy));
      } else {
        alert(ALERT_BOUNCE);
      }
      break;

    case BRL_CMD_LNUP:
      upOneLine();
      break;
    case BRL_CMD_LNDN:
      downOneLine();
      break;

    case BRL_CMD_PRDIFLN:
      upDifferentLine(isSameText);
      break;
    case BRL_CMD_NXDIFLN:
      downDifferentLine(isSameText);
      break;

    case BRL_CMD_ATTRUP:
      upDifferentLine(isSameAttributes);
      break;
    case BRL_CMD_ATTRDN:
      downDifferentLine(isSameAttributes);
      break;

    case BRL_CMD_PRPGRPH: {
      typedef enum {
        STARTING,
        START_LINE_NOT_BLANK,
        FINDING_LAST_LINE,
        FINDING_FIRST_LINE
      } State;

      State state = STARTING;
      ScreenCharacter characters[scr.cols];
      int line = ses->winy;

      while (1) {
        int isBlankLine;

        readScreenRow(line, scr.cols, characters);
        isBlankLine = isAllSpaceCharacters(characters, scr.cols);

        switch (state) {
          case STARTING:
            state = isBlankLine? FINDING_LAST_LINE: START_LINE_NOT_BLANK;
            break;

          case START_LINE_NOT_BLANK:
            state = isBlankLine? FINDING_LAST_LINE: FINDING_FIRST_LINE;
            break;

          case FINDING_LAST_LINE:
            if (!isBlankLine) state = FINDING_FIRST_LINE;
            break;

          case FINDING_FIRST_LINE:
            if (!isBlankLine) break;
            line += 1;
            goto foundFirstLine;
        }

        if (!line) break;
        line -= 1;
      }

      if (state == FINDING_FIRST_LINE) {
      foundFirstLine:
        ses->winy = line;
        ses->winx = 0;
      } else {
        alert(ALERT_BOUNCE);
      }

      break;
    }

    case BRL_CMD_NXPGRPH: {
      int width = scr.cols;
      ScreenCharacter characters[width];

      int found = 0;
      int findBlankLine = 1;
      int line = ses->winy;

      while (line < scr.rows) {
        readScreenRow(line, width, characters);

        if (isAllSpaceCharacters(characters, width) == findBlankLine) {
          if (!findBlankLine) {
            ses->winy = line;
            ses->winx = 0;
            found = 1;
            break;
          }

          findBlankLine = 0;
        }

        line += 1;
      }

      if (!found) alert(ALERT_BOUNCE);
      break;
    }

    {
      int increment;

    case BRL_CMD_PRPROMPT:
      increment = -1;
      goto findPrompt;

    case BRL_CMD_NXPROMPT:
      increment = 1;
      goto findPrompt;

    findPrompt:
      {
        size_t length = scr.cols;
        ScreenCharacter characters[length];
        readScreenRow(ses->winy, length, characters);

        if (promptPatterns) {
          findRow(length, increment, testPromptPatterns, characters);
        } else {
          int column = 0;

          while (column < length) {
            if (characters[column].text == WC_C(' ')) break;
            column += 1;
          }

          if (column < length) {
            findRow(column, increment, testPromptOriginal, characters);
          } else {
            alert(ALERT_BOUNCE);
          }
        }
      }

      break;
    }

    case BRL_CMD_LNBEG:
      if (ses->winx) {
        ses->winx = 0;
      } else {
        alert(ALERT_BOUNCE);
      }
      break;

    case BRL_CMD_LNEND: {
      int end = MAX(scr.cols, textCount) - textCount;

      if (ses->winx < end) {
        ses->winx = end;
      } else {
        alert(ALERT_BOUNCE);
      }
      break;
    }

    case BRL_CMD_CHRLT:
      if (!moveBrailleWindowLeft(1)) alert(ALERT_BOUNCE);
      break;
    case BRL_CMD_CHRRT:
      if (!moveBrailleWindowRight(1)) alert(ALERT_BOUNCE);
      break;

    case BRL_CMD_HWINLT:
      if (!shiftBrailleWindowLeft(halfWindowShift)) alert(ALERT_BOUNCE);
      break;

    case BRL_CMD_HWINRT:
      if (!shiftBrailleWindowRight(halfWindowShift)) alert(ALERT_BOUNCE);
      break;

    case BRL_CMD_PRNBWIN:
      toPreviousNonblankWindow();
      break;

    case BRL_CMD_NXNBWIN:
      toNextNonblankWindow();
      break;

    case BRL_CMD_FWINLTSKIP:
      if (prefs.skipBlankBrailleWindowsMode == sbwAll) {
        toPreviousNonblankWindow();
        break;
      }

    {
      int skipBlankBrailleWindows;

      skipBlankBrailleWindows = 1;
      goto moveLeft;

    case BRL_CMD_FWINLT:
      skipBlankBrailleWindows = 0;
      goto moveLeft;

    moveLeft:
      {
        int oldX = ses->winx;

        if (shiftBrailleWindowLeft(fullWindowShift)) {
          if (skipBlankBrailleWindows) {
            if (prefs.skipBlankBrailleWindowsMode == sbwEndOfLine) goto skipEndOfLine;
            int charCount = MIN(scr.cols, ses->winx+textCount);

            if (!showScreenCursor() ||
                (scr.posy != ses->winy) ||
                (scr.posx < 0) ||
                (scr.posx >= charCount)) {
              int charIndex;
              ScreenCharacter characters[charCount];

              readScreenRow(ses->winy, charCount, characters);

              for (charIndex=0; charIndex<charCount; charIndex+=1) {
                wchar_t text = characters[charIndex].text;

                if (text != WC_C(' ')) break;
              }

              if (charIndex == charCount) goto wrapUp;
            }
          }

          break;
        }

      wrapUp:
        if (ses->winy == 0) {
          ses->winx = oldX;
          alert(ALERT_BOUNCE);
          break;
        }

        alert(ALERT_WRAP_UP);
        upLine(isSameText);
        placeBrailleWindowRight();

      skipEndOfLine:
        if (skipBlankBrailleWindows && (prefs.skipBlankBrailleWindowsMode == sbwEndOfLine)) {
          ScreenCharacter characters[scr.cols];
          readScreenRow(ses->winy, scr.cols, characters);
          int last = scr.cols;

          while (last > 0) {
            if (!isWordBreak(characters, --last)) break;
          }

          if (ses->winx > last) placeRightEdge(last);
        }

        if (prefs.wordWrap) setWordWrapStart(ses->winx);
      }

      break;
    }

    case BRL_CMD_FWINRTSKIP:
      if (prefs.skipBlankBrailleWindowsMode == sbwAll) {
        toNextNonblankWindow();
        break;
      }

    {
      int skipBlankBrailleWindows;

      skipBlankBrailleWindows = 1;
      goto moveRight;

    case BRL_CMD_FWINRT:
      skipBlankBrailleWindows = 0;
      goto moveRight;

    moveRight:
      {
        int oldX = ses->winx;

        if (shiftBrailleWindowRight(fullWindowShift)) {
          if (skipBlankBrailleWindows) {
            if (!showScreenCursor() ||
                (scr.posy != ses->winy) ||
                (scr.posx < ses->winx)) {
              int charCount = scr.cols - ses->winx;
              int charIndex;
              ScreenCharacter characters[charCount];

              readScreen(ses->winx, ses->winy, charCount, 1, characters);

              for (charIndex=0; charIndex<charCount; charIndex+=1) {
                wchar_t text = characters[charIndex].text;

                if (text != WC_C(' ')) break;
              }

              if (charIndex == charCount) goto wrapDown;
            }
          }

          break;
        }

      wrapDown:
        if (ses->winy >= (scr.rows - brl.textRows)) {
          ses->winx = oldX;

          alert(ALERT_BOUNCE);
          break;
        }

        alert(ALERT_WRAP_DOWN);
        downLine(isSameText);
        ses->winx = 0;
      }

      break;
    }

    case BRL_CMD_RETURN:
      if ((ses->winx != ses->motx) || (ses->winy != ses->moty)) goto doBack;
      /* fall through */
    case BRL_CMD_HOME:
      if (!trackScreenCursor(1)) alert(ALERT_COMMAND_REJECTED);
      break;

    doBack:
    case BRL_CMD_BACK:
      ses->winx = ses->motx;
      ses->winy = ses->moty;
      break;

    case BRL_CMD_CSRJMP_VERT: {
      if (!startScreenCursorRouting(-1, ses->winy)) {
        alert(ALERT_COMMAND_REJECTED);
      }

      break;
    }

    default: {
      int blk = command & BRL_MSK_BLK;
      int arg = command & BRL_MSK_ARG;
      int flags = command & BRL_MSK_FLG;

      switch (blk) {
        case BRL_CMD_BLK(ROUTE): {
          int column, row;

          if (getCharacterCoordinates(arg, &row, &column, NULL, 1)) {
            if (startScreenCursorRouting(column, row)) {
              break;
            }
          }

          alert(ALERT_COMMAND_REJECTED);
          break;
        }

        case BRL_CMD_BLK(ROUTE_LINE): {
          if (!startScreenCursorRouting(-1, arg)) {
            alert(ALERT_COMMAND_REJECTED);
          }

          break;
        }

        case BRL_CMD_BLK(SETLEFT): {
          int column, row;

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

        case BRL_CMD_BLK(GOTOLINE):
          if (flags & BRL_FLG_MOTION_SCALED) {
            arg = rescaleInteger(arg, BRL_MSK_ARG, scr.rows-1);
          }
          if (arg < scr.rows) {
            slideBrailleWindowVertically(arg);
            oldwiny = -1;
          } else {
            alert(ALERT_COMMAND_REJECTED);
          }
          break;

        case BRL_CMD_BLK(SETMARK): {
          ScreenLocation *mark = &ses->marks[arg];
          mark->column = ses->winx;
          mark->row = ses->winy;
          alert(ALERT_MARK_SET);
          break;
        }
        case BRL_CMD_BLK(GOTOMARK): {
          ScreenLocation *mark = &ses->marks[arg];
          ses->winx = mark->column;
          ses->winy = mark->row;
          break;
        }

        {
          int column, row;
          int increment;

        case BRL_CMD_BLK(PRINDENT):
          increment = -1;
          goto findIndent;

        case BRL_CMD_BLK(NXINDENT):
          increment = 1;

        findIndent:
          if (getCharacterCoordinates(arg, &row, &column, NULL, 0)) {
            ses->winy = row;
            findRow(column, increment, testIndent, NULL);
          } else {
            alert(ALERT_COMMAND_REJECTED);
          }
          break;
        }

        case BRL_CMD_BLK(PRDIFCHAR): {
          int column, row;

          if (getCharacterCoordinates(arg, &row, &column, NULL, 0)) {
            ses->winy = row;
            upDifferentCharacter(isSameText, column);
          } else {
            alert(ALERT_COMMAND_REJECTED);
          }
          break;
        }

        case BRL_CMD_BLK(NXDIFCHAR): {
          int column, row;

          if (getCharacterCoordinates(arg, &row, &column, NULL, 0)) {
            ses->winy = row;
            downDifferentCharacter(isSameText, column);
          } else {
            alert(ALERT_COMMAND_REJECTED);
          }
          break;
        }

        default:
          return 0;
      }

      break;
    }
  }

  if (ses->winy != oldwiny) {
    if (command & BRL_FLG_MOTION_TOLEFT) {
      ses->winx = 0;
    }
  }

  cancelDelayedCursorTrackingAlarm();
  return 1;
}

int
addNavigationCommands (void) {
  return pushCommandHandler("navigation", KTB_CTX_DEFAULT,
                            handleNavigationCommands, NULL, NULL);
}
