/*
 * Copyright (C) 2014 Freie Universität Berlin
 *
 * This file is subject to the terms and conditions of the GNU Lesser
 * General Public License v2.1. See the file LICENSE in the top level
 * directory for more details.
 */

/**
 * @ingroup     nhdp
 * @{
 *
 * @file
 * @brief       Neighbor Information Base implementation for NHDP
 *
 * @author      Fabian Nack <nack@inf.fu-berlin.de>
 *
 * @}
 */

#include "timex.h"
#include "mutex.h"
#include "xtimer.h"
#include "utlist.h"

#include "rfc5444/rfc5444_iana.h"
#include "rfc5444/rfc5444_writer.h"

#include "nib_table.h"
#include "iib_table.h"
#include "nhdp_address.h"
#include "nhdp_writer.h"

/* Internal variables */
static mutex_t mtx_nib_access = MUTEX_INIT;
static nib_entry_t *nib_entry_head = NULL;
static nib_lost_address_entry_t *nib_lost_address_entry_head = NULL;

/* Internal function prototypes */
static nib_entry_t *add_nib_entry_for_nb_addr_list(void);
static void rem_nib_entry(nib_entry_t *nib_entry, timex_t *now);
static void clear_nb_addresses(nib_entry_t *nib_entry, timex_t *now);
static int add_lost_neighbor_address(nhdp_addr_t *lost_addr, timex_t *now);
static void rem_ln_entry(nib_lost_address_entry_t *ln_entry);


/*---------------------------------------------------------------------------*
 *                       Neighbor Information Base API                       *
 *---------------------------------------------------------------------------*/

nib_entry_t *nib_process_hello(void)
{
    nib_entry_t *nb_match = NULL;
    nib_entry_t *nib_elt, *nib_tmp;
    timex_t now;
    uint8_t matches = 0;

    mutex_lock(&mtx_nib_access);

    xtimer_now_timex(&now);

    LL_FOREACH_SAFE(nib_entry_head, nib_elt, nib_tmp) {
        nhdp_addr_entry_t *list_elt;
        LL_FOREACH(nib_elt->address_list_head, list_elt) {
            if (NHDP_ADDR_TMP_IN_NB_LIST(list_elt->address)) {
                /* Matching neighbor tuple */
                matches++;

                if (matches > 1) {
                    /* Multiple matching nb tuples, delete the previous one */
                    iib_propagate_nb_entry_change(nb_match, nib_elt);
                    rem_nib_entry(nb_match, &now);
                }

                nb_match = nib_elt;
                break;
            }
        }
    }

    /* Add or update nb tuple */
    if (matches > 0) {
        /* We found matching nb tuples, reuse the last one */
        clear_nb_addresses(nb_match, &now);

        if (matches > 1) {
            nb_match->symmetric = 0;
        }

        nb_match->address_list_head = nhdp_generate_addr_list_from_tmp(NHDP_ADDR_TMP_NB_LIST);

        if (!nb_match->address_list_head) {
            /* Insufficient memory */
            LL_DELETE(nib_entry_head, nb_match);
            free(nb_match);
            nb_match = NULL;
        }
    }
    else {
        nb_match = add_nib_entry_for_nb_addr_list();
    }

    mutex_unlock(&mtx_nib_access);
    return nb_match;
}

void nib_fill_wr_addresses(struct rfc5444_writer *wr)
{
    nib_entry_t *nib_elt;
    nhdp_addr_entry_t *addr_elt;
    nib_lost_address_entry_t *lost_elt, *lost_tmp;
    timex_t now;

    mutex_lock(&mtx_nib_access);

    xtimer_now_timex(&now);

    /* Add addresses of symmetric neighbors to HELLO msg */
    LL_FOREACH(nib_entry_head, nib_elt) {
        if (nib_elt->symmetric) {
            LL_FOREACH(nib_elt->address_list_head, addr_elt) {
                /* Check whether address is not already included with link status symmetric */
                if (!NHDP_ADDR_TMP_IN_SYM(addr_elt->address)) {
                    nhdp_writer_add_addr(wr, addr_elt->address, RFC5444_ADDRTLV_OTHER_NEIGHB,
                                         RFC5444_OTHERNEIGHB_SYMMETRIC,
                                         rfc5444_metric_encode(nib_elt->metric_in),
                                         rfc5444_metric_encode(nib_elt->metric_out));
                    addr_elt->address->in_tmp_table = NHDP_ADDR_TMP_SYM;
                }
            }
        }
    }

    /* Add lost addresses of neighbors to HELLO msg */
    LL_FOREACH_SAFE(nib_lost_address_entry_head, lost_elt, lost_tmp) {
        if (timex_cmp(lost_elt->expiration_time, now) != 1) {
            /* Entry expired, remove it */
            rem_ln_entry(lost_elt);
        }
        else {
            /* Check if address is not already present in one of the temporary lists */
            if (!NHDP_ADDR_TMP_IN_ANY(lost_elt->address)) {
                /* Address is not present in one of the lists, add it */
                nhdp_writer_add_addr(wr, lost_elt->address, RFC5444_ADDRTLV_OTHER_NEIGHB,
                                     RFC5444_OTHERNEIGHB_LOST, NHDP_METRIC_UNKNOWN,
                                     NHDP_METRIC_UNKNOWN);
            }
        }
    }

    mutex_unlock(&mtx_nib_access);
}

void nib_rem_nb_entry(nib_entry_t *nib_entry)
{
    nhdp_free_addr_list(nib_entry->address_list_head);
    LL_DELETE(nib_entry_head, nib_entry);
    free(nib_entry);
}

void nib_set_nb_entry_sym(nib_entry_t *nib_entry)
{
    nib_lost_address_entry_t *ln_elt, *ln_tmp;
    nhdp_addr_entry_t *nb_elt;

    nib_entry->symmetric = 1;
    LL_FOREACH(nib_entry->address_list_head, nb_elt) {
        LL_FOREACH_SAFE(nib_lost_address_entry_head, ln_elt, ln_tmp) {
            /* Remove all Lost Neighbor Tuples matching an address of the newly sym nb */
            if (ln_elt->address == nb_elt->address) {
                rem_ln_entry(ln_elt);
                break;
            }
        }
    }
}

void nib_reset_nb_entry_sym(nib_entry_t *nib_entry, timex_t *now)
{
    nhdp_addr_entry_t *nb_elt;

    nib_entry->symmetric = 0;
    LL_FOREACH(nib_entry->address_list_head, nb_elt) {
        /* Add a Lost Neighbor Tuple for each address of the neighbor */
        if (add_lost_neighbor_address(nb_elt->address, now) == -1) {
            /* Insufficient memory */
            return;
        }
    }
}


/*------------------------------------------------------------------------------------*/
/*                                Internal functions                                  */
/*------------------------------------------------------------------------------------*/

/**
 * Add a Neighbor Tuple for the neighbor address list
 */
static nib_entry_t *add_nib_entry_for_nb_addr_list(void)
{
    nib_entry_t *new_elem;

    new_elem = malloc(sizeof(nib_entry_t));

    if (!new_elem) {
        /* Insufficient memory */
        return NULL;
    }

    /* Copy neighbor address list to new neighbor tuple */
    new_elem->address_list_head = nhdp_generate_addr_list_from_tmp(NHDP_ADDR_TMP_NB_LIST);

    if (!new_elem->address_list_head) {
        /* Insufficient memory */
        free(new_elem);
        return NULL;
    }

    new_elem->symmetric = 0;
    new_elem->metric_in = NHDP_METRIC_UNKNOWN;
    new_elem->metric_out = NHDP_METRIC_UNKNOWN;
    LL_PREPEND(nib_entry_head, new_elem);

    return new_elem;
}

/**
 * Remove a given Neighbor Tuple
 */
static void rem_nib_entry(nib_entry_t *nib_entry, timex_t *now)
{
    clear_nb_addresses(nib_entry, now);
    LL_DELETE(nib_entry_head, nib_entry);
    free(nib_entry);
}

/**
 * Clear address list of a Neighbor Tuple and add Lost Neighbor Tuple for addresses
 * no longer used by this neighbor
 */
static void clear_nb_addresses(nib_entry_t *nib_entry, timex_t *now)
{
    nhdp_addr_entry_t *nib_elt, *nib_tmp;

    LL_FOREACH_SAFE(nib_entry->address_list_head, nib_elt, nib_tmp) {
        /* Check whether address is still present in the new neighbor address list */
        if (!NHDP_ADDR_TMP_IN_NB_LIST(nib_elt->address)) {
            /* Address is not in the newly received address list of the neighbor */
            /* Add it to the Removed Address List */
            nib_elt->address->in_tmp_table |= NHDP_ADDR_TMP_REM_LIST;
            /* Increment usage counter of address in central NHDP address storage */
            nib_elt->address->usg_count++;

            if (nib_entry->symmetric) {
                /* Additionally create a Lost Neighbor Tuple for symmetric neighbors */
                add_lost_neighbor_address(nib_elt->address, now);
            }
        }

        /* Free the address entry */
        nhdp_free_addr_entry(nib_elt);
    }
    nib_entry->address_list_head = NULL;
}

/**
 * Add or update a Lost Neighbor Tuple
 */
static int add_lost_neighbor_address(nhdp_addr_t *lost_addr, timex_t *now)
{
    nib_lost_address_entry_t *elt;
    timex_t n_hold = timex_from_uint64(((uint64_t)NHDP_N_HOLD_TIME_MS) * US_PER_MS);

    LL_FOREACH(nib_lost_address_entry_head, elt) {
        if (elt->address == lost_addr) {
            /* Existing entry for this address, no need to add a new one */
            if (timex_cmp(elt->expiration_time, *now) == -1) {
                /* Entry expired, so just update expiration time */
                elt->expiration_time = timex_add(*now, n_hold);
            }

            return 0;
        }
    }

    /* No existing entry, create a new one */
    elt = malloc(sizeof(nib_lost_address_entry_t));

    if (!elt) {
        /* Insufficient memory */
        return -1;
    }

    /* Increment usage counter of address in central NHDP address storage */
    lost_addr->usg_count++;
    elt->address = lost_addr;
    elt->expiration_time = timex_add(*now, n_hold);
    LL_PREPEND(nib_lost_address_entry_head, elt);

    return 0;
}

/**
 * Remove a given Lost Neighbor Tuple
 */
static void rem_ln_entry(nib_lost_address_entry_t *ln_entry)
{
    nhdp_decrement_addr_usage(ln_entry->address);
    LL_DELETE(nib_lost_address_entry_head, ln_entry);
    free(ln_entry);
}
