/*
 * Copyright 2012 Luke Dashjr
 *
 * 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.  See COPYING for more details.
 */

#include "config.h"

#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>

#include <libusb.h>

#include "compat.h"
#include "fpgautils.h"
#include "ft232r.h"
#include "logging.h"
#include "miner.h"

#define FT232R_IDVENDOR   0x0403
#define FT232R_IDPRODUCT  0x6001

struct ft232r_device_info {
	libusb_device *libusb_dev;
	char *product;
	char *serial;
};

static struct ft232r_device_info **ft232r_devinfo_list;

void ft232r_scan_free()
{
	if (!ft232r_devinfo_list)
		return;

	struct ft232r_device_info *info;

	for (struct ft232r_device_info **infop = ft232r_devinfo_list; (info = *infop); ++infop) {
		libusb_unref_device(info->libusb_dev);
		free(info->product);
		free(info->serial);
		free(info);
	}
	free(ft232r_devinfo_list);
	ft232r_devinfo_list = NULL;
}

void ft232r_scan()
{
	ssize_t count, n, i, found = 0;
	libusb_device **list;
	struct libusb_device_descriptor desc;
	libusb_device_handle *handle;
	struct ft232r_device_info *info;
	int err;
	unsigned char buf[0x100];

	ft232r_scan_free();

	count = libusb_get_device_list(NULL, &list);
	if (unlikely(count < 0)) {
		applog(LOG_ERR, "ft232r_scan: Error getting USB device list: %s", libusb_error_name(count));
		ft232r_devinfo_list = calloc(1, sizeof(struct ft232r_device_info *));
		return;
	}

	ft232r_devinfo_list = malloc(sizeof(struct ft232r_device_info *) * (count + 1));

	for (i = 0; i < count; ++i) {
		err = libusb_get_device_descriptor(list[i], &desc);
		if (unlikely(err)) {
			applog(LOG_ERR, "ft232r_scan: Error getting device descriptor: %s", libusb_error_name(err));
			continue;
		}
		if (!(desc.idVendor == FT232R_IDVENDOR && desc.idProduct == FT232R_IDPRODUCT)) {
			applog(LOG_DEBUG, "ft232r_scan: Found %04x:%04x - not a ft232r", desc.idVendor, desc.idProduct);
			continue;
		}

		err = libusb_open(list[i], &handle);
		if (unlikely(err)) {
			applog(LOG_ERR, "ft232r_scan: Error opening device: %s", libusb_error_name(err));
			continue;
		}

		n = libusb_get_string_descriptor_ascii(handle, desc.iProduct, buf, sizeof(buf)-1);
		if (unlikely(n < 0)) {
			libusb_close(handle);
			applog(LOG_ERR, "ft232r_scan: Error getting iProduct string: %s", libusb_error_name(n));
			continue;
		}
		buf[n] = '\0';
		info = malloc(sizeof(struct ft232r_device_info));
		info->product = strdup((char*)buf);

		n = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, buf, sizeof(buf)-1);
		libusb_close(handle);
		if (unlikely(n < 0)) {
			applog(LOG_ERR, "ft232r_scan: Error getting iSerialNumber string: %s", libusb_error_name(n));
			n = 0;
		}
		buf[n] = '\0';
		info->serial = strdup((char*)buf);
		info->libusb_dev = libusb_ref_device(list[i]);
		ft232r_devinfo_list[found++] = info;

		applog(LOG_DEBUG, "ft232r_scan: Found \"%s\" serial \"%s\"", info->product, info->serial);
	}

	ft232r_devinfo_list[found] = NULL;
	libusb_free_device_list(list, 1);
}

int ft232r_detect(const char *product_needle, const char *serial, foundusb_func_t cb)
{
	struct ft232r_device_info *info;
	int found = 0;

	for (struct ft232r_device_info **infop = ft232r_devinfo_list; (info = *infop); ++infop) {
		if (serial) {
			// If we are searching for a specific serial, pay no attention to the product id
			if (strcmp(serial, info->serial))
				continue;
		}
		else
		if (!strstr(info->product, product_needle))
			continue;
		if (!info->libusb_dev)
			continue;
		if (!cb(info->libusb_dev, info->product, info->serial))
			continue;
		info->libusb_dev = NULL;
		++found;
	}

	return found;
}

#define FTDI_REQTYPE  (LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE)
#define FTDI_REQTYPE_IN   (FTDI_REQTYPE | LIBUSB_ENDPOINT_IN)
#define FTDI_REQTYPE_OUT  (FTDI_REQTYPE | LIBUSB_ENDPOINT_OUT)

#define FTDI_REQUEST_RESET           0
#define FTDI_REQUEST_SET_BAUDRATE    3
#define FTDI_REQUEST_SET_BITMODE  0x0b
#define FTDI_REQUEST_GET_PINS     0x0c
#define FTDI_REQUEST_GET_BITMODE  0x0c

#define FTDI_BAUDRATE_3M  0,0

#define FTDI_INDEX       1
#define FTDI_TIMEOUT  1000

struct ft232r_device_handle {
	libusb_device_handle *h;
	uint8_t i;
	uint8_t o;
	unsigned char ibuf[256];
	int ibufLen;
	uint16_t osz;
	unsigned char *obuf;
	uint16_t obufsz;
};

struct ft232r_device_handle *ft232r_open(libusb_device *dev)
{
	// FIXME: Cleanup on errors
	libusb_device_handle *devh;
	struct ft232r_device_handle *ftdi;

	if (libusb_open(dev, &devh)) {
		applog(LOG_ERR, "ft232r_open: Error opening device");
		return NULL;
	}
	libusb_reset_device(devh);
	libusb_detach_kernel_driver(devh, 0);
	if (libusb_set_configuration(devh, 1)) {
		applog(LOG_ERR, "ft232r_open: Error setting configuration");
		return NULL;
	}
	if (libusb_claim_interface(devh, 0)) {
		applog(LOG_ERR, "ft232r_open: Error claiming interface");
		return NULL;
	}
	if (libusb_control_transfer(devh, FTDI_REQTYPE_OUT, FTDI_REQUEST_SET_BAUDRATE, FTDI_BAUDRATE_3M, NULL, 0, FTDI_TIMEOUT) < 0) {
		applog(LOG_ERR, "ft232r_open: Error performing control transfer");
		return NULL;
	}

	ftdi = calloc(1, sizeof(*ftdi));
	ftdi->h = devh;

	struct libusb_config_descriptor *cfg;
	if (libusb_get_config_descriptor(dev, 0, &cfg)) {
		applog(LOG_ERR, "ft232r_open: Error getting config descriptor");
		return NULL;
	}
	const struct libusb_interface_descriptor *altcfg = &cfg->interface[0].altsetting[0];
	if (altcfg->bNumEndpoints < 2) {
		applog(LOG_ERR, "ft232r_open: Too few endpoints");
		return NULL;
	}
	ftdi->i = altcfg->endpoint[0].bEndpointAddress;
	ftdi->o = altcfg->endpoint[1].bEndpointAddress;
	ftdi->osz = 0x1000;
	ftdi->obuf = malloc(ftdi->osz);
	libusb_free_config_descriptor(cfg);

	return ftdi;
}

void ft232r_close(struct ft232r_device_handle *dev)
{
	libusb_release_interface(dev->h, 0);
	libusb_reset_device(dev->h);
	libusb_close(dev->h);
}

bool ft232r_purge_buffers(struct ft232r_device_handle *dev, enum ft232r_reset_purge purge)
{
	if (ft232r_flush(dev) < 0)
		return false;

	if (purge & FTDI_PURGE_RX) {
		if (libusb_control_transfer(dev->h, FTDI_REQTYPE_OUT, FTDI_REQUEST_RESET, FTDI_PURGE_RX, FTDI_INDEX, NULL, 0, FTDI_TIMEOUT))
			return false;
		dev->ibufLen = 0;
	}
	if (purge & FTDI_PURGE_TX)
		if (libusb_control_transfer(dev->h, FTDI_REQTYPE_OUT, FTDI_REQUEST_RESET, FTDI_PURGE_TX, FTDI_INDEX, NULL, 0, FTDI_TIMEOUT))
			return false;
	return true;
}

bool ft232r_set_bitmode(struct ft232r_device_handle *dev, uint8_t mask, uint8_t mode)
{
	if (ft232r_flush(dev) < 0)
		return false;
	if (libusb_control_transfer(dev->h, FTDI_REQTYPE_OUT, FTDI_REQUEST_SET_BITMODE, mask, FTDI_INDEX, NULL, 0, FTDI_TIMEOUT))
		return false;
	return !libusb_control_transfer(dev->h, FTDI_REQTYPE_OUT, FTDI_REQUEST_SET_BITMODE, (mode << 8) | mask, FTDI_INDEX, NULL, 0, FTDI_TIMEOUT);
}

static ssize_t ft232r_readwrite(struct ft232r_device_handle *dev, unsigned char endpoint, void *data, size_t count)
{
	int transferred;
	switch (libusb_bulk_transfer(dev->h, endpoint, data, count, &transferred, FTDI_TIMEOUT)) {
		case LIBUSB_ERROR_TIMEOUT:
			if (!transferred) {
				errno = ETIMEDOUT;
				return -1;
			}
		case 0:
			return transferred;
		default:
			errno = EIO;
			return -1;
	}
}

ssize_t ft232r_flush(struct ft232r_device_handle *dev)
{
	if (!dev->obufsz)
		return 0;
	ssize_t r = ft232r_readwrite(dev, dev->o, dev->obuf, dev->obufsz);
	if (r == dev->obufsz) {
		dev->obufsz = 0;
	} else if (r > 0) {
		dev->obufsz -= r;
		memmove(dev->obuf, &dev->obuf[r], dev->obufsz);
	}
	return r;
}

ssize_t ft232r_write(struct ft232r_device_handle *dev, void *data, size_t count)
{
	uint16_t bufleft;
	ssize_t r;
	
	bufleft = dev->osz - dev->obufsz;
	
	if (count < bufleft) {
		// Just add to output buffer
		memcpy(&dev->obuf[dev->obufsz], data, count);
		dev->obufsz += count;
		return count;
	}
	
	// Fill up buffer and flush
	memcpy(&dev->obuf[dev->obufsz], data, bufleft);
	dev->obufsz += bufleft;
	r = ft232r_flush(dev);
	
	if (unlikely(r <= 0)) {
		// In this case, no bytes were written supposedly, so remove this data from buffer
		dev->obufsz -= bufleft;
		return r;
	}
	
	// Even if not all <bufleft> bytes from this write got out, the remaining are still buffered
	return bufleft;
}

typedef ssize_t (*ft232r_rwfunc_t)(struct ft232r_device_handle *, void*, size_t);

static ssize_t ft232r_rw_all(ft232r_rwfunc_t rwfunc, struct ft232r_device_handle *dev, void *data, size_t count)
{
	char *p = data;
	ssize_t writ, total = 0;

	while (count && (writ = rwfunc(dev, p, count)) > 0) {
		p += writ;
		count -= writ;
		total += writ;
	}
	return total ?: writ;
}

ssize_t ft232r_write_all(struct ft232r_device_handle *dev, void *data, size_t count)
{
	return ft232r_rw_all(ft232r_write, dev, data, count);
}

ssize_t ft232r_read(struct ft232r_device_handle *dev, void *data, size_t count)
{
	ssize_t r;
	int adj;
	
	// Flush any pending output before reading
	r = ft232r_flush(dev);
	if (r < 0)
		return r;
	
	// First 2 bytes of every 0x40 are FTDI status or something
	while (dev->ibufLen <= 2) {
		// TODO: Implement a timeout for status byte repeating
		int transferred = ft232r_readwrite(dev, dev->i, dev->ibuf, sizeof(dev->ibuf));
		if (transferred <= 0)
			return transferred;
		dev->ibufLen = transferred;
		for (adj = 0x40; dev->ibufLen > adj; adj += 0x40 - 2) {
			dev->ibufLen -= 2;
			memmove(&dev->ibuf[adj], &dev->ibuf[adj+2], dev->ibufLen - adj);
		}
	}
	unsigned char *ibufs = &dev->ibuf[2];
	size_t ibufsLen = dev->ibufLen - 2;
	
	if (count > ibufsLen)
		count = ibufsLen;
	memcpy(data, ibufs, count);
	dev->ibufLen -= count;
	ibufsLen -= count;
	if (ibufsLen) {
		memmove(ibufs, &ibufs[count], ibufsLen);
		applog(LOG_DEBUG, "ft232r_read: %u bytes extra", ibufsLen);
	}
	return count;
}

ssize_t ft232r_read_all(struct ft232r_device_handle *dev, void *data, size_t count)
{
	return ft232r_rw_all(ft232r_read, dev, data, count);
}

bool ft232r_get_pins(struct ft232r_device_handle *dev, uint8_t *pins)
{
	return libusb_control_transfer(dev->h, FTDI_REQTYPE_IN, FTDI_REQUEST_GET_PINS, 0, FTDI_INDEX, pins, 1, FTDI_TIMEOUT) == 1;
}

bool ft232r_get_bitmode(struct ft232r_device_handle *dev, uint8_t *out_mode)
{
	return libusb_control_transfer(dev->h, FTDI_REQTYPE_IN, FTDI_REQUEST_GET_BITMODE, 0, FTDI_INDEX, out_mode, 1, FTDI_TIMEOUT) == 1;
}

bool ft232r_set_cbus_bits(struct ft232r_device_handle *dev, bool sc, bool cs)
{
	uint8_t pin_state = (cs ? (1<<2) : 0)
	                  | (sc ? (1<<3) : 0);
	return ft232r_set_bitmode(dev, 0xc0 | pin_state, 0x20);
}

bool ft232r_get_cbus_bits(struct ft232r_device_handle *dev, bool *out_sio0, bool *out_sio1)
{
	uint8_t data;
	if (!ft232r_get_bitmode(dev, &data))
		return false;
	*out_sio0 = data & 1;
	*out_sio1 = data & 2;
	return true;
}

#if 0
int main() {
	libusb_init(NULL);
	ft232r_scan();
	ft232r_scan_free();
	libusb_exit(NULL);
}
void applog(int prio, const char *fmt, ...)
{
	va_list ap;

	va_start(ap, fmt);
	vprintf(fmt, ap);
	puts("");
	va_end(ap);
}
#endif
