// *************************************************************************
// * Taken from the GSM TA/ME library
// *
// * File:	  kopete_unix_serial.cpp
// *
// * Purpose: UNIX serial port implementation with extras
// *
// * Original Author:  Peter Hofmann (software@pxh.de)
// * Modified by: Justin Huff (jjhuff@mspin.net)
// *
// * Created: 10.5.1999
// *************************************************************************
#include "config.h"

#include <gsmlib/gsm_util.h>
#include <termios.h>
#include <fcntl.h>
#include <iostream>
#include <sstream>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <signal.h>
#include <pthread.h>
#include <cassert>
#include <assert.h>

#include <tqsocketnotifier.h>

#include "kopete_unix_serial.h"

using namespace std;
using namespace gsmlib;

static const int holdoff[] = {2000000, 1000000, 400000};
static const int holdoffArraySize = sizeof(holdoff)/sizeof(int);
  
// alarm handling for socket read/write
// the timerMtx is necessary since several threads cannot use the
// timer indepently of each other

static pthread_mutex_t timerMtx = PTHREAD_MUTEX_INITIALIZER;
#define pthread_mutex_lock(x) 
#define pthread_mutex_unlock(x) 

// for non-GNU systems, define alarm()
#ifndef HAVE_ALARM
unsigned int alarm(unsigned int seconds)
{
  struct itimerval old, newt;
  newt.it_interval.tv_usec = 0;
  newt.it_interval.tv_sec = 0;
  newt.it_value.tv_usec = 0;
  newt.it_value.tv_sec = (long int)seconds;
  if (setitimer(ITIMER_REAL, &newt, &old) < 0)
	return 0;
  else
	return old.it_value.tv_sec;
}
#endif

// this routine is called in case of a timeout
static void catchAlarm(int)
{
  // do nothing
}

// start timer
static void startTimer()
{
  pthread_mutex_lock(&timerMtx);
  struct sigaction newAction;
  newAction.sa_handler = catchAlarm;
  newAction.sa_flags = 0;
  sigaction(SIGALRM, &newAction, NULL);
  alarm(1);
}

// reset timer
static void stopTimer()
{
  alarm(0);
  sigaction(SIGALRM, NULL, NULL);
  pthread_mutex_unlock(&timerMtx);
}

// KopeteUnixSerialPort members

void KopeteUnixSerialPort::throwModemException(string message)
{
  ostringstream os;
  os << message << " (errno: " << errno << "/" << strerror(errno) << ")";
  throw GsmException(os.str(), OSError, errno);
}

void KopeteUnixSerialPort::putBack(unsigned char c)
{
  assert(_oldChar == -1);
  _oldChar = c;
}

int KopeteUnixSerialPort::readByte()
{
  if (_oldChar != -1)
  {
	int result = _oldChar;
	_oldChar = -1;
	return result;
  }

  unsigned char c;
  int timeElapsed = 0;
  struct timeval oneSecond;
  bool readDone = false;

  while (! readDone && timeElapsed < _timeoutVal)
  {
	if (interrupted())
	  throwModemException("interrupted when reading from TA");

	// setup fd_set data structure for select()
	fd_set fdSet;
	oneSecond.tv_sec = 1;
	oneSecond.tv_usec = 0;
	FD_ZERO(&fdSet);
	FD_SET(_fd, &fdSet);

	switch (select(FD_SETSIZE, &fdSet, NULL, NULL, &oneSecond))
	{
	case 1:
	{
	  int res = read(_fd, &c, 1);
	  if (res != 1)
		throwModemException("end of file when reading from TA");
	  else
		readDone = true;
	  break;
	}
	case 0:
	  ++timeElapsed;
	  break;
	default:
	  if (errno != EINTR)
		throwModemException("reading from TA");
	  break;
	}
  }
  if (! readDone)
	throwModemException("timeout when reading from TA");

#ifndef NDEBUG
  if (debugLevel() >= 2)
  {
	// some useful debugging code
	if (c == LF)
	  cerr << "<LF>";
	else if (c == CR)
	  cerr << "<CR>";
	else cerr << "<'" << (char) c << "'>";
	cerr.flush();
  }
#endif
  return c;
}

KopeteUnixSerialPort::KopeteUnixSerialPort(string device, speed_t lineSpeed,
							   string initString, bool swHandshake) :
  _oldChar(-1), _timeoutVal(TIMEOUT_SECS)
{
	_readNotifier = NULL;
	
  struct termios t;

  // open device
  _fd = open(device.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK);
  if (_fd == -1)
	throwModemException("opening device");

  // switch off non-blocking mode
  int fdFlags;
  if ((fdFlags = fcntl(_fd, F_GETFL)) == -1)
  {
	close(_fd);
	throwModemException("getting file status flags failed");
  }
  fdFlags &= ~O_NONBLOCK;
  if (fcntl(_fd, F_SETFL, fdFlags) == -1)
  {
	close(_fd);
	throwModemException("switching of non-blocking mode failed");
  }

	// Set the close on exec flag
	if ((fdFlags = fcntl(_fd, F_GETFD)) == -1)
	{
		close(_fd);
		throwModemException("getting file status flags failed");
	}
	fdFlags |= FD_CLOEXEC;
	if (fcntl(_fd, F_SETFD, fdFlags) == -1)
	{
		close(_fd);
		throwModemException("switching of non-blocking mode failed");
	}

  long int saveTimeoutVal = _timeoutVal;
  _timeoutVal = 3;
  int initTries = holdoffArraySize;
  while (initTries-- > 0)
  {
	// flush all pending output
	tcflush(_fd, TCOFLUSH);

	// toggle DTR to reset modem
	int mctl = TIOCM_DTR;
	if (ioctl(_fd, TIOCMBIC, &mctl) < 0 && errno != ENOTTY)
	{
	  close(_fd);
	  throwModemException("clearing DTR failed");
	}
	// the waiting time for DTR toggling is increased with each loop
	usleep(holdoff[initTries]);
	if (ioctl(_fd, TIOCMBIS, &mctl) < 0 && errno != ENOTTY)
	{
	  close(_fd);
	  throwModemException("setting DTR failed");
	}
	// get line modes
	if (tcgetattr(_fd, &t) < 0)
	{
	  close(_fd);
	  throwModemException("tcgetattr device");
	}

	// set line speed
	cfsetispeed(&t, lineSpeed);
	cfsetospeed(&t, lineSpeed);

	// set the device to a sane state
	t.c_iflag |= IGNPAR | (swHandshake ? IXON | IXOFF : 0);
	t.c_iflag &= ~(INPCK | ISTRIP | IMAXBEL |
				   (swHandshake ? 0 : IXON |  IXOFF)
				   | IXANY | IGNCR | ICRNL | IMAXBEL | INLCR | IGNBRK);
	t.c_oflag &= ~(OPOST);
	// be careful, only touch "known" flags
	t.c_cflag &= ~(CSIZE | CSTOPB | PARENB | PARODD |
				  (swHandshake ? CRTSCTS : 0 ));
	t.c_cflag |= CS8 | CREAD | HUPCL | (swHandshake ? 0 : CRTSCTS) | CLOCAL;
	t.c_lflag &= ~(ECHO | ECHOE | ECHOPRT | ECHOK | ECHOKE | ECHONL |
				   ECHOCTL | ISIG | IEXTEN | TOSTOP | FLUSHO | ICANON);
	t.c_lflag |= NOFLSH;
	t.c_cc[VMIN] = 1;
	t.c_cc[VTIME] = 0;

	t.c_cc[VSUSP] = 0;

	// write back
	if(tcsetattr (_fd, TCSANOW, &t) < 0)
	{
	  close(_fd);
	  throwModemException("tcsetattr device");
	}
	// the waiting time for writing to the ME/TA is increased with each loop
	usleep(holdoff[initTries]);

	// flush all pending input
	tcflush(_fd, TCIFLUSH);

	try
	{
	  // reset modem
	  putLine("ATZ");
	  bool foundOK = false;
	  int readTries = 5;
	  while (readTries-- > 0)
	  {
		// for the first call getLine() waits only 3 seconds
		// because of _timeoutVal = 3
		string s = getLine();
		if (s.find("OK") != string::npos ||
			s.find("CABLE: GSM") != string::npos)
		{
		  foundOK = true;
		  readTries = 0;		// found OK, exit loop
		}
		else if (s.find("ERROR") != string::npos)
		  readTries = 0;		// error, exit loop
	  }

	  // set getLine/putLine timeout back to old value
	  _timeoutVal = saveTimeoutVal;

	  if (foundOK)
	  {
		// init modem
		readTries = 5;
		putLine("AT" + initString);
		while (readTries-- > 0)
		{
		  string s = getLine();
		  if (s.find("OK") != string::npos ||
			  s.find("CABLE: GSM") != string::npos)
		  {
				_readNotifier = new TQSocketNotifier(_fd, TQSocketNotifier::Read);
					connect( _readNotifier, TQ_SIGNAL(activated(int)), this, TQ_SIGNAL(activated()));
				return;					// found OK, return
		  }
		}
	  }
	}
	catch (GsmException &e)
	{
	  _timeoutVal = saveTimeoutVal;
	  if (initTries == 0)
	  {
		close(_fd);
		throw e;
	  }
	}
  }
  // no response after 3 tries
  close(_fd);
  throwModemException("reset modem failed");
}

string KopeteUnixSerialPort::getLine()
{
  string result;
  int c;
  while ((c = readByte()) >= 0)
  {
	while (c == CR)
	{
	  c = readByte();
	}
	if (c == LF)
	  break;
	result += c;
  }

#ifndef NDEBUG
  if (debugLevel() >= 1)
	cerr << "<-- " << result << endl;
#endif

  return result;
}

void KopeteUnixSerialPort::putLine(string line, bool carriageReturn)
{
#ifndef NDEBUG
  if (debugLevel() >= 1)
	cerr << "--> " << line << endl;
#endif

  if (carriageReturn) line += CR;
  const char *l = line.c_str();
  
  int timeElapsed = 0;
  struct timeval oneSecond;

  ssize_t bytesWritten = 0;
  while (bytesWritten < (ssize_t)line.length() && timeElapsed < _timeoutVal)
  {
	if (interrupted())
	  throwModemException("interrupted when writing to TA");

	// setup fd_set data structure for select()
	fd_set fdSet;
	oneSecond.tv_sec = 1;
	oneSecond.tv_usec = 0;
	FD_ZERO(&fdSet);
	FD_SET(_fd, &fdSet);

	switch (select(FD_SETSIZE, NULL, &fdSet, NULL, &oneSecond))
	{
	case 1:
	{
	  ssize_t bw = write(_fd, l + bytesWritten, line.length() - bytesWritten);
	  if (bw < 0)
		throwModemException("writing to TA");
	  bytesWritten += bw;
	  break;
	}
	case 0:
	  ++timeElapsed;
	  break;
	default:
	  if (errno != EINTR)
		throwModemException("writing to TA");
	  break;
	}
  }
  
  while (timeElapsed < _timeoutVal)
  {
	if (interrupted())
	  throwModemException("interrupted when writing to TA");
	::startTimer();
	int res = tcdrain(_fd);		// wait for output to be read by TA
	::stopTimer();
	if (res == 0)
	  break;
	else
	{
	  assert(errno == EINTR);
	  ++timeElapsed;
	}
  }
  if (timeElapsed >= _timeoutVal)
	throwModemException("timeout when writing to TA");

  // echo CR LF must be removed by higher layer functions in gsm_at because
  // in order to properly handle unsolicited result codes from the ME/TA
}

bool KopeteUnixSerialPort::wait(GsmTime timeout)
{
  fd_set fds;
  FD_ZERO(&fds);
  FD_SET(_fd, &fds);
  return select(FD_SETSIZE, &fds, NULL, NULL, timeout) != 0;
}

// set timeout for read or write in seconds.
void KopeteUnixSerialPort::setTimeOut(unsigned int timeout)
{
  _timeoutVal = timeout;
}

KopeteUnixSerialPort::~KopeteUnixSerialPort()
{
	delete _readNotifier;
	_readNotifier = NULL;
	if (_fd != -1)
		close(_fd);
}

#include "kopete_unix_serial.moc"
