#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <grp.h>
#include <pwd.h>
#include <sys/types.h>

#include "general.H"
#include "x10io.H"
#include "settings.H"
#include "twoway.H"
#include "cp290.H"

#define DEFAULT_X10_TTY	"/dev/ttyS1"

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

static X10 *line;
static FILE *logfile;
static char *logfile_name;
static char *start_script;
static int newLog;
static char *program_name;
static char print_file_line;

int debug_level = DEBUG_INFO;

void log (const char *file, int line, const char *function, int debug,
	 const char *format,
	  ...) {
	va_list args;
	va_start (args, format);

	static int old_year, old_month, old_day;

	if (newLog && logfile) {
		fclose (logfile);
		logfile = NULL;
		newLog = 0;
		old_year = old_month = old_day = 0;
	}
	if (debug > debug_level)
		return;
	if (!logfile && logfile_name && *logfile_name && (strcmp (logfile_name, "-") != 0)) {
		logfile = fopen (logfile_name, "a");
		if (!logfile) {
			perror (logfile_name);
			fprintf (stderr, "Logging to standard error.\n");
		}
	}

	FILE *logto = logfile ? logfile : stderr;

	time_t nowtime;
	time (&nowtime);

	struct tm *now = localtime (&nowtime);
	if (old_year != now->tm_year || old_month != now->tm_mon ||
	    old_day != now->tm_mday) {
		fprintf (logto, "\n\n---- %04d-%02d-%02d ----\n\n",
			 now->tm_year + 1900, now->tm_mon + 1, now->tm_mday);
		old_year = now->tm_year;
		old_month = now->tm_mon;
		old_day = now->tm_mday;
	}

	if (print_file_line) {
		fprintf (logto, *function ? "%s:%d (%s)\n" : "%s:%d\n",
			 file, line, function);
	}
	const char *level;
	switch (debug) {
		case (DEBUG_ERROR):	level = "Error"; break;
		case (DEBUG_WARN):	level = "Warning"; break;
		case (DEBUG_INFO):	level = "Info"; break;
		case (DEBUG_TRACE):	level = "Trace"; break;
		default:	level = "?"; break;
	}
	fprintf (logto, "%02d:%02d:%02d %s %s: ",
		 now->tm_hour, now->tm_min, now->tm_sec, program_name, level);
	vfprintf (logto, format, args);
	fprintf (logto, "\n");
	fflush (logto);
	va_end (args);
}

void loghex (const char *file, int line, const char *function, int debug,
             const char *buffer, int size)
{
	if (debug > debug_level)
		return;

	FILE *logto = logfile ? logfile : stderr;

	for (int i = 0; i < size; i += 16) {
		char block [17];
		fprintf (logto, "         %04x ", i);
		for (int j = 0; j < 16; j++) {
			if (j % 4 == 0)
				fputc (' ', logto);
			if (i + j < size) {
				fprintf (logto, "%02x",
					(unsigned char) buffer [i + j]);
				block [j] = isprint(buffer [i+j]) ?
					    buffer [i+j] : '.';
			
			} else {
				fprintf (logto, "  ");
				block [j] = '\0';
			}
		}
		block[16] = '\0';
		fprintf (logto, " %s\n", block);
	}
}

void HupHandler(int sig)
{
	// set a flag to open a new log.
	newLog = 1;
}

void TermHandler(int sig)
{
	line->Shutdown();
}



void usage (void) {
	fprintf (stderr, "%s: Usage: %s [options]\n"
		"Options are:\n"
		"   -c timeout    : command timeout\n"
		"   -d device     : x10 controller device\n"
		"   -t timeout    : x10 device timeout\n"
		"   -T devicetype : X10 device type (twoway, cm17a, ...)\n"
		"   -s socketfile : socket interface file\n"
		"   -m map file   : map file\n"
		"   -g group      : group to run as\n"
		"   -u user       : user to run as \n"
		"   -L filename   : log filename\n"
		"   -i initfile   : initialization file\n"
		"   -S initscript : startup script\n"
		"   -z level      : set debug level\n"
		, program_name, program_name);
}



static void set_string (char **dest, const char *value) {
	if (*dest)
		free (*dest);
	*dest = strdup (value);
	if (!*dest) {
		log (ERROR, "strdup(): %s", strerror (errno));
		exit (1);
	}
}


static void config_io (const char *file, CONFIGURATION *config) {
	int status;
	ReadFileSettings settings (file, "Basic", &status,
				 1 /* use defaults if file can't be opened */);

	int ok;
	ok = settings.Value ("SocketFile", &(config->socket_file), "/tmp/x10")
	  && settings.Value ("MapFile",    &(config->map_file), "/tmp/x10map")
	  && settings.Value ("Port",       &(config->device), DEFAULT_X10_TTY)
	  && settings.Value ("Group",      &(config->group), "")
	  && settings.Value ("User",       &(config->user), "")
	  && settings.Value ("InitScript", &start_script, "")
	  && settings.Value ("LogFile",    &logfile_name, "")
	  && settings.Value ("Timeout",    &(config->device_timeout), 10)
	  && settings.Value ("CommandTimeout",&(config->command_timeout), 15)
	  && settings.Value ("DeviceType", &(config->device_type), "twoway")
	  && settings.Value ("RunMe",      &(config->run_format),
					   "/usr/local/bin/x10/%c%02d")
	  && settings.Value ("KeepInForeground",
					   &(config->foreground),
					   0);

	if (!ok) {
		perror ("config_io");
	}
}

int main(int argc, char **argv)
{
	if ((program_name = strrchr (*argv, '/')))
		program_name++;
	else
		program_name = *argv;

	CONFIGURATION config;
	memset (&config, 0, sizeof (config));
	int option;

	config_io ("x10iod", &config);
	
	while ((option = getopt (argc, argv, "t:c:T:r:m:i:hd:g:u:s:z:ZL:S:k")) != EOF) {
		switch (option) {
			case 'i':
				config_io (optarg, &config);
				break;
			case 'k':
				config.foreground = 1;
				break;
			case 'd':
				set_string (&(config.device), optarg);
				break;
			case 'u':
				set_string (&(config.user), optarg);
				break;
			case 'g':
				set_string (&(config.group), optarg);
				break;
			case 'm':
				set_string (&(config.map_file), optarg);
				break;
			case 's':
				set_string (&(config.socket_file), optarg);
				break;
			case 'S':
				set_string (&start_script, optarg);
				break;
			case 'r':
				set_string (&(config.run_format), optarg);
				break;
			case 't':
				config.device_timeout = MAX(3,atoi (optarg));
				break;
			case 'c':
				config.command_timeout = MAX(10,atoi (optarg));
				break;
			case 'L':
				set_string (&logfile_name, optarg);
				break;
			case 'z':
				debug_level = atoi (optarg);
				break;
			case 'Z':
				debug_level = 255;
				set_string (&logfile_name, "");
				print_file_line = 1;
				break;
			case 'T':
				set_string (&config.device_type, optarg);
				break;
			case 'h':
			case '?':
				usage ();
				return 1;
		}
	}

	// Set the user and group this process runs as, if they were specified
	if (*config.group) {
		struct group *mygroup;
		if (! ((mygroup = getgrnam (config.group) ))) {
			log (ERROR, "group %s: Not found", config.group);
			return 1;
		}
		if (setgid (mygroup->gr_gid) < 0) {
			log (ERROR, "setgid(): %s", strerror (errno));
			return 1;
		}
		if (chown (config.device, -1U, mygroup->gr_gid) < 0) {
			log (ERROR, "%s: Can not set group %s: %s",
			     config.device, config.group, strerror (errno));
			return 1;
		}
		endgrent ();
	}

	if (*(config.user)) {
		struct passwd *mypasswd;
		if (! ((mypasswd = getpwnam (config.user) ))) {
			log (ERROR, "user %s: Not found", config.user);
			return 1;
		}
		if (setuid (mypasswd->pw_uid) < 0) {
			log (ERROR, "setuid(): %s", strerror (errno));
			return 1;
		}

		// Put new USER= in environment
		char *newenv = (char *) malloc (strlen (mypasswd->pw_name) + 6);
		if (!newenv) {
			log (ERROR, "malloc(): %s", strerror (errno));
			return 1;
		}
		strcpy (newenv, "USER=");
		strcat (newenv, mypasswd->pw_name);
		if (putenv (newenv) != 0) {
			log (ERROR, "putenv(): %s", strerror (errno));
			return 1;
		};

		// Put new HOME= in environment
		newenv = (char *) malloc (strlen (mypasswd->pw_dir) + 6);
		if (!newenv) {
			log (ERROR, "malloc(): %s", strerror (errno));
			return 1;
		}
		strcpy (newenv, "HOME=");
		strcat (newenv, mypasswd->pw_dir);
		if (putenv (newenv) != 0) {
			log (ERROR, "putenv(): %s", strerror (errno));
			return 1;
		};

		if (chdir (mypasswd->pw_dir) != 0) {
			log (ERROR, "chdir(%s): %s", mypasswd->pw_dir,
				strerror (errno));
			return 1;
		}
	}

	endpwent ();

	log (INFO, "X10-device=%s   Socketfile=%s   type=%s", config.device,
		 config.socket_file, config.device_type);

	if (strcasecmp (config.device_type, "twoway") == 0) {
		line = new TwoWay (&config);
	} else if (strcasecmp (config.device_type, "cp290") == 0) {
		line = new CP290 (&config);
	} else {
		log (ERROR, "%s: Invalid interface type", config.device_type);
		usage ();
		exit (3);
	}
	if (!line) {
		log (ERROR, "%s: Failed to instantiate", config.device_type);
		exit (4);
	}
	if (line->Open()) {
		log (ERROR, "%s @ %s: Failed to open",
			 config.device_type, config.device);
		exit (5);
	}
	signal(SIGHUP, HupHandler);
	// signal(SIGINT, TermHandler);
	signal(SIGTERM, TermHandler);
	unlink(config.socket_file);
	// Ignore error - we were just unlinking out of paranoia.

	if (strcmp (start_script, "") != 0) {
		log (TRACE, "Running startup script: %s", start_script);
		int startup_status;
		startup_status = system (start_script);
		if (startup_status != 0) {
			log (ERROR, "%s: Error running startup script",
			     start_script);
		}
	}

	int status = 0;
	// when requested in foreground or debugging, don't fork.
	if (!config.foreground && debug_level != 255) {
		status = fork ();
		if (status < 0) {
			log (ERROR, "fork(): %s", strerror (errno));
			exit (6);
		}
	}
	if (status == 0) {
		// We are the child.
		line->Run(&config);
	}
	delete line;
	return 0;
}


