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

#include "log.h"

struct UsbDeviceExtensionStruct {
  char *path;
  int file;
  int timeout;
};

struct UsbEndpointExtensionStruct {
  int file;
  int timeout;
};

static int
usbSetTimeout (int file, int new, int *old) {
  if (!old || (new != *old)) {
    int arg = new;
    if (ioctl(file, USB_SET_TIMEOUT, &arg) == -1) {
      logSystemError("USB timeout set");
      return 0;
    }
    if (old) *old = new;
  }
  return 1;
}

static int
usbSetShortTransfers (int file, int arg) {
  if (ioctl(file, USB_SET_SHORT_XFER, &arg) != -1) return 1;
  logSystemError("USB set short transfers");
  return 0;
}

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

int
usbSetConfiguration (UsbDevice *device, unsigned char configuration) {
  UsbDeviceExtension *devx = device->extension;
  int arg = configuration;
  if (ioctl(devx->file, USB_SET_CONFIG, &arg) != -1) return 1;
  logSystemError("USB configuration set");
  return 0;
}

int
usbClaimInterface (UsbDevice *device, unsigned char interface) {
  return 1;
/*
  logUnsupportedFunction();
  return 0;
*/
}

int
usbReleaseInterface (UsbDevice *device, unsigned char interface) {
  return 1;
/*
  logUnsupportedFunction();
  return 0;
*/
}

int
usbSetAlternative (
  UsbDevice *device,
  unsigned char interface,
  unsigned char alternative
) {
  UsbDeviceExtension *devx = device->extension;
  struct usb_alt_interface arg;
  memset(&arg, 0, sizeof(arg));
  arg.uai_interface_index = interface;
  arg.uai_alt_no = alternative;
  if (ioctl(devx->file, USB_SET_ALTINTERFACE, &arg) != -1) return 1;
  logSystemError("USB alternative set");
  return 0;
}

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

int
usbClearHalt (UsbDevice *device, unsigned char endpointAddress) {
  logUnsupportedFunction();
  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;
  struct usb_ctl_request arg;
  memset(&arg, 0, sizeof(arg));
  arg.ucr_request.bmRequestType = direction | recipient | type;
  arg.ucr_request.bRequest = request;
  USETW(arg.ucr_request.wValue, value);
  USETW(arg.ucr_request.wIndex, index);
  USETW(arg.ucr_request.wLength, length);
  arg.ucr_data = buffer;
  arg.ucr_flags = USBD_SHORT_XFER_OK;
  if (usbSetTimeout(devx->file, timeout, &devx->timeout)) {
    if (ioctl(devx->file, USB_DO_REQUEST, &arg) != -1) return arg.ucr_actlen;
    logSystemError("USB control transfer");
  }
  return -1;
}

void *
usbSubmitRequest (
  UsbDevice *device,
  unsigned char endpointAddress,
  void *buffer,
  size_t length,
  void *context
) {
  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
) {
  ssize_t count = -1;
  UsbEndpoint *endpoint = usbGetInputEndpoint(device, endpointNumber);
  if (endpoint) {
    UsbEndpointExtension *eptx = endpoint->extension;
    if (usbSetTimeout(eptx->file, timeout, &eptx->timeout)) {
      if ((count = read(eptx->file, buffer, length)) != -1) {
        if (!usbApplyInputFilters(endpoint, buffer, length, &count)) {
          errno = EIO;
          count = -1;
        }
      } else if (errno != ETIMEDOUT) {
        logSystemError("USB endpoint read");
      }
    }
  }
  return count;
}

ssize_t
usbWriteEndpoint (
  UsbDevice *device,
  unsigned char endpointNumber,
  const void *buffer,
  size_t length,
  int timeout
) {
  UsbEndpoint *endpoint = usbGetOutputEndpoint(device, endpointNumber);
  if (endpoint) {
    UsbEndpointExtension *eptx = endpoint->extension;
    if (usbSetTimeout(eptx->file, timeout, &eptx->timeout)) {
      ssize_t count = write(eptx->file, buffer, length);
      if (count != -1) return count;
      logSystemError("USB endpoint write");
    }
  }
  return -1;
}

int
usbReadDeviceDescriptor (UsbDevice *device) {
  UsbDeviceExtension *devx = device->extension;
  if (ioctl(devx->file, USB_GET_DEVICE_DESC, &device->descriptor) != -1) {
    return 1;
  }
  logSystemError("USB device descriptor read");
  return 0;
}

int
usbAllocateEndpointExtension (UsbEndpoint *endpoint) {
  UsbDeviceExtension *devx = endpoint->device->extension;
  UsbEndpointExtension *eptx;

  if ((eptx = malloc(sizeof(*eptx)))) {
    const char *prefix = devx->path;
    const char *dot = strchr(prefix, '.');
    int length = dot? (dot - prefix): strlen(prefix);
    char path[PATH_MAX+1];
    int flags = O_RDWR;

    snprintf(path, sizeof(path), USB_ENDPOINT_PATH_FORMAT,
             length, prefix, USB_ENDPOINT_NUMBER(endpoint->descriptor));

    switch (USB_ENDPOINT_DIRECTION(endpoint->descriptor)) {
      case UsbEndpointDirection_Input : flags = O_RDONLY; break;
      case UsbEndpointDirection_Output: flags = O_WRONLY; break;
    }

    if ((eptx->file = open(path, flags)) != -1) {
      if (((flags & O_ACCMODE) != O_RDONLY) || 
          usbSetShortTransfers(eptx->file, 1)) {
        eptx->timeout = -1;

        endpoint->extension = eptx;
        return 1;
      }

      close(eptx->file);
    }

    free(eptx);
  }

  return 0;
}

void
usbDeallocateEndpointExtension (UsbEndpointExtension *eptx) {
  if (eptx->file != -1) {
    close(eptx->file);
    eptx->file = -1;
  }

  free(eptx);
}

void
usbDeallocateDeviceExtension (UsbDeviceExtension *devx) {
  if (devx->file != -1) {
    close(devx->file);
    devx->file = -1;
  }

  free(devx->path);
  free(devx);
}

UsbDevice *
usbFindDevice (UsbDeviceChooser *chooser, UsbChooseChannelData *data) {
  UsbDevice *device = NULL;
  int busNumber = 0;
  while (1) {
    char busPath[PATH_MAX+1];
    int bus;
    snprintf(busPath, sizeof(busPath), "/dev/usb%d", busNumber);
    if ((bus = open(busPath, O_RDONLY)) != -1) {
      int deviceNumber;
      for (deviceNumber=1; deviceNumber<USB_MAX_DEVICES; deviceNumber++) {
        struct usb_device_info info;
        memset(&info, 0, sizeof(info));
        info.udi_addr = deviceNumber;
        if (ioctl(bus, USB_DEVICEINFO, &info) != -1) {
          static const char *driver = "ugen";
          const char *deviceName = info.udi_devnames[0];

          logMessage(LOG_CATEGORY(USB_IO), "device [%d,%d]: vendor=%s product=%s",
                     busNumber, deviceNumber, info.udi_vendor, info.udi_product);
          {
            int nameNumber;
            for (nameNumber=0; nameNumber<USB_MAX_DEVNAMES; nameNumber++) {
              const char *name = info.udi_devnames[nameNumber];
              if (*name)
                logMessage(LOG_CATEGORY(USB_IO), "name %d: %s", nameNumber, name);
            }
          }

          if (strncmp(deviceName, driver, strlen(driver)) == 0) {
            char devicePath[PATH_MAX+1];
            snprintf(devicePath, sizeof(devicePath), USB_CONTROL_PATH_FORMAT, deviceName);

            {
              UsbDeviceExtension *devx;
              if ((devx = malloc(sizeof(*devx)))) {
                if ((devx->path = strdup(devicePath))) {
                  if ((devx->file = open(devx->path, O_RDWR)) != -1) {
                    devx->timeout = -1;
                    if ((device = usbTestDevice(devx, chooser, data))) {
                      close(bus);
                      return device;
                    }
                    close(devx->file);
                  }
                  free(devx->path);
                }
                free(devx);
              }
            }
          }
        } else if (errno != ENXIO) {
          logSystemError("USB device query");
        }
      }
      close(bus);
    } else if (errno == ENOENT) {
      break;
    } else if (errno != ENXIO) {
      logSystemError("USB bus open");
    }
    busNumber++;
  }
  return device;
}

void
usbForgetDevices (void) {
}
