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

#include "prologue.h"

#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>

#include "log.h"
#include "async_wait.h"
#include "timing.h"
#include "io_misc.h"
#include "ports.h"

#include "serial_msdos.h"
#include "serial_internal.h"

#include <dos.h>
#include <dpmi.h>
#include <bios.h>
#include <go32.h>
#include <sys/farptr.h>

#define SERIAL_DIVISOR_BASE 115200
#define SERIAL_DIVISOR(baud) (SERIAL_DIVISOR_BASE / (baud))
#define SERIAL_SPEED(baud) {.divisor=SERIAL_DIVISOR(baud), .bps=SERIAL_BIOS_BAUD_##baud}
#define SERIAL_BAUD_ENTRY(baud) {baud, .speed=SERIAL_SPEED(baud)}

BEGIN_SERIAL_BAUD_TABLE
  SERIAL_BAUD_ENTRY(110),
  SERIAL_BAUD_ENTRY(150),
  SERIAL_BAUD_ENTRY(300),
  SERIAL_BAUD_ENTRY(600),
  SERIAL_BAUD_ENTRY(1200),
  SERIAL_BAUD_ENTRY(2400),
  SERIAL_BAUD_ENTRY(4800),
  SERIAL_BAUD_ENTRY(9600),
  SERIAL_BAUD_ENTRY(19200),
  SERIAL_BAUD_ENTRY(38400),
  SERIAL_BAUD_ENTRY(57600),
  SERIAL_BAUD_ENTRY(115200),
END_SERIAL_BAUD_TABLE

static inline int
serialGetPort (SerialDevice *serial) {
  return serial->package.deviceIndex;
}

static unsigned short
serialPortBase (SerialDevice *serial) {
  return _farpeekw(_dos_ds, (0X0400 + (2 * serialGetPort(serial))));
}

static unsigned char
serialReadPort (SerialDevice *serial, unsigned char port) {
  return readPort1(serialPortBase(serial)+port);
}

static void
serialWritePort (SerialDevice *serial, unsigned char port, unsigned char value) {
  writePort1(serialPortBase(serial)+port, value);
}

static unsigned int
serialBiosCommand (SerialDevice *serial, int command, unsigned char data) {
  return _bios_serialcom(command, serialGetPort(serial), data);
}

static int
serialTestInput (SerialDevice *serial) {
  return !!(serialBiosCommand(serial, _COM_STATUS, 0) & SERIAL_BIOS_STATUS_DATA_READY);
}

void
serialPutInitialAttributes (SerialAttributes *attributes) {
  attributes->speed = serialGetBaudEntry(9600)->speed;
  attributes->bios.fields.bps = attributes->speed.bps;
  attributes->bios.fields.dataBits = SERIAL_BIOS_DATA_8;
  attributes->bios.fields.stopBits = SERIAL_BIOS_STOP_1;
  attributes->bios.fields.parity = SERIAL_BIOS_PARITY_NONE;
}

int
serialPutSpeed (SerialAttributes *attributes, SerialSpeed speed) {
  logMessage(LOG_CATEGORY(SERIAL_IO), "put speed: bps=%u divisor=%u",
             speed.bps, speed.divisor);

  attributes->speed = speed;
  attributes->bios.fields.bps = attributes->speed.bps;
  return 1;
}

int
serialPutDataBits (SerialAttributes *attributes, unsigned int bits) {
  if (bits == 8) {
    attributes->bios.fields.dataBits = SERIAL_BIOS_DATA_8;
  } else if (bits == 7) {
    attributes->bios.fields.dataBits = SERIAL_BIOS_DATA_7;
  } else {
    return 0;
  }

  return 1;
}

int
serialPutStopBits (SerialAttributes *attributes, SerialStopBits bits) {
  if (bits == SERIAL_STOP_1) {
    attributes->bios.fields.stopBits = SERIAL_BIOS_STOP_1;
  } else if (bits == SERIAL_STOP_2) {
    attributes->bios.fields.stopBits = SERIAL_BIOS_STOP_2;
  } else {
    return 0;
  }

  return 1;
}

int
serialPutParity (SerialAttributes *attributes, SerialParity parity) {
  switch (parity) {
    case SERIAL_PARITY_NONE:
      attributes->bios.fields.parity = SERIAL_BIOS_PARITY_NONE;
      break;

    case SERIAL_PARITY_ODD:
      attributes->bios.fields.parity = SERIAL_BIOS_PARITY_ODD;
      break;

    case SERIAL_PARITY_EVEN:
      attributes->bios.fields.parity = SERIAL_BIOS_PARITY_EVEN;
      break;

    default:
      return 0;
  }

  return 1;
}

SerialFlowControl
serialPutFlowControl (SerialAttributes *attributes, SerialFlowControl flow) {
  return flow;
}

int
serialPutModemState (SerialAttributes *attributes, int enabled) {
  return !enabled;
}

unsigned int
serialGetDataBits (const SerialAttributes *attributes) {
  switch (attributes->bios.fields.dataBits) {
    default:
    case SERIAL_BIOS_DATA_8: return 8;
    case SERIAL_BIOS_DATA_7: return 7;
  }
}

unsigned int
serialGetStopBits (const SerialAttributes *attributes) {
  switch (attributes->bios.fields.stopBits) {
    default:
    case SERIAL_BIOS_STOP_1: return 1;
    case SERIAL_BIOS_STOP_2: return 2;
  }
}

unsigned int
serialGetParityBits (const SerialAttributes *attributes) {
  return (attributes->bios.fields.parity == SERIAL_BIOS_PARITY_NONE)? 0: 1;
}

int
serialGetAttributes (SerialDevice *serial, SerialAttributes *attributes) {
  unsigned char lcr;
  int divisor;

  {
    int wasEnabled = disable();

    lcr = serialReadPort(serial, UART_PORT_LCR);
    serialWritePort(serial, UART_PORT_LCR, (lcr | UART_FLAG_LCR_DLAB));

    divisor = (serialReadPort(serial, UART_PORT_DLH) << 8) |
               serialReadPort(serial, UART_PORT_DLL);
    serialWritePort(serial, UART_PORT_LCR, lcr);

    if (wasEnabled) enable();
  }

  attributes->bios.byte = lcr;

  {
    const SerialBaudEntry *baud = serialGetBaudEntry(SERIAL_DIVISOR_BASE/divisor);

    if (baud) {
      attributes->speed = baud->speed;
    } else {
      logMessage(LOG_WARNING, "unsupported serial divisor: %d", divisor);
      memset(&attributes->speed, 0, sizeof(attributes->speed));
    }
  }

  attributes->bios.fields.bps = attributes->speed.bps;
  return 1;
}

int
serialPutAttributes (SerialDevice *serial, const SerialAttributes *attributes) {
  if (attributes->speed.bps < (0X1 << 3)) {
    unsigned char byte = attributes->bios.byte;

    logMessage(LOG_CATEGORY(SERIAL_IO), "put attributes: port=%d byte=0X%02X",
               serialGetPort(serial), byte);
    serialBiosCommand(serial, _COM_INIT, byte);
  } else {
    SerialBiosConfiguration lcr = attributes->bios;

    lcr.fields.bps = 0;
    logMessage(LOG_CATEGORY(SERIAL_IO), "put attributes: port=%d lcr=0X%02X divisor=%u",
               serialGetPort(serial), lcr.byte, attributes->speed.divisor);

    {
      int wasEnabled = disable();

      serialWritePort(serial, UART_PORT_LCR, (lcr.byte | UART_FLAG_LCR_DLAB));
      serialWritePort(serial, UART_PORT_DLL, (attributes->speed.divisor & 0XFF));
      serialWritePort(serial, UART_PORT_DLH, (attributes->speed.divisor >> 8));
      serialWritePort(serial, UART_PORT_LCR, lcr.byte);

      if (wasEnabled) enable();
    }
  }

  return 1;
}

int
serialCancelInput (SerialDevice *serial) {
  return 1;
}

int
serialCancelOutput (SerialDevice *serial) {
  return 1;
}

int
serialMonitorInput (SerialDevice *serial, AsyncMonitorCallback *callback, void *data) {
  return 0;
}

int
serialPollInput (SerialDevice *serial, int timeout) {
  TimePeriod period;

  if (timeout) startTimePeriod(&period, timeout);

  while (1) {
    if (serialTestInput(serial)) return 1;
    if (!timeout) break;
    if (afterTimePeriod(&period, NULL)) break;
    asyncWait(1);
  }

  errno = EAGAIN;
  return 0;
}

int
serialDrainOutput (SerialDevice *serial) {
  return 1;
}

ssize_t
serialGetData (
  SerialDevice *serial,
  void *buffer, size_t size,
  int initialTimeout, int subsequentTimeout
) {
  unsigned char *const start = buffer;
  unsigned char *const end = start + size;
  unsigned char *byte = start;
  int timeout = initialTimeout;

  while (byte < end) {
    if (!serialPollInput(serial, timeout)) break;
    timeout = subsequentTimeout;

    {
      int status = serialBiosCommand(serial, _COM_RECEIVE, 0);

      *byte++ = status & 0XFF;
    }
  }

  return byte - start;
}

ssize_t
serialPutData (
  SerialDevice *serial,
  const void *data, size_t size
) {
  return writeFile(serial->fileDescriptor, data, size);
}

int
serialGetLines (SerialDevice *serial) {
  serial->linesState = serialReadPort(serial, UART_PORT_MSR) & 0XF0;
  return 1;
}

int
serialPutLines (SerialDevice *serial, SerialLines high, SerialLines low) {
  int wasEnabled = disable();
  unsigned char oldMCR = serialReadPort(serial, UART_PORT_MCR);

  serialWritePort(serial, UART_PORT_MCR,
                  (oldMCR | high) & ~low);
  if (wasEnabled) enable();
  return 1;
}

int
serialRegisterWaitLines (SerialDevice *serial, SerialLines lines) {
  return 1;
}

int
serialMonitorWaitLines (SerialDevice *serial) {
  return 0;
}

int
serialConnectDevice (SerialDevice *serial, const char *device) {
  if ((serial->fileDescriptor = open(device, O_RDWR|O_NOCTTY|O_NONBLOCK)) != -1) {
    serial->package.deviceIndex = -1;

    {
      char *truePath;

      if ((truePath = _truename(device, NULL))) {
        char *com;

        {
          char *c = truePath;

          while (*c) {
            *c = toupper(*c);
            c += 1;
          }
        }

        if ((com = strstr(truePath, "COM"))) {
          serial->package.deviceIndex = atoi(com+3) - 1;
        }

        free(truePath);
      }
    }

    if (serial->package.deviceIndex >= 0) {
      if (serialPrepareDevice(serial)) {
        logMessage(LOG_CATEGORY(SERIAL_IO), "device opened: %s: fd=%d",
                   device, serial->fileDescriptor);
        return 1;
      }
    } else {
      logMessage(LOG_ERR, "could not determine serial device number: %s", device);
    }

    close(serial->fileDescriptor);
  } else {
    logMessage(LOG_ERR, "cannot open serial device: %s: %s", device, strerror(errno));
  }

  return 0;
}

void
serialDisconnectDevice (SerialDevice *serial) {
}

int
serialEnsureFileDescriptor (SerialDevice *serial) {
  return 1;
}

void
serialClearError (SerialDevice *serial) {
}

