/*
 * Copyright 2018 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "usb_if.h"

/* Return 0 on error, since it's never gonna be EP 0 */
static int find_endpoint(const struct libusb_interface_descriptor *iface,
			 uint16_t subclass,
			 uint16_t protocol,
			 struct usb_endpoint *uep)
{
	const struct libusb_endpoint_descriptor *ep;

	if (iface->bInterfaceClass == 255 &&
	    iface->bInterfaceSubClass == subclass &&
	    iface->bInterfaceProtocol == protocol &&
	    iface->bNumEndpoints) {
		ep = &iface->endpoint[0];
		uep->ep_num = ep->bEndpointAddress & 0x7f;
		uep->chunk_len = ep->wMaxPacketSize;
		return 1;
	}

	return 0;
}

/* Return -1 on error */
static int find_interface(uint16_t subclass,
			  uint16_t protocol,
			  struct usb_endpoint *uep)
{
	int iface_num = -1;
	int r, i, j;
	struct libusb_device *dev;
	struct libusb_config_descriptor *conf = 0;
	const struct libusb_interface *iface0;
	const struct libusb_interface_descriptor *iface;

	dev = libusb_get_device(uep->devh);
	r = libusb_get_active_config_descriptor(dev, &conf);
	if (r < 0) {
		USB_ERROR("libusb_get_active_config_descriptor", r);
		goto out;
	}

	for (i = 0; i < conf->bNumInterfaces; i++) {
		iface0 = &conf->interface[i];
		for (j = 0; j < iface0->num_altsetting; j++) {
			iface = &iface0->altsetting[j];
			if (find_endpoint(iface, subclass, protocol, uep)) {
				iface_num = i;
				goto out;
			}
		}
	}

out:
	libusb_free_config_descriptor(conf);
	return iface_num;
}

static libusb_device_handle *check_device(libusb_device *dev,
	uint16_t vid, uint16_t pid, const char *serial)
{
	struct libusb_device_descriptor desc;
	libusb_device_handle *handle = NULL;
	char sn[256];
	size_t sn_size = 0;

	if (libusb_get_device_descriptor(dev, &desc) < 0)
		return NULL;

	if (libusb_open(dev, &handle) != LIBUSB_SUCCESS)
		return NULL;

	if (desc.iSerialNumber && serial) {
		sn_size = libusb_get_string_descriptor_ascii(handle,
				desc.iSerialNumber, (unsigned char *)sn,
				sizeof(sn));
	}
	/*
	 * If the VID, PID, and serial number don't match, then it's not the
	 * correct device. Close the handle and return NULL.
	 */
	if ((vid && vid != desc.idVendor) ||
	    (pid && pid != desc.idProduct) ||
	    (serial && ((sn_size != strlen(serial)) ||
	    memcmp(sn, serial, sn_size)))) {
		libusb_close(handle);
		return NULL;
	}
	return handle;
}

int usb_findit(const char *serial, uint16_t vid, uint16_t pid,
	       uint16_t subclass, uint16_t protocol, struct usb_endpoint *uep)
{
	int iface_num, r, i;
	libusb_device **devs;
	libusb_device_handle *devh = NULL;
	ssize_t count;

	memset(uep, 0, sizeof(*uep));

	/* Must supply either serial or vendor and product ids. */
	if (!serial && !(vid && pid))
		goto terminate_usb_findit;

	r = libusb_init(NULL);
	if (r < 0) {
		USB_ERROR("libusb_init", r);
		goto terminate_usb_findit;
	}

	printf("finding_device ");
	if (vid)
		printf("%04x:%04x ", vid, pid);
	if (serial)
		printf("%s", serial);
	printf("\n");

	count = libusb_get_device_list(NULL, &devs);
	if (count < 0)
		goto terminate_usb_findit;

	for (i = 0; i < count; i++) {
		devh = check_device(devs[i], vid, pid, serial);
		if (devh) {
			printf("Found device.\n");
			break;
		}
	}

	libusb_free_device_list(devs, 1);

	if (!devh) {
		fprintf(stderr, "Can't find device\n");
		goto terminate_usb_findit;
	}
	uep->devh = devh;

	iface_num = find_interface(subclass, protocol, uep);
	if (iface_num < 0) {
		fprintf(stderr, "USB interface %d is not found\n", uep->ep_num);
		goto terminate_usb_findit;
	}
	if (!uep->chunk_len) {
		fprintf(stderr, "wMaxPacketSize isn't valid\n");
		goto terminate_usb_findit;
	}

	printf("found interface %d endpoint %d, chunk_len %d\n",
	       iface_num, uep->ep_num, uep->chunk_len);

	libusb_set_auto_detach_kernel_driver(uep->devh, 1);
	r = libusb_claim_interface(uep->devh, iface_num);
	if (r < 0) {
		USB_ERROR("libusb_claim_interface", r);
		goto terminate_usb_findit;
	}

	printf("READY\n-------\n");
	return 0;

terminate_usb_findit:
	if (uep->devh)
		usb_shut_down(uep);
	return -1;
}

int usb_trx(struct usb_endpoint *uep, void *outbuf, int outlen,
	    void *inbuf, int inlen, int allow_less, size_t *rxed_count)
{

	int r, actual;

	/* Send data out */
	if (outbuf && outlen) {
		actual = 0;
		r = libusb_bulk_transfer(uep->devh, uep->ep_num,
					 outbuf, outlen,
					 &actual, 1000);
		if (r < 0) {
			USB_ERROR("libusb_bulk_transfer", r);
			return -1;
		}
		if (actual != outlen) {
			fprintf(stderr, "%s:%d, only sent %d/%d bytes\n",
				__FILE__, __LINE__, actual, outlen);
			usb_shut_down(uep);
		}
	}

	/* Read reply back */
	if (inbuf && inlen) {

		actual = 0;
		r = libusb_bulk_transfer(uep->devh, uep->ep_num | 0x80,
					 inbuf, inlen,
					 &actual, 1000);
		if (r < 0) {
			USB_ERROR("libusb_bulk_transfer", r);
			return -1;
		}
		if ((actual != inlen) && !allow_less) {
			fprintf(stderr, "%s:%d, only received %d/%d bytes\n",
				__FILE__, __LINE__, actual, inlen);
			usb_shut_down(uep);
		}

		if (rxed_count)
			*rxed_count = actual;
	}

	return 0;
}

void usb_shut_down(struct usb_endpoint *uep)
{
	libusb_close(uep->devh);
	libusb_exit(NULL);
}
