/* Copyright (C) 2007-2016 Open Information Security Foundation
 *
 * You can copy, redistribute or modify this Program under the terms of
 * the GNU General Public License version 2 as published by the Free
 * Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * version 2 along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

/**
 * \file
 *
 * \author Victor Julien <victor@inliniac.net>
 *
 */

#include "suricata-common.h"
#include "conf.h"

#include "util-debug.h"
#include "util-thash.h"

#include "util-random.h"
#include "util-misc.h"
#include "util-byte.h"

#include "util-hash-lookup3.h"
#include "util-validate.h"

static THashData *THashGetUsed(THashTableContext *ctx);
static void THashDataEnqueue (THashDataQueue *q, THashData *h);

void THashDataMoveToSpare(THashTableContext *ctx, THashData *h)
{
    THashDataEnqueue(&ctx->spare_q, h);
    (void) SC_ATOMIC_SUB(ctx->counter, 1);
}

static THashDataQueue *THashDataQueueInit (THashDataQueue *q)
{
    if (q != NULL) {
        memset(q, 0, sizeof(THashDataQueue));
        HQLOCK_INIT(q);
    }
    return q;
}

THashDataQueue *THashDataQueueNew(void)
{
    THashDataQueue *q = (THashDataQueue *)SCMalloc(sizeof(THashDataQueue));
    if (q == NULL) {
        SCLogError("Fatal error encountered in THashDataQueueNew. Exiting...");
        exit(EXIT_SUCCESS);
    }
    q = THashDataQueueInit(q);
    return q;
}

/**
 *  \brief Destroy a queue
 *
 *  \param q the queue to destroy
 */
static void THashDataQueueDestroy (THashDataQueue *q)
{
    HQLOCK_DESTROY(q);
}

/**
 *  \brief add to queue
 *
 *  \param q queue
 *  \param h data
 */
static void THashDataEnqueue (THashDataQueue *q, THashData *h)
{
#ifdef DEBUG
    BUG_ON(q == NULL || h == NULL);
#endif

    HQLOCK_LOCK(q);

    /* more data in queue */
    if (q->top != NULL) {
        h->next = q->top;
        q->top->prev = h;
        q->top = h;
    /* only data */
    } else {
        q->top = h;
        q->bot = h;
    }
    q->len++;
#ifdef DBG_PERF
    if (q->len > q->dbg_maxlen)
        q->dbg_maxlen = q->len;
#endif /* DBG_PERF */
    HQLOCK_UNLOCK(q);
}

/**
 *  \brief remove data from the queue
 *
 *  \param q queue
 *
 *  \retval h data or NULL if empty list.
 */
static THashData *THashDataDequeue (THashDataQueue *q)
{
    HQLOCK_LOCK(q);

    THashData *h = q->bot;
    if (h == NULL) {
        HQLOCK_UNLOCK(q);
        return NULL;
    }

    /* more packets in queue */
    if (q->bot->prev != NULL) {
        q->bot = q->bot->prev;
        q->bot->next = NULL;
    /* just the one we remove, so now empty */
    } else {
        q->top = NULL;
        q->bot = NULL;
    }

#ifdef DEBUG
    BUG_ON(q->len == 0);
#endif
    if (q->len > 0)
        q->len--;

    h->next = NULL;
    h->prev = NULL;

    HQLOCK_UNLOCK(q);
    return h;
}

#if 0
static uint32_t THashDataQueueLen(THashDataQueue *q)
{
    uint32_t len;
    HQLOCK_LOCK(q);
    len = q->len;
    HQLOCK_UNLOCK(q);
    return len;
}
#endif

static THashData *THashDataAlloc(THashTableContext *ctx)
{
    const size_t data_size = THASH_DATA_SIZE(ctx);

    if (!(THASH_CHECK_MEMCAP(ctx, data_size))) {
        return NULL;
    }

    (void) SC_ATOMIC_ADD(ctx->memuse, data_size);

    THashData *h = SCCalloc(1, data_size);
    if (unlikely(h == NULL))
        goto error;

    /* points to data right after THashData block */
    h->data = (uint8_t *)h + sizeof(THashData);

//    memset(h, 0x00, data_size);

    SCMutexInit(&h->m, NULL);
    SC_ATOMIC_INIT(h->use_cnt);
    return h;

error:
    return NULL;
}

static void THashDataFree(THashTableContext *ctx, THashData *h)
{
    if (h != NULL) {
        DEBUG_VALIDATE_BUG_ON(SC_ATOMIC_GET(h->use_cnt) != 0);

        if (h->data != NULL) {
            ctx->config.DataFree(h->data);
        }
        SCMutexDestroy(&h->m);
        SCFree(h);
        (void) SC_ATOMIC_SUB(ctx->memuse, THASH_DATA_SIZE(ctx));
    }
}

#define THASH_DEFAULT_HASHSIZE 4096
#define THASH_DEFAULT_MEMCAP 16777216
#define THASH_DEFAULT_PREALLOC 1000

#define GET_VAR(prefix,name) \
    snprintf(varname, sizeof(varname), "%s.%s", (prefix), (name))

/** \brief initialize the configuration
 *  \warning Not thread safe */
static int THashInitConfig(THashTableContext *ctx, const char *cnf_prefix)
{
    char varname[256];

    SCLogDebug("initializing thash engine...");

    /* Check if we have memcap and hash_size defined at config */
    const char *conf_val;
    uint32_t configval = 0;

    /** set config values for memcap, prealloc and hash_size */
    GET_VAR(cnf_prefix, "memcap");
    if ((ConfGet(varname, &conf_val)) == 1)
    {
        if (ParseSizeStringU64(conf_val, &ctx->config.memcap) < 0) {
            SCLogError("Error parsing %s "
                       "from conf file - %s.  Killing engine",
                    varname, conf_val);
            return -1;
        }
    }
    GET_VAR(cnf_prefix, "hash-size");
    if ((ConfGet(varname, &conf_val)) == 1)
    {
        if (StringParseUint32(&configval, 10, (uint16_t)strlen(conf_val), conf_val) > 0) {
            ctx->config.hash_size = configval;
        }
    }

    GET_VAR(cnf_prefix, "prealloc");
    if ((ConfGet(varname, &conf_val)) == 1)
    {
        if (StringParseUint32(&configval, 10, (uint16_t)strlen(conf_val), conf_val) > 0) {
            ctx->config.prealloc = configval;
        } else {
            WarnInvalidConfEntry(varname, "%"PRIu32, ctx->config.prealloc);
        }
    }

    /* alloc hash memory */
    uint64_t hash_size = ctx->config.hash_size * sizeof(THashHashRow);
    if (!(THASH_CHECK_MEMCAP(ctx, hash_size))) {
        SCLogError("allocating hash failed: "
                   "max hash memcap is smaller than projected hash size. "
                   "Memcap: %" PRIu64 ", Hash table size %" PRIu64 ". Calculate "
                   "total hash size by multiplying \"hash-size\" with %" PRIuMAX ", "
                   "which is the hash bucket size.",
                ctx->config.memcap, hash_size, (uintmax_t)sizeof(THashHashRow));
        return -1;
    }
    ctx->array = SCMallocAligned(ctx->config.hash_size * sizeof(THashHashRow), CLS);
    if (unlikely(ctx->array == NULL)) {
        SCLogError("Fatal error encountered in THashInitConfig. Exiting...");
        return -1;
    }
    memset(ctx->array, 0, ctx->config.hash_size * sizeof(THashHashRow));

    uint32_t i = 0;
    for (i = 0; i < ctx->config.hash_size; i++) {
        HRLOCK_INIT(&ctx->array[i]);
    }
    (void) SC_ATOMIC_ADD(ctx->memuse, (ctx->config.hash_size * sizeof(THashHashRow)));

    /* pre allocate prealloc */
    for (i = 0; i < ctx->config.prealloc; i++) {
        if (!(THASH_CHECK_MEMCAP(ctx, THASH_DATA_SIZE(ctx)))) {
            SCLogError("preallocating data failed: "
                       "max thash memcap reached. Memcap %" PRIu64 ", "
                       "Memuse %" PRIu64 ".",
                    ctx->config.memcap,
                    ((uint64_t)SC_ATOMIC_GET(ctx->memuse) + THASH_DATA_SIZE(ctx)));
            return -1;
        }

        THashData *h = THashDataAlloc(ctx);
        if (h == NULL) {
            SCLogError("preallocating data failed: %s", strerror(errno));
            return -1;
        }
        THashDataEnqueue(&ctx->spare_q,h);
    }

    return 0;
}

THashTableContext *THashInit(const char *cnf_prefix, size_t data_size,
        int (*DataSet)(void *, void *), void (*DataFree)(void *),
        uint32_t (*DataHash)(uint32_t, void *), bool (*DataCompare)(void *, void *),
        bool reset_memcap, uint64_t memcap, uint32_t hashsize)
{
    THashTableContext *ctx = SCCalloc(1, sizeof(*ctx));
    BUG_ON(!ctx);

    ctx->config.data_size = data_size;
    ctx->config.DataSet = DataSet;
    ctx->config.DataFree = DataFree;
    ctx->config.DataHash = DataHash;
    ctx->config.DataCompare = DataCompare;

    /* set defaults */
    ctx->config.hash_rand = (uint32_t)RandomGet();
    ctx->config.hash_size = hashsize > 0 ? hashsize : THASH_DEFAULT_HASHSIZE;
    /* Reset memcap in case of loading from file to the highest possible value
     unless defined by the rule keyword */
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
    // limit memcap size to default when fuzzing
    ctx->config.memcap = THASH_DEFAULT_MEMCAP;
#else
    if (memcap > 0) {
        ctx->config.memcap = memcap;
    } else {
        ctx->config.memcap = reset_memcap ? UINT64_MAX : THASH_DEFAULT_MEMCAP;
    }
#endif
    ctx->config.prealloc = THASH_DEFAULT_PREALLOC;

    SC_ATOMIC_INIT(ctx->counter);
    SC_ATOMIC_INIT(ctx->memuse);
    SC_ATOMIC_INIT(ctx->prune_idx);
    THashDataQueueInit(&ctx->spare_q);

    if (THashInitConfig(ctx, cnf_prefix) < 0) {
        THashShutdown(ctx);
        ctx = NULL;
    }
    return ctx;
}

/* \brief Set memcap to current memuse
 * */
void THashConsolidateMemcap(THashTableContext *ctx)
{
    ctx->config.memcap = MAX(SC_ATOMIC_GET(ctx->memuse), ctx->config.memcap);
    SCLogDebug("memcap after load set to: %" PRIu64, ctx->config.memcap);
}

/** \brief shutdown the flow engine
 *  \warning Not thread safe */
void THashShutdown(THashTableContext *ctx)
{
    THashData *h;

    /* free spare queue */
    while ((h = THashDataDequeue(&ctx->spare_q))) {
        BUG_ON(SC_ATOMIC_GET(h->use_cnt) > 0);
        THashDataFree(ctx, h);
    }

    /* clear and free the hash */
    if (ctx->array != NULL) {
        for (uint32_t u = 0; u < ctx->config.hash_size; u++) {
            h = ctx->array[u].head;
            while (h) {
                THashData *n = h->next;
                THashDataFree(ctx, h);
                h = n;
            }

            HRLOCK_DESTROY(&ctx->array[u]);
        }
        SCFreeAligned(ctx->array);
        ctx->array = NULL;
        (void)SC_ATOMIC_SUB(ctx->memuse, ctx->config.hash_size * sizeof(THashHashRow));
    }
    THashDataQueueDestroy(&ctx->spare_q);
    SCFree(ctx);
    return;
}

/** \brief Walk the hash
 *
 */
int THashWalk(THashTableContext *ctx, THashFormatFunc FormatterFunc, THashOutputFunc OutputterFunc, void *output_ctx)
{
    uint32_t u;

    if (ctx->array == NULL)
        return -1;

    bool err = false;
    for (u = 0; u < ctx->config.hash_size; u++) {
        THashHashRow *hb = &ctx->array[u];
        HRLOCK_LOCK(hb);
        THashData *h = hb->head;
        while (h) {
            char output_string[1024] = "";
            int size = FormatterFunc(h->data, output_string, sizeof(output_string));
            if (size > 0) {
                if (OutputterFunc(output_ctx, (const uint8_t *)output_string, size) < 0) {
                    err = true;
                    break;
                }
            }
            h = h->next;
        }
        HRLOCK_UNLOCK(hb);
        if (err == true)
            return -1;
    }
    return 0;
}

/** \brief Cleanup the thash engine
 *
 * Cleanup the thash engine from tag and threshold.
 *
 */
void THashCleanup(THashTableContext *ctx)
{
    uint32_t u;

    if (ctx->array == NULL)
        return;

    for (u = 0; u < ctx->config.hash_size; u++) {
        THashHashRow *hb = &ctx->array[u];
        HRLOCK_LOCK(hb);
        THashData *h = hb->head;
        while (h) {
            if ((SC_ATOMIC_GET(h->use_cnt) > 0)) {
                h = h->next;
            } else {
                THashData *n = h->next;
                /* remove from the hash */
                if (h->prev != NULL)
                    h->prev->next = h->next;
                if (h->next != NULL)
                    h->next->prev = h->prev;
                if (hb->head == h)
                    hb->head = h->next;
                if (hb->tail == h)
                    hb->tail = h->prev;
                h->next = NULL;
                h->prev = NULL;
                THashDataMoveToSpare(ctx, h);
                h = n;
            }
        }
        HRLOCK_UNLOCK(hb);
    }
    return;
}

/* calculate the hash key for this packet
 *
 * we're using:
 *  hash_rand -- set at init time
 *  source address
 */
static uint32_t THashGetKey(const THashConfig *cnf, void *data)
{
    uint32_t key;

    key = cnf->DataHash(cnf->hash_rand, data);
    key %= cnf->hash_size;

    return key;
}

static inline int THashCompare(const THashConfig *cnf, void *a, void *b)
{
    if (cnf->DataCompare(a, b))
        return 1;
    return 0;
}

/**
 *  \brief Get new data
 *
 *  Get new data. We're checking memcap first and will try to make room
 *  if the memcap is reached.
 *
 *  \retval h *LOCKED* data on succes, NULL on error.
 */
static THashData *THashDataGetNew(THashTableContext *ctx, void *data)
{
    THashData *h = NULL;

    /* get data from the spare queue */
    h = THashDataDequeue(&ctx->spare_q);
    if (h == NULL) {
        /* If we reached the max memcap, we get used data */
        if (!(THASH_CHECK_MEMCAP(ctx, THASH_DATA_SIZE(ctx)))) {
            h = THashGetUsed(ctx);
            if (h == NULL) {
                return NULL;
            }

            if (!SC_ATOMIC_GET(ctx->memcap_reached)) {
                SC_ATOMIC_SET(ctx->memcap_reached, true);
            }

            /* freed data, but it's unlocked */
        } else {
            /* now see if we can alloc a new data */
            h = THashDataAlloc(ctx);
            if (h == NULL) {
                return NULL;
            }

            /* data is initialized but *unlocked* */
        }
    } else {
        /* data has been recycled before it went into the spare queue */

        /* data is initialized (recycled) but *unlocked* */
    }

    // setup the data
    BUG_ON(ctx->config.DataSet(h->data, data) != 0);

    (void) SC_ATOMIC_ADD(ctx->counter, 1);
    SCMutexLock(&h->m);
    return h;
}

/*
 * returns a *LOCKED* data or NULL
 */

struct THashDataGetResult
THashGetFromHash (THashTableContext *ctx, void *data)
{
    struct THashDataGetResult res = { .data = NULL, .is_new = false, };
    THashData *h = NULL;

    /* get the key to our bucket */
    uint32_t key = THashGetKey(&ctx->config, data);
    /* get our hash bucket and lock it */
    THashHashRow *hb = &ctx->array[key];
    HRLOCK_LOCK(hb);

    /* see if the bucket already has data */
    if (hb->head == NULL) {
        h = THashDataGetNew(ctx, data);
        if (h == NULL) {
            HRLOCK_UNLOCK(hb);
            return res;
        }

        /* data is locked */
        hb->head = h;
        hb->tail = h;

        /* initialize and return */
        (void) THashIncrUsecnt(h);

        HRLOCK_UNLOCK(hb);
        res.data = h;
        res.is_new = true;
        return res;
    }

    /* ok, we have data in the bucket. Let's find out if it is our data */
    h = hb->head;

    /* see if this is the data we are looking for */
    if (THashCompare(&ctx->config, h->data, data) == 0) {
        THashData *ph = NULL; /* previous data */

        while (h) {
            ph = h;
            h = h->next;

            if (h == NULL) {
                h = ph->next = THashDataGetNew(ctx, data);
                if (h == NULL) {
                    HRLOCK_UNLOCK(hb);
                    return res;
                }
                hb->tail = h;

                /* data is locked */

                h->prev = ph;

                /* initialize and return */
                (void) THashIncrUsecnt(h);

                HRLOCK_UNLOCK(hb);
                res.data = h;
                res.is_new = true;
                return res;
            }

            if (THashCompare(&ctx->config, h->data, data) != 0) {
                /* we found our data, lets put it on top of the
                 * hash list -- this rewards active data */
                if (h->next) {
                    h->next->prev = h->prev;
                }
                if (h->prev) {
                    h->prev->next = h->next;
                }
                if (h == hb->tail) {
                    hb->tail = h->prev;
                }

                h->next = hb->head;
                h->prev = NULL;
                hb->head->prev = h;
                hb->head = h;

                /* found our data, lock & return */
                SCMutexLock(&h->m);
                (void) THashIncrUsecnt(h);
                HRLOCK_UNLOCK(hb);
                res.data = h;
                res.is_new = false;
                /* coverity[missing_unlock : FALSE] */
                return res;
            }
        }
    }

    /* lock & return */
    SCMutexLock(&h->m);
    (void) THashIncrUsecnt(h);
    HRLOCK_UNLOCK(hb);
    res.data = h;
    res.is_new = false;
    /* coverity[missing_unlock : FALSE] */
    return res;
}

/** \brief look up data in the hash
 *
 *  \param data data to look up
 *
 *  \retval h *LOCKED* data or NULL
 */
THashData *THashLookupFromHash (THashTableContext *ctx, void *data)
{
    THashData *h = NULL;

    /* get the key to our bucket */
    uint32_t key = THashGetKey(&ctx->config, data);
    /* get our hash bucket and lock it */
    THashHashRow *hb = &ctx->array[key];
    HRLOCK_LOCK(hb);

    if (hb->head == NULL) {
        HRLOCK_UNLOCK(hb);
        return h;
    }

    /* ok, we have data in the bucket. Let's find out if it is our data */
    h = hb->head;

    /* see if this is the data we are looking for */
    if (THashCompare(&ctx->config, h->data, data) == 0) {
        while (h) {
            h = h->next;
            if (h == NULL) {
                HRLOCK_UNLOCK(hb);
                return h;
            }

            if (THashCompare(&ctx->config, h->data, data) != 0) {
                /* we found our data, lets put it on top of the
                 * hash list -- this rewards active data */
                if (h->next) {
                    h->next->prev = h->prev;
                }
                if (h->prev) {
                    h->prev->next = h->next;
                }
                if (h == hb->tail) {
                    hb->tail = h->prev;
                }

                h->next = hb->head;
                h->prev = NULL;
                hb->head->prev = h;
                hb->head = h;

                /* found our data, lock & return */
                SCMutexLock(&h->m);
                (void) THashIncrUsecnt(h);
                HRLOCK_UNLOCK(hb);
                return h;
            }
        }
    }

    /* lock & return */
    SCMutexLock(&h->m);
    (void) THashIncrUsecnt(h);
    HRLOCK_UNLOCK(hb);
    return h;
}

/** \internal
 *  \brief Get data from the hash directly.
 *
 *  Called in conditions where the spare queue is empty and memcap is
 *  reached.
 *
 *  Walks the hash until data can be freed. "prune_idx" atomic int makes
 *  sure we don't start at the top each time since that would clear the top
 *  of the hash leading to longer and longer search times under high
 *  pressure (observed).
 *
 *  \retval h data or NULL
 */
static THashData *THashGetUsed(THashTableContext *ctx)
{
    uint32_t idx = SC_ATOMIC_GET(ctx->prune_idx) % ctx->config.hash_size;
    uint32_t cnt = ctx->config.hash_size;

    while (cnt--) {
        if (++idx >= ctx->config.hash_size)
            idx = 0;

        THashHashRow *hb = &ctx->array[idx];

        if (HRLOCK_TRYLOCK(hb) != 0)
            continue;

        THashData *h = hb->tail;
        if (h == NULL) {
            HRLOCK_UNLOCK(hb);
            continue;
        }

        if (SCMutexTrylock(&h->m) != 0) {
            HRLOCK_UNLOCK(hb);
            continue;
        }

        if (SC_ATOMIC_GET(h->use_cnt) > 0) {
            HRLOCK_UNLOCK(hb);
            SCMutexUnlock(&h->m);
            continue;
        }

        /* remove from the hash */
        if (h->prev != NULL)
            h->prev->next = h->next;
        if (h->next != NULL)
            h->next->prev = h->prev;
        if (hb->head == h)
            hb->head = h->next;
        if (hb->tail == h)
            hb->tail = h->prev;

        h->next = NULL;
        h->prev = NULL;
        HRLOCK_UNLOCK(hb);

        if (h->data != NULL) {
            ctx->config.DataFree(h->data);
        }
        SCMutexUnlock(&h->m);

        (void) SC_ATOMIC_ADD(ctx->prune_idx, (ctx->config.hash_size - cnt));
        return h;
    }

    return NULL;
}

/**
 * \retval int -1 not found
 * \retval int 0 found, but it was busy (ref cnt)
 * \retval int 1 found and removed */
int THashRemoveFromHash (THashTableContext *ctx, void *data)
{
    /* get the key to our bucket */
    uint32_t key = THashGetKey(&ctx->config, data);
    /* get our hash bucket and lock it */
    THashHashRow *hb = &ctx->array[key];

    HRLOCK_LOCK(hb);
    THashData *h = hb->head;
    while (h != NULL) {
        /* see if this is the data we are looking for */
        if (THashCompare(&ctx->config, h->data, data) == 0) {
            h = h->next;
            continue;
        }

        SCMutexLock(&h->m);
        if (SC_ATOMIC_GET(h->use_cnt) > 0) {
            SCMutexUnlock(&h->m);
            HRLOCK_UNLOCK(hb);
            return 0;
        }

        /* remove from the hash */
        if (h->prev != NULL)
            h->prev->next = h->next;
        if (h->next != NULL)
            h->next->prev = h->prev;
        if (hb->head == h)
            hb->head = h->next;
        if (hb->tail == h)
            hb->tail = h->prev;

        h->next = NULL;
        h->prev = NULL;
        SCMutexUnlock(&h->m);
        HRLOCK_UNLOCK(hb);
        THashDataFree(ctx, h);
        SCLogDebug("found and removed");
        return 1;
    }

    HRLOCK_UNLOCK(hb);
    SCLogDebug("data not found");
    return -1;
}
