/* $Header$
*/

#include "Nice/Chat/Core.h"

#include "Nice/Util/Sync.h"

#include <boost/numeric/conversion/cast.hpp>

#ifdef _WIN32
#include <windows.h>
#include <winsock2.h>
#else
extern "C"
{
#include <sys/resource.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/select.h>
#include <sys/poll.h>
#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/termios.h>
#include <sys/signal.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <errno.h>
#include <limits.h>
#include <netdb.h>
#include <sys/time.h>

#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <fnmatch.h>
#include <string.h>
}

#include <cstdarg>

#endif

#include <string>


#undef DEBUG

#undef LOG_NAME
#define LOG_NAME m_name

namespace Nice
{

Nice::Mutex s_mutex_gethostbyname;
struct hostent * gethostbyname_protected(const char * name)
{
    Nice::Guard guard(s_mutex_gethostbyname);
    return ::gethostbyname(name);
}

ChatCore::ChatCore () : m_fd ( -1 )
{
    T_LOG ( "Constructing" );
    setStdSettings ();
}

ChatCore::ChatCore ( const char *device ) : m_fd ( -1 )
{
    T_LOG ( "Constructing" );
    setStdSettings ();
    ChatCore::open ( device );
}

ChatCore::ChatCore ( int fd ) : m_fd ( -1 )
{
    T_LOG ( "Constructing" );
    setStdSettings ();
    ChatCore::open ( fd );
}

ChatCore::~ChatCore ()
{
    T_LOG ( "Destruct" );
    ChatCore::close ( );
}

void ChatCore::setStdSettings ()
{
    m_timeout = Nice::Time::seconds(NICE_CHAT_TIMEOUT_IN_SECONDS);
    m_persistent = true;
}

int ChatCore::open ( const char *_device )  
{
    ChatCore::close();

    T_LOG ( "try connecting to " << _device );
    m_device_name = _device;
    std::string con;
    std::string bits;
    std::string parity;
    int param1;
    int num_bits;


    std::string::size_type idx_type = m_device_name.find ( ':' );

    if ( idx_type == std::string::npos )
        E_THROW ( ::Nice::Chat::ConnectionFormatException, "Wrong connection name format!" );

    std::string type = m_device_name.substr ( 0, idx_type );
    T_LOG ( "Conntype=" << type );

    if ( type == "tcp" )
    {
        std::string::size_type idx_con =
            m_device_name.substr ( idx_type + 3 ).find ( ':' );

        con = m_device_name.substr ( idx_type + 3, idx_con );
        if ( idx_con == std::string::npos )
        {
            E_THROW(::Nice::Chat::ConnectionFormatException,
                    "No port specified for device: " << _device);
        }
        else
        {
            param1 = atoi ( m_device_name.substr ( idx_type + idx_con + 4 ).c_str () );
        }
        openTCP ( con, boost::numeric_cast<uint16_t>(param1) );

    }
    else if ( type == "tty" )
    {
        std::string::size_type idx_con =
            m_device_name.substr ( idx_type + 1 ).find ( ':' );

        con = m_device_name.substr ( idx_type + 1, idx_con );
        if ( idx_con == std::string::npos )
            param1 = 19200;
        else
            param1 = atoi ( m_device_name.substr ( idx_type + idx_con + 2 ).c_str () );
        T_LOG ( "Baud=" << param1 );

        /*****************************************************************/
        std::string::size_type idx_baud =
            m_device_name.substr ( idx_type + idx_con + 4 ).find ( ':' );

        std::string::size_type idx_bits =
            m_device_name.substr ( idx_type + idx_con + idx_baud + 5 ).find ( ':' );

        bits = m_device_name.substr ( idx_type + idx_con + idx_baud + 5, idx_bits );
        if ( idx_bits == std::string::npos )
            num_bits = 8;
        else
            num_bits = atoi ( m_device_name.substr ( idx_type + idx_con + idx_baud + 5 ).c_str () );
        T_LOG ( "Number of bits=" << num_bits );

        parity = m_device_name.substr ( idx_type + idx_con + idx_baud + idx_bits + 6 );

        T_LOG ( "Parity=" << parity );
        /******************************************************************/

        openTTY ( con, param1, num_bits, parity );
    }
    else if ( type == "file" )
    {

    }
    else
    {
        E_THROW ( ::Nice::Chat::ConnectionFormatException, "Wrong connection name format!" );
    }
    return m_fd;
}

int ChatCore::openTCP ( std::string & _host, uint16_t _port )
{
#ifndef _WIN32
    int rc, opt;
    socklen_t optlen;
    struct sockaddr_in localAddr, servAddr;     // should contain host address
    struct hostent *h;
    struct timeval tv = { 2, 0 };
    fd_set wset;

    D_LOG ( "Network=" << _host << ":" << _port );

//    h =::gethostbyname ( _host.c_str () );
    h = Nice::gethostbyname_protected(_host.c_str());
    if ( h == NULL )
    {
        E_THROW ( ::Nice::ChatException, "Host '" <<  _host << "' not found" );
    }

    servAddr.sin_family = (sa_family_t) h->h_addrtype;
//    void * address_handle = h->h_addr_list[0];
//    I_LOG("s_addr address : " << &servAddr.sin_addr.s_addr << "; h_addr_list[0] : " << address_handle << "; h_length : " << h->h_length);
    memcpy ( ( char * ) &servAddr.sin_addr.s_addr, h->h_addr_list[0], h->h_length );
    servAddr.sin_port = htons ( _port );

    /* create socket */
    m_fd = socket ( AF_INET, SOCK_STREAM, 0 );   // create TCP socket
    if ( m_fd < 0 ){
        E_THROW ( ::Nice::ChatException, createMessageForErrno( errno ) ); }

    opt = 1;

    assert(0 <= m_fd);
    if ( setsockopt
         ( m_fd, IPPROTO_TCP, TCP_NODELAY, ( char * ) &opt,
           ( socklen_t ) sizeof ( opt ) ) )
    {
        const int savedErrno = errno;
        ChatCore::close();
        E_THROW ( ::Nice::ChatException, createMessageForErrno ( savedErrno ) );
    }

    /* bind any port number */
    localAddr.sin_family = AF_INET;
    localAddr.sin_addr.s_addr = htonl ( INADDR_ANY );
    localAddr.sin_port = htons ( 0 );

    rc = bind ( m_fd, ( struct sockaddr * ) &localAddr, sizeof ( localAddr ) );
    if ( rc < 0 )
    {
        const int savedErrno = errno;
        ChatCore::close();
        E_THROW ( ::Nice::ChatException, createMessageForErrno ( savedErrno ) );
    }

    if (-1 == fcntl ( m_fd, F_SETFL, fcntl ( m_fd, F_GETFL ) | O_NONBLOCK ))
    {
        const int savedErrno = errno;
        ChatCore::close();
        E_THROW ( ::Nice::ChatException, createMessageForErrno ( savedErrno ) );
    }

    opt = 0;
    optlen = sizeof ( optlen );

    /* connect to server */
    T_LOG ( "Try connecting " << _host << ":" << _port << "..." );
    rc =::connect ( m_fd, ( struct sockaddr * ) &servAddr, sizeof ( servAddr ) );

    if ( rc < 0 )
    {
        if ( errno == EINPROGRESS )
        {
            FD_ZERO ( &wset );
            FD_SET ( m_fd, &wset );

            if (-1 == fcntl ( m_fd, F_SETFL, fcntl ( m_fd, F_GETFL ) & ~O_NONBLOCK ))
            {
                ChatCore::close();
                E_THROW( ::Nice::Chat::BlockingIOException,
                        "Failed to set blocking I/O for " << m_device_name)
            }

            rc =::select ( m_fd + 1, NULL, &wset, NULL, &tv );
            if ( rc <= 0 )
            {
		        ChatCore::close();
                E_THROW ( ::Nice::Chat::TimeOutException,
                        "Connection timeout for " << m_device_name);
            }
            ::getsockopt ( m_fd, SOL_SOCKET, SO_ERROR, &opt, &optlen );
            if ( opt != 0 )
            {
		        ChatCore::close();
                E_THROW ( ::Nice::Chat::ConnectionRefusedException,
                        "Connection refused for "  << m_device_name);
            }
        }
        else
        {
            const int savedErrno = errno;
            ChatCore::close();
            E_THROW ( ::Nice::ChatException,
                    std::string("Failed to connect to ") << m_device_name <<
                    ": " << createMessageForErrno ( savedErrno ) );
        }
    }

    if (-1 == fcntl ( m_fd, F_SETFL, fcntl ( m_fd, F_GETFL ) & ~O_NONBLOCK ))
    {
        ChatCore::close();
        E_THROW ( ::Nice::ChatException, "Failed to unset socket to non-blocking" );
    }

#endif
    return m_fd;
}

int ChatCore::openTTY ( std::string & _device,
                        int baud,
                        int numbits,
                        std::string parity )  
{
#ifndef _WIN32
    int ttyspeed;
    int configurationBits;
    struct termios options;

    T_LOG ( "Serial=" << _device << ":" << baud << ":" << numbits << ":" << parity );

    m_fd =::open ( _device.c_str (), O_RDWR | O_NOCTTY | O_NDELAY );
    if ( m_fd < 0 )
        E_THROW ( ::Nice::ChatException, createMessageForErrno ( errno ) );

    if (-1 == fcntl ( m_fd, F_SETFL, 0 ))
    {
        const int savedErrno = errno;
        ChatCore::close();
        E_THROW ( ::Nice::ChatException, createMessageForErrno ( savedErrno ) );
    }

    if (-1 == tcgetattr ( m_fd, &options ))
    {
        const int savedErrno = errno;
        ChatCore::close();
        E_THROW ( ::Nice::ChatException, createMessageForErrno ( savedErrno ) );
    }

    switch ( baud )
    {
    case 300:
        ttyspeed = B300;
        break;
    case 600:
        ttyspeed = B600;
        break;
    case 1200:
        ttyspeed = B1200;
        break;
    case 2400:
        ttyspeed = B2400;
        break;
    case 4800:
        ttyspeed = B4800;
        break;
    case 9600:
        ttyspeed = B9600;
        break;
    case 19200:
        ttyspeed = B19200;
        break;
    case 38400:
        ttyspeed = B38400;
        break;
    case 57600:
        ttyspeed = B57600;
        break;
    case 115200:
        ttyspeed = B115200;
        break;
    case 230400:
        ttyspeed = B230400;
        break;
//    case 460800:
//      ttyspeed = B460800;
//      break;
    default:
        ChatCore::close();
        E_THROW ( ::Nice::ChatException, "wrong tty speed" );
        break;
    }

    configurationBits = ( CLOCAL | CREAD );

    switch ( numbits )
    {
    case 7:
        configurationBits |= CS7;
        break;
    case 8:
        configurationBits |= CS8;
        break;
    default:
        ChatCore::close();
        E_THROW ( ::Nice::ChatException, "wrong number of bits (7 | 8)" );
        break;
    }

    if ( parity == "odd" )
        configurationBits |= ( PARENB| PARODD );
    else if ( parity == "even" )
        configurationBits |= PARENB;
    else if ( parity == "no" )
        configurationBits &= ~ ( PARENB|PARODD );
    else
    {
        ChatCore::close();
        E_THROW ( ::Nice::ChatException, "wrong parity (odd | even | no)" );
    }

    if (-1 == cfsetispeed ( &options, ttyspeed ))
    {
        const int savedErrno = errno;
        ChatCore::close();
        E_THROW ( ::Nice::ChatException, createMessageForErrno ( savedErrno ) );
    }

    if (-1 == cfsetospeed ( &options, ttyspeed ))
    {
        const int savedErrno = errno;
        ChatCore::close();
        E_THROW ( ::Nice::ChatException, createMessageForErrno ( savedErrno ) );
    }

    options.c_cflag |= configurationBits;
    options.c_lflag &= ~ ( ICANON | ECHO | ECHOE | ISIG );
    options.c_cflag &= ~CRTSCTS;
    options.c_iflag &= ~ ( IXON | IXOFF | IXANY ); //  Disable Software Flow Control
    options.c_cc[VMIN] = 1;        /* min. of received characters  */
    options.c_cc[VTIME] = 0;        /* timeout in time/10 sec. */
    if (-1 == tcsetattr ( m_fd, TCSANOW, &options ))
    {
        const int savedErrno = errno;
        ChatCore::close();
        E_THROW ( ::Nice::ChatException, createMessageForErrno ( savedErrno ) );
    }
#else
#error "TODO 2011-03-04 Please implement me!"
#endif
    return m_fd;
}

int ChatCore::open ()  
{
    if ( m_device_name.length () )
        open ( m_device_name.c_str () );
    else
        E_THROW ( ::Nice::ChatException, "No device" );
    return m_fd;
}

int ChatCore::open ( int _fd )  
{
    m_fd = _fd;
#ifndef _WIN32
    fcntl ( m_fd, F_SETFL, fcntl ( m_fd, F_GETFL ) & ~O_NONBLOCK );
#endif
    return m_fd;
}

int ChatCore::close ()
{
    if(m_fd > 0) ::close ( m_fd );
    m_fd = -1;
    return m_fd;
}

int ChatCore::openListenSocket ( int _port )  
{
#ifndef _WIN32
    int listenfd, on = 1;
    struct sockaddr_in servaddr;

    listenfd = socket ( AF_INET, SOCK_STREAM, 0 );
    memset ( &servaddr, 0, sizeof ( servaddr ) );
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl ( INADDR_ANY );
    servaddr.sin_port = htons ( boost::numeric_cast<short>(_port) );
    struct linger linger;

    /* Set socket reusable */
    if ( setsockopt
         ( listenfd, SOL_SOCKET, SO_REUSEADDR, ( const char * ) &on,
           sizeof ( on ) ) == -1 )
        E_THROW ( ::Nice::ChatException, strerror ( errno ) );

    linger.l_onoff = linger.l_linger = 0;

    if ( ::
         setsockopt ( listenfd, SOL_SOCKET, SO_LINGER, ( char * ) &linger,
                      ( socklen_t ) sizeof ( linger ) ) )
        E_THROW ( ::Nice::ChatException, strerror ( errno ) );

    if ( ( bind ( listenfd, ( struct sockaddr * ) &servaddr, sizeof ( servaddr ) ) ) ==
         -1 )
        E_THROW ( ::Nice::ChatException, strerror ( errno ) );

    listen ( listenfd, 1 );

    /* Set socket blocking. */
    fcntl ( listenfd, F_SETFL, fcntl ( listenfd, F_GETFL ) & ~O_NONBLOCK );

    /* Ignore this to receive EPIPE. */
    signal ( SIGPIPE, SIG_IGN );

    m_fd = listenfd;
#endif
    return m_fd;

}

int ChatCore::tryAccept ( ChatCore & _newConnection )
{
    fd_set rset;
    struct timeval timeout;
    int connfd;

    FD_ZERO ( &rset );
    FD_SET ( m_fd, &rset );

    if ( time < NULL )
    {
        select ( m_fd + 1, &rset, NULL, NULL, NULL );
    }
    else
    {
        timeout.tv_sec = ( int ) m_timeout;
        timeout.tv_usec = ( int ) ( m_timeout * 1000000 ) % 1000000;
        select ( m_fd + 1, &rset, NULL, NULL, &timeout );
    }

    if ( FD_ISSET ( m_fd, &rset ) )
    {
        FD_CLR ( m_fd, &rset );
        connfd =::accept ( m_fd, NULL, NULL );
        setStdSettings ();
        _newConnection.open ( connfd );
        return m_fd;
    }
    return 0;
}

int ChatCore::wait ( Nice::Time _time )
{
    int ret = 0;
    fd_set rset;
    struct timeval timeout;

    FD_ZERO ( &rset );
    FD_SET ( m_fd, &rset );
    if ( _time.toMilliSecondsDouble() < 0 )
    {
        ret =::select ( m_fd + 1, &rset, NULL, NULL, NULL );
    }
    else
    {
        timeout.tv_sec =  _time.second();
        timeout.tv_usec = _time.microSecond();

        ret =::select ( m_fd + 1, &rset, NULL, NULL, &timeout );
    }

    if (ret < 0)
    {
        E_THROW ( ::Nice::ChatException, "Unexpected select return." );
    }
    return ret;
}

std::string ChatCore::createMessageForErrno(int errorNumber)
{
    char errorMessage[255]; // XXX(kulas) 2011-03-30:  Is the size right?
    ::memset(&errorMessage[0], 0, sizeof(errorMessage));
    ::strerror_r(errorNumber, &errorMessage[0], sizeof(errorMessage));

    return std::string(errorMessage);
}

};
