/*
spacenavd - a free software replacement driver for 6dof space-mice.
Copyright (C) 2007-2021 John Tsiombikas <nuclear@member.fsf.org>

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 3 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, see <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/un.h>
#include "spnavd.h"
#include "logger.h"
#include "dev.h"
#include "hotplug.h"
#include "client.h"
#include "proto_unix.h"
#ifdef USE_X11
#include "proto_x11.h"
#endif

static void print_usage(const char *argv0);
static void cleanup(void);
static void redir_log(int fallback_syslog);
static void daemonize(void);
static int write_pid_file(void);
static int find_running_daemon(void);
static void handle_events(fd_set *rset);
static void sig_handler(int s);
static char *fix_path(char *str);

char *cfgfile = DEF_CFGFILE;
static char *logfile = DEF_LOGFILE;
static int pfd[2];

int main(int argc, char **argv)
{
	int i, pid, ret, become_daemon = 1;
	int force_logfile = 0;

	for(i=1; i<argc; i++) {
		if(argv[i][0] == '-') {
			if(argv[i][1] == 'v') {
				char *p = argv[i] + 1;
				while(*p++) verbose++;

			} else if(argv[i][2] == 0) {
				switch(argv[i][1]) {
				case 'd':
					become_daemon = !become_daemon;
					break;

				case 'c':
					if(!argv[++i]) {
						fprintf(stderr, "-c must be followed by the config file name\n");
						return 1;
					}
					cfgfile = fix_path(argv[i]);
					break;

				case 'l':
					if(!argv[++i]) {
						fprintf(stderr, "-l must be followed by a logfile name or \"syslog\"\n");
						return 1;
					}
					if(strcmp(argv[i], "syslog") == 0) {
						logfile = 0;
					} else {
						logfile = fix_path(argv[i]);
						if(strcmp(logfile, argv[i]) != 0) {
							printf("logfile: %s\n", logfile);
						}
						/* when the user specifies a log file in the command line
						 * the expectation is to use it, regardless of whether
						 * spacenavd is started daemonized or not.
						 */
						force_logfile = 1;
					}
					break;

				case 'V':
					printf("spacenavd " VERSION "\n");
					return 0;

				case 'h':
					print_usage(argv[0]);
					return 0;

				default:
					fprintf(stderr, "invalid option: %s\n", argv[i]);
					return 1;
				}

			} else if(strcmp(argv[i], "-version") == 0) {
				printf("spacenavd " VERSION "\n");
				return 0;

			} else if(strcmp(argv[i], "-help") == 0 || strcmp(argv[i], "--help") == 0) {
				print_usage(argv[0]);
				return 0;

			} else {
				fprintf(stderr, "invalid option: %s\n\n", argv[i]);
				print_usage(argv[0]);
				return 1;
			}

		} else {
			fprintf(stderr, "unexpected argument: %s\n\n", argv[i]);
			print_usage(argv[0]);
			return 1;
		}
	}

	if((pid = find_running_daemon()) != -1) {
		fprintf(stderr, "Spacenav daemon already running (pid: %d). Aborting.\n", pid);
		return 1;
	}

	if(become_daemon) {
		daemonize();
	} else {
		if(force_logfile) {
			redir_log(0);
		}
	}
	write_pid_file();

	logmsg(LOG_INFO, "Spacenav daemon " VERSION "\n");

	read_cfg(cfgfile, &cfg);
	prev_cfg = cfg;
	pipe(pfd);

	signal(SIGINT, sig_handler);
	signal(SIGTERM, sig_handler);
	signal(SIGSEGV, sig_handler);
	signal(SIGHUP, sig_handler);
	signal(SIGUSR1, sig_handler);
	signal(SIGUSR2, sig_handler);
	signal(SIGPIPE, SIG_IGN);

	init_devices();
	init_hotplug();

	init_unix();
#ifdef USE_X11
	init_x11();
#endif

	atexit(cleanup);

	for(;;) {
		fd_set rset;
		int fd, max_fd = 0;
		struct client *client_iter;
		struct device *dev;

		FD_ZERO(&rset);

		dev = get_devices();
		while(dev) {
			if((fd = get_device_fd(dev)) != -1) {
				FD_SET(fd, &rset);
				if(fd > max_fd) max_fd = fd;
			}
			dev = dev->next;
		}

		if((fd = get_hotplug_fd()) != -1) {
			FD_SET(fd, &rset);
			if(fd > max_fd) max_fd = fd;
		}

		/* the UNIX domain socket listening for connections */
		if((fd = get_unix_socket()) != -1) {
			FD_SET(fd, &rset);
			if(fd > max_fd) max_fd = fd;
		}

		/* all the UNIX socket clients */
		client_iter = first_client();
		while(client_iter) {
			if(get_client_type(client_iter) == CLIENT_UNIX) {
				int s = get_client_socket(client_iter);
				assert(s >= 0);

				FD_SET(s, &rset);
				if(s > max_fd) max_fd = s;
			}
			client_iter = next_client();
		}

		/* and the X server socket */
#ifdef USE_X11
		if((fd = get_x11_socket()) != -1) {
			FD_SET(fd, &rset);
			if(fd > max_fd) max_fd = fd;
		}
#endif

		/* also the self-pipe read-end for safe SIGHUP handling */
		FD_SET(pfd[0], &rset);
		if(pfd[0] > max_fd) max_fd = fd;

		do {
			/* if there is at least one device out of the deadzone and repeat is enabled
			 * wait for only as long as specified in cfg.repeat_msec
			 */
			struct timeval tv, *timeout = 0;
			if(cfg.repeat_msec >= 0) {
				dev = get_devices();
				while(dev) {
					if(is_device_valid(dev) && !in_deadzone(dev)) {
						tv.tv_sec = cfg.repeat_msec / 1000;
						tv.tv_usec = cfg.repeat_msec % 1000;
						timeout = &tv;
						break;
					}
					dev = dev->next;
				}
			}

			ret = select(max_fd + 1, &rset, 0, 0, timeout);
		} while(ret == -1 && errno == EINTR);

		if(ret > 0) {
			handle_events(&rset);
		} else {
			if(cfg.repeat_msec >= 0) {
				dev = get_devices();
				while(dev) {
					if(!in_deadzone(dev)) {
						repeat_last_event(dev);
					}
					dev = dev->next;
				}
			}
		}
	}
	return 0;	/* unreachable */
}

static void print_usage(const char *argv0)
{
	printf("usage: %s [options]\n", argv0);
	printf("options:\n");
	printf(" -d: do not daemonize\n");
	printf(" -c <file>: config file path (default: " DEF_CFGFILE ")\n");
	printf(" -l <file>|syslog: log file path or log to syslog (default: " DEF_LOGFILE ")\n");
	printf(" -v: verbose output (use multiple times for greater effect)\n");
	printf(" -V,-version: print version number and exit\n");
	printf(" -h,-help: print usage information and exit\n");
}

static void cleanup(void)
{
	struct device *dev;

#ifdef USE_X11
	close_x11();	/* call to avoid leaving garbage in the X server's root windows */
#endif
	close_unix();

	shutdown_hotplug();

	dev = get_devices();
	while(dev) {
		struct device *tmp = dev;
		dev = dev->next;
		remove_device(tmp);
	}

	remove(PIDFILE);
}

static void redir_log(int fallback_syslog)
{
	int i, fd = -1;

	if(logfile) {
		fd = start_logfile(logfile);
	}

	if(fd >= 0 || fallback_syslog) {
		/* redirect standard input/output/error
		 * best effort attempt to make either the logfile or the syslog socket
		 * accessible through stdout/stderr, just in case any printfs survived
		 * the logmsg conversion.
		 */
		for(i=0; i<3; i++) {
			close(i);
		}

		open("/dev/zero", O_RDONLY);

		if(fd == -1) {
			fd = start_syslog(SYSLOG_ID);
			dup(1);		/* not guaranteed to work */
		} else {
			dup(fd);
		}
	}

	setvbuf(stdout, 0, _IOLBF, 0);
	setvbuf(stderr, 0, _IONBF, 0);
}

static void daemonize(void)
{
	int pid;

	chdir("/");

	redir_log(1);

	/* release controlling terminal */
	if((pid = fork()) == -1) {
		perror("failed to fork");
		exit(1);
	} else if(pid) {
		exit(0);
	}

	setsid();
}

static int write_pid_file(void)
{
	FILE *fp;
	int pid = getpid();

	if(!(fp = fopen(PIDFILE, "w"))) {
		return -1;
	}
	fprintf(fp, "%d\n", pid);
	fclose(fp);
	return 0;
}

static int find_running_daemon(void)
{
	FILE *fp;
	int s, pid;
	struct sockaddr_un addr;

	/* try to open the pid-file */
	if(!(fp = fopen(PIDFILE, "r"))) {
		return -1;
	}
	if(fscanf(fp, "%d\n", &pid) != 1) {
		fclose(fp);
		return -1;
	}
	fclose(fp);

	/* make sure it's not just a stale pid-file */
	if((s = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
		return -1;
	}
	memset(&addr, 0, sizeof addr);
	addr.sun_family = AF_UNIX;
	strncpy(addr.sun_path, SOCK_NAME, sizeof addr.sun_path);

	if(connect(s, (struct sockaddr*)&addr, sizeof addr) == -1) {
		close(s);
		return -1;
	}

	/* managed to connect alright, it's running... */
	close(s);
	return pid;
}

static void handle_events(fd_set *rset)
{
	int dev_fd, hotplug_fd;
	struct device *dev;
	struct dev_input inp;

	/* handle signal pipe */
	if(FD_ISSET(pfd[0], rset)) {
		int tmp;
		read(pfd[0], &tmp, sizeof tmp);	/* eat up the junk char */

		read_cfg(cfgfile, &cfg);
		cfg_changed();
	}

	/* handle anything coming through the UNIX socket */
	handle_uevents(rset);

#ifdef USE_X11
	/* handle any X11 events (magellan protocol) */
	handle_xevents(rset);
#endif

	/* finally read any pending device input data */
	dev = get_devices();
	while(dev) {
		/* keep the next pointer because read_device can potentially destroy
		 * the device node if the read fails.
		 */
		struct device *next = dev->next;

		if((dev_fd = get_device_fd(dev)) != -1 && FD_ISSET(dev_fd, rset)) {
			/* read an event from the device ... */
			while(read_device(dev, &inp) != -1) {
				/* ... and process it, possibly dispatching a spacenav event to clients */
				process_input(dev, &inp);
			}
			/* flush any pending events if we run out of input */
			inp.type = INP_FLUSH;
			process_input(dev, &inp);
		}
		dev = next;
	}

	if((hotplug_fd = get_hotplug_fd()) != -1) {
		if(FD_ISSET(hotplug_fd, rset)) {
			handle_hotplug();
		}
	}
}

void cfg_changed(void)
{
	if(cfg.led != prev_cfg.led) {
		struct device *dev = get_devices();
		while(dev) {
			if(is_device_valid(dev)) {
				if(verbose) {
					logmsg(LOG_INFO, "led %s, device: %s\n", cfg.led ? (cfg.led == LED_AUTO ? "auto" : "on"): "off", dev->name);
				}
				if(cfg.led == LED_ON || (cfg.led == LED_AUTO && first_client())) {
					set_device_led(dev, 1);
				} else {
					set_device_led(dev, 0);
				}
			}
			dev = dev->next;
		}
	}

	if(strcmp(cfg.serial_dev, prev_cfg.serial_dev) != 0) {
		struct device *dev, *iter = get_devices();
		while(iter) {
			dev = iter;
			iter = iter->next;
			if(strcmp(dev->path, prev_cfg.serial_dev) == 0) {
				remove_device(dev);
			}
		}
		init_devices_serial();
	}

	prev_cfg = cfg;
}

/* signals usr1 & usr2 are sent by the spnav_x11 script to start/stop the
 * daemon's connection to the X server.
 */
static void sig_handler(int s)
{
	switch(s) {
	case SIGHUP:
		write(pfd[1], &s, 1);	/* write *something* to the pipe to trigger a re-read */
		break;

	case SIGSEGV:
		logmsg(LOG_ERR, "Segmentation fault caught, trying to exit gracefully\n");
	case SIGINT:
	case SIGTERM:
		exit(0);

#ifdef USE_X11
	case SIGUSR1:
		init_x11();
		break;

	case SIGUSR2:
		close_x11();
		break;
#endif

	default:
		break;
	}
}


static char *fix_path(char *str)
{
	char *buf, *tmp;
	int sz, len;

	if(str[0] == '/') return str;

	len = strlen(str) + 1;	/* +1 for the path separator */
	sz = PATH_MAX;

	if(!(buf = malloc(sz + len))) {
		perror("failed to allocate path buffer");
		return 0;
	}

	while(!getcwd(buf, sz)) {
		if(errno == ERANGE) {
			sz *= 2;
			if(!(tmp = realloc(buf, sz + len))) {
				perror("failed to reallocate path buffer");
				free(buf);
				return 0;
			}
			buf = tmp;
		} else {
			perror("getcwd failed");
			free(buf);
			return 0;
		}
	}

	sprintf(buf + strlen(buf), "/%s", str);
	return buf;
}
