/*
 * Copyright (C) 2007-2023 Xagasoft, All rights reserved.
 *
 * This file is part of the libbu++ library and is released under the
 * terms of the license contained in the file LICENSE.
 */

#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <errno.h>
#include <fcntl.h>
#include "bu/sockettcp.h"

#include "bu/config.h"

#ifndef WIN32
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <netdb.h>
 #include <arpa/inet.h>
#else
 #include <Winsock2.h>
#endif

#define RBS (1024*2)

namespace Bu { subExceptionDef( SocketTcpException ) }

Bu::SocketTcp::SocketTcp( handle nSocketTcp ) :
    nSocketTcp( nSocketTcp ),
    bActive( true ),
    bBlocking( true )
{
#ifdef WIN32
    Bu::Winsock2::getInstance();
#endif
    setAddress();
}

Bu::SocketTcp::SocketTcp( const Bu::String &sAddr, int nPort, int nTimeout,
        bool bBlocking ) :
    nSocketTcp( 0 ),
    bActive( false ),
    bBlocking( true )
{
#ifdef WIN32
    Bu::Winsock2::getInstance();
#endif

    /* Create the socket. */
    nSocketTcp = bu_socket( PF_INET, SOCK_STREAM, 0 );
    
#ifdef WIN32
    if( nSocketTcp == INVALID_SOCKET )
#else
    if( nSocketTcp < 0 )
#endif
    {
        throw ExceptionBase("Couldn't create socket.\n");
    }

    setBlocking( false );
     
    /* Connect to the server. */
    //printf("Resolving hostname (%s)...\n", sAddr );
    {
        struct addrinfo *pAddr = NULL;
        struct addrinfo aiHints;
        memset( &aiHints, 0, sizeof(addrinfo) );
        aiHints.ai_flags = AI_CANONNAME;
        aiHints.ai_family = AF_INET;
        aiHints.ai_socktype = SOCK_STREAM;
        char ibuf[10]; 
        sprintf( ibuf, "%d", nPort );
     
        int ret;
        if( (ret = bu_getaddrinfo(
            sAddr.getStr(), ibuf, &aiHints, &pAddr )) != 0 )
        {
            close();
            throw Bu::SocketTcpException("Couldn't resolve hostname %s (%s).\n",
                sAddr.getStr(), bu_gai_strerror(ret));
        }

        bu_connect(
            nSocketTcp,
            pAddr->ai_addr,
            pAddr->ai_addrlen
            );

        sAddress = pAddr->ai_canonname;

        bu_freeaddrinfo( pAddr );
    }

    bActive = true;

    if( nTimeout > 0 )
    {
        fd_set rfds, wfds, efds;
        int retval;
        
        FD_ZERO(&rfds);
        FD_SET(nSocketTcp, &rfds);
        FD_ZERO(&wfds);
        FD_SET(nSocketTcp, &wfds);
        FD_ZERO(&efds);
        FD_SET(nSocketTcp, &efds);

        struct timeval tv;
        tv.tv_sec = nTimeout;
        tv.tv_usec = 0;
        
        retval = bu_select( nSocketTcp+1, &rfds, &wfds, &efds, &tv );

        if( retval == 0 )
        {
            close();
            throw ExceptionBase("Connection timeout.\n");
        }
        read( NULL, 0 ); // See if we can get any errors out of the way early.
    }

    if( bBlocking )
        setBlocking( bBlocking );
}

Bu::SocketTcp::~SocketTcp()
{
    close();
}

void Bu::SocketTcp::close()
{
    if( bActive )
    {
#ifndef WIN32
        fsync( nSocketTcp );
#endif
#ifdef WIN32
 #ifndef SHUT_RDWR
  #define SHUT_RDWR (SD_BOTH)
 #endif
#endif
        bu_shutdown( nSocketTcp, SHUT_RDWR );
        ::close( nSocketTcp );
    }
    bActive = false;
}

Bu::size Bu::SocketTcp::read( void *pBuf, Bu::size nBytes )
{
    fd_set rfds;
    FD_ZERO(&rfds);
    FD_SET(nSocketTcp, &rfds);
    struct timeval tv = {0, 0};
    if( bu_select( nSocketTcp+1, &rfds, NULL, NULL, &tv ) < 0 )
    {
        int iErr = errno;
        close();
        throw SocketTcpException( SocketTcpException::cRead, strerror(iErr) );
    }
    if( FD_ISSET( nSocketTcp, &rfds ) || bBlocking )
    {
        int nRead = TEMP_FAILURE_RETRY( 
            bu_recv( nSocketTcp, (char *) pBuf, nBytes, 0 ) );
        if( nRead == 0 && nBytes > 0 )
        {
            close();
            throw SocketTcpException( SocketTcpException::cClosed, "SocketTcp closed.");
        }
        if( nRead < 0 )
        {
#ifdef WIN32
            int iWSAError = bu_WSAGetLastError();
            if( iWSAError == WSAEWOULDBLOCK )
                return 0;
#else
            if( errno == ENETRESET || errno == ECONNRESET )
            {
                close();
                throw SocketTcpException( SocketTcpException::cClosed,
                    strerror(errno) );
            }
            if( errno == EAGAIN )
                return 0;
            int iErr = errno;
            close();
            throw SocketTcpException( SocketTcpException::cRead, strerror(iErr) );
#endif
        }
        return nRead;
    }
    return 0;
}

Bu::size Bu::SocketTcp::read( void *pBuf, Bu::size nBytes,
        uint32_t nSec, uint32_t nUSec )
{
    struct timeval tv;
    Bu::size nRead = 0;
    
    fd_set rfds;
    FD_ZERO(&rfds);
    FD_SET(nSocketTcp, &rfds);

#ifdef WIN32
    DWORD dwStart = GetTickCount();
    uint64_t uOver = dwStart + ((nUSec / 1000) * (nSec * 1000));
    DWORD dwEnd = uOver>4294967295U?uOver-4294967295U:uOver;
#else
    struct timeval nt, ct;
    gettimeofday( &nt, NULL );
    nt.tv_sec += nSec;
    nt.tv_usec += nUSec;
#endif

    for(;;)
    {
        tv.tv_sec = nSec;
        tv.tv_usec = nUSec;
        bu_select( nSocketTcp+1, &rfds, NULL, NULL, &tv );
        nRead += read( ((char *)pBuf)+nRead, nBytes-nRead );
        if( nRead >= nBytes )
            break;
#ifdef WIN32
        DWORD dwNow = GetTickCount();
        if( dwNow > dwEnd )
            break;
#else
        gettimeofday( &ct, NULL );
        if( (ct.tv_sec > nt.tv_sec) ||
            (ct.tv_sec == nt.tv_sec &&
            ct.tv_usec >= nt.tv_usec) )
            break;
#endif
    }
    return nRead;
}

Bu::size Bu::SocketTcp::write( const void *pBuf, Bu::size nBytes )
{
//#ifdef WIN32
    int nWrote = TEMP_FAILURE_RETRY( 
        bu_send( nSocketTcp, (const char *) pBuf, nBytes, 0 ) );
//#else
//  int nWrote = TEMP_FAILURE_RETRY( ::write( nSocketTcp, pBuf, nBytes ) );
//#endif
    if( nWrote < 0 )
    {
#ifdef WIN32
        int iWSAError = bu_WSAGetLastError();
        if( iWSAError == WSAEWOULDBLOCK )
            return 0;
#else
        if( errno == EAGAIN ) return 0;
#endif
        throw SocketTcpException( SocketTcpException::cWrite, strerror(errno) );
    }
    return nWrote;
}

Bu::size Bu::SocketTcp::write( const void *pBuf, Bu::size nBytes, uint32_t nSec, uint32_t nUSec )
{
    struct timeval tv;
    Bu::size nWrote = 0;
    
    fd_set wfds;
    FD_ZERO(&wfds);
    FD_SET(nSocketTcp, &wfds);

#ifdef WIN32
    DWORD dwStart = GetTickCount();
    uint64_t uOver = dwStart + ((nUSec / 1000) * (nSec * 1000));
    DWORD dwEnd = uOver>4294967295U?uOver-4294967295U:uOver;
#else
    struct timeval nt, ct;
    gettimeofday( &nt, NULL );
    nt.tv_sec += nSec;
    nt.tv_usec += nUSec;
#endif

    for(;;)
    {
        tv.tv_sec = nSec;
        tv.tv_usec = nUSec;
        bu_select( nSocketTcp+1, NULL, &wfds, NULL, &tv );
        nWrote += write( ((char *)pBuf)+nWrote, nBytes-nWrote );
        if( nWrote >= nBytes )
            break;
#ifdef WIN32
        DWORD dwNow = GetTickCount();
        if( dwNow > dwEnd )
            break;
#else
        gettimeofday( &ct, NULL );
        if( (ct.tv_sec > nt.tv_sec) ||
            (ct.tv_sec == nt.tv_sec &&
            ct.tv_usec >= nt.tv_usec) )
            break;
#endif
    }
    return nWrote;
}

Bu::size Bu::SocketTcp::tell()
{
    throw UnsupportedException();
}

void Bu::SocketTcp::seek( Bu::size )
{
    throw UnsupportedException();
}

void Bu::SocketTcp::setPos( Bu::size )
{
    throw UnsupportedException();
}

void Bu::SocketTcp::setPosEnd( Bu::size )
{
    throw UnsupportedException();
}

bool Bu::SocketTcp::isEos()
{
    return !bActive;
}

bool Bu::SocketTcp::canRead()
{
    fd_set rfds;
    FD_ZERO(&rfds);
    FD_SET(nSocketTcp, &rfds);
    struct timeval tv = { 0, 0 };
    int retval = bu_select( nSocketTcp+1, &rfds, NULL, NULL, &tv );
    if( retval == -1 )
        throw SocketTcpException(
            SocketTcpException::cBadRead,
            "Bad Read error"
            );

    if( !FD_ISSET( nSocketTcp, &rfds ) )
        return false;
    return true;
}

bool Bu::SocketTcp::canWrite()
{
    fd_set wfds;
    FD_ZERO(&wfds);
    FD_SET(nSocketTcp, &wfds);
    struct timeval tv = { 0, 0 };
    int retval = bu_select( nSocketTcp+1, NULL, &wfds, NULL, &tv );
    if( retval == -1 )
        throw SocketTcpException(
            SocketTcpException::cBadRead,
            "Bad Read error"
            );
    if( !FD_ISSET( nSocketTcp, &wfds ) )
        return false;
    return true;
}

bool Bu::SocketTcp::isReadable()
{
    return true;
}

bool Bu::SocketTcp::isWritable()
{
    return true;
}

bool Bu::SocketTcp::isSeekable()
{
    return false;
}

bool Bu::SocketTcp::isBlocking()
{
#ifndef WIN32
    return ((fcntl( nSocketTcp, F_GETFL, 0 ) & O_NONBLOCK) != O_NONBLOCK);
#else
    return false;
#endif
}

void Bu::SocketTcp::setBlocking( bool bBlocking )
{
    this->bBlocking = bBlocking;
#ifndef WIN32
    if( bBlocking )
    {
        fcntl( nSocketTcp, F_SETFL, fcntl( nSocketTcp, F_GETFL, 0 ) & (~O_NONBLOCK) );
    }
    else
    {
        fcntl( nSocketTcp, F_SETFL, fcntl( nSocketTcp, F_GETFL, 0 ) | O_NONBLOCK );
    }
#else
    u_long iMode;
    if( bBlocking )
        iMode = 0;
    else
        iMode = 1;
    //-------------------------
    // Set the socket I/O mode: In this case FIONBIO
    // enables or disables the blocking mode for the 
    // socket based on the numerical value of iMode.
    // If iMode = 0, blocking is enabled; 
    // If iMode != 0, non-blocking mode is enabled.
    bu_ioctlsocket(nSocketTcp, FIONBIO, &iMode);
#endif  
}

void Bu::SocketTcp::setSize( Bu::size )
{
}

void Bu::SocketTcp::flush()
{
}

bool Bu::SocketTcp::isOpen()
{
    return bActive;
}

void Bu::SocketTcp::setAddress()
{
    struct sockaddr_in addr;
    socklen_t len = sizeof(addr);
    addr.sin_family = AF_INET;
    bu_getpeername( nSocketTcp, (sockaddr *)(&addr), &len );
    sAddress = bu_inet_ntoa( addr.sin_addr );
}

Bu::String Bu::SocketTcp::getAddress() const
{
    return sAddress;
}

Bu::SocketTcp::operator Bu::SocketTcp::handle() const
{
    return nSocketTcp;
}

Bu::SocketTcp::handle Bu::SocketTcp::getHandle() const
{
    return nSocketTcp;
}

Bu::SocketTcp::handle Bu::SocketTcp::takeHandle()
{
    handle nRet = nSocketTcp;
    bActive = false;
    nSocketTcp = 0;
    return nRet;
}

Bu::size Bu::SocketTcp::getSize() const
{
    throw UnsupportedException();
}

Bu::size Bu::SocketTcp::getBlockSize() const
{
    return 1500; //TODO: Fix this, it's stupid.
}

Bu::String Bu::SocketTcp::getLocation() const
{
    return getAddress();
}

bool Bu::SocketTcp::getFd( int &rFdOut ) const
{
    rFdOut = nSocketTcp;
    return true;
}

