/*
 * 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 "usb_serial.h"
#include "usb_ftdi.h"

static int
usbInputFilter_FTDI (UsbInputFilterData *data) {
  return usbSkipInitialBytes(data, 2);
}

static int
usbSetAttribute_FTDI (UsbDevice *device, unsigned char request, unsigned int value, unsigned int index) {
  logMessage(LOG_CATEGORY(SERIAL_IO), "FTDI request: %02X %04X %04X", request, value, index);
  return usbControlWrite(device, UsbControlRecipient_Device, UsbControlType_Vendor,
                         request, value, index, NULL, 0, 1000) != -1;
}

static int
usbSetBaud_FTDI (UsbDevice *device, unsigned int divisor) {
  return usbSetAttribute_FTDI(device, 3, divisor&0XFFFF, divisor>>0X10);
}

static int
usbSetBaud_FTDI_SIO (UsbDevice *device, unsigned int baud) {
  unsigned int divisor;
  switch (baud) {
    case    300: divisor = 0; break;
    case    600: divisor = 1; break;
    case   1200: divisor = 2; break;
    case   2400: divisor = 3; break;
    case   4800: divisor = 4; break;
    case   9600: divisor = 5; break;
    case  19200: divisor = 6; break;
    case  38400: divisor = 7; break;
    case  57600: divisor = 8; break;
    case 115200: divisor = 9; break;
    default:
      logUnsupportedBaud(baud);
      errno = EINVAL;
      return 0;
  }
  return usbSetBaud_FTDI(device, divisor);
}

static int
usbSetBaud_FTDI_FT8U232AM (UsbDevice *device, unsigned int baud) {
  if (baud > 3000000) {
    logUnsupportedBaud(baud);
    errno = EINVAL;
    return 0;
  }
  {
    const unsigned int base = 48000000;
    unsigned int eighths = base / 2 / baud;
    unsigned int divisor;
    if ((eighths & 07) == 7) eighths++;
    divisor = eighths >> 3;
    divisor |= (eighths & 04)? 0X4000:
               (eighths & 02)? 0X8000:
               (eighths & 01)? 0XC000:
                               0X0000;
    if (divisor == 1) divisor = 0;
    return usbSetBaud_FTDI(device, divisor);
  }
}

static int
usbSetBaud_FTDI_FT232BM (UsbDevice *device, unsigned int baud) {
  if (baud > 3000000) {
    logUnsupportedBaud(baud);
    errno = EINVAL;
    return 0;
  }
  {
    static const unsigned char mask[8] = {00, 03, 02, 04, 01, 05, 06, 07};
    const unsigned int base = 48000000;
    const unsigned int eighths = base / 2 / baud;
    unsigned int divisor = (eighths >> 3) | (mask[eighths & 07] << 14);
    if (divisor == 1) {
      divisor = 0;
    } else if (divisor == 0X4001) {
      divisor = 1;
    }
    return usbSetBaud_FTDI(device, divisor);
  }
}

static int
usbSetFlowControl_FTDI (UsbDevice *device, SerialFlowControl flow) {
  unsigned int index = 0;
#define FTDI_FLOW(from,to) if ((flow & (from)) == (from)) flow &= ~(from), index |= (to)
  FTDI_FLOW(SERIAL_FLOW_OUTPUT_CTS|SERIAL_FLOW_INPUT_RTS, 0X0100);
  FTDI_FLOW(SERIAL_FLOW_OUTPUT_DSR|SERIAL_FLOW_INPUT_DTR, 0X0200);
  FTDI_FLOW(SERIAL_FLOW_OUTPUT_XON|SERIAL_FLOW_INPUT_XON, 0X0400);
#undef FTDI_FLOW
  if (flow) {
    logUnsupportedFlowControl(flow);
  }
  return usbSetAttribute_FTDI(device, 2, ((index & 0X0400)? 0X1311: 0), index);
}

static int
usbSetDataFormat_FTDI (UsbDevice *device, unsigned int dataBits, SerialStopBits stopBits, SerialParity parity) {
  int ok = 1;
  unsigned int value = dataBits & 0XFF;
  if (dataBits != value) {
    logUnsupportedDataBits(dataBits);
    ok = 0;
  }
  switch (parity) {
    case SERIAL_PARITY_NONE:  value |= 0X000; break;
    case SERIAL_PARITY_ODD:   value |= 0X100; break;
    case SERIAL_PARITY_EVEN:  value |= 0X200; break;
    case SERIAL_PARITY_MARK:  value |= 0X300; break;
    case SERIAL_PARITY_SPACE: value |= 0X400; break;
    default:
      logUnsupportedParity(parity);
      ok = 0;
      break;
  }
  switch (stopBits) {
    case SERIAL_STOP_1: value |= 0X0000; break;
    case SERIAL_STOP_2: value |= 0X1000; break;
    default:
      logUnsupportedStopBits(stopBits);
      ok = 0;
      break;
  }
  if (!ok) {
    errno = EINVAL;
    return 0;
  }
  return usbSetAttribute_FTDI(device, 4, value, 0);
}

static int
usbSetModemState_FTDI (UsbDevice *device, int state, int shift, const char *name) {
  if ((state < 0) || (state > 1)) {
    logMessage(LOG_WARNING, "Unsupported FTDI %s state: %d", name, state);
    errno = EINVAL;
    return 0;
  }
  return usbSetAttribute_FTDI(device, 1, ((1 << (shift + 8)) | (state << shift)), 0);
}

static int
usbSetDtrState_FTDI (UsbDevice *device, int state) {
  return usbSetModemState_FTDI(device, state, 0, "DTR");
}

static int
usbSetRtsState_FTDI (UsbDevice *device, int state) {
  return usbSetModemState_FTDI(device, state, 1, "RTS");
}

const UsbSerialOperations usbSerialOperations_FTDI_SIO = {
  .name = "FTDI_SIO",

  .setBaud = usbSetBaud_FTDI_SIO,
  .setDataFormat = usbSetDataFormat_FTDI,
  .setFlowControl = usbSetFlowControl_FTDI,

  .setDtrState = usbSetDtrState_FTDI,
  .setRtsState = usbSetRtsState_FTDI
};

const UsbSerialOperations usbSerialOperations_FTDI_FT8U232AM = {
  .name = "FTDI_FT8U232AM",

  .setBaud = usbSetBaud_FTDI_FT8U232AM,
  .setDataFormat = usbSetDataFormat_FTDI,
  .setFlowControl = usbSetFlowControl_FTDI,

  .setDtrState = usbSetDtrState_FTDI,
  .setRtsState = usbSetRtsState_FTDI,

  .inputFilter = usbInputFilter_FTDI
};

const UsbSerialOperations usbSerialOperations_FTDI_FT232BM = {
  .name = "FTDI_FT232BM",

  .setBaud = usbSetBaud_FTDI_FT232BM,
  .setDataFormat = usbSetDataFormat_FTDI,
  .setFlowControl = usbSetFlowControl_FTDI,

  .setDtrState = usbSetDtrState_FTDI,
  .setRtsState = usbSetRtsState_FTDI,

  .inputFilter = usbInputFilter_FTDI
};
