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

/* MiniBraille/braille.c - Braille display library
 * the following Tieman B.V. braille terminals are supported
 *
 * - MiniBraille v 1.5 (20 braille cells + 2 status)
 *   (probably other versions too)
 *
 * Brailcom o.p.s. <technik@brailcom.cz>
 *
 * Thanks to Tieman B.V., which gives me protocol information. Author.
 */

#include "prologue.h"

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

#include "log.h"
#include "timing.h"
#include "ascii.h"
#include "message.h"

#define BRL_STATUS_FIELDS sfCursorAndWindowColumn, sfCursorAndWindowRow, sfStateDots
#define BRL_HAVE_STATUS_CELLS
#include "brl_driver.h"

#include "io_serial.h"
static SerialDevice *serialDevice = NULL;
static const unsigned int serialBaud = 9600;
static unsigned int serialCharactersPerSecond;

#define KEY_F1     0x01
#define KEY_F2     0x02
#define KEY_LEFT   0x04
#define KEY_UP     0x08
#define KEY_CENTER 0x10
#define KEY_DOWN   0x20
#define KEY_RIGHT  0x40

#define POST_COMMAND_DELAY 30

static unsigned char textCells[20];
static unsigned char statusCells[2];
static int refreshNeeded;

static int
writeData (BrailleDisplay *brl, const unsigned char *bytes, int count) {
  ssize_t result = serialWriteData(serialDevice, bytes, count);

  if (result == -1) {
    logSystemError("write");
    return 0;
  }

  drainBrailleOutput(brl, 0);
  brl->writeDelay += (result * 1000 / serialCharactersPerSecond) + POST_COMMAND_DELAY;
  return 1;
}

static int
writeCells  (BrailleDisplay *brl) {
  static const unsigned char beginSequence[] = {ESC, 'Z', '1'};
  static const unsigned char endSequence[] = {CR};

  unsigned char buffer[sizeof(beginSequence) + sizeof(statusCells) + sizeof(textCells) + sizeof(endSequence)];
  unsigned char *byte = buffer;

  byte = mempcpy(byte, beginSequence, sizeof(beginSequence));
  byte = translateOutputCells(byte, statusCells, sizeof(statusCells));
  byte = translateOutputCells(byte, textCells, sizeof(textCells));
  byte = mempcpy(byte, endSequence, sizeof(endSequence));

  return writeData(brl, buffer, byte-buffer);
}

static void
updateCells (unsigned char *target, const unsigned char *source, size_t count) {
  if (cellsHaveChanged(target, source, count, NULL, NULL, NULL)) {
    refreshNeeded = 1;
  }
}

static void
clearCells (unsigned char *cells, size_t count) {
  memset(cells, 0, count);
  refreshNeeded = 1;
}

static int
beep (BrailleDisplay *brl) {
  static const unsigned char sequence[] = {ESC, 'B', CR};
  return writeData(brl, sequence, sizeof(sequence));
}

static int
inputFunction_showTime (BrailleDisplay *brl) {
  time_t clock = time(NULL);
  const struct tm *local = localtime(&clock);
  char text[sizeof(textCells) + 1];
  strftime(text, sizeof(text), "%Y-%m-%d %H:%M:%S", local);
  message(NULL, text, 0);
  return BRL_CMD_NOOP;
}

static unsigned char cursorDots;
static unsigned char cursorOffset;

static void
putCursor (BrailleDisplay *brl) {
  brl->buffer[cursorOffset] = cursorDots;
}

static int
inputFunction_incrementCursor (BrailleDisplay *brl) {
  if (++cursorOffset < sizeof(textCells)) return BRL_CMD_NOOP;

  cursorOffset = 0;
  return BRL_CMD_FWINRT;
}

static int
inputFunction_decrementCursor (BrailleDisplay *brl) {
  if (cursorOffset) {
    --cursorOffset;
    return BRL_CMD_NOOP;
  }

  cursorOffset = sizeof(textCells) - 1;
  return BRL_CMD_FWINLT;
}

typedef struct InputModeStruct InputMode;

typedef enum {
  IBT_unbound = 0, /* automatically set if not explicitly initialized */
  IBT_command,
  IBT_block,
  IBT_function,
  IBT_submode
} InputBindingType;

typedef union {
  int command;
  int block;
  int (*function) (BrailleDisplay *brl);
  const InputMode *submode;
} InputBindingValue;

typedef struct {
  InputBindingType type;
  InputBindingValue value;
} InputBinding;

struct InputModeStruct {
  InputBinding keyF1, keyF2, keyLeft, keyUp, keyCenter, keyDown, keyRight;

  unsigned temporary:1;
  void (*modifyWindow) (BrailleDisplay *brl);
  const char *name;
};

#define BIND(k,t,v) .key##k = {.type = IBT_##t, .value.t = (v)}
#define BIND_COMMAND(k,c) BIND(k, command, BRL_CMD_##c)
#define BIND_BLOCK(k,b) BIND(k, block, BRL_CMD_BLK(b))
#define BIND_FUNCTION(k,f) BIND(k, function, inputFunction_##f)
#define BIND_SUBMODE(k,m) BIND(k, submode, &inputMode_##m)

static const InputMode inputMode_char_f1 = {
  BIND_BLOCK(F1, SETLEFT),
  BIND_BLOCK(F2, DESCCHAR),
  BIND_BLOCK(Left, CLIP_ADD),
  BIND_BLOCK(Up, CLIP_NEW),
  BIND_BLOCK(Center, ROUTE),
  BIND_BLOCK(Down, COPY_RECT),
  BIND_BLOCK(Right, COPY_LINE),

  .temporary = 1,
  .name = "Char-F1"
};

static const InputMode inputMode_f1_f1 = {
  BIND_COMMAND(F1, HELP),
  BIND_COMMAND(F2, LEARN),
  BIND_COMMAND(Left, INFO),
  BIND_FUNCTION(Right, showTime),
  BIND_COMMAND(Up, PREFLOAD),
  BIND_COMMAND(Down, PREFMENU),
  BIND_COMMAND(Center, PREFSAVE),

  .temporary = 1,
  .name = "F1-F1"
};

static const InputMode inputMode_f1_f2 = {
  BIND_COMMAND(F1, FREEZE),
  BIND_COMMAND(F2, DISPMD),
  BIND_COMMAND(Left, ATTRVIS),
  BIND_COMMAND(Right, CSRVIS),
  BIND_COMMAND(Up, SKPBLNKWINS),
  BIND_COMMAND(Down, SKPIDLNS),
  BIND_COMMAND(Center, SIXDOTS),

  .temporary = 1,
  .name = "F1-F2"
};

static const InputMode inputMode_f1_left = {

  .temporary = 1,
  .name = "F1-Left"
};

static const InputMode inputMode_f1_right = {
  BIND_COMMAND(F2, AUTOSPEAK),
  BIND_COMMAND(Left, SAY_ABOVE),
  BIND_COMMAND(Right, SAY_BELOW),
  BIND_COMMAND(Up, MUTE),
  BIND_COMMAND(Down, SAY_LINE),
  BIND_COMMAND(Center, SPKHOME),

  .temporary = 1,
  .name = "F1-Right"
};

static const InputMode inputMode_f1_up = {
  BIND_COMMAND(F1, PRSEARCH),
  BIND_COMMAND(F2, NXSEARCH),
  BIND_COMMAND(Left, ATTRUP),
  BIND_COMMAND(Right, ATTRDN),
  BIND_COMMAND(Up, PRPGRPH),
  BIND_COMMAND(Down, NXPGRPH),
  BIND_COMMAND(Center, CSRJMP_VERT),

  .temporary = 1,
  .name = "F1-Up"
};

static const InputMode inputMode_f1_down = {
  BIND_COMMAND(F1, PRPROMPT),
  BIND_COMMAND(F2, NXPROMPT),
  BIND_COMMAND(Left, FWINLTSKIP),
  BIND_COMMAND(Right, FWINRTSKIP),
  BIND_COMMAND(Up, PRDIFLN),
  BIND_COMMAND(Down, NXDIFLN),
  BIND_COMMAND(Center, PASTE),

  .temporary = 1,
  .name = "F1-Down"
};

static const InputMode inputMode_f1_center = {
  BIND_SUBMODE(F1, char_f1),
  BIND_FUNCTION(Left, decrementCursor),
  BIND_FUNCTION(Right, incrementCursor),
  BIND_COMMAND(Up, LNUP),
  BIND_COMMAND(Down, LNDN),

  .temporary = 0,
  .modifyWindow = putCursor,
  .name = "F1-Center"
};

static const InputMode inputMode_f1 = {
  BIND_SUBMODE(F1, f1_f1),
  BIND_SUBMODE(F2, f1_f2),
  BIND_SUBMODE(Left, f1_left),
  BIND_SUBMODE(Right, f1_right),
  BIND_SUBMODE(Up, f1_up),
  BIND_SUBMODE(Down, f1_down),
  BIND_SUBMODE(Center, f1_center),

  .temporary = 1,
  .name = "F1"
};

static const InputMode inputMode_f2 = {
  BIND_COMMAND(F1, TOP_LEFT),
  BIND_COMMAND(F2, BOT_LEFT),
  BIND_COMMAND(Left, LNBEG),
  BIND_COMMAND(Right, LNEND),
  BIND_COMMAND(Up, TOP),
  BIND_COMMAND(Down, BOT),
  BIND_COMMAND(Center, CSRTRK),

  .temporary = 1,
  .name = "F2"
};

static const InputMode inputMode_basic = {
  BIND_SUBMODE(F1, f1),
  BIND_SUBMODE(F2, f2),
  BIND_COMMAND(Left, FWINLT),
  BIND_COMMAND(Right, FWINRT),
  BIND_COMMAND(Up, LNUP),
  BIND_COMMAND(Down, LNDN),
  BIND_COMMAND(Center, RETURN),

  .temporary = 0,
  .name = "Basic"
};

static const InputMode *inputMode;
static TimePeriod inputPeriod;

static void
setInputMode (const InputMode *mode) {
  if (mode->temporary) {
    char title[sizeof(textCells) + 1];
    snprintf(title, sizeof(title), "%s Mode", mode->name);
    message(NULL, title, MSG_NODELAY|MSG_SILENT);
  }

  inputMode = mode;
  startTimePeriod(&inputPeriod, 3000);
}

static void
resetInputMode (void) {
  setInputMode(&inputMode_basic);
}

static int
brl_construct (BrailleDisplay *brl, char **parameters, const char *device) {
  if (!isSerialDeviceIdentifier(&device)) {
    unsupportedDeviceIdentifier(device);
    return 0;
  }

  if ((serialDevice = serialOpenDevice(device))) {
    if (serialRestartDevice(serialDevice, serialBaud)) {
      serialCharactersPerSecond = serialBaud / serialGetCharacterBits(serialDevice);

      /* hm, how to switch to 38400 ? 
      static const unsigned char sequence[] = {ESC, 'V', CR};
      writeData(brl, sequence, sizeof(sequence));
      serialDiscardInput(serialDevice);
      serialSetBaud(serialDevice, 38400);
      */

      MAKE_OUTPUT_TABLE(0X01, 0X02, 0X04, 0X80, 0X40, 0X20, 0X08, 0X10);
      clearCells(textCells,  sizeof(textCells));
      clearCells(statusCells,  sizeof(statusCells));
      resetInputMode();

      cursorDots = 0XFF;
      cursorOffset = sizeof(textCells) / 2;

      brl->textColumns = sizeof(textCells);
      brl->textRows = 1;
      brl->statusColumns = sizeof(statusCells);
      brl->statusRows = 1;

      beep(brl);
      return 1;
    }

    serialCloseDevice(serialDevice);
    serialDevice = NULL;
  }

  return 0;
}

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

static int
brl_writeWindow (BrailleDisplay *brl, const wchar_t *text) {
  if (inputMode->modifyWindow) inputMode->modifyWindow(brl);
  updateCells(textCells, brl->buffer, sizeof(textCells));
  if (refreshNeeded && !inputMode->temporary) {
    writeCells(brl);
    refreshNeeded = 0;
  }
  return 1;
}

static int
brl_writeStatus (BrailleDisplay *brl, const unsigned char *s) {
  updateCells(statusCells, s, sizeof(statusCells));
  return 1;
}

static int
brl_readCommand (BrailleDisplay *brl, KeyTableCommandContext context) {
  unsigned char byte;
  const InputMode *mode;
  const InputBinding *binding;

  {
    int result = serialReadData(serialDevice, &byte, 1, 0, 0);

    if (result == 0) {
      if (inputMode->temporary)
        if (afterTimePeriod(&inputPeriod, NULL))
          resetInputMode();

      return EOF;
    }

    if (result == -1) {
      logSystemError("read");
      return BRL_CMD_RESTARTBRL;
    }
  }

  mode = inputMode;
  if (mode->temporary) resetInputMode();

  switch (byte) {
    case KEY_F1:     binding = &mode->keyF1;     break;
    case KEY_F2:     binding = &mode->keyF2;     break;
    case KEY_LEFT:   binding = &mode->keyLeft;   break;
    case KEY_RIGHT:  binding = &mode->keyRight;  break;
    case KEY_UP:     binding = &mode->keyUp;     break;
    case KEY_DOWN:   binding = &mode->keyDown;   break;
    case KEY_CENTER: binding = &mode->keyCenter; break;

    default:
      logMessage(LOG_WARNING, "unhandled key: %s -> %02X", mode->name, byte);
      beep(brl);
      return EOF;
  }

  switch (binding->type) {
    case IBT_unbound:
      logMessage(LOG_WARNING, "unbound key: %s -> %02X", mode->name, byte);
      beep(brl);
      break;

    case IBT_command:
      return binding->value.command;

    case IBT_block:
      return binding->value.block + cursorOffset;

    case IBT_function:
      return binding->value.function(brl);

    case IBT_submode: {
      setInputMode(binding->value.submode);
      break;
    }

    default:
      logMessage(LOG_WARNING, "unhandled input binding type: %02X", binding->type);
      break;
  }

  return BRL_CMD_NOOP;
}
