/*
 * This file is part of the Black Magic Debug project.
 *
 * Copyright (C) 2020 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/>.
 */
/* Base on code from git://repo.or.cz/libjaylink.git
 * and https://github.com/afaerber/jlink.git*/

#include "general.h"
#include "gdb_if.h"
#include "adiv5.h"
#include "exception.h"

#include <assert.h>
#include <unistd.h>
#include <signal.h>
#include <ctype.h>
#include <sys/time.h>

#include "cl_utils.h"
#include "jlink.h"

#define USB_PID_SEGGER       0x1366

/* Only two devices PIDS tested so long */
#define USB_VID_SEGGER_0101  0x0101
#define USB_VID_SEGGER_0105  0x0105
#define USB_VID_SEGGER_1020  0x1020

static uint32_t emu_caps;
static uint32_t emu_speed_kHz;
static uint16_t emu_min_divisor;
static uint16_t emu_current_divisor;

static void jlink_print_caps(bmp_info_t *info)
{
	uint8_t cmd[1] = {CMD_GET_CAPS};
	uint8_t res[4];
	send_recv(info->usb_link, cmd, 1, res, sizeof(res));
	emu_caps = res[0] | (res[1] << 8) | (res[2] << 16) | (res[3] << 24);
	DEBUG_INFO("Caps %" PRIx32 "\n", emu_caps);
	if (emu_caps & JLINK_CAP_GET_HW_VERSION) {
		uint8_t cmd[1] = {CMD_GET_HW_VERSION};
		send_recv(info->usb_link, cmd, 1, NULL, 0);
		send_recv(info->usb_link, NULL, 0, res, sizeof(res));
		DEBUG_INFO("HW: Type %d, Major %d, Minor %d, Rev %d\n",
			   res[3], res[2], res[1], res[0]);
	}
}
static void jlink_print_speed(bmp_info_t *info)
{
	uint8_t cmd[1] = {CMD_GET_SPEEDS};
	uint8_t res[6];
	send_recv(info->usb_link, cmd, 1, res, sizeof(res));
	emu_speed_kHz = (res[0] | (res[1] << 8) | (res[2] << 16) | (res[3] << 24)) /
					 1000;
	emu_min_divisor = res[4] | (res[5] << 8);
	DEBUG_INFO("Emulator speed %d kHz, Mindiv %d%s\n", emu_speed_kHz,
			   emu_min_divisor,
			   (emu_caps & JLINK_CAP_GET_SPEEDS) ? "" : ", fixed");
}

static void jlink_print_version(bmp_info_t *info)
{
	uint8_t cmd[1] = {CMD_GET_VERSION};
	uint8_t len_str[2];
	send_recv(info->usb_link, cmd, 1, len_str, sizeof(len_str));
	uint8_t version[0x70];
	send_recv(info->usb_link, NULL, 0, version, sizeof(version));
	DEBUG_INFO("%s\n", version );
}

static void jlink_print_interfaces(bmp_info_t *info)
{
	uint8_t cmd[2] = {CMD_GET_SELECT_IF, JLINK_IF_GET_ACTIVE};
	uint8_t res[4];
	send_recv(info->usb_link, cmd, 2, res, sizeof(res));
	cmd[1] = JLINK_IF_GET_AVAILABLE;
	uint8_t res1[4];
	send_recv(info->usb_link, cmd, 2, res1, sizeof(res1));
	DEBUG_INFO("%s active", (res[0] == SELECT_IF_SWD) ? "SWD":
		   (res[0] == SELECT_IF_JTAG) ? "JTAG" : "NONE");
	uint8_t other_interface = res1[0] - (res[0] + 1);
	if (other_interface)
		DEBUG_INFO(", %s available\n",
			   (other_interface == JLINK_IF_SWD) ? "SWD": "JTAG");
	else
		DEBUG_INFO(", %s not available\n",
			   ((res[0] + 1) == JLINK_IF_SWD) ? "JTAG": "SWD");
}

static void jlink_info(bmp_info_t *info)
{
	jlink_print_version(info);
	jlink_print_caps(info);
	jlink_print_speed(info);
	jlink_print_interfaces(info);
}

/* On success endpoints are set and return 0, !0 else */
static int initialize_handle(bmp_info_t *info, libusb_device *dev)
{
	struct libusb_config_descriptor *config;
	int ret =  libusb_get_active_config_descriptor(dev, &config);
	if (ret != LIBUSB_SUCCESS) {
		DEBUG_WARN( "Failed to get configuration descriptor: %s.",
				libusb_error_name(ret));
		return -1;
	}
	const struct libusb_interface *interface;
	bool found_interface = false;
	const struct libusb_interface_descriptor *desc;
	for (int i = 0; i < config->bNumInterfaces; i++) {
		interface = &config->interface[i];
		desc = &interface->altsetting[0];
		if (desc->bInterfaceClass != LIBUSB_CLASS_VENDOR_SPEC)
			continue;
		if (desc->bInterfaceSubClass != LIBUSB_CLASS_VENDOR_SPEC)
			continue;
		if (desc->bNumEndpoints < 2)
			continue;
		found_interface = true;
		if (libusb_claim_interface (
				info->usb_link->ul_libusb_device_handle, i)) {
			DEBUG_WARN( " Can not claim handle\n");
			found_interface = false;
		}
		break;
	}
	if (!found_interface) {
		DEBUG_WARN( "No suitable interface found.");
		libusb_free_config_descriptor(config);
		return -1;
	}
	for (int i = 0; i < desc->bNumEndpoints; i++) {
		const struct libusb_endpoint_descriptor *epdesc = &desc->endpoint[i];
		if (epdesc->bEndpointAddress & LIBUSB_ENDPOINT_IN) {
			info->usb_link->ep_rx = epdesc->bEndpointAddress;
		} else {
			info->usb_link->ep_tx = epdesc->bEndpointAddress;
		}
	}
	libusb_free_config_descriptor(config);
	return 0;
}
/* Return 0 if single J-Link device connected or
 * serial given matches one of several J-Link devices.
 */
int jlink_init(bmp_info_t *info)
{
	usb_link_t *jl = calloc(1, sizeof(usb_link_t));
	if (!jl)
		return -1;
	info->usb_link = jl;
	jl->ul_libusb_ctx = info->libusb_ctx;
	int ret = -1;
	libusb_device **devs;
    if (libusb_get_device_list(info->libusb_ctx, &devs) < 0) {
        DEBUG_WARN( "libusb_get_device_list() failed");
		return ret;
	}
	int i = 0;
	for (;  devs[i]; i++) {
		libusb_device *dev =  devs[i];
		struct libusb_device_descriptor desc;
		if (libusb_get_device_descriptor(dev, &desc) < 0) {
            DEBUG_WARN( "libusb_get_device_descriptor() failed");
			goto error;;
		}
		if (desc.idVendor !=  USB_PID_SEGGER)
			continue;
		if ((desc.idProduct != USB_VID_SEGGER_0101) &&
			(desc.idProduct != USB_VID_SEGGER_0105) &&
			(desc.idProduct != USB_VID_SEGGER_1020))
			continue;
		int res = libusb_open(dev, &jl->ul_libusb_device_handle);
		if (res != LIBUSB_SUCCESS)
			continue;
		char buf[32];
		res = libusb_get_string_descriptor_ascii(jl->ul_libusb_device_handle,
			desc.iSerialNumber, (uint8_t*) buf, sizeof(buf));
		if ((res <= 0) || (!strstr(buf, info->serial))) {
			libusb_close(jl->ul_libusb_device_handle);
			continue;
		}
		break;
	}
	if (!devs[i])
		goto error;
	if (initialize_handle(info, devs[i]))
		goto error;
	jl->req_trans = libusb_alloc_transfer(0);
	jl->rep_trans = libusb_alloc_transfer(0);
	if (!jl->req_trans || !jl->rep_trans ||
		!jl->ep_tx || !jl->ep_rx) {
		DEBUG_WARN("Device setup failed\n");
		goto error;
	}
	libusb_free_device_list(devs, 1);
	jlink_info(info);
	return 0;
  error:
	libusb_free_device_list(devs, 1);
	return -1;

}

const char *jlink_target_voltage(bmp_info_t *info)
{
        uint8_t cmd[1] = {CMD_GET_HW_STATUS};
		uint8_t res[8];
        send_recv(info->usb_link, cmd, 1, res, sizeof(res));
        uint16_t mVolt = res[0] | (res[1] << 8);
        static char ret[7];
        sprintf(ret, "%2d.%03d", mVolt / 1000, mVolt % 1000);
        return ret;
}

static bool srst_status = false;
void jlink_srst_set_val(bmp_info_t *info, bool assert)
{
        uint8_t cmd[1];
        cmd[0]= (assert)? CMD_HW_RESET0: CMD_HW_RESET1;
        send_recv(info->usb_link, cmd, 1, NULL, 0);
        platform_delay(2);
        srst_status = assert;
}

bool jlink_srst_get_val(bmp_info_t *info) {
        uint8_t cmd[1] = {CMD_GET_HW_STATUS};
        uint8_t res[8];
        send_recv(info->usb_link, cmd, 1, res, sizeof(res));
        return !(res[6]);
}

void jlink_max_frequency_set(bmp_info_t *info, uint32_t freq)
{
	if (!(emu_caps &  JLINK_CAP_GET_SPEEDS))
		return;
	if (!info->is_jtag)
		return;
	uint16_t freq_kHz = freq /1000;
	uint16_t divisor = (emu_speed_kHz + freq_kHz - 1) / freq_kHz;
	if (divisor < emu_min_divisor)
		divisor = emu_min_divisor;
	emu_current_divisor = divisor;
	uint16_t speed_kHz = emu_speed_kHz / divisor;
	uint8_t cmd[3] = {CMD_SET_SPEED, speed_kHz & 0xff, speed_kHz >> 8};
	DEBUG_WARN("Set Speed %d\n", speed_kHz);
	send_recv(info->usb_link, cmd, 3, NULL, 0);
}

uint32_t jlink_max_frequency_get(bmp_info_t *info)
{
	if ((emu_caps & JLINK_CAP_GET_SPEEDS) && (info->is_jtag))
		return (emu_speed_kHz * 1000L)/ emu_current_divisor;
	return FREQ_FIXED;
}
