#include "common.h"

#include <event2/event.h>
#include <event2/listener.h>

#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
#include <event2/bufferevent.h>
#include <event2/buffer_compat.h>
#include <event2/bufferevent_struct.h>
#endif


#include "seafile-session.h"
#include "utils.h"
#include "net.h"
#include "listen-mgr.h"
#include "block-tx-server.h"

#define DEBUG_FLAG SEAFILE_DEBUG_OTHER
#include "log.h"


#define DEFAULT_SERVER_PORT 12001

#define TOKEN_LEN                   37   /* a uuid */
#define CHECK_EXPIRE_INTERVAL       1
#define READ_TOKEN_TIMEOUT          180 /* bufferevent read timeout */

struct _SeafListenManagerPriv {
    GHashTable             *token_hash;
    struct evconnlistener  *listener;
    CcnetTimer             *check_timer;
};

typedef struct {
    int ttl;
    ConnAcceptedCB func;
    void  *user_data;
} CallBackStruct;

static void accept_connection (struct evconnlistener *listener,
                               evutil_socket_t connfd,
                               struct sockaddr *saddr,
                               int socklen,
                               void *vmanager);

static int token_expire_pulse (void * vmanager);
static void read_cb (struct bufferevent *bufev, void *user_data);
static void error_cb (struct bufferevent *bufev, short what, void *user_data);

static int
get_listen_port (SeafileSession *session)
{
    char *port_str;
    int port = 0;

    port_str = g_key_file_get_string (session->config, "network", "port", NULL);
    if (port_str) {
        port = atoi(port_str);

        if (port <= 0 || port > 65535)
            port = DEFAULT_SERVER_PORT;

        g_free(port_str);
    }

    return port;
}


SeafListenManager *
seaf_listen_manager_new (SeafileSession *session)
{
    SeafListenManager *mgr;
    mgr = g_new0 (SeafListenManager, 1);
    mgr->port = get_listen_port(session);

    mgr->priv = g_new0 (SeafListenManagerPriv, 1);
    mgr->priv->token_hash = g_hash_table_new_full (
        g_str_hash, g_str_equal, g_free, g_free);

    return mgr;
}

int
seaf_listen_manager_start (SeafListenManager *mgr)
{
    evutil_socket_t listenfd;
    unsigned flags;
    SeafListenManagerPriv *priv = mgr->priv;

    if (mgr->port == 0) {
        return 0;
    }

    listenfd = ccnet_net_bind_tcp (mgr->port, 1);
    if (listenfd < 0) {
        seaf_warning ("[listen mgr] failed to bind port %d\n", mgr->port);
        return -1;
    }

    flags = LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_EXEC;

    /* start to listen on block transfer port */
    priv->listener = evconnlistener_new (NULL,     /* base */
                                accept_connection, mgr, /* cb & arg */
                                flags,                  /* flags */
                                -1,                     /* backlog */
                                listenfd);              /* socket */

    if (!priv->listener) {
        seaf_warning ("[listen mgr] failed to start evlistener\n");
        evutil_closesocket (listenfd);
        return -1;
    }

    priv->check_timer = ccnet_timer_new (token_expire_pulse, mgr,
                                         CHECK_EXPIRE_INTERVAL * 1000);

    seaf_message ("listen on port %d for block tranfer\n", mgr->port);
    return 0;
}

static void
accept_connection (struct evconnlistener *listener,
                   evutil_socket_t connfd,
                   struct sockaddr *saddr, int socklen,
                   void *vmanager)
{
    struct bufferevent   *bufev;
    struct timeval tv;
    tv.tv_sec = READ_TOKEN_TIMEOUT;
    tv.tv_usec = 0;

    bufev = bufferevent_socket_new (NULL, connfd, 0);
    bufferevent_setcb (bufev, read_cb, NULL, error_cb, vmanager);
    bufferevent_setwatermark (bufev, EV_READ, TOKEN_LEN, TOKEN_LEN);
    bufferevent_set_timeouts (bufev, &tv, NULL);

    bufferevent_enable (bufev, EV_READ);
    /* no write is needed here*/
    bufferevent_disable (bufev, EV_WRITE);
}

static void
read_cb (struct bufferevent *bufev, void *user_data)
{
    char *token;
    CallBackStruct *cbstruct;
    SeafListenManager *mgr = user_data;
    size_t len = EVBUFFER_LENGTH(bufev->input);
    evutil_socket_t connfd = bufferevent_getfd(bufev);

    /* we set the high & low watermark to TOKEN_LEN, so the received data can
     * only be this length. */
    if (len != TOKEN_LEN) {
        seaf_warning ("[listen mgr] token with incorrect length received: %d\n",
                      (int)len);
        goto error;
    }

    token = (char *)(EVBUFFER_DATA (bufev->input));

    /* Switch to new block protocol */
    if (strcmp (token, BLOCK_PROTOCOL_SIGNATURE) == 0) {
        if (ccnet_net_make_socket_blocking (connfd) < 0) {
            seaf_warning ("[listen mgr] Failed to set socket blocking.\n");
            goto error;
        }        
        if (block_tx_server_start (connfd) < 0) {
            seaf_warning ("Failed to start block tx server.\n");
            goto error;
        }
        bufferevent_free (bufev);
        return;
    }

    cbstruct = g_hash_table_lookup (mgr->priv->token_hash, token);
    if (!cbstruct) {
        seaf_warning ("[listen mgr] unknown token received: %s\n", token);
        goto error;
    }

    /* The connfd should be non-blocking for adding to bufferevent.
     * But now we want it to be blocking again.
     */
    if (ccnet_net_make_socket_blocking (connfd) < 0) {
        seaf_warning ("[listen mgr] Failed to set socket blocking.\n");
        goto error;
    }

    /* client is now connected, execute the callback function  */
    cbstruct->func (connfd, cbstruct->user_data);

    g_hash_table_remove (mgr->priv->token_hash, token);
    bufferevent_free (bufev);
    return;

error:
    evutil_closesocket(connfd);
    bufferevent_free (bufev);
}

static void
error_cb (struct bufferevent *bufev, short what, void *user_data)
{
    if (what & BEV_EVENT_TIMEOUT)
        seaf_warning ("[listen mgr] client timeout\n");
    else
        seaf_warning ("[listen mgr] error when reading token\n");

    /* We don't specify BEV_OPT_CLOSE_ON_FREE, so we need to close the socket
     * manually. */
    evutil_closesocket(bufferevent_getfd(bufev));
    bufferevent_free (bufev);
}


int
seaf_listen_manager_register_token (SeafListenManager *mgr,
                                    const char *token,
                                    ConnAcceptedCB cb,
                                    void *cb_arg,
                                    int timeout_sec)
{
    CallBackStruct *cbstruct;
    if (!token)
        return -1;

    if (timeout_sec <= 0)
        return -1;

    cbstruct = g_new0(CallBackStruct, 1);
    cbstruct->func = cb;
    cbstruct->user_data = cb_arg;
    cbstruct->ttl = timeout_sec;

    g_hash_table_insert (mgr->priv->token_hash, g_strdup(token), cbstruct);
    return 0;
}

char *
seaf_listen_manager_generate_token (SeafListenManager *mgr)
{
    return gen_uuid();
}

static gboolean
is_token_expired (gpointer key, gpointer value, gpointer user_data)
{
    CallBackStruct *cbstruct = value;

    if (cbstruct->ttl == 0) {
        /* client doesn't connect before timeout, so token is expired */
        seaf_warning ("[listen mgr] token timeout\n");
        cbstruct->func (-1, cbstruct->user_data);
        return TRUE;
    }

    --cbstruct->ttl;

    return FALSE;
}

static int
token_expire_pulse (void * vmanager)
{
    SeafListenManager *mgr = vmanager;
    g_hash_table_foreach_remove (mgr->priv->token_hash,
                                 (GHRFunc)is_token_expired,
                                 NULL);

    return TRUE;
}

