/*
Copyright (c) 2009 by Juliusz Chroboczek

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

/* ansi */
#include <errno.h>
#include <stdio.h>

/* posix */
#include <netinet/in.h> /* sockaddr_in */
#include <signal.h> /* sig_atomic_t */
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h> /* socket(), bind() */
#include <unistd.h> /* close() */

/* third party */
#include <event.h>
#include <dht/dht.h>

/* libT */
#include "transmission.h"
#include "bencode.h"
#include "crypto.h"
#include "net.h"
#include "peer-mgr.h" /* tr_peerMgrCompactToPex() */
#include "platform.h" /* tr_threadNew() */
#include "session.h"
#include "torrent.h" /* tr_torrentFindFromHash() */
#include "tr-dht.h"
#include "trevent.h" /* tr_runInEventThread() */
#include "utils.h"
#include "version.h"

#ifdef WITHOUT_DHT

  /* These are the stubs for when we're building without DHT support */
  int tr_dhtInit( tr_session * session UNUSED,
                  tr_address * address UNUSED ) { return TR_DHT_STOPPED; }
  void tr_dhtUninit( tr_session * session UNUSED ) { }
  tr_bool tr_dhtEnabled( const tr_session * session UNUSED ) { return FALSE; }
  tr_port tr_dhtPort ( const tr_session * sesssion UNUSED ) { return 0; }
  int tr_dhtStatus( tr_session * session     UNUSED,
                    int        * setmeCount  UNUSED ) { return TR_DHT_STOPPED; }
  int tr_dhtAddNode( tr_session * session    UNUSED,
                     tr_address * addr       UNUSED,
                     tr_port      port       UNUSED,
                     tr_bool      bootstrap  UNUSED ) { return 0; }
  int tr_dhtAnnounce( tr_torrent * session UNUSED,
                      tr_bool announce UNUSED ) { return -1; }


#else

static int dht_socket;
static struct event dht_event;
static tr_port dht_port;
static unsigned char myid[20];
static tr_session *session = NULL;

static void event_callback(int s, short type, void *ignore);

struct bootstrap_closure {
    tr_session *session;
    uint8_t *nodes;
    size_t len;
};

static void
dht_bootstrap(void *closure)
{
    struct bootstrap_closure *cl = closure;
    size_t i;

    if(session != cl->session)
        return;

    for(i = 0; i < cl->len; i += 6)
    {
        struct timeval tv;
        tr_port port;
        struct tr_address addr;
        int status;

        memset(&addr, 0, sizeof(addr));
        addr.type = TR_AF_INET;
        memcpy(&addr.addr.addr4, &cl->nodes[i], 4);
        memcpy(&port, &cl->nodes[i + 4], 2);
        port = ntohs(port);
        /* There's no race here -- if we uninit between the test and the
           AddNode, the AddNode will be ignored. */
        status = tr_dhtStatus(cl->session, NULL);
        if(status == TR_DHT_STOPPED || status >= TR_DHT_FIREWALLED)
            break;
        tr_dhtAddNode(cl->session, &addr, port, 1);
        tr_timevalSet( &tv, 2 + tr_cryptoWeakRandInt( 5 ), tr_cryptoWeakRandInt( 1000000 ) );
        select( 0, NULL, NULL, NULL, &tv );
    }
    tr_free( cl->nodes );
    tr_free( closure );
    tr_ndbg( "DHT", "Finished bootstrapping" );
}

int
tr_dhtInit(tr_session *ss, tr_address * tr_addr)
{
    struct sockaddr_in sin;
    tr_benc benc;
    int rc;
    tr_bool have_id = FALSE;
    char * dat_file;
    uint8_t * nodes = NULL;
    const uint8_t * raw;
    size_t len;
    char v[5];

    if( session ) /* already initialized */
        return -1;

    dht_port = tr_sessionGetPeerPort(ss);
    if(dht_port <= 0)
        return -1;

    tr_ndbg( "DHT", "Initialising DHT" );

    dht_socket = socket(PF_INET, SOCK_DGRAM, 0);
    if(dht_socket < 0)
        goto fail;

    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    memcpy(&(sin.sin_addr), &(tr_addr->addr.addr4), sizeof (struct in_addr));
    sin.sin_port = htons(dht_port);
    rc = bind(dht_socket, (struct sockaddr*)&sin, sizeof(sin));
    if(rc < 0)
        goto fail;

    if( getenv( "TR_DHT_VERBOSE" ) != NULL )
        dht_debug = stderr;

    dat_file = tr_buildPath( ss->configDir, "dht.dat", NULL );
    rc = tr_bencLoadFile( &benc, TR_FMT_BENC, dat_file );
    tr_free( dat_file );
    if(rc == 0) {
        if(( have_id = tr_bencDictFindRaw( &benc, "id", &raw, &len ) && len==20 ))
            memcpy( myid, raw, len );
        if( tr_bencDictFindRaw( &benc, "nodes", &raw, &len ) && !(len%6) ) {
            nodes = tr_memdup( raw, len );
            tr_ninf( "DHT", "Bootstrapping from %d old nodes", (int)(len/6) );
        }
        tr_bencFree( &benc );
    }

    if( have_id )
        tr_ninf( "DHT", "Reusing old id" );
    else {
        /* Note that DHT ids need to be distributed uniformly,
         * so it should be something truly random. */
        tr_ninf( "DHT", "Generating new id" );
        tr_cryptoRandBuf( myid, 20 );
    }

    v[0] = 'T';
    v[1] = 'R';
    v[2] = (SVN_REVISION_NUM >> 8) & 0xFF;
    v[3] = SVN_REVISION_NUM & 0xFF;
    rc = dht_init( dht_socket, myid, (const unsigned char*)v );
    if(rc < 0)
        goto fail;

    session = ss;

    if(nodes) {
        struct bootstrap_closure * cl = tr_new( struct bootstrap_closure, 1 );
        cl->session = session;
        cl->nodes = nodes;
        cl->len = len;
        tr_threadNew( dht_bootstrap, cl );
    }

    event_set( &dht_event, dht_socket, EV_READ, event_callback, NULL );
    tr_timerAdd( &dht_event, 0, tr_cryptoWeakRandInt( 1000000 ) );

    tr_ndbg( "DHT", "DHT initialised" );

    return 1;

    fail:
    {
        const int save = errno;
        close(dht_socket);
        dht_socket = -1;
        session = NULL;
        tr_ndbg( "DHT", "DHT initialisation failed (errno = %d)", save );
        errno = save;
    }

    return -1;
}

void
tr_dhtUninit(tr_session *ss)
{
    if(session != ss)
        return;

    tr_ndbg( "DHT", "Uninitialising DHT" );

    event_del(&dht_event);

    /* Since we only save known good nodes, avoid erasing older data if we
       don't know enough nodes. */
    if(tr_dhtStatus(ss, NULL) < TR_DHT_FIREWALLED)
        tr_ninf( "DHT", "Not saving nodes, DHT not ready" );
    else {
        tr_benc benc;
        struct sockaddr_in sins[300];
        char compact[300 * 6];
        char *dat_file;
        int i;
        int n = dht_get_nodes(sins, 300);
        int j = 0;

        tr_ninf( "DHT", "Saving %d nodes", n );
        for( i=0; i<n; ++i ) {
            memcpy( compact + j, &sins[i].sin_addr, 4 );
            memcpy( compact + j + 4, &sins[i].sin_port, 2 );
            j += 6;
        }
        tr_bencInitDict( &benc, 2 );
        tr_bencDictAddRaw( &benc, "id", myid, 20 );
        tr_bencDictAddRaw( &benc, "nodes", compact, j );
        dat_file = tr_buildPath( ss->configDir, "dht.dat", NULL );
        tr_bencToFile( &benc, TR_FMT_BENC, dat_file );
        tr_bencFree( &benc );
        tr_free( dat_file );
    }

    dht_uninit( dht_socket, 0 );
    EVUTIL_CLOSESOCKET( dht_socket );

    tr_ndbg("DHT", "Done uninitialising DHT");

    session = NULL;
}

tr_bool
tr_dhtEnabled( const tr_session * ss )
{
    return ss && ( ss == session );
}

struct getstatus_closure
{
    sig_atomic_t status;
    sig_atomic_t count;
};

static void
getstatus( void * closure )
{
    struct getstatus_closure * ret = closure;
    int good, dubious, incoming;

    dht_nodes( &good, &dubious, NULL, &incoming );

    ret->count = good + dubious;

    if( good < 4 || good + dubious <= 8 )
        ret->status = TR_DHT_BROKEN;
    else if( good < 40 )
        ret->status = TR_DHT_POOR;
    else if( incoming < 8 )
        ret->status = TR_DHT_FIREWALLED;
    else
        ret->status = TR_DHT_GOOD;
}

int
tr_dhtStatus( tr_session * ss, int * nodes_return )
{
    struct getstatus_closure ret = { -1, - 1 };

    if( !tr_dhtEnabled( ss ) )
        return TR_DHT_STOPPED;

    tr_runInEventThread( ss, getstatus, &ret );
    while( ret.status < 0 )
        tr_wait( 10 /*msec*/ );

    if( nodes_return )
        *nodes_return = ret.count;

    return ret.status;
}

tr_port
tr_dhtPort( const tr_session *ss )
{
    return tr_dhtEnabled( ss ) ? dht_port : 0;
}

int
tr_dhtAddNode(tr_session *ss, tr_address *address, tr_port port,
              tr_bool bootstrap)
{
    struct sockaddr_in sin;

    if( !tr_dhtEnabled( ss ) )
        return 0;

    if( address->type != TR_AF_INET )
        return 0;

    /* Since we don't want to abuse our bootstrap nodes,
     * we don't ping them if the DHT is in a good state. */
    if(bootstrap) {
        if(tr_dhtStatus(ss, NULL) >= TR_DHT_FIREWALLED)
            return 0;
    }

    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    memcpy(&sin.sin_addr, &address->addr.addr4, 4);
    sin.sin_port = htons(port);
    dht_ping_node(dht_socket, &sin);

    return 1;
}

const char *
tr_dhtPrintableStatus(int status)
{
    switch(status) {
    case TR_DHT_STOPPED: return "stopped";
    case TR_DHT_BROKEN: return "broken";
    case TR_DHT_POOR: return "poor";
    case TR_DHT_FIREWALLED: return "firewalled";
    case TR_DHT_GOOD: return "good";
    default: return "???";
    }
}

static void
callback( void *ignore UNUSED, int event,
          unsigned char *info_hash, void *data, size_t data_len )
{
    if( event == DHT_EVENT_VALUES )
    {
        tr_torrent *tor;
        tr_globalLock( session );
        tor = tr_torrentFindFromHash( session, info_hash );
        if( tor && tr_torrentAllowsDHT( tor ))
        {
            size_t i, n;
            tr_pex * pex = tr_peerMgrCompactToPex(data, data_len, NULL, 0, &n);
            for( i=0; i<n; ++i )
                tr_peerMgrAddPex( tor, TR_PEER_FROM_DHT, pex+i );
            tr_free(pex);
            tr_torinf(tor, "Learned %d peers from DHT", (int)n);
        }
        tr_globalUnlock( session );
    }
    else if( event == DHT_EVENT_SEARCH_DONE )
    {
        tr_torrent * tor = tr_torrentFindFromHash( session, info_hash );
        if( tor ) {
            tr_torinf(tor, "DHT announce done");
            tor->dhtAnnounceInProgress = 0;
        }
    }
}

int
tr_dhtAnnounce(tr_torrent *tor, tr_bool announce)
{
    int rc, status, numnodes;

    if( !tr_torrentAllowsDHT( tor ) )
        return -1;

    status = tr_dhtStatus( tor->session, &numnodes );
    if(status < TR_DHT_POOR ) {
        tr_tordbg(tor, "DHT not ready (%s, %d nodes)",
                  tr_dhtPrintableStatus(status), numnodes);
        return 0;
    }

    rc = dht_search( dht_socket, tor->info.hash,
                     announce ? tr_sessionGetPeerPort(session) : 0,
                     callback, NULL);

    if( rc >= 1 ) {
        tr_torinf(tor, "Starting DHT announce (%s, %d nodes)",
                  tr_dhtPrintableStatus(status), numnodes);
        tor->dhtAnnounceInProgress = TRUE;
    } else {
        tr_torerr(tor, "DHT announce failed, errno = %d (%s, %d nodes)",
                  errno, tr_dhtPrintableStatus(status), numnodes);
    }

    return 1;
}

static void
event_callback(int s, short type, void *ignore UNUSED )
{
    time_t tosleep;

    if( dht_periodic(s, type == EV_READ, &tosleep, callback, NULL) < 0 ) {
        if(errno == EINTR) {
            tosleep = 0;
        } else {
            tr_nerr("DHT", "dht_periodic failed (errno = %d)", errno);
            if(errno == EINVAL || errno == EFAULT)
                    abort();
            tosleep = 1;
        }
    }

    /* Being slightly late is fine,
       and has the added benefit of adding some jitter. */
    tr_timerAdd( &dht_event, tosleep, tr_cryptoWeakRandInt( 1000000 ) );
}

void
dht_hash(void *hash_return, int hash_size,
         const void *v1, int len1,
         const void *v2, int len2,
         const void *v3, int len3)
{
    unsigned char sha1[SHA_DIGEST_LENGTH];
    tr_sha1( sha1, v1, len1, v2, len2, v3, len3, NULL );
    memset( hash_return, 0, hash_size );
    memcpy( hash_return, sha1, MIN( hash_size, SHA_DIGEST_LENGTH ) );
}

int
dht_random_bytes( void * buf, size_t size )
{
    tr_cryptoRandBuf( buf, size );
    return size;
}

#endif
