/*
 * 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-2022 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 "brl_driver.h"
#include "brldefs-hd.h"

#define PROBE_RETRY_LIMIT 2
#define PROBE_INPUT_TIMEOUT 1000

#define MAXIMUM_RESPONSE_SIZE 3
#define MAXIMUM_TEXT_CELL_COUNT 80
#define MAXIMUM_STATUS_CELL_COUNT 4

BEGIN_KEY_NAME_TABLE(pfl)
  KEY_NAME_ENTRY(HD_PFL_K1, "K1"),
  KEY_NAME_ENTRY(HD_PFL_K2, "K2"),
  KEY_NAME_ENTRY(HD_PFL_K3, "K3"),

  KEY_NAME_ENTRY(HD_PFL_B1, "B1"),
  KEY_NAME_ENTRY(HD_PFL_B2, "B2"),
  KEY_NAME_ENTRY(HD_PFL_B3, "B3"),
  KEY_NAME_ENTRY(HD_PFL_B4, "B4"),
  KEY_NAME_ENTRY(HD_PFL_B5, "B5"),
  KEY_NAME_ENTRY(HD_PFL_B6, "B6"),
  KEY_NAME_ENTRY(HD_PFL_B7, "B7"),
  KEY_NAME_ENTRY(HD_PFL_B8, "B8"),
END_KEY_NAME_TABLE

BEGIN_KEY_NAME_TABLE(mbl)
  KEY_NAME_ENTRY(HD_MBL_B1, "B1"),
  KEY_NAME_ENTRY(HD_MBL_B2, "B2"),
  KEY_NAME_ENTRY(HD_MBL_B3, "B3"),

  KEY_NAME_ENTRY(HD_MBL_B4, "B4"),
  KEY_NAME_ENTRY(HD_MBL_B5, "B5"),
  KEY_NAME_ENTRY(HD_MBL_B6, "B6"),

  KEY_NAME_ENTRY(HD_MBL_K1, "K1"),
  KEY_NAME_ENTRY(HD_MBL_K2, "K2"),
  KEY_NAME_ENTRY(HD_MBL_K3, "K3"),
END_KEY_NAME_TABLE

BEGIN_KEY_NAME_TABLE(routing)
  KEY_GROUP_ENTRY(HD_GRP_RoutingKeys, "RoutingKey"),
END_KEY_NAME_TABLE

BEGIN_KEY_NAME_TABLES(pfl)
  KEY_NAME_TABLE(pfl),
  KEY_NAME_TABLE(routing),
END_KEY_NAME_TABLES

BEGIN_KEY_NAME_TABLES(mbl)
  KEY_NAME_TABLE(mbl),
  KEY_NAME_TABLE(routing),
END_KEY_NAME_TABLES

DEFINE_KEY_TABLE(pfl)
DEFINE_KEY_TABLE(mbl)

BEGIN_KEY_TABLE_LIST
  &KEY_TABLE_DEFINITION(pfl),
  &KEY_TABLE_DEFINITION(mbl),
END_KEY_TABLE_LIST

typedef int KeysPacketInterpreter (BrailleDisplay *brl, const unsigned char *packet);

typedef struct {
  const char *modelName;
  const KeyTableDefinition *keyTableDefinition;

  BraillePacketVerifier *verifyPacket;
  KeysPacketInterpreter *interpretKeysPacket;

  unsigned char textCellCount;
  unsigned char statusCellCount;

  unsigned char firstRoutingKey;
  unsigned char acknowledgementResponse;
} ModelEntry;

struct BrailleDataStruct {
  const ModelEntry *model;

  unsigned char forceRewrite;
  unsigned char textCells[MAXIMUM_TEXT_CELL_COUNT];
  unsigned char statusCells[MAXIMUM_STATUS_CELL_COUNT];

  KeyNumberSet navigationKeys;
};

static BraillePacketVerifierResult
verifyPacket_ProfiLine (
  BrailleDisplay *brl,
  unsigned char *bytes, size_t size,
  size_t *length, void *data
) {
  switch (size) {
    case 1:
      *length = 1;
      break;

    default:
      break;
  }

  return BRL_PVR_INCLUDE;
}

static int
interpretKeysPacket_ProfiLine (BrailleDisplay *brl, const unsigned char *packet) {
  const unsigned char code = packet[0];
  const unsigned char release = 0X80;
  const int press = !(code & release);
  unsigned char key = code & ~release;
  KeyGroup group;

  if (key < brl->data->model->firstRoutingKey) {
    group = HD_GRP_NavigationKeys;
  } else if (key < (brl->data->model->firstRoutingKey + brl->textColumns)) {
    group = HD_GRP_RoutingKeys;
    key -= brl->data->model->firstRoutingKey;
  } else {
    return 0;
  }

  enqueueKeyEvent(brl, group, key, press);
  return 1;
}

static const ModelEntry modelEntry_ProfiLine = {
  .modelName = "ProfiLine USB",
  .keyTableDefinition = &KEY_TABLE_DEFINITION(pfl),

  .verifyPacket = verifyPacket_ProfiLine,
  .interpretKeysPacket = interpretKeysPacket_ProfiLine,

  .textCellCount = 80,
  .statusCellCount = 4,

  .firstRoutingKey = 0X20,
  .acknowledgementResponse = 0X7E
};

static BraillePacketVerifierResult
verifyPacket_MobilLine (
  BrailleDisplay *brl,
  unsigned char *bytes, size_t size,
  size_t *length, void *data
) {
  off_t index = size - 1;
  unsigned char byte = bytes[index];

  if ((byte >> 4) == index) {
    if (index == 0) *length = 3;
  } else if (size == 1) {
    *length = 1;
  } else {
    return BRL_PVR_INVALID;
  }

  return BRL_PVR_INCLUDE;
}

static int
interpretKeysPacket_MobilLine (BrailleDisplay *brl, const unsigned char *packet) {
  const unsigned char *byte = packet;

  if (!(*byte >> 4)) {
    const unsigned char *end = packet + 3;
    KeyNumberSet keys = 0;
    unsigned char shift = 0;

    while (byte < end) {
      keys |= (*byte++ & 0XF) << shift;
      shift += 4;
    }

    enqueueUpdatedKeys(brl, keys, &brl->data->navigationKeys,
                       HD_GRP_NavigationKeys, 0);
    return 1;
  }

  if (*byte >= brl->data->model->firstRoutingKey) {
    unsigned char key = *byte - brl->data->model->firstRoutingKey;

    if (key < brl->textColumns) {
      enqueueKey(brl, HD_GRP_RoutingKeys, key);
      return 1;
    }
  }

  return 0;
}

static const ModelEntry modelEntry_MobilLine = {
  .modelName = "MobilLine USB",
  .keyTableDefinition = &KEY_TABLE_DEFINITION(mbl),

  .verifyPacket = verifyPacket_MobilLine,
  .interpretKeysPacket = interpretKeysPacket_MobilLine,

  .textCellCount = 40,
  .statusCellCount = 2,

  .firstRoutingKey = 0X40,
  .acknowledgementResponse = 0X30
};

static size_t
readPacket (BrailleDisplay *brl, void *packet, size_t size) {
  return readBraillePacket(brl, NULL, packet, size, brl->data->model->verifyPacket, NULL);
}

static int
connectResource (BrailleDisplay *brl, const char *identifier) {
  static const SerialParameters serialParameters_ProfiLine = {
    SERIAL_DEFAULT_PARAMETERS,
    .baud = 19200,
    .parity = SERIAL_PARITY_ODD
  };

  static const SerialParameters serialParameters_MobilLine = {
    SERIAL_DEFAULT_PARAMETERS,
    .baud = 9600,
    .parity = SERIAL_PARITY_ODD
  };

  BEGIN_USB_STRING_LIST(usbManufacturers_0403_6001)
    "Hedo Reha Technik GmbH",
  END_USB_STRING_LIST

  BEGIN_USB_CHANNEL_DEFINITIONS
    { /* ProfiLine */
      .vendor=0X0403, .product=0XDE59,
      .configuration=1, .interface=0, .alternative=0,
      .inputEndpoint=1, .outputEndpoint=2,
      .serial = &serialParameters_ProfiLine,
      .data = &modelEntry_ProfiLine
    },

    { /* MobilLine */
      .vendor=0X0403, .product=0XDE58,
      .configuration=1, .interface=0, .alternative=0,
      .inputEndpoint=1, .outputEndpoint=2,
      .serial = &serialParameters_MobilLine,
      .data = &modelEntry_MobilLine
    },

    { /* MobilLine */
      .vendor=0X0403, .product=0X6001,
      .manufacturers = usbManufacturers_0403_6001,
      .configuration=1, .interface=0, .alternative=0,
      .inputEndpoint=1, .outputEndpoint=2,
      .serial = &serialParameters_MobilLine,
      .data = &modelEntry_MobilLine
    },
  END_USB_CHANNEL_DEFINITIONS

  GioDescriptor descriptor;
  gioInitializeDescriptor(&descriptor);

  descriptor.usb.channelDefinitions = usbChannelDefinitions;

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

  return 0;
}

static void
disconnectResource (BrailleDisplay *brl) {
  disconnectBrailleResource(brl, NULL);
}

static int
writeCells (BrailleDisplay *brl, int wait) {
  unsigned char packet[1 + brl->data->model->statusCellCount + brl->data->model->textCellCount];
  unsigned char *byte = packet;

  *byte++ = HD_REQ_WRITE_CELLS;
  byte = mempcpy(byte, brl->data->statusCells, brl->data->model->statusCellCount);
  byte = translateOutputCells(byte, brl->data->textCells, brl->data->model->textCellCount);

  {
    size_t count = byte - packet;

    if (wait) return writeBrailleMessage(brl, NULL, 0, packet, count);
    return writeBraillePacket(brl, NULL, packet, count);
  }
}

static int
writeIdentifyRequest (BrailleDisplay *brl) {
  memset(brl->data->textCells, 0, sizeof(brl->data->textCells));
  memset(brl->data->statusCells, 0, sizeof(brl->data->statusCells));
  return writeCells(brl, 0);
}

static BrailleResponseResult
isIdentityResponse (BrailleDisplay *brl, const void *packet, size_t size) {
  const unsigned char *bytes = packet;

  return (bytes[0] == brl->data->model->acknowledgementResponse)? BRL_RSP_DONE: BRL_RSP_UNEXPECTED;
}

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));

    if (connectResource(brl, device)) {
      unsigned char response[MAXIMUM_RESPONSE_SIZE];

      brl->data->model = gioGetApplicationData(brl->gioEndpoint);
      brl->textColumns = brl->data->model->textCellCount;
      makeOutputTable(dotsTable_ISO11548_1);

      if (probeBrailleDisplay(brl, PROBE_RETRY_LIMIT, NULL, PROBE_INPUT_TIMEOUT,
                              writeIdentifyRequest,
                              readPacket, &response, sizeof(response),
                              isIdentityResponse)) {
        setBrailleKeyTable(brl, brl->data->model->keyTableDefinition);

        brl->data->forceRewrite = 1;
        return 1;
      }

      disconnectResource(brl);
    }

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

  return 0;
}

static void
brl_destruct (BrailleDisplay *brl) {
  disconnectResource(brl);

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

static int
brl_writeWindow (BrailleDisplay *brl, const wchar_t *text) {
  if (cellsHaveChanged(brl->data->textCells, brl->buffer, brl->textColumns, NULL, NULL, &brl->data->forceRewrite)) {
    if (!writeCells(brl, 1)) return 0;
  }

  return 1;
}

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

  while ((size = readPacket(brl, packet, sizeof(packet)))) {
    if (packet[0] == brl->data->model->acknowledgementResponse) {
      acknowledgeBrailleMessage(brl);
    } else if (!brl->data->model->interpretKeysPacket(brl, packet)) {
      logUnexpectedPacket(packet, size);
    }
  }

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