/* ttio.c

   Terminal I/O routines... */

/*
 * Copyright (c) 1995 RadioMail Corporation.  All rights reserved.
 * Copyright (c) 1996 Vixie Enterprises.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of RadioMail Corporation nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY RADIOMAIL CORPORATION AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * RADIOMAIL CORPORATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software was written for RadioMail Corporation by Ted Lemon
 * under a contract with Vixie Enterprises, and is based on an earlier
 * design by Paul Vixie.
 */

#ifndef lint
static char copyright[] =
"@(#) Copyright (c) 1996 Vixie Enterprises.  All rights reserved.\n";
#endif /* not lint */

#include "osdep.h"
#include "cdefs.h"
#include "global.h"
#include <stdio.h>
#include <fcntl.h>
#include <syslog.h>
#include "mcap.h"
#include "ttio.h"
#include <pwd.h>

static void dectl PROTO ((char *, int, char *, int, char *));

TTY_STATE ts;

int ttsetup (name)
     char *name;
{
  int fd;
  char nbuf [1024];
  struct passwd *pw;

  /* Convert terminal name to device name. */
  CVT_TTYNAME (nbuf, name);

  /* Chown the tty so that uucp/tip can open it... */
  if (!(pw = getpwnam ("uucp")))
    warn ("Can't get group id of uucp!");
  else
    {
      if (chown (nbuf, pw -> pw_uid, pw -> pw_gid) < 0)
	warn ("Can't set owner on %s: %m", nbuf);
    }

  /* Open the terminal device. */
  fd = open (nbuf, TTY_OPEN_FLAGS, TTY_OPEN_MODE);
  if (fd < 0)
    error ("Can't open %s: %m", nbuf);

  if (fcntl (fd, F_SETFL, FNDELAY) < 0)
    error ("Can't set FNDELAY on %s: %m");

  /* Initialize terminal state... */
  INIT_TTY_STATE (ts);

  /* Parity and bits per word are often intertwined in Unix terminal
     configuration structures.   The target macros should assume that
     bits per word will be set before parity, and check for correctness
     when parity is set... */

  /* Set bits per word... */
  SET_BITS (ts, modemcap.bits);

  /* Set parity... */
  if (!strcmp (modemcap.parity, "none"))
    SET_PARITY (ts, PARITY_NONE);
  else if (!strcmp (modemcap.parity, "even"))
    SET_PARITY (ts, PARITY_EVEN);
  else if (!strcmp (modemcap.parity, "odd"))
    SET_PARITY (ts, PARITY_ODD);
  else if (!strcmp (modemcap.parity, "mark"))
    SET_PARITY (ts, PARITY_MARK);
  else if (!strcmp (modemcap.parity, "space"))
    SET_PARITY (ts, PARITY_SPACE);
  else
    {
      warn ("Unknown parity: %s; assuming none.", modemcap.parity);
      SET_PARITY (ts, PARITY_NONE);
    }

  /* Set flow control... */
  SET_NO_FLOW_CTL (ts);
  if (modemcap.fc_rts_cts)
    {
      SET_HW_FLOW_CTL (ts);
    }
  if (modemcap.fc_xon_xoff)
    {
      SET_XON_XOFF_FLOW_CTL (ts);
    }

  /* Set speed... */
  SET_TX_SPEED (ts, modemcap.tx_speed);
  SET_RX_SPEED (ts, modemcap.rx_speed);

  if (!PUT_TTY_STATE (fd, ts))
    error ("Can't set tty state: %m");
  return fd;
}

void ttnormal (fd)
{
  NORMAL_TTY (ts);
  PUT_TTY_STATE (fd, ts);
}

void tthupcl (fd)
{
  HUPCL_TTY (ts);
  PUT_TTY_STATE (fd, ts);
}

void redirect_std (fd)
     int fd;
{
  int i;

  /* Make sure FNDELAY is clear... */
  if (fcntl (fd, F_SETFL, 0) < 0)
    error ("Unable to clear FNDELAY: %m");

  /* Close STDIN, STDOUT and STDERR (unless fd is one of them) */
  for (i = 0; i < 2; i++)
    if (fd != i)
      close (i);

  /* Make fd STDIN, STDOUT and STDERR... */
  if ((fd != 0 && dup2 (fd, 0) < 0)
      || (fd != 1 && dup2 (fd, 1) < 0)
      || (fd != 2 && dup2 (fd, 2) < 0))
    error ("Unable to redirect stdio to tty: %m");

  /* Close extra descriptor except as above... */
  if (fd > 2)
    close (fd);

  /* Make this process a group leader and set the controlling terminal.
     setsid() is only likely to fail if we're already a process group
     leader, which would mean that we're running (being tested) under
     the shell.  In that case, fork, exit from the parent and run
     setsid() again in the child. */
  if (setsid () < 0)
    {
      int pid = fork ();
      if (pid > 0)
	exit (0);
      if (pid < 0)
	error ("fork failed: %m");
      if (setsid () < 0)
	error ("setsid failed with pid = %d, pgrp = %d: %m",
	       getpid (), getpgrp ());
    }
  /* Now make the tty the controlling terminal... */
  SET_CTTY (0);
}

void ttset_ctty (fd)
     int fd;
{
  setsid ();
  SET_CTTY (fd);
}

/* Circular input buffer for ttmatch/ttgetc... */
static unsigned char ttbuf [256];
static int ttend = 0;
static int ttstart = 0;
static int tteof = 0;
#define NEXT(x) (((x) + 1) >= sizeof ttbuf ? 0 : (x) + 1)

/* Match the specified string with input from the specified tty.
   Return nonzero if input matches, zero if not. */
ttmatch (ANSI_DECL (int) tty, ANSI_DECL (int) timeout, VA_DOTDOTDOT)
     KandR (int tty;)
     KandR (int timeout;)
     va_dcl
{
  TIME entry, now, to;
  int cur, curmatch, next;
  int len, status;
  int count = 1;
  char ttcbuf [sizeof ttbuf + 1];
  char *string;
  int matchID;
  va_list list;

  /* Remember when we started... */
  GET_TIME (&entry);
  now = entry;
  while (!timeout || TIME_DIFF_US (&entry, &now) < timeout)
    {
      if (ttstart != ttend)
	debug ("Match buffer [%d:%d] contains ``%s''",
	       ttstart, ttend, ttstring (tty));

      /* Match each string given against the internal buffer.   If none
	 match, try reading some more data from the tty... */
      matchID = 1;
      VA_start (list, timeout);
      while (string = va_arg (list, char *))
	{
	  syslog (LOG_DEBUG, "Matching against %s", string);
	  for (cur = ttstart; cur != ttend; cur = NEXT (cur))
	    {
	      /* Compare string to the input buffer... */
	      for ((curmatch = 0), (next = cur);
		   next != ttend
		   && string [curmatch] && string [curmatch] == ttbuf [next];
		   curmatch++, next = NEXT (next))
		;
	      
	      /* If we match everything in string, slurp up that much of
		 the input buffer and return nonzero. */
	      
	      if (!string [curmatch])
		{
		  ttstart = next;
		  return matchID;
		}
	    }
	  ++matchID;
	}
      va_end (list);

      /* We didn't get a match with what's buffered, so try reading
	 something more.   If a nonzero timeout was specified, only
	 wait that number of microseconds. */
      if (timeout)
	{
	  fd_set r, w, x;

	  /* Select on read from tty only... */
	  FD_ZERO (&r);
	  FD_ZERO (&w);
	  FD_ZERO (&x);
	  FD_SET (tty, &r);

	  /* Timeout at selected interval. */
	  SET_TIME (&to, timeout);

	  /* Wait for input... */
	  if ((count = select (tty + 1, &r, &w, &x, &to)) < 0)
	    warn ("Select failed: %m");
	  if (count == 0)
	    break;
	}
      else
	{
	  if (fcntl (tty, F_SETFL, 0) < 0)
	    error ("Unable to clear FNDELAY: %m");
	}

      if (ttend < ttstart)
	len = ttstart - ttend - 1;
      else
	len = (sizeof ttbuf) - ttend - 1;
      status = read (tty, &ttbuf [ttend], (sizeof ttbuf) - ttend - 1);
      if (status < 0)
	error ("Unable to read from tty: %m");
      if (status != 0)
	{
	  dectl (ttcbuf, status, (char *)&ttbuf [ttend], 0, (char *)0);
	  syslog (LOG_DEBUG, "ttmatch read %d bytes: ``%s''", status, ttcbuf);
	}
      if (ttstart > ttend && ttend + status > ttstart)
	{
	  ttstart = ttend + status + 1;
	  if (ttstart > sizeof ttbuf)
	    ttstart = 0;
	}
      ttend += status;
      if (ttend == sizeof ttbuf)
	ttend = 0;
      if (ttend == ttstart)
	++ttstart;
      GET_TIME (&now);
    }
  syslog (LOG_DEBUG, "ttmatch timed out.");
  if (ttend != ttstart)
    return 0;
  else
    return -1;
}

/* Read a connect speed from standard in... */
ttread_connect_speed (tty, timeout)
     int tty;
     int timeout;
{
  TIME entry, now, to;
  int status;
  int count = 1;
  int i;
  int value;
  int innum = 0;
  int done = 0;
  char buf [10];
  char ttcbuf [11];

#define DO_CHARS(s, start, end)					\
  for (i = start; i < end; i++)					\
    {								\
      if (innum)						\
	{							\
	  if (!isascii (s [i]) || !isdigit (s [i]))		\
	    {							\
	      done = 1;						\
	      break;						\
	    }							\
	  value = value * 10 + s [i] - '0';			\
	}							\
      else							\
	{							\
	  if (isascii (s [i]) && isdigit (s [i]))		\
	    {							\
	      innum = 1;					\
	      value = s [i] - '0';				\
	    }							\
	  else if (s [i] == '\r' || s [i] == '\n')		\
	    {							\
	      syslog (LOG_ERR, "ttyread_connect_speed: no speed found."); \
	      return 0;						\
	    }							\
	}							\
    }

  /* First look for data in the match buffer... */
  if (ttstart != ttend)
    debug ("Match buffer [%d:%d] contains ``%s''",
	   ttstart, ttend, ttstring (tty));

  if (ttstart < ttend)
    {
      DO_CHARS (ttbuf, ttstart, ttend);
    }
  else if (ttstart > ttend)
    {
      DO_CHARS (ttbuf, ttstart, sizeof ttbuf);
      DO_CHARS (ttbuf, 0, ttend);
    }
  ttstart = ttend = 0;

  /* Remember when we started... */
  GET_TIME (&entry);
  now = entry;
  while (!done && (!timeout || TIME_DIFF_US (&entry, &now) < timeout))
    {
      if (ttstart != ttend)
	debug ("Match buffer [%d:%d] contains ``%s''",
	       ttstart, ttend, ttstring (tty));

      /* We didn't get a match with what's buffered, so try reading
	 something more.   If a nonzero timeout was specified, only
	 wait that number of microseconds. */
      if (timeout)
	{
	  fd_set r, w, x;

	  /* Select on read from tty only... */
	  FD_ZERO (&r);
	  FD_ZERO (&w);
	  FD_ZERO (&x);
	  FD_SET (tty, &r);

	  /* Timeout at selected interval. */
	  SET_TIME (&to, timeout);

	  /* Wait for input... */
	  if ((count = select (tty + 1, &r, &w, &x, &to)) < 0)
	    warn ("Select failed: %m");
	  if (count == 0)
	    break;
	}
      else
	{
	  if (fcntl (tty, F_SETFL, 0) < 0)
	    error ("Unable to clear FNDELAY: %m");
	}

      status = read (tty, buf, sizeof buf);
      if (status < 0)
	error ("Unable to read from tty: %m");
      if (status != 0)
	{
	  dectl (ttcbuf, status, buf, 0, (char *)0);
	  syslog (LOG_DEBUG, "ttread_connect_speed read %d bytes: ``%s''",
		  status, ttcbuf);
	  DO_CHARS (buf, 0, status);
	}
      GET_TIME (&now);
    }
  if (!done)
    {
      syslog (LOG_ERR, "ttyread_connect_speed timed out.");
      return 0;
    }
  syslog (LOG_DEBUG, "ttyread_connect_speed: connected at %d", value);

  /* Set speed... */
  SET_TX_SPEED (ts, value);
  SET_RX_SPEED (ts, value);
  PUT_TTY_STATE (tty, ts);

  return 1;
}

/* Wait forever for the descriptor to be ready for input. */
void ttwait (tty)
     int tty;
{
  int status;
  fd_set r, w, x;

  FD_ZERO (&r);
  FD_ZERO (&w);
  FD_ZERO (&x);
  FD_SET (tty, &r);
  status = select (tty + 1, &r, &w, &x, (TIME *)0);
  if (status < 0)
    error ("ttwait: select failed: %m");
}

/* Drain the match buffer and the unix input buffer... */
void ttdrain (tty)
     int tty;
{
  char ibuf [256];
  int status;
  fd_set r, w, x;
  TIME tv;

  ttstart = ttend = 0;

  /* Make sure FNDELAY is set... */
  if (fcntl (tty, F_SETFL, FNDELAY) < 0)
    error ("Unable to set FNDELAY: %m");

  /* Don't wait more than 2ms for some output from the modem... */
  FD_ZERO (&r);
  FD_ZERO (&w);
  FD_ZERO (&x);
  FD_SET (tty, &r);
  SET_TIME (&tv, 100000);
  status = select (tty + 1, &r, &w, &x, &tv);
  if (status != 1)
    return;

  do {
    status = read (tty, ibuf, sizeof ibuf);
    if (status < 0) {
      if (errno != EWOULDBLOCK)
	error ("Can't drain tty: %m");
    } else {
      dectl (ibuf, status, ibuf, 0, (char *)0);
      if (status == sizeof ibuf)
	--status;
      ibuf [status] = 0;
      syslog (LOG_DEBUG, "ttdrain read ``%s''", ibuf);
    }
  } while (status > 0);
}

/* Read and return one character from the tty. */
int ttgetc (tty)
     int tty;
{
  int ibix;
  int status;
  fd_set r, w, x;
  int len;
  char ttcbuf [sizeof ttbuf + 1];

  /* If we have something buffered, return it. */
  if (ttstart != ttend)
    {
      ibix = ttstart;
      ttstart = NEXT (ttstart);
      syslog (LOG_DEBUG, "ttgetc: read %d", ttbuf [ibix]);
      return ttbuf [ibix];
    }
  if (tteof)
    {
      syslog (LOG_DEBUG, "ttgetc: EOF\n");
      return EOF;
    }

  /* Flush the output buffer... */
  ttoflush (tty);

  /* Wait for input... */
  FD_ZERO (&r);
  FD_ZERO (&w);
  FD_ZERO (&x);
  FD_SET (tty, &r);
  status = select (tty + 1, &r, &w, &x, (struct timeval *)0);
  if (status != 1)
    return EOF;

  /* Fill the buffer... */
  len = sizeof ttbuf;
  status = read (tty, ttbuf, sizeof ttbuf - 1);
  if (status < 0)
    error ("Unable to read from tty: %m");
  if (status == 0)
    {
      syslog (LOG_DEBUG, "ttgetc: read EOF\n");
      tteof = 1;
      return EOF;
    }
  dectl (ttcbuf, status, (char *)ttbuf, 0, (char *)0);
  syslog (LOG_DEBUG, "ttgetc read %d bytes: ``%s''", status, ttcbuf);
  syslog (LOG_DEBUG, "ttgetc returns %d\n", ttbuf [0]);
  ttend = status;
  ttstart = 1;
  return ttbuf [0];
}

/* Buffered output to tty... */
static char ttobuf [256];
static int ttobix;

void ttputc (tty, ch)
     int tty;
     int ch;
{
  ttobuf [ttobix++] = ch;
  if (ttobix == sizeof ttobuf)
    ttoflush(tty);
}

void ttputs (tty, s)
     int tty;
     char *s;
{
  char *t = s;
  while (*t)
    ttputc (tty, *t++);
}

void ttoflush (tty)
     int tty;
{
  char *s = ttobuf;
  int len = ttobix;
  int result;

  /* Don't flush if there's nothing there... */
  if (!ttobix)
    return;

  debug ("ttoflush: %d bytes ``%*.*s''", ttobix, ttobix, ttobix, ttobuf);

  /* Write the data... */
  while (len)
    {
      result = write (tty, s, len);
      if (result < 0)
	error ("Can't write to tty: %m");
      len -= result;
      s += result;
    }

  ttobix = 0;
}  

/* Write NUL-terminated string to terminal. */
void ttwrite (tty, string)
     int tty;
     char *string;
{
  int len = strlen (string);
  int result;
  char *s = string;

  if (modemcap.slow_modem)
    {
      ttwriteslow (tty, string);
      return;
    }

  syslog (LOG_DEBUG, "ttwrite: %d bytes ``%s''", len, s);

  /* Write the data... */
  while (len)
    {
      result = write (tty, s, len);
      if (result < 0)
	error ("Can't write to tty: %m");
      len -= result;
      s += result;
    }
  /* Wait until it's made it to the modem... */
  TTY_DRAIN (tty);
}

/* Write NUL-terminated string to terminal. */
void ttwriteslow (tty, string)
     int tty;
     char *string;
{
  int len = strlen (string);
  int result;
  char *s = string;

  syslog (LOG_DEBUG, "ttwriteslow: %d bytes ``%s''", len, s);

  /* Write the data... */
  while (len)
    {
      result = write (tty, s, 1);
      if (result < 0)
	error ("Can't write to tty: %m");
      len -= result;
      s += result;
      /* Wait until it's made it to the modem... */
      TTY_DRAIN (tty);
      MSDELAY (modemcap.msdelay);
    }
}

char *ttstring (tty)
     int tty;
{
  static char ttcbuf [256];
  if (ttstart < ttend)
    dectl (ttcbuf, ttend - ttstart, (char *)&ttbuf [ttstart], 0, (char *)0);
  else if (ttstart > ttend)
    dectl (ttcbuf, sizeof ttbuf - ttstart, (char *)&ttbuf [ttstart],
	   ttend, (char *)ttbuf);
  else
    ttcbuf [0] = 0;
  return ttcbuf;
}

static void dectl (dest, len1, buf1, len2, buf2)
     char *dest;
     int len1, len2;
     char *buf1, *buf2;
{
  char *p;
  int i;

  p = dest;

  for (i = 0; i < len1; i++)
    {
      if (buf1 [i] < 32 || buf1 [i] > 126)
	*p++ = ' ';
      else
	*p++ = buf1 [i];
    }

  for (i = 0; i < len2; i++)
    {
      if (buf2 [i] < 32 || buf2 [i] > 126)
	*p++ = ' ';
      else
	*p++ = buf2 [i];
    }
  *p = 0;
}
