#define USE_MMAP

#define X10_MAP_FILE "/tmp/x10map"

#ifdef USE_MMAP
// #define _POSIX_MAPPED_FILES
#define PAGESIZE 4096
#endif

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include "x10io.H"
#include "x10map.H"
#include "general.H"

#ifndef PATH_MAX
#include <linux/limits.h>
#endif

static const char *rcs_id = "@(#) $Id: x10map.C 1 2022-02-25 17:20:12Z perette $" ;

const char *parameter [] = {
	"AllOff",
	"LightsOn",
	"On",
	"Off",
	"Dim",
	"Bright",
	"LightsOff",
	"HailRequest",
	"HailAcknowledge",
	"PresetDim0",
	"PresetDim1",
	"StatusOn",
	"StatusOff",
	"StatusRequest"
};




X10map::X10map (char *format, int event_timeout)
{
	int i;

	run_format = strdup (format);

#ifdef USE_MMAP
	struct stat filedata;
	struct x10state initializer [256];
	xmap = (struct x10state *) -1;

	int maplen = PAGESIZE * ((sizeof(initializer) + PAGESIZE-1) / PAGESIZE);
	if ((mfd = open(X10_MAP_FILE,
#ifdef _BSD
			O_RDWR | O_CREAT | O_SYNC,
#else
			O_RDWR | O_CREAT,
#endif
			S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)) == -1) {
		log (ERROR, "open(): %s: %s", X10_MAP_FILE, strerror (errno));
		return;
	}
	if (fstat (mfd, &filedata) < 0) {
		log (ERROR, "stat(): %s: %s", X10_MAP_FILE, strerror (errno));
		return;
	}
	log (TRACE, "Map file size=%d, expected=%d", filedata.st_size,
		sizeof (initializer));
	if (filedata.st_size == (int) sizeof (initializer)) {
		log (INFO, "opening existing map file.");
	} else if (filedata.st_size > (int) sizeof (initializer)) {
		log (WARN, "existing map file is larger than expected, "
			    "but using it anyway.");
	} else {
		log (WARN, "existing map file is too small - clearing.");
		memset (initializer, 0, sizeof (initializer));
		write(mfd, initializer, sizeof (initializer));
	}
	log (TRACE, "Mapping %d bytes", maplen);
	xmap = (struct x10state *) mmap(0, maplen,
		PROT_READ | PROT_WRITE, MAP_SHARED, mfd, 0);
	if (xmap == (struct x10state *)(-1)) {
		log (ERROR, "mmap(): %s", strerror (errno));
		return;
	}
#endif
	for (i=0; i<16; i++)
		active_unit[i] = 0;
	timeout_pending = 0;
	timeout = event_timeout;
}


X10map::~X10map()
{
	// if (mfd == -1) return;
#ifdef USE_MMAP
	if (xmap == (struct x10state *)(-1))
		munmap((char *)xmap, sizeof(struct x10state)*256);
#endif
	close(mfd);
}


int X10map::Update(int housecode, int cmd, int received)
{
	int house = housecode - 'A';
#ifdef USE_MMAP
	if (xmap == (struct x10state *)(-1))
		return 0;
#else
	if ((mfd = open(X10_MAP_FILE,
			O_RDWR | O_CREAT | O_SYNC,
			S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)) == -1) {
		log (ERROR, "open(): %s: %s", X10_MAP_FILE, strerror (errno));
		return;
	}
	xmap = (struct x10state *)malloc(256*sizeof(struct x10state));
	read(mfd, xmap, (256*sizeof(struct x10state)));
#endif
	int start, stop, u;
	start = stop = active_unit[house] - 1;
	struct x10state *unit;

	if (cmd == X10_ALLOFF || cmd == X10_LIGHTSOFF || cmd == X10_LIGHTSON) {
		start = 0;
		stop = 15;
	} else if (cmd > X10_SELECTUNIT && cmd <= X10_SELECTUNIT + 16) {
		active_unit[house] = cmd - X10_SELECTUNIT;
		start = active_unit [house] - 1;
		stop = start;
		cmd = X10_SELECTUNIT;
		unit = xmap + ((house * 16) + start);
		unit->flags |= (XS_EXIST | XS_ALIVE);
	} else if (active_unit [house] == 0) {
		/* Active unit for this house code has not yet been set. */
		return 0;
	}

	for (u = start; u <= stop; u++) {
	    unit = xmap + ((house * 16) + u);

	    switch (cmd) {
		case (X10_LIGHTSOFF):
		    // For lights off command, only two-ways respond.
		    if (!(unit->flags & XS_TWOWAY) || (!unit->flags & XS_LIGHT))
			    break;
		    // fall through
		case (X10_OFF):
		case (X10_ALLOFF):
		    // If it's not a two-way, it will restart at max level.
		    if (!(unit->flags & XS_TWOWAY))
			    unit->level = 20;
		    // fall through
		case (X10_STATOFF):
		    unit->flags &= ~XS_ON;
		    break;

		case (X10_LIGHTSON):
		    if (!(unit->flags & XS_LIGHT))
			    break;;
		    // fall through
		case (X10_ON):
		case (X10_STATON):
		    unit->flags |= XS_ON;
		    break;

		case (X10_DIM):
		    if (unit->level > 0)
			    unit->level --;
		    break;

		case (X10_BRIGHT):
		    if (unit->level < 20)
			    unit->level ++;
		    break;

		case (X10_HAILREQ):
		case (X10_HAILACK):
		case (X10_PSD0):
		case (X10_PSD1):
		case (X10_STATREQ):
		    // these are unimplemented
		    break;
		case (X10_SELECTUNIT):
		    // no-op
		    break;
	    }

	    // Received flag indicates it's received from powerline, not a confirmation.
	    if (unit->flags & XS_ME && cmd >= 0
				    && cmd < (int) countof (parameter)
				    && received) {
		if (unit->ifunc) {
		    unit->ifunc (housecode, u + 1, cmd);
		} else {
		    x10event event;
		    event.house = housecode;
		    event.unit = u + 1;
		    event.cmd = cmd;
		    q.push (event);
		    log (TRACE, "Queued event for execution.");
		}
	    }
	}

#ifdef USE_MMAP
	msync((char *)xmap, sizeof(struct x10state)*256,
		MS_SYNC | MS_INVALIDATE);
#else
	lseek(mfd, 0, SEEK_SET);
	write(mfd, xmap, 256*sizeof(struct x10state));
	close(mfd);
#endif
	return 0;
}


int X10map::GetActiveUnit (int housecode)
{
	int house = housecode - 'A';
	return (active_unit [house]);
}


int X10map::GetTimeout (void)
{
	CheckTimeout();
	return (timeout_pending);
}

void X10map::Timeout (void)
{
	CheckTimeout();
}

void X10map::CheckTimeout (void)
{
	int status;
	time_t now = time (NULL);
	if (timeout_pending) {
		if (wait4 (kidpid, &status, WNOHANG, NULL) > 0) {
			if (WIFEXITED (status)) {
				timeout_pending = 0;
				if (WEXITSTATUS (status) == 0) {
					log (TRACE, "Child process finished");
				} else {
					log (ERROR, "Child process: exit %d",
						WEXITSTATUS (status));
				}
			} else if (WIFSIGNALED (status)) {
				timeout_pending = 0;
				log (WARN, "Child process: killed by signal %d",
					WTERMSIG (status));
			}
		}
	}
	if (!timeout_pending) {
		log (TRACE, "Execution queue is %s.",
			q.empty () ? "empty" : "populated");
		if (!q.empty ()) {
			x10event &event = q.front();
			ExecuteCommand (event.house, event.unit, event.cmd);
			q.pop();
		}
	} else if (timeout_pending <= now) {
		log (ERROR, "Killing rogue child %d", kidpid);
		kill (kidpid, SIGKILL);
		timeout_pending = now + timeout;
	}
}


void X10map::ExecuteCommand(int house, int unit, int cmd)
{
	char prog[PATH_MAX];
	char housecode[25];
	char unitcode[25];

	sprintf(prog, run_format, (char) house, unit);
	sprintf(housecode, "%c", (char) house);
	sprintf(unitcode,  "%02d", unit);
	const char *av[] = {
		prog, housecode, unitcode, parameter [cmd], NULL
	};
	log (INFO, "Executing: %s %s %s %s", prog, housecode, unitcode,
		parameter [cmd]);
	if ((kidpid = fork()) == -1) {
		log (ERROR, "fork(): %s", strerror (errno));
		return;
	}
	if (kidpid == 0) {
		execv(prog, (char *const *) av);
		log (ERROR, "%s: %s", prog, strerror (errno));
		_exit(1);
	}
	timeout_pending = time (NULL) + timeout;
}

