/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/*
 * list.c
 *
 * This contains the implementation of NSS's thread-safe linked list.
 */

#ifndef BASE_H
#include "base.h"
#endif /* BASE_H */

struct nssListElementStr {
    PRCList link;
    void *data;
};

typedef struct nssListElementStr nssListElement;

struct nssListStr {
    NSSArena *arena;
    PRLock *lock;
    nssListElement *head;
    PRUint32 count;
    nssListCompareFunc compareFunc;
    nssListSortFunc sortFunc;
    PRBool i_alloced_arena;
};

struct nssListIteratorStr {
    PRLock *lock;
    nssList *list;
    nssListElement *current;
};

#define NSSLIST_LOCK_IF(list) \
    if ((list)->lock)         \
    PR_Lock((list)->lock)

#define NSSLIST_UNLOCK_IF(list) \
    if ((list)->lock)           \
    PR_Unlock((list)->lock)

static PRBool
pointer_compare(void *a, void *b)
{
    return (PRBool)(a == b);
}

static nssListElement *
nsslist_get_matching_element(nssList *list, void *data)
{
    nssListElement *node;
    node = list->head;
    if (!node) {
        return NULL;
    }
    while (node) {
        /* using a callback slows things down when it's just compare ... */
        if (list->compareFunc(node->data, data)) {
            break;
        }
        if (&node->link == PR_LIST_TAIL(&list->head->link)) {
            node = NULL;
            break;
        }
        node = (nssListElement *)PR_NEXT_LINK(&node->link);
    }
    return node;
}

NSS_IMPLEMENT nssList *
nssList_Create(NSSArena *arenaOpt, PRBool threadSafe)
{
    NSSArena *arena;
    nssList *list;
    PRBool i_alloced;
    if (arenaOpt) {
        arena = arenaOpt;
        i_alloced = PR_FALSE;
    } else {
        arena = nssArena_Create();
        i_alloced = PR_TRUE;
    }
    if (!arena) {
        return (nssList *)NULL;
    }
    list = nss_ZNEW(arena, nssList);
    if (!list) {
        if (!arenaOpt) {
            NSSArena_Destroy(arena);
        }
        return (nssList *)NULL;
    }
    if (threadSafe) {
        list->lock = PR_NewLock();
        if (!list->lock) {
            if (arenaOpt) {
                nss_ZFreeIf(list);
            } else {
                NSSArena_Destroy(arena);
            }
            return (nssList *)NULL;
        }
    }
    list->arena = arena;
    list->i_alloced_arena = i_alloced;
    list->compareFunc = pointer_compare;
    return list;
}

NSS_IMPLEMENT PRStatus
nssList_Destroy(nssList *list)
{
    if (!list) {
        return PR_SUCCESS;
    }
    if (!list->i_alloced_arena) {
        nssList_Clear(list, NULL);
    }
    if (list->lock) {
        (void)PR_DestroyLock(list->lock);
    }
    if (list->i_alloced_arena) {
        NSSArena_Destroy(list->arena);
        list = NULL;
    }
    nss_ZFreeIf(list);
    return PR_SUCCESS;
}

NSS_IMPLEMENT void
nssList_SetCompareFunction(nssList *list, nssListCompareFunc compareFunc)
{
    list->compareFunc = compareFunc;
}

NSS_IMPLEMENT void
nssList_SetSortFunction(nssList *list, nssListSortFunc sortFunc)
{
    /* XXX if list already has elements, sort them */
    list->sortFunc = sortFunc;
}

NSS_IMPLEMENT nssListCompareFunc
nssList_GetCompareFunction(nssList *list)
{
    return list->compareFunc;
}

NSS_IMPLEMENT void
nssList_Clear(nssList *list, nssListElementDestructorFunc destructor)
{
    PRCList *link;
    nssListElement *node, *tmp;
    if (!list) {
        return;
    }
    NSSLIST_LOCK_IF(list);
    node = list->head;
    list->head = NULL;
    while (node && list->count > 0) {
        if (destructor)
            (*destructor)(node->data);
        link = &node->link;
        tmp = (nssListElement *)PR_NEXT_LINK(link);
        PR_REMOVE_LINK(link);
        nss_ZFreeIf(node);
        node = tmp;
        --list->count;
    }
    NSSLIST_UNLOCK_IF(list);
}

static PRStatus
nsslist_add_element(nssList *list, void *data)
{
    nssListElement *node = nss_ZNEW(list->arena, nssListElement);
    if (!node) {
        return PR_FAILURE;
    }
    PR_INIT_CLIST(&node->link);
    node->data = data;
    if (list->head) {
        if (list->sortFunc) {
            PRCList *link;
            nssListElement *currNode;
            currNode = list->head;
            /* insert in ordered list */
            while (currNode) {
                link = &currNode->link;
                if (list->sortFunc(data, currNode->data) <= 0) {
                    /* new element goes before current node */
                    PR_INSERT_BEFORE(&node->link, link);
                    /* reset head if this is first */
                    if (currNode == list->head)
                        list->head = node;
                    break;
                }
                if (link == PR_LIST_TAIL(&list->head->link)) {
                    /* reached end of list, append */
                    PR_INSERT_AFTER(&node->link, link);
                    break;
                }
                currNode = (nssListElement *)PR_NEXT_LINK(&currNode->link);
            }
        } else {
            /* not sorting */
            PR_APPEND_LINK(&node->link, &list->head->link);
        }
    } else {
        list->head = node;
    }
    ++list->count;
    return PR_SUCCESS;
}

NSS_IMPLEMENT PRStatus
nssList_Add(nssList *list, void *data)
{
    NSSLIST_LOCK_IF(list);
    (void)nsslist_add_element(list, data);
    NSSLIST_UNLOCK_IF(list);
    return PR_SUCCESS;
}

NSS_IMPLEMENT PRStatus
nssList_AddUnique(nssList *list, void *data)
{
    PRStatus nssrv;
    nssListElement *node;
    NSSLIST_LOCK_IF(list);
    node = nsslist_get_matching_element(list, data);
    if (node) {
        /* already in, finish */
        NSSLIST_UNLOCK_IF(list);
        return PR_SUCCESS;
    }
    nssrv = nsslist_add_element(list, data);
    NSSLIST_UNLOCK_IF(list);
    return nssrv;
}

NSS_IMPLEMENT PRStatus
nssList_Remove(nssList *list, void *data)
{
    nssListElement *node;
    NSSLIST_LOCK_IF(list);
    node = nsslist_get_matching_element(list, data);
    if (node) {
        if (node == list->head) {
            list->head = (nssListElement *)PR_NEXT_LINK(&node->link);
        }
        PR_REMOVE_LINK(&node->link);
        nss_ZFreeIf(node);
        if (--list->count == 0) {
            list->head = NULL;
        }
    }
    NSSLIST_UNLOCK_IF(list);
    return PR_SUCCESS;
}

NSS_IMPLEMENT void *
nssList_Get(nssList *list, void *data)
{
    nssListElement *node;
    NSSLIST_LOCK_IF(list);
    node = nsslist_get_matching_element(list, data);
    NSSLIST_UNLOCK_IF(list);
    return (node) ? node->data : NULL;
}

NSS_IMPLEMENT PRUint32
nssList_Count(nssList *list)
{
    return list->count;
}

NSS_IMPLEMENT PRStatus
nssList_GetArray(nssList *list, void **rvArray, PRUint32 maxElements)
{
    nssListElement *node;
    PRUint32 i = 0;
    PR_ASSERT(maxElements > 0);
    node = list->head;
    if (!node) {
        return PR_SUCCESS;
    }
    NSSLIST_LOCK_IF(list);
    while (node) {
        rvArray[i++] = node->data;
        if (i == maxElements)
            break;
        node = (nssListElement *)PR_NEXT_LINK(&node->link);
        if (node == list->head) {
            break;
        }
    }
    NSSLIST_UNLOCK_IF(list);
    return PR_SUCCESS;
}

NSS_IMPLEMENT nssList *
nssList_Clone(nssList *list)
{
    nssList *rvList;
    nssListElement *node;
    rvList = nssList_Create(NULL, (list->lock != NULL));
    if (!rvList) {
        return NULL;
    }
    NSSLIST_LOCK_IF(list);
    if (list->count > 0) {
        node = list->head;
        while (PR_TRUE) {
            nssList_Add(rvList, node->data);
            node = (nssListElement *)PR_NEXT_LINK(&node->link);
            if (node == list->head) {
                break;
            }
        }
    }
    NSSLIST_UNLOCK_IF(list);
    return rvList;
}

NSS_IMPLEMENT nssListIterator *
nssList_CreateIterator(nssList *list)
{
    nssListIterator *rvIterator;
    rvIterator = nss_ZNEW(NULL, nssListIterator);
    if (!rvIterator) {
        return NULL;
    }
    rvIterator->list = nssList_Clone(list);
    if (!rvIterator->list) {
        nss_ZFreeIf(rvIterator);
        return NULL;
    }
    rvIterator->current = rvIterator->list->head;
    if (list->lock) {
        rvIterator->lock = PR_NewLock();
        if (!rvIterator->lock) {
            nssList_Destroy(rvIterator->list);
            nss_ZFreeIf(rvIterator);
            rvIterator = NULL;
        }
    }
    return rvIterator;
}

NSS_IMPLEMENT void
nssListIterator_Destroy(nssListIterator *iter)
{
    if (iter->lock) {
        (void)PR_DestroyLock(iter->lock);
    }
    if (iter->list) {
        nssList_Destroy(iter->list);
    }
    nss_ZFreeIf(iter);
}

NSS_IMPLEMENT void *
nssListIterator_Start(nssListIterator *iter)
{
    NSSLIST_LOCK_IF(iter);
    if (iter->list->count == 0) {
        return NULL;
    }
    iter->current = iter->list->head;
    return iter->current->data;
}

NSS_IMPLEMENT void *
nssListIterator_Next(nssListIterator *iter)
{
    nssListElement *node;
    PRCList *link;
    if (iter->list->count == 1 || iter->current == NULL) {
        /* Reached the end of the list.  Don't change the state, force to
         * user to call nssList_Finish to clean up.
         */
        return NULL;
    }
    node = (nssListElement *)PR_NEXT_LINK(&iter->current->link);
    link = &node->link;
    if (link == PR_LIST_TAIL(&iter->list->head->link)) {
        /* Signal the end of the list. */
        iter->current = NULL;
        return node->data;
    }
    iter->current = node;
    return node->data;
}

NSS_IMPLEMENT PRStatus
nssListIterator_Finish(nssListIterator *iter)
{
    iter->current = iter->list->head;
    return (iter->lock) ? PR_Unlock(iter->lock) : PR_SUCCESS;
}
