
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

#include "main.h"
#include "conf.h"
#include "net.h"
#include "log.h"
#include "utils.h"
#include "ext-fwd.h"
#ifdef FWD_NATPMP
#include "natpmp.h"
#endif
#ifdef FWD_UPNP
#include "upnp.h"
#endif


/*
* Frontend for NAT-PMP and UPnP wrappers.
*/

#ifdef FWD_NATPMP
static struct natpmp_handle_t *natpmp = NULL;
#endif

#ifdef FWD_UPNP
static struct upnp_handle_t *upnp = NULL;
#endif

static time_t g_fwd_retry = 0;
static struct forwarding_t *g_fwds = NULL;
static struct forwarding_t *g_fwd_cur = NULL;


struct forwarding_t *fwd_get(void)
{
    return g_fwds;
}

int fwd_count(void)
{
    struct forwarding_t *cur;
    size_t count;

    count = 0;
    cur = g_fwds;
    while (cur) {
        count++;
        cur = cur->next;
    }

    return count;
}

void fwd_debug(FILE *fp)
{
    struct forwarding_t *cur;
    time_t now;
    size_t counter;

    now = time_now_sec();
    counter = 0;
    cur = g_fwds;

    while (cur) {
        fprintf(fp, " port: %d\n", cur->port);

        if (cur->refreshed == 0) {
            fprintf(fp, "  refreshed: never\n");
        } else {
            fprintf(fp, "  refreshed: %d minutes ago\n", (int) (now - cur->refreshed) / 60);
        }

        if (cur->lifetime == LONG_MAX) {
            fprintf(fp, "  lifetime: infinite\n");
        } else {
            fprintf(fp, "  lifetime: %d minutes remaining\n", (int) (cur->lifetime -  now) / 60);
        }

        counter++;
        cur = cur->next;
    }

    fprintf(fp, " Found %zu forwardings.\n", counter);
}

void fwd_add(int port, time_t lifetime)
{
    struct forwarding_t *cur;
    struct forwarding_t *new;

    if (port <= 1 || port > 65535) {
        return;
    }

    cur = g_fwds;
    while (cur) {
        if (cur->port == port) {
            cur->lifetime = lifetime;
            return;
        }
        cur = cur->next;
    }

    new = (struct forwarding_t*) calloc(1, sizeof(struct forwarding_t));
    new->port = port;
    new->lifetime = lifetime;
    new->refreshed = 0;
    new->next = g_fwds;

    g_fwds = new;
    g_fwd_retry = 0; // Trigger quick handling
}

// Remove a port from the list - internal use only
void fwd_remove(struct forwarding_t *item)
{
    struct forwarding_t *pre;
    struct forwarding_t *cur;

    if (g_fwd_cur == item) {
        g_fwd_cur = NULL;
    }

    pre = NULL;
    cur = g_fwds;
    while (cur) {
        if (cur == item) {
            if (pre) {
                pre->next = cur->next;
            } else {
                g_fwds = cur->next;
            }
            free(cur);
            return;
        }
        pre = cur;
        cur = cur->next;
    }
}

/*
* Try to add a port forwarding to a router.
* We do not actually check if we are in a private network.
* This function is called in intervals.
*/
void fwd_handle(int _rc, int _sock)
{
    struct forwarding_t *item;
    int rc;
    time_t lifespan;
    time_t now;

    now = time_now_sec();
    item = g_fwd_cur;

    // Handle current forwarding entry or wait 60 seconds to select a new one to process
    if (item == NULL) {
        if (g_fwd_retry > now) {
            return;
        } else {
            item = g_fwds;
            g_fwd_retry = now + (1 * 60);
        }
    }

    while (item) {
        if ((item->refreshed + (30 * 60)) < now) {
            break;
        }
        item = item->next;
    }

    if (item == NULL) {
        g_fwd_cur = NULL;
        return;
    } else {
        g_fwd_cur = item;
    }

    if (item->lifetime < now) {
        lifespan = 0;
    } else {
        lifespan = (32 * 60);
    }

#ifdef FWD_NATPMP
    if (natpmp) {
        rc = natpmp_handler(natpmp, item->port, lifespan, now);

        if (rc == PF_DONE) {
            if (lifespan == 0) {
                log_debug("FWD: Remove NAT-PMP forwarding for port %hu.", item->port);
                fwd_remove(item);
            } else {
                log_debug("FWD: Add NAT-PMP forwarding for port %hu.", item->port);
                item->refreshed = now;
            }
            return;
        } else if (rc == PF_ERROR) {
            log_info("FWD: Disable NAT-PMP - not available.");
            natpmp_uninit(&natpmp);
        } else if (rc == PF_RETRY) {
            // return;
        } else {
            log_error("FWD: Unhandled NAT-PMP reply.");
        }
    }
#endif

#ifdef FWD_UPNP
    if (upnp) {
        rc = upnp_handler(upnp, item->port, lifespan, now);

        if (rc == PF_DONE) {
            if (lifespan == 0) {
                log_debug("FWD: Remove UPnP forwarding for port %hu.", item->port);
                fwd_remove(item);
            } else {
                log_debug("FWD: Add UPnP forwarding for port %hu.", item->port);
                item->refreshed = now;
            }
            return;
        } else if (rc == PF_ERROR) {
            log_info("FWD: Disable UPnP - not available.");
            upnp_uninit(&upnp);
        } else if (rc == PF_RETRY) {
            //return;
        } else {
            log_error("FWD: Unhandled UPnP reply.");
        }
    }
#endif
}

bool fwd_setup(void)
{
    if (gconf->fwd_disable) {
        return false;
    }

#ifdef FWD_NATPMP
    log_info("FWD: enable NAT-PMP");
    natpmp_init(&natpmp);
#endif
#ifdef FWD_UPNP
    log_info("FWD: enable UPnP");
    upnp_init(&upnp);
#endif

    // Add a port forwarding for the DHT for the entire run time
    fwd_add(gconf->dht_port, LONG_MAX);

    // Cause the callback to be called in intervals
    net_add_handler(-1, &fwd_handle);

    return true;
}

void fwd_free(void)
{
    struct forwarding_t *cur;
    struct forwarding_t *next;

    cur = g_fwds;
    while (cur) {
        next = cur->next;
        free(cur);
        cur = next;
    }
    g_fwds = NULL;

#ifdef FWD_NATPMP
    natpmp_uninit(&natpmp);
#endif
#ifdef FWD_UPNP
    upnp_uninit(&upnp);
#endif
}
