#include "tdhkit.h"
#include "shsql.h"
#include <fcntl.h>
#include <unistd.h>  /* for usleep() and close() */
#define FILETAG "wlock"


/* A given process may lock only one data table file at a time.  This isn't enforced here however. */

static int Fd = -1;  			/* File descriptor of currently locked file */
static char Table[ MAXPATH ] = "";	/* Name of currently locked data table */
static int Nest = 0; 		/* Nested locks required only by sequences.c for internal uses.  
				   User applications use record locking. */

/* ============================== */
/* LOCK - lock a shsql data file for exclusive write access, by 
 * 	using fcntl() to set a write lock on a lock file.  
 *
 *	Returns 0 if successful.
 *	Returns 1 if lock could not be obtained after multiple attempts
 *	Returns 2 on an error.
 */

int
SHSQL_lock( table )
char *table;	/* a filename relative to datadir */
{
int i;
int stat;
char lockfilename[MAXPATH];
struct flock lk;
char fakename[MAXPATH];
	
if( table[0] == '\0' ) return( 1 ); /* degenerate case */


if( strcmp( table, Table )==0 ) {
	Nest++;
	if( SHSQL_debug ) fprintf( stderr, "[Nest %d lock call on %s]\n", Nest, table );
	return( 0 ); /* table has already been locked by same process - allow */
	}
else if( Table[0] != '\0' ) return( SHSQL_err( 260, "attempt to gain new write lock without releasing held write lock", table ));
	
strcpy( fakename, table );
for( i = 0; fakename[i] != '\0'; i++ ) if( fakename[i] == '/' ) fakename[i] = '!';
sprintf( lockfilename, "%s/locks/%s.%s", SHSQL_projdir, fakename, FILETAG );

lk.l_type = F_WRLCK;
lk.l_start = 0L;
lk.l_whence = SEEK_SET;
lk.l_len = 0; /* zero means "to eof" */

Fd = open( lockfilename, O_WRONLY | O_CREAT | O_APPEND, 00666 );
if( Fd < 0 ) {
	Fd = -1;
	return( 2 ); /* can't open */
	}

for( i = 0; i < SHSQL_writelock_ntries; i++ ) {
	stat = fcntl( Fd, F_SETLK, &lk ); /* set write lock on the lock file */
	if( stat == 0 ) break;  /* got the lock */

	/* otherwise we didn't get it, wait a bit then try again */
	if( i < 3 ) usleep( 100000 );  /* 1/10th of a second */
	else if( i < 6 ) usleep( 200000 );  /* 2/10ths of a second */
	else usleep( 900000 ); /* 9/10 second */
	}

if( i >= SHSQL_writelock_ntries ) {
	close( Fd );
	return( 1 ); /* file locked by someone else and unavailable */
	}

strcpy( Table, table );
return( 0 );	     /* success */
}
		

/* ============================== */
/* SHSQL_UNLOCK - unlock the most recently locked file. */

int
SHSQL_unlock()
{

if( Nest ) {
	if( SHSQL_debug ) fprintf( stderr, "[Nest %d unlock call on %s]\n", Nest, Table );
	Nest--;
	return( 0 );
	}

if( Fd == -1 ) { strcpy( Table, ""); return( 0 ); } /* already was unlocked.. ok */

close( Fd ); /* kernel guarantees that file will still exist until closed, Stevens 4.15 */
Fd = -1;
strcpy( Table, "" );
return( 0 );
}



/* =========================================== */
/* READLOCK - Set, clear, or check a read lock.
		 A read lock causes all other processes to wait for access to the table.

		 Hence, read locks should be held for as short a time as possible, and
		 limited to administrative (etc.) use.

		 For 'set' and 'clear' modes this returns 0 on success; or an error code.
		 For 'try' mode this returns 0 if lock clear or 1 if lock set.
 */

int
SHSQL_readlock( tablename, mode )
char *tablename;
int mode;	/* 1 = set    0 = clear   2 = try */
{
int i;
char fakename[MAXPATH], lockfile[MAXPATH];
FILE *fp;


strcpy( fakename, tablename );
for( i = 0; fakename[i] != '\0'; i++ ) if( fakename[i] == '/' ) fakename[i] = '!';
sprintf( lockfile, "%s/locks/%s.readlock", SHSQL_projdir, fakename );

if( mode == 0 ) {
	unlink( lockfile );
	return( 0 );
	}

if( mode == 1 ) usleep( 500000 ); /* wait a bit for any in-progress reads to get through.. */

fp = fopen( lockfile, "r" );
if( fp != NULL ) {
	fclose( fp );
	if( mode == 2 ) return( 1 );
	return( SHSQL_err( 280, "table already locked against reads .. cannot proceed\n", tablename ) );
 	}

if( mode == 2 ) return( 0 );

fp = fopen( lockfile, "w" );
if( fp == NULL ) return( SHSQL_err( 281, "cannot create read lock file\n", lockfile )); 
		
fprintf( fp, "%d\n", (int) getpid() );
fclose( fp );
return( 0 );
}


#ifdef TESTING

/* to test, compile cc tablelock.c then run a bunch of a.outs in the background concurrently */
/* ================================== */
char SHSQL_projdir[MAXPATH] = "./";
int SHSQL_debug = 1;

main()
{
int d, stat;


stat = SHSQL_lock( "locktest" );
if( stat != 0 ) { printf( "%05d: unable to get access (%d)\n", (int) getpid(), stat ); exit(1); }
printf( "%05d: Got access.\n", (int) getpid() ); fflush( stdout );
usleep( 150000 );
stat = SHSQL_unlock();
printf( "%05d: Released.\n", (int) getpid() ); fflush( stdout );
exit( 0 );
}

SHSQL_err( n, msg, parm )
int n;
char *msg, *parm;
{
fprintf( stderr, "ERROR %d: %s %s\n", n, msg, parm );
return( n );
}
#endif
