/*******************************************************************************

  Copyright(c) 2003-2004 Aurelien Reynaud.

  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 2 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, write to the Free Software Foundation, Inc., 59
  Temple Place - Suite 330, Boston, MA  02111-1307, USA.

  The full GNU General Public License is included in this distribution in the
  file called COPYING.

*******************************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include "debug.h"
#include "ggiterm.h"
#include "lowlevel/lowlevel.h"

#include <stdio.h>
#include "lib/getopt.h"

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>	/* signals */

/* for openpty() */
#include <sys/ioctl.h> /* some systems (BSD) need it even with OPENPTY_HEADER */
#ifdef OPENPTY_HEADER
# include OPENPTY_HEADER
#else
# include <termios.h>
int openpty (int *amaster, int *aslave, char *name, struct termios *termp, struct winsize *winp);
#endif

/* for login_tty */
#ifdef NO_SYSTEM_LOGIN_TTY
int login_tty (int fd);
#else
# include <utmp.h>
#endif

/* for pid_t, strangely not included by OPENPTY_HEADER */
#include <sys/types.h>
/* for execve, read, write */
#include <unistd.h>
#include <fcntl.h>


/* for select and associated macros */
#ifdef HAVE_SYS_SELECT_H
# ifndef TV_IN_SYS_SELECT_H
#  include <sys/time.h>
# endif
# include <sys/select.h>
#else
# include <sys/time.h>
# include <sys/types.h>
# include <unistd.h>
#endif


/* Global variables */
int quit = 0;
int fontsize = -1;
int history = -1;
char *font_file = NULL;
char *arg_mode = NULL;
char **prog_argv = NULL;
char *default_argv[] = { DEFAULT_SHELL, NULL };

/* External functions */
int terminal_init (int width, int height, int history, int fd);
void terminal_exit ();
void terminal_recv (unsigned char **, size_t *);
void terminal_send (unsigned char *buffer, size_t *buffer_size);


void show_args (char **argv)
{
#ifdef DEBUG
	int i = 0;
	
	while (argv[i] != NULL) {
		debug (DEBUG_INIT, "argv[%d] = [%s]", i, argv[i]);
		i++;
	}
#endif /* DEBUG */
}

void version (void)
{
	fprintf (stdout, " ggiterm version %s by A. Reynaud\n", VERSION);
}

void usage (void)
{
	fprintf (stdout,
		"Usage: ggiterm [OPTIONS ...]\n\n"
		"Option                  Description\n");
	fprintf (stdout,
		" -h, --help             Show this help\n");
	fprintf (stdout,
		" -v, --version          Print version and exit\n");
	fprintf (stdout,
		" -d, --debuglevel n     Specify subsystem to debug. Add numbers to specify more than one.\n"
		"                        %d:none, %d:init, %d:input, %d:output, %d:freetype, %d: history, %d:functions (default:%d)\n",
		DEBUG_NONE, DEBUG_INIT, DEBUG_INPUT, DEBUG_OUTPUT, DEBUG_FT, DEBUG_HISTORY, DEBUG_FUNCTION, DEBUG_NONE);
	fprintf (stdout,
		" -f, --font file        The font to use\n");
	fprintf (stdout,
		" -s, --font-size n      Set the size of the selected font (default: %d)\n",
	        DEFAULT_FONTSIZE);
	fprintf (stdout,
		" -l, --history n        Number of history lines (default: visible lines x 2)\n");
	fprintf (stdout,
		" -m, --mode ggi_mode    Try this mode (default: target specific, format: see GGI doc)\n");
	fprintf (stdout,
		" -e, --exec prog [args] Execute prog instead of the default shell\n"
		"                        (must be the last option on the command line)\n");
}

int parse_options (int argc, char **argv)
{     
	int c;
	int go_on = 1;
	char *endptr;
	struct option long_options[] = {
		{"help", no_argument, NULL, 'h'},
		{"version", no_argument, NULL, 'v'},
		{"debuglevel", required_argument, NULL, 'd'},
		{"font", required_argument, NULL, 'f'},
		{"font-size", required_argument, NULL, 's'},
		{"history", required_argument, NULL, 'l'},
		{"mode", required_argument, NULL, 'm'},
		{"exec", required_argument, NULL, 'e'},
		{0, 0, 0, 0}
		};
 
	while (go_on) {
		c = getopt_long (argc, argv, "hvd:f:s:l:m:e:", long_options, NULL);
		if (c == -1) break;

		switch (c) {
			case 'f':
#ifdef HAVE_FREETYPE
				if (font_file == NULL) {
					font_file = optarg;
				} else {
					error ("Argument set twice");
					return 1;
				}
				break;
#else
				error ("Freetype support not compiled in");
				return 1;
#endif /* HAVE_FREETYPE */
			case 'm':
				if (arg_mode == NULL) {
					arg_mode = optarg;
				} else {
					error ("Argument set twice");
					return 1;
				}
				break;
			case 'd':
#ifdef DEBUG
				if (debuglevel == 0) {
					debuglevel = (int)strtol (optarg, &endptr, 0);
					if (*endptr != '\0') {
						error ("Wrong argument type");
						return 1;
					}
				} else {
					error ("Argument set twice");
					return 1;
				}
				break;
#else
			error ("Debug support not compiled in");
			return 1;
#endif /* DEBUG */
				break;
			case 'e':
				prog_argv = argv + optind - 1;
				prog_argv[0] = optarg;
				go_on = 0;
				break;
			case 's':
#ifdef HAVE_FREETYPE
				if (fontsize == -1) {
					fontsize = (int)strtol (optarg, &endptr, 0);
					if (*endptr != '\0') {
						error ("Wrong argument type");
						return 1;
					}
				} else {
					error ("Argument set twice");
					return 1;
				}
				break;
#else
				error ("Freetype support not compiled in");
				return 1;
#endif /* HAVE_FREETYPE */   
			case 'l':
				if (history == -1) {
					history = (int)strtol (optarg, &endptr, 0);
					if (*endptr != '\0') {
						error ("Wrong argument type");
						return 1;
					}
				} else {
					error ("Argument set twice");
					return 1;
				}
				break;
			case 'v':
				version ();
				return 1;
			case '?':
			case 'h':
			default:
				usage ();
				return 1;
			}
		}
	
	/* set default values if the corresponding options are not given */
	if (fontsize == -1) fontsize = DEFAULT_FONTSIZE;
	if (prog_argv == NULL) {
		prog_argv = default_argv;
		if (getenv ("SHELL") != NULL) {
			prog_argv[0] = getenv ("SHELL");
		}
	}	
	return 0;
}

void child_died ()
{
	debug (DEBUG_FUNCTION, "Entering");
	quit = 2;
	debug (DEBUG_FUNCTION, "Leaving");
}

int main (int argc, char **argv)
{
	int err;
	pid_t shell_pid;
	unsigned char app_output[APP_OUTPUT_SIZE];
	size_t app_output_size = 0;
	unsigned char *input;
	size_t input_size = 0;
#ifndef DEBUG /* buf is best set to NULL because of buffer overflow risk */
	char *buf = NULL;
#else         /* ... but it's useful when debugging! */
	char buf[128];
#endif
	int master_fd, slave_fd;
	struct timeval tv;
	fd_set rfds, wfds;
	int fd_flags;
	int width, height;


	debug (DEBUG_FUNCTION, "Entering");
	
	err = parse_options (argc, argv);
	debug (DEBUG_INIT, "Command line arguments after parsing:");
	show_args (argv);
	if (err) {
		return 1;
	}
	debug (DEBUG_INIT, "exec-ed program arguments:");
	show_args (prog_argv);

	debug(DEBUG_INIT, "Initializing low level interface");
	err = lowlevel_init(arg_mode, font_file, fontsize, prog_argv[0], &width, &height);
	if (err) {
		error("Low level initialization failed");
		return 1;
	}
	debug(DEBUG_INIT, "Low level initialization complete");

	err = openpty(&master_fd, &slave_fd, buf, NULL, NULL);
	if (err) {
		error("Couldn't find any pseudo-tty");
		lowlevel_exit();
		return 1;
	}
	
	debug (DEBUG_INIT, "Initializing terminal");
	err = terminal_init(width, height, history, master_fd);
	if (err) {
		error("Cannot setup terminal");
		close(master_fd);
		close(slave_fd);
		lowlevel_exit();
		return 1;
	}
	debug(DEBUG_INIT, "Terminal setup complete");

	signal (SIGCHLD, child_died);

	shell_pid = fork ();
	if (shell_pid == -1) {
		error ("Couldn't fork! Exiting.");
		terminal_exit ();
		close (master_fd);
		close (slave_fd);
		lowlevel_exit ();
		return 1;
	}
	if (shell_pid == 0) {
		close (master_fd);
		login_tty (slave_fd);
#ifdef HAVE_PUTENV
		putenv ("TERM=ggiterm");
#else
		setenv ("TERM", "ggiterm", 1);
#endif /* HAVE_PUTENV */
		debug (DEBUG_INIT,
		       "This is the forked process (PID %d, child of PID %d)",
		       (int)getpid (), (int)getppid ());
		debug (DEBUG_INIT, "Now exec-ing '%s'", prog_argv[0]);
		debug (DEBUG_INIT, "Good luck debugging ggiterm!");
		execvp (prog_argv[0], prog_argv);
		error ("exec() failed. Exiting!");
		return 1;
	}
	close (slave_fd);
	debug (DEBUG_INIT, "Child forked (PID %d), pseudo tty is %s",
	       (int)shell_pid, buf);
	debug (DEBUG_INIT, "Waiting for its death...");

	fd_flags = fcntl(master_fd, F_GETFL);
	fd_flags |= O_NONBLOCK;
	fcntl(master_fd, F_SETFL, fd_flags);
	
	while (!quit) {
		/* do we have pending GGI events?
		 * (non blocking)
		 */
		/*TerminalWasResized ();*/
		if (!input_size) terminal_recv (&input, &input_size);

		tv.tv_sec  = 0;
		tv.tv_usec = 20000;
		/*tv.tv_usec = 0;*/
		FD_ZERO (&rfds);
		FD_ZERO (&wfds);
		FD_SET (master_fd, &rfds);
		/* if we have anything pending, then try to write it */
		if (input_size)
			FD_SET (master_fd, &wfds);

		err = select (master_fd + 1, &rfds, &wfds, NULL, &tv);
		if (err < 0) {
			if (errno != EINTR) {
				perror ("ggiterm: select");
				/* Maybe quit here */
			}
			continue;
		}
		if (err) {
			/* debug only if something happened */
			debug ((DEBUG_INPUT|DEBUG_OUTPUT),
			       "%d file descriptors updated", err);
		}

		if (FD_ISSET (master_fd, &rfds)) {
			debug (DEBUG_OUTPUT, "master_fd has data pending");
			debug (DEBUG_OUTPUT,
			       "Buffer index is %d", (int)app_output_size);
			do {
				err = read (master_fd,
				            app_output + app_output_size,
				            APP_OUTPUT_SIZE - app_output_size);
			} while (err < 0 && (errno == EINTR) && !quit);
			/* if err < 0, errno should be EINTR or EAGAIN.
			   EINTR is handled above, and EAGAIN is just ignored.
			   If errno has another value, never use the fd again */
			if (err == 0) quit = 1;
			if (err < 0 && errno != EAGAIN) {
				perror ("ggiterm: read");
				quit = 3;
			}
			if (quit) break;
			if (errno != EAGAIN) {
				debug (DEBUG_OUTPUT,
				       "Read %d bytes from master_fd", err);
				app_output_size += err;
				debug (DEBUG_OUTPUT,
				       "Buffer index is now %d",
				       (int)app_output_size);
#ifndef JUMP_SCROLL
				terminal_send (app_output, &app_output_size);
#endif
			}
		}
#ifdef JUMP_SCROLL
		/* Called even with empty input. This is interpreted as "flush
		   pending scrollup */
		terminal_send (app_output, &app_output_size);
#endif

		if (FD_ISSET (master_fd, &wfds)) {
			debug (DEBUG_INPUT, "master_fd is ready for writing");
			do err = write (master_fd, input, input_size);
			while (err < 0 && (errno == EINTR) && !quit);
			/* if err < 0, errno should be EINTR or EAGAIN.
			   EINTR is handled above, and EAGAIN is just ignored.
			   If errno has another value, never use the fd again */
			if (err < 0 && errno != EAGAIN) {
				perror ("ggiterm: write");
				quit = 3;
			}
			if (quit) break;
			if (errno != EAGAIN) {
				debug (DEBUG_INPUT,
		 		       "Written %d bytes out of %d to master_fd",
				       err, (int)input_size);
				input += err;
				input_size -= err;
			}
		}
	}


	switch (quit) {
	case 1:
		debug (DEBUG_INIT, "Exiting: EOF condition");
		break;
	case 2:
		debug (DEBUG_INIT, "Exiting: Child process has died");
		break;
	case 3:
		debug (DEBUG_INIT, "Exiting: I/O error");
		break;
	}
	terminal_exit ();
	close (master_fd);
	lowlevel_exit ();
	
	debug (DEBUG_FUNCTION, "Leaving");
	return 0;
}
