#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <locale.h>

#include <unistd.h>
#include <termios.h>

#include <fcntl.h>
#include <sys/termios.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <dirent.h>

#if HAVE_SYSV_PTY
#include <stropts.h>    /* System-V stream I/O */
char *ptsname(int fd);
int grantpt(int fd);
int unlockpt(int fd);
#endif

#include "libtecla.h"

/*
 * Pseudo-terminal devices are found in the following directory.
 */
#define PTY_DEV_DIR "/dev/"

/*
 * Pseudo-terminal controller device file names start with the following
 * prefix.
 */
#define PTY_CNTRL "pty"

/*
 * Pseudo-terminal slave device file names start with the following
 * prefix.
 */
#define PTY_SLAVE "tty"

/*
 * Specify the maximum suffix length for the control and slave device
 * names.
 */
#define PTY_MAX_SUFFIX 10

/*
 * Set the maximum length of the master and slave terminal device filenames,
 * including space for a terminating '\0'.
 */
#define PTY_MAX_NAME (sizeof(PTY_DEV_DIR)-1 + \
		      (sizeof(PTY_SLAVE) > sizeof(PTY_CNTRL) ? \
		       sizeof(PTY_SLAVE) : sizeof(PTY_CNTRL))-1 \
		      + PTY_MAX_SUFFIX + 1)
/*
 * Set the maximum length of an input line.
 */
#define PTY_MAX_LINE 4096

/*
 * Set the size of the buffer used for accumulating bytes written by the
 * user's terminal to its stdout.
 */
#define PTY_MAX_READ 1000

/*
 * Set the amount of memory used to record history.
 */
#define PTY_HIST_SIZE 10000

/*
 * Set the timeout delay used to check for quickly arriving
 * sequential output from the application.
 */
#define PTY_READ_TIMEOUT 100000    /* micro-seconds */

static int pty_open_master(const char *prog, int *cntrl, char *slave_name);
static int pty_open_slave(const char *prog, char *slave_name);
static int pty_child(const char *prog, int slave, char *argv[]);
static int pty_parent(const char *prog, int cntrl);
static int pty_stop_parent(int waserr, int cntrl, GetLine *gl, char *rbuff);
static GL_FD_EVENT_FN(pty_read_from_program);
static int pty_write_to_fd(int fd, const char *string, int n);
static void pty_child_exited(int sig);
static int pty_master_readable(int fd, long usec);

/*.......................................................................
 * Run a program with enhanced terminal editing facilities.
 *
 * Usage:
 *  enhance program [args...]
 */
int main(int argc, char *argv[])
{
  int cntrl = -1;  /* The fd of the pseudo-terminal controller device */
  int slave = -1;  /* The fd of the pseudo-terminal slave device */
  pid_t pid;       /* The return value of fork() */
  int status;      /* The return statuses of the parent and child functions */
  char slave_name[PTY_MAX_NAME]; /* The filename of the slave end of the */
                                 /*  pseudo-terminal. */
  char *prog;      /* The name of the program (ie. argv[0]) */
/*
 * Check the arguments.
 */
  if(argc < 2) {
    fprintf(stderr, "Usage: %s <program> [arguments...]\n", argv[0]);
    return 1;
  };
/*
 * Get the name of the program.
 */
  prog = argv[0];
/*
 * If the user has the LC_CTYPE or LC_ALL environment variables set,
 * enable display of characters corresponding to the specified locale.
 */
  (void) setlocale(LC_CTYPE, "");
/*
 * If the program is taking its input from a pipe or a file, or
 * sending its output to something other than a terminal, run the
 * program without tecla.
 */
  if(!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) {
    if(execvp(argv[1], argv + 1) < 0) {
      fprintf(stderr, "%s: Unable to execute %s (%s).\n", prog, argv[1],
	      strerror(errno));
      fflush(stderr);
      _exit(1);
    };
  };
/*
 * Open the master side of a pseudo-terminal pair, and return
 * the corresponding file descriptor and the filename of the
 * slave end of the pseudo-terminal.
 */
  if(pty_open_master(prog, &cntrl, slave_name))
    return 1;
/*
 * Set up a signal handler to watch for the child process exiting.
 */
  signal(SIGCHLD, pty_child_exited);
/*
 * The above signal handler sends the parent process a SIGINT signal.
 * This signal is caught by gl_get_line(), which resets the terminal
 * settings, and if the application signal handler for this signal
 * doesn't abort the process, gl_get_line() returns NULL with errno
 * set to EINTR. Arrange to ignore the signal, so that gl_get_line()
 * returns and we have a chance to cleanup.
 */
  signal(SIGINT, SIG_IGN);
/*
 * We will read user input in one process, and run the user's program
 * in a child process.
 */
  pid = fork();
  if(pid < 0) {
    fprintf(stderr, "%s: Unable to fork child process (%s).\n", prog,
	    strerror(errno));
    return 1;
  };
/*
 * Are we the parent?
 */
  if(pid!=0) {
    status = pty_parent(prog, cntrl);
    close(cntrl);
  } else {
    close(cntrl); /* The child doesn't use the slave device */
    signal(SIGCHLD, pty_child_exited);
    if((slave = pty_open_slave(prog, slave_name)) >= 0) {
      status = pty_child(prog, slave, argv + 1);
      close(slave);
    } else {
      status = 1;
    };
  };
  return status;
}

/*.......................................................................
 * Open the master side of a pseudo-terminal pair, and return
 * the corresponding file descriptor and the filename of the
 * slave end of the pseudo-terminal.
 *
 * Input/Output:
 *  prog  const char *  The name of this program.
 *  cntrl        int *  The file descriptor of the pseudo-terminal
 *                      controller device will be assigned tp *cntrl.
 *  slave_name  char *  The file-name of the pseudo-terminal slave device
 *                      will be recorded in slave_name[], which must have
 *                      at least PTY_MAX_NAME elements.
 * Output:
 *  return       int    0 - OK.
 *                      1 - Error.
 */
static int pty_open_master(const char *prog, int *cntrl, char *slave_name)
{
  char master_name[PTY_MAX_NAME]; /* The filename of the master device */
  DIR *dir;                       /* The directory iterator */
  struct dirent *file;            /* A file in "/dev" */
/*
 * Mark the controller device as not opened yet.
 */
  *cntrl = -1;
/*
 * On systems with the Sys-V pseudo-terminal interface, we don't
 * have to search for a free master terminal. We just open /dev/ptmx,
 * and if there is a free master terminal device, we are given a file
 * descriptor connected to it.
 */
#if HAVE_SYSV_PTY
  *cntrl = open("/dev/ptmx", O_RDWR);
  if(*cntrl >= 0) {
/*
 * Get the filename of the slave side of the pseudo-terminal.
 */
    char *name = ptsname(*cntrl);
    if(name) {
      if(strlen(name)+1 > PTY_MAX_NAME) {
	fprintf(stderr, "%s: Slave pty filename too long.\n", prog);
	return 1;
      };
      strncpy(slave_name, name, PTY_MAX_NAME);
/*
 * If unable to get the slave name, discard the controller file descriptor,
 * ready to try a search instead.
 */
    } else {
      close(*cntrl);
      *cntrl = -1;
    };
  } else {
#endif
/*
 * On systems without /dev/ptmx, or if opening /dev/ptmx failed,
 * we open one master terminal after another, until one that isn't
 * in use by another program is found.
 *
 * Open the devices directory.
 */
    dir = opendir(PTY_DEV_DIR);
    if(!dir) {
      fprintf(stderr, "%s: Couldn't open %s (%s)\n", prog, PTY_DEV_DIR,
	      strerror(errno));
      return 1;
    };
/*
 * Look for pseudo-terminal controller device files in the devices
 * directory.
 */
    while(*cntrl < 0 && (file = readdir(dir))) {
      if(strncmp(file->d_name, PTY_CNTRL, sizeof(PTY_CNTRL)-1) == 0) {
/*
 * Get the common extension of the control and slave filenames.
 */
	const char *ext = file->d_name + sizeof(PTY_CNTRL)-1;
	if(strlen(ext) > PTY_MAX_SUFFIX)
	  continue;
/*
 * Attempt to open the control file.
 */
	snprintf(master_name, sizeof(master_name), "%s%s%s", PTY_DEV_DIR, PTY_CNTRL, ext);
	*cntrl = open(master_name, O_RDWR);
	if(*cntrl < 0)
	  continue;
/*
 * Attempt to open the matching slave file.
 */
	snprintf(slave_name, PTY_MAX_NAME, "%s%s%s", PTY_DEV_DIR, PTY_SLAVE, ext);
      };
    };
    closedir(dir);
#if HAVE_SYSV_PTY
  };
#endif
/*
 * Did we fail to find a pseudo-terminal pair that we could open?
 */
  if(*cntrl < 0) {
    fprintf(stderr, "%s: Unable to find a free pseudo-terminal.\n", prog);
    return 1;
  };
/*
 * System V systems require the program that opens the master to
 * grant access to the slave side of the pseudo-terminal.
 */
#ifdef HAVE_SYSV_PTY
  if(grantpt(*cntrl) < 0 ||
     unlockpt(*cntrl) < 0) {
    fprintf(stderr, "%s: Unable to unlock terminal (%s).\n", prog,
	    strerror(errno));
    return 1;
  };
#endif
/*
 * Success.
 */
  return 0;
}

/*.......................................................................
 * Open the slave end of a pseudo-terminal.
 *
 * Input:
 *  prog   const char *  The name of this program.
 *  slave_name   char *  The filename of the slave device.
 * Output:
 *  return        int    The file descriptor of the successfully opened
 *                       slave device, or < 0 on error.
 */
static int pty_open_slave(const char *prog, char *slave_name)
{
  int fd;  /* The file descriptor of the slave device */
/*
 * Place the process in its own process group. In system-V based
 * OS's, this ensures that when the pseudo-terminal is opened, it
 * becomes the controlling terminal of the process.
 */
  if(setsid() < 0) {
    fprintf(stderr, "%s: Unable to form new process group (%s).\n", prog,
	    strerror(errno));
    return -1;
  };
/*
 * Attempt to open the specified device.
 */
  fd = open(slave_name, O_RDWR);
  if(fd < 0) {
    fprintf(stderr, "%s: Unable to open pseudo-terminal slave device (%s).\n",
	    prog, strerror(errno));
    return -1;
  };
/*
 * On system-V streams based systems, we need to push the stream modules
 * that implement pseudo-terminal and termio interfaces. At least on
 * Solaris, which pushes these automatically when a slave is opened,
 * this is redundant, so ignore errors when pushing the modules.
 */
#if HAVE_SYSV_PTY
  (void) ioctl(fd, I_PUSH, "ptem");
  (void) ioctl(fd, I_PUSH, "ldterm");
/*
 * On BSD based systems other than SunOS 4.x, the following makes the
 * pseudo-terminal the controlling terminal of the child process.
 * According to the pseudo-terminal example code in Steven's
 * Advanced programming in the unix environment, the !defined(CIBAUD)
 * part of the clause prevents this from being used under SunOS. Since
 * I only have his code with me, and won't have access to the book,
 * I don't know why this is necessary.
 */
#elif defined(TIOCSCTTY) && !defined(CIBAUD)
  if(ioctl(fd, TIOCSCTTY, (char *) 0) < 0) {
    fprintf(stderr, "%s: Unable to establish controlling terminal (%s).\n",
	    prog, strerror(errno));
    close(fd);
    return -1;
  };
#endif
  return fd;
}

/*.......................................................................
 * Read input from the controlling terminal of the program, using
 * gl_get_line(), and feed it to the user's program running in a child
 * process, via the controller side of the pseudo-terminal. Also pass
 * data received from the user's program via the conroller end of
 * the pseudo-terminal, to stdout.
 *
 * Input:
 *  prog  const char *  The name of this program.
 *  cntrl        int    The file descriptor of the controller end of the
 *                      pseudo-terminal.
 * Output:
 *  return       int    0 - OK.
 *                      1 - Error.
 */
static int pty_parent(const char *prog, int cntrl)
{
  GetLine *gl = NULL;  /* The gl_get_line() resource object */
  char *line;          /* An input line read from the user */
  char *rbuff=NULL;    /* A buffer for reading from the pseudo terminal */
/*
 * Allocate the gl_get_line() resource object.
 */
  gl = new_GetLine(PTY_MAX_LINE, PTY_HIST_SIZE);
  if(!gl)
    return pty_stop_parent(1, cntrl, gl, rbuff);
/*
 * Allocate a buffer to use to accumulate bytes read from the
 * pseudo-terminal.
 */
  rbuff = (char *) malloc(PTY_MAX_READ+1);
  if(!rbuff)
    return pty_stop_parent(1, cntrl, gl, rbuff);
  rbuff[0] = '\0';
/*
 * Register an event handler to watch for data appearing from the
 * user's program on the controller end of the pseudo terminal.
 */
  if(gl_watch_fd(gl, cntrl, GLFD_READ, pty_read_from_program, rbuff))
    return pty_stop_parent(1, cntrl, gl, rbuff);
/*
 * Read input lines from the user and pass them on to the user's program,
 * by writing to the controller end of the pseudo-terminal.
 */
  while((line=gl_get_line(gl, rbuff, NULL, 0))) {
    if(pty_write_to_fd(cntrl, line, strlen(line)))
       return pty_stop_parent(1, cntrl, gl, rbuff);
    rbuff[0] = '\0';
  };
  return pty_stop_parent(0, cntrl, gl, rbuff);
}

/*.......................................................................
 * This is a private return function of pty_parent(), used to release
 * dynamically allocated resources, close the controller end of the
 * pseudo-terminal, and wait for the child to exit. It returns the
 * exit status of the child process, unless the caller reports an
 * error itself, in which case the caller's error status is returned.
 *
 * Input:
 *  waserr   int    True if the caller is calling this function because
 *                  an error occured.
 *  cntrl    int    The file descriptor of the controller end of the
 *                  pseudo-terminal.
 *  gl   GetLine *  The resource object of gl_get_line().
 *  rbuff   char *  The buffer used to accumulate bytes read from
 *                  the pseudo-terminal.
 * Output:
 *  return  int    The desired exit status of the program.
 */
static int pty_stop_parent(int waserr, int cntrl, GetLine *gl, char *rbuff)
{
  int status;  /* The return status of the child process */
/*
 * Close the controller end of the terminal.
 */
  close(cntrl);
/*
 * Delete the resource object.
 */
  gl = del_GetLine(gl);
/*
 * Delete the read buffer.
 */
  if(rbuff)
    free(rbuff);
/*
 * Wait for the user's program to end.
 */
  (void) wait(&status);
/*
 * Return either our error status, or the return status of the child
 * program.
 */
  return waserr ? 1 : status;
}

/*.......................................................................
 * Run the user's program, with its stdin and stdout connected to the
 * slave end of the psuedo-terminal.
 *
 * Input:
 *  prog  const char *   The name of this program.
 *  slave        int     The file descriptor of the slave end of the
 *                       pseudo terminal.
 *  argv        char *[] The argument vector to pass to the user's program,
 *                       where argv[0] is the name of the user's program,
 *                       and the last argument is followed by a pointer
 *                       to NULL.
 * Output:
 *  return   int         If this function returns at all, an error must
 *                       have occured when trying to overlay the process
 *                       with the user's program. In this case 1 is
 *                       returned.
 */
static int pty_child(const char *prog, int slave, char *argv[])
{
  struct termios attr; /* The terminal attributes */
/*
 * We need to stop the pseudo-terminal from echoing everything that we send it.
 */
  if(tcgetattr(slave, &attr)) {
    fprintf(stderr, "%s: Can't get pseudo-terminal attributes (%s).\n", prog,
	    strerror(errno));
    return 1;
  };
  attr.c_lflag &= ~(ECHO);
  while(tcsetattr(slave, TCSADRAIN, &attr)) {
    if(errno != EINTR) {
      fprintf(stderr, "%s: tcsetattr error: %s\n", prog, strerror(errno));
      return 1;
    };
  };
/*
 * Arrange for stdin, stdout and stderr to be connected to the slave device,
 * ignoring errors that imply that either stdin or stdout is closed.
 */
  while(dup2(slave, STDIN_FILENO) < 0 && errno==EINTR)
    ;
  while(dup2(slave, STDOUT_FILENO) < 0 && errno==EINTR)
    ;
  while(dup2(slave, STDERR_FILENO) < 0 && errno==EINTR)
    ;
/*
 * Run the user's program.
 */
  if(execvp(argv[0], argv) < 0) {
    fprintf(stderr, "%s: Unable to execute %s (%s).\n", prog, argv[0],
	    strerror(errno));
    fflush(stderr);
    _exit(1);
  };
  return 0;  /* This should never be reached */
}

/*.......................................................................
 * This is the event-handler that is called by gl_get_line() whenever
 * there is tet waiting to be read from the user's program, via the
 * controller end of the pseudo-terminal. See libtecla.h for details
 * about its arguments.
 */
static GL_FD_EVENT_FN(pty_read_from_program)
{
  char *nlptr;   /* A pointer to the last newline in the accumulated string */
  char *crptr;   /* A pointer to the last '\r' in the accumulated string */
  char *nextp;   /* A pointer to the next unprocessed character */
/*
 * Get the read buffer in which we are accumulating a line to be
 * forwarded to stdout.
 */
  char *rbuff = (char *) data;
/*
 * New data may arrive while we are processing the current read, and
 * it is more efficient to display this here than to keep returning to
 * gl_get_line() and have it display the latest prefix as a prompt,
 * followed by the current input line, so we loop, delaying a bit at
 * the end of each iteration to check for more data arriving from
 * the application, before finally returning to gl_get_line() when
 * no more input is available.
 */
  do {
/*
 * Get the current length of the output string.
 */
    int len = strlen(rbuff);
/*
 * Read the text from the program.
 */
    int nnew = read(fd, rbuff + len, PTY_MAX_READ - len);
    if(nnew < 0)
      return GLFD_ABORT;
    len += nnew;
/*
 * Nul terminate the accumulated string.
 */
    rbuff[len] = '\0';
/*
 * Find the last newline and last carriage return in the buffer, if any.
 */
    nlptr = strrchr(rbuff, '\n');
    crptr = strrchr(rbuff, '\r');
/*
 * We want to output up to just before the last newline or carriage
 * return. If there are no newlines of carriage returns in the line,
 * and the buffer is full, then we should output the whole line. In
 * all cases a new output line will be started after the latest text
 * has been output. The intention is to leave any incomplete line
 * in the buffer, for (perhaps temporary) use as the current prompt.
 */
    if(nlptr) {
      nextp = crptr && crptr < nlptr ? crptr : nlptr;
    } else if(crptr) {
      nextp = crptr;
    } else if(len >= PTY_MAX_READ) {
      nextp = rbuff + len;
    } else {
      nextp = NULL;
    };
/*
 * Do we have any text to output yet?
 */
    if(nextp) {
/*
 * If there was already some text in rbuff before this function
 * was called, then it will have been used as a prompt. Arrange
 * to rewrite this prefix, plus the new suffix, by moving back to
 * the start of the line.
 */
      if(len > 0)
	(void) pty_write_to_fd(STDOUT_FILENO, "\r", 1);
/*
 * Write everything up to the last newline to stdout.
 */
      (void) pty_write_to_fd(STDOUT_FILENO, rbuff, nextp - rbuff);
/*
 * Start a new line.
 */
      (void) pty_write_to_fd(STDOUT_FILENO, "\r\n", 2);
/*
 * Skip trailing carriage returns and newlines.
 */
      while(*nextp=='\n' || *nextp=='\r')
	nextp++;
/*
 * Move any unwritten text following the newline, to the start of the
 * buffer.
 */
      memmove(rbuff, nextp, len - (nextp - rbuff) + 1);
    };
  } while(pty_master_readable(fd, PTY_READ_TIMEOUT));
/*
 * Make the incomplete line in the output buffer the current prompt.
 */
  gl_replace_prompt(gl, rbuff);
  return GLFD_REFRESH;
}

/*.......................................................................
 * Write a given string to a specified file descriptor.
 *
 * Input:
 *  fd             int     The file descriptor to write to.
 *  string  const char *   The string to write (of at least 'n' characters).
 *  n              int     The number of characters to write.
 * Output:
 *  return         int     0 - OK.
 *                         1 - Error.
 */
static int pty_write_to_fd(int fd, const char *string, int n)
{
  int ndone = 0;  /* The number of characters written so far */
/*
 * Do as many writes as are needed to write the whole string.
 */
  while(ndone < n) {
    int nnew = write(fd, string + ndone, n - ndone);
    if(nnew > 0)
      ndone += nnew;
    else if(errno != EINTR)
      return 1;
  };
  return 0;
}

/*.......................................................................
 * This is the signal handler that is called when the child process
 * that is running the user's program exits for any reason. It closes
 * the slave end of the terminal, so that gl_get_line() in the parent
 * process sees an end of file.
 */
static void pty_child_exited(int sig)
{
  raise(SIGINT);
}

/*.......................................................................
 * Return non-zero after a given amount of time if there is data waiting
 * to be read from a given file descriptor.
 *
 * Input:
 *  fd        int  The descriptor to watch.
 *  usec     long  The number of micro-seconds to wait for input to
 *                 arrive before giving up.
 * Output:
 *  return    int  0 - No data is waiting to be read (or select isn't
 *                     available).
 *                 1 - Data is waiting to be read.
 */
static int pty_master_readable(int fd, long usec)
{
#if HAVE_SELECT
  fd_set rfds;             /* The set of file descriptors to check */
  struct timeval timeout;  /* The timeout */
  FD_ZERO(&rfds);
  FD_SET(fd, &rfds);
  timeout.tv_sec = 0;
  timeout.tv_usec = usec;
  return select(fd+1, &rfds, NULL, NULL, &timeout) == 1;
#else
  return 0;
#endif
}
