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

#include "alloc-util.h"
#include "hashmap.h"
#include "libudev-list-internal.h"
#include "list.h"
#include "sort-util.h"

/**
 * SECTION:libudev-list
 * @short_description: list operation
 *
 * Libudev list operations.
 */

/**
 * udev_list_entry:
 *
 * Opaque object representing one entry in a list. An entry contains
 * contains a name, and optionally a value.
 */
struct udev_list_entry {
        struct udev_list *list;
        char *name;
        char *value;

        LIST_FIELDS(struct udev_list_entry, entries);
};

struct udev_list {
        Hashmap *unique_entries;
        LIST_HEAD(struct udev_list_entry, entries);
        bool unique:1;
        bool uptodate:1;
};

static struct udev_list_entry *udev_list_entry_free(struct udev_list_entry *entry) {
        if (!entry)
                return NULL;

        if (entry->list) {
                if (entry->list->unique && entry->name)
                        hashmap_remove(entry->list->unique_entries, entry->name);

                if (!entry->list->unique || entry->list->uptodate)
                        LIST_REMOVE(entries, entry->list->entries, entry);
        }

        free(entry->name);
        free(entry->value);

        return mfree(entry);
}

DEFINE_TRIVIAL_CLEANUP_FUNC(struct udev_list_entry *, udev_list_entry_free);

struct udev_list *udev_list_new(bool unique) {
        struct udev_list *list;

        list = new(struct udev_list, 1);
        if (!list)
                return NULL;

        *list = (struct udev_list) {
                .unique = unique,
        };

        return list;
}

struct udev_list_entry *udev_list_entry_add(struct udev_list *list, const char *_name, const char *_value) {
        _cleanup_(udev_list_entry_freep) struct udev_list_entry *entry = NULL;
        _cleanup_free_ char *name = NULL, *value = NULL;

        assert(list);
        assert(_name);

        name = strdup(_name);
        if (!name)
                return NULL;

        if (_value) {
                value = strdup(_value);
                if (!value)
                        return NULL;
        }

        entry = new(struct udev_list_entry, 1);
        if (!entry)
                return NULL;

        *entry = (struct udev_list_entry) {
                .name = TAKE_PTR(name),
                .value = TAKE_PTR(value),
        };

        if (list->unique) {
                udev_list_entry_free(hashmap_get(list->unique_entries, entry->name));

                if (hashmap_ensure_put(&list->unique_entries, &string_hash_ops, entry->name, entry) < 0)
                        return NULL;

                list->uptodate = false;
        } else
                LIST_APPEND(entries, list->entries, entry);

        entry->list = list;

        return TAKE_PTR(entry);
}

void udev_list_cleanup(struct udev_list *list) {
        if (!list)
                return;

        if (list->unique) {
                list->uptodate = false;
                hashmap_clear_with_destructor(list->unique_entries, udev_list_entry_free);
        } else
                LIST_FOREACH(entries, i, list->entries)
                        udev_list_entry_free(i);
}

struct udev_list *udev_list_free(struct udev_list *list) {
        if (!list)
                return NULL;

        udev_list_cleanup(list);
        hashmap_free(list->unique_entries);

        return mfree(list);
}

static int udev_list_entry_compare_func(struct udev_list_entry * const *a, struct udev_list_entry * const *b) {
        return strcmp((*a)->name, (*b)->name);
}

struct udev_list_entry *udev_list_get_entry(struct udev_list *list) {
        if (!list)
                return NULL;

        if (list->unique && !list->uptodate) {
                size_t n;

                LIST_HEAD_INIT(list->entries);

                n = hashmap_size(list->unique_entries);
                if (n == 0)
                        ;
                else if (n == 1)
                        LIST_PREPEND(entries, list->entries, hashmap_first(list->unique_entries));
                else {
                        _cleanup_free_ struct udev_list_entry **buf = NULL;
                        struct udev_list_entry *entry, **p;

                        buf = new(struct udev_list_entry *, n);
                        if (!buf)
                                return NULL;

                        p = buf;
                        HASHMAP_FOREACH(entry, list->unique_entries)
                                *p++ = entry;

                        typesafe_qsort(buf, n, udev_list_entry_compare_func);

                        for (size_t j = n; j > 0; j--)
                                LIST_PREPEND(entries, list->entries, buf[j-1]);
                }

                list->uptodate = true;
        }

        return list->entries;
}

/**
 * udev_list_entry_get_next:
 * @list_entry: current entry
 *
 * Get the next entry from the list.
 *
 * Returns: udev_list_entry, #NULL if no more entries are available.
 */
_public_ struct udev_list_entry *udev_list_entry_get_next(struct udev_list_entry *list_entry) {
        if (!list_entry)
                return NULL;
        if (list_entry->list->unique && !list_entry->list->uptodate)
                return NULL;
        return list_entry->entries_next;
}

/**
 * udev_list_entry_get_by_name:
 * @list_entry: current entry
 * @name: name string to match
 *
 * Lookup an entry in the list with a certain name.
 *
 * Returns: udev_list_entry, #NULL if no matching entry is found.
 */
_public_ struct udev_list_entry *udev_list_entry_get_by_name(struct udev_list_entry *list_entry, const char *name) {
        if (!list_entry)
                return NULL;
        if (!list_entry->list->unique || !list_entry->list->uptodate)
                return NULL;
        return hashmap_get(list_entry->list->unique_entries, name);
}

/**
 * udev_list_entry_get_name:
 * @list_entry: current entry
 *
 * Get the name of a list entry.
 *
 * Returns: the name string of this entry.
 */
_public_ const char *udev_list_entry_get_name(struct udev_list_entry *list_entry) {
        if (!list_entry)
                return NULL;
        return list_entry->name;
}

/**
 * udev_list_entry_get_value:
 * @list_entry: current entry
 *
 * Get the value of list entry.
 *
 * Returns: the value string of this entry.
 */
_public_ const char *udev_list_entry_get_value(struct udev_list_entry *list_entry) {
        if (!list_entry)
                return NULL;
        return list_entry->value;
}