/*
 * 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>.
 */

/* LogText/braille.c - Braille display library
 * For Tactilog's LogText
 * Author: Dave Mielke <dave@mielke.cc>
 */

#include "prologue.h"

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>

#include "log.h"
#include "file.h"
#include "device.h"
#include "async_wait.h"
#include "ascii.h"

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

static SerialDevice *serialDevice = NULL;

#define screenHeight 25
#define screenWidth 80
typedef unsigned char ScreenImage[screenHeight][screenWidth];
static ScreenImage sourceImage;
static ScreenImage targetImage;

static const char *downloadPath = "logtext-download";

typedef enum {
   DEV_OFFLINE,
   DEV_ONLINE,
   DEV_READY
} DeviceStatus;
static DeviceStatus deviceStatus;

static KeyTableCommandContext currentContext;
static unsigned char currentLine;
static unsigned char cursorRow;
static unsigned char cursorColumn;

#ifndef __MINGW32__
static int
makeFifo (const char *path, mode_t mode) {
   struct stat status;
   if (lstat(path, &status) != -1) {
      if (S_ISFIFO(status.st_mode)) return 1;
      logMessage(LOG_ERR, "Download object not a FIFO: %s", path);
   } else if (errno == ENOENT) {
      lockUmask();
      mode_t mask = umask(0);
      int result = mkfifo(path, mode);
      int error = errno;
      umask(mask);
      unlockUmask();

      if (result != -1) return 1;
      errno = error;
      logSystemError("Download FIFO creation");
   }
   return 0;
}
#endif /* __MINGW32__ */

static int
makeDownloadFifo (void) {
#ifdef __MINGW32__
   return 0;
#else /* __MINGW32__ */
   return makeFifo(downloadPath, S_IRUSR|S_IWUSR|S_IWGRP|S_IWOTH);
#endif /* __MINGW32__ */
}

static int
brl_construct (BrailleDisplay *brl, char **parameters, const char *device) {
   {
      static TranslationTable outputTable = {
#include "brl-out.h"
      };

      setOutputTable(outputTable);
      makeInputTable();

      {
         const unsigned char byte = 0XFF;

         if (memchr(outputTable, byte, sizeof(outputTable))) {
            outputTable[translateInputCell(byte)] = ASCII_SUB;
         }
      }
   }

   if (!isSerialDeviceIdentifier(&device)) {
      unsupportedDeviceIdentifier(device);
      return 0;
   }

   makeDownloadFifo();
   if ((serialDevice = serialOpenDevice(device))) {
      if (serialRestartDevice(serialDevice, 9600)) {
         brl->textRows = screenHeight;
         brl->textColumns = screenWidth;
         brl->buffer = &sourceImage[0][0];
         memset(sourceImage, 0, sizeof(sourceImage));
         deviceStatus = DEV_ONLINE;
         return 1;
      }
      serialCloseDevice(serialDevice);
      serialDevice = NULL;
   }
   return 0;
}

static void
brl_destruct (BrailleDisplay *brl) {
   serialCloseDevice(serialDevice);
   serialDevice = NULL;
}

static int
checkData (const unsigned char *data, unsigned int length) {
   if ((length < 5) || (length != (data[4] + 5))) {
      logMessage(LOG_ERR, "Bad length: %d", length);
   } else if (data[0] != 255) {
      logMessage(LOG_ERR, "Bad header: %d", data[0]);
   } else if ((data[1] < 1) || (data[1] > screenHeight)) {
      logMessage(LOG_ERR, "Bad line: %d", data[1]);
   } else if (data[2] > screenWidth) {
      logMessage(LOG_ERR, "Bad cursor: %d", data[2]);
   } else if ((data[3] < 1) || (data[3] > screenWidth)) {
      logMessage(LOG_ERR, "Bad column: %d", data[3]);
   } else if (data[4] > (screenWidth - (data[3] - 1))) {
      logMessage(LOG_ERR, "Bad count: %d", data[4]);
   } else {
      return 1;
   }
   return 0;
}

static int
sendBytes (const unsigned char *bytes, size_t count) {
   if (serialWriteData(serialDevice, bytes, count) == -1) {
      logSystemError("LogText write");
      return 0;
   }
   return 1;
}

static int
sendData (unsigned char line, unsigned char column, unsigned char count) {
   unsigned char data[5 + count];
   unsigned char *target = data;
   unsigned char *source = &targetImage[line][column];
   *target++ = 0XFF;
   *target++ = line + 1;
   *target++ = (line == cursorRow)? cursorColumn+1: 0;
   *target++ = column + 1;
   *target++ = count;
   logBytes(LOG_DEBUG, "Output dots", source, count);
   target = translateOutputCells(target, source, count);
   count = target - data;
   logBytes(LOG_DEBUG, "LogText write", data, count);
   if (checkData(data, count)) {
      if (sendBytes(data, count)) {
         return 1;
      }
   }
   return 0;
}

static int
sendLine (unsigned char line, int force) {
   unsigned char *source = &sourceImage[line][0];
   unsigned char *target = &targetImage[line][0];
   unsigned char start = 0;
   unsigned char count = screenWidth;
   while (count > 0) {
      if (source[count-1] != target[count-1]) break;
      --count;
   }
   while (start < count) {
      if (source[start] != target[start]) break;
      ++start;
   }
   if ((count -= start) || force) {
      logMessage(LOG_DEBUG, "LogText line: line=%d, column=%d, count=%d", line, start, count);
      memcpy(&target[start], &source[start], count);
      if (!sendData(line, start, count)) {
         return 0;
      }
   }
   return 1;
}

static int
sendCurrentLine (void) {
   return sendLine(currentLine, 0);
}

static int
sendCursorRow (void) {
   return sendLine(cursorRow, 1);
}

static int
handleUpdate (unsigned char line) {
   logMessage(LOG_DEBUG, "Request line: (0X%2.2X) 0X%2.2X dec=%d", KEY_UPDATE, line, line);
   if (!line) return sendCursorRow();
   if (line <= screenHeight) {
      currentLine = line - 1;
      return sendCurrentLine();
   }
   logMessage(LOG_WARNING, "Invalid line request: %d", line);
   return 1;
}

static int
brl_writeWindow (BrailleDisplay *brl, const wchar_t *text) {
   if (deviceStatus == DEV_READY) {
      sendCurrentLine();
   }
   return 1;
}

static int
isOnline (void) {
   int online = serialTestLineDSR(serialDevice);
   if (online) {
      if (deviceStatus < DEV_ONLINE) {
         deviceStatus = DEV_ONLINE;
         logMessage(LOG_WARNING, "LogText online.");
      }
   } else {
      if (deviceStatus > DEV_OFFLINE) {
         deviceStatus = DEV_OFFLINE;
         logMessage(LOG_WARNING, "LogText offline.");
      }
   }
   return online;
}

static int
brl_writeStatus (BrailleDisplay *brl, const unsigned char *status) {
   if (isOnline()) {
      if (status[GSC_FIRST] == GSC_MARKER) {
         unsigned char row = status[gscScreenCursorRow];
         unsigned char column = status[gscScreenCursorColumn];
         row = MAX(1, MIN(row, screenHeight)) - 1;
         column = MAX(1, MIN(column, screenWidth)) - 1;
         if (deviceStatus < DEV_READY) {
            memset(targetImage, 0, sizeof(targetImage));
            currentContext = KTB_CTX_DEFAULT;
            currentLine = row;
            cursorRow = screenHeight;
            cursorColumn = screenWidth;
            deviceStatus = DEV_READY;
         }
         if ((row != cursorRow) || (column != cursorColumn)) {
            logMessage(LOG_DEBUG, "cursor moved: [%d,%d] -> [%d,%d]", cursorColumn, cursorRow, column, row);
            cursorRow = row;
            cursorColumn = column;
            sendCursorRow();
         }
      }
   }
   return 1;
}

static int
readKey (void) {
   unsigned char key;
   unsigned char arg;
   if (serialReadData(serialDevice, &key, 1, 0, 0) != 1) return EOF;
   switch (key) {
      default:
         arg = 0;
         break;
      case KEY_FUNCTION:
      case KEY_FUNCTION2:
      case KEY_UPDATE:
         while (serialReadData(serialDevice, &arg, 1, 0, 0) != 1) asyncWait(1);
         break;
   }
   {
      int result = COMPOUND_KEY(key, arg);
      logMessage(LOG_DEBUG, "Key read: %4.4X", result);
      return result;
   }
}

/*askUser
static unsigned char *selectedLine;

static void
replaceCharacters (const unsigned char *address, size_t count) {
   translateInputCells(&selectedLine[cursorColumn], address, count);
   cursorColumn += count;
}

static void
insertCharacters (const unsigned char *address, size_t count) {
   memmove(&selectedLine[cursorColumn+count], &selectedLine[cursorColumn], screenWidth-cursorColumn-count);
   replaceCharacters(address, count);
}

static void
deleteCharacters (size_t count) {
   memmove(&selectedLine[cursorColumn], &selectedLine[cursorColumn+count], screenWidth-cursorColumn-count);
   memset(&selectedLine[screenWidth-count], translateInputCell(' '), count);
}

static void
clearCharacters (void) {
   cursorColumn = 0;
   deleteCharacters(screenWidth);
}

static void
selectLine (unsigned char line) {
   selectedLine = &sourceImage[cursorRow = line][0];
   clearCharacters();
   deviceStatus = DEV_ONLINE;
}

static unsigned char *
askUser (const unsigned char *prompt) {
   unsigned char from;
   unsigned char to;
   selectLine(screenHeight-1);
   logMessage(LOG_DEBUG, "Prompt: %s", prompt);
   replaceCharacters(prompt, strlen(prompt));
   from = to = ++cursorColumn;
   sendCursorRow();
   while (1) {
      int key = readKey();
      if (key == EOF) {
         asyncWait(1);
         continue;
      }
      if ((key & KEY_MASK) == KEY_UPDATE) {
         handleUpdate(key >> KEY_SHIFT);
         continue;
      }
      if (isgraph(key)) {
         if (to < screenWidth) {
            unsigned char character = key & KEY_MASK;
            insertCharacters(&character, 1);
            ++to;
         } else {
            ringConsoleBell();
         }
      } else {
         switch (key) {
            case CR:
               if (to > from) {
                  size_t length = to - from;
                  unsigned char *response = malloc(length+1);
                  if (response) {
                     translateOutputCells(response, &selectedLine[from], length);
                     response[length] = 0;
                     logMessage(LOG_DEBUG, "Response: %s", response);
                     return response;
                  } else {
                     logSystemError("Download file path allocation");
                  }
               }
               return NULL;
            case BS:
               if (cursorColumn > from) {
                  --cursorColumn;
                  deleteCharacters(1);
                  --to;
               } else {
                  ringConsoleBell();
               }
               break;
            case DEL:
               if (cursorColumn < to) {
                  deleteCharacters(1);
                  --to;
               } else {
                  ringConsoleBell();
               }
               break;
            case KEY_FUNCTION_CURSOR_LEFT:
               if (cursorColumn > from) {
                  --cursorColumn;
               } else {
                  ringConsoleBell();
               }
               break;
            case KEY_FUNCTION_CURSOR_LEFT_JUMP:
               if (cursorColumn > from) {
                  cursorColumn = from;
               } else {
                  ringConsoleBell();
               }
               break;
            case KEY_FUNCTION_CURSOR_RIGHT:
               if (cursorColumn < to) {
                  ++cursorColumn;
               } else {
                  ringConsoleBell();
               }
               break;
            case KEY_FUNCTION_CURSOR_RIGHT_JUMP:
               if (cursorColumn < to) {
                  cursorColumn = to;
               } else {
                  ringConsoleBell();
               }
               break;
            default:
               ringConsoleBell();
               break;
         }
      }
      sendCursorRow();
   }
}
*/

static void
downloadFile (void) {
   if (makeDownloadFifo()) {
      int file = open(downloadPath, O_RDONLY);
      if (file != -1) {
         struct stat status;
         if (fstat(file, &status) != -1) {
            unsigned char buffer[0X400];
            const unsigned char *address = buffer;
            int count = 0;
            while (1) {
               const unsigned char *newline;
               if (!count) {
                  count = read(file, buffer, sizeof(buffer));
                  if (!count) {
                     static const unsigned char fileTrailer[] = {0X1A};
                     sendBytes(fileTrailer, sizeof(fileTrailer));
                     break;
                  }
                  if (count == -1) {
                     logSystemError("Download file read");
                     break;
                  }
                  address = buffer;
               }
               if ((newline = memchr(address, '\n', count))) {
                  static const unsigned char lineTrailer[] = {ASCII_CR, ASCII_LF};
                  size_t length = newline - address;
                  if (!sendBytes(address, length++)) break;
                  if (!sendBytes(lineTrailer, sizeof(lineTrailer))) break;
                  address += length;
                  count -= length;
               } else {
                  if (!sendBytes(address, count)) break;
                  count = 0;
               }
            }
         } else {
            logSystemError("Download file status");
         }
         if (close(file) == -1) {
            logSystemError("Download file close");
         }
      } else {
         logSystemError("Download file open");
      }
   } else {
      logMessage(LOG_WARNING, "Download path not specified.");
   }
}

static int
brl_readCommand (BrailleDisplay *brl, KeyTableCommandContext context) {
   int key = readKey();
   if (context != currentContext) {
      logMessage(LOG_DEBUG, "Context switch: %d -> %d", currentContext, context);
      switch (currentContext = context) {
         case KTB_CTX_DEFAULT:
            deviceStatus = DEV_ONLINE;
            break;
         default:
            break;
      }
   }
   if (key != EOF) {
      switch (key) {
         case KEY_FUNCTION_ENTER:
            return BRL_CMD_KEY(ENTER);
         case KEY_FUNCTION_TAB:
            return BRL_CMD_KEY(TAB);
         case KEY_FUNCTION_CURSOR_UP:
            return BRL_CMD_KEY(CURSOR_UP);
         case KEY_FUNCTION_CURSOR_DOWN:
            return BRL_CMD_KEY(CURSOR_DOWN);
         case KEY_FUNCTION_CURSOR_LEFT:
            return BRL_CMD_KEY(CURSOR_LEFT);
         case KEY_FUNCTION_CURSOR_RIGHT:
            return BRL_CMD_KEY(CURSOR_RIGHT);
         case KEY_FUNCTION_CURSOR_UP_JUMP:
            return BRL_CMD_KEY(HOME);
         case KEY_FUNCTION_CURSOR_DOWN_JUMP:
            return BRL_CMD_KEY(END);
         case KEY_FUNCTION_CURSOR_LEFT_JUMP:
            return BRL_CMD_KEY(PAGE_UP);
         case KEY_FUNCTION_CURSOR_RIGHT_JUMP:
            return BRL_CMD_KEY(PAGE_DOWN);
         case KEY_FUNCTION_F1:
            return BRL_CMD_KFN(1);
         case KEY_FUNCTION_F2:
            return BRL_CMD_KFN(2);
         case KEY_FUNCTION_F3:
            return BRL_CMD_KFN(3);
         case KEY_FUNCTION_F4:
            return BRL_CMD_KFN(4);
         case KEY_FUNCTION_F5:
            return BRL_CMD_KFN(5);
         case KEY_FUNCTION_F6:
            return BRL_CMD_KFN(6);
         case KEY_FUNCTION_F7:
            return BRL_CMD_KFN(7);
         case KEY_FUNCTION_F9:
            return BRL_CMD_KFN(9);
         case KEY_FUNCTION_F10:
            return BRL_CMD_KFN(10);
         case KEY_COMMAND: {
            int command;
            while ((command = readKey()) == EOF) asyncWait(1);
            logMessage(LOG_DEBUG, "Received command: (0x%2.2X) 0x%4.4X", KEY_COMMAND, command);
            switch (command) {
               case KEY_COMMAND:
                  /* pressing the escape command twice will pass it through */
                  return BRL_CMD_BLK(PASSDOTS) + translateInputCell(KEY_COMMAND);
               case KEY_COMMAND_SWITCHVT_PREV:
                  return BRL_CMD_SWITCHVT_PREV;
               case KEY_COMMAND_SWITCHVT_NEXT:
                  return BRL_CMD_SWITCHVT_NEXT;
               case KEY_COMMAND_SWITCHVT_1:
                  return BRL_CMD_BLK(SWITCHVT) + 0;
               case KEY_COMMAND_SWITCHVT_2:
                  return BRL_CMD_BLK(SWITCHVT) + 1;
               case KEY_COMMAND_SWITCHVT_3:
                  return BRL_CMD_BLK(SWITCHVT) + 2;
               case KEY_COMMAND_SWITCHVT_4:
                  return BRL_CMD_BLK(SWITCHVT) + 3;
               case KEY_COMMAND_SWITCHVT_5:
                  return BRL_CMD_BLK(SWITCHVT) + 4;
               case KEY_COMMAND_SWITCHVT_6:
                  return BRL_CMD_BLK(SWITCHVT) + 5;
               case KEY_COMMAND_SWITCHVT_7:
                  return BRL_CMD_BLK(SWITCHVT) + 6;
               case KEY_COMMAND_SWITCHVT_8:
                  return BRL_CMD_BLK(SWITCHVT) + 7;
               case KEY_COMMAND_SWITCHVT_9:
                  return BRL_CMD_BLK(SWITCHVT) + 8;
               case KEY_COMMAND_SWITCHVT_10:
                  return BRL_CMD_BLK(SWITCHVT) + 9;
               case KEY_COMMAND_PAGE_UP:
                  return BRL_CMD_KEY(PAGE_UP);
               case KEY_COMMAND_PAGE_DOWN:
                  return BRL_CMD_KEY(PAGE_DOWN);
               case KEY_COMMAND_PREFMENU:
                  currentLine = 0;
                  cursorRow = 0;
                  cursorColumn = 31;
                  sendCursorRow();
                  return BRL_CMD_PREFMENU;
               case KEY_COMMAND_PREFSAVE:
                  return BRL_CMD_PREFSAVE;
               case KEY_COMMAND_PREFLOAD:
                  return BRL_CMD_PREFLOAD;
               case KEY_COMMAND_FREEZE_ON:
                  return BRL_CMD_FREEZE | BRL_FLG_TOGGLE_ON;
               case KEY_COMMAND_FREEZE_OFF:
                  return BRL_CMD_FREEZE | BRL_FLG_TOGGLE_OFF;
               case KEY_COMMAND_RESTARTBRL:
                  return BRL_CMD_RESTARTBRL;
               case KEY_COMMAND_DOWNLOAD:
                  downloadFile();
                  break;
               default:
                  logMessage(LOG_WARNING, "Unknown command: (0X%2.2X) 0X%4.4X", KEY_COMMAND, command);
                  break;
            }
            break;
         }
         default:
            switch (key & KEY_MASK) {
               case KEY_UPDATE:
                  handleUpdate(key >> KEY_SHIFT);
                  break;
               case KEY_FUNCTION:
                  logMessage(LOG_WARNING, "Unknown function: (0X%2.2X) 0X%4.4X", KEY_COMMAND, key>>KEY_SHIFT);
                  break;
               default: {
                  unsigned char dots = translateInputCell(key);
                  logMessage(LOG_DEBUG, "Received character: 0X%2.2X dec=%d dots=%2.2X", key, key, dots);
                  return BRL_CMD_BLK(PASSDOTS) + dots;
               }
            }
            break;
      }
   }
   return EOF;
}
