/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"

#include "nm-ref-string.h"

/*****************************************************************************/

G_LOCK_DEFINE_STATIC(gl_lock);
static GHashTable *gl_hash;

/*****************************************************************************/

static void
_ref_string_get(const NMRefString *rstr, const char **out_str, gsize *out_len)
{
    if (rstr->len == G_MAXSIZE) {
        *out_len = rstr->_priv_lookup.l_len;
        *out_str = rstr->_priv_lookup.l_str;
    } else {
        *out_len = rstr->len;
        *out_str = rstr->str;
    }
}

static guint
_ref_string_hash(gconstpointer ptr)
{
    const char *cstr;
    gsize       len;

    _ref_string_get(ptr, &cstr, &len);
    return nm_hash_mem(1463435489u, cstr, len);
}

static gboolean
_ref_string_equal(gconstpointer ptr_a, gconstpointer ptr_b)
{
    const char *cstr_a;
    const char *cstr_b;
    gsize       len_a;
    gsize       len_b;

    _ref_string_get(ptr_a, &cstr_a, &len_a);
    _ref_string_get(ptr_b, &cstr_b, &len_b);

    /* memcmp() accepts "n=0" argument, but it's not clear whether in that case
     * all pointers must still be valid. The input pointer might be provided by
     * the user via nm_ref_string_new_len(), and for len=0 we want to allow
     * also invalid pointers. Hence, this extra "len_a==0" check. */
    return len_a == len_b && (len_a == 0 || (memcmp(cstr_a, cstr_b, len_a) == 0));
}

/*****************************************************************************/

void
_nm_assert_nm_ref_string(NMRefString *rstr)
{
    int r;

    nm_assert(rstr);

    if (NM_MORE_ASSERTS > 0) {
        r = g_atomic_int_get(&rstr->_ref_count);
        nm_assert(r > 0);
        nm_assert(r < G_MAXINT);
    }

    nm_assert(rstr->str[rstr->len] == '\0');

    if (NM_MORE_ASSERTS > 10) {
        G_LOCK(gl_lock);
        r = g_atomic_int_get(&rstr->_ref_count);
        nm_assert(r > 0);
        nm_assert(r < G_MAXINT);

        nm_assert(rstr == g_hash_table_lookup(gl_hash, rstr));
        G_UNLOCK(gl_lock);
    }
}

/**
 * nm_ref_string_new_len:
 * @cstr: the string to intern. Must contain @len bytes.
 *   If @len is zero, @cstr may be %NULL. Note that it is
 *   acceptable that the string contains a NUL character
 *   within the first @len bytes. That is, the string is
 *   not treated as a NUL terminated string, but as binary.
 *   Also, contrary to strncpy(), this will read all the
 *   first @len bytes. It won't stop at the first NUL.
 * @len: the length of the string (usually there is no NUL character
 *   within the first @len bytes, but that would be acceptable as well
 *   to add binary data).
 *
 * Note that the resulting NMRefString instance will always be NUL terminated
 * (at position @len).
 *
 * Note that NMRefString are always interned/deduplicated. If such a string
 * already exists, the existing instance will be referred and returned.
 *
 *
 * Since all NMRefString are shared and interned, you may use
 * pointer equality to compare them. Note that if a NMRefString contains
 * a NUL character (meaning, if
 *
 *    strlen (nm_ref_string_get_str (str)) != nm_ref_string_get_len (str)
 *
 * ), then pointer in-equality does not mean that the NUL terminated strings
 * are also unequal. In other words, for strings that contain NUL characters,
 *
 *    if (str1 != str2)
 *       assert (!nm_streq0 (nm_ref_string_get_str (str1), nm_ref_string_get_str (str2)));
 *
 * might not hold!
 *
 *
 * NMRefString is thread-safe.
 *
 * Returns: (transfer full): the interned string. This is
 *   never %NULL, but note that %NULL is also a valid NMRefString.
 *   The result must be unrefed with nm_ref_string_unref().
 */
NMRefString *
nm_ref_string_new_len(const char *cstr, gsize len)
{
    NMRefString *rstr;

    /* @len cannot be close to G_MAXSIZE. For one, that would mean our call
     * to malloc() below overflows. Also, we use G_MAXSIZE as special length
     * to indicate using _priv_lookup. */
    nm_assert(len < G_MAXSIZE - G_STRUCT_OFFSET(NMRefString, str) - 1u);

    G_LOCK(gl_lock);

    if (G_UNLIKELY(!gl_hash)) {
        gl_hash = g_hash_table_new_full(_ref_string_hash, _ref_string_equal, g_free, NULL);
        rstr    = NULL;
    } else {
        NMRefString rr_lookup = {
            .len = G_MAXSIZE,
            ._priv_lookup =
                {
                    .l_len = len,
                    .l_str = cstr,
                },
        };

        rstr = g_hash_table_lookup(gl_hash, &rr_lookup);
    }

    if (rstr) {
        nm_assert(({
            int r = g_atomic_int_get(&rstr->_ref_count);

            (r >= 0 && r < G_MAXINT);
        }));
        g_atomic_int_inc(&rstr->_ref_count);
    } else {
        rstr = g_malloc((G_STRUCT_OFFSET(NMRefString, str) + 1u) + len);
        if (len > 0)
            memcpy((char *) rstr->str, cstr, len);
        ((char *) rstr->str)[len] = '\0';
        *((gsize *) &rstr->len)   = len;
        rstr->_ref_count          = 1;

        if (!g_hash_table_add(gl_hash, rstr))
            nm_assert_not_reached();
    }

    G_UNLOCK(gl_lock);

    return rstr;
}

/**
 * nmtst_ref_string_find_len:
 * @cstr: the string to find.
 * @len: length of @cstr.
 *
 * Returns: (transfer none): %NULL, if the string is currently
 *   not interned. Otherwise a reference to the interned string.
 *   Beware: this does not return ownership of the reference,
 *   it is thus not thread safe. Only use this from unit tests
 *   when you know that your thread holds a reference to the string
 *   to keep it alive. Otherwise, this might be a dangling pointer.
 */
NMRefString *
nmtst_ref_string_find_len(const char *cstr, gsize len)
{
    NMRefString *rstr = NULL;

    /* @len cannot be close to G_MAXSIZE. For one, that would mean our call
     * to malloc() below overflows. Also, we use G_MAXSIZE as special length
     * to indicate using _priv_lookup. */
    nm_assert(len < G_MAXSIZE - G_STRUCT_OFFSET(NMRefString, str) - 1u);

    G_LOCK(gl_lock);

    if (G_LIKELY(gl_hash)) {
        NMRefString rr_lookup = {
            .len = G_MAXSIZE,
            ._priv_lookup =
                {
                    .l_len = len,
                    .l_str = cstr,
                },
        };

        rstr = g_hash_table_lookup(gl_hash, &rr_lookup);
    }

    G_UNLOCK(gl_lock);

    return rstr;
}

void
_nm_ref_string_unref_slow_path(NMRefString *rstr)
{
    G_LOCK(gl_lock);

    nm_assert(g_hash_table_lookup(gl_hash, rstr) == rstr);

    if (G_LIKELY(g_atomic_int_dec_and_test(&rstr->_ref_count))) {
        if (!g_hash_table_remove(gl_hash, rstr))
            nm_assert_not_reached();
    }

    G_UNLOCK(gl_lock);
}

/*****************************************************************************/
