/* **********************************************************************
 * *                                                                    *
 * *                    TTY I/O Support Routines                        *
 * *                                                                    *
 * **********************************************************************
 * File: libs/user/ttyio.c
 *
 *                        COPYRIGHT (c) 2002  BY
 *          SIGMET INCORPORATED, WESTFORD MASSACHUSETTS, U.S.A.
 * 
 * THIS SOFTWARE IS FURNISHED UNDER A LICENSE AND MAY BE USED AND  COPIED
 * ONLY  IN  ACCORDANCE WITH  THE  TERMS  OF  SUCH  LICENSE  AND WITH THE
 * INCLUSION OF THE ABOVE COPYRIGHT NOTICE.  THIS SOFTWARE  OR  ANY OTHER
 * COPIES  THEREOF MAY NOT BE PROVIDED OR OTHERWISE MADE AVAILABLE TO ANY
 * OTHER PERSON.  NO TITLE TO AND OWNERSHIP OF  THE  SOFTWARE  IS  HEREBY
 * TRANSFERED.
 */
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <ctype.h>
#include <termios.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <sys/poll.h>

#define INCLUDE_TERMIOS_PROTOTYPES 1
#include "sigtypes.h"
#include "user_lib.h"

static MESSAGE openRealTTY( struct ttydev *tty_a ) ;
static MESSAGE openFIFOPair( struct ttydev *tty_a ) ;

static MESSAGE openFIFODevice
( FILE **stream_a, const char *sName_a, const char *sMode_a ) ;

static UINT1 lPollTestFD( int iFD_a, SINT4 iTimeoutMS_a, SINT4 iPollEvent_a ) ;

/* ------------------------------
 * Open a TTY device or FIFO pair for read/write.
 */
MESSAGE openTTYDev
( struct ttydev *tty_a, const struct ttyspec *spec_a )
{
  MESSAGE istatus ;

  /* Initialize the TTY result structure.  Filter out some operations
   * that do not make very much sense.
   */
  memset( tty_a, 0, sizeof(struct ttydev) ) ;
  tty_a->spec = *spec_a ;

  if( spec_a->lLockDevice &&
      (0 == strcmp(spec_a->sDevice, "/dev/tty")) ) {
    tty_a->spec.lLockDevice = FALSE ;
  }
  /* A blank device name results in a non-error return of a non-open
   * TTY device.  If the device pathname ends in "-x" or "-y", then
   * assume that we are opening a FIFO pair that will be used in
   * loopback mode.
   */
  if( ! isgraph( spec_a->sDevice[0] ) ) istatus = SS_NORMAL ;
  else {
    SINT4 iNameLen = strlen( spec_a->sDevice ) ;
    if( (iNameLen > 2) &&
	( ( 0 == strcmp( &spec_a->sDevice[iNameLen-2], "-x" ) ) ||
	  ( 0 == strcmp( &spec_a->sDevice[iNameLen-2], "-y" ) ) ) ) {

      tty_a->lRealTTY = FALSE ; tty_a->spec.lLockDevice = FALSE ;
      istatus = openFIFOPair( tty_a ) ;

    } else {
      tty_a->lRealTTY = TRUE  ;
      istatus = openRealTTY( tty_a ) ;
    }
  }
  return( istatus ) ;
}

/* ------------------------------
 * Open a pair of FIFO devices to serve as a full duplex loopback
 * connection.  The named device is the one that will be used for
 * writing, and has already been verified as ending in "-x" or "-y".
 */
static MESSAGE openFIFOPair( struct ttydev *tty_a )
{
  char sAltDev[PATHNAME_SIZE] ; SINT4 iNameLen ; MESSAGE istatus ;

  /* Make up the name of the alternate device
   */
  strcpy( sAltDev, tty_a->spec.sDevice ) ; iNameLen = strlen( sAltDev ) ;
  if( sAltDev[iNameLen-1] == 'x' ) sAltDev[iNameLen-1] = 'y' ;
  else                             sAltDev[iNameLen-1] = 'x' ;

  /* Handle read/write cases.  Open only one of the devices if we are
   * readonly or writeonly, otherwise open both of them.
   */
  if( tty_a->spec.lWriteOnly ) {
    istatus = openFIFODevice( &tty_a->Tx, tty_a->spec.sDevice, "w" ) ;

  } else if( tty_a->spec.lReadOnly ) {
    istatus = openFIFODevice( &tty_a->Rx, sAltDev, "r" ) ;

  } else {
    istatus = openFIFODevice( &tty_a->Tx, tty_a->spec.sDevice, "w" ) ;
    if( istatus == SS_NORMAL ) {
      istatus = openFIFODevice( &tty_a->Rx, sAltDev, "r" ) ;
      if( istatus != SS_NORMAL ) fclose( tty_a->Tx ) ;
    }
  }
  tty_a->lOpenOkay = ( istatus == SS_NORMAL ) ;
  return( istatus ) ;
}

/* ------------------------------
 * Open a single FIFO device
 */
static MESSAGE openFIFODevice
( FILE **stream_a, const char *sName_a, const char *sMode_a )
{
  int fd ; struct stat statbuf ; FILE *stream ;

  /* Try to open an individual FIFO device.  We open for both reading
   * and writing, which prevents the open from blocking.
   */
  if( (fd = open( sName_a, O_RDWR )) < 0 ) {
    return( errno_to_message_va( errno, 1, sName_a ) ) ;
  }
  /* Verify that this is a FIFO special file
   */
  if( fstat( fd, &statbuf ) < 0 ) {
    close( fd ) ; return( errno_to_message_va( errno, 1, sName_a ) ) ;
  } else {
    if( ! S_ISFIFO(statbuf.st_mode) ) {
      close( fd ) ; return( iMessageMod_va( USER_NOTAFIFO, 1, sName_a ) ) ;
    }
  }
  /* Create the associated streams for these file descriptors
   */
  if( ( stream = fdopen( fd, sMode_a ) ) == NULL ) {
    close( fd ) ; return( errno_to_message_va( errno, 1, sName_a ) ) ;
  }

  *stream_a = stream ; return( SS_NORMAL ) ;
}

/* ------------------------------
 * Open a real TTY device for read/write, setting up all of the
 * parameters for raw mode operation at a specified baud rate.  The
 * device can be locked against further opens if we successfully get
 * it.
 */
static MESSAGE openRealTTY( struct ttydev *tty_a )
{
  int fd, fdDup ;

  /* Try to open the serial port.  Do not allow it to become the
   * controlling terminal for this process at any time in the future.
   */
  if( (fd = open( tty_a->spec.sDevice, O_RDWR | O_NOCTTY )) < 0 ) {
    return( errno_to_message_va( errno, 1, tty_a->spec.sDevice ) ) ;
  }
  /* Verify that the device seems to be a real TTY.
   */
  if( ! isatty(fd) ) {
    close( fd ) ;
    return( iMessageMod_va( USER_NOTATTY , 1, tty_a->spec.sDevice ) ) ;
  }
  /* Device was opened successfully.  Optionally try to grab it
   * exclusively for our own use.
   */
  if( tty_a->spec.lLockDevice ) {
    if( lockf( fd, F_TLOCK, 0 ) ) {
      close( fd ) ;
      return( iMessageMod_va( USER_DEVALLOC , 1, tty_a->spec.sDevice ) ) ;
    }
  }
  /* Setup the new TTY parameters, unless we were told to preserve
   * everything the way we found it.
   */
  if( ! tty_a->spec.lPreserveAll ) {
    struct termios tio ; memset( &tio, 0, sizeof( tio ) ) ;

    /* Save the original TTY parameters
     */
    if( tcgetattr( fd, &tty_a->tioOrig ) < 0 ) {
      close( fd ) ; goto errno_return ;
    }

    /* Setup all the new stuff...
     */
    if( tty_a->spec.iBaud ) {
      int iBCode ;
      if     ( tty_a->spec.iBaud <    600 ) iBCode =    B300 ;
      else if( tty_a->spec.iBaud <   1200 ) iBCode =    B600 ;
      else if( tty_a->spec.iBaud <   2400 ) iBCode =   B1200 ;
      else if( tty_a->spec.iBaud <   4800 ) iBCode =   B2400 ;
      else if( tty_a->spec.iBaud <   9600 ) iBCode =   B4800 ;
      else if( tty_a->spec.iBaud <  19200 ) iBCode =   B9600 ;
      else if( tty_a->spec.iBaud <  38400 ) iBCode =  B19200 ;
      else if( tty_a->spec.iBaud <  57600 ) iBCode =  B38400 ;
      else if( tty_a->spec.iBaud < 115200 ) iBCode =  B57600 ;
      else                                  iBCode = B115200 ;

      cfsetospeed( &tio, iBCode ) ; cfsetispeed( &tio, iBCode ) ;
    }
    tio.c_cflag |= (CS8 | CLOCAL | CREAD) ;
    switch( tty_a->spec.iParity ) {
    case TTYPARITY_ODD : tio.c_cflag |= (PARENB | PARODD) ; break ;
    case TTYPARITY_EVEN: tio.c_cflag |=  PARENB           ; break ;
    }
    tio.c_iflag |= IGNPAR ;	/* No parity error detection */

    tio.c_oflag |= 0 ;		/* Raw output and input */
    tio.c_lflag |= 0 ;

    /* Initialize control characters (all others are zero)
     */
    tio.c_cc[ VTIME ] = 0 ;	/* Don't use the timer */
    tio.c_cc[ VMIN  ] = 1 ;	/* Wait for one character. */

    if( tty_a->spec.iIntrChar ) {
      tio.c_cc[ VINTR ] = tty_a->spec.iIntrChar ; tio.c_lflag |= ISIG ;
    }

    /* Flush any prior data, and set the new port params.
     */
    if( (   tcflush( fd, TCIOFLUSH     ) < 0 ) ||
	( tcsetattr( fd, TCSANOW, &tio ) < 0 ) ) {
      close( fd ) ; goto errno_return ;
    }
  }

  /* Create the associated read and write streams, duplicating the TTY
   * file descriptor if necessary.  Entry flags may direct us to only
   * create one stream or the other.
   */
  if( tty_a->spec.lWriteOnly ) {
    if( (tty_a->Tx = fdopen( fd, "w" )) == NULL ) {
      close( fd ) ; goto errno_return ;
    }
  } else if( tty_a->spec.lReadOnly ) {
    if( (tty_a->Rx = fdopen( fd, "r" )) == NULL ) {
      close( fd ) ; goto errno_return ;
    }
  } else {
    if( (fdDup = dup( fd ) ) < 0 ) {
      close( fd ) ; goto errno_return ;
    } else if( (tty_a->Tx = fdopen( fd, "w" )) == NULL ) {
      close( fd ) ; close( fdDup ) ; goto errno_return ;
    } else if( (tty_a->Rx = fdopen( fdDup, "r" )) == NULL ) {
      fclose( tty_a->Tx ) ; close( fdDup ) ; goto errno_return ;
    }
  }
  tty_a->lOpenOkay = TRUE ; tty_a->fdOrig = fd ;
  return( SS_NORMAL ) ;

 errno_return:
  if( tty_a->spec.lLockDevice ) lockf( fd, F_ULOCK, 0 ) ;
  return( errno_to_message_va( errno, 1, tty_a->spec.sDevice ) ) ;
}

/* ------------------------------
 * Close a TTY device that was opened with openTTYDev().  Restore the
 * original TTY settings (if any), remove the lock (if any), and close
 * the stream(s).
 */
MESSAGE closeTTYDev( struct ttydev *tty_a )
{
  MESSAGE istatus = SS_NORMAL ;
  if( tty_a->lOpenOkay ) {

    if( tty_a->lRealTTY && (! tty_a->spec.lPreserveAll) ) {
      if( tcsetattr( tty_a->fdOrig, TCSANOW, &tty_a->tioOrig ) < 0 ) {
	istatus = errno_to_message( errno ) ;
      }
    }
    if( tty_a->spec.lLockDevice ) {
      if( lockf( tty_a->fdOrig, F_ULOCK, 0 ) )
	istatus = errno_to_message( errno ) ;
    }
    if( tty_a->Rx && fclose( tty_a->Rx ) ) istatus = errno_to_message( errno ) ;
    if( tty_a->Tx && fclose( tty_a->Tx ) ) istatus = errno_to_message( errno ) ;

    memset( tty_a, 0, sizeof(struct ttydev) ) ;
  }
  return( istatus ) ;
}

/* ------------------------------
 * Wait for buffered TTY output to drain, but only if the device is a
 * real TTY.
 */
void drainTxTTYDev( const struct ttydev *tty_a )
{
  if( tty_a->lOpenOkay && tty_a->Tx ) {
    if( tty_a->lRealTTY ) tcdrain( fileno(tty_a->Tx) ) ;
  }
}

/* ------------------------------
 * Flush any buffered TTY input that may have accumulated so far.
 */
void flushRxTTYDev( const struct ttydev *tty_a )
{
  if( tty_a->lOpenOkay && tty_a->Rx ) {
    UINT1 iFlushBuf[500] ; SINT4 iFD = fileno( tty_a->Rx ) ;
    while( lReadAvailFD( iFD, 0 ) ) read( iFD, iFlushBuf, sizeof(iFlushBuf) ) ;
  }
}

/* ------------------------------
 * Pair of routines to check whether read data are available from a
 * file descriptor, and whether free space is available for writing to
 * a file descriptor.  An optional timeout delay in milliseconds can
 * be supplied in both cases.  These routines work on any type of file
 * descriptor, not just those associated with a TTY device.
 */
UINT1 lReadAvailFD( int iFD_a, SINT4 iTimeoutMS_a )
{
  return( lPollTestFD( iFD_a, iTimeoutMS_a, POLLIN ) ) ;
}
UINT1 lWriteFreeFD( int iFD_a, SINT4 iTimeoutMS_a )
{
  return( lPollTestFD( iFD_a, iTimeoutMS_a, POLLOUT ) ) ;
}
static UINT1 lPollTestFD( int iFD_a, SINT4 iTimeoutMS_a, SINT4 iPollEvent_a )
{
  struct pollfd pollList[1] ; int iRetVal ; UINT1 lOkay = FALSE ;

  pollList[0].fd      = iFD_a ;
  pollList[0].events  = iPollEvent_a ;
  pollList[0].revents = 0 ;
  
  iRetVal = poll( pollList, 1, iTimeoutMS_a ) ;
  if( iRetVal < 0 ) {
    if( errno != EINTR ) {
      sig_signal( errno_to_message_va( errno, 1, "lReadAvailFD()" ) ) ;
    }
  } else if( iRetVal == 1 ) {
    lOkay = ( ( pollList[0].revents & iPollEvent_a ) != 0 ) ;
  }
  return( lOkay ) ;
}
