/** BEGIN COPYRIGHT BLOCK
 * Copyright (C) 2001 Sun Microsystems, Inc. Used by permission.
 * Copyright (C) 2005 Red Hat, Inc.
 * All rights reserved.
 *
 * License: GPL (version 3 or any later version).
 * See LICENSE for details.
 * END COPYRIGHT BLOCK **/

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif


#include "slapi-plugin.h"
#include "slapi-private.h"

/*
 * The "wrapper" placed around objects in our object set.
 */
typedef struct objset_object
{
    Object *obj;                /* pointer to actual object */
    struct objset_object *next; /* pointer to next object in list */
} objset_object;

/*
 * The object set itself.
 */
typedef struct objset
{
    objset_object *head; /* pointer to linked list of objects */
    objset_object *tail; /* pointer to tail of linked list */
    PRLock *lock;        /* Lock - protects addition/deletion from list */
    FNFree destructor;   /* Destructor callback for objset itself */
} objset;

/* Forward declarations */
static void unlinkObjsetObjectNoLock(Objset *o, objset_object *obj_to_unlink);


/*
 * Create a new, empty object set.
 * Returns a pointer to the new object set, or NULL if an error occurred.
 */
Objset *
objset_new(FNFree objset_destructor)
{
    objset *set;

    set = (objset *)slapi_ch_malloc(sizeof(objset));
    set->lock = PR_NewLock();
    if (NULL == set->lock) {
        slapi_ch_free((void **)&set);
    } else {
        set->head = set->tail = NULL;
        set->destructor = objset_destructor;
    }
    return set;
}


/*
 * Delete an object set. All references to contained objects
 * are released, and the objset is deleted.
 */
void
objset_delete(Objset **setp)
{
    objset_object *o, *o_next;
    Objset *set;

    PR_ASSERT(NULL != setp);
    set = *setp;
    PR_ASSERT(NULL != set);
    PR_Lock(set->lock);
    o = set->head;
    while (NULL != o) {
        o_next = o->next;
        object_release(o->obj);     /* release our reference */
        slapi_ch_free((void **)&o); /* Free wrapper */
        o = o_next;
    }
    PR_Unlock(set->lock);
    PR_DestroyLock(set->lock);
    if (NULL != set->destructor) {
        set->destructor((void **)setp);
    }
    slapi_ch_free((void **)setp);
}


/*
 * Add a new object to an object set.
 * Return values:
 * OBJSET_SUCCESS: the insertion was succesful.
 * OBJSET_ALREADY_EXISTS: the object already exists in the set.
 */
int
objset_add_obj(Objset *set, Object *object)
{
    objset_object *p;
    int exists = 0;
    int rc = OBJSET_SUCCESS;

    PR_ASSERT(NULL != set);
    PR_ASSERT(NULL != object);

    PR_Lock(set->lock);
    /* Make sure this object isn't already in the set */
    p = set->head;
    while (NULL != p) {
        if (p->obj == object) {
            exists = 1;
            break;
        }
        p = p->next;
    }
    if (exists) {
        rc = OBJSET_ALREADY_EXISTS;
    } else {
        objset_object *new_node = (objset_object *)slapi_ch_malloc(sizeof(objset_object));
        object_acquire(object); /* Record our reference */
        new_node->obj = object;
        new_node->next = NULL;

        if (NULL == set->head) {
            set->head = set->tail = new_node;
        } else {
            set->tail->next = new_node;
            set->tail = new_node;
        }
    }
    PR_Unlock(set->lock);
    return rc;
}


/*
 * Locate an object in an object set.
 *
 * Arguments:
 * set: the object set to search
 * compare_fn: a caller-provided function used to compare the
 *             name against an object. This function should return
 *             a negtive value, zero, or a positive value if the
 *             object's name is, respectively, less that, equal
 *             to, or greater than the provided name.
 * name: the name (value) to find.
 *
 * The returned object, if any, is referenced. The caller must
 * call object_release() when finished with the object.
 *
 * Returns the object, if found. Otherwise, returns NULL.
 * Implementation note: since we store objects in an unordered
 * linked list, all that's really important is that the compare_fn
 * return 0 when a match is found, and non-zero otherwise.
 * Other types of implementations might try to optimize the
 * storage, e.g. binary search.
 */
Object *
objset_find(Objset *set, CMPFn compare_fn, const void *name)
{
    objset_object *found = NULL;

    PR_ASSERT(NULL != set);
    PR_ASSERT(NULL != name);
    PR_ASSERT(NULL != compare_fn);

    PR_Lock(set->lock);
    found = set->head;
    while (NULL != found) {
        if (compare_fn(found->obj, name) == 0) {
            break;
        }
        found = found->next;
    }
    if (NULL != found) {
        /* acquire object */
        object_acquire(found->obj);
    }
    PR_Unlock(set->lock);
    return found == NULL ? NULL : found->obj;
}


/*
 * Remove an object from an objset.
 * Returns OBJSET_SUCCESS if the object was found and removed, or
 * OBJSET_NO_SUCH_OBJECT if the object was not found in the list.
 */
int
objset_remove_obj(Objset *set, Object *object)
{
    int rc = OBJSET_SUCCESS;
    objset_object *found;

    PR_ASSERT(NULL != set);
    PR_ASSERT(NULL != object);

    PR_Lock(set->lock);
    found = set->head;
    while (NULL != found) {
        if (found->obj == object) {
            break;
        }
        found = found->next;
    }
    if (NULL == found) {
        rc = OBJSET_NO_SUCH_OBJECT;
    } else {
        Object *saved = found->obj;

        /* Unlink from list */
        unlinkObjsetObjectNoLock(set, found);

        /* Release reference on object */
        object_release(saved);
    }
    PR_Unlock(set->lock);
    return rc;
}


/*
 * Prepare for iteration across an object set. Returns the first
 * object in the set. The returned object is referenced, therefore
 * the caller must either release the object, or
 * pass it to an objset_next_obj call, which will
 * implicitly release the object.
 * Returns the first object, or NULL if the objset contains no
 * objects.
 */
Object *
objset_first_obj(Objset *set)
{
    Object *return_object;

    /* Be tolerant (for the replication plugin) */
    if (set == NULL)
        return NULL;

    PR_Lock(set->lock);
    if (NULL == set->head) {
        return_object = NULL;
    } else {
        object_acquire(set->head->obj);
        return_object = set->head->obj;
    }
    PR_Unlock(set->lock);
    return return_object;
}


/*
 * Return the next object in an object set, or NULL if there are
 * no more objects.
 * The returned object is referenced, therefore
 * the caller must either release the object, or
 * pass it to an objset_next_ob call, which will
 * implicitly release the object.
 */
Object *
objset_next_obj(Objset *set, Object *previous)
{
    Object *return_object = NULL;
    objset_object *p;

    PR_ASSERT(NULL != set);
    PR_Lock(set->lock);

    /* First, find the current object */
    p = set->head;
    while (NULL != p && p->obj != previous) {
        p = p->next;
    }
    /* Find the next object */
    if (NULL != p && NULL != p->next) {
        return_object = p->next->obj;
        object_acquire(return_object);
    }
    PR_Unlock(set->lock);
    object_release(previous); /* Release the previous object */
    return return_object;
}


/*
 * Return a non-zero value if the object set is empty, or
 * zero if the object set is empty.
 */
int
objset_is_empty(Objset *set)
{
    int return_value;

    PR_ASSERT(NULL != set);

    PR_Lock(set->lock);
    return_value = (set->head == NULL);
    PR_Unlock(set->lock);
    return return_value;
}


/*
 * Return the count of objects in the object set.
 */
int
objset_size(Objset *set)
{
    int count = 0;
    objset_object *p;

    PR_ASSERT(NULL != set);
    PR_Lock(set->lock);
    for (p = set->head; p; p = p->next)
        count++;
    PR_Unlock(set->lock);
    return count;
}


/*
 * Utility function: remove object from list.
 * This needs to be called with the objset locked.
 */
static void
unlinkObjsetObjectNoLock(Objset *o, objset_object *obj_to_unlink)
{

    objset_object *p = o->head;

    PR_ASSERT(NULL != o->head);
    /* Unlink from list */
    if (o->head == obj_to_unlink) {
        /* Object to unlink was at head of list */
        p = o->head->next;
        o->head = obj_to_unlink->next;
    } else {
        while (NULL != p->next && p->next != obj_to_unlink) {
            p = p->next;
        }
        if (NULL != p->next) {
            /* p points to object prior to one being removed */
            p->next = p->next->next;
        }
    }
    if (o->tail == obj_to_unlink) {
        o->tail = p;
    }

    /* Free the wrapper */
    slapi_ch_free((void **)&obj_to_unlink);
}
