#include "main.h"
#include "bcmxcp.h"
#include "bcmxcp_io.h"
#include "common.h"
#include "usb-common.h"
#include "timehead.h"
#include "nut_stdint.h" /* for uint16_t */
#include <ctype.h>
#include <sys/file.h>
#include <sys/types.h>
#include <unistd.h>

#define SUBDRIVER_NAME    "USB communication subdriver"
#define SUBDRIVER_VERSION "0.28"

/* communication driver description structure */
upsdrv_info_t comm_upsdrv_info = {
	SUBDRIVER_NAME,
	SUBDRIVER_VERSION,
	NULL,
	0,
	{ NULL }
};

#define MAX_TRY 4
#define MAX_TRY_OPENUSB 32

/* Powerware */
#define POWERWARE_VENDORID	0x0592

/* Phoenixtec Power Co., Ltd */
#define PHOENIXTEC_VENDORID	0x06da

/* Hewlett Packard */
#define HP_VENDORID	0x03f0

static USBDevice_t curDevice;

/* USB functions */
usb_dev_handle *nutusb_open(const char *port);
int nutusb_close(usb_dev_handle *dev_h, const char *port);
/* unified failure reporting: call these often */
void nutusb_comm_fail(const char *fmt, ...)
	__attribute__ ((__format__ (__printf__, 1, 2)));
void nutusb_comm_good(void);
/* function pointer, set depending on which device is used */
/* FIXME? Use usb_ctrl_* typedefs*/
static int (*usb_set_descriptor)(usb_dev_handle *udev, unsigned char type,
	unsigned char index, void *buf, size_t size);

/* usb_set_descriptor() for Powerware devices */
/* FIXME? Use usb_ctrl_* typedefs*/
static int usb_set_powerware(usb_dev_handle *udev, unsigned char type, unsigned char index, void *buf, size_t size)
{
	assert (size < INT_MAX);
	return usb_control_msg(udev, USB_ENDPOINT_OUT, USB_REQ_SET_DESCRIPTOR, (type << 8) + index, 0, (usb_ctrl_charbuf)buf, (int)size, 1000);
}

static void *powerware_ups(USBDevice_t *device) {
	NUT_UNUSED_VARIABLE(device);
	usb_set_descriptor = &usb_set_powerware;
	return NULL;
}

/* usb_set_descriptor() for Phoenixtec devices */
/* FIXME? Use usb_ctrl_* typedefs*/
static int usb_set_phoenixtec(usb_dev_handle *udev, unsigned char type, unsigned char index, void *buf, size_t size)
{
	NUT_UNUSED_VARIABLE(index);
	NUT_UNUSED_VARIABLE(type);
	assert (size < INT_MAX);
	return usb_control_msg(udev, 0x42, 0x0d, (0x00 << 8) + 0x0, 0, (usb_ctrl_charbuf)buf, (int)size, 1000);
}

static void *phoenixtec_ups(USBDevice_t *device) {
	NUT_UNUSED_VARIABLE(device);
	usb_set_descriptor = &usb_set_phoenixtec;
	return NULL;
}

/* USB IDs device table */
static usb_device_id_t pw_usb_device_table[] = {
	/* various models */
	{ USB_DEVICE(POWERWARE_VENDORID, 0x0002), &powerware_ups },

	/* various models */
	{ USB_DEVICE(PHOENIXTEC_VENDORID, 0x0002), &phoenixtec_ups },

	/* T500 */
	{ USB_DEVICE(HP_VENDORID, 0x1f01), &phoenixtec_ups },
	/* T750 */
	{ USB_DEVICE(HP_VENDORID, 0x1f02), &phoenixtec_ups },

	/* Terminating entry */
	{ 0, 0, NULL }
};

/* limit the amount of spew that goes in the syslog when we lose the UPS */
#define USB_ERR_LIMIT 10 /* start limiting after 10 in a row  */
#define USB_ERR_RATE 10  /* then only print every 10th error */
#define XCP_USB_TIMEOUT 5000 /* in msec */

/* global variables */
static usb_dev_handle *upsdev = NULL;
extern int exit_flag;
static unsigned int comm_failures = 0;

/* Functions implementations */
void send_read_command(unsigned char command)
{
	unsigned char buf[4];

	if (upsdev) {
		buf[0] = PW_COMMAND_START_BYTE;
		buf[1] = 0x01;                    /* data length */
		buf[2] = command;                 /* command to send */
		buf[3] = calc_checksum(buf);      /* checksum */
		upsdebug_hex (3, "send_read_command", buf, 4);
		usb_set_descriptor(upsdev, USB_DT_STRING, 4, buf, 4); /* FIXME: Ignore error */
	}
}

void send_write_command(unsigned char *command, size_t command_length)
{
	unsigned char sbuf[128];

	if (upsdev) {
		/* Prepare the send buffer */
		sbuf[0] = PW_COMMAND_START_BYTE;
		sbuf[1] = (unsigned char)(command_length);
		memcpy(sbuf+2, command, command_length);
		command_length += 2;

		/* Add checksum */
		sbuf[command_length] = calc_checksum(sbuf);
		command_length += 1;
		upsdebug_hex (3, "send_write_command", sbuf, command_length);
		usb_set_descriptor(upsdev, USB_DT_STRING, 4, sbuf, command_length);  /* FIXME: Ignore error */
	}
}

#define PW_HEADER_SIZE (PW_HEADER_LENGTH + 1)
#define PW_CMD_BUFSIZE	256
/* get the answer of a command from the ups. And check that the answer is for this command */
ssize_t get_answer(unsigned char *data, unsigned char command)
{
	unsigned char buf[PW_CMD_BUFSIZE], *my_buf = buf;
	ssize_t res;
	int endblock, need_data;
	long elapsed_time; /* milliseconds */
	ssize_t tail;
	size_t bytes_read, end_length, length;
	unsigned char block_number, sequence, seq_num;
	struct timeval start_time, now;

	if (upsdev == NULL)
		return -1;

	need_data = PW_HEADER_SIZE; /* 4 - cmd response header length, 1 for csum */
	end_length = 0;    /* total length of sequence(s), not counting header(s) */
	endblock = 0;      /* signal the last sequence in the block */
	bytes_read = 0;    /* total length of data read, including XCP header */
	res = 0;
	elapsed_time = 0;
	seq_num = 1;       /* current theoric sequence */

	upsdebugx(1, "entering get_answer(%x)", command);

	/* Store current time */
	gettimeofday(&start_time, NULL);
	memset(&buf, 0x0, PW_CMD_BUFSIZE);

#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE
#pragma GCC diagnostic push
#endif
#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE
#pragma GCC diagnostic ignored "-Wunreachable-code"
#endif
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code"
#endif
	/* Stay ahead of possible redefinitions... */
	assert (XCP_USB_TIMEOUT < INT_MAX);
#ifdef __clang__
#pragma clang diagnostic pop
#endif
#ifdef HAVE_PRAGMAS_FOR_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE
#pragma GCC diagnostic pop
#endif

	while ( (!endblock) && ((XCP_USB_TIMEOUT - elapsed_time)  > 0) ) {

		/* Get (more) data if needed */
		if (need_data > 0) {
			res = usb_interrupt_read(upsdev,
				0x81,
				(usb_ctrl_charbuf) buf + bytes_read,
				128,
				(int)(XCP_USB_TIMEOUT - elapsed_time));

			/* Update time */
			gettimeofday(&now, NULL);
			elapsed_time = (now.tv_sec - start_time.tv_sec)*1000 +
					(now.tv_usec - start_time.tv_usec)/1000;

			/* Check libusb return value */
			if (res < 0)
			{
				/* Clear any possible endpoint stalls */
				usb_clear_halt(upsdev, 0x81);
				/* continue; */ /* FIXME: seems a break would be better! */
				break;
			}

			/* this seems to occur on XSlot USB card */
			if (res == 0)
			{
				/* FIXME: */
				continue;
			}
			/* Else, we got some input bytes */
			bytes_read += (size_t)res;
			need_data -= res;
			upsdebug_hex(1, "get_answer", buf, bytes_read);
		}

		if (need_data > 0) /* We need more data */
			continue;

		/* Now validate XCP frame */
		/* Check header */
		if ( my_buf[0] != PW_COMMAND_START_BYTE ) {
			upsdebugx(2, "get_answer: wrong header 0xab vs. %02x", my_buf[0]);
			/* Sometime we read something wrong. bad cables? bad ports? */
			my_buf = (unsigned char*)memchr(my_buf, PW_COMMAND_START_BYTE, bytes_read);
			if (!my_buf)
				return -1;
		}

		/* Read block number byte */
		block_number = my_buf[1];
		upsdebugx(1, "get_answer: block_number = %x", block_number);

		/* Check data length byte (remove the header length) */
		length = my_buf[2];
		upsdebugx(3, "get_answer: data length = %" PRIuSIZE, length);
		if (bytes_read < (length + PW_HEADER_SIZE)) {
			if (need_data < 0) --need_data; /* count zerro byte too */
			need_data += length + 1; /* packet lenght + checksum */
			upsdebugx(2, "get_answer: need to read %d more data", need_data);
			continue;
		}
		/* Check if Length conforms to XCP (121 for normal, 140 for Test mode) */
		/* Use the more generous length for testing */
		if (length > 140) {
			upsdebugx(2, "get_answer: bad length");
			return -1;
		}

		if (bytes_read >= SSIZE_MAX) {
			upsdebugx(2, "get_answer: bad length (incredibly large read)");
			return -1;
		}

		/* Test the Sequence # */
		sequence = my_buf[3];
		if ((sequence & PW_SEQ_MASK) != seq_num) {
			nutusb_comm_fail("get_answer: not the right sequence received %x!!!\n", (unsigned int)(sequence & PW_SEQ_MASK));
			return -1;
		}
		else {
			upsdebugx(2, "get_answer: sequence number (%x) is ok", (unsigned int)(sequence & PW_SEQ_MASK));
		}

		/* Validate checksum */
		if (!checksum_test(my_buf)) {
			nutusb_comm_fail("get_answer: checksum error! ");
			return -1;
		}
		else {
			upsdebugx(2, "get_answer: checksum is ok");
		}

		/* Check if it's the last sequence */
		if (sequence & PW_LAST_SEQ) {
			/* we're done receiving data */
			upsdebugx(2, "get_answer: all data received");
			endblock = 1;
		}
		else {
			seq_num++;
			upsdebugx(2, "get_answer: next sequence is %d", seq_num);
		}

		/* copy the current valid XCP frame back */
		memcpy(data+end_length, my_buf + 4, length);
		/* increment pointers to process the next sequence */
		end_length += length;

		/* Work around signedness of comparison result, SSIZE_MAX checked above: */
		tail = (ssize_t)bytes_read;
		tail -= (ssize_t)(length + PW_HEADER_SIZE);
		if (tail > 0)
			my_buf = (unsigned char*)memmove(&buf[0], my_buf + length + PW_HEADER_SIZE, (size_t)tail);
		else if (tail == 0)
			my_buf = &buf[0];
		else { /* if (tail < 0) */
			upsdebugx(1, "get_answer(): did not expect to get negative tail size: %" PRIiSIZE, tail);
			return -1;
		}

		bytes_read = (size_t)tail;
	}

	upsdebug_hex (5, "get_answer", data, end_length);
	assert (end_length < SSIZE_MAX);
	return (ssize_t)end_length;
}

/* Sends a single command (length=1). and get the answer */
ssize_t command_read_sequence(unsigned char command, unsigned char *data)
{
	ssize_t bytes_read = 0;
	size_t retry = 0;

	while ((bytes_read < 1) && (retry < 5)) {
		send_read_command(command);
		bytes_read = get_answer(data, command);
		retry++;
	}

	if (bytes_read < 1) {
		nutusb_comm_fail("Error executing command");
		dstate_datastale();
		return -1;
	}

	return bytes_read;
}

/* Sends a setup command (length > 1) */
ssize_t command_write_sequence(unsigned char *command, size_t command_length, unsigned char *answer)
{
	ssize_t bytes_read = 0;
	size_t retry = 0;

	while ((bytes_read < 1) && (retry < 5)) {
		send_write_command(command, command_length);
		sleep(PW_SLEEP);
		bytes_read = get_answer(answer, command[0]);
		retry ++;
	}

	if (bytes_read < 1) {
		nutusb_comm_fail("Error executing command");
		dstate_datastale();
		return -1;
	}

	return bytes_read;
}


void upsdrv_comm_good(void)
{
	nutusb_comm_good();
}

void upsdrv_initups(void)
{
	upsdev = nutusb_open("USB");
}

void upsdrv_cleanup(void)
{
	upslogx(LOG_ERR, "CLOSING\n");
	nutusb_close(upsdev, "USB");
	free(curDevice.Vendor);
	free(curDevice.Product);
	free(curDevice.Serial);
	free(curDevice.Bus);
	free(curDevice.Device);
}

void upsdrv_reconnect(void)
{
	dstate_setinfo("driver.state", "reconnect.trying");

	upsdebugx(4, "==================================================");
	upsdebugx(4, "= device has been disconnected, try to reconnect =");
	upsdebugx(4, "==================================================");

	nutusb_close(upsdev, "USB");
	upsdev = NULL;

	upsdrv_initups();

	dstate_setinfo("driver.state", "quiet");
}

/* USB functions */
static void nutusb_open_error(const char *port)
	__attribute__((noreturn));

static void nutusb_open_error(const char *port)
{
	printf("Unable to find POWERWARE UPS device on USB bus (%s)\n\n", port);

	printf("Things to try:\n\n");
	printf(" - Connect UPS device to USB bus\n\n");
	printf(" - Run this driver as another user (upsdrvctl -u or 'user=...' in ups.conf).\n");
	printf("   See upsdrvctl(8) and ups.conf(5).\n\n");

	fatalx(EXIT_FAILURE, "Fatal error: unusable configuration");
}

/* FIXME: this part of the opening can go into common... */
static usb_dev_handle *open_powerware_usb(void)
{
#if WITH_LIBUSB_1_0
	libusb_device **devlist;
	ssize_t devcount = 0;
	libusb_device_handle *udev;
	struct libusb_device_descriptor dev_desc;
	uint8_t bus_num;
	/* TODO: consider device_addr */
	int i;

	devcount = libusb_get_device_list(NULL, &devlist);
	if (devcount <= 0)
		fatal_with_errno(EXIT_FAILURE, "No USB device found");

	for (i = 0; i < devcount; i++) {

		libusb_device *device = devlist[i];
		libusb_get_device_descriptor(device, &dev_desc);

		if (dev_desc.bDeviceClass != LIBUSB_CLASS_PER_INTERFACE) {
			continue;
		}

		curDevice.VendorID = dev_desc.idVendor;
		curDevice.ProductID = dev_desc.idProduct;
		bus_num = libusb_get_bus_number(device);
		curDevice.Bus = (char *)malloc(4);
		if (curDevice.Bus == NULL) {
			libusb_free_device_list(devlist, 1);
			fatal_with_errno(EXIT_FAILURE, "Out of memory");
		}
		snprintf(curDevice.Bus, 4, "%03d", bus_num);

		/* FIXME: we should also retrieve
		 * dev->descriptor.iManufacturer
		 * dev->descriptor.iProduct
		 * dev->descriptor.iSerialNumber
		 * as in libusb.c->libusb_open()
		 * This is part of the things to put in common... */

		if (is_usb_device_supported(pw_usb_device_table, &curDevice) == SUPPORTED) {
			libusb_open(device, &udev);
			libusb_free_device_list(devlist, 1);
			return udev;
		}
	}
	libusb_free_device_list(devlist, 1);
#else /* not WITH_LIBUSB_1_0 */
	struct usb_bus *busses = usb_get_busses();
	struct usb_bus *bus;

	for (bus = busses; bus; bus = bus->next)
	{
		struct usb_device *dev;

		for (dev = bus->devices; dev; dev = dev->next)
		{
			if (dev->descriptor.bDeviceClass != USB_CLASS_PER_INTERFACE) {
				continue;
			}

			curDevice.VendorID = dev->descriptor.idVendor;
			curDevice.ProductID = dev->descriptor.idProduct;
			curDevice.Bus = xstrdup(bus->dirname);

			/* FIXME: we should also retrieve
			 * dev->descriptor.iManufacturer
			 * dev->descriptor.iProduct
			 * dev->descriptor.iSerialNumber
			 * as in libusb.c->libusb_open()
			 * This is part of the things to put in common... */

			if (is_usb_device_supported(pw_usb_device_table, &curDevice) == SUPPORTED) {
				return usb_open(dev);
			}
		}
	}
#endif /* WITH_LIBUSB_1_0 */
	return 0;
}

usb_dev_handle *nutusb_open(const char *port)
{
	int            dev_claimed = 0;
	usb_dev_handle *dev_h = NULL;
	int            retry, errout = 0;
	int            ret = 0;

	upsdebugx(1, "entering nutusb_open()");
	warn_if_bad_usb_port_filename(device_path);

	/* Initialize Libusb */
#if WITH_LIBUSB_1_0
	if (libusb_init(NULL) < 0) {
		libusb_exit(NULL);
		fatal_with_errno(EXIT_FAILURE, "Failed to init libusb 1.0");
	}
#else /* not WITH_LIBUSB_1_0 */
	usb_init();
	usb_find_busses();
	usb_find_devices();
#endif /* WITH_LIBUSB_1_0 */

	/* for (retry = 0; dev_h == NULL && retry < MAX_TRY_OPENUSB; retry++) */
	for (retry = 0; retry < MAX_TRY ; retry++)
	{
		dev_h = open_powerware_usb();
		if (!dev_h) {
			upsdebugx(1, "Can't open POWERWARE USB device");
			errout = 1;
		}
		else {
			upsdebugx(1, "device %s opened successfully", curDevice.Bus);
			errout = 0;

#ifdef WIN32
			if ((ret = usb_set_configuration(dev_h, 1)) < 0)
			{
				upsdebugx(1, "Can't set POWERWARE USB configuration: %s", nut_usb_strerror(ret));
				errout = 1;
			}
#endif	/* WIN32 */
			if ((ret = usb_claim_interface(dev_h, 0)) < 0)
			{
				upsdebugx(1, "Can't claim POWERWARE USB interface: %s", nut_usb_strerror(ret));
				errout = 1;
			}
			else {
				dev_claimed = 1;
				errout = 0;
			}
/* FIXME: the above part of the opening can go into common... up to here at least */

			if ((ret = usb_clear_halt(dev_h, 0x81)) < 0)
			{
				upsdebugx(1, "Can't reset POWERWARE USB endpoint: %s", nut_usb_strerror(ret));
				if (dev_claimed)
					usb_release_interface(dev_h, 0);
				usb_reset(dev_h);
				sleep(5);	/* Wait reconnect */
				errout = 1;
			}
			else
				errout = 0;
		}

		/* Test if we succeeded */
		if ( (dev_h != NULL) && dev_claimed && (errout == 0) )
			break;
		else {
			/* Clear errors, and try again */
			errout = 0;
		}
	}

	if (!dev_h && !dev_claimed && retry == MAX_TRY)
		errout = 1;
	else
		return dev_h;

	if (dev_h && dev_claimed)
		usb_release_interface(dev_h, 0);

	nutusb_close(dev_h, port);

	if (errout == 1)
		nutusb_open_error(port);

	return NULL;
}

/* FIXME: this part can go into common... */
int nutusb_close(usb_dev_handle *dev_h, const char *port)
{
	int ret = 0;
	NUT_UNUSED_VARIABLE(port);

	if (dev_h)
	{
		usb_release_interface(dev_h, 0);
#if WITH_LIBUSB_1_0
		libusb_close(dev_h);
		libusb_exit(NULL);
#else
		ret = usb_close(dev_h);
#endif
	}

	return ret;
}

void nutusb_comm_fail(const char *fmt, ...)
{
	int ret;
	char why[SMALLBUF];
	va_list ap;

	/* this means we're probably here because select was interrupted */
	if (exit_flag != 0)
		return; /* ignored, since we're about to exit anyway */

	comm_failures++;

	if ((comm_failures == USB_ERR_LIMIT) ||
		((comm_failures % USB_ERR_RATE) == 0))
	{
		upslogx(LOG_WARNING, "Warning: excessive comm failures, "
			"limiting error reporting");
	}

	/* once it's past the limit, only log once every USB_ERR_LIMIT calls */
	if ((comm_failures > USB_ERR_LIMIT) &&
		((comm_failures % USB_ERR_LIMIT) != 0)) {
		/* Try reconnection */
		upsdebugx(1, "Got to reconnect!\n");
		upsdrv_reconnect();
		return;
	}

	/* generic message if the caller hasn't elaborated */
	if (!fmt)
	{
		upslogx(LOG_WARNING, "Communications with UPS lost - check cabling");
		return;
	}

	va_start(ap, fmt);
	ret = vsnprintf(why, sizeof(why), fmt, ap);
	va_end(ap);

	if ((ret < 1) || (ret >= (int) sizeof(why)))
		upslogx(LOG_WARNING, "usb_comm_fail: vsnprintf needed "
			"more than %d bytes", (int)sizeof(why));

	upslogx(LOG_WARNING, "Communications with UPS lost: %s", why);
}

void nutusb_comm_good(void)
{
	if (comm_failures == 0)
		return;

	upslogx(LOG_NOTICE, "Communications with UPS re-established");
	comm_failures = 0;
}
