/**
 * @file dict.c
 * @author Radek Krejci <rkrejci@cesnet.cz>
 * @author Michal Vasko <mvasko@cesnet.cz>
 * @brief libyang dictionary for storing strings
 *
 * Copyright (c) 2015 - 2023 CESNET, z.s.p.o.
 *
 * This source code is licensed under BSD 3-Clause License (the "License").
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://opensource.org/licenses/BSD-3-Clause
 */

#include "dict.h"

#include <assert.h>
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "compat.h"
#include "log.h"
#include "ly_common.h"

/* starting size of the dictionary */
#define LYDICT_MIN_SIZE 1024

/**
 * @brief Comparison callback for dictionary's hash table
 *
 * Implementation of ::lyht_value_equal_cb.
 */
static ly_bool
lydict_val_eq(void *val1_p, void *val2_p, ly_bool UNUSED(mod), void *cb_data)
{
    const char *str1, *str2;
    size_t *len1;

    LY_CHECK_ARG_RET(NULL, val1_p, val2_p, cb_data, 0);

    str1 = ((struct ly_dict_rec *)val1_p)->value;
    str2 = ((struct ly_dict_rec *)val2_p)->value;
    len1 = cb_data;

    LY_CHECK_ERR_RET(!str1, LOGARG(NULL, val1_p), 0);
    LY_CHECK_ERR_RET(!str2, LOGARG(NULL, val2_p), 0);

    if (!strncmp(str1, str2, *len1) && !str2[*len1]) {
        return 1;
    }

    return 0;
}

void
lydict_init(struct ly_dict *dict)
{
    LY_CHECK_ARG_RET(NULL, dict, );

    dict->hash_tab = lyht_new(LYDICT_MIN_SIZE, sizeof(struct ly_dict_rec), lydict_val_eq, NULL, 1);
    LY_CHECK_ERR_RET(!dict->hash_tab, LOGINT(NULL), );
    pthread_mutex_init(&dict->lock, NULL);
}

void
lydict_clean(struct ly_dict *dict)
{
    struct ly_dict_rec *dict_rec = NULL;
    struct ly_ht_rec *rec = NULL;
    uint32_t hlist_idx;
    uint32_t rec_idx;

    LY_CHECK_ARG_RET(NULL, dict, );

    LYHT_ITER_ALL_RECS(dict->hash_tab, hlist_idx, rec_idx, rec) {
        /*
         * this should not happen, all records inserted into
         * dictionary are supposed to be removed using lydict_remove()
         * before calling lydict_clean()
         */
        dict_rec = (struct ly_dict_rec *)rec->val;
        LOGWRN(NULL, "String \"%s\" not freed from the dictionary, refcount %" PRIu32 ".", dict_rec->value, dict_rec->refcount);
        /* if record wasn't removed before free string allocated for that record */
#ifdef NDEBUG
        free(dict_rec->value);
#endif
    }

    /* free table and destroy mutex */
    lyht_free(dict->hash_tab, NULL);
    pthread_mutex_destroy(&dict->lock);
}

static ly_bool
lydict_resize_val_eq(void *val1_p, void *val2_p, ly_bool mod, void *UNUSED(cb_data))
{
    const char *str1, *str2;

    LY_CHECK_ARG_RET(NULL, val1_p, val2_p, 0);

    str1 = ((struct ly_dict_rec *)val1_p)->value;
    str2 = ((struct ly_dict_rec *)val2_p)->value;

    if (mod) {
        /* used when inserting new values */
        if (strcmp(str1, str2) == 0) {
            return 1;
        }
    } else {
        /* used when finding the original value again in the resized table */
        if (str1 == str2) {
            return 1;
        }
    }

    return 0;
}

LIBYANG_API_DEF LY_ERR
lydict_remove(const struct ly_ctx *ctx, const char *value)
{
    LY_ERR ret = LY_SUCCESS;
    size_t len;
    uint32_t hash;
    struct ly_dict_rec rec, *match = NULL;
    char *val_p;

    if (!ctx || !value) {
        return LY_SUCCESS;
    }

    LOGDBG(LY_LDGDICT, "removing \"%s\"", value);

    len = strlen(value);
    hash = lyht_hash(value, len);

    /* create record for lyht_find call */
    rec.value = (char *)value;
    rec.refcount = 0;

    pthread_mutex_lock((pthread_mutex_t *)&ctx->dict.lock);
    /* set len as data for compare callback */
    lyht_set_cb_data(ctx->dict.hash_tab, (void *)&len);
    /* check if value is already inserted */
    ret = lyht_find(ctx->dict.hash_tab, &rec, hash, (void **)&match);

    if (ret == LY_SUCCESS) {
        LY_CHECK_ERR_GOTO(!match, LOGINT(ctx), finish);

        /* if value is already in dictionary, decrement reference counter */
        match->refcount--;
        if (match->refcount == 0) {
            /*
             * remove record
             * save pointer to stored string before lyht_remove to
             * free it after it is removed from hash table
             */
            val_p = match->value;
            ret = lyht_remove_with_resize_cb(ctx->dict.hash_tab, &rec, hash, lydict_resize_val_eq);
            free(val_p);
            LY_CHECK_ERR_GOTO(ret, LOGINT(ctx), finish);
        }
    } else if (ret == LY_ENOTFOUND) {
        LOGERR(ctx, LY_ENOTFOUND, "Value \"%s\" was not found in the dictionary.", value);
    } else {
        LOGINT(ctx);
    }

finish:
    pthread_mutex_unlock((pthread_mutex_t *)&ctx->dict.lock);
    return ret;
}

static LY_ERR
dict_insert(const struct ly_ctx *ctx, char *value, size_t len, ly_bool zerocopy, const char **str_p)
{
    LY_ERR ret = LY_SUCCESS;
    struct ly_dict_rec *match = NULL, rec;
    uint32_t hash;

    LOGDBG(LY_LDGDICT, "inserting \"%.*s\"", (int)len, value);

    hash = lyht_hash(value, len);
    /* set len as data for compare callback */
    lyht_set_cb_data(ctx->dict.hash_tab, (void *)&len);
    /* create record for lyht_insert */
    rec.value = value;
    rec.refcount = 1;

    ret = lyht_insert_with_resize_cb(ctx->dict.hash_tab, (void *)&rec, hash, lydict_resize_val_eq, (void **)&match);
    if (ret == LY_EEXIST) {
        match->refcount++;
        if (zerocopy) {
            free(value);
        }
        ret = LY_SUCCESS;
    } else if (ret == LY_SUCCESS) {
        if (!zerocopy) {
            /*
             * allocate string for new record
             * record is already inserted in hash table
             */
            match->value = malloc(sizeof *match->value * (len + 1));
            LY_CHECK_ERR_RET(!match->value, LOGMEM(ctx), LY_EMEM);
            if (len) {
                memcpy(match->value, value, len);
            }
            match->value[len] = '\0';
        }
    } else {
        /* lyht_insert returned error */
        if (zerocopy) {
            free(value);
        }
        return ret;
    }

    *str_p = match->value;

    return ret;
}

LIBYANG_API_DEF LY_ERR
lydict_insert(const struct ly_ctx *ctx, const char *value, size_t len, const char **str_p)
{
    LY_ERR result;

    LY_CHECK_ARG_RET(ctx, ctx, str_p, LY_EINVAL);

    if (!value) {
        *str_p = NULL;
        return LY_SUCCESS;
    }

    if (!len) {
        len = strlen(value);
    }

    pthread_mutex_lock((pthread_mutex_t *)&ctx->dict.lock);
    result = dict_insert(ctx, (char *)value, len, 0, str_p);
    pthread_mutex_unlock((pthread_mutex_t *)&ctx->dict.lock);

    return result;
}

LIBYANG_API_DEF LY_ERR
lydict_insert_zc(const struct ly_ctx *ctx, char *value, const char **str_p)
{
    LY_ERR result;

    LY_CHECK_ARG_RET(ctx, ctx, str_p, LY_EINVAL);

    if (!value) {
        *str_p = NULL;
        return LY_SUCCESS;
    }

    pthread_mutex_lock((pthread_mutex_t *)&ctx->dict.lock);
    result = dict_insert(ctx, value, strlen(value), 1, str_p);
    pthread_mutex_unlock((pthread_mutex_t *)&ctx->dict.lock);

    return result;
}

static LY_ERR
dict_dup(const struct ly_ctx *ctx, char *value, const char **str_p)
{
    LY_ERR ret = LY_SUCCESS;
    struct ly_dict_rec *match = NULL, rec;
    uint32_t hash;

    /* set new callback to only compare memory addresses */
    lyht_value_equal_cb prev = lyht_set_cb(ctx->dict.hash_tab, lydict_resize_val_eq);

    LOGDBG(LY_LDGDICT, "duplicating %s", value);
    hash = lyht_hash(value, strlen(value));
    rec.value = value;

    ret = lyht_find(ctx->dict.hash_tab, (void *)&rec, hash, (void **)&match);
    if (ret == LY_SUCCESS) {
        /* record found, increase refcount */
        match->refcount++;
        *str_p = match->value;
    }

    /* restore callback */
    lyht_set_cb(ctx->dict.hash_tab, prev);

    return ret;
}

LIBYANG_API_DEF LY_ERR
lydict_dup(const struct ly_ctx *ctx, const char *value, const char **str_p)
{
    LY_ERR result;

    LY_CHECK_ARG_RET(ctx, ctx, str_p, LY_EINVAL);

    if (!value) {
        *str_p = NULL;
        return LY_SUCCESS;
    }

    pthread_mutex_lock((pthread_mutex_t *)&ctx->dict.lock);
    result = dict_dup(ctx, (char *)value, str_p);
    pthread_mutex_unlock((pthread_mutex_t *)&ctx->dict.lock);

    return result;
}
