//***************************************************************************
// Group VDR/GraphTFT
// File tcpchannel.c
// Date 31.10.06 - Jörg Wendel
// This code is distributed under the terms and conditions of the
// GNU GENERAL PUBLIC LICENSE. See the file COPYING for details.
//--------------------------------------------------------------------------
// Class TcpChannel
//***************************************************************************

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <netdb.h>
#include <errno.h>

#include "tcpchannel.h"
#include "common.h"

//***************************************************************************
// Object
//***************************************************************************

TcpChannel::TcpChannel()
{
   localAddr = 0;
   listener = 0;
   client = 0;
   port = 0;
   localAddr = 0;

   nTtlSent = 0;
   nTtlReceived = 0;

   lookAheadChar = false;
   lookAhead = 0;

   timeout = 30;
}

TcpChannel::~TcpChannel()
{
   if (listener) ::close(listener);
}

//***************************************************************************
// Open -> Start Listener
//***************************************************************************

int TcpChannel::open(unsigned short aPort, const char* aLocalHost)
{
   struct sockaddr_in localSockAddr;
   struct hostent* hostInfo;
   int value = 1;
   int aHandle;
   int status;

   // clear

   memset((char*)&localSockAddr, 0, sizeof(localSockAddr));

   // init

   localSockAddr.sin_family = AF_INET;

   // resolve local host

   if (aLocalHost && *aLocalHost)
   {
      // search alias

      if ((hostInfo = ::gethostbyname(aLocalHost)))
         memcpy((char*)&localAddr, hostInfo->h_addr, hostInfo->h_length);

      else if ((unsigned int)(localAddr = inet_addr(aLocalHost)) == INADDR_NONE)
      {
         tell(1, "unknown hostname '%s'", aLocalHost);
         return fail;
      }

      // set local endpoint

      memcpy(&localSockAddr.sin_addr, &localAddr, sizeof(struct in_addr));
   }

   // Server-Socket

   localSockAddr.sin_port = htons(aPort);

   // open socket

   if ((aHandle = ::socket(PF_INET, SOCK_STREAM, 0)) < 0)
   {
      tell(1, "Error: ");
      return fail;
   }

   // set socket non-blocking

   status = fcntl(aHandle, F_SETFL, O_NONBLOCK);

   if (status < 0)
      tell(1, "Error: ");

   setsockopt(aHandle, SOL_SOCKET, SO_REUSEADDR,
               (char*)&value, sizeof(value));

   // bind address to socket

   if (::bind(aHandle, (struct sockaddr*)&localSockAddr, sizeof(localSockAddr)) < 0)
   {
      ::close(aHandle);
      tell(1, "Error: ");

      return fail;
   }

   if (::listen(aHandle, 5) < 0)
   {
      ::close(aHandle);

      return fail;
   }

   // save

   listener = aHandle;
   port = aPort;

   return success;
}

//***************************************************************************
// Close
//***************************************************************************

int TcpChannel::close()
{
   if (client)
   {
      ::close(client);
      client = 0;
   }

   return success;
}

//***************************************************************************
// Listen
//***************************************************************************

int TcpChannel::listen()
{
   struct sockaddr_in remote;
   struct timeval timeout;
   fd_set readFD;
   int aHandle, num, len;

   timeout.tv_sec  = 0;
   timeout.tv_usec = 1;  // return imediatly
   len = sizeof(remote);

   // clear and set file-descriptor

   FD_ZERO(&readFD);
   FD_SET(listener, &readFD);

   // call select to look for request

   if ((num = ::select(listener+1, &readFD,(fd_set*)0,(fd_set*)0, &timeout)) < 0)
      return checkErrno();

   if (!FD_ISSET(listener, &readFD))
      return wrnNoConnectIndication;

   // connect to client

   if ((aHandle = ::accept(listener, (struct sockaddr*)&remote, (socklen_t*)&len)) < 0)
   {
      tell(1, "Error: Accept failed, errno was %d - '%s'", errno, strerror(errno));
      return errAcceptFailed;
   }

   client = aHandle;

   return success;
}

//***************************************************************************
// Write to client
//***************************************************************************

int TcpChannel::write(int command, const char* buf, int bufLen)
{
   struct timeval wait;
   int result,  nfds;
   fd_set writeFD;
   int nSent = 0;
   Header header;

   cMutexLock lock(&_mutex);

   if (buf && !bufLen)
      bufLen = strlen(buf);

   tell(3, "Writing (%d) header bytes, command (%d), size (%ld)", 
     sizeof(Header), command, bufLen);

   header.command = htonl(command);
   header.size = htonl(bufLen);
   result = ::write(client, &header, sizeof(Header));

   if (result != sizeof(Header))
      return errIOError;

   if (!buf)
      return success;

   tell(3, "Writing (%ld) kb now", bufLen/1024);

   do
   {
      result = ::write(client, buf + nSent, bufLen - nSent);

      if (result < 0)
      {
         if (errno != EWOULDBLOCK)
            return checkErrno();
         
         // time-out for select

         wait.tv_sec  = timeout;
         wait.tv_usec = 0;

         // clear and set file-descriptors

         FD_ZERO(&writeFD);
         FD_SET(client, &writeFD);

         // look event

         if ((nfds = ::select(client+1, 0, &writeFD, 0, &wait)) < 0)
         {
            // Error: Select failed

            return checkErrno();
         }

         // no event occured -> timeout

         if (nfds == 0)
            return wrnTimeout;
      }
      else
      {
         nSent += result;
      }

   } while (nSent < bufLen);

   // increase send counter

   nTtlSent += nSent;

   return success;
}

//***************************************************************************
// Look
//***************************************************************************

int TcpChannel::look(int aTimeout)
{
   struct timeval timeout;
   fd_set readFD, writeFD, exceptFD;
   int n;

   // time-out for select

   timeout.tv_sec  = aTimeout;
   timeout.tv_usec = 1;

   // clear and set file-descriptors

   FD_ZERO(&readFD);
   FD_ZERO(&writeFD);
   FD_ZERO(&exceptFD);

   FD_SET(client, &readFD);
   FD_SET(client, &writeFD);
   FD_SET(client, &exceptFD);

   // look event

   n = ::select(client+1, &readFD, (aTimeout ? 0 : &writeFD) , &exceptFD, &timeout);

   if (n < 0)
      return checkErrno();

   // check exception

   if (FD_ISSET(client, &exceptFD))
      return errUnexpectedEvent;

   // check write ok

   if (!FD_ISSET(client, &writeFD))
      return wrnChannelBlocked;

   // check read-event

   if (!FD_ISSET(client, &readFD))
      return wrnNoEventPending;

   // check first-char

   if (::read(client, &lookAheadChar, 1) == 0)
      return errConnectionClosed;

   // look ahead char received

   lookAhead = true;

   return success;
}

//***************************************************************************
// Read
//***************************************************************************

int TcpChannel::read(char* buf, int bufLen)
{
   int nfds, result;
   fd_set readFD;
   int nReceived;
   struct timeval wait;
   
   memset(buf, 0, bufLen);
   nReceived = 0;

   if (lookAhead)
   {
      *(buf) = lookAheadChar;
      lookAhead = false;
      nReceived++;
   }
   
   while (nReceived < bufLen)
   {
      result = ::read(client, buf + nReceived, bufLen - nReceived);
      
      if (result < 0)
      {
         if (errno != EWOULDBLOCK)
            return checkErrno();
         
         // time-out for select
         
         wait.tv_sec  = timeout;
         wait.tv_usec = 0;
         
         // clear and set file-descriptors
         
         FD_ZERO(&readFD);
         FD_SET(client, &readFD);
         
         // look event
         
         if ((nfds = ::select(client+1, &readFD, 0, 0, &wait)) < 0)
            return checkErrno();
         
         // no event occured -> timeout
         
         if (nfds == 0)
            return wrnTimeout;
      }
      
      else if (result == 0)
      {
         // connection closed -> eof received
         
         return errConnectionClosed;
      }
      
      else
      {
         // inc read char count
         
         nReceived += result;
      }
   }

   nTtlReceived += nReceived;
   tell(3, "Debug: Received %d bytes\n", nReceived);

   return success;
}

//***************************************************************************
// Check Errno
//***************************************************************************

int TcpChannel::checkErrno()
{
   switch (errno)
   {
      case  EINTR:       return wrnSysInterrupt;
      case  EBADF:       return errInvalidEndpoint;
      case  EWOULDBLOCK: return wrnNoDataAvaileble;
      case  ECONNRESET:  return errConnectionClosed;
      default:           return errIOError;
   }
}
