/*
 * blazer_usb.c: support for Megatec/Q1 USB protocol based UPSes
 *
 * A document describing the protocol implemented by this driver can be
 * found online at "http://www.networkupstools.org/protocols/megatec.html".
 *
 * Copyright (C) 2003-2009  Arjen de Korte <adkorte-guest@alioth.debian.org>
 * Copyright (C) 2011-2012  Arnaud Quette <arnaud.quette@free.fr>
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include "main.h"
#include "libusb.h"
#include "usb-common.h"
#include "blazer.h"

#define DRIVER_NAME	"Megatec/Q1 protocol USB driver"
#define DRIVER_VERSION	"0.12"

/* driver description structure */
upsdrv_info_t upsdrv_info = {
	DRIVER_NAME,
	DRIVER_VERSION,
	"Arjen de Korte <adkorte-guest@alioth.debian.org>\n" \
	"Arnaud Quette <arnaud.quette@free.fr>",
	DRV_BETA,
	{ NULL }
};

#ifndef TESTING

static usb_communication_subdriver_t *usb = &usb_subdriver;
static usb_dev_handle		*udev = NULL;
static USBDevice_t		usbdevice;
static USBDeviceMatcher_t	*reopen_matcher = NULL;
static USBDeviceMatcher_t	*regex_matcher = NULL;
static int					langid_fix = -1;

static int (*subdriver_command)(const char *cmd, char *buf, size_t buflen) = NULL;


static int cypress_command(const char *cmd, char *buf, size_t buflen)
{
	char	tmp[SMALLBUF];
	int	ret;
	size_t	i;

	memset(tmp, 0, sizeof(tmp));
	snprintf(tmp, sizeof(tmp), "%s", cmd);

	for (i = 0; i < strlen(tmp); i += ret) {

		/* Write data in 8-byte chunks */
		/* ret = usb->set_report(udev, 0, (unsigned char *)&tmp[i], 8); */
		ret = usb_control_msg(udev, USB_ENDPOINT_OUT + USB_TYPE_CLASS + USB_RECIP_INTERFACE,
			0x09, 0x200, 0, &tmp[i], 8, 5000);

		if (ret <= 0) {
			upsdebugx(3, "send: %s", ret ? usb_strerror() : "timeout");
			return ret;
		}
	}

	upsdebugx(3, "send: %.*s", (int)strcspn(tmp, "\r"), tmp);

	memset(buf, 0, buflen);

	for (i = 0; (i <= buflen-8) && (strchr(buf, '\r') == NULL); i += ret) {

		/* Read data in 8-byte chunks */
		/* ret = usb->get_interrupt(udev, (unsigned char *)&buf[i], 8, 1000); */
		ret = usb_interrupt_read(udev, 0x81, &buf[i], 8, 1000);

		/*
		 * Any errors here mean that we are unable to read a reply (which
		 * will happen after successfully writing a command to the UPS)
		 */
		if (ret <= 0) {
			upsdebugx(3, "read: %s", ret ? usb_strerror() : "timeout");
			return ret;
		}
	}

	upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
	return i;
}


static int phoenix_command(const char *cmd, char *buf, size_t buflen)
{
	char	tmp[SMALLBUF];
	int	ret;
	size_t	i;

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

		/* Read data in 8-byte chunks */
		/* ret = usb->get_interrupt(udev, (unsigned char *)tmp, 8, 1000); */
		ret = usb_interrupt_read(udev, 0x81, tmp, 8, 1000);

		/*
		 * This USB to serial implementation is crappy. In order to read correct
		 * replies we need to flush the output buffers of the converter until we
		 * get no more data (ie, it times out).
		 */
		switch (ret)
		{
		case -EPIPE:		/* Broken pipe */
			usb_clear_halt(udev, 0x81);
		case -ETIMEDOUT:	/* Connection timed out */
			break;
		}

		if (ret < 0) {
			upsdebugx(3, "flush: %s", usb_strerror());
			break;
		}

		upsdebug_hex(4, "dump", tmp, ret);
	}

	memset(tmp, 0, sizeof(tmp));
	snprintf(tmp, sizeof(tmp), "%s", cmd);

	for (i = 0; i < strlen(tmp); i += ret) {

		/* Write data in 8-byte chunks */
		/* ret = usb->set_report(udev, 0, (unsigned char *)&tmp[i], 8); */
		ret = usb_control_msg(udev, USB_ENDPOINT_OUT + USB_TYPE_CLASS + USB_RECIP_INTERFACE,
			0x09, 0x200, 0, &tmp[i], 8, 1000);

		if (ret <= 0) {
			upsdebugx(3, "send: %s", ret ? usb_strerror() : "timeout");
			return ret;
		}
	}

	upsdebugx(3, "send: %.*s", (int)strcspn(tmp, "\r"), tmp);

	memset(buf, 0, buflen);

	for (i = 0; (i <= buflen-8) && (strchr(buf, '\r') == NULL); i += ret) {

		/* Read data in 8-byte chunks */
		/* ret = usb->get_interrupt(udev, (unsigned char *)&buf[i], 8, 1000); */
		ret = usb_interrupt_read(udev, 0x81, &buf[i], 8, 1000);

		/*
		 * Any errors here mean that we are unable to read a reply (which
		 * will happen after successfully writing a command to the UPS)
		 */
		if (ret <= 0) {
			upsdebugx(3, "read: %s", ret ? usb_strerror() : "timeout");
			return ret;
		}
	}

	upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
	return i;
}


static int ippon_command(const char *cmd, char *buf, size_t buflen)
{
	char	tmp[64];
	int	ret, len;
	size_t	i;

	snprintf(tmp, sizeof(tmp), "%s", cmd);

	for (i = 0; i < strlen(tmp); i += ret) {

		/* Write data in 8-byte chunks */
		ret = usb_control_msg(udev, USB_ENDPOINT_OUT + USB_TYPE_CLASS + USB_RECIP_INTERFACE,
			0x09, 0x2, 0, &tmp[i], 8, 1000);

		if (ret <= 0) {
			upsdebugx(3, "send: %s", (ret != -ETIMEDOUT) ? usb_strerror() : "Connection timed out");
			return ret;
		}
	}

	upsdebugx(3, "send: %.*s", (int)strcspn(tmp, "\r"), tmp);

	/* Read all 64 bytes of the reply in one large chunk */
	ret = usb_interrupt_read(udev, 0x81, tmp, sizeof(tmp), 1000);

	/*
	 * Any errors here mean that we are unable to read a reply (which
	 * will happen after successfully writing a command to the UPS)
	 */
	if (ret <= 0) {
		upsdebugx(3, "read: %s", (ret != -ETIMEDOUT) ? usb_strerror() : "Connection timed out");
		return ret;
	}

	/*
	 * As Ippon will always return 64 bytes in response, we have to
	 * calculate and return length of actual response data here.
	 * Empty response will look like 0x00 0x0D, otherwise it will be
	 * data string terminated by 0x0D.
	 */
	len = (int)strcspn(tmp, "\r");
	upsdebugx(3, "read: %.*s", len, tmp);
	if (len > 0) {
		len ++;
	}
	snprintf(buf, buflen, "%.*s", len, tmp);
	return len;
}


static int krauler_command(const char *cmd, char *buf, size_t buflen)
{
	/*
	 * Still not implemented:
	 * 0x6	T<n>	(don't know how to pass the parameter)
	 * 0x68 and 0x69 both cause shutdown after an undefined interval
	*/
	const struct {
		const char	*str;		/* Megatec command */
		const int	index;	/* Krauler string index for this command */
		const char	prefix;	/* character to replace the first byte in reply */
	}  command[] = {
		{ "Q1\r", 0x03, '(' },
		{ "F\r",  0x0d, '#' },
		{ "I\r",  0x0c, '#' },
		{ "T\r",  0x04, '\r' },
		{ "TL\r", 0x05, '\r' },
		{ "Q\r",  0x07, '\r' },
		{ "C\r",  0x0b, '\r' },
		{ "CT\r", 0x0b, '\r' },
		{ NULL }
	};

	int	i;

	upsdebugx(3, "send: %.*s", (int)strcspn(cmd, "\r"), cmd);

	for (i = 0; command[i].str; i++) {
		int	retry;

		if (strcmp(cmd, command[i].str)) {
			continue;
		}

		for (retry = 0; retry < 10; retry++) {
			int	ret;

			if (langid_fix != -1) {
				/* Apply langid_fix value */
				ret = usb_get_string(udev, command[i].index, langid_fix, buf, buflen);
			}
			else {
				ret = usb_get_string_simple(udev, command[i].index, buf, buflen);
			}

			if (ret <= 0) {
				upsdebugx(3, "read: %s", ret ? usb_strerror() : "timeout");
				return ret;
			}

			/* this may serve in the future */
			upsdebugx(1, "received %d (%d)", ret, buf[0]);

			if (langid_fix != -1) {
				/* Limit this check, at least for now */
				/* Invalid receive size - message corrupted */
				if (ret != buf[0]) 
				{
					upsdebugx(1, "size mismatch: %d / %d", ret, buf[0]);
					continue;
				}

				/* Simple unicode -> ASCII inplace conversion
				 * FIXME: this code is at least shared with mge-shut/libshut
				 * Create a common function? */
				unsigned int di, si, size = buf[0];
				for (di = 0, si = 2; si < size; si += 2) {
					if (di >= (buflen - 1))
						break;

					if (buf[si + 1]) /* high byte */
						buf[di++] = '?';
					else
						buf[di++] = buf[si];
				}
				buf[di] = 0;
				ret = di;
			}

			/* "UPS No Ack" has a special meaning */
			if (!strcasecmp(buf, "UPS No Ack")) {
				upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
				continue;
			}

			/* Replace the first byte of what we received with the correct one */
			buf[0] = command[i].prefix;

			upsdebugx(3, "read: %.*s", (int)strcspn(buf, "\r"), buf);
			return ret;
		}

		return 0;
	}

	/* echo the unknown command back */
	upsdebugx(3, "read: %.*s", (int)strcspn(cmd, "\r"), cmd);
	return snprintf(buf, buflen, "%s", cmd);
}


static void *cypress_subdriver(USBDevice_t *device)
{
	subdriver_command = &cypress_command;
	return NULL;
}


static void *ippon_subdriver(USBDevice_t *device)
{
	subdriver_command = &ippon_command;
	return NULL;
}


static void *krauler_subdriver(USBDevice_t *device)
{
	subdriver_command = &krauler_command;
	return NULL;
}


static void *phoenix_subdriver(USBDevice_t *device)
{
	subdriver_command = &phoenix_command;
	return NULL;
}


static usb_device_id_t blazer_usb_id[] = {
	{ USB_DEVICE(0x05b8, 0x0000), &cypress_subdriver },	/* Agiler UPS */
	{ USB_DEVICE(0x0001, 0x0000), &krauler_subdriver },	/* Krauler UP-M500VA */
	{ USB_DEVICE(0xffff, 0x0000), &krauler_subdriver },	/* Ablerex 625L USB */
	{ USB_DEVICE(0x0665, 0x5161), &cypress_subdriver },	/* Belkin F6C1200-UNV */
	{ USB_DEVICE(0x06da, 0x0002), &cypress_subdriver },	/* Online Yunto YQ450 */
	{ USB_DEVICE(0x06da, 0x0003), &ippon_subdriver },	/* Mustek Powermust */
	{ USB_DEVICE(0x06da, 0x0004), &cypress_subdriver },	/* Phoenixtec Innova 3/1 T */
	{ USB_DEVICE(0x06da, 0x0005), &cypress_subdriver },	/* Phoenixtec Innova RT */
	{ USB_DEVICE(0x06da, 0x0201), &cypress_subdriver },	/* Phoenixtec Innova T */
	{ USB_DEVICE(0x06da, 0x0601), &phoenix_subdriver },	/* Online Zinto A */
	{ USB_DEVICE(0x0f03, 0x0001), &cypress_subdriver },	/* Unitek Alpha 1200Sx */
	{ USB_DEVICE(0x14f0, 0x00c9), &phoenix_subdriver },	/* GE EP series */
	/* end of list */
	{-1, -1, NULL}
};


static int device_match_func(USBDevice_t *hd, void *privdata)
{
	if (subdriver_command) {
		return 1;
	}

	switch (is_usb_device_supported(blazer_usb_id, hd))
	{
	case SUPPORTED:
		return 1;

	case POSSIBLY_SUPPORTED:
	case NOT_SUPPORTED:
	default:
		return 0;
	}
}


static USBDeviceMatcher_t device_matcher = {
	&device_match_func,
	NULL,
	NULL
};

#endif	/* TESTING */


/*
 * Generic command processing function. Send a command and read a reply.
 * Returns < 0 on error, 0 on timeout and the number of bytes read on
 * success.
 */
int blazer_command(const char *cmd, char *buf, size_t buflen)
{
#ifndef TESTING
	int	ret;

	if (udev == NULL) {
		ret = usb->open(&udev, &usbdevice, reopen_matcher, NULL);

		if (ret < 1) {
			return ret;
		}
	}

	ret = (*subdriver_command)(cmd, buf, buflen);
	if (ret >= 0) {
		return ret;
	}

	switch (ret)
	{
	case -EBUSY:		/* Device or resource busy */
		fatal_with_errno(EXIT_FAILURE, "Got disconnected by another driver");

	case -EPERM:		/* Operation not permitted */
		fatal_with_errno(EXIT_FAILURE, "Permissions problem");

	case -EPIPE:		/* Broken pipe */
		if (usb_clear_halt(udev, 0x81) == 0) {
			upsdebugx(1, "Stall condition cleared");
			break;
		}
#ifdef ETIME
	case -ETIME:		/* Timer expired */
#endif
		if (usb_reset(udev) == 0) {
			upsdebugx(1, "Device reset handled");
		}
	case -ENODEV:		/* No such device */
	case -EACCES:		/* Permission denied */
	case -EIO:		/* I/O error */
	case -ENXIO:		/* No such device or address */
	case -ENOENT:		/* No such file or directory */
		/* Uh oh, got to reconnect! */
		usb->close(udev);
		udev = NULL;
		break;

	case -ETIMEDOUT:	/* Connection timed out */
	case -EOVERFLOW:	/* Value too large for defined data type */
#ifdef EPROTO
	case -EPROTO:		/* Protocol error */
#endif
	default:
		break;
	}

	return ret;
#else
	const struct {
		const char	*command;
		const char	*answer;
	} testing[] = {
		{ "Q1\r", "(215.0 195.0 230.0 014 49.0 2.27 30.0 00101000\r" },
		{ "F\r",  "#230.0 000 024.0 50.0\r" },
		{ "I\r",  "#-------------   ------     VT12046Q  \r" },
		{ NULL }
	};

	int	i;

	memset(buf, 0, buflen);

	for (i = 0; testing[i].command; i++) {

		if (strcasecmp(cmd, testing[i].command)) {
			continue;
		}

		return snprintf(buf, buflen, "%s", testing[i].answer);
	}

	return snprintf(buf, buflen, "%s", testing[i].command);
#endif
}


void upsdrv_help(void)
{
	printf("Read The Fine Manual ('man 8 blazer_usb')\n");
}


void upsdrv_makevartable(void)
{
	addvar(VAR_VALUE, "subdriver", "Serial-over-USB subdriver selection");
	nut_usb_addvars();

	addvar(VAR_VALUE, "langid_fix", "Apply the language ID workaround to the krauler subdriver (0x409 or 0x4095)");

	blazer_makevartable();
}


void upsdrv_initups(void)
{
#ifndef TESTING
	const struct {
		const char	*name;
		int		(*command)(const char *cmd, char *buf, size_t buflen);
	} subdriver[] = {
		{ "cypress", &cypress_command },
		{ "phoenix", &phoenix_command },
		{ "ippon", &ippon_command },
		{ "krauler", &krauler_command },
		{ NULL }
	};

	int	ret, langid;
	char	tbuf[255]; /* Some devices choke on size > 255 */
	char	*regex_array[6];

	char	*subdrv = getval("subdriver");

	regex_array[0] = getval("vendorid");
	regex_array[1] = getval("productid");
	regex_array[2] = getval("vendor");
	regex_array[3] = getval("product");
	regex_array[4] = getval("serial");
	regex_array[5] = getval("bus");

	/* check for language ID workaround (#1) */
	if (getval("langid_fix")) {
		/* skip "0x" prefix and set back to hexadecimal */
		if (sscanf(getval("langid_fix") + 2, "%x", &langid_fix) != 1) {
			upslogx(LOG_NOTICE, "Error enabling language ID workaround");
		}
		else {
			upsdebugx(2, "language ID workaround enabled (using '0x%x')", langid_fix);
		}
	}

	/* pick up the subdriver name if set explicitly */
	if (subdrv) {
		int	i;

		if (!regex_array[0] || !regex_array[1]) {
			fatalx(EXIT_FAILURE, "When specifying a subdriver, 'vendorid' and 'productid' are mandatory.");
		}

		for (i = 0; subdriver[i].name; i++) {

			if (strcasecmp(subdrv, subdriver[i].name)) {
				continue;
			}

			subdriver_command =  subdriver[i].command;
			break;
		}

		if (!subdriver_command) {
			fatalx(EXIT_FAILURE, "Subdriver \"%s\" not found!", subdrv);
		}
	}

	ret = USBNewRegexMatcher(&regex_matcher, regex_array, REG_ICASE | REG_EXTENDED);
	switch (ret)
	{
	case -1:
		fatal_with_errno(EXIT_FAILURE, "USBNewRegexMatcher");
	case 0:
		break;	/* all is well */
	default:
		fatalx(EXIT_FAILURE, "invalid regular expression: %s", regex_array[ret]);
	}


	/* link the matchers */
	regex_matcher->next = &device_matcher;

	ret = usb->open(&udev, &usbdevice, regex_matcher, NULL);
	if (ret < 0) {
		fatalx(EXIT_FAILURE,
			"No supported devices found. Please check your device availability with 'lsusb'\n"
			"and make sure you have an up-to-date version of NUT. If this does not help,\n"
			"try running the driver with at least 'subdriver', 'vendorid' and 'productid'\n"
			"options specified. Please refer to the man page for details about these options\n"
			"(man 8 blazer_usb).\n");
	}

	if (!subdriver_command) {
		fatalx(EXIT_FAILURE, "No subdriver selected");
	}

	/* create a new matcher for later reopening */
	ret = USBNewExactMatcher(&reopen_matcher, &usbdevice);
	if (ret) {
		fatal_with_errno(EXIT_FAILURE, "USBNewExactMatcher");
	}

	/* link the matchers */
	reopen_matcher->next = regex_matcher;

	dstate_setinfo("ups.vendorid", "%04x", usbdevice.VendorID);
	dstate_setinfo("ups.productid", "%04x", usbdevice.ProductID);

	/* check for language ID workaround (#2) */
	if (langid_fix != -1) {
		/* Future improvement:
		 *   Asking for the zero'th index is special - it returns a string
		 *   descriptor that contains all the language IDs supported by the
		 *   device. Typically there aren't many - often only one. The
		 *   language IDs are 16 bit numbers, and they start at the third byte
		 *   in the descriptor. See USB 2.0 specification, section 9.6.7, for
		 *   more information on this.
		 * This should allow automatic application of the workaround */
		ret = usb_get_string(udev, 0, 0, tbuf, sizeof(tbuf));
		if (ret >= 4) {
			langid = tbuf[2] | (tbuf[3] << 8);
			upsdebugx(1, "First supported language ID: 0x%x (please report to the NUT maintainer!)", langid);
		}
	}
#endif
	blazer_initups();
}


void upsdrv_initinfo(void)
{
	blazer_initinfo();
}


void upsdrv_cleanup(void)
{
#ifndef TESTING
	usb->close(udev);
	USBFreeExactMatcher(reopen_matcher);
	USBFreeRegexMatcher(regex_matcher);
	free(usbdevice.Vendor);
	free(usbdevice.Product);
	free(usbdevice.Serial);
	free(usbdevice.Bus);
#endif
}
