/* $Id$ */
/*-
 * Copyright (c) 2001 Anton Blanchard <anton@samba.org>
 * Copyright (c) 2005 Benedikt Meurer <benny@xfce.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * This file was originally part of the tdb library, which in turn is
 * part of the Samba suite, a Unix SMB/CIFS implementation.
 */

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

#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif
#ifdef HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif

#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#ifdef HAVE_SCHED_H
#include <sched.h>
#endif
#include <stdio.h>
#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#endif
#ifdef HAVE_TIME_H
#include <time.h>
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <tdb/spinlock.h>

#ifdef USE_SPINLOCKS

/*
 * ARCH SPECIFIC
 */

#if defined(SPARC_SPINLOCKS)

static inline int __spin_trylock(spinlock_t *lock)
{
	unsigned int result;

	asm volatile("ldstub    [%1], %0"
		: "=r" (result)
		: "r" (lock)
		: "memory");

	return (result == 0) ? 0 : EBUSY;
}

static inline void __spin_unlock(spinlock_t *lock)
{
	asm volatile("":::"memory");
	*lock = 0;
}

static inline void __spin_lock_init(spinlock_t *lock)
{
	*lock = 0;
}

static inline int __spin_is_locked(spinlock_t *lock)
{
	return (*lock != 0);
}

#elif defined(POWERPC_SPINLOCKS) 

static inline int __spin_trylock(spinlock_t *lock)
{
	unsigned int result;

	__asm__ __volatile__(
"1:	lwarx		%0,0,%1\n\
	cmpwi		0,%0,0\n\
	li		%0,0\n\
	bne-		2f\n\
	li		%0,1\n\
	stwcx.		%0,0,%1\n\
	bne-		1b\n\
	isync\n\
2:"	: "=&r"(result)
	: "r"(lock)
	: "cr0", "memory");

	return (result == 1) ? 0 : EBUSY;
}

static inline void __spin_unlock(spinlock_t *lock)
{
	asm volatile("eieio":::"memory");
	*lock = 0;
}

static inline void __spin_lock_init(spinlock_t *lock)
{
	*lock = 0;
}

static inline int __spin_is_locked(spinlock_t *lock)
{
	return (*lock != 0);
}

#elif defined(INTEL_SPINLOCKS) 

static inline int __spin_trylock(spinlock_t *lock)
{
	int oldval;

	asm volatile("xchgl %0,%1"
		: "=r" (oldval), "=m" (*lock)
		: "0" (0)
		: "memory");

	return oldval > 0 ? 0 : EBUSY;
}

static inline void __spin_unlock(spinlock_t *lock)
{
	asm volatile("":::"memory");
	*lock = 1;
}

static inline void __spin_lock_init(spinlock_t *lock)
{
	*lock = 1;
}

static inline int __spin_is_locked(spinlock_t *lock)
{
	return (*lock != 1);
}

#elif defined(MIPS_SPINLOCKS) && defined(sgi) && (_COMPILER_VERSION >= 730)

/* Implement spinlocks on IRIX using the MIPSPro atomic fetch operations. See
 * sync(3) for the details of the intrinsic operations.
 *
 * "sgi" and "_COMPILER_VERSION" are always defined by MIPSPro.
 */

#ifdef STANDALONE

/* MIPSPro 7.3 has "__inline" as an extension, but not "inline. */
#define inline __inline

#endif /* STANDALONE */

/* Returns 0 if the lock is acquired, EBUSY otherwise. */
static inline int __spin_trylock(spinlock_t *lock)
{
        unsigned int val;
        val = __lock_test_and_set(lock, 1);
        return val == 0 ? 0 : EBUSY;
}

static inline void __spin_unlock(spinlock_t *lock)
{
        __lock_release(lock);
}

static inline void __spin_lock_init(spinlock_t *lock)
{
        __lock_release(lock);
}

/* Returns 1 if the lock is held, 0 otherwise. */
static inline int __spin_is_locked(spinlock_t *lock)
{
        unsigned int val;
        val = __add_and_fetch(lock, 0);
	return val;
}

#elif defined(MIPS_SPINLOCKS) 

static inline unsigned int load_linked(unsigned long addr)
{
	unsigned int res;

	__asm__ __volatile__("ll\t%0,(%1)"
		: "=r" (res)
		: "r" (addr));

	return res;
}

static inline unsigned int store_conditional(unsigned long addr, unsigned int value)
{
	unsigned int res;

	__asm__ __volatile__("sc\t%0,(%2)"
		: "=r" (res)
		: "0" (value), "r" (addr));
	return res;
}

static inline int __spin_trylock(spinlock_t *lock)
{
	unsigned int mw;

	do {
		mw = load_linked(lock);
		if (mw) 
			return EBUSY;
	} while (!store_conditional(lock, 1));

	asm volatile("":::"memory");

	return 0;
}

static inline void __spin_unlock(spinlock_t *lock)
{
	asm volatile("":::"memory");
	*lock = 0;
}

static inline void __spin_lock_init(spinlock_t *lock)
{
	*lock = 0;
}

static inline int __spin_is_locked(spinlock_t *lock)
{
	return (*lock != 0);
}

#else
#error Need to implement spinlock code in spinlock.c
#endif

/*
 * OS SPECIFIC
 */

static void yield_cpu(void)
{
	struct timespec tm;

#ifdef HAVE_SCHED_YIELD
	sched_yield();
#else
	/* Linux will busy loop for delays < 2ms on real time tasks */
	tm.tv_sec = 0;
	tm.tv_nsec = 2000000L + 1;
	nanosleep(&tm, NULL);
#endif
}

static int this_is_smp(void)
{
#if defined(HAVE_SYSCONF) && defined(SYSCONF_SC_NPROC_ONLN)
        return (sysconf(_SC_NPROC_ONLN) > 1) ? 1 : 0;
#else
	return 0;
#endif
}

/*
 * GENERIC
 */

static int smp_machine = 0;

static inline void __spin_lock(spinlock_t *lock)
{
	int ntries = 0;

	while(__spin_trylock(lock)) {
		while(__spin_is_locked(lock)) {
			if (smp_machine && ntries++ < MAX_BUSY_LOOPS)
				continue;
			yield_cpu();
		}
	}
}

static void __read_lock(tdb_rwlock_t *rwlock)
{
	int ntries = 0;

	while(1) {
		__spin_lock(&rwlock->lock);

		if (!(rwlock->count & RWLOCK_BIAS)) {
			rwlock->count++;
			__spin_unlock(&rwlock->lock);
			return;
		}
	
		__spin_unlock(&rwlock->lock);

		while(rwlock->count & RWLOCK_BIAS) {
			if (smp_machine && ntries++ < MAX_BUSY_LOOPS)
				continue;
			yield_cpu();
		}
	}
}

static void __write_lock(tdb_rwlock_t *rwlock)
{
	int ntries = 0;

	while(1) {
		__spin_lock(&rwlock->lock);

		if (rwlock->count == 0) {
			rwlock->count |= RWLOCK_BIAS;
			__spin_unlock(&rwlock->lock);
			return;
		}

		__spin_unlock(&rwlock->lock);

		while(rwlock->count != 0) {
			if (smp_machine && ntries++ < MAX_BUSY_LOOPS)
				continue;
			yield_cpu();
		}
	}
}

static void __write_unlock(tdb_rwlock_t *rwlock)
{
	__spin_lock(&rwlock->lock);

#ifdef DEBUG
	if (!(rwlock->count & RWLOCK_BIAS))
		fprintf(stderr, "bug: write_unlock\n");
#endif

	rwlock->count &= ~RWLOCK_BIAS;
	__spin_unlock(&rwlock->lock);
}

static void __read_unlock(tdb_rwlock_t *rwlock)
{
	__spin_lock(&rwlock->lock);

#ifdef DEBUG
	if (!rwlock->count)
		fprintf(stderr, "bug: read_unlock\n");

	if (rwlock->count & RWLOCK_BIAS)
		fprintf(stderr, "bug: read_unlock\n");
#endif

	rwlock->count--;
	__spin_unlock(&rwlock->lock);
}

/* TDB SPECIFIC */

/* lock a list in the database. list -1 is the alloc list */
int tdb_spinlock(TDB_CONTEXT *tdb, int list, int rw_type)
{
	tdb_rwlock_t *rwlocks;

	if (!tdb->map_ptr) return -1;
	rwlocks = (tdb_rwlock_t *)((char *)tdb->map_ptr + tdb->header.rwlocks);

	switch(rw_type) {
	case F_RDLCK:
		__read_lock(&rwlocks[list+1]);
		break;

	case F_WRLCK:
		__write_lock(&rwlocks[list+1]);
		break;

	default:
		return TDB_ERRCODE(TDB_ERR_LOCK, -1);
	}
	return 0;
}

/* unlock the database. */
int tdb_spinunlock(TDB_CONTEXT *tdb, int list, int rw_type)
{
	tdb_rwlock_t *rwlocks;

	if (!tdb->map_ptr) return -1;
	rwlocks = (tdb_rwlock_t *)((char *)tdb->map_ptr + tdb->header.rwlocks);

	switch(rw_type) {
	case F_RDLCK:
		__read_unlock(&rwlocks[list+1]);
		break;

	case F_WRLCK:
		__write_unlock(&rwlocks[list+1]);
		break;

	default:
		return TDB_ERRCODE(TDB_ERR_LOCK, -1);
	}

	return 0;
}

int tdb_create_rwlocks(int fd, unsigned int hash_size)
{
	unsigned size, i;
	tdb_rwlock_t *rwlocks;

	size = TDB_SPINLOCK_SIZE(hash_size);
	rwlocks = malloc(size);
	if (!rwlocks)
		return -1;

	for(i = 0; i < hash_size+1; i++) {
		__spin_lock_init(&rwlocks[i].lock);
		rwlocks[i].count = 0;
	}

	/* Write it out (appending to end) */
	if (write(fd, rwlocks, size) != size) {
		free(rwlocks);
		return -1;
	}
	smp_machine = this_is_smp();
	free(rwlocks);
	return 0;
}

int tdb_clear_spinlocks(TDB_CONTEXT *tdb)
{
	tdb_rwlock_t *rwlocks;
	unsigned i;

	if (tdb->header.rwlocks == 0) return 0;
	if (!tdb->map_ptr) return -1;

	/* We're mmapped here */
	rwlocks = (tdb_rwlock_t *)((char *)tdb->map_ptr + tdb->header.rwlocks);
	for(i = 0; i < tdb->header.hash_size+1; i++) {
		__spin_lock_init(&rwlocks[i].lock);
		rwlocks[i].count = 0;
	}
	return 0;
}
#else
int tdb_create_rwlocks(int fd, unsigned int hash_size) { return 0; }
int tdb_spinlock(TDB_CONTEXT *tdb, int list, int rw_type) { return -1; }
int tdb_spinunlock(TDB_CONTEXT *tdb, int list, int rw_type) { return -1; }

/* Non-spinlock version: remove spinlock pointer */
int tdb_clear_spinlocks(TDB_CONTEXT *tdb)
{
	tdb_off off = (tdb_off)((char *)&tdb->header.rwlocks
				- (char *)&tdb->header);

	tdb->header.rwlocks = 0;
	if (lseek(tdb->fd, off, SEEK_SET) != off
	    || write(tdb->fd, (void *)&tdb->header.rwlocks,
		     sizeof(tdb->header.rwlocks)) 
	    != sizeof(tdb->header.rwlocks))
		return -1;
	return 0;
}
#endif
