/*
 * 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-2018 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.com/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

#include "prologue.h"

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <usb.h>

#include "log.h"
#include "io_usb.h"
#include "usb_internal.h"

#ifdef __MINGW32__
#ifndef ETIMEDOUT
#define ETIMEDOUT 116
#endif /* ETIMEDOUT */
#endif /* __MINGW32__ */

struct UsbDeviceExtensionStruct {
  struct usb_dev_handle *handle;
};

int
usbDisableAutosuspend (UsbDevice *device) {
  logUnsupportedFunction();
  return 0;
}

int
usbSetConfiguration (UsbDevice *device, unsigned char configuration) {
  UsbDeviceExtension *devx = device->extension;
  int result;

  logMessage(LOG_CATEGORY(USB_IO), "setting configuration: %u", configuration);
  result = usb_set_configuration(devx->handle, configuration);
  if (result >= 0) return 1;

  errno = -result;
  logSystemError("USB configuration set");
  return 0;
}

int
usbClaimInterface (UsbDevice *device, unsigned char interface) {
  UsbDeviceExtension *devx = device->extension;
  int detached = 0;
  int result;

  logMessage(LOG_CATEGORY(USB_IO), "claiming interface: %u", interface);

  while (1) {
    char driver[0X100];

    result = usb_claim_interface(devx->handle, interface);
    if (result >= 0) return 1;

    if (result != -EBUSY) break;
    if (detached) break;

#ifdef LIBUSB_HAS_GET_DRIVER_NP
    result = usb_get_driver_np(devx->handle, interface, driver, sizeof(driver));

    if (result < 0)
#endif /* LIBUSB_HAS_GET_DRIVER_NP */

    {
      strcpy(driver, "unknown");
    }

    logMessage(LOG_WARNING, "USB interface in use: %u (%s)", interface, driver);

    if (strcmp(driver, "usbfs") == 0) {
      result = -EBUSY;
      break;
    }

#ifdef LIBUSB_HAS_DETACH_KERNEL_DRIVER_NP
    logMessage(LOG_CATEGORY(USB_IO), "detaching kernel driver: %u (%s)",
               interface, driver);

    result = usb_detach_kernel_driver_np(devx->handle, interface);

    if (result >= 0) {
      logMessage(LOG_CATEGORY(USB_IO), "detached kernel driver: %u (%s)",
                 interface, driver);

      detached = 1;
      continue;
    }

    result = -EBUSY;
#endif /* LIBUSB_HAS_DETACH_KERNEL_DRIVER_NP */

    break;
  }

  errno = -result;
  logSystemError("USB interface claim");
  return 0;
}

int
usbReleaseInterface (UsbDevice *device, unsigned char interface) {
  UsbDeviceExtension *devx = device->extension;
  int result;

  logMessage(LOG_CATEGORY(USB_IO), "releasing interface: %u", interface);
  result = usb_release_interface(devx->handle, interface);
  if (result >= 0) return 1;

  errno = -result;
  logSystemError("USB interface release");
  return 0;
}

int
usbSetAlternative (
  UsbDevice *device,
  unsigned char interface,
  unsigned char alternative
) {
  UsbDeviceExtension *devx = device->extension;
  int result;

  logMessage(LOG_CATEGORY(USB_IO), "setting alternative: %u[%u]", interface, alternative);
  result = usb_set_altinterface(devx->handle, alternative);
  if (result >= 0) return 1;

  errno = -result;
  logSystemError("USB alternative set");
  return 0;
}

int
usbClearHalt (UsbDevice *device, unsigned char endpointAddress) {
  UsbDeviceExtension *devx = device->extension;
  int result;

  logMessage(LOG_CATEGORY(USB_IO), "clearing endpoint: %02X", endpointAddress);
  result = usb_clear_halt(devx->handle, endpointAddress);
  if (result >= 0) return 1;

  errno = -result;
  logSystemError("USB endpoint clear");
  return 0;
}

ssize_t
usbControlTransfer (
  UsbDevice *device,
  uint8_t direction,
  uint8_t recipient,
  uint8_t type,
  uint8_t request,
  uint16_t value,
  uint16_t index,
  void *buffer,
  uint16_t length,
  int timeout
) {
  UsbDeviceExtension *devx = device->extension;
  UsbSetupPacket setup;
  int result;

  usbMakeSetupPacket(&setup, direction, recipient, type, request, value, index, length);

  if (direction == UsbControlDirection_Output) {
    if (length) logBytes(LOG_CATEGORY(USB_IO), "control output", buffer, length);
  }

  result = usb_control_msg(devx->handle, setup.bRequestType, setup.bRequest,
                           getLittleEndian16(setup.wValue),
                           getLittleEndian16(setup.wIndex), buffer,
                           getLittleEndian16(setup.wLength), timeout);

  if (result >= 0) {
    if (direction == UsbControlDirection_Input) {
      logBytes(LOG_CATEGORY(USB_IO), "control input", buffer, result);
    }

    return result;
  }

  errno = -result;
  logSystemError("USB control transfer");
  return -1;
}

void *
usbSubmitRequest (
  UsbDevice *device,
  unsigned char endpointAddress,
  void *buffer,
  size_t length,
  void *data
) {
  logUnsupportedFunction();
  return NULL;
}

int
usbCancelRequest (UsbDevice *device, void *request) {
  logUnsupportedFunction();
  return 0;
}

void *
usbReapResponse (
  UsbDevice *device,
  unsigned char endpointAddress,
  UsbResponse *response,
  int wait
) {
  logUnsupportedFunction();
  return NULL;
}

int
usbMonitorInputEndpoint (
  UsbDevice *device, unsigned char endpointNumber,
  AsyncMonitorCallback *callback, void *data
) {
  return 0;
}

ssize_t
usbReadEndpoint (
  UsbDevice *device,
  unsigned char endpointNumber,
  void *buffer,
  size_t length,
  int timeout
) {
  UsbDeviceExtension *devx = device->extension;
  UsbEndpoint *endpoint;

  if ((endpoint = usbGetInputEndpoint(device, endpointNumber))) {
    const UsbEndpointDescriptor *descriptor = endpoint->descriptor;
    UsbEndpointTransfer transfer = USB_ENDPOINT_TRANSFER(descriptor);
    ssize_t result = -1;

    switch (transfer) {
      case UsbEndpointTransfer_Bulk:
        result = usb_bulk_read(devx->handle, descriptor->bEndpointAddress,
                               buffer, length, timeout);
        break;

      case UsbEndpointTransfer_Interrupt:
        result = usb_interrupt_read(devx->handle, descriptor->bEndpointAddress,
                                    buffer, length, timeout);
        break;

      default:
        logMessage(LOG_ERR, "USB endpoint input transfer not supported: 0X%02X", transfer);
        result = -ENOSYS;
        break;
    }

    if (result >= 0) {
      if (!usbApplyInputFilters(endpoint, buffer, length, &result)) {
        result = -EIO;
      }
    }

    if (result >= 0) return result;
    errno = -result;
  }

#ifdef ETIMEDOUT
  if (errno == ETIMEDOUT) errno = EAGAIN;
#endif /* ETIMEDOUT */

  if (errno != EAGAIN) logSystemError("USB endpoint read");
  return -1;
}

ssize_t
usbWriteEndpoint (
  UsbDevice *device,
  unsigned char endpointNumber,
  const void *buffer,
  size_t length,
  int timeout
) {
  UsbDeviceExtension *devx = device->extension;
  UsbEndpoint *endpoint;

  if ((endpoint = usbGetOutputEndpoint(device, endpointNumber))) {
    const UsbEndpointDescriptor *descriptor = endpoint->descriptor;
    UsbEndpointTransfer transfer = USB_ENDPOINT_TRANSFER(descriptor);
    int result = -1;

    usbLogEndpointData(endpoint, "output", buffer, length);

    switch (transfer) {
      case UsbEndpointTransfer_Bulk:
        result = usb_bulk_write(devx->handle, descriptor->bEndpointAddress,
                                (char *)buffer, length, timeout);
        break;

      case UsbEndpointTransfer_Interrupt:
        result = usb_interrupt_write(devx->handle, descriptor->bEndpointAddress,
                                     (char *)buffer, length, timeout);
        break;

      default:
        logMessage(LOG_ERR, "USB endpoint output transfer not supported: 0X%02X", transfer);
        result = -ENOSYS;
        break;
    }

    if (result >= 0) return result;
    errno = -result;
  }

  logSystemError("USB endpoint write");
  return -1;
}

int
usbReadDeviceDescriptor (UsbDevice *device) {
  UsbDeviceExtension *devx = device->extension;
  memcpy(&device->descriptor, &usb_device(devx->handle)->descriptor, UsbDescriptorSize_Device);
  return 1;
}

int
usbAllocateEndpointExtension (UsbEndpoint *endpoint) {
  return 1;
}

void
usbDeallocateEndpointExtension (UsbEndpointExtension *eptx) {
}

void
usbDeallocateDeviceExtension (UsbDeviceExtension *devx) {
  if (devx->handle) {
    usb_close(devx->handle);
    devx->handle = NULL;
  }

  free(devx);
}

UsbDevice *
usbFindDevice (UsbDeviceChooser *chooser, UsbChooseChannelData *data) {
  UsbDevice *device = NULL;
  int result;

  {
    static int initialized = 0;
    if (!initialized) {
      usb_init();
      initialized = 1;
    }
  }

  if ((result = usb_find_busses()) >= 0) {
    if ((result = usb_find_devices()) >= 0) {
      struct usb_bus *bus = usb_get_busses();

      if (bus) {
        struct usb_bus *bus0 = bus;

        do {
          struct usb_device *dev = bus->devices;

          if (dev) {
            struct usb_device *dev0 = dev;

            do {
              UsbDeviceExtension *devx;

              if ((devx = malloc(sizeof(*devx)))) {
                if ((devx->handle = usb_open(dev))) {
                  if ((device = usbTestDevice(devx, chooser, data))) return device;

                  usb_close(devx->handle);
                } else {
                  logMessage(LOG_ERR, "USB open error: vendor=%X product=%X",
                             getLittleEndian16(dev->descriptor.idVendor),
                             getLittleEndian16(dev->descriptor.idProduct));
                }

                free(devx);
              } else {
                logSystemError("USB device extension allocate");
              }

              if ((dev = dev->next) == dev0) dev = NULL;
            } while (dev);
          }

          if ((bus = bus->next) == bus0) bus = NULL;
        } while (bus);
      }
    } else {
      errno = -result;
      logSystemError("USB devices find");
    }
  } else {
    errno = -result;
    logSystemError("USB busses find");
  }

  return device;
}

void
usbForgetDevices (void) {
}
