#include <stdio.h>
#include <stdlib.h>
#include <termio.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <unistd.h>
#include <getopt.h>
#include <stdint.h>
#include <string.h>
#include <syslog.h>
#include <fcntl.h>
#include <err.h>
#include <linux/serial.h>

int hex = 0;
int screen = 0;
int unprocessed = 0;

int process(const uint8_t *buf, int len);

int serial_open(const char *device, int rate)
{
	struct termios options;
	struct serial_struct serinfo;
	int fd;

	/* Open and configure serial port */
	if ((fd = open(device,O_RDWR|O_NOCTTY)) == -1)
		err(1, "%s", device);

        serinfo.reserved_char[0] = 0;
        if (ioctl(fd, TIOCGSERIAL, &serinfo) < 0)
		err(1, "%s: TIOCGSERIAL", device);
	serinfo.flags &= ~ASYNC_SPD_MASK;
	serinfo.flags |= ASYNC_SPD_CUST;
	serinfo.custom_divisor = (serinfo.baud_base + (rate / 2)) / rate;
	if (serinfo.custom_divisor < 1) 
		serinfo.custom_divisor = 1;
        if (ioctl(fd, TIOCSSERIAL, &serinfo) < 0)
		err(1, "%s: TIOCSSERIAL", device);
        if (ioctl(fd, TIOCGSERIAL, &serinfo) < 0)
		err(1, "%s: TIOCGSERIAL 2", device);
	if (serinfo.custom_divisor * rate != serinfo.baud_base) {
		warnx("actual baudrate is %d / %d = %f",
		      serinfo.baud_base, serinfo.custom_divisor,
		      (float)serinfo.baud_base / serinfo.custom_divisor);
	}

	fcntl(fd, F_SETFL, 0);
	tcgetattr(fd, &options);
	cfsetispeed(&options, B38400);
	cfsetospeed(&options, B38400);
	cfmakeraw(&options);
	options.c_cflag |= (CLOCAL | CREAD);
	options.c_cflag &= ~CRTSCTS;
	if (tcsetattr(fd, TCSANOW, &options) != 0)
		err(1, "TCSANOW");

	return fd;
}

int main(int argc, char *argv[])
{
	char *device=strdup("/dev/ttyUSB0");
	int rate=500000;
	int fd;
	int getopt_index;
	int i;
	char buf[1024];
	int len;

	static struct option long_opts[] = {
		{ "device", required_argument, NULL, 'd' },
		{ "rate", required_argument, NULL, 'r' },
		{ "hex", no_argument, NULL, 'x' },
		{ "unprocessed", no_argument, NULL, 'u' },
		{ "screen", no_argument, NULL, 's' },
		{ "help", no_argument, NULL, 'h' },
		{ 0, 0, 0, 0 }
	};
	int help=0;
	char c;

	while ((c = getopt_long(argc, argv, "d:r:xush?",
				long_opts, &getopt_index)) != -1) {
		switch(c) 
		{
		case 'd':
			free(device);
			device = strdup(optarg);
			break;
		case 'r':
			rate = atoi(optarg);
			if(rate == 0)
				errx(1, "invalid rate: %s",optarg);
			break;
		case 'x':
			hex = 1;
			break;
		case 'u':
			unprocessed = 1;
			break;
		case 's':
			screen = 1;
			break;
		case 'h':
		case '?':
		default:
			help = 1;
			break;
		}
	}
	
	if (help) {
		fprintf(stderr, "Zoom Nilm Client\n");
		fprintf(stderr, "usage: %s [options]\n\n", *argv);
		fprintf(stderr, "  -d,  --device /dev/ttyUSB0   serial port\n");
		fprintf(stderr, "  -r,  --rate 1000000          baud rate\n");
		fprintf(stderr, "  -x,  --hex                   hex out\n");
		fprintf(stderr, "  -u,  --unprocessed           dump raw unprocessed data\n");
		fprintf(stderr, "  -s,  --screen                send \\r instead of \\n\n");
		fprintf(stderr, "  -h,  --help                  this cruft\n");
		return 1;
	}

	if ((fd = serial_open(device, rate)) == -1)
		errx(1, "serial_open failed");

	len = 0;
	while (1) {
		int processed, n;
		n = read(fd, buf + len, sizeof(buf) - len);
		if (n <= 0) 
			err(1, "read");
		len += n;
		processed = process(buf, len);
		memmove(buf, buf + processed, len - processed);
		len -= processed;
	}
	
	return 1;
}

int process(const uint8_t *buf, int len)
{
	int n = 0;
	uint16_t dac, adc;

	if (unprocessed) {
		n = write(fileno(stdout), buf, len);
		if (n >= 0)
			return n;
		return 0;
	}

	/* Process blocks */
 retry:
	for (; (n + 4) <= len; buf += 4, n += 4) {
		if ((!buf[0] & 0x80) ||
		    (buf[1] & 0x80) ||
		    (buf[2] & 0x80) ||
		    (buf[3] & 0x80)) {
			/* badly formed data, eat one byte and retry */
			buf++;
			n++;
			goto retry;
		}

		adc = ((buf[0] & 0x7f) << 5) |
			((buf[1] & 0x7c) >> 2);
		dac = ((buf[1] & 0x03) << 14) |
			(buf[2] << 7) | buf[3];
		
		printf(hex ? "%04x %04x%c" : "%d %d%c", dac, adc, screen ? '\r' : '\n');
	}
	return n;
}
