/* 
 * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
 * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 */
#include <pj/lock.h>
#include <pj/os.h>
#include <pj/assert.h>
#include <pj/log.h>
#include <pj/pool.h>
#include <pj/string.h>
#include <pj/errno.h>

#define THIS_FILE       "lock.c"

typedef void LOCK_OBJ;

/*
 * Lock structure.
 */
struct pj_lock_t
{
    LOCK_OBJ *lock_object;

    pj_status_t (*acquire)      (LOCK_OBJ*);
    pj_status_t (*tryacquire)   (LOCK_OBJ*);
    pj_status_t (*release)      (LOCK_OBJ*);
    pj_status_t (*destroy)      (LOCK_OBJ*);
};

typedef pj_status_t (*FPTR)(LOCK_OBJ*);

/******************************************************************************
 * Implementation of lock object with mutex.
 */
static pj_lock_t mutex_lock_template = 
{
    NULL,
    (FPTR) &pj_mutex_lock,
    (FPTR) &pj_mutex_trylock,
    (FPTR) &pj_mutex_unlock,
    (FPTR) &pj_mutex_destroy
};

static pj_status_t create_mutex_lock( pj_pool_t *pool,
                                      const char *name,
                                      int type,
                                      pj_lock_t **lock )
{
    pj_lock_t *p_lock;
    pj_mutex_t *mutex;
    pj_status_t rc;

    PJ_ASSERT_RETURN(pool && lock, PJ_EINVAL);

    p_lock = PJ_POOL_ALLOC_T(pool, pj_lock_t);
    if (!p_lock)
        return PJ_ENOMEM;

    pj_memcpy(p_lock, &mutex_lock_template, sizeof(pj_lock_t));
    rc = pj_mutex_create(pool, name, type, &mutex);
    if (rc != PJ_SUCCESS)
        return rc;

    p_lock->lock_object = mutex;
    *lock = p_lock;
    return PJ_SUCCESS;
}


PJ_DEF(pj_status_t) pj_lock_create_simple_mutex( pj_pool_t *pool,
                                                 const char *name,
                                                 pj_lock_t **lock )
{
    return create_mutex_lock(pool, name, PJ_MUTEX_SIMPLE, lock);
}

PJ_DEF(pj_status_t) pj_lock_create_recursive_mutex( pj_pool_t *pool,
                                                    const char *name,
                                                    pj_lock_t **lock )
{
    return create_mutex_lock(pool, name, PJ_MUTEX_RECURSE, lock);
}


/******************************************************************************
 * Implementation of NULL lock object.
 */
static pj_status_t null_op(void *arg)
{
    PJ_UNUSED_ARG(arg);
    return PJ_SUCCESS;
}

static pj_lock_t null_lock_template = 
{
    NULL,
    &null_op,
    &null_op,
    &null_op,
    &null_op
};

PJ_DEF(pj_status_t) pj_lock_create_null_mutex( pj_pool_t *pool,
                                               const char *name,
                                               pj_lock_t **lock )
{
    PJ_UNUSED_ARG(name);
    PJ_UNUSED_ARG(pool);

    PJ_ASSERT_RETURN(lock, PJ_EINVAL);

    *lock = &null_lock_template;
    return PJ_SUCCESS;
}


/******************************************************************************
 * Implementation of semaphore lock object.
 */
#if defined(PJ_HAS_SEMAPHORE) && PJ_HAS_SEMAPHORE != 0

static pj_lock_t sem_lock_template = 
{
    NULL,
    (FPTR) &pj_sem_wait,
    (FPTR) &pj_sem_trywait,
    (FPTR) &pj_sem_post,
    (FPTR) &pj_sem_destroy
};

PJ_DEF(pj_status_t) pj_lock_create_semaphore(  pj_pool_t *pool,
                                               const char *name,
                                               unsigned initial,
                                               unsigned max,
                                               pj_lock_t **lock )
{
    pj_lock_t *p_lock;
    pj_sem_t *sem;
    pj_status_t rc;

    PJ_ASSERT_RETURN(pool && lock, PJ_EINVAL);

    p_lock = PJ_POOL_ALLOC_T(pool, pj_lock_t);
    if (!p_lock)
        return PJ_ENOMEM;

    pj_memcpy(p_lock, &sem_lock_template, sizeof(pj_lock_t));
    rc = pj_sem_create( pool, name, initial, max, &sem);
    if (rc != PJ_SUCCESS)
        return rc;

    p_lock->lock_object = sem;
    *lock = p_lock;

    return PJ_SUCCESS;
}


#endif  /* PJ_HAS_SEMAPHORE */


PJ_DEF(pj_status_t) pj_lock_acquire( pj_lock_t *lock )
{
    PJ_ASSERT_RETURN(lock != NULL, PJ_EINVAL);
    return (*lock->acquire)(lock->lock_object);
}

PJ_DEF(pj_status_t) pj_lock_tryacquire( pj_lock_t *lock )
{
    PJ_ASSERT_RETURN(lock != NULL, PJ_EINVAL);
    return (*lock->tryacquire)(lock->lock_object);
}

PJ_DEF(pj_status_t) pj_lock_release( pj_lock_t *lock )
{
    PJ_ASSERT_RETURN(lock != NULL, PJ_EINVAL);
    return (*lock->release)(lock->lock_object);
}

PJ_DEF(pj_status_t) pj_lock_destroy( pj_lock_t *lock )
{
    PJ_ASSERT_RETURN(lock != NULL, PJ_EINVAL);
    return (*lock->destroy)(lock->lock_object);
}


/******************************************************************************
 * Group lock
 */

/* Individual lock in the group lock */
typedef struct grp_lock_item
{
    PJ_DECL_LIST_MEMBER(struct grp_lock_item);
    int          prio;
    pj_lock_t   *lock;

} grp_lock_item;

/* Destroy callbacks */
typedef struct grp_destroy_callback
{
    PJ_DECL_LIST_MEMBER(struct grp_destroy_callback);
    void        *comp;
    void        (*handler)(void*);
} grp_destroy_callback;

#if PJ_GRP_LOCK_DEBUG
/* Store each add_ref caller */
typedef struct grp_lock_ref
{
    PJ_DECL_LIST_MEMBER(struct grp_lock_ref);
    const char  *file;
    int          line;
} grp_lock_ref;
#endif

/* The group lock */
struct pj_grp_lock_t
{
    pj_lock_t            base;

    pj_pool_t           *pool;
    pj_atomic_t         *ref_cnt;
    pj_lock_t           *own_lock;

    pj_thread_t         *owner;
    int                  owner_cnt;

    grp_lock_item        lock_list;
    grp_destroy_callback destroy_list;

#if PJ_GRP_LOCK_DEBUG
    grp_lock_ref         ref_list;
    grp_lock_ref         ref_free_list;
#endif
};


PJ_DEF(void) pj_grp_lock_config_default(pj_grp_lock_config *cfg)
{
    pj_bzero(cfg, sizeof(*cfg));
}

static void grp_lock_set_owner_thread(pj_grp_lock_t *glock)
{
    if (!glock->owner) {
#if PJ_HAS_THREADS
        glock->owner = pj_thread_this();
#else
        glock->owner = (pj_thread_t *) -1;
#endif
        glock->owner_cnt = 1;
    } else {
#if PJ_HAS_THREADS
        pj_assert(glock->owner == pj_thread_this());
#endif
        glock->owner_cnt++;
    }
}

static void grp_lock_unset_owner_thread(pj_grp_lock_t *glock)
{
#if PJ_HAS_THREADS
    pj_assert(glock->owner == pj_thread_this());
#endif
    pj_assert(glock->owner_cnt > 0);
    if (--glock->owner_cnt <= 0) {
        glock->owner = NULL;
        glock->owner_cnt = 0;
    }
}

static pj_status_t grp_lock_acquire(LOCK_OBJ *p)
{
    pj_grp_lock_t *glock = (pj_grp_lock_t*)p;
    grp_lock_item *lck;

    pj_assert(pj_atomic_get(glock->ref_cnt) > 0);

    lck = glock->lock_list.next;
    while (lck != &glock->lock_list) {
        pj_lock_acquire(lck->lock);
        lck = lck->next;
    }
    grp_lock_set_owner_thread(glock);
    pj_grp_lock_add_ref(glock);
    return PJ_SUCCESS;
}

static pj_status_t grp_lock_tryacquire(LOCK_OBJ *p)
{
    pj_grp_lock_t *glock = (pj_grp_lock_t*)p;
    grp_lock_item *lck;

    pj_assert(pj_atomic_get(glock->ref_cnt) > 0);

    lck = glock->lock_list.next;
    while (lck != &glock->lock_list) {
        pj_status_t status = pj_lock_tryacquire(lck->lock);
        if (status != PJ_SUCCESS) {
            lck = lck->prev;
            while (lck != &glock->lock_list) {
                pj_lock_release(lck->lock);
                lck = lck->prev;
            }
            return status;
        }
        lck = lck->next;
    }
    grp_lock_set_owner_thread(glock);
    pj_grp_lock_add_ref(glock);
    return PJ_SUCCESS;
}

static pj_status_t grp_lock_release(LOCK_OBJ *p)
{
    pj_grp_lock_t *glock = (pj_grp_lock_t*)p;
    grp_lock_item *lck;

    grp_lock_unset_owner_thread(glock);

    lck = glock->lock_list.prev;
    while (lck != &glock->lock_list) {
        pj_lock_release(lck->lock);
        lck = lck->prev;
    }
    return pj_grp_lock_dec_ref(glock);
}

static pj_status_t grp_lock_add_handler( pj_grp_lock_t *glock,
                                         pj_pool_t *pool,
                                         void *comp,
                                         void (*destroy)(void *comp),
                                         pj_bool_t acquire_lock)
{
    grp_destroy_callback *cb;

    if (acquire_lock)
        grp_lock_acquire(glock);

    if (pool == NULL)
        pool = glock->pool;

    cb = PJ_POOL_ZALLOC_T(pool, grp_destroy_callback);
    cb->comp = comp;
    cb->handler = destroy;
    pj_list_push_back(&glock->destroy_list, cb);

    if (acquire_lock)
        grp_lock_release(glock);

    return PJ_SUCCESS;
}

static pj_status_t grp_lock_destroy(LOCK_OBJ *p)
{
    pj_grp_lock_t *glock = (pj_grp_lock_t*)p;
    pj_pool_t *pool = glock->pool;
    grp_lock_item *lck;
    grp_destroy_callback *cb;

    if (!glock->pool) {
        /* already destroyed?! */
        return PJ_EINVAL;
    }

    /* Release all chained locks */
    lck = glock->lock_list.next;
    while (lck != &glock->lock_list) {
        if (lck->lock != glock->own_lock) {
            int i;
            for (i=0; i<glock->owner_cnt; ++i)
                pj_lock_release(lck->lock);
        }
        lck = lck->next;
    }

    /* Call callbacks */
    cb = glock->destroy_list.next;
    while (cb != &glock->destroy_list) {
        grp_destroy_callback *next = cb->next;
        cb->handler(cb->comp);
        cb = next;
    }

    pj_lock_destroy(glock->own_lock);
    pj_atomic_destroy(glock->ref_cnt);
    glock->pool = NULL;
    pj_pool_release(pool);

    return PJ_SUCCESS;
}


PJ_DEF(pj_status_t) pj_grp_lock_create( pj_pool_t *pool,
                                        const pj_grp_lock_config *cfg,
                                        pj_grp_lock_t **p_grp_lock)
{
    pj_grp_lock_t *glock;
    grp_lock_item *own_lock;
    pj_status_t status;

    PJ_ASSERT_RETURN(pool && p_grp_lock, PJ_EINVAL);

    PJ_UNUSED_ARG(cfg);

    pool = pj_pool_create(pool->factory, "glck%p", 512, 512, NULL);
    if (!pool)
        return PJ_ENOMEM;

    glock = PJ_POOL_ZALLOC_T(pool, pj_grp_lock_t);
    glock->base.lock_object = glock;
    glock->base.acquire = &grp_lock_acquire;
    glock->base.tryacquire = &grp_lock_tryacquire;
    glock->base.release = &grp_lock_release;
    glock->base.destroy = &grp_lock_destroy;

    glock->pool = pool;
    pj_list_init(&glock->lock_list);
    pj_list_init(&glock->destroy_list);
#if PJ_GRP_LOCK_DEBUG
    pj_list_init(&glock->ref_list);
    pj_list_init(&glock->ref_free_list);
#endif

    status = pj_atomic_create(pool, 0, &glock->ref_cnt);
    if (status != PJ_SUCCESS)
        goto on_error;

    status = pj_lock_create_recursive_mutex(pool, pool->obj_name,
                                            &glock->own_lock);
    if (status != PJ_SUCCESS)
        goto on_error;

    own_lock = PJ_POOL_ZALLOC_T(pool, grp_lock_item);
    own_lock->lock = glock->own_lock;
    pj_list_push_back(&glock->lock_list, own_lock);

    *p_grp_lock = glock;
    return PJ_SUCCESS;

on_error:
    grp_lock_destroy(glock);
    return status;
}

PJ_DEF(pj_status_t) pj_grp_lock_create_w_handler( pj_pool_t *pool,
                                                  const pj_grp_lock_config *cfg,
                                                  void *member,
                                                  void (*handler)(void *member),
                                                  pj_grp_lock_t **p_grp_lock)
{
    pj_status_t status;

    status = pj_grp_lock_create(pool, cfg, p_grp_lock);
    if (status == PJ_SUCCESS) {
        pj_pool_t *pool = (*p_grp_lock)->pool;
        grp_lock_add_handler(*p_grp_lock, pool, member, handler, PJ_FALSE);
    }
    
    return status;
}

PJ_DEF(pj_status_t) pj_grp_lock_destroy( pj_grp_lock_t *grp_lock)
{
    return grp_lock_destroy(grp_lock);
}

PJ_DEF(pj_status_t) pj_grp_lock_acquire( pj_grp_lock_t *grp_lock)
{
    return grp_lock_acquire(grp_lock);
}

PJ_DEF(pj_status_t) pj_grp_lock_tryacquire( pj_grp_lock_t *grp_lock)
{
    return grp_lock_tryacquire(grp_lock);
}

PJ_DEF(pj_status_t) pj_grp_lock_release( pj_grp_lock_t *grp_lock)
{
    return grp_lock_release(grp_lock);
}

PJ_DEF(pj_status_t) pj_grp_lock_replace( pj_grp_lock_t *old_lock,
                                         pj_grp_lock_t *new_lock)
{
    grp_destroy_callback *ocb;

    /* Move handlers from old to new */
    ocb = old_lock->destroy_list.next;
    while (ocb != &old_lock->destroy_list) {
        grp_destroy_callback *ncb;

        ncb = PJ_POOL_ALLOC_T(new_lock->pool, grp_destroy_callback);
        ncb->comp = ocb->comp;
        ncb->handler = ocb->handler;
        pj_list_push_back(&new_lock->destroy_list, ncb);

        ocb = ocb->next;
    }

    pj_list_init(&old_lock->destroy_list);

    grp_lock_destroy(old_lock);
    return PJ_SUCCESS;
}

PJ_DEF(pj_status_t) pj_grp_lock_add_handler( pj_grp_lock_t *glock,
                                             pj_pool_t *pool,
                                             void *comp,
                                             void (*destroy)(void *comp))
{
    return grp_lock_add_handler(glock, pool, comp, destroy, PJ_TRUE);
}

PJ_DEF(pj_status_t) pj_grp_lock_del_handler( pj_grp_lock_t *glock,
                                             void *comp,
                                             void (*destroy)(void *comp))
{
    grp_destroy_callback *cb;

    grp_lock_acquire(glock);

    cb = glock->destroy_list.next;
    while (cb != &glock->destroy_list) {
        if (cb->comp == comp && cb->handler == destroy)
            break;
        cb = cb->next;
    }

    if (cb != &glock->destroy_list)
        pj_list_erase(cb);

    grp_lock_release(glock);
    return PJ_SUCCESS;
}

static pj_status_t grp_lock_add_ref(pj_grp_lock_t *glock)
{
    pj_atomic_inc(glock->ref_cnt);
    return PJ_SUCCESS;
}

static pj_status_t grp_lock_dec_ref(pj_grp_lock_t *glock)
{
    int cnt; /* for debugging */
    if ((cnt=pj_atomic_dec_and_get(glock->ref_cnt)) == 0) {
        grp_lock_destroy(glock);
        return PJ_EGONE;
    }
    pj_assert(cnt > 0);
    return PJ_SUCCESS;
}

#if PJ_GRP_LOCK_DEBUG
static pj_status_t grp_lock_dec_ref_dump(pj_grp_lock_t *glock)
{
    pj_status_t status;

    status = grp_lock_dec_ref(glock);
    if (status == PJ_SUCCESS) {
        pj_grp_lock_dump(glock);
    } else if (status == PJ_EGONE) {
        PJ_LOG(4,(THIS_FILE, "Group lock %p destroyed.", glock));
    }

    return status;
}

PJ_DEF(pj_status_t) pj_grp_lock_add_ref_dbg(pj_grp_lock_t *glock,
                                            const char *file,
                                            int line)
{
    grp_lock_ref *ref;
    pj_status_t status;

    pj_enter_critical_section();
    if (!pj_list_empty(&glock->ref_free_list)) {
        ref = glock->ref_free_list.next;
        pj_list_erase(ref);
    } else {
        ref = PJ_POOL_ALLOC_T(glock->pool, grp_lock_ref);
    }

    ref->file = file;
    ref->line = line;
    pj_list_push_back(&glock->ref_list, ref);

    pj_leave_critical_section();

    status = grp_lock_add_ref(glock);

    if (status != PJ_SUCCESS) {
        pj_enter_critical_section();
        pj_list_erase(ref);
        pj_list_push_back(&glock->ref_free_list, ref);
        pj_leave_critical_section();
    }

    return status;
}

PJ_DEF(pj_status_t) pj_grp_lock_dec_ref_dbg(pj_grp_lock_t *glock,
                                            const char *file,
                                            int line)
{
    grp_lock_ref *ref;

    PJ_UNUSED_ARG(line);

    pj_enter_critical_section();
    /* Find the same source file */
    ref = glock->ref_list.next;
    while (ref != &glock->ref_list) {
        if (strcmp(ref->file, file) == 0) {
            pj_list_erase(ref);
            pj_list_push_back(&glock->ref_free_list, ref);
            break;
        }
        ref = ref->next;
    }
    pj_leave_critical_section();

    if (ref == &glock->ref_list) {
        PJ_LOG(2,(THIS_FILE, "pj_grp_lock_dec_ref_dbg() could not find "
                              "matching ref for %s", file));
    }

    return grp_lock_dec_ref_dump(glock);
}
#else
PJ_DEF(pj_status_t) pj_grp_lock_add_ref(pj_grp_lock_t *glock)
{
    return grp_lock_add_ref(glock);
}

PJ_DEF(pj_status_t) pj_grp_lock_dec_ref(pj_grp_lock_t *glock)
{
    return grp_lock_dec_ref(glock);
}
#endif

PJ_DEF(int) pj_grp_lock_get_ref(pj_grp_lock_t *glock)
{
    return pj_atomic_get(glock->ref_cnt);
}

PJ_DEF(pj_status_t) pj_grp_lock_chain_lock( pj_grp_lock_t *glock,
                                            pj_lock_t *lock,
                                            int pos)
{
    grp_lock_item *lck, *new_lck;
    int i;

    grp_lock_acquire(glock);

    for (i=0; i<glock->owner_cnt; ++i)
        pj_lock_acquire(lock);

    lck = glock->lock_list.next;
    while (lck != &glock->lock_list) {
        if (lck->prio >= pos)
            break;
        lck = lck->next;
    }

    new_lck = PJ_POOL_ZALLOC_T(glock->pool, grp_lock_item);
    new_lck->prio = pos;
    new_lck->lock = lock;
    pj_list_insert_before(lck, new_lck);

    /* this will also release the new lock */
    grp_lock_release(glock);
    return PJ_SUCCESS;
}

PJ_DEF(pj_status_t) pj_grp_lock_unchain_lock( pj_grp_lock_t *glock,
                                              pj_lock_t *lock)
{
    grp_lock_item *lck;

    grp_lock_acquire(glock);

    lck = glock->lock_list.next;
    while (lck != &glock->lock_list) {
        if (lck->lock == lock)
            break;
        lck = lck->next;
    }

    if (lck != &glock->lock_list) {
        int i;

        pj_list_erase(lck);
        for (i=0; i<glock->owner_cnt; ++i)
            pj_lock_release(lck->lock);
    }

    grp_lock_release(glock);
    return PJ_SUCCESS;
}

PJ_DEF(void) pj_grp_lock_dump(pj_grp_lock_t *grp_lock)
{
#if PJ_GRP_LOCK_DEBUG
    grp_lock_ref *ref;
    char info_buf[1000];
    pj_str_t info;

    info.ptr = info_buf;
    info.slen = 0;

    grp_lock_add_ref(grp_lock);
    pj_enter_critical_section();

    ref = grp_lock->ref_list.next;
    while (ref != &grp_lock->ref_list && info.slen < sizeof(info_buf)) {
        char *start = info.ptr + info.slen;
        int max_len = sizeof(info_buf) - info.slen;
        int len;

        len = pj_ansi_snprintf(start, max_len, "\t%s:%d\n", ref->file, ref->line);
        if (len < 1 || len >= max_len) {
            len = strlen(ref->file);
            if (len > max_len - 1)
                len = max_len - 1;

            memcpy(start, ref->file, len);
            start[len++] = '\n';
        }

        info.slen += len;

        ref = ref->next;
    }

    if (ref != &grp_lock->ref_list) {
        int i;
        for (i=0; i<4; ++i)
            info_buf[sizeof(info_buf)-i-1] = '.';
    }
    info.ptr[info.slen-1] = '\0';

    pj_leave_critical_section();

    PJ_LOG(4,(THIS_FILE, "Group lock %p, ref_cnt=%d. Reference holders:\n%s",
               grp_lock, pj_grp_lock_get_ref(grp_lock)-1, info.ptr));

    grp_lock_dec_ref(grp_lock);
#else
    PJ_LOG(4,(THIS_FILE, "Group lock %p, ref_cnt=%d.",
               grp_lock, pj_grp_lock_get_ref(grp_lock)));
#endif
}
