/*
 * This file is part of the Black Magic Debug project.
 *
 * Copyright(C) 2020 - 2022 Uwe Bonnes (bon@elektron.ikp.physik.tu-darmstadt.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 3 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/>.
 */

/* Find all known usb connected debuggers */
#include "general.h"
#include "libusb-1.0/libusb.h"
#include "cl_utils.h"
#include "ftdi_bmp.h"
#include "version.h"

#define NO_SERIAL_NUMBER "<no serial number>"

void bmp_ident(bmp_info_t *info)
{
	DEBUG_INFO("BMP hosted %s\n for ST-Link V2/3, CMSIS_DAP, JLINK and "
			   "LIBFTDI/MPSSE\n", FIRMWARE_VERSION);
	if (info && info->vid && info->pid)
		DEBUG_INFO("Using %04x:%04x %s %s\n %s\n", info->vid, info->pid,
				   (info->serial[0]) ? info->serial : NO_SERIAL_NUMBER,
				   info->manufacturer,
				   info->product);
}

void libusb_exit_function(bmp_info_t *info)
{
	if (!info->usb_link)
		return;
	libusb_free_transfer(info->usb_link->req_trans);
	libusb_free_transfer(info->usb_link->rep_trans);
	if (info->usb_link->ul_libusb_device_handle) {
		libusb_release_interface (
			info->usb_link->ul_libusb_device_handle, 0);
		libusb_close(info->usb_link->ul_libusb_device_handle);
	}
}

static bmp_type_t find_cmsis_dap_interface(libusb_device *dev,bmp_info_t *info) {
	bmp_type_t type = BMP_TYPE_NONE;

	struct libusb_config_descriptor *conf;
	char interface_string[128];

	int res = libusb_get_active_config_descriptor(dev, &conf);
	if (res < 0) {
		DEBUG_WARN( "WARN: libusb_get_active_config_descriptor() failed: %s",
				libusb_strerror(res));
		return type;
	}

	libusb_device_handle *handle;
	res = libusb_open(dev, &handle);
	if (res != LIBUSB_SUCCESS) {
		DEBUG_INFO("INFO: libusb_open() failed: %s\n",
					libusb_strerror(res));
		libusb_free_config_descriptor(conf);
		return type;
	}

	for (int i = 0; i < conf->bNumInterfaces; i++) {
		const struct libusb_interface_descriptor *interface = &conf->interface[i].altsetting[0];

		if (!interface->iInterface) {
			continue;
		}

		res = libusb_get_string_descriptor_ascii(
			handle, interface->iInterface, (uint8_t*)interface_string,
			sizeof(interface_string));
		if (res < 0) {
			DEBUG_WARN( "WARN: libusb_get_string_descriptor_ascii() failed: %s\n",
					libusb_strerror(res));
			continue;
		}

		if (!strstr(interface_string, "CMSIS")) {
			continue;
		}
		type = BMP_TYPE_CMSIS_DAP;

		if (interface->bInterfaceClass == 0xff && interface->bNumEndpoints == 2) {
			info->interface_num = interface->bInterfaceNumber;

			for (int j = 0; j < interface->bNumEndpoints; j++) {
				uint8_t n = interface->endpoint[j].bEndpointAddress;

				if (n & 0x80) {
					info->in_ep = n;
				} else {
					info->out_ep = n;
				}
			}

			/* V2 is preferred, return early. */
			break;
		}
	}
	libusb_free_config_descriptor(conf);
	return type;
}

int find_debuggers(BMP_CL_OPTIONS_t *cl_opts, bmp_info_t *info)
{
	libusb_device **devs;
	int res = libusb_init(&info->libusb_ctx);
	if (res) {
		DEBUG_WARN( "Fatal: Failed to get USB context: %s\n",
				libusb_strerror(res));
		exit(-1);
	}
	if (cl_opts->opt_cable) {
		if (!strcmp(cl_opts->opt_cable, "list") ||
			!strcmp(cl_opts->opt_cable, "l")) {
			cable_desc_t *cable = cable_desc;
			DEBUG_WARN("Available cables:\n");
			for (; cable->name; ++cable) {
				DEBUG_WARN("\t%s\n", cable->name);
			}
			exit(0);
		}
		info->bmp_type = BMP_TYPE_LIBFTDI;
	}
	int n_devs = libusb_get_device_list(info->libusb_ctx, &devs);
    if (n_devs < 0) {
        DEBUG_WARN( "WARN:libusb_get_device_list() failed");
		return -1;
	}
	bool report = false;
	int found_debuggers;
	struct libusb_device_descriptor desc;
	char serial[64];
	char manufacturer[128];
	char product[128];
	bool access_problems = false;
	char *active_cable = NULL;
	bool ftdi_unknown = false;
rescan:
	found_debuggers = 0;
	serial[0] = 0;
	manufacturer[0] = 0;
	product[0] = 0;
	access_problems = false;
	active_cable = NULL;
	ftdi_unknown = false;
	for (size_t i = 0; devs[i]; ++i) {
		bmp_type_t type = BMP_TYPE_NONE;
		libusb_device *dev = devs[i];
		int res = libusb_get_device_descriptor(dev, &desc);
		if (res < 0) {
            DEBUG_WARN( "WARN: libusb_get_device_descriptor() failed: %s",
					libusb_strerror(res));
			libusb_free_device_list(devs, 1);
			continue;
		}
		/* Exclude hubs from testing. Probably more classes could be excluded here!*/
		switch (desc.bDeviceClass) {
		case LIBUSB_CLASS_HUB:
		case LIBUSB_CLASS_WIRELESS:
			continue;
		}
		libusb_device_handle *handle = NULL;
		res = libusb_open(dev, &handle);
		if (res != LIBUSB_SUCCESS) {
			if (!access_problems) {
				DEBUG_INFO("INFO: Open USB %04x:%04x class %2x failed\n",
						   desc.idVendor, desc.idProduct, desc.bDeviceClass);
				access_problems = true;
			}
			continue;
		}
		/* If the device even has a serial number string, fetch it */
		if (desc.iSerialNumber) {
			res = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber,
				(uint8_t *)serial, sizeof(serial));
			/* If the call fails and it's not because the device gave us STALL, continue to the next one */
			if (res < 0 && res != LIBUSB_ERROR_PIPE) {
				libusb_close(handle);
				continue;
			}
			/* Device has no serial and that's ok. */
			else if (res <= 0)
				serial[0] = '\0';
		}
		else
			serial[0] = '\0';
		if (cl_opts->opt_serial && !strstr(serial, cl_opts->opt_serial)) {
			libusb_close(handle);
			continue;
		}
		/* Attempt to get the manufacturer string */
		if (desc.iManufacturer) {
			res = libusb_get_string_descriptor_ascii(handle, desc.iManufacturer,
				(uint8_t *)manufacturer, sizeof(manufacturer));
			/* If the call fails and it's not because the device gave us STALL, continue to the next one */
			if (res < 0 && res != LIBUSB_ERROR_PIPE) {
				DEBUG_WARN("WARN: libusb_get_string_descriptor_ascii() call to fetch manufacturer string failed: %s\n",
					libusb_strerror(res));
				libusb_close(handle);
				continue;
			}
			/* Device has no manufacturer string and that's ok. */
			else if (res <= 0)
				manufacturer[0] = '\0';
		}
		else
			manufacturer[0] = '\0';
		/* Attempt to get the product string */
		if (desc.iProduct) {
			res = libusb_get_string_descriptor_ascii(handle, desc.iProduct,
				(uint8_t *)product, sizeof(product));
			/* If the call fails and it's not because the device gave us STALL, continue to the next one */
			if (res < 0 && res != LIBUSB_ERROR_PIPE) {
				DEBUG_WARN("WARN: libusb_get_string_descriptor_ascii() call to fetch product string failed: %s\n",
					libusb_strerror(res));
				libusb_close(handle);
				continue;
			}
			/* Device has no product string and that's ok. */
			else if (res <= 0)
				product[0] = '\0';
		}
		else
			product[0] = '\0';
		libusb_close(handle);
		if (cl_opts->opt_ident_string) {
			char *match_manu = NULL;
			char *match_product = NULL;
			match_manu = strstr(manufacturer, cl_opts->opt_ident_string);
			match_product = strstr(product, cl_opts->opt_ident_string);
			if (!match_manu && !match_product)
				continue;
		}
		/* Either serial and/or ident_string match or are not given.
		 * Check type.*/
		if (desc.idVendor == VENDOR_ID_BMP) {
			if (desc.idProduct == PRODUCT_ID_BMP)
				type = BMP_TYPE_BMP;
			else {
				if (desc.idProduct == PRODUCT_ID_BMP_BL)
					DEBUG_WARN("BMP in bootloader mode found. Restart or reflash!\n");
				continue;
			}
		} else if (type == BMP_TYPE_NONE &&
				   (type = find_cmsis_dap_interface(dev, info)) != BMP_TYPE_NONE) {
			/* find_cmsis_dap_interface has set valid type*/
		} else if (strstr(manufacturer, "CMSIS") || strstr(product, "CMSIS"))
			type = BMP_TYPE_CMSIS_DAP;
		else if (desc.idVendor ==  VENDOR_ID_STLINK) {
			if (desc.idProduct == PRODUCT_ID_STLINKV2 ||
				desc.idProduct == PRODUCT_ID_STLINKV21 ||
				desc.idProduct == PRODUCT_ID_STLINKV21_MSD ||
				desc.idProduct == PRODUCT_ID_STLINKV3_NO_MSD ||
				desc.idProduct == PRODUCT_ID_STLINKV3_BL ||
				desc.idProduct == PRODUCT_ID_STLINKV3 ||
				desc.idProduct == PRODUCT_ID_STLINKV3E)
				type = BMP_TYPE_STLINKV2;
			else {
				if (desc.idProduct == PRODUCT_ID_STLINKV1)
					DEBUG_WARN( "INFO: STLINKV1 not supported\n");
				continue;
			}
		} else if (desc.idVendor ==  VENDOR_ID_SEGGER)
			type = BMP_TYPE_JLINK;
		else {
			cable_desc_t *cable = cable_desc;
			for (; cable->name; ++cable) {
				bool found = false;
				if (cable->vendor != desc.idVendor || cable->product != desc.idProduct)
					continue; /* VID/PID do not match*/
				if (cl_opts->opt_cable) {
					if (strncmp(cable->name, cl_opts->opt_cable, strlen(cable->name)))
						continue; /* cable names do not match*/
					else
						found = true;
				}
				if (cable->description) {
					if (strncmp(cable->description, product, strlen(cable->description)))
						continue; /* discriptions do not match*/
					else
						found = true;
				} else { /* VID/PID fits, but no cl_opts->opt_cable and no description*/
					if (cable->vendor == 0x0403 && /* FTDI*/
						(cable->product == 0x6010 || /* FT2232C/D/H*/
						 cable->product == 0x6011 || /* FT4232H Quad HS USB-UART/FIFO IC */
						 cable->product == 0x6014)) {  /* FT232H Single HS USB-UART/FIFO IC */
						ftdi_unknown = true;
						continue; /* Cable name is needed */
					}
				}
				if (found) {
					active_cable = cable->name;
					type = BMP_TYPE_LIBFTDI;
					break;
				}
			}
			if (!cable->name)
				continue;
		}
		if (report) {
			DEBUG_WARN("%2d: %s, %s, %s\n", found_debuggers + 1,
				serial[0] ? serial :  NO_SERIAL_NUMBER,
				manufacturer,product);
		}
		info->vid = desc.idVendor;
		info->pid = desc.idProduct;
		info->bmp_type = type;
		strncpy(info->serial, serial, sizeof(info->serial));
		strncpy(info->product, product, sizeof(info->product));
		strncpy(info->manufacturer, manufacturer, sizeof(info->manufacturer));
		if (cl_opts->opt_position &&
			cl_opts->opt_position == found_debuggers + 1) {
			found_debuggers = 1;
			break;
		} else
			++found_debuggers;
	}
	if (found_debuggers == 0 && ftdi_unknown)
		DEBUG_WARN("Generic FTDI MPSSE VID/PID found. Please specify exact type with \"-c <cable>\" !\n");
	if (found_debuggers == 1 && !cl_opts->opt_cable && info->bmp_type == BMP_TYPE_LIBFTDI)
		cl_opts->opt_cable = active_cable;
	if (!found_debuggers && cl_opts->opt_list_only)
		DEBUG_WARN("No usable debugger found\n");
	if (found_debuggers > 1 ||
		(found_debuggers == 1 && cl_opts->opt_list_only)) {
		if (!report) {
			if (found_debuggers > 1)
				DEBUG_WARN("%d debuggers found!\nSelect with -P <pos> "
						   "or -s <(partial)serial no.>\n",
						   found_debuggers);
			report = true;
			goto rescan;
		} else {
			if (found_debuggers > 0)
				access_problems = false;
			found_debuggers = 0;
		}
	}
	if (!found_debuggers && access_problems)
		DEBUG_WARN(
			"No debugger found. Please check access rights to USB devices!\n");
	libusb_free_device_list(devs, 1);
	return found_debuggers == 1 ? 0 : -1;
}

static void LIBUSB_CALL on_trans_done(struct libusb_transfer *trans)
{
    struct trans_ctx * const ctx = trans->user_data;

    if (trans->status != LIBUSB_TRANSFER_COMPLETED)
    {
		DEBUG_WARN("on_trans_done: ");
        if (trans->status == LIBUSB_TRANSFER_TIMED_OUT)
            DEBUG_WARN(" Timeout\n");
        else if (trans->status == LIBUSB_TRANSFER_CANCELLED)
            DEBUG_WARN(" cancelled\n");
        else if (trans->status == LIBUSB_TRANSFER_NO_DEVICE)
            DEBUG_WARN(" no device\n");
        else
            DEBUG_WARN(" unknown\n");
        ctx->flags |= TRANS_FLAGS_HAS_ERROR;
    }
    ctx->flags |= TRANS_FLAGS_IS_DONE;
}

static int submit_wait(usb_link_t *link, struct libusb_transfer *trans) {
	struct trans_ctx trans_ctx;
	enum libusb_error error;

	trans_ctx.flags = 0;

	/* brief intrusion inside the libusb interface */
	trans->callback = on_trans_done;
	trans->user_data = &trans_ctx;

	if ((error = libusb_submit_transfer(trans))) {
		DEBUG_WARN("libusb_submit_transfer(%d): %s\n", error,
			  libusb_strerror(error));
		exit(-1);
	}

	uint32_t start_time = platform_time_ms();
	while (trans_ctx.flags == 0) {
		struct timeval timeout;
		timeout.tv_sec = 1;
		timeout.tv_usec = 0;
		if (libusb_handle_events_timeout(link->ul_libusb_ctx, &timeout)) {
			DEBUG_WARN("libusb_handle_events()\n");
			return -1;
		}
		uint32_t now = platform_time_ms();
		if (now - start_time > 1000) {
			libusb_cancel_transfer(trans);
			DEBUG_WARN("libusb_handle_events() timeout\n");
			return -1;
		}
	}
	if (trans_ctx.flags & TRANS_FLAGS_HAS_ERROR) {
		DEBUG_WARN("libusb_handle_events() | has_error\n");
		return -1;
	}

	return 0;
}

/* One USB transaction */
int send_recv(usb_link_t *link,
					 uint8_t *txbuf, size_t txsize,
					 uint8_t *rxbuf, size_t rxsize)
{
	int res = 0;
	if (txsize) {
		libusb_fill_bulk_transfer(link->req_trans,
								  link->ul_libusb_device_handle,
								  link->ep_tx | LIBUSB_ENDPOINT_OUT,
								  txbuf, txsize,
								  NULL, NULL, 0);
		size_t i = 0;
		DEBUG_WIRE(" Send (%3zu): ", txsize);
		for (; i < txsize; ++i) {
			DEBUG_WIRE("%02x", txbuf[i]);
			if ((i & 7U) == 7U)
				DEBUG_WIRE(".");
			if ((i & 31U) == 31U)
				DEBUG_WIRE("\n             ");
		}
		if (!(i & 31U))
			DEBUG_WIRE("\n");
		if (submit_wait(link, link->req_trans)) {
			libusb_clear_halt(link->ul_libusb_device_handle, link->ep_tx);
			return -1;
		}
	}
	/* send_only */
	if (rxsize != 0) {
		/* read the response */
		libusb_fill_bulk_transfer(link->rep_trans, link->ul_libusb_device_handle,
								  link->ep_rx | LIBUSB_ENDPOINT_IN,
								  rxbuf, rxsize, NULL, NULL, 0);

		if (submit_wait(link, link->rep_trans)) {
			DEBUG_WARN("clear 1\n");
			libusb_clear_halt(link->ul_libusb_device_handle, link->ep_rx);
			return -1;
		}
		res = link->rep_trans->actual_length;
		if (res > 0) {
			const size_t rxlen = (size_t)res;
			DEBUG_WIRE(" Rec (%zu/%zu)", rxsize, rxlen);
			for (size_t i = 0; i < rxlen && i < 32 ; ++i) {
				if (i && ((i & 7U) == 0U))
					DEBUG_WIRE(".");
				DEBUG_WIRE("%02x", rxbuf[i]);
			}
		}
	}
	DEBUG_WIRE("\n");
	return res;
}
