//
// Common device support code for LPrint, a Label Printer Application
//
// Copyright © 2019-2020 by Michael R Sweet.
// Copyright © 2007-2019 by Apple Inc.
//
// Licensed under Apache License v2.0.  See the file "LICENSE" for more
// information.
//

//
// Include necessary headers...
//

#include "lprint.h"
#include <stdarg.h>


//
// Local functions...
//

static void	lprint_error(lprint_deverr_cb_t err_cb, void *err_data, const char *message, ...);
#ifdef HAVE_LIBUSB
static int	lprint_find_usb(lprint_device_cb_t cb, const void *user_data, lprint_device_t *device, lprint_deverr_cb_t err_cb, void *err_data);
static int	lprint_open_cb(const char *device_uri, const void *user_data);
#endif // HAVE_LIBUSB


//
// 'lprintCloseDevice()' - Close a device connection.
//

void
lprintCloseDevice(
    lprint_device_t *device)		// I - Device to close
{
  if (device)
  {
    if (device->debug_fd >= 0)
      close(device->debug_fd);

    if (device->fd >= 0)
      close(device->fd);
#ifdef HAVE_LIBUSB
    else if (device->handle)
    {
      libusb_close(device->handle);
      libusb_unref_device(device->device);
    }
#endif /* HAVE_LIBUSB */

    free(device);
  }
}


//
// 'lprintListDevices()' - List available devices.
//

void
lprintListDevices(
    lprint_device_cb_t cb,		// I - Callback function
    const void         *user_data,	// I - User data for callback
    lprint_deverr_cb_t err_cb,		// I - Error callback
    void               *err_data)	// I - Data for error callback
{
#ifdef HAVE_LIBUSB
  lprint_device_t	junk;		// Dummy device data


  lprint_find_usb(cb, user_data, &junk, err_cb, err_data);
#endif /* HAVE_LIBUSB */
}


//
// 'lprintOpenDevice()' - Open a connection to a device.
//
// Currently only "file:///dev/filename", "socket://address:port", and
// "usb://make/model?serial=value" URIs are supported.
//

lprint_device_t	*			// O - Device connection or `NULL`
lprintOpenDevice(
    const char         *device_uri,	// I - Device URI
    lprint_deverr_cb_t err_cb,		// I - Error callback
    void               *err_data)	// I - Data for error callback
{
  lprint_device_t	*device;	// Device structure
  char			scheme[32],	// URI scheme
			userpass[32],	// Username/password (not used)
			host[256],	// Host name or make
			resource[256],	// Resource path, if any
			*options;	// Pointer to options, if any
  int			port;		// Port number
  http_uri_status_t	status;		// URI status


  if (!device_uri)
    return (NULL);

  if ((status = httpSeparateURI(HTTP_URI_CODING_ALL, device_uri, scheme, sizeof(scheme), userpass, sizeof(userpass), host, sizeof(host), &port, resource, sizeof(resource))) < HTTP_URI_STATUS_OK)
  {
    lprint_error(err_cb, err_data, "Bad device URI '%s': %s", device_uri, httpURIStatusString(status));
    return (NULL);
  }

  if ((options = strchr(resource, '?')) != NULL)
    *options++ = '\0';

  if ((device = calloc(1, sizeof(lprint_device_t))) != NULL)
  {
    const char *lprint_device_debug = getenv("LPRINT_DEVICE_DEBUG");

    if (!strcmp(scheme, "file"))
    {
      // Character device file...
      if ((device->fd = open(resource, O_RDWR | O_EXCL)) < 0)
      {
        lprint_error(err_cb, err_data, "Unable to open '%s': %s", resource, strerror(errno));
        goto error;
      }
    }
    else if (!strcmp(scheme, "socket"))
    {
      // Raw socket (JetDirect or similar)
      char		port_str[32];	// String for port number
      http_addrlist_t	*list;		// Address list

      snprintf(port_str, sizeof(port_str), "%d", port);
      if ((list = httpAddrGetList(host, AF_UNSPEC, port_str)) == NULL)
      {
        lprint_error(err_cb, err_data, "Unable to lookup '%s:%d': %s", host, port, cupsLastErrorString());
        goto error;
      }

      httpAddrConnect2(list, &device->fd, 30000, NULL);
      httpAddrFreeList(list);

      if (device->fd < 0)
      {
        lprint_error(err_cb, err_data, "Unable to connect to '%s:%d': %s", host, port, cupsLastErrorString());
        goto error;
      }
    }
#ifdef HAVE_LIBUSB
    else if (!strcmp(scheme, "usb"))
    {
      // USB printer class device
      device->fd = -1;

      if (!lprint_find_usb(lprint_open_cb, device_uri, device, err_cb, err_data))
        goto error;
    }
#endif // HAVE_LIBUSB
    else
    {
      lprint_error(err_cb, err_data, "Unsupported device URI scheme '%s'.", scheme);
      goto error;
    }

    if (lprint_device_debug)
      device->debug_fd = open(lprint_device_debug, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    else
      device->debug_fd = -1;
  }

  return (device);

  error:

  free(device);
  return (NULL);
}


//
// 'lprintPrintfDevice()' - Write a formatted string.
//

ssize_t					// O - Number of characters or -1 on error
lprintPrintfDevice(
    lprint_device_t *device,		// I - Device
    const char      *format,		// I - Printf-style format string
    ...)				// I - Additional args as needed
{
  va_list	ap;			// Pointer to additional args
  char		buffer[8192];		// Output buffer


  va_start(ap, format);
  vsnprintf(buffer, sizeof(buffer), format, ap);
  va_end(ap);

  return (lprintWriteDevice(device, buffer, strlen(buffer)));
}


//
// 'lprintPutsDevice()' - Write a literal string.
//

ssize_t					// O - Number of characters or -1 on error
lprintPutsDevice(
    lprint_device_t *device,		// I - Device
    const char      *s)			// I - Literal string
{
  return (lprintWriteDevice(device, s, strlen(s)));
}


//
// 'lprintReadDevice()' - Read from a device.
//

ssize_t					// O - Number of bytes read or -1 on error
lprintReadDevice(
    lprint_device_t *device,		// I - Device
    void            *buffer,		// I - Read buffer
    size_t          bytes)		// I - Max bytes to read
{
  if (!device)
    return (-1);
  else if (device->fd >= 0)
  {
    ssize_t	count;			// Bytes read this time

    while ((count = read(device->fd, buffer, bytes)) < 0)
      if (errno != EINTR && errno != EAGAIN)
        break;

    return (count);
  }
#ifdef HAVE_LIBUSB
  else if (device->handle)
  {
    int	count;				// Bytes that were read

    if (libusb_bulk_transfer(device->handle, device->read_endp, buffer, (int)bytes, &count, 0) < 0)
      return (-1);
    else
      return (count);
  }
#endif // HAVE_LIBUSB

  return (-1);
}


//
// 'lprintWriteDevice()' - Write to a device.
//

ssize_t					// O - Number of bytes written or -1 on error
lprintWriteDevice(
    lprint_device_t *device,		// I - Device
    const void      *buffer,		// I - Write buffer
    size_t          bytes)		// I - Number of bytes to write
{
  if (!device)
    return (-1);

  if (device->debug_fd >= 0)
    write(device->debug_fd, buffer, bytes);

  if (device->fd >= 0)
  {
    const char	*ptr = (const char *)buffer;
					// Pointer into buffer
    ssize_t	total = 0,		// Total bytes written
		count;			// Bytes written this time

    while (total < bytes)
    {
      if ((count = write(device->fd, ptr, bytes - total)) < 0)
      {
        if (errno == EINTR || errno == EAGAIN)
          continue;

        return (-1);
      }

      total += (size_t)count;
      ptr   += count;
    }

    return ((ssize_t)total);
  }
#ifdef HAVE_LIBUSB
  else if (device->handle)
  {
    int	count;				// Bytes that were written

    if (libusb_bulk_transfer(device->handle, device->write_endp, (unsigned char *)buffer, (int)bytes, &count, 0) < 0)
      return (-1);
    else
      return (count);
  }
#endif // HAVE_LIBUSB

  return (-1);
}


//
// 'lprint_error()' - Report an error.
//

static void
lprint_error(
    lprint_deverr_cb_t err_cb,		// I - Error callback
    void               *err_data,	// I - Error callback data
    const char         *message,	// I - Printf-style message
    ...)				// I - Additional args as needed
{
  va_list	ap;			// Pointer to additional args
  char		buffer[8192];		// Formatted message


  if (!err_cb)
    return;

  va_start(ap, message);
  vsnprintf(buffer, sizeof(buffer), message, ap);
  va_end(ap);

  (*err_cb)(buffer, err_data);
}


#ifdef HAVE_LIBUSB
//
// 'lprint_find_usb()' - Find a USB printer.
//

static int				// O - 1 if found, 0 if not
lprint_find_usb(
    lprint_device_cb_t cb,		// I - Callback function
    const void         *user_data,	// I - User data pointer
    lprint_device_t    *device,		// O - Device info
    lprint_deverr_cb_t err_cb,		// I - Error callback
    void               *err_data)	// I - Error callback data
{
  ssize_t	err = 0,		// Current error
		i,			// Looping var
		num_udevs;		// Number of USB devices
  libusb_device	**udevs;		// USB devices


 /*
  * Get the list of connected USB devices...
  */

  device->device = NULL;
  device->handle = NULL;

  if ((err = libusb_init(NULL)) != 0)
  {
    lprint_error(err_cb, err_data, "Unable to initialize USB access: %s", libusb_strerror((enum libusb_error)err));
    return (0);
  }

  num_udevs = libusb_get_device_list(NULL, &udevs);

  LPRINT_DEBUG("lprint_find_usb: num_udevs=%d\n", (int)num_udevs);

 /*
  * Find the printers and do the callback until we find a match.
  */

  for (i = 0; i < num_udevs; i ++)
  {
    libusb_device *udevice = udevs[i];	// Current device
    char	device_id[1024],	// Current device ID
		device_uri[1024];	// Current device URI
    struct libusb_device_descriptor devdesc;
					// Current device descriptor
    struct libusb_config_descriptor *confptr = NULL;
					// Pointer to current configuration
    const struct libusb_interface *ifaceptr = NULL;
					// Pointer to current interface
    const struct libusb_interface_descriptor *altptr = NULL;
					// Pointer to current alternate setting
    const struct libusb_endpoint_descriptor *endpptr = NULL;
					// Pointer to current endpoint
    uint8_t	conf,			// Current configuration
		iface,			// Current interface
		altset,			// Current alternate setting
		endp,			// Current endpoint
		read_endp,		// Current read endpoint
		write_endp;		// Current write endpoint

    // Ignore devices with no configuration data and anything that is not
    // a printer...
    if (libusb_get_device_descriptor(udevice, &devdesc) < 0)
    {
      LPRINT_DEBUG("lprint_find_usb: udev%d - no descriptor.\n", (int)i);
      continue;
    }

    LPRINT_DEBUG("lprint_find_usb: udev%d -\n", (int)i);
    LPRINT_DEBUG("lprint_find_usb:     bLength=%d\n", devdesc.bLength);
    LPRINT_DEBUG("lprint_find_usb:     bDescriptorType=%d\n", devdesc.bDescriptorType);
    LPRINT_DEBUG("lprint_find_usb:     bcdUSB=%04x\n", devdesc.bcdUSB);
    LPRINT_DEBUG("lprint_find_usb:     bDeviceClass=%d\n", devdesc.bDeviceClass);
    LPRINT_DEBUG("lprint_find_usb:     bDeviceSubClass=%d\n", devdesc.bDeviceSubClass);
    LPRINT_DEBUG("lprint_find_usb:     bDeviceProtocol=%d\n", devdesc.bDeviceProtocol);
    LPRINT_DEBUG("lprint_find_usb:     bMaxPacketSize0=%d\n", devdesc.bMaxPacketSize0);
    LPRINT_DEBUG("lprint_find_usb:     idVendor=0x%04x\n", devdesc.idVendor);
    LPRINT_DEBUG("lprint_find_usb:     idProduct=0x%04x\n", devdesc.idProduct);
    LPRINT_DEBUG("lprint_find_usb:     bcdDevice=%04x\n", devdesc.bcdDevice);
    LPRINT_DEBUG("lprint_find_usb:     iManufacturer=%d\n", devdesc.iManufacturer);
    LPRINT_DEBUG("lprint_find_usb:     iProduct=%d\n", devdesc.iProduct);
    LPRINT_DEBUG("lprint_find_usb:     iSerialNumber=%d\n", devdesc.iSerialNumber);
    LPRINT_DEBUG("lprint_find_usb:     bNumConfigurations=%d\n", devdesc.bNumConfigurations);

    if (!devdesc.bNumConfigurations || !devdesc.idVendor || !devdesc.idProduct)
      continue;

    if (devdesc.idVendor == 0x05ac)
      continue;				// Skip Apple devices...

    device->device     = udevice;
    device->handle     = NULL;
    device->conf       = -1;
    device->origconf   = -1;
    device->iface      = -1;
    device->ifacenum   = -1;
    device->altset     = -1;
    device->write_endp = -1;
    device->read_endp  = -1;
    device->protocol   = 0;

    for (conf = 0; conf < devdesc.bNumConfigurations; conf ++)
    {
      if (libusb_get_config_descriptor(udevice, conf, &confptr) < 0)
      {
        LPRINT_DEBUG("lprint_find_usb:     conf%d - no descriptor\n", conf);
	continue;
      }

      LPRINT_DEBUG("lprint_find_usb:     conf%d -\n", conf);
      LPRINT_DEBUG("lprint_find_usb:         bLength=%d\n", confptr->bLength);
      LPRINT_DEBUG("lprint_find_usb:         bDescriptorType=%d\n", confptr->bDescriptorType);
      LPRINT_DEBUG("lprint_find_usb:         wTotalLength=%d\n", confptr->wTotalLength);
      LPRINT_DEBUG("lprint_find_usb:         bNumInterfaces=%d\n", confptr->bNumInterfaces);
      LPRINT_DEBUG("lprint_find_usb:         bConfigurationValue=%d\n", confptr->bConfigurationValue);
      LPRINT_DEBUG("lprint_find_usb:         iConfiguration=%d\n", confptr->iConfiguration);
      LPRINT_DEBUG("lprint_find_usb:         bmAttributes=%d\n", confptr->bmAttributes);
      LPRINT_DEBUG("lprint_find_usb:         MaxPower=%d\n", confptr->MaxPower);
      LPRINT_DEBUG("lprint_find_usb:         interface=%p\n", confptr->interface);
      LPRINT_DEBUG("lprint_find_usb:         extra=%p\n", confptr->extra);
      LPRINT_DEBUG("lprint_find_usb:         extra_length=%d\n", confptr->extra_length);

      // Some printers offer multiple interfaces...
      for (iface = 0, ifaceptr = confptr->interface; iface < confptr->bNumInterfaces; iface ++, ifaceptr ++)
      {
        if (!ifaceptr->altsetting)
        {
          LPRINT_DEBUG("lprint_find_usb:         iface%d - no alternate setting\n", iface);
          continue;
        }

	LPRINT_DEBUG("lprint_find_usb:         iface%d -\n", iface);
	LPRINT_DEBUG("lprint_find_usb:             num_altsetting=%d\n", ifaceptr->num_altsetting);
	LPRINT_DEBUG("lprint_find_usb:             altsetting=%p\n", ifaceptr->altsetting);

	for (altset = 0, altptr = ifaceptr->altsetting; (int)altset < ifaceptr->num_altsetting; altset ++, altptr ++)
	{
	  LPRINT_DEBUG("lprint_find_usb:             altset%d - bInterfaceClass=%d, bInterfaceSubClass=%d, bInterfaceProtocol=%d\n", altset, altptr->bInterfaceClass, altptr->bInterfaceSubClass, altptr->bInterfaceProtocol);

	  if (altptr->bInterfaceClass != LIBUSB_CLASS_PRINTER || altptr->bInterfaceSubClass != 1)
	    continue;

	  if (altptr->bInterfaceProtocol != 1 && altptr->bInterfaceProtocol != 2)
	    continue;

	  if (altptr->bInterfaceProtocol < device->protocol)
	    continue;

	  read_endp  = 0xff;
	  write_endp = 0xff;

	  for (endp = 0, endpptr = altptr->endpoint; endp < altptr->bNumEndpoints; endp ++, endpptr ++)
	  {
	    if ((endpptr->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK) == LIBUSB_TRANSFER_TYPE_BULK)
	    {
	      if (endpptr->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK)
		read_endp = endp;
	      else
		write_endp = endp;
	    }
	  }

	  if (write_endp != 0xff)
	  {
	    // Save the best match so far...
	    device->protocol   = altptr->bInterfaceProtocol;
	    device->altset     = altptr->bAlternateSetting;
	    device->ifacenum   = altptr->bInterfaceNumber;
	    device->write_endp = write_endp;
	    if (device->protocol > 1)
	      device->read_endp = read_endp;
	  }
	}

	if (device->protocol > 0)
	{
	  device->conf  = conf;
	  device->iface = iface;

	  if (!libusb_open(udevice, &device->handle))
	  {
	    uint8_t	current;	// Current configuration

	    // Opened the device, try to set the configuration...
	    if (libusb_control_transfer(device->handle, LIBUSB_REQUEST_TYPE_STANDARD | LIBUSB_ENDPOINT_IN | LIBUSB_RECIPIENT_DEVICE, 8, /* GET_CONFIGURATION */ 0, 0, (unsigned char *)&current, 1, 5000) < 0)
	      current = 0;

            if (confptr->bConfigurationValue != current)
            {
              // Select the configuration we want...
              if (libusb_set_configuration(device->handle, confptr->bConfigurationValue) < 0)
              {
                libusb_close(device->handle);
                device->handle = NULL;
              }
            }

#ifdef __linux
            if (device->handle)
            {
	      // Make sure the old, busted usblp kernel driver is not loaded...
	      if (libusb_kernel_driver_active(device->handle, device->iface) == 1)
	      {
		if ((err = libusb_detach_kernel_driver(device->handle, device->iface)) < 0)
		{
		  lprint_error(err_cb, err_data, "Unable to detach usblp kernel driver for USB printer %04x:%04x: %s", devdesc.idVendor, devdesc.idProduct, libusb_strerror((enum libusb_error)err));
		  libusb_close(device->handle);
		  device->handle = NULL;
		}
	      }
	    }
#endif // __linux

            if (device->handle)
            {
              // Claim the interface...
              if ((err = libusb_claim_interface(device->handle, device->ifacenum)) < 0)
              {
		lprint_error(err_cb, err_data, "Unable to claim USB interface: %s", libusb_strerror((enum libusb_error)err));
                libusb_close(device->handle);
                device->handle = NULL;
              }
            }

            if (device->handle && ifaceptr->num_altsetting > 1)
            {
              // Set the alternate setting as needed...
              if ((err = libusb_set_interface_alt_setting(device->handle, device->ifacenum, device->altset)) < 0)
              {
		lprint_error(err_cb, err_data, "Unable to set alternate USB interface: %s", libusb_strerror((enum libusb_error)err));
                libusb_close(device->handle);
                device->handle = NULL;
              }
            }

            if (device->handle)
            {
              // Get the 1284 Device ID...
              if ((err = libusb_control_transfer(device->handle, LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_ENDPOINT_IN | LIBUSB_RECIPIENT_INTERFACE, 0, device->conf, (device->iface << 8) | device->altset, (unsigned char *)device_id, sizeof(device_id), 5000)) < 0)
              {
		lprint_error(err_cb, err_data, "Unable to get IEEE-1284 device ID: %s", libusb_strerror((enum libusb_error)err));
                device_id[0] = '\0';
                libusb_close(device->handle);
                device->handle = NULL;
              }
              else
              {
                int length = ((device_id[0] & 255) << 8) | (device_id[1] & 255);
                if (length < 14 || length > sizeof(device_id))
                  length = ((device_id[1] & 255) << 8) | (device_id[0] & 255);

                if (length > sizeof(device_id))
                  length = sizeof(device_id);

                length -= 2;
                memmove(device_id, device_id + 2, (size_t)length);
                device_id[length] = '\0';

                LPRINT_DEBUG("lprint_find_usb:     device_id=\"%s\"\n", device_id);
              }
            }

            if (device->handle)
            {
              // Build the device URI...
              char	*make,		// Pointer to make
			*model,		// Pointer to model
			*serial = NULL,	// Pointer to serial number
			*ptr,		// Pointer into device ID
			temp[256];	// Temporary string for serial #

              if ((make = strstr(device_id, "MANUFACTURER:")) != NULL)
                make += 13;
              else if ((make = strstr(device_id, "MFG:")) != NULL)
                make += 4;

              if ((model = strstr(device_id, "MODEL:")) != NULL)
                model += 6;
              else if ((model = strstr(device_id, "MDL:")) != NULL)
                model += 4;

              if ((serial = strstr(device_id, "SERIALNUMBER:")) != NULL)
                serial += 12;
              else if ((serial = strstr(device_id, "SERN:")) != NULL)
                serial += 5;
              else if ((serial = strstr(device_id, "SN:")) != NULL)
                serial += 3;

              if (serial)
              {
                if ((ptr = strchr(serial, ';')) != NULL)
                  *ptr = '\0';
              }
              else
              {
                int length = libusb_get_string_descriptor_ascii(device->handle, devdesc.iSerialNumber, (unsigned char *)temp, sizeof(temp) - 1);
                if (length > 0)
                {
                  temp[length] = '\0';
                  serial       = temp;
                }
              }

              if (make)
              {
                if ((ptr = strchr(make, ';')) != NULL)
                  *ptr = '\0';
              }
              else
                make = "Unknown";

              if (model)
              {
                if ((ptr = strchr(model, ';')) != NULL)
                  *ptr = '\0';
              }
              else
                model = "Unknown";

              if (serial)
                httpAssembleURIf(HTTP_URI_CODING_ALL, device_uri, sizeof(device_uri), "usb", NULL, make, 0, "/%s?serial=%s", model, serial);
              else
                httpAssembleURIf(HTTP_URI_CODING_ALL, device_uri, sizeof(device_uri), "usb", NULL, make, 0, "/%s", model);

              if ((*cb)(device_uri, user_data))
              {
                LPRINT_DEBUG("lprint_find_usb:     Found a match.\n");

		libusb_ref_device(device->device);

		if (device->read_endp != -1)
		  device->read_endp = confptr->interface[device->iface].altsetting[device->altset].endpoint[device->read_endp].bEndpointAddress;

		if (device->write_endp != -1)
		  device->write_endp = confptr->interface[device->iface].altsetting[device->altset].endpoint[device->write_endp].bEndpointAddress;

                goto match_found;
              }

	      libusb_close(device->handle);
	      device->handle = NULL;
            }
	  }
	}
      } // iface loop

      libusb_free_config_descriptor(confptr);
    } // conf loop
  }

  match_found:

  LPRINT_DEBUG("lprint_find_usb: device->handle=%p\n", device->handle);

  // Clean up ....
  if (num_udevs >= 0)
    libusb_free_device_list(udevs, 1);

  return (device->handle != NULL);
}


//
// 'lprint_open_cb()' - Look for a matching device URI.
//

static int				// O - 1 on match, 0 otherwise
lprint_open_cb(const char *device_uri,	// I - This device's URI
	       const void *user_data)	// I - URI we are looking for
{
  int match = !strcmp(device_uri, (const char *)user_data);
					// Does this match?

  LPRINT_DEBUG("lprint_open_cb(device_uri=\"%s\", user_data=\"%s\") returning %d.\n", device_uri, (char *)user_data, match);

  return (match);
}
#endif // HAVE_LIBUSB
