/*
 * 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 "async_handle.h"
#include "async_wait.h"
#include "async_alarm.h"
#include "io_generic.h"
#include "gio_internal.h"
#include "io_serial.h"

const GioProperties *const gioProperties[] = {
  &gioProperties_serial,
  &gioProperties_usb,
  &gioProperties_bluetooth,
  &gioProperties_hid,
  &gioProperties_null,
  NULL
};

static void
gioInitializeOptions (GioOptions *options) {
  options->applicationData = NULL;
  options->readyDelay = 0;
  options->inputTimeout = 0;
  options->outputTimeout = 0;
  options->requestTimeout = 0;
}

void
gioInitializeDescriptor (GioDescriptor *descriptor) {
  descriptor->serial.parameters = NULL;
  gioInitializeOptions(&descriptor->serial.options);
  descriptor->serial.options.inputTimeout = 100;

  descriptor->usb.channelDefinitions = NULL;
  descriptor->usb.setConnectionProperties = NULL;
  gioInitializeOptions(&descriptor->usb.options);
  descriptor->usb.options.inputTimeout = 1000;
  descriptor->usb.options.outputTimeout = 1000;
  descriptor->usb.options.requestTimeout = 1000;

  descriptor->bluetooth.channelNumber = 0;
  descriptor->bluetooth.discoverChannel = 0;
  gioInitializeOptions(&descriptor->bluetooth.options);
  descriptor->bluetooth.options.inputTimeout = 1000;
  descriptor->bluetooth.options.requestTimeout = 5000;

  gioInitializeOptions(&descriptor->hid.options);

  gioInitializeOptions(&descriptor->null.options);
}

void
gioInitializeSerialParameters (SerialParameters *parameters) {
  parameters->baud = SERIAL_DEFAULT_BAUD;
  parameters->dataBits = SERIAL_DEFAULT_DATA_BITS;
  parameters->stopBits = SERIAL_DEFAULT_STOP_BITS;
  parameters->parity = SERIAL_DEFAULT_PARITY;
  parameters->flowControl = SERIAL_DEFAULT_FLOW_CONTROL;
}

void
gioSetBytesPerSecond (GioEndpoint *endpoint, const SerialParameters *parameters) {
  endpoint->bytesPerSecond = parameters->baud / serialGetCharacterSize(parameters);
}

void
gioSetApplicationData (GioEndpoint *endpoint, const void *data) {
  endpoint->options.applicationData = data;
}

static int
gioStartEndpoint (GioEndpoint *endpoint) {
  {
    int delay = endpoint->options.readyDelay;
    if (delay) asyncWait(delay);
  }

  return 1;
}

static const GioProperties *
gioGetProperties (
  const char **identifier,
  const GioDescriptor *descriptor
) {
  for (const GioProperties *const *properties = gioProperties;
       *properties; properties+=1) {
    if (descriptor) {
      GioIsSupportedMethod *isSupported = (*properties)->private->isSupported;
      if (!isSupported) continue;
      if (!isSupported(descriptor)) continue;
    }

    {
      GioTestIdentifierMethod *testIdentifier = (*properties)->public->testIdentifier;
      if (!testIdentifier) continue;
      if (testIdentifier(identifier)) return *properties;
    }
  }

  errno = ENOSYS;
  logMessage(LOG_WARNING, "unsupported generic resource identifier: %s", *identifier);
  return NULL;
}

const GioPublicProperties *
gioGetPublicProperties (const char **identifier) {
  const GioProperties *properties = gioGetProperties(identifier, NULL);
  if (properties == NULL) return NULL;
  return properties->public;
}

GioEndpoint *
gioConnectResource (
  const char *identifier,
  const GioDescriptor *descriptor
) {
  const GioProperties *properties = gioGetProperties(&identifier, descriptor);

  if (properties) {
    GioEndpoint *endpoint;

    if ((endpoint = malloc(sizeof(*endpoint)))) {
      memset(endpoint, 0, sizeof(*endpoint));
      endpoint->referenceCount = 1;

      endpoint->resourceType = properties->public->type.identifier;
      endpoint->bytesPerSecond = 0;

      endpoint->input.error = 0;
      endpoint->input.from = 0;
      endpoint->input.to = 0;

      if (descriptor && properties->private->getOptions) {
        endpoint->options = *properties->private->getOptions(descriptor);
      } else {
        gioInitializeOptions(&endpoint->options);
      }

      if (properties->private->getHandleMethods) {
        endpoint->handleMethods = properties->private->getHandleMethods();
      } else {
        endpoint->handleMethods = NULL;
      }

      if (properties->private->connectResource) {
        if ((endpoint->handle = properties->private->connectResource(identifier, descriptor))) {
          {
            GioGetChainedEndpointMethod *getChainedEndpoint = endpoint->handleMethods->getChainedEndpoint;

            if (getChainedEndpoint) {
              GioEndpoint *chainedEndpoint = getChainedEndpoint(endpoint->handle);

              if (chainedEndpoint) {
                chainedEndpoint->referenceCount += 1;
                gioDisconnectResource(endpoint);
                return chainedEndpoint;
              }
            }
          }

          if (!properties->private->prepareEndpoint || properties->private->prepareEndpoint(endpoint)) {
            if (gioStartEndpoint(endpoint)) {
              return endpoint;
            }
          }

          {
            int originalErrno = errno;
            gioDisconnectResource(endpoint);
            errno = originalErrno;
          }

          return NULL;
        }
      } else {
        logUnsupportedOperation("connectResource");
      }

      free(endpoint);
    } else {
      logMallocError();
    }
  }

  return NULL;
}

const void *
gioGetApplicationData (GioEndpoint *endpoint) {
  return endpoint->options.applicationData;
}

int
gioDisconnectResource (GioEndpoint *endpoint) {
  if (--endpoint->referenceCount > 0) return 1;

  int ok = 0;
  GioDisconnectResourceMethod *method = endpoint->handleMethods->disconnectResource;

  if (!method) {
    logUnsupportedOperation("disconnectResource");
    errno = ENOSYS;
  } else if (method(endpoint->handle)) {
    ok = 1;
  }

  free(endpoint);
  return ok;
}

const char *
gioMakeResourceIdentifier (GioEndpoint *endpoint, char *buffer, size_t size) {
  const char *identifier = NULL;
  MakeResourceIdentifierMethod *method = endpoint->handleMethods->makeResourceIdentifier;

  if (!method) {
    logUnsupportedOperation("makeResourceIdentifier");
    errno = ENOSYS;
  } else {
    identifier = method(endpoint->handle, buffer, size);
  }

  return identifier;
}

char *
gioGetResourceIdentifier (GioEndpoint *endpoint) {
  char buffer[0X100];
  const char *identifier = gioMakeResourceIdentifier(endpoint, buffer, sizeof(buffer));
  if (!identifier) return NULL;

  char *copy = strdup(identifier);
  if (!copy) logMallocError();
  return copy;
}

char *
gioGetResourceName (GioEndpoint *endpoint) {
  char *name = NULL;
  GioGetResourceNameMethod *method = endpoint->handleMethods->getResourceName;

  if (!method) {
    logUnsupportedOperation("getResourceName");
    errno = ENOSYS;
  } else {
    name = method(endpoint->handle, endpoint->options.requestTimeout);
  }

  return name;
}

GioTypeIdentifier
gioGetResourceType (GioEndpoint *endpoint) {
  return endpoint->resourceType;
}

void *
gioGetResourceObject (GioEndpoint *endpoint) {
  GioGetResourceObjectMethod *method = endpoint->handleMethods->getResourceObject;

  if (method) return method(endpoint->handle);
  logUnsupportedOperation("getResourceObject");
  return NULL;
}

ssize_t
gioWriteData (GioEndpoint *endpoint, const void *data, size_t size) {
  GioWriteDataMethod *method = endpoint->handleMethods->writeData;

  if (!method) {
    logUnsupportedOperation("writeData");
    errno = ENOSYS;
    return -1;
  }

  logBytes(LOG_CATEGORY(GENERIC_IO), "output", data, size);

  ssize_t result = method(endpoint->handle, data, size,
                          endpoint->options.outputTimeout);

  if (endpoint->options.ignoreWriteTimeouts) {
    if (result == -1) {
      if ((errno == EAGAIN)
#ifdef ETIMEDOUT
       || (errno == ETIMEDOUT)
#endif /* ETIMEDOUT */
      ) result = size;
    }
  }

  return result;
}

int
gioAwaitInput (GioEndpoint *endpoint, int timeout) {
  GioAwaitInputMethod *method = endpoint->handleMethods->awaitInput;

  if (!method) {
    logUnsupportedOperation("awaitInput");
    errno = ENOSYS;
    return 0;
  }

  if (endpoint->input.to - endpoint->input.from) return 1;

  return method(endpoint->handle, timeout);
}

ssize_t
gioReadData (GioEndpoint *endpoint, void *buffer, size_t size, int wait) {
  GioReadDataMethod *method = endpoint->handleMethods->readData;

  if (!method) {
    logUnsupportedOperation("readData");
    errno = ENOSYS;
    return -1;
  }

  {
    unsigned char *start = buffer;
    unsigned char *next = start;

    while (size) {
      {
        unsigned int count = endpoint->input.to - endpoint->input.from;

        if (count) {
          if (count > size) count = size;
          memcpy(next, &endpoint->input.buffer[endpoint->input.from], count);

          endpoint->input.from += count;
          next += count;
          size -= count;
          continue;
        }

        endpoint->input.from = endpoint->input.to = 0;
      }

      if (endpoint->input.error) {
        if (next != start) break;
        errno = endpoint->input.error;
        endpoint->input.error = 0;
        return -1;
      }

      {
        ssize_t result = method(endpoint->handle,
                                &endpoint->input.buffer[endpoint->input.to],
                                sizeof(endpoint->input.buffer) - endpoint->input.to,
                                (wait? endpoint->options.inputTimeout: 0), 0);

        if (result > 0) {
          logBytes(LOG_CATEGORY(GENERIC_IO), "input", &endpoint->input.buffer[endpoint->input.to], result);
          endpoint->input.to += result;
          wait = 1;
        } else {
          if (!result) break;
          if (errno == EAGAIN) break;
          endpoint->input.error = errno;
        }
      }
    }

    if (next == start) errno = EAGAIN;
    return next - start;
  }
}

int
gioReadByte (GioEndpoint *endpoint, unsigned char *byte, int wait) {
  ssize_t result = gioReadData(endpoint, byte, 1, wait);
  if (result > 0) return 1;
  if (result == 0) errno = EAGAIN;
  return 0;
}

int
gioDiscardInput (GioEndpoint *endpoint) {
  unsigned char byte;
  while (gioReadByte(endpoint, &byte, 0));
  return errno == EAGAIN;
}

int
gioMonitorInput (GioEndpoint *endpoint, AsyncMonitorCallback *callback, void *data) {
  GioMonitorInputMethod *method = endpoint->handleMethods->monitorInput;

  if (method) {
    if (method(endpoint->handle, callback, data)) {
      return 1;
    }
  }

  return 0;
}

int
gioReconfigureResource (
  GioEndpoint *endpoint,
  const SerialParameters *parameters
) {
  int ok = 1;
  GioReconfigureResourceMethod *method = endpoint->handleMethods->reconfigureResource;

  if (!method) {
    logUnsupportedOperation("reconfigureResource");
  } else if (method(endpoint->handle, parameters)) {
    gioSetBytesPerSecond(endpoint, parameters);
  } else {
    ok = 0;
  }

  return ok;
}

unsigned int
gioGetBytesPerSecond (GioEndpoint *endpoint) {
  return endpoint->bytesPerSecond;
}

unsigned int
gioGetMillisecondsToTransfer (GioEndpoint *endpoint, size_t bytes) {
  return endpoint->bytesPerSecond? (((bytes * 1000) / endpoint->bytesPerSecond) + 1): 0;
}

ssize_t
gioTellResource (
  GioEndpoint *endpoint,
  uint8_t recipient, uint8_t type,
  uint8_t request, uint16_t value, uint16_t index,
  const void *data, uint16_t size
) {
  GioTellResourceMethod *method = endpoint->handleMethods->tellResource;

  if (!method) {
    logUnsupportedOperation("tellResource");
    errno = ENOSYS;
    return -1;
  }

  return method(endpoint->handle, recipient, type,
                request, value, index, data, size,
                endpoint->options.requestTimeout);
}

ssize_t
gioAskResource (
  GioEndpoint *endpoint,
  uint8_t recipient, uint8_t type,
  uint8_t request, uint16_t value, uint16_t index,
  void *buffer, uint16_t size
) {
  GioAskResourceMethod *method = endpoint->handleMethods->askResource;

  if (!method) {
    logUnsupportedOperation("askResource");
    errno = ENOSYS;
    return -1;
  }

  return method(endpoint->handle, recipient, type,
                request, value, index, buffer, size,
                endpoint->options.requestTimeout);
}

int
gioGetHidReportSize (
  GioEndpoint *endpoint,
  HidReportIdentifier identifier,
  HidReportSize *size
) {
  GioGetHidReportSizeMethod *method = endpoint->handleMethods->getHidReportSize;

  if (!method) {
    logUnsupportedOperation("getHidReportSize");
    errno = ENOSYS;
    return 0;
  }

  int ok = method(
    endpoint->handle, identifier, size,
    endpoint->options.requestTimeout
  );

  if (ok) return 1;
  logMessage(LOG_WARNING, "HID report not found: %02X", identifier);
  return 0;
}

size_t
gioGetHidInputSize (
  GioEndpoint *endpoint,
  HidReportIdentifier identifier
) {
  HidReportSize size;
  if (!gioGetHidReportSize(endpoint, identifier, &size)) return 0;
  return size.input;
}

size_t
gioGetHidOutputSize (
  GioEndpoint *endpoint,
  HidReportIdentifier identifier
) {
  HidReportSize size;
  if (!gioGetHidReportSize(endpoint, identifier, &size)) return 0;
  return size.output;
}

size_t
gioGetHidFeatureSize (
  GioEndpoint *endpoint,
  HidReportIdentifier identifier
) {
  HidReportSize size;
  if (!gioGetHidReportSize(endpoint, identifier, &size)) return 0;
  return size.feature;
}

ssize_t
gioGetHidReport (
  GioEndpoint *endpoint, HidReportIdentifier identifier,
  unsigned char *buffer, size_t size
) {
  GioGetHidReportMethod *method = endpoint->handleMethods->getHidReport;

  if (!method) {
    logUnsupportedOperation("getHidReport");
    errno = ENOSYS;
    return -1;
  }

  buffer[0] = identifier;
  return method(endpoint->handle, identifier,
                buffer, size, endpoint->options.requestTimeout);
}

ssize_t
gioReadHidReport (
  GioEndpoint *endpoint,
  unsigned char *buffer, size_t size
) {
  return gioGetHidReport(endpoint, buffer[0], buffer, size);
}

ssize_t
gioSetHidReport (
  GioEndpoint *endpoint, HidReportIdentifier identifier,
  const unsigned char *data, size_t size
) {
  GioSetHidReportMethod *method = endpoint->handleMethods->setHidReport;

  if (!method) {
    logUnsupportedOperation("setHidReport");
    errno = ENOSYS;
    return -1;
  }

  return method(endpoint->handle, identifier,
                data, size, endpoint->options.requestTimeout);
}

ssize_t
gioWriteHidReport (
  GioEndpoint *endpoint,
  const unsigned char *data, size_t size
) {
  HidReportIdentifier identifier = data[0];
  if (!identifier) data += 1, size -= 1;
  return gioSetHidReport(endpoint, identifier, data, size);
}

ssize_t
gioGetHidFeature (
  GioEndpoint *endpoint, HidReportIdentifier identifier,
  unsigned char *buffer, size_t size
) {
  GioGetHidFeatureMethod *method = endpoint->handleMethods->getHidFeature;

  if (!method) {
    logUnsupportedOperation("getHidFeature");
    errno = ENOSYS;
    return -1;
  }

  buffer[0] = identifier;
  return method(endpoint->handle, identifier,
                buffer, size, endpoint->options.requestTimeout);
}

ssize_t
gioReadHidFeature (
  GioEndpoint *endpoint,
  unsigned char *buffer, size_t size
) {
  return gioGetHidFeature(endpoint, buffer[0], buffer, size);
}

ssize_t
gioSetHidFeature (
  GioEndpoint *endpoint, HidReportIdentifier identifier,
  const unsigned char *data, size_t size
) {
  GioSetHidFeatureMethod *method = endpoint->handleMethods->setHidFeature;

  if (!method) {
    logUnsupportedOperation("setHidFeature");
    errno = ENOSYS;
    return -1;
  }

  return method(endpoint->handle, identifier,
                data, size, endpoint->options.requestTimeout);
}

ssize_t
gioWriteHidFeature (
  GioEndpoint *endpoint,
  const unsigned char *data, size_t size
) {
  HidReportIdentifier identifier = data[0];
  if (!identifier) data += 1, size -= 1;
  return gioSetHidFeature(endpoint, identifier, data, size);
}

struct GioHandleInputObjectStruct {
  GioEndpoint *endpoint;
  AsyncHandle pollAlarm;

  GioInputHandler *handler;
  void *data;
};

static int
handleInput (GioHandleInputObject *hio, int error) {
  GioHandleInputParameters parameters = {
    .error = error,
    .data = hio->data
  };

  return hio->handler(&parameters);
}

ASYNC_MONITOR_CALLBACK(gioInputMonitor) {
  GioHandleInputObject *hio = parameters->data;

  handleInput(hio, parameters->error);
  return 1;
}

ASYNC_ALARM_CALLBACK(handleInputAlarm) {
  GioHandleInputObject *hio = parameters->data;

  if (handleInput(hio, 0)) asyncResetAlarmIn(hio->pollAlarm, 0);
}

GioHandleInputObject *
gioNewHandleInputObject (
  GioEndpoint *endpoint, int pollInterval,
  GioInputHandler *handler, void *data
) {
  GioHandleInputObject *hio;

  if ((hio = malloc(sizeof(*hio)))) {
    memset(hio, 0, sizeof(*hio));

    hio->endpoint = endpoint;
    hio->pollAlarm = NULL;

    hio->handler = handler;
    hio->data = data;

    if (endpoint) {
      if (gioMonitorInput(endpoint, gioInputMonitor, hio)) {
        handleInput(hio, 0);
        return hio;
      }
    }

    if (asyncNewRelativeAlarm(&hio->pollAlarm, 0, handleInputAlarm, hio)) {
      if (asyncResetAlarmInterval(hio->pollAlarm, pollInterval)) {
        return hio;
      }

      asyncCancelRequest(hio->pollAlarm);
    }

    free(hio);
  } else {
    logMallocError();
  }

  return NULL;
}

void
gioDestroyHandleInputObject (GioHandleInputObject *hio) {
  if (hio->pollAlarm) {
    asyncCancelRequest(hio->pollAlarm);
  } else {
    gioMonitorInput(hio->endpoint, NULL, NULL);
  }

  free(hio);
}
