/*
 * 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-2021 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 <string.h>
#include <errno.h>

#include "log.h"
#include "crc_generate.h"
#include "async.h"
#include "async_alarm.h"
#include "timing.h"

#include "brl_driver.h"
#include "brldefs-cn.h"

#define PROBE_RETRY_LIMIT 0
#define PROBE_RESPONSE_TIMEOUT 1000
#define COMMAND_RESPONSE_TIMEOUT 10000
#define MAXIMUM_RESPONSE_SIZE 0X100

#define KEYS_POLL_INTERVAL 100
#define MOTORS_POLL_INTERVAL 400
#define ROW_UPDATE_TIME 1200
#define CELLS_RESET_TIME 14000
#define UPDATE_RETRY_DELAY 5000

BEGIN_KEY_NAME_TABLE(navigation)
  KEY_NAME_ENTRY(CN_KEY_Help, "Help"),
  KEY_NAME_ENTRY(CN_KEY_Refresh, "Refresh"),

  KEY_NAME_ENTRY(CN_KEY_Line1, "Line1"),
  KEY_NAME_ENTRY(CN_KEY_Line2, "Line2"),
  KEY_NAME_ENTRY(CN_KEY_Line3, "Line3"),
  KEY_NAME_ENTRY(CN_KEY_Line4, "Line4"),
  KEY_NAME_ENTRY(CN_KEY_Line5, "Line5"),
  KEY_NAME_ENTRY(CN_KEY_Line6, "Line6"),
  KEY_NAME_ENTRY(CN_KEY_Line7, "Line7"),
  KEY_NAME_ENTRY(CN_KEY_Line8, "Line8"),
  KEY_NAME_ENTRY(CN_KEY_Line9, "Line9"),

  KEY_NAME_ENTRY(CN_KEY_Back, "Back"),
  KEY_NAME_ENTRY(CN_KEY_Menu, "Menu"),
  KEY_NAME_ENTRY(CN_KEY_Forward, "Forward"),
END_KEY_NAME_TABLE

BEGIN_KEY_NAME_TABLES(all)
  KEY_NAME_TABLE(navigation),
END_KEY_NAME_TABLES

DEFINE_KEY_TABLE(all)

BEGIN_KEY_TABLE_LIST
  &KEY_TABLE_DEFINITION(all),
END_KEY_TABLE_LIST

typedef struct {
  unsigned char hasChanged:1;

  unsigned char force;
  unsigned char cells[];
} RowEntry;

typedef BrailleResponseResult ProbeResponseHandler (
  BrailleDisplay *brl,
  const unsigned char *response, size_t size
);

struct BrailleDataStruct {
  CRCGenerator *crcGenerator;
  AsyncHandle keysPollerAlarm;

  struct {
    ProbeResponseHandler *responseHandler;
    unsigned int protocolVersion;
  } probe;

  struct {
    TimePeriod timeout;
    unsigned char command;
    unsigned char waiting:1;
  } response;

  struct {
    TimePeriod retryDelay;
    RowEntry **rowEntries;
    unsigned int firstChangedRow;
    unsigned int lastRowSent;
    unsigned char resetCells:1;
  } window;

  struct {
    TimePeriod delay;
    CN_PacketInteger flags;
  } status;

  struct {
    KeyNumberSet pressed;
  } keys;
};

static crc_t
makePacketChecksum (BrailleDisplay *brl, const void *packet, size_t size) {
  CRCGenerator *crc = brl->data->crcGenerator;
  crcResetGenerator(crc);
  crcAddData(crc, packet, size);
  return crcGetChecksum(crc);
}

typedef enum {
  PVS_WAITING,
  PVS_STARTED,
  PVS_DONE
} PacketVerificationState;

typedef struct {
  PacketVerificationState state;
  unsigned escaped:1;
} PacketVerificationData;

static BraillePacketVerifierResult
verifyPacket (
  BrailleDisplay *brl,
  unsigned char *bytes, size_t size,
  size_t *length, void *data
) {
  PacketVerificationData *pvd = data;
  unsigned char *byte = &bytes[size-1];

  if (*byte == CN_PACKET_FRAMING_BYTE) {
    if ((pvd->state += 1) == PVS_DONE) {
      if (pvd->escaped) return BRL_PVR_INVALID;
      *length = size - 1;
    } else {
      *length = MAXIMUM_RESPONSE_SIZE;
    }

    return BRL_PVR_EXCLUDE;
  }

  if (pvd->state == PVS_WAITING) {
    return BRL_PVR_INVALID;
  }

  if (*byte == CN_PACKET_ESCAPE_BYTE) {
    if (pvd->escaped) return BRL_PVR_INVALID;
    pvd->escaped = 1;
    return BRL_PVR_EXCLUDE;
  }

  if (pvd->escaped) {
    pvd->escaped = 0;
    *byte ^= CN_PACKET_ESCAPE_BIT;
  }

  return BRL_PVR_INCLUDE;
}

static size_t
readPacket (BrailleDisplay *brl, void *packet, size_t size) {
  while (1) {
    PacketVerificationData pvd = {
      .state = PVS_WAITING
    };

    size_t length = readBraillePacket(brl, NULL, packet, size, verifyPacket, &pvd);

    if (length > 0) {
      if (length < 3) {
        logShortPacket(packet, length);
        continue;
      }

      {
        crc_t expected = CN_getResponseInteger(packet, (length -= 2));
        crc_t actual = makePacketChecksum(brl, packet, length);

        if (actual != expected) {
          logBytes(LOG_WARNING,
            "input packet checksum mismatch:"
            " Actual:%"PRIcrc " Expected:%"PRIcrc,
            packet, length,
            actual, expected
          );

          continue;
        }
      }

      {
        const unsigned char *bytes = packet;
        size_t expected = 0;

        switch (bytes[0]) {
          case CN_CMD_COLUMN_COUNT:
          case CN_CMD_ROW_COUNT:
          case CN_CMD_PROTOCOL_VERSION:
          case CN_CMD_FIRMWARE_VERSION:
          case CN_CMD_DEVICE_STATUS:
          case CN_CMD_PRESSED_KEYS:
          case CN_CMD_SEND_ROW:
          case CN_CMD_RESET_CELLS:
            expected = 3;
            break;

          default:
            logUnexpectedPacket(packet, length);
            continue;
        }

        if (length < expected) {
          logTruncatedPacket(packet, length);
          continue;
        }
      }
    }

    return length;
  }
}

static inline void
addByteToPacket (unsigned char **target, unsigned char byte) {
  if ((byte == CN_PACKET_ESCAPE_BYTE) || (byte == CN_PACKET_FRAMING_BYTE)) {
    *(*target)++ = CN_PACKET_ESCAPE_BYTE;
    byte ^= CN_PACKET_ESCAPE_BIT;
  }

  *(*target)++ = byte;
}

static int
writePacket (BrailleDisplay *brl, const unsigned char *packet, size_t size) {
  logBytes(LOG_CATEGORY(OUTPUT_PACKETS), "raw", packet, size);

  unsigned char buffer[1 + ((size + 2) * 2) + 1];
  unsigned char *target = buffer;
  *target++ = CN_PACKET_FRAMING_BYTE;

  {
    const unsigned char *source = packet;
    const unsigned char *end = source + size;
    while (source < end) addByteToPacket(&target, *source++);
  }

  {
    uint16_t checksum = makePacketChecksum(brl, packet, size);
    addByteToPacket(&target, (checksum & UINT8_MAX));
    addByteToPacket(&target, (checksum >> 8));
  }

  *target++ = CN_PACKET_FRAMING_BYTE;
  size = target - buffer;
  int ok = writeBraillePacket(brl, NULL, buffer, size);

  if (ok) {
    brl->data->response.waiting = 1;
    startTimePeriod(&brl->data->response.timeout, COMMAND_RESPONSE_TIMEOUT);
    brl->data->response.command = packet[0];
  } else {
    brl->hasFailed = 1;
  }

  return ok;
}

static int
writeSimpleCommand (BrailleDisplay *brl, unsigned char command) {
  const unsigned char packet[] = {command};
  return writePacket(brl, packet, sizeof(packet));
}

static RowEntry *
getRowEntry (BrailleDisplay *brl, unsigned int index) {
  return brl->data->window.rowEntries[index];
}

static void
deallocateRowEntries (BrailleDisplay *brl, unsigned int count) {
  RowEntry ***rowEntries = &brl->data->window.rowEntries;

  if (*rowEntries) {
    while (count > 0) free(getRowEntry(brl, --count));
    free(*rowEntries);
    *rowEntries = NULL;
  }
}

static int
allocateRowEntries (BrailleDisplay *brl) {
  RowEntry ***rowEntries = &brl->data->window.rowEntries;

  if (!(*rowEntries = malloc(ARRAY_SIZE(*rowEntries, brl->textRows)))) {
    logMallocError();
    return 0;
  }

  for (unsigned int index=0; index<brl->textRows; index+=1) {
    RowEntry **row = &(*rowEntries)[index];
    size_t size = sizeof(**row) + brl->textColumns;

    if (!(*row = malloc(size))) {
      logMallocError();
      deallocateRowEntries(brl, (index + 1));
      return 0;
    }

    memset(*row, 0, size);
    (*row)->force = 1;
  }

  return 1;
}

static void
setRowHasChanged (BrailleDisplay *brl, unsigned int index) {
  getRowEntry(brl, index)->hasChanged = 1;

  if (index < brl->data->window.firstChangedRow) {
    brl->data->window.firstChangedRow = index;
  }
}

static void
resendRow (BrailleDisplay *brl) {
  setRowHasChanged(brl, brl->data->window.lastRowSent);
}

static int
refreshAllRows (BrailleDisplay *brl) {
  brl->data->window.resetCells = 1;
  return 1;
}

static int
refreshRow (BrailleDisplay *brl, int row) {
  return refreshAllRows(brl); // for now
}

ASYNC_ALARM_CALLBACK(CN_keysPoller) {
  BrailleDisplay *brl = parameters->data;

  if (!brl->data->response.waiting) {
    writeSimpleCommand(brl, CN_CMD_PRESSED_KEYS);
  } else if (afterTimePeriod(&brl->data->response.timeout, NULL)) {
    unsigned char command = brl->data->response.command;
    logMessage(LOG_WARNING, "command response timeout: Cmd:0X%02X", command);

    switch (command) {
      case CN_CMD_SEND_ROW:
        resendRow(brl);
        break;

      case CN_CMD_RESET_CELLS:
        brl->data->window.resetCells = 1;;
        break;

      default:
        break;
    }

    writeSimpleCommand(brl, CN_CMD_DEVICE_STATUS);
  }
}

static void
stopKeysPoller (BrailleDisplay *brl) {
  AsyncHandle *alarm = &brl->data->keysPollerAlarm;

  if (*alarm) {
    asyncCancelRequest(*alarm);
    *alarm = NULL;
  }
}

static int
startKeysPoller (BrailleDisplay *brl) {
  AsyncHandle alarm = brl->data->keysPollerAlarm;
  if (alarm) return 1;

  if (asyncNewRelativeAlarm(&alarm, 0, CN_keysPoller, brl)) {
    if (asyncResetAlarmInterval(alarm, KEYS_POLL_INTERVAL)) {
      brl->data->keysPollerAlarm = alarm;
      return 1;
    }

    asyncCancelRequest(alarm);
  }

  return 0;
}

static BrailleResponseResult
isIdentityResponse (BrailleDisplay *brl, const void *packet, size_t size) {
  brl->data->response.waiting = 0;
  ProbeResponseHandler *handler = brl->data->probe.responseHandler;
  brl->data->probe.responseHandler = NULL;
  return handler(brl, packet, size);
}

static BrailleResponseResult
writeProbeCommand (BrailleDisplay *brl, unsigned char command, ProbeResponseHandler *handler) {
  if (!writeSimpleCommand(brl, command)) return 0;
  brl->data->probe.responseHandler = handler;
  return 1;
}

static BrailleResponseResult
writeNextProbeCommand (BrailleDisplay *brl, unsigned char command, ProbeResponseHandler *handler) {
  return writeProbeCommand(brl, command, handler)? BRL_RSP_CONTINUE: BRL_RSP_FAIL;
}

static BrailleResponseResult
handleDeviceStatus (BrailleDisplay *brl, const unsigned char *response, size_t size) {
  if (response[0] != CN_CMD_DEVICE_STATUS) return BRL_RSP_UNEXPECTED;
  brl->data->status.flags = CN_getResponseResult(response);
  return BRL_RSP_DONE;
}

static BrailleResponseResult
handleFirmwareVersion (BrailleDisplay *brl, const unsigned char *response, size_t size) {
  if (response[0] != CN_CMD_FIRMWARE_VERSION) return BRL_RSP_UNEXPECTED;

  response += 1;
  size -= 1;
  logMessage(LOG_INFO, "Firmware Version: %.*s", (int)size, response);

  return writeNextProbeCommand(brl, CN_CMD_DEVICE_STATUS, handleDeviceStatus);
}

static BrailleResponseResult
handleProtocolVersion (BrailleDisplay *brl, const unsigned char *response, size_t size) {
  if (response[0] != CN_CMD_PROTOCOL_VERSION) return BRL_RSP_UNEXPECTED;
  brl->data->probe.protocolVersion = CN_getResponseResult(response);
  logMessage(LOG_INFO, "Protocol Version: %u", brl->data->probe.protocolVersion);
  return writeNextProbeCommand(brl, CN_CMD_FIRMWARE_VERSION, handleFirmwareVersion);
}

static BrailleResponseResult
handleRowCount (BrailleDisplay *brl, const unsigned char *response, size_t size) {
  if (response[0] != CN_CMD_ROW_COUNT) return BRL_RSP_UNEXPECTED;
  brl->textRows = CN_getResponseResult(response);
  return writeNextProbeCommand(brl, CN_CMD_PROTOCOL_VERSION, handleProtocolVersion);
}

static BrailleResponseResult
handleColumnCount (BrailleDisplay *brl, const unsigned char *response, size_t size) {
  if (response[0] != CN_CMD_COLUMN_COUNT) return BRL_RSP_UNEXPECTED;
  brl->textColumns = CN_getResponseResult(response);
  return writeNextProbeCommand(brl, CN_CMD_ROW_COUNT, handleRowCount);
}

static int
writeIdentifyRequest (BrailleDisplay *brl) {
  return writeProbeCommand(brl, CN_CMD_COLUMN_COUNT, handleColumnCount);
}

static int
connectResource (BrailleDisplay *brl, const char *identifier) {
  static const SerialParameters serialParameters = {
    SERIAL_DEFAULT_PARAMETERS,
    .baud = 9600
  };

  BEGIN_USB_STRING_LIST(usbManufacturers_16C0_05E1)
    "bristolbraille.co.uk",
  END_USB_STRING_LIST

  BEGIN_USB_STRING_LIST(usbProducts_16C0_05E1)
    "Canute 360",
  END_USB_STRING_LIST

  BEGIN_USB_CHANNEL_DEFINITIONS
    { /* all models */
      .vendor=0X16C0, .product=0X05E1,
      .manufacturers = usbManufacturers_16C0_05E1,
      .products = usbProducts_16C0_05E1,
      .configuration=1, .interface=1, .alternative=0,
      .inputEndpoint=3, .outputEndpoint=2,
      .serial = &serialParameters,
      .resetDevice = 1
    },
  END_USB_CHANNEL_DEFINITIONS

  GioDescriptor descriptor;
  gioInitializeDescriptor(&descriptor);

  descriptor.serial.parameters = &serialParameters;

  descriptor.usb.channelDefinitions = usbChannelDefinitions;

  if (connectBrailleResource(brl, identifier, &descriptor, NULL)) {
    return 1;
  }

  return 0;
}

static int
brl_construct (BrailleDisplay *brl, char **parameters, const char *device) {
  if ((brl->data = malloc(sizeof(*brl->data)))) {
    memset(brl->data, 0, sizeof(*brl->data));

    brl->data->crcGenerator = NULL;
    brl->data->keysPollerAlarm = NULL;

    brl->data->probe.responseHandler = NULL;
    brl->data->probe.protocolVersion = 0;

    brl->data->response.waiting = 0;

    startTimePeriod(&brl->data->window.retryDelay, 0);
    brl->data->window.rowEntries = NULL;
    brl->data->window.resetCells = 0;

    brl->data->keys.pressed = 0;

    {
      static const CRCAlgorithm algorithm = {
        .primaryName = CN_CRC_ALGORITHM_NAME,
        .checksumWidth = CN_CRC_CHECKSUM_WIDTH,
        .reflectData = CN_CRC_REFLECT_DATA,
        .reflectResult = CN_CRC_REFLECT_RESULT,
        .generatorPolynomial = UINT16_C(CN_CRC_GENERATOR_POLYNOMIAL),
        .initialValue = UINT16_C(CN_CRC_INITIAL_VALUE),
        .xorMask = UINT16_C(CN_CRC_XOR_MASK),
        .checkValue = UINT16_C(CN_CRC_CHECK_VALUE),
        .residue = UINT16_C(CN_CRC_RESIDUE),
      };

      brl->data->crcGenerator = crcNewGenerator(&algorithm);
    }

    if (brl->data->crcGenerator) {
      if (connectResource(brl, device)) {
        unsigned char response[MAXIMUM_RESPONSE_SIZE];

        if (probeBrailleDisplay(brl, PROBE_RETRY_LIMIT,
                                NULL, PROBE_RESPONSE_TIMEOUT,
                                writeIdentifyRequest,
                                readPacket, &response, sizeof(response),
                                isIdentityResponse)) {
          if (allocateRowEntries(brl)) {
            brl->refreshBrailleDisplay = refreshAllRows;
            brl->refreshBrailleRow = refreshRow;
            brl->cellSize = 6;

            setBrailleKeyTable(brl, &KEY_TABLE_DEFINITION(all));
            makeOutputTable(dotsTable_ISO11548_1);

            if (startKeysPoller(brl)) {
              return 1;
            }

            deallocateRowEntries(brl, brl->textRows);
          }
        }

        disconnectBrailleResource(brl, NULL);
      }

      crcDestroyGenerator(brl->data->crcGenerator);
    }

    free(brl->data);
  } else {
    logMallocError();
  }

  return 0;
}

static void
brl_destruct (BrailleDisplay *brl) {
  stopKeysPoller(brl);
  disconnectBrailleResource(brl, NULL);

  deallocateRowEntries(brl, brl->textRows);
  crcDestroyGenerator(brl->data->crcGenerator);

  free(brl->data);
  brl->data = NULL;
}

static int
brl_writeWindow (BrailleDisplay *brl, const wchar_t *text) {
  unsigned int length = brl->textColumns;
  const unsigned char *cells = brl->buffer;

  for (unsigned int index=0; index<brl->textRows; index+=1) {
    RowEntry *row = getRowEntry(brl, index);

    if (cellsHaveChanged(row->cells, cells, length, NULL, NULL, &row->force)) {
      setRowHasChanged(brl, index);
    }

    cells += length;
  }

  return 1;
}

static int
startUpdate (BrailleDisplay *brl) {
  if (!afterTimePeriod(&brl->data->window.retryDelay, NULL)) return 0;

  if (brl->data->window.resetCells) {
    brl->data->window.resetCells = 0;
    brl->data->window.firstChangedRow = 0;

    for (unsigned int index=0; index<brl->textRows; index+=1) {
      getRowEntry(brl, index)->hasChanged = 1;
    }

    writeSimpleCommand(brl, CN_CMD_RESET_CELLS);
    return 1;
  }

  while (brl->data->window.firstChangedRow < brl->textRows) {
    RowEntry *row = getRowEntry(brl, brl->data->window.firstChangedRow);

    if (row->hasChanged) {
      unsigned int length = brl->textColumns;
      unsigned char packet[2 + length];
      unsigned char *byte = packet;

      *byte++ = CN_CMD_SEND_ROW;
      *byte++ = brl->data->window.firstChangedRow;
      byte = translateOutputCells(byte, row->cells, length);

      if (writePacket(brl, packet, (byte - packet))) {
        row->hasChanged = 0;
        brl->data->window.lastRowSent = brl->data->window.firstChangedRow++;
      }

      return 1;
    }

    brl->data->window.firstChangedRow += 1;
  }

  return 0;
}

static void
startNextCommand (BrailleDisplay *brl) {
  if (!(brl->data->status.flags & CN_STATUS_MOTORS_ACTIVE)) {
    startUpdate(brl);
  } else if (afterTimePeriod(&brl->data->status.delay, NULL)) {
    startTimePeriod(&brl->data->status.delay, MOTORS_POLL_INTERVAL);
    writeSimpleCommand(brl, CN_CMD_DEVICE_STATUS);
  }
}

static int
brl_readCommand (BrailleDisplay *brl, KeyTableCommandContext context) {
  unsigned char packet[MAXIMUM_RESPONSE_SIZE];
  size_t size;

  while ((size = readPacket(brl, packet, sizeof(packet)))) {
    brl->data->response.waiting = 0;
    brl->writeDelay = 0;

    unsigned char command = packet[0];
    CN_PacketInteger result = CN_getResponseResult(packet);

    unsigned int motorsTime = 0;

    switch (command) {
      case CN_CMD_PRESSED_KEYS:
        enqueueUpdatedKeys(brl, result, &brl->data->keys.pressed, CN_GRP_NavigationKeys, 0);
        startNextCommand(brl);
        continue;

      case CN_CMD_DEVICE_STATUS:
        brl->data->status.flags = result;
        continue;

      case CN_CMD_SEND_ROW:
        motorsTime = ROW_UPDATE_TIME;
        break;

      case CN_CMD_RESET_CELLS:
        motorsTime = CELLS_RESET_TIME;
        break;

      default:
        logUnexpectedPacket(packet, size);
        continue;
    }

    if (result) {
      logMessage(LOG_WARNING,
        "command failed: Cmd:0X%02X Err:0X%02X",
        command, result
      );

      switch (command) {
        case CN_CMD_SEND_ROW:
          resendRow(brl);
          goto UPDATE_FAILED;

        case CN_CMD_RESET_CELLS:
          brl->data->window.resetCells = 1;
          goto UPDATE_FAILED;

        UPDATE_FAILED:
          startTimePeriod(&brl->data->window.retryDelay, UPDATE_RETRY_DELAY);
          continue;

        default:
          continue;;
      }
    } else if (motorsTime) {
      brl->data->status.flags |= CN_STATUS_MOTORS_ACTIVE;
      startTimePeriod(&brl->data->status.delay, motorsTime);
    }
  }

  return (errno == EAGAIN)? EOF: BRL_CMD_RESTARTBRL;
}
