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

#include "log.h"

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

#define PROBE_RETRY_LIMIT 2
#define PROBE_INPUT_TIMEOUT 1000
#define START_INPUT_TIMEOUT 1000

#define MM_KEY_GROUP_ENTRY(s,n) BRL_KEY_GROUP_ENTRY(MM, s, n)
#define MM_KEY_NAME_ENTRY(s,k,n) BRL_KEY_NAME_ENTRY(MM, s, k, n)

#define MM_SHIFT_KEY_ENTRY(k,n) MM_KEY_NAME_ENTRY(SHIFT, k, n)
#define MM_DOT_KEY_ENTRY(k) MM_KEY_NAME_ENTRY(DOT, k, "dot" #k)
#define MM_EDIT_KEY_ENTRY(k,n) MM_KEY_NAME_ENTRY(EDIT, k, n)
#define MM_ARROW_KEY_ENTRY(k,n) MM_KEY_NAME_ENTRY(ARROW, k, n)
#define MM_DISPLAY_KEY_ENTRY(k,n) MM_KEY_NAME_ENTRY(DISPLAY, k, n)

BEGIN_KEY_NAME_TABLE(shift)
  MM_SHIFT_KEY_ENTRY(F1, "PanLeft"),
  MM_SHIFT_KEY_ENTRY(F3, "Extension"),
  MM_SHIFT_KEY_ENTRY(F4, "PanRight"),

  MM_SHIFT_KEY_ENTRY(F1, "F1"),
  MM_SHIFT_KEY_ENTRY(F2, "F2"),
  MM_SHIFT_KEY_ENTRY(F3, "F3"),
  MM_SHIFT_KEY_ENTRY(F4, "F4"),

  MM_SHIFT_KEY_ENTRY(CONTROL, "Control"),
  MM_SHIFT_KEY_ENTRY(ALT, "Alt"),
  MM_SHIFT_KEY_ENTRY(SELECT, "Select"),
  MM_SHIFT_KEY_ENTRY(READ, "Read"),
END_KEY_NAME_TABLE

BEGIN_KEY_NAME_TABLE(dot)
  MM_DOT_KEY_ENTRY(1),
  MM_DOT_KEY_ENTRY(2),
  MM_DOT_KEY_ENTRY(3),
  MM_DOT_KEY_ENTRY(4),
  MM_DOT_KEY_ENTRY(5),
  MM_DOT_KEY_ENTRY(6),
  MM_DOT_KEY_ENTRY(7),
  MM_DOT_KEY_ENTRY(8),
END_KEY_NAME_TABLE

BEGIN_KEY_NAME_TABLE(edit)
  MM_EDIT_KEY_ENTRY(ESC, "Escape"),
  MM_EDIT_KEY_ENTRY(INF, "Info"),

  MM_EDIT_KEY_ENTRY(BS, "Backspace"),
  MM_EDIT_KEY_ENTRY(DEL, "Delete"),
  MM_EDIT_KEY_ENTRY(INS, "Insert"),

  MM_EDIT_KEY_ENTRY(CHANGE, "Change"),
  MM_EDIT_KEY_ENTRY(OK, "OK"),
  MM_EDIT_KEY_ENTRY(SET, "Set"),
END_KEY_NAME_TABLE

BEGIN_KEY_NAME_TABLE(arrow)
  MM_ARROW_KEY_ENTRY(UP, "ArrowUp"),
  MM_ARROW_KEY_ENTRY(DOWN, "ArrowDown"),
  MM_ARROW_KEY_ENTRY(LEFT, "ArrowLeft"),
  MM_ARROW_KEY_ENTRY(RIGHT, "ArrowRight"),
END_KEY_NAME_TABLE

BEGIN_KEY_NAME_TABLE(route)
  MM_KEY_GROUP_ENTRY(ROUTE, "RoutingKey"),
END_KEY_NAME_TABLE

BEGIN_KEY_NAME_TABLE(display)
  MM_DISPLAY_KEY_ENTRY(BACKWARD, "Backward"),
  MM_DISPLAY_KEY_ENTRY(FORWARD, "Forward"),

  MM_DISPLAY_KEY_ENTRY(LSCROLL, "ScrollLeft"),
  MM_DISPLAY_KEY_ENTRY(RSCROLL, "ScrollRight"),
END_KEY_NAME_TABLE

BEGIN_KEY_NAME_TABLES(pocket)
  KEY_NAME_TABLE(shift),
  KEY_NAME_TABLE(dot),
  KEY_NAME_TABLE(edit),
  KEY_NAME_TABLE(arrow),
  KEY_NAME_TABLE(route),
  KEY_NAME_TABLE(display),
END_KEY_NAME_TABLES

BEGIN_KEY_NAME_TABLES(smart)
  KEY_NAME_TABLE(shift),
  KEY_NAME_TABLE(dot),
  KEY_NAME_TABLE(edit),
  KEY_NAME_TABLE(arrow),
  KEY_NAME_TABLE(route),
  KEY_NAME_TABLE(display),
END_KEY_NAME_TABLES

BEGIN_KEY_NAME_TABLES(nt)
  KEY_NAME_TABLE(shift),
  KEY_NAME_TABLE(arrow),
  KEY_NAME_TABLE(route),
  KEY_NAME_TABLE(display),
END_KEY_NAME_TABLES

DEFINE_KEY_TABLE(pocket)
DEFINE_KEY_TABLE(smart)
DEFINE_KEY_TABLE(nt)

BEGIN_KEY_TABLE_LIST
  &KEY_TABLE_DEFINITION(pocket),
  &KEY_TABLE_DEFINITION(smart),
  &KEY_TABLE_DEFINITION(nt),
END_KEY_TABLE_LIST

typedef struct {
  const char *identityPrefix;
  const char *modelName;
  const KeyTableDefinition *keyTableDefinition;
} ModelEntry;

static const ModelEntry modelEntry_pocket = {
  .identityPrefix = "BMpk",
  .modelName = "Braille Memo Pocket",
  .keyTableDefinition = &KEY_TABLE_DEFINITION(pocket)
};

static const ModelEntry modelEntry_smart = {
  .identityPrefix = "BMsmart",
  .modelName = "Braille Memo Smart",
  .keyTableDefinition = &KEY_TABLE_DEFINITION(smart)
};

static const ModelEntry modelEntry_nextTouch = {
  .identityPrefix = "BM_disp",
  .modelName = "Next Touch",
  .keyTableDefinition = &KEY_TABLE_DEFINITION(nt)
};

static const ModelEntry *const modelEntries[] = {
  &modelEntry_pocket,
  &modelEntry_smart,
  &modelEntry_nextTouch,
  NULL
};

struct BrailleDataStruct {
  const ModelEntry *model;

  unsigned char forceRewrite;
  unsigned char textCells[MM_MAXIMUM_LINE_LENGTH];
};

static const unsigned char sizeTable[] = {16, 24, 32, 40, 46};
static const unsigned char sizeCount = ARRAY_COUNT(sizeTable);

static int
isValidSize (unsigned char size) {
  return memchr(sizeTable, size, sizeCount) != NULL;
}

static int
writeBytes (BrailleDisplay *brl, const unsigned char *bytes, size_t count) {
  return writeBraillePacket(brl, NULL, bytes, count);
}

static int
writePacket (
  BrailleDisplay *brl,
  unsigned char code, unsigned char subcode,
  const unsigned char *data, size_t length
) {
  unsigned char bytes[sizeof(MM_CommandHeader) + length];
  unsigned char *byte = bytes;

  *byte++ = MM_HEADER_ID1;
  *byte++ = MM_HEADER_ID2;

  *byte++ = code;
  *byte++ = subcode;

  *byte++ = (length >> 0) & 0XFF;
  *byte++ = (length >> 8) & 0XFF;

  if (data) byte = mempcpy(byte, data, length);

  return writeBytes(brl, bytes, byte-bytes);
}

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

  switch (size) {
    case 1:
      switch (byte) {
        case MM_HEADER_ACK:
        case MM_HEADER_NAK:
          *length = 1;
          break;

        case MM_HEADER_ID1:
          *length = sizeof(MM_CommandHeader);
          break;

        default:
          if (isValidSize(byte)) {
            *length = 1;
            break;
          }

          return BRL_PVR_INVALID;
      }
      break;

    case 2:
      if (byte != MM_HEADER_ID2) return BRL_PVR_INVALID;
      break;

    case 5:
      *length += byte;
      break;

    case 6:
      *length += byte << 8;
      break;

    default:
      break;
  }

  return BRL_PVR_INCLUDE;
}

static size_t
readBytes (BrailleDisplay *brl, void *packet, size_t size) {
  return readBraillePacket(brl, NULL, packet, size, verifyPacket, NULL);
}

static size_t
readPacket (BrailleDisplay *brl, MM_CommandPacket *packet) {
  return readBytes(brl, packet, sizeof(*packet));
}

static int
startDisplayMode (BrailleDisplay *brl) {
  static const unsigned char data[] = {MM_BLINK_NO, 0};

  if (writePacket(brl, MM_CMD_StartDisplayMode, 0, data, sizeof(data))) {
    if (awaitBrailleInput(brl, START_INPUT_TIMEOUT)) {
      MM_CommandPacket response;
      size_t size = readPacket(brl, &response);

      if (size) {
        if (response.fields.header.id1 == MM_HEADER_ACK) return 1;
        logUnexpectedPacket(response.bytes, size);
      }
    }
  }

  return 0;
}

static int
endDisplayMode (BrailleDisplay *brl) {
  return writePacket(brl, MM_CMD_EndDisplayMode, 0, NULL, 0);
}

static int
sendBrailleData (BrailleDisplay *brl, const unsigned char *cells, size_t count) {
  return writePacket(brl, MM_CMD_SendBrailleData, 0, cells, count);
}

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

  BEGIN_USB_STRING_LIST(usbManufacturers_10C4_EA60)
    "Silicon Labs",
  END_USB_STRING_LIST

  BEGIN_USB_CHANNEL_DEFINITIONS
    { /* Pocket */
      .vendor=0X10C4, .product=0XEA60,
      .manufacturers = usbManufacturers_10C4_EA60,
      .configuration=1, .interface=0, .alternative=0,
      .inputEndpoint=1, .outputEndpoint=1,
      .verifyInterface=1,
      .serial=&serialParameters
    },

    { /* Next Touch 40 */
      .vendor=0X10C4, .product=0XEA60,
      .manufacturers = usbManufacturers_10C4_EA60,
      .configuration=1, .interface=0, .alternative=0,
      .inputEndpoint=2, .outputEndpoint=2,
      .verifyInterface=1,
      .serial=&serialParameters
    },

    { /* Smart */
      .vendor=0X1148, .product=0X0301,
      .configuration=1, .interface=1, .alternative=0,
      .inputEndpoint=3, .outputEndpoint=2,
      .serial=&serialParameters
    },
  END_USB_CHANNEL_DEFINITIONS

  GioDescriptor descriptor;
  gioInitializeDescriptor(&descriptor);

  descriptor.serial.parameters = &serialParameters;

  descriptor.usb.channelDefinitions = usbChannelDefinitions;

  descriptor.bluetooth.channelNumber = 1;
  descriptor.bluetooth.discoverChannel = 1;

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

  return 0;
}

static int
detectModel (BrailleDisplay *brl, const MM_IdentityPacket *identity) {
  const ModelEntry *const *model = modelEntries;

  while (*model) {
    const char *prefix = (*model)->identityPrefix;

    if (strncmp(identity->hardwareName, prefix, strlen(prefix)) == 0) {
      brl->data->model = *model;
      logMessage(LOG_INFO, "detected model: %s", brl->data->model->modelName);
      return 1;
    }

    model += 1;
  }

  logMessage(LOG_WARNING, "unrecognized model: %s", identity->hardwareName);
  brl->data->model = &modelEntry_pocket;
  logMessage(LOG_INFO, "assumed model: %s", brl->data->model->modelName);
  return 0;
}

static int
writeIdentifyRequest (BrailleDisplay *brl) {
  return writePacket(brl, MM_CMD_QueryIdentity, 0, NULL, 0);
}

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

  switch (size) {
    case 1:
      switch (byte) {
        case 0X01: // sent by the Pocket and Smart models
        case 0X02: // sent by the Next Touch models
          *length = sizeof(MM_IdentityPacket);
          break;

        default:
          return BRL_PVR_INVALID;
      }
      break;

    default:
      break;
  }

  return BRL_PVR_INCLUDE;
}

static size_t
readIdentityResponse (BrailleDisplay *brl, void *packet, size_t size) {
  return readBraillePacket(brl, NULL, packet, size, verifyIdentityResponse, NULL);
}

static BrailleResponseResult
isIdentityResponse (BrailleDisplay *brl, const void *packet, size_t size) {
  const MM_IdentityPacket *identity = packet;

  if ((identity->lineLength == 0) || (identity->lineLength > MM_MAXIMUM_LINE_LENGTH)) return BRL_RSP_UNEXPECTED;
  if ((identity->lineCount == 0) || (identity->lineCount > MM_MAXIMUM_LINE_COUNT)) return BRL_RSP_UNEXPECTED;

  {
    const char *byte = identity->hardwareName;
    const char *end = byte + sizeof(identity->hardwareName);

    while (byte < end) {
      if (!*byte) break;
      if (!iswprint(*byte)) return BRL_RSP_UNEXPECTED;
      byte += 1;
    }
  }

  return BRL_RSP_DONE;
}

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)) {
      MM_IdentityPacket identity;

      if (probeBrailleDisplay(brl, PROBE_RETRY_LIMIT, NULL, PROBE_INPUT_TIMEOUT,
                              writeIdentifyRequest,
                              readIdentityResponse, &identity, sizeof(identity),
                              isIdentityResponse)) {
        detectModel(brl, &identity);
        brl->textColumns = identity.lineLength;

        if (startDisplayMode(brl)) {
          setBrailleKeyTable(brl, brl->data->model->keyTableDefinition);
          MAKE_OUTPUT_TABLE(0X80, 0X40, 0X20, 0X08, 0X04, 0X02, 0X10, 0X01);

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

      disconnectBrailleResource(brl, NULL);
    }

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

  return 0;
}

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

  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)) {
    unsigned char cells[brl->textColumns];

    translateOutputCells(cells, brl->data->textCells, brl->textColumns);
    if (!sendBrailleData(brl, cells, sizeof(cells))) return 0;
  }

  return 1;
}

static int
brl_readCommand (BrailleDisplay *brl, KeyTableCommandContext context) {
  MM_CommandPacket packet;
  size_t size;

  while ((size = readPacket(brl, &packet))) {
    if ((packet.fields.header.id1 == MM_HEADER_ID1) &&
        (packet.fields.header.id2 == MM_HEADER_ID2)) {
      switch (packet.fields.header.code) {
        case MM_CMD_KeyCombination:
          switch (packet.fields.data.keys.group) {
            case MM_GRP_SHIFT:
              if (!packet.fields.data.keys.value) {
                enqueueKeys(brl, packet.fields.data.keys.shift, MM_GRP_SHIFT, 0);
                continue;
              }
              break;

            case MM_GRP_DOT:
            case MM_GRP_EDIT:
            case MM_GRP_ARROW:
            case MM_GRP_DISPLAY:
            {
              KeyNumberSet shift = 0;

              enqueueUpdatedKeys(brl, packet.fields.data.keys.shift, &shift, MM_GRP_SHIFT, 0);
              enqueueKeys(brl, packet.fields.data.keys.value, packet.fields.data.keys.group, 0);
              enqueueUpdatedKeys(brl, 0, &shift, MM_GRP_SHIFT, 0);
              continue;
            }

            case MM_GRP_ROUTE:
            {
              unsigned char key = packet.fields.data.keys.value;

              if ((key > 0) && (key <= brl->textColumns)) {
                KeyNumberSet shift = 0;

                enqueueUpdatedKeys(brl, packet.fields.data.keys.shift, &shift, MM_GRP_SHIFT, 0);
                enqueueKey(brl, packet.fields.data.keys.group, key-1);
                enqueueUpdatedKeys(brl, 0, &shift, MM_GRP_SHIFT, 0);
                continue;
              }

              break;
            }

            default:
              break;
          }
          break;

        case MM_CMD_ShiftPress:
        case MM_CMD_ShiftRelease:
          continue;

        default:
          break;
      }
    }

    logUnexpectedPacket(packet.bytes, size);
  }

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