/**
 * Copyright - See the COPYRIGHT that is included with this distribution.
 * pvAccessCPP is distributed subject to a Software License Agreement found
 * in file LICENSE that is included with this distribution.
 */

#include <pv/blockingTCP.h>
#include "codec.h"
#include <pv/remote.h>
#include <pv/logger.h>

#include <pv/epicsException.h>

#include <osiSock.h>
#include <epicsThread.h>

#include <sstream>

using std::ostringstream;
using namespace epics::pvData;

namespace epics {
namespace pvAccess {

        BlockingTCPAcceptor::BlockingTCPAcceptor(
                Context::shared_pointer const & context,
                ResponseHandlerFactory::shared_pointer const & responseHandlerFactory,
                int port,
                int receiveBufferSize) :
            _context(context),
            _responseHandlerFactory(responseHandlerFactory),
            _bindAddress(),
            _serverSocketChannel(INVALID_SOCKET),
            _receiveBufferSize(receiveBufferSize),
            _destroyed(false),
            _threadId(0)
        {
            initialize(port);
        }

        BlockingTCPAcceptor::~BlockingTCPAcceptor() {
            destroy();
        }

        int BlockingTCPAcceptor::initialize(unsigned short port) {
            // specified bind address
            _bindAddress.ia.sin_family = AF_INET;
            _bindAddress.ia.sin_port = htons(port);
            _bindAddress.ia.sin_addr.s_addr = htonl(INADDR_ANY);

            char strBuffer[64];
            char ipAddrStr[48];
            ipAddrToDottedIP(&_bindAddress.ia, ipAddrStr, sizeof(ipAddrStr));

            int tryCount = 0;
            while(tryCount<2) {

                LOG(logLevelDebug, "Creating acceptor to %s.", ipAddrStr);

                _serverSocketChannel = epicsSocketCreate(AF_INET, SOCK_STREAM, IPPROTO_TCP);
                if(_serverSocketChannel==INVALID_SOCKET) {
                    epicsSocketConvertErrnoToString(strBuffer, sizeof(strBuffer));
                    ostringstream temp;
                    temp<<"Socket create error: "<<strBuffer;
                    LOG(logLevelError, "%s", temp.str().c_str());
                    THROW_BASE_EXCEPTION(temp.str().c_str());
                }
                else {
                    
                    //epicsSocketEnableAddressReuseDuringTimeWaitState(_serverSocketChannel);

                    // try to bind
                    int retval = ::bind(_serverSocketChannel, &_bindAddress.sa, sizeof(sockaddr));
                    if(retval<0) {
                        epicsSocketConvertErrnoToString(strBuffer, sizeof(strBuffer));
                        LOG(logLevelDebug, "Socket bind error: %s.", strBuffer);
                        if(_bindAddress.ia.sin_port!=0) {
                            // failed to bind to specified bind address,
                            // try to get port dynamically, but only once
                            LOG(
                                    logLevelDebug,
                                    "Configured TCP port %d is unavailable, trying to assign it dynamically.",
                                    port);
                            _bindAddress.ia.sin_port = htons(0);
                        }
                        else {
                            epicsSocketDestroy(_serverSocketChannel);
                            break; // exit while loop
                        }
                    }
                    else { // if(retval<0)
                        // bind succeeded

                        // update bind address, if dynamically port selection was used
                        if(ntohs(_bindAddress.ia.sin_port)==0) {
                            osiSocklen_t sockLen = sizeof(sockaddr);
                            // read the actual socket info
                            retval = ::getsockname(_serverSocketChannel, &_bindAddress.sa, &sockLen);
                            if(retval<0) {
                                // error obtaining port number
                                epicsSocketConvertErrnoToString(strBuffer, sizeof(strBuffer));
                                LOG(logLevelDebug, "getsockname error: %s", strBuffer);
                            }
                            else {
                                LOG(
                                        logLevelInfo,
                                        "Using dynamically assigned TCP port %d.",
                                        ntohs(_bindAddress.ia.sin_port));
                            }
                        }

                        retval = ::listen(_serverSocketChannel, 1024);
                        if(retval<0) {
                            epicsSocketConvertErrnoToString(strBuffer, sizeof(strBuffer));
                            ostringstream temp;
                            temp<<"Socket listen error: "<<strBuffer;
                            LOG(logLevelError, "%s", temp.str().c_str());
                            THROW_BASE_EXCEPTION(temp.str().c_str());
                        }

                        _threadId
                                = epicsThreadCreate(
                                        "TCP-acceptor",
                                        epicsThreadPriorityMedium,
                                        epicsThreadGetStackSize(
                                                epicsThreadStackMedium),
                                        BlockingTCPAcceptor::handleEventsRunner,
                                        this);

                        // all OK, return
                        return ntohs(_bindAddress.ia.sin_port);
                    } // successful bind
                } // successfully obtained socket
                tryCount++;
            } // while

            ostringstream temp;
            temp<<"Failed to create acceptor to "<<ipAddrStr;
            THROW_BASE_EXCEPTION(temp.str().c_str());
        }

        void BlockingTCPAcceptor::handleEvents() {
            // rise level if port is assigned dynamically
            char ipAddrStr[48];
            ipAddrToDottedIP(&_bindAddress.ia, ipAddrStr, sizeof(ipAddrStr));
            LOG(logLevelDebug, "Accepting connections at %s.", ipAddrStr);

            bool socketOpen = true;
            char strBuffer[64];

            while(socketOpen) {
                
                {
                    Lock guard(_mutex);
                    if (_destroyed)
                        break;
                }
                
                osiSockAddr address;
                osiSocklen_t len = sizeof(sockaddr);

                SOCKET newClient = epicsSocketAccept(_serverSocketChannel, &address.sa, &len);
                if(newClient!=INVALID_SOCKET) {
                    // accept succeeded
                    ipAddrToDottedIP(&address.ia, ipAddrStr, sizeof(ipAddrStr));
                    LOG(logLevelDebug, "Accepted connection from PVA client: %s.", ipAddrStr);

                    // enable TCP_NODELAY (disable Nagle's algorithm)
                    int optval = 1; // true
                    int retval = ::setsockopt(newClient, IPPROTO_TCP, TCP_NODELAY, (char *)&optval, sizeof(int));
                    if(retval<0) {
                        epicsSocketConvertErrnoToString(strBuffer, sizeof(strBuffer));
                        LOG(logLevelDebug, "Error setting TCP_NODELAY: %s.", strBuffer);
                    }

                    // enable TCP_KEEPALIVE
                    retval = ::setsockopt(newClient, SOL_SOCKET, SO_KEEPALIVE, (char *)&optval, sizeof(int));
                    if(retval<0) {
                        epicsSocketConvertErrnoToString(strBuffer, sizeof(strBuffer));
                        LOG(logLevelDebug, "Error setting SO_KEEPALIVE: %s.", strBuffer);
                    }

                    // do NOT tune socket buffer sizes, this will disable auto-tunning
                      
                    // get TCP send buffer size
                    osiSocklen_t intLen = sizeof(int);
                    int _socketSendBufferSize;
                    retval = getsockopt(newClient, SOL_SOCKET, SO_SNDBUF, (char *)&_socketSendBufferSize, &intLen);
                    if(retval<0) {
                        epicsSocketConvertErrnoToString(strBuffer, sizeof(strBuffer));
                        LOG(logLevelDebug, "Error getting SO_SNDBUF: %s.", strBuffer);
                    }

                    /**
                     * Create transport, it registers itself to the registry.
                     */
                    std::auto_ptr<ResponseHandler> responseHandler = _responseHandlerFactory->createResponseHandler();
                    detail::BlockingServerTCPTransportCodec::shared_pointer transport =
                                    detail::BlockingServerTCPTransportCodec::create(
                                            _context,
                                            newClient,
                                            responseHandler,
                                            _socketSendBufferSize,
                                            _receiveBufferSize);

                    // validate connection
                    if(!validateConnection(transport, ipAddrStr)) {
                        // TODO
                        // wait for negative response to be sent back and
                        // hold off the client for retrying at very high rate
                        epicsThreadSleep(1.0);

                        transport->close();
                        LOG(
                                logLevelDebug,
                                "Connection to PVA client %s failed to be validated, closing it.",
                                ipAddrStr);
                        continue;
                    }

                    LOG(logLevelDebug, "Serving to PVA client: %s.", ipAddrStr);

                }// accept succeeded
                else
                    socketOpen = false;
            } // while
        }

        bool BlockingTCPAcceptor::validateConnection(Transport::shared_pointer const & transport, const char* address) {
            try {
                return transport->verify(5000);
            } catch(...) {
                LOG(logLevelDebug, "Validation of %s failed.", address);
                return false;
            }
        }

        void BlockingTCPAcceptor::handleEventsRunner(void* param) {
            ((BlockingTCPAcceptor*)param)->handleEvents();
        }

        void BlockingTCPAcceptor::destroy() {
            Lock guard(_mutex);
            if(_destroyed) return;
            _destroyed = true;

            if(_serverSocketChannel!=INVALID_SOCKET) {
                char ipAddrStr[48];
                ipAddrToDottedIP(&_bindAddress.ia, ipAddrStr, sizeof(ipAddrStr));
                LOG(logLevelDebug, "Stopped accepting connections at %s.", ipAddrStr);

                epicsSocketDestroy(_serverSocketChannel);
            }
        }

    }
}
