/*
 * This file is part of the libjaylink project.
 *
 * Copyright (C) 2014-2016 Marc Schink <jaylink-dev@marcschink.de>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <sys/types.h>

#include "libjaylink.h"
#include "libjaylink-internal.h"

/*
 * libusb.h includes windows.h and therefore must be included after anything
 * that includes winsock2.h.
 */
#include <libusb.h>

/**
 * @file
 *
 * Device discovery (USB).
 */

/** @cond PRIVATE */
/** USB Vendor ID (VID) of SEGGER products. */
#define USB_VENDOR_ID			0x1366

/* USB Product IDs (PID) and their corresponding USB addresses. */
static const struct {
	uint16_t pid;
	enum jaylink_usb_address usb_address;
} pids[] = {
	{0x0101, JAYLINK_USB_ADDRESS_0},
	{0x0102, JAYLINK_USB_ADDRESS_1},
	{0x0103, JAYLINK_USB_ADDRESS_2},
	{0x0104, JAYLINK_USB_ADDRESS_3},
	{0x0105, JAYLINK_USB_ADDRESS_0},
	{0x0107, JAYLINK_USB_ADDRESS_0},
	{0x0108, JAYLINK_USB_ADDRESS_0},
	{0x1010, JAYLINK_USB_ADDRESS_0},
	{0x1011, JAYLINK_USB_ADDRESS_0},
	{0x1012, JAYLINK_USB_ADDRESS_0},
	{0x1013, JAYLINK_USB_ADDRESS_0},
	{0x1014, JAYLINK_USB_ADDRESS_0},
	{0x1015, JAYLINK_USB_ADDRESS_0},
	{0x1016, JAYLINK_USB_ADDRESS_0},
	{0x1017, JAYLINK_USB_ADDRESS_0},
	{0x1018, JAYLINK_USB_ADDRESS_0},
	{0x1020, JAYLINK_USB_ADDRESS_0},
	{0x1051, JAYLINK_USB_ADDRESS_0},
	{0x1055, JAYLINK_USB_ADDRESS_0},
	{0x1061, JAYLINK_USB_ADDRESS_0}
};

/** Maximum length of the USB string descriptor for the serial number. */
#define USB_SERIAL_NUMBER_LENGTH	12

/**
 * Maximum number of digits in a serial number
 *
 * The serial number of a device consists of at most 9 digits but user defined
 * serial numbers are allowed with up to 10 digits.
 */
#define MAX_SERIAL_NUMBER_DIGITS	10
/** @endcond */

static bool parse_serial_number(const char *str, uint32_t *serial_number)
{
	size_t length;

	length = strlen(str);

	/*
	 * Skip the first digits which are not part of a valid serial number.
	 * This is necessary because some devices erroneously use random digits
	 * instead of zeros for padding.
	 */
	if (length > MAX_SERIAL_NUMBER_DIGITS)
		str = str + (length - MAX_SERIAL_NUMBER_DIGITS);

	if (jaylink_parse_serial_number(str, serial_number) != JAYLINK_OK)
		return false;

	return true;
}

static bool compare_devices(const void *a, const void *b)
{
	const struct jaylink_device *dev;
	const struct libusb_device *usb_dev;

	dev = a;
	usb_dev = b;

	if (dev->iface != JAYLINK_HIF_USB)
		return false;

	if (dev->usb_dev == usb_dev)
		return true;

	return false;
}

static struct jaylink_device *find_device(const struct jaylink_context *ctx,
		const struct libusb_device *usb_dev)
{
	struct list *item;

	item = list_find_custom(ctx->devs, &compare_devices, usb_dev);

	if (item)
		return item->data;

	return NULL;
}

static struct jaylink_device *probe_device(struct jaylink_context *ctx,
		struct libusb_device *usb_dev)
{
	int ret;
	struct libusb_device_descriptor desc;
	struct libusb_device_handle *usb_devh;
	struct jaylink_device *dev;
	char buf[USB_SERIAL_NUMBER_LENGTH + 1];
	enum jaylink_usb_address usb_address;
	uint32_t serial_number;
	bool has_serial_number;
	bool found_device;

	ret = libusb_get_device_descriptor(usb_dev, &desc);

	if (ret != LIBUSB_SUCCESS) {
		log_warn(ctx, "Failed to get device descriptor: %s",
			libusb_error_name(ret));
		return NULL;
	}

	if (desc.idVendor != USB_VENDOR_ID)
		return NULL;

	found_device = false;

	for (size_t i = 0; i < sizeof(pids) / sizeof(pids[0]); i++) {
		if (pids[i].pid == desc.idProduct) {
			usb_address = pids[i].usb_address;
			found_device = true;
			break;
		}
	}

	if (!found_device)
		return NULL;

	log_dbg(ctx, "Found device (VID:PID = %04x:%04x, bus:address = "
		"%03u:%03u)", desc.idVendor, desc.idProduct,
		libusb_get_bus_number(usb_dev),
		libusb_get_device_address(usb_dev));

	/*
	 * Search for an already allocated device instance for this device and
	 * if found return a reference to it.
	 */
	dev = find_device(ctx, usb_dev);

	if (dev) {
		log_dbg(ctx, "Device: USB address = %u", dev->usb_address);

		if (dev->has_serial_number)
			log_dbg(ctx, "Device: Serial number = %u",
				dev->serial_number);
		else
			log_dbg(ctx, "Device: Serial number = N/A");

		log_dbg(ctx, "Using existing device instance");
		return jaylink_ref_device(dev);
	}

	/* Open the device to be able to retrieve its serial number. */
	ret = libusb_open(usb_dev, &usb_devh);

	if (ret != LIBUSB_SUCCESS) {
		log_warn(ctx, "Failed to open device: %s",
			libusb_error_name(ret));
		return NULL;
	}

	serial_number = 0;
	has_serial_number = true;

	ret = libusb_get_string_descriptor_ascii(usb_devh, desc.iSerialNumber,
		(unsigned char *)buf, USB_SERIAL_NUMBER_LENGTH + 1);

	libusb_close(usb_devh);

	if (ret < 0) {
		log_warn(ctx, "Failed to retrieve serial number: %s",
			libusb_error_name(ret));
		has_serial_number = false;
	}

	if (has_serial_number) {
		if (!parse_serial_number(buf, &serial_number)) {
			log_warn(ctx, "Failed to parse serial number");
			return NULL;
		}
	}

	log_dbg(ctx, "Device: USB address = %u", usb_address);

	if (has_serial_number)
		log_dbg(ctx, "Device: Serial number = %u", serial_number);
	else
		log_dbg(ctx, "Device: Serial number = N/A");

	log_dbg(ctx, "Allocating new device instance");

	dev = device_allocate(ctx);

	if (!dev) {
		log_warn(ctx, "Device instance malloc failed");
		return NULL;
	}

	dev->iface = JAYLINK_HIF_USB;
	dev->usb_dev = libusb_ref_device(usb_dev);
	dev->usb_address = usb_address;
	dev->serial_number = serial_number;
	dev->has_serial_number = has_serial_number;

	return dev;
}

JAYLINK_PRIV int discovery_usb_scan(struct jaylink_context *ctx)
{
	ssize_t ret;
	struct libusb_device **devs;
	struct jaylink_device *dev;
	size_t num;

	ret = libusb_get_device_list(ctx->usb_ctx, &devs);

	if (ret == LIBUSB_ERROR_IO) {
		log_err(ctx, "Failed to retrieve device list: input/output "
			"error");
		return JAYLINK_ERR_IO;
	} else if (ret < 0) {
		log_err(ctx, "Failed to retrieve device list: %s",
			libusb_error_name(ret));
		return JAYLINK_ERR;
	}

	num = 0;

	for (size_t i = 0; devs[i]; i++) {
		dev = probe_device(ctx, devs[i]);

		if (!dev)
			continue;

		ctx->discovered_devs = list_prepend(ctx->discovered_devs, dev);
		num++;
	}

	libusb_free_device_list(devs, true);
	log_dbg(ctx, "Found %zu USB device(s)", num);

	return JAYLINK_OK;
}
