#include <ctype.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdarg.h>
#include "x10io.H"
#include "x10map.H"
#include "general.H"

static const char *rcs_id = "@(#) $Id: x10io.C 3 2022-02-25 18:13:05Z perette $" ;

static int system_run (const char *format, ...) {
	va_list args;
	char command [2000];

	va_start (args, format);
	if (vsprintf (command, format, args) > (int) sizeof (command)) {
		log (ERROR, "Buffer overrun in system_run - bailing out!");
		exit (1);
	};
	va_end (args);
	int exitstatus = system (command);
	if (exitstatus != 0) {
		if (errno != 0) {
			log (ERROR, "system(): %s: %s", command,
				strerror (errno));
		} else {
			log (ERROR, "system(): %s: exited with %d status",
				command, exitstatus);
		}
	}
	return (exitstatus);
}


static const char *CommandName (int command) {
	static char id [30];
	if (command >= 0 && command <= X10_LAST)
		return (parameter [command]);
	if (command == X10_SELECTUNIT)
		return ("Select unit");
	sprintf (id, "#%d (invalid)", command);
	return (id);
}
	

X10::X10(CONFIGURATION *config)
{
	ctty = strdup (config->device);
	timeout = config->device_timeout;
	q = NULL;
	tail = NULL;
	rpend = 0;
	shutdown = 0;
	receive_size = 0;
	if ((receive_buffer = CALLOC (64, char)))
		receive_size = 64;
	receive_count = 0;
	map = new X10map (config->run_format, config->command_timeout);
	log (INFO, "Using device: %s", ctty);
}


X10::~X10()
{
	log (TRACE, "Closing interface down");
	delete map;
	if (ctty)
		free (ctty);
	close(x10_ioport);
}


// This function opens the ports & stuff.
// Can't do it in the constructor, because we need to call
// virtual functions, which don't actually virtual in the constructor.
//
int X10::Open ()
{
	struct termios tio;

	log (TRACE, "Trying to open(%s)", ctty);
	/* At boot-time, serial port might not be up yet.  Keep trying
	   for 60 seconds before giving up. */
	for (int i = 60; i > 0; i--) {
		x10_ioport = open(ctty,
				   O_RDWR | O_EXCL | O_NOCTTY | O_NONBLOCK);
		if (x10_ioport != -1)
			break;
		sleep (1);
	}
	if (x10_ioport == -1) {
		log (ERROR, "open(): %s: %s", ctty, strerror (errno));
		return (-1);
	}
	log (TRACE, "open(%s): Succeeded, stream #%d", ctty, x10_ioport);

	// Set up default port settings, then call our decendent to make
	// any changes it needs.  Then set up the port.
	if (tcgetattr(x10_ioport, &tio))
		log (WARN, "tcgetattr(): %s: %s", ctty, strerror (errno));

	// Set the port in exactly the mode we want:
	// 8 data bits, receiver enabled, ignore modem control lines
	// use CTS/RTS.  Set each of tio.c_[iocl]flag.
	tio.c_cflag = CS8 | CREAD | CLOCAL | CRTSCTS;
	// Macintosh OS X documentation says it does OLCUC, but not defined.
	#ifdef OLCUC
        tio.c_oflag = OLCUC;
	#else
	tio.c_oflag = 0;
	#endif
	tio.c_iflag = IGNBRK | ICRNL;
	tio.c_lflag = 0;
	cfsetispeed(&tio, B1200);
	cfsetospeed(&tio, B1200);
	log (TRACE, "stream #%d: Initial configuration done", x10_ioport);
	ConfigDevice (&tio);
	log (TRACE, "stream #%d: Custom configuration done", x10_ioport);
	if (tcsetattr(x10_ioport, TCSANOW, &tio))
		log (WARN, "tcsetattr(): %s: %s", ctty, strerror (errno));
	return (0);
}


//
// This function may be overridden to request a different device config.
//
void X10::ConfigDevice(struct termios *tio)
{
	log (TRACE, "Using default device configuration");
}

void X10::Shutdown()
{
//	int buf[5];
	shutdown = 1;
//	buf[0] = -1;
//	buf[1] = 0;
//	buf[2] = 0;
//	buf[3] = 0;
//	buf[4] = -1;
//	sendto(server_socket, buf, sizeof(buf), 0, &sock, sizeof(sock));
}


int X10::Run(CONFIGURATION *config)
{
	fd_set read_fds;
	fd_set write_fds;
	struct timeval tv, *tvp;
	int retval;
	char buf[128];
	time_t event_timeout;

	log (TRACE, "Opening socket %s", config->socket_file);
	if ((server_socket = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) {
		log (ERROR, "socket(): %s: %s", config->socket_file, 
			strerror (errno));
		exit(1);
	}
	sock.sa_family = AF_UNIX;
	strcpy(sock.sa_data, config->socket_file);
	if (bind(server_socket, &sock, sizeof(sock)) == -1) {
		log (ERROR, "bind(): %s", strerror (errno));
		exit(1);
	}
	log (TRACE, "Socket ready.  Adjusting permissions...");

	system_run ("chmod g+rw %s", config->socket_file);

	while (!shutdown) {
		fflush(stdout);

		// Set up what file descriptors we want select to select on
		// We must set these up every time, because the results of
		// select go back into them.
		// Accept incoming data from either server socket or x10
		FD_ZERO(&read_fds);
		FD_SET(x10_ioport, &read_fds);
		FD_SET(server_socket, &read_fds);

		// Look for write ability only if there is stuff in queue
		// and we're not waiting for a command to finish going out.
		FD_ZERO(&write_fds);
		if ((q != NULL) && (!rpend))
			FD_SET(x10_ioport, &write_fds);

		// time out.
		if (q) {
			tvp = &tv;
			tv.tv_sec = MAX(0, q->lasttry + timeout - time(NULL));
			log (TRACE, "Waiting, q is not NULL, timeout is %d sec",
			     tv.tv_sec);
		} else {
			tvp = NULL;
		}
		if ((event_timeout = map->GetTimeout())) {
			int event_time = MAX(0, event_timeout - time (NULL));
			log (TRACE, "Waiting, event timeout is %d sec",
			     event_time);
			if (tvp) {
				tv.tv_sec = MIN (tv.tv_sec, event_time);
			} else {
				tvp = &tv;
				tv.tv_sec = event_time;
			}
		}
		if (!tvp) {
			log (TRACE, "Waiting, q is NULL, no timeout.");
		}
		tv.tv_usec = 0;

		retval = select(MAX (x10_ioport, server_socket)+1,
				&read_fds, &write_fds, NULL, tvp);
		if (retval < 0) {
			log (ERROR, "select(): %s", strerror (errno));
		} else if (retval) {
			// Retrieve data
			if (FD_ISSET(x10_ioport, &read_fds)) {
				int count;
				while ((count = read(x10_ioport,
						   buf, sizeof(buf)-1)) > 0) {
					buf[count] = '\0';
					ProcessInput(count, buf);
				}
				if (count < 0 && errno != EAGAIN) {
					log (ERROR, "read(): %d: %s", errno,
						strerror (errno));
				}
			}
			if (FD_ISSET(server_socket, &read_fds))
				ProcessPacket(server_socket);
			if (FD_ISSET(x10_ioport, &write_fds))
				Send();
		} else if (q) {
			if (q->lasttry + timeout < time(NULL))
				Timeout ();
		}
		if (event_timeout && event_timeout < time(NULL))
			map->Timeout();
	}
	log (INFO, "Shutting down");
	close(server_socket);
	if (unlink(config->socket_file))
		log (ERROR, "unlink(): %s", strerror (errno));
	return 0;
}



int X10::ProcessPacket(int s)
{
	int retval;
	int buf[64];

	retval = recvfrom(server_socket, buf, sizeof(buf)-1, 0, NULL, NULL);
	if (retval != 6 * sizeof (int)) {
		log (WARN, "Invalid packet received, size=%d.", retval);
		return 0;
	}
	if (buf[0] == -1) {
		log (INFO, "Received shutdown packet.");
		// ZZZZZZZZ should there be a call to shutdown here?
		return 0;
	}
	if (buf[0] != 1) {
		log (INFO, "Invalid value in packet header.");
		return 0;
	}
	if (buf[5] != buf[0] + buf[1] + buf[2] + buf[3] + buf [4]) {
		log (INFO, "Invalid checksum.");
		return 0;
	}
	ValidateCommand (buf[3], buf[1], buf[2], buf[4]);
	return 0;
}





int X10::QueueBinary (int command, int house, int unit, int size, char *data)
{
	char *copy = NULL;
	struct x10cmdq *newitem = NULL;
	if ((copy = CALLOC (size, char)) &&
	    (newitem = MALLOC (struct x10cmdq))) {
		memcpy (copy, data, size);
		if (tail == NULL) {
			tail = newitem;
			q = tail;
		} else {
			tail->next = newitem;
			tail = tail->next;
		}
		tail->cmd = copy;
		tail->cmdsize = size;
		tail->pending = 0;
		tail->tries = 5;
		tail->lasttry = 0;
		tail->command = command;
		tail->house = house;
		tail->unit = unit;
		tail->next = NULL;
		log (TRACE, "Queued command %c%02d %s:",
		    (char) q->house, q->unit, CommandName (q->command));
		loghex (TRACE, tail->cmd, tail->cmdsize);
		return (0);
	}
	log (ERROR, "malloc(): %s", strerror (errno));
	if (copy)
		free (copy);
	return (1);
}



int X10::QueueText (int command, int house, int unit, char *text)
{
	int status;
	status = QueueBinary (command, house, unit, strlen(text), text);
	return (status);
}




int X10::ValidateCommand (int cmd, int hc, int uc, int count)
{
	//
	// Added bounds checking on house and unit codes.  Out of
	// range values (such as uc == 0) would segfault futher on.
	//
	int needUnitCode = 1;
	int status = 0;

	if ((hc < 'A') || (hc > 'P')) {
		log (WARN, "Received bad command: invalid house code %c (%d)",
			(char) hc, hc);
		return -1;
	}

	switch (cmd) {
	    case (X10_BRIGHT):
	    case (X10_DIM):
	    case (X10_ON):
	    case (X10_OFF):
	    case (X10_HAILREQ):
	    case (X10_STATREQ):
		break;
	    case (X10_LIGHTSON):
	    case (X10_LIGHTSOFF):
	    case (X10_ALLOFF):
		needUnitCode = 0;
		break;
	    // Expert commands -- if we get a unit code, pass it through.
	    // If we get these, someone should know what they're doing...
	    case (X10_STATOFF):
	    case (X10_STATON):
	    case (X10_PSD1):
	    case (X10_PSD0):
	    case (X10_HAILACK):
		needUnitCode = uc;
		break;
	    default:
		log (WARN, "Received bad command: invalid command #%d", cmd);
		return -1;
	}

	if (!count) {
		log (WARN, "Received command with count=0");
	}

	if (needUnitCode && ((uc < 1) || (uc > 16))) {
		log (WARN, "Received bad command: invalid unit code %d", uc);
		return -1;
	}
	
	if (needUnitCode)
		status = CreateCommand (X10_SELECTUNIT, hc, uc);
	else
		uc = 0;

	while (count-- && status == 0)
		CreateCommand (cmd, hc, uc);

	return 0;
}


int X10::Dequeue()
{
	struct x10cmdq *temp;

	temp = q;
	q = q->next;
	if (q == NULL) tail = NULL;
	log (TRACE, "free(temp->cmd)");
	free(temp->cmd);
	log (TRACE, "free(temp)");
	free(temp);
	log (TRACE, "free is done");
	rpend = 0;
	return 0;
}

int X10::Send()
{
	int size;
	if (q->pending) return 0;
	log (TRACE, "Sending command %c%02d %s:",
	    (char) q->house, q->unit, CommandName (q->command));
	loghex (TRACE, tail->cmd, tail->cmdsize);

	for (size = 0; size < q->cmdsize; size++) {
		write (x10_ioport, &(q->cmd [size]), 1);
		usleep (5000);
	}


//#	if ((size = write(x10_ioport, q->cmd, q->cmdsize)) < 0)
	if (size < 0)
		log (ERROR, "write(): %s", strerror (errno));
	else if (size < q->cmdsize)
		log (ERROR, "write(): Incomplete (%d of %d bytes.)\n",
		size, q->cmdsize);
	q->pending = 1;
	q->lasttry = time(NULL);
	rpend = 1;
	usleep(10);
	return 0;
}


void X10::ProcessInput (int size, char *data)
{
	if (receive_count + size > receive_size) {
		int desired = receive_count + size + 64;
		char *newbuf = (char *) realloc (receive_buffer, desired);
		if (newbuf) {
			receive_size = desired;
			receive_buffer = newbuf;
		} else {
			log (ERROR, "Could not increase receive buffer size");
			log (WARN, "Incoming data dropped.");
			size = receive_size - receive_count;
		}
	}

	memcpy (receive_buffer + receive_count, data, size);
	receive_count += size;

	int processed;
	while (receive_count && 
	       (processed = ParseCommand (receive_count, receive_buffer))) {
		// Swallow the processed characters
		log (TRACE, "Parser consumed:");
		loghex (TRACE, receive_buffer, processed);
		receive_count -= processed;
		memmove (receive_buffer, receive_buffer + processed,
			 receive_count);
	}

}


void X10::Receive (int command, int housecode, int unitcode)
{
	if (UNITOK (unitcode)) {
		log (INFO, "House code %c: Select unit %d", (char) housecode,
		     unitcode);
		map->Update (housecode, X10_SELECTUNIT + unitcode);
	}

	int isCurrentCommand = (q && command == q->command && housecode == q->house
				&& unitcode == q->unit);
	if (COMMANDOK (command)) {
		log (INFO, "House code %c: Command %s (%s)", (char) housecode,
		     CommandName (command), isCurrentCommand ? "Confirmed" : "Received");
		map->Update (housecode, command, !isCurrentCommand);
	}

	if (isCurrentCommand) {
		Dequeue ();
	}
}

//
// Default action for a timeout: retry.
//
void X10::Timeout()
{
	log (WARN, "Timeout");
	Retry ();
}


//
//  Handle any errors returned, indicated by a ? message.
//

void X10::Retry ()
{
	if ((q->tries--) > 0) {
		log (WARN, "Command %c%02d %s: Retrying.",
		    (char) q->house, q->unit, CommandName (q->command));
		q->pending = 0;
		rpend = 0;
	} else {
		log (ERROR, "Command %c%02d %s: Maximum tries exceeded."
		     "  Dequeuing...", (char) q->house, q->unit,
		     CommandName (q->command));
		Dequeue();
	}
}

