// SPDX-License-Identifier: Apache-2.0
/*
 * Copyright (C) 2023 Alexey Starikovskiy <aystarik@gmail.com>
 */

#define _DEFAULT_SOURCE

#include <libusb.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <cassert>
#include <iostream>
#include <map>
#include <string>
#include <vector>

#include "ch347jtag.hpp"
#include "display.hpp"

using namespace std;

#define CH347JTAG_VID 0x1a86
#define CH347T_JTAG_PID 0x55dd    //ch347T
#define CH347F_JTAG_PID 0x55de    //ch347F

#define KHZ(n) (uint32_t)((n)*UINT32_C(1000))
#define MHZ(n) (uint32_t)((n)*UINT32_C(1000000))
#define GHZ(n) (uint32_t)((n)*UINT32_C(1000000000))

#define CH347JTAG_INTF        2
#define CH347JTAG_WRITE_EP    0x06
#define CH347JTAG_READ_EP     0x86

#define CH347JTAG_TIMEOUT     200

enum CH347JtagCmd {
	CMD_BYTES_WO = 0xd3,
	CMD_BYTES_WR = 0xd4,
	CMD_BITS_WO  = 0xd1,
	CMD_BITS_WR  = 0xd2,
	CMD_CLK      = 0xd0,
};

enum CH347JtagSig {
	SIG_TCK =       0b1,
	SIG_TMS =      0b10,
	SIG_TDI =   0b10000,
};

// defer should only be used with rlen == 0

int CH347Jtag::usb_xfer(unsigned wlen, unsigned rlen, unsigned *ract, bool defer) 
{
	int actual_length = 0;
	if (_verbose) {
		fprintf(stderr, "usb_xfer: deferred: %ld\n", obuf - _obuf);
	}
	if (defer && !rlen && obuf - _obuf + wlen < (MAX_BUFFER - 12)) {
		obuf += wlen;
		return 0;
	}

	if (obuf - _obuf > MAX_BUFFER) {
		throw runtime_error("buffer overflow");
	}

	wlen += obuf - _obuf;
	if (wlen > MAX_BUFFER) {
		throw runtime_error("buffer overflow");
	}
	obuf = _obuf;

	if (wlen == 0) {
		return 0;
	}

	if (_verbose) {
		fprintf(stderr, "obuf[%d] = {", wlen);
		for (unsigned i = 0; i < wlen; ++i) {
			fprintf(stderr, "%02x ", obuf[i]);
		}
		fprintf(stderr, "}\n\n");
	}

	int r = 0;
	if (wlen) {
		if ((r = libusb_bulk_transfer(dev_handle, CH347JTAG_WRITE_EP, obuf, wlen, &actual_length, CH347JTAG_TIMEOUT)) < 0 ) {
			return r;
		}
	}
	if (_verbose) {
		fprintf(stderr, "obuf[%d] = {", wlen);
		for (unsigned i = 0; i < wlen; ++i) {
			fprintf(stderr, "%02x ", obuf[i]);
		}
		fprintf(stderr, "}\n\n");
	}
	obuf = _obuf;
	int rlen_total = 0;
	uint8_t *pibuf = ibuf;
	if (rlen){
		while (rlen) {
			if ((r = libusb_bulk_transfer(dev_handle, CH347JTAG_READ_EP, pibuf, rlen, &actual_length, CH347JTAG_TIMEOUT)) < 0 ) {
				return r;
			}
			if (_verbose) {
				fprintf(stderr, "ibuf[%d] = {", actual_length);
				for (int i = rlen_total; i < rlen_total + actual_length; ++i) {
					fprintf(stderr, "%02x ", ibuf[i]);
				}
				fprintf(stderr, "}\n\n");
			}
			rlen -= actual_length;
			pibuf += actual_length;
			rlen_total += actual_length;
		}
		*ract = rlen_total;
	}
	return 0;
}

int CH347Jtag::setClk(const uint8_t &factor) {
	// flush the obuf
	usb_xfer(0, 0, 0, false); // is called from constructor, don't replace with virtual flush()
	memset(obuf, 0, 16);
	obuf[0] = CMD_CLK;
	obuf[1] = 6;
	obuf[4] = factor;
	unsigned actual = 0;
	int rv = usb_xfer(9, 4, &actual, false);
	if (rv || actual != 4)
		return -1;
	if (ibuf[0] != 0xd0 || ibuf[3] != 0)
		return -1;
	return 0;
}

CH347Jtag::CH347Jtag(uint32_t clkHZ, int8_t verbose, int vid, int pid, uint8_t bus_addr, uint8_t dev_addr):
      _verbose(verbose>1), dev_handle(NULL), usb_ctx(NULL), obuf(_obuf)
{
	libusb_device** devs;
	int actual_length = 0;
	int i = 0;
	ssize_t cnt;
	struct libusb_device_descriptor desc;
	struct libusb_device *dev;
	int rv;
	if (libusb_init(&usb_ctx) < 0) {
		printError("libusb init failed");
		goto err_exit;
	}
	cnt = libusb_get_device_list(NULL, &devs);
	if (cnt < 0) goto err_exit;
	while ((dev = devs[i++]) != NULL)  {
		if (libusb_get_device_descriptor(dev, &desc) < 0)
			continue;
		if (desc.idVendor != vid || desc.idProduct != pid)
    		continue;
		if (bus_addr != 0 && dev_addr != 0 && (libusb_get_bus_number(dev) != bus_addr || libusb_get_device_address(dev) != dev_addr))
    		continue;
		libusb_open(dev, &dev_handle);
		break;
	}
	libusb_free_device_list(devs, 1);
	if (!dev_handle) {
		printError("fails to open device");
		goto usb_exit;
	}
	dev = libusb_get_device(dev_handle);
	if (!dev) {
		printError("Couldnt get bus number and address of device");
		goto usb_exit;
	}

	rv = libusb_get_device_descriptor(dev, &desc);
	if (rv < 0) {
		printError("failed to get device descriptor");
		goto usb_exit;
	}

	if (desc.bcdDevice < 0x241 && pid == CH347T_JTAG_PID) {
		_is_largerPack = false;
		printWarn("Old version of the chip, JTAG might not work");
	}else{
		_is_largerPack = true;
	}

	if (libusb_set_auto_detach_kernel_driver(dev_handle, true)) {
		printError("libusb error wrile setting auto-detach of kernel driver");
		goto usb_exit;
	}
	if (libusb_claim_interface(dev_handle, CH347JTAG_INTF)) {
		printError("libusb error while claiming CH347JTAG interface");
		goto usb_close;
	}
	rtrans = libusb_alloc_transfer(0);
	wtrans = libusb_alloc_transfer(0);
	if (!rtrans || !wtrans) {
		printError("libusb failed to alloc transfers");
		goto usb_release;
	}
	libusb_bulk_transfer(dev_handle, CH347JTAG_READ_EP, ibuf, 512,
		&actual_length, CH347JTAG_TIMEOUT);
	_setClkFreq(clkHZ);
	return;
usb_release:
	libusb_release_interface(dev_handle, CH347JTAG_INTF);
usb_close:
	libusb_close(dev_handle);
usb_exit:
	libusb_exit(usb_ctx);
err_exit:
	throw std::exception();
}

CH347Jtag::~CH347Jtag()
{
	if (rtrans) libusb_free_transfer(rtrans);
	if (wtrans) libusb_free_transfer(wtrans);

	if (dev_handle) {
		libusb_release_interface(dev_handle, CH347JTAG_INTF);
		libusb_close(dev_handle);
		dev_handle = 0;
	}
	if (usb_ctx) {
		libusb_exit(usb_ctx);
		usb_ctx = 0;
	}
}

int CH347Jtag::_setClkFreq(uint32_t clkHZ)
{
    int setClk_index = 0;
	uint32_t speed_clock_larger_pack[8] = {
		KHZ(468.75), KHZ(937.5), MHZ(1.875), MHZ(3.75),
		MHZ(7.5), MHZ(15), MHZ(30), MHZ(60)
	};
	uint32_t speed_clock_standard_pack[6] = {
		MHZ(1.875), MHZ(3.75),
		MHZ(7.5), MHZ(15), MHZ(30), MHZ(60)
	};
	uint32_t *ptr = _is_largerPack ? speed_clock_larger_pack : speed_clock_standard_pack;
	int size = (_is_largerPack?sizeof(speed_clock_larger_pack):sizeof(speed_clock_standard_pack)) / sizeof(uint32_t);
    for (int i = 0; i < size; ++i) {
		if (clkHZ > ptr[i] && clkHZ <= ptr[i+1]){
			setClk_index = i + 1;
		}
	}
	if (setClk(setClk_index)) {
		printError("failed to set clock rate");
		return 0;
	}
	char mess[256];
	snprintf(mess, 256, "JTAG TCK frequency set to %.3f MHz\n\n", (double)ptr[setClk_index] / MHZ(1));
	printInfo(mess);
	return _clkHZ;
}

int CH347Jtag::writeTMS(const uint8_t *tms, uint32_t len, bool flush_buffer,
		__attribute__((unused)) const uint8_t tdi)
{
	// if (get_obuf_length() < (int)(len * 2 + 4)) { // check if there is enough room left
	// 	flush();
	// }
 	flush();
	uint8_t *ptr = obuf;
	for (uint32_t i = 0; i < len; ++i) {
		if (ptr == obuf) {
			*ptr++ = CMD_BITS_WO;
			ptr += 2;  // leave place for length;
		}
		uint8_t x = ((tms[i >> 3] & (1 << (i & 7))) ? SIG_TMS : 0);
		*ptr++ = x;
		*ptr++ = x | SIG_TCK;
		int wlen = ptr - obuf;
		if (wlen + 1 >= get_obuf_length() || i == len - 1) {
			*ptr++ = x;  // clear TCK
			wlen = ptr - obuf;
			obuf[1] = wlen - 3;
			obuf[2] = (wlen - 3) >> 8;
			int ret = usb_xfer(wlen, 0, 0, !flush_buffer);
			if (ret < 0) {
				cerr << "writeTMS: usb bulk write failed: " <<
					libusb_strerror(static_cast<libusb_error>(ret)) << endl;
				return -EXIT_FAILURE;
			}
			ptr = obuf;
		}
	}
	return len;
}

int CH347Jtag::toggleClk(uint8_t tms, uint8_t tdi, uint32_t len)
{
	uint8_t bits = 0;
	if (tms) bits |= SIG_TMS;
	if (tdi) bits |= SIG_TDI;
	if (!bits && len > 7) {
		return writeTDI(0, 0, len, false);
	}
	
	if (get_obuf_length() < (int)(len * 2 + 4)) {
		flush();
	}

	uint8_t *ptr = obuf;
	for (uint32_t i = 0; i < len; ++i) {
		if (ptr == obuf) {
			*ptr++ = CMD_BITS_WO;
			ptr += 2;  // leave place for length;
		}
		*ptr++ = bits;
		*ptr++ = bits | SIG_TCK;
		int wlen = ptr - obuf;
		if (wlen + 1 >= get_obuf_length() || i == len - 1) {
			*ptr++ = bits;  // clear TCK
			wlen = ptr - obuf;
			obuf[1] = wlen - 3;
			obuf[2] = (wlen - 3) >> 8;
			int ret = usb_xfer(wlen, 0, 0, true);
			if (ret < 0) {
				cerr << "writeCLK: usb bulk write failed: " <<
					libusb_strerror(static_cast<libusb_error>(ret)) << endl;
				return -EXIT_FAILURE;
			}
			ptr = obuf;
		}
	}
	return EXIT_SUCCESS;
}

int CH347Jtag::writeTDI(const uint8_t *tx, uint8_t *rx, uint32_t len, bool end)
{
	if (len == 0)
		return 0;
	unsigned bytes = (len - (end ? 1 : 0)) / 8;
	unsigned bits = len - bytes * 8;
	uint8_t *rptr = rx;
	const uint8_t *tptr = tx;
	const uint8_t *txend = tx + bytes;
	uint8_t cmd = (rx != nullptr) ? CMD_BYTES_WR : CMD_BYTES_WO;
	while (tptr < txend) {
		if (get_obuf_length() < 4) {
			flush();
		}
		int avail = get_obuf_length() - 3;
		int chunk = (txend - tptr < avail)? txend - tptr: avail;
		if (tx) {
			memcpy(&obuf[3], tptr, chunk);
		} else {
			memset(&obuf[3], 0, chunk);
		}
		tptr += chunk;
		// write header
		obuf[0] = cmd;
		obuf[1] = chunk;
		obuf[2] = chunk >> 8;
		unsigned actual_length = 0;
		int ret = usb_xfer(chunk + 3, (rx) ? chunk + 3 : 0, &actual_length, rx == 0 && get_obuf_length());
		if (ret < 0) {
			cerr << "writeTDI: usb bulk read failed: " <<
				libusb_strerror(static_cast<libusb_error>(ret)) << endl;
			return -EXIT_FAILURE;
		}
		if (!rx)
			continue;
		unsigned size = ibuf[1] + ibuf[2] * 0x100;
		if (ibuf[0] != CMD_BYTES_WR || actual_length - 3 != size) {
			cerr << "writeTDI: invalid read data: " << ret << endl;
			return -EXIT_FAILURE;
		}
		memcpy(rptr, &ibuf[3], size);
		rptr += size;
	}
	unsigned actual_length;
	if (bits == 0)
		return EXIT_SUCCESS;
	cmd = (rx) ? CMD_BITS_WR : CMD_BITS_WO;
	if (get_obuf_length() < (int)(4 + bits * 2)) {
		flush();
	}
	uint8_t *ptr = &obuf[3];
	uint8_t x = 0;
	const uint8_t *bptr = tx + bytes;
	for (unsigned i = 0; i < bits; ++i) {
		uint8_t txb = (tx) ? bptr[i >> 3] : 0;
		uint8_t _tdi = (txb & (1 << (i & 7))) ? SIG_TDI : 0;
		x = _tdi;
		if (end && i == bits - 1) {
			x |= SIG_TMS;
		}
		*ptr++ = x;
		*ptr++ = x | SIG_TCK;
	}
	*ptr++ = x & ~SIG_TCK;
	unsigned wlen = ptr - obuf;
	obuf[0] = cmd;
	obuf[1] = wlen - 3;
	obuf[2] = (wlen - 3) >> 8;
	int ret = usb_xfer(wlen, (rx) ? (bits + 3) : 0, &actual_length, rx == nullptr);

	if (ret < 0) {
		cerr << "writeTDI: usb bulk read failed: " <<
			libusb_strerror(static_cast<libusb_error>(ret)) << endl;
		return -EXIT_FAILURE;
	}
	if (!rx)
		return EXIT_SUCCESS;

	unsigned size = ibuf[1] + ibuf[2] * 0x100;

	if (ibuf[0] != CMD_BITS_WR || actual_length - 3 != size) {
		cerr << "writeTDI: invalid read data: " << endl;
		return -EXIT_FAILURE;
	}
	for (unsigned i = 0; i < size; ++i) {
		if (ibuf[3 + i] == 0x01) {
			*rptr |= (0x01 << i);
		}else{
			*rptr &= ~(0x01 << i);
		}	
	}
	return EXIT_SUCCESS;
}
