/*
    KMLOCfg

    A utility to configure the ELSA MicroLink(tm) Office modem.

    Copyright (C) 2000 Oliver Gantz <Oliver.Gantz@epost.de>

    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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

    ------
    ELSA and MicroLink are trademarks of ELSA AG, Aachen.
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/file.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <pwd.h>
#include <errno.h>

#include <tqglobal.h>

#include <tdelocale.h>
#include <kdebug.h>

#include "modem.h"


#ifndef CSOH
#define CSOH  01
#endif

#ifndef CSTX
#define CSTX  02
#endif

#ifndef CEOT
#define CEOT  04
#endif

#ifndef CACK
#define CACK  06
#endif

#ifndef CNAK
#define CNAK 025
#endif

#ifndef CCAN
#define CCAN 030
#endif



Modem::Modem( KandyPrefs *kprefs, TQObject *parent, const char *name ) :
  TQObject(parent, name), fd(-1)
{
  mOpen = false;

  prefs = kprefs;

  timer = new TQTimer( this, "modemtimer" );
  TQ_CHECK_PTR( timer );
  connect( timer, TQ_SIGNAL( timeout() ), TQ_SLOT( timerDone() ) );

  init();
  xreset();
}


Modem::~Modem()
{
  close();
}


void Modem::setSpeed( int speed )
{
  switch ( speed ) {
    case 300:
      cspeed = B300;
      break;
    case 600:
      cspeed = B600;
      break;
    case 1200:
      cspeed = B1200;
      break;
    case 2400:
      cspeed = B2400;
      break;
    case 4800:
      cspeed = B4800;
      break;
    case 9600:
      cspeed = B9600;
      break;
    case 19200:
      cspeed = B19200;
      break;
    case 38400:
      cspeed = B38400;
      break;
    case 57600:
      cspeed = B57600;
      break;
    case 115200:
      cspeed = B115200;
      break;
    case 230400:
      cspeed = B230400;
      break;
    default:
#ifdef MODEM_DEBUG
      fprintf(stderr, "Modem: setSpeed(): fallback to default speed.\n");
#endif
      cspeed = B38400;
  }
}


void Modem::setData( int data )
{
  cflag &= ~CSIZE;

  switch ( data ) {
    case 5:
      cflag |= CS5;
      break;
    case 6:
      cflag |= CS6;
      break;
    case 7:
      cflag |= CS7;
      break;
    default:
      cflag |= CS8;
  }
}


void Modem::setParity( char parity )
{
  cflag &= ~( PARENB | PARODD );

  if ( parity == 'E' )
    cflag |= PARENB;
  else if ( parity == 'O' )
    cflag |= PARENB | PARODD;
}


void Modem::setStop( int stop )
{
  if (stop == 2)
    cflag |= CSTOPB;
  else
    cflag &= ~CSTOPB;
}


bool Modem::open()
{
  struct termios tty;

  close();

	if (fd == -1)
	{
		TQCString dev = TQFile::encodeName( (*prefs).serialDevice() );
		const char *fdev = dev.data();
		if ( ( fd = ::open( fdev, O_RDWR | O_NOCTTY | O_NONBLOCK ) ) == -1 ) {
			emit errorMessage( i18n( "Unable to open device '%1'. "
															"Please check that you have sufficient permissions." )
															.arg( fdev ) );
			return false;
		}
	}

  if ( !lockDevice() )
    return false;

  tcflush( fd, TCIOFLUSH );
  if ( tcgetattr( fd, &init_tty ) == -1 ) {
    int errnumber = errno;
    emit errorMessage( i18n( "Communication setup failed (tcgetattr code: %1)" )
        .arg(strerror(errnumber)) );
		unlockDevice();
    ::close( fd );
    fd = -1;
    return false;
  }

  memset( &tty, 0, sizeof( tty ) );
  tty.c_iflag = IGNBRK | IGNPAR;
  tty.c_oflag = 0;
  tty.c_cflag = cflag;
  tty.c_lflag = 0;
  cfsetospeed( &tty, cspeed );
  cfsetispeed( &tty, cspeed );
  tcdrain( fd );

  if ( tcsetattr( fd, TCSANOW, &tty ) == -1 ) {
    emit errorMessage( i18n( "tcsetattr() failed." ) );
		unlockDevice();
    ::close( fd );
    fd = -1;
    return false;
  }

  sn = new TQSocketNotifier( fd, TQSocketNotifier::Read, this,
                            "modemsocketnotifier" );
  TQ_CHECK_PTR( sn );
  connect( sn, TQ_SIGNAL( activated( int ) ), TQ_SLOT( readChar( int ) ) );

  mOpen = true;

  return true;
}


void Modem::close()
{
  timer->stop();

  delete sn;
  sn = 0;

  unlockDevice();

  if ( fd ) {
    tcflush( fd, TCIOFLUSH );
    tcsetattr( fd, TCSANOW, &init_tty );
    ::close( fd );
    fd = -1;
  }

  xreset();

  mOpen = false;
}


void Modem::flush()
{
  if ( fd != -1) {
    tcflush( fd, TCIOFLUSH );
    bufpos = 0;
  }
}

bool Modem::lockDevice()
{
  if (is_locked)
    return true;

	if (flock(fd, LOCK_EX))
	{
	  // Locking failed
    is_locked = false;
    emit errorMessage(i18n("Unable to lock device '%1'.").arg((*prefs).serialDevice()));
	}
	else
	{
  	is_locked = true;
  }

	return is_locked;
}


void Modem::unlockDevice()
{
  if (fd != -1 && is_locked)
  {
		flock(fd, LOCK_UN);
		is_locked = false;
	}
}


bool Modem::dsrOn()
{
  int flags;


  if ( fd == -1 ) {
#ifdef MODEM_DEBUG
    fprintf( stderr, "Modem: dsrOn(): File not open.\n" );
#endif
    return false;
  }

  if ( ioctl( fd, TIOCMGET, &flags ) == -1 ) {
#ifdef MODEM_DEBUG
    fprintf( stderr, "Modem: dsrOn(): ioctl() failed.\n" );
#endif
    return false;
  }

  return ( flags & TIOCM_DSR ) != 0;
}


bool Modem::ctsOn()
{
  int flags;


  if ( fd == -1 ) {
#ifdef MODEM_DEBUG
    fprintf( stderr, "Modem: ctsOn(): File not open.\n" );
#endif
    return false;
  }

  if ( ioctl( fd, TIOCMGET, &flags ) == -1) {
#ifdef MODEM_DEBUG
    fprintf( stderr, "Modem: ctsOn(): ioctl() failed.\n" );
#endif
    return false;
  }

  return ( flags & TIOCM_CTS ) != 0;
}


void Modem::writeChar( const char c )
{
  write( fd, (const void *) &c, 1 );
}


void Modem::writeLine( const char *line )
{
  kdDebug() << "Modem::writeLine(): " << line << endl;

  write( fd, (const void *) line, strlen( line ) );
  writeChar( '\r' );
}


void Modem::timerStart( int msec )
{
  timer->start( msec, true );
}


void Modem::receiveXModem( bool crc )
{
  disconnect( sn, 0, this, 0 );
  connect( sn, TQ_SIGNAL( activated( int ) ), TQ_SLOT( readXChar( int ) ) );

  xcrc = crc;

  if ( xcrc ) {
    writeChar( 'C' );
    xstate = 1;
    timerStart( 3000 );
  } else {
    writeChar( CNAK );
    xstate = 5;
    timerStart( 10000 );
  }

  xblock = 1;
}


void Modem::abortXModem()
{
  timer->stop();
  writeChar( CCAN );
  xreset();
  emit xmodemDone( false );
}


void Modem::timerDone()
{
#ifdef MODEM_DEBUG
  fprintf( stderr, "Modem: timeout, xstate = %d.\n", xstate );
#endif

  switch ( xstate ) {
    case  0:			/* non-XModem mode	*/
      emit timeout();
      break;

    case  1:			/* 1st 'C' sent		*/
    case  2:			/* 2nd 'C' sent		*/
    case  3:			/* 3rd 'C' sent		*/
      writeChar( 'C' );
      xstate++;
      timerStart( 1000 );	/* Should be 3000 in original XModem	*/
      break;

    case  4:			/* 4th 'C' sent		*/
      xcrc = false;

    case  5:			/* 1st <NAK> sent	*/
    case  6:			/* 2nd <NAK> sent	*/
    case  7:			/* 3rd <NAK> sent	*/
    case  8:			/* 4th <NAK> sent	*/
    case  9:			/* 5th <NAK> sent	*/
      writeChar( CNAK );
      xstate++;
      timerStart( 1000 );	/* Should be 10000 in original XModem	*/
      break;

    case 10:			/* 6th <NAK> sent	*/
      xreset();
      emit xmodemDone( false );
      break;

    default:			/* pending XModem block	*/
      writeChar( CNAK );
      xstate = 5;
      timerStart( 1000 );	/* Should be 10000 in original XModem	*/
	}
}


void Modem::readChar( int )
{
  uchar c;


  while ( read( fd, (void *) &c, 1 ) == 1 ) {
    if ( c == '\n' ) {
      buffer[ bufpos ] = 0;
      bufpos = 0;
      emit gotLine( (const char *) buffer );
      break;
    } else
    if ( ( bufpos < 1000 ) && ( c != '\r' ) )
      buffer[ bufpos++ ] = c;
  }
}


void Modem::readXChar( int )
{
  uchar c;
  static uchar crc_hi, block, cblock;


  while ( read( fd, (void *) &c, 1 ) == 1 ) {
    switch ( xstate ) {
      case  1:	/* 1st 'C' sent 	*/
      case  2:	/* 2nd 'C' sent		*/
      case  3:	/* 3rd 'C' sent		*/
      case  4:	/* 4th 'C' sent		*/
      case  5:	/* 1st <NAK> sent	*/
      case  6:	/* 2nd <NAK> sent	*/
      case  7:	/* 3rd <NAK> sent	*/
      case  8:	/* 4th <NAK> sent	*/
      case  9:	/* 5th <NAK> sent	*/
      case 10:	/* 6th <NAK> sent	*/
	if ( c == CSOH ) {
	  timerStart( 1000 );
	  xsize = 128;
	  xstate = 11;
	} else
	if ( c == CSTX ) {
	  timerStart( 1000 );
	  xsize = 1024;
	  xstate = 11;
        } else
	if ( c == CEOT ) {
	  timer->stop();
	  writeChar( CACK );
	  xreset();
	  emit xmodemDone( true );
	} else
	  timerStart( 1000 );
	break;

      case 11:	/* <SOH> or <STX> received	 */
	timerStart( 1000 );
	block = c;
	xstate++;
	break;

      case 12:	/* block number received	*/
	timerStart( 1000 );
	cblock = c;
	xstate++;
	bufpos = 0;
	break;

      case 13:	/* complement block number received	*/
	timerStart( 1000 );
	buffer[ bufpos++ ] = c;
	if ( bufpos == xsize ) {
	  bufpos = 0;
	  xstate++;
	  if ( !xcrc )
	    xstate++;
	}
	break;

      case 14:	/* data block received	*/
	timerStart( 1000 );
	crc_hi = c;
	xstate++;
	break;

      case 15:	/* crc high-byte received	*/
	timerStart( 10000 );
	xstate = 4;
	if ( (uchar) ( block ^ cblock ) != 0xff ) {
	  writeChar( CNAK );
	  break;
	}
	if ( block+1 == xblock ) {
	  writeChar( CACK );
	  break;
	}
	if ( block != xblock ) {
	  timer->stop();
	  writeChar( CCAN );
	  xreset();
	  emit xmodemDone( false );
	  break;
	}
	if ( xcrc ) {
	  if ( ( (ushort) crc_hi << 8 | (ushort) c ) != calcCRC() ) {
	    writeChar( CNAK );
	    break;
	  }
	} else {
	  if ( c != calcChecksum() ) {
	    writeChar( CNAK );
	    break;
	  }
	}
	writeChar( CACK );
	xblock++;
	emit gotXBlock( buffer, xsize );
	break;

      default:
	break;
    }
  }
}


void Modem::init()
{
  is_locked = false;

  fd = -1;
  sn = 0;

  cspeed = B38400;

  // No flow control
  cflag = CS8 | CREAD | CLOCAL;
  // cflag = CS8 | CREAD | CLOCAL | CRTSCTS;

  bufpos = 0;
}


void Modem::xreset()
{
  bufpos = 0;

  xstate = 0;
  xcrc = false;
  xblock = 0;
  xsize = 0;

  if ( sn ) {
    disconnect( sn, 0, this, 0 );
    connect( sn, TQ_SIGNAL( activated( int ) ), TQ_SLOT( readChar( int ) ) );
  }
}


uchar Modem::calcChecksum()
{
  int i;
  uchar c = 0;


  for ( i = 0; i < xsize; i++ )
    c += buffer[ i ];

  return c;
}


ushort Modem::calcCRC()
{
  int i, j;
  ushort c = 0;


  for ( i = 0; i < xsize; i++ ) {
    c ^= (ushort) buffer[ i ] << 8;
    for ( j = 0; j < 8; j++ )
      if ( c & 0x8000 )
        c = c << 1 ^ 0x1021;
      else
	c <<= 1;
  }

  return c;
}

#include "modem.moc"
