/** \file
 *
 *  Contains the Socket class implementation.
 *
 *  Copyright (c) 2007,2008,2009 MBARI
 *  MBARI Proprietary Information.  All Rights Reserved
 */

#include <cctype>
#include <cerrno>
#include <cstdio>
#include <fcntl.h>
#include <sys/time.h>

#include "Socket.h"

Socket::Socket()
    : sock_( -1 ),
      connected_( false )
{
    memset( &addr_, 0, sizeof( addr_ ) );
}

Socket::~Socket()
{
    if( isValid() )
    {
        ::close( sock_ );
    }
}

void Socket::create()
#ifndef __GAZEBO
throw( SocketException& )
#endif
{
    //sock_ = socket( PF_INET, SOCK_DGRAM, IPPROTO_UDP );
    sock_ = socket( PF_INET, SOCK_STREAM, 0 );

    if( ! isValid() )
    {
        throw SocketException( "Could not create socket due to error: ", strerror( errno ) );
    }

    // TIME_WAIT - argh
    int on = 1;
    if( 0 > setsockopt( sock_, SOL_SOCKET, SO_REUSEADDR, ( const char* ) & on, sizeof( on ) ) )
    {
        throw SocketException( "Could not configure socket due to error: ", strerror( errno ) );
    }

}

void Socket::bind( const int port )
#ifndef __GAZEBO
throw( SocketException& )
#endif
{
    if( ! isValid() )
    {
        throw SocketException( "Can not bind to closed connection." );
    }
    addr_.sin_family = AF_INET;
    addr_.sin_addr.s_addr = INADDR_ANY;
    addr_.sin_port = htons( port );
    if( 0 > ::bind( sock_, ( struct sockaddr * ) & addr_, sizeof( addr_ ) ) )
    {
        throw SocketException( "Could not bind socket to port due to error: ", strerror( errno ), errno );
    }

}

void Socket::listen() const
#ifndef __GAZEBO
throw( SocketException& )
#endif
{
    if( ! isValid() )
    {
        throw SocketException( "Can not listen to closed connection." );
    }
    if( 0 > ::listen( sock_, MAXCONNECTIONS ) )
    {
        throw SocketException( "Could not listen to socket due to error: ", strerror( errno ), errno );
    }
}

void Socket::accept( Socket& newSocket )
#ifndef __GAZEBO
throw( SocketException& )
#endif
{
    if( connected_ )
    {
        throw SocketException( "Can not accept connection on connected socket." );
    }
    int addr_length = sizeof( addr_ );
    newSocket.sock_ = ::accept( sock_, ( sockaddr * ) & addr_, ( socklen_t * ) & addr_length );
    if( 0 > newSocket.sock_ )
    {
        throw SocketException( "Could not accept socket connections due to error: ", strerror( errno ), errno );
    }
    newSocket.connected_ = true;
}

void Socket::accept( Socket& newSocket, int timeoutMillis )
#ifndef __GAZEBO
throw( SocketException& )
#endif
{
    if( connected_ )
    {
        throw SocketException( "Can not accept connection on connected socket." );
    }
    setNonBlocking( true );

    fd_set read_fds;
    FD_ZERO( &read_fds );
    FD_SET( sock_, &read_fds );

    struct timeval tv;
    setTimeval( timeoutMillis, tv );

    const char* errormsg = 0;
    int   errorcode = 0;  // to store errno in case of error

    int retval = select( sock_ + 1, &read_fds, 0, 0, &tv );
    if( retval < 0 )
    {
        errormsg = "Could not accept socket connections due to error in select: ";
        errorcode = errno;
    }
    else if( retval )
    {
        // ok, proceed with accept:
        int addr_length = sizeof( addr_ );
        newSocket.sock_ = ::accept( sock_, ( sockaddr * ) & addr_, ( socklen_t * ) & addr_length );
        if( newSocket.sock_ >= 0 )
        {
            newSocket.connected_ = true;
        }
        else
        {
            errormsg = "Could not accept socket connections due to error: ";
            errorcode = errno;
        }
    }
    else
    {
        errormsg = "Timeout while waiting for client connection";
        errorcode = EAGAIN;
    }

    setNonBlocking( false );

    if( errormsg != 0 )
    {
        throw SocketException( errormsg, errorcode );
    }
}

/// If return value is -1, check errno
int Socket::send( const char* buffer, size_t bufferLength ) const
#ifndef __GAZEBO
throw( SocketException& )
#endif
{
    if( !connected_ )
    {
        throw SocketException( "Can not send to unconnected socket." );
    }
#ifndef __APPLE_CC__
    int status = ::send( sock_, buffer, bufferLength, MSG_NOSIGNAL );
#else
    int status = ::send( sock_, buffer, bufferLength, 0 );
#endif
    if( -1 == status )
    {
        throw SocketException( "Could not send to socket due to error: ", strerror( errno ), errno );
    }
    return status;
}

int Socket::sendall( const char* buffer, size_t bufferLength )
{
    if( !connected_ )
    {
        return -2;
    }

    int bytesleft = bufferLength; // #bytes left to send
    int total = 0;                // #bytes sent
    int n;

    while( total < ( int ) bufferLength )
    {
#ifndef __APPLE_CC__
        n = ::send( sock_, buffer + total, bytesleft, MSG_NOSIGNAL );
#else
        n = ::send( sock_, buffer + total, bytesleft, 0 );
#endif
        if( n == -1 )
        {
            return -1;
        }
        total += n;
        bytesleft -= n;
    }

    return total;
}

/// If return value is -1, check errno
int Socket::recv( char* buffer, size_t bufferLength ) const
#ifndef __GAZEBO
throw( SocketException& )
#endif
{
    if( !connected_ )
    {
        throw SocketException( "Can not recieve from unconnected socket." );
    }
    int status = ::recv( sock_, buffer, bufferLength, 0 );
    if( -1 == status )
    {
        throw SocketException( "Could not read from socket due to error: ", strerror( errno ), errno );
    }
    return status;
}

/// If return value is -1, check errno
int Socket::recv( char* buffer, size_t bufferLength, int timeoutMillis )
#ifndef __GAZEBO
throw( SocketException& )
#endif
{
    if( !connected_ )
    {
        throw SocketException( "Can not receive from unconnected socket." );
    }
    setNonBlocking( true );

    fd_set read_fds;
    FD_ZERO( &read_fds );
    FD_SET( sock_, &read_fds );

    struct timeval tv;
    setTimeval( timeoutMillis, tv );

    int errorcode = 0;  // to store errno in case of error

    int retval = select( sock_ + 1, &read_fds, 0, 0, &tv );
    if( retval < 0 )
    {
        errorcode = errno;     // save errno before this other call ...
        setNonBlocking( false );
        errno = errorcode ;    // ... and restore it
        throw SocketException( "Could not recv due to error in select: ", errorcode );
    }
    else if( retval )
    {
        // ok we got something; proceed with recv, which should normally complete ok
        retval = ::recv( sock_, buffer, bufferLength, 0 );
        errorcode = errno;
    }
    else
    {
        // it is a timeout; capture EAGAIN in errno (via errorcode) as a way to expose this
        retval = -1;
        errorcode = EAGAIN;
    }

    setNonBlocking( false );

    if( retval == - 1 )
    {
        errno = errorcode;
    }

    return retval;
}

int Socket::setTimeout( const int milliseconds )
{
    struct timeval tv;
    tv.tv_sec = milliseconds / 1000;
    tv.tv_usec = ( milliseconds % 1000 ) * 1000;
    return setsockopt( sock_, SOL_SOCKET, SO_RCVTIMEO, ( void* )&tv, sizeof( tv ) );
}

void Socket::setTimeval( const int milliseconds, struct timeval& tv )
{
    tv.tv_sec = milliseconds / 1000;
    tv.tv_usec = ( milliseconds % 1000 ) * 1000;
}

int Socket::close()
{
    if( connected_ )
    {
        connected_ = false;
        return ::close( sock_ );
    }
    else
    {
        return 0;
    }
}

void Socket::connect( const char* host, const int port )
#ifndef __GAZEBO
throw( SocketException& )
#endif
{
    if( connected_ )
    {
        throw SocketException( "Can not reconnect socket." );
    }

    if( ! isValid() )
    {
        throw SocketException( "Can not connect to closed connection." );
    }

    addr_.sin_family = AF_INET;
    addr_.sin_port = htons( port );

    int status = -1;
    if( isdigit( host[0] ) )
    {
        status = inet_pton( AF_INET, host, &addr_.sin_addr );
    }
    if( status <= 0 )
    {
        struct hostent* he = gethostbyname( host );
        if( he == NULL )  // do some error checking
        {
            throw SocketException( "Can not connect to invalid network address: ", host );
        }
        addr_.sin_addr = *( ( struct in_addr* )he->h_addr );
    }

    status = ::connect( sock_, ( sockaddr * ) & addr_, sizeof( addr_ ) );
    connected_ = 0 == status;

    if( 0 > status )
    {
        throw SocketException( "Could not connect due to error: ", strerror( errno ), errno );
    }
}

void Socket::connect( const char* host, const int port, int timeoutMillis )
#ifndef __GAZEBO
throw( SocketException& )
#endif
{
    if( connected_ )
    {
        throw SocketException( "Can not reconnect socket." );
    }

    if( ! isValid() )
    {
        throw SocketException( "Can not connect to closed connection." );
    }

    addr_.sin_family = AF_INET;
    addr_.sin_port = htons( port );

    int status = -1;
    if( isdigit( host[0] ) )
    {
        status = inet_pton( AF_INET, host, &addr_.sin_addr );
    }
    if( status <= 0 )
    {
        struct hostent* he = gethostbyname( host );
        if( he == NULL )  // do some error checking
        {
            throw SocketException( "Can not connect to invalid network address: ", host );
        }
        addr_.sin_addr = *( ( struct in_addr* )he->h_addr );
    }

    // up to here, similar to initial part of connect( const char* host, const int port ).

    // devel convenience flag for some ad hoc logging below:
    const bool DBG  = false;

    setNonBlocking( true );

    const char* errormsg = 0;
    int   errorcode = 0;  // to store errno in case of error

    // try to connect:
    int conRes = ::connect( sock_, ( sockaddr * ) & addr_, sizeof( addr_ ) );

    if( conRes < 0 )
    {
        if( DBG ) fprintf( stderr, "~~connect returned=%d, errno=%d: %s\n", conRes, errno, strerror( errno ) );

        if( errno == EINPROGRESS )
        {
            /*
             * From `man connect`:
             * EINPROGRESS: The  socket  is  non-blocking  and  the connection cannot be completed immediately.
             * It is possible to select(2) or poll(2) for completion by selecting the socket for writing. After
             * select(2) indicates writability, use getsockopt(2) to read the SO_ERROR option at level SOL_SOCKET
             * to determine whether connect() completed successfully (SO_ERROR is zero) or unsuccessfully
             * (SO_ERROR is one  of  the usual error codes listed here, explaining the reason for the failure).
             */

            errno = 0;

            fd_set fds;
            FD_ZERO( &fds );
            FD_SET( sock_, &fds );

            struct timeval tv;
            setTimeval( timeoutMillis, tv );

            int selRes = ::select( sock_ + 1, 0, &fds, 0, &tv );

            if( selRes > 0 )
            {
                // socket is "ready" -- check with getsockopt:
                errno = 0;
                int valopt;
                socklen_t lon = sizeof( int );
                int optRes = ::getsockopt( sock_, SOL_SOCKET, SO_ERROR, ( void * )( &valopt ), &lon );

                if( DBG ) fprintf( stderr, "~~getsockopt returned=%d, errno=%d: %s\n", optRes, errno, strerror( errno ) );

                if( optRes < 0 )
                {
                    errormsg = "Could not connect socket due to error in getsockopt: ";
                    errorcode = errno;
                }
                else
                {
                    if( DBG ) fprintf( stderr, "~~getsockopt reported valopt=%d: %s\n", valopt, strerror( valopt ) );
                    if( valopt == ECONNREFUSED )
                    {
                        errormsg = "Connection refused while connecting socket: ";
                        errorcode = EAGAIN;
                    }
                    else if( valopt )
                    {
                        errormsg = "Could not connect socket; getsockopt reports: ";
                        errorcode = valopt;
                    }
                }
                // else: connection ok.
            }
            else if( selRes < 0 && errno != EINTR )
            {
                errormsg = "Could not connect socket due to error in select: ";
                errorcode = errno;
            }
            else
            {
                // timeout or EINTR:
                errormsg = "Timeout while connecting socket";
                errorcode = EAGAIN;
            }
        }
        else
        {
            errormsg = "Could not connect socket due to error in connect: ";
            errorcode = errno;
        }
    }

    setNonBlocking( false );

    if( errormsg != 0 )
    {
        if( DBG ) fprintf( stderr, "~~errormsg=%s errorcode=%d: %s\n", errormsg, errorcode, strerror( errorcode ) );
        throw SocketException( errormsg, errorcode );
    }
    else
    {
        connected_ = true;
    }

}

void Socket::setNonBlocking( const bool nonBlocking )
#ifndef __GAZEBO
throw( SocketException& )
#endif
{

    int opts = fcntl( sock_, F_GETFL );
    if( 0 > opts )
    {
        throw SocketException( "Could not set non-blocking due to error: ", strerror( errno ), errno );
    }
    if( nonBlocking )
    {
        opts = ( opts | O_NONBLOCK );
    }
    else
    {
        opts = ( opts & ~O_NONBLOCK );
    }
    int status = fcntl( sock_, F_SETFL, opts );
    if( 0 > status )
    {
        throw SocketException( "Could not set non-blocking due to error: ", strerror( errno ), errno );
    }
}

