/*
 * Copyright (C) 2015 Greenstone Digital Libraries, University of Waikato
 * $Id$
 *
 *  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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <jni.h>
/** JNI Primitive Data-types to keep in mind:
 *  jbyte => signed 8 bits
 *  jchar => unsigned 16 bits
 *  jint  => signed 32 bits
 *  jlong => signed 64 bits
 *  jsize => jint
 */

#include "tdb.h"
/** TDB Data-structures to keep in mind:
 *
 *  typedef struct TDB_DATA {
 * 	unsigned char *dptr;
 *	size_t dsize;
 *  } TDB_DATA;
 *
 *  where:
 *  size_t => unsigned 32 bits
 */

/* Cookie for you if you can figure out from the above two comments where I
 * will spend a nightmarish amount of time trying to get the code to work. */

#include "org_greenstone_tdbjava_TDBJava.h"

#ifdef DEBUG
#define ASSERT(x) if (!(x)) { \
  fprintf(stderr, "%s:%d: assertion failed\n", __FILE__, __LINE__); \
  abort(); }
#else /* !DEBUG */
#define ASSERT(x)
#endif /* !DEBUG */

/* The Java class within which the native methods are declared */
#define JAVA_CLASS "org.greenstone.tdbjava.TDBJava"

/** While tdb.h uses a non-pointer TDB_CONTEXT, I'll add this to make the
 * code transported from javaGDBM more compatible. */
typedef struct tdb_context *TDB_FILE;

/** These defined functions come straight from javagdbm. */
#define TDB_EXCEPTION(env) tdbException(env, __FILE__, __LINE__)
#define NULL_PTR_EXCEPTION(env) nullPtrException(env, __FILE__, __LINE__)
#define CHECK_NOT_NULL(ptr, env) if (!ptr) { NULL_PTR_EXCEPTION(env); return 0; }
#define CHECK_NOT_NULL_VOID(ptr, env) if (!ptr) { NULL_PTR_EXCEPTION(env); return; }

/* Convert between a jlong and a void ptr using a well-defined cast.
 * (Casting between a pointer and an integer of different sizes spooks
 * both gcc and mbp. */
#if (SIZEOF_VOID_P == SIZEOF_LONG)
#  define DBF_TO_JLONG(x) ((jlong)((long) x))
#  define JLONG_TO_DBF(x) ((TDB_CONTEXT *)((long) x))
#elif (SIZEOF_VOID_P == SIZEOF_INT)
#  define DBF_TO_JLONG(x) ((jlong)((int) (x)))
#  define JLONG_TO_DBF(x) ((TDB_CONTEXT *)((int) (x)))
#else
#  define DBF_TO_JLONG(x) ((jlong)(x))
#  define JLONG_TO_DBF(x) ((TDB_CONTEXT *)(x))
#endif


/** ===== Prototypes ===== **/
/* Can't go in header as it is autogenerated. */
void tdbException(JNIEnv *env, const char *file, int line);
void nullPtrException(JNIEnv *env, const char *file, int line);
void releaseArray(JNIEnv *env, jbyteArray array, TDB_DATA *fromDatum);
void releaseArrayAbort(JNIEnv *env, jbyteArray array, TDB_DATA *fromDatum);
int arrayToDatum(JNIEnv *env, jbyteArray fromArray, TDB_DATA *toDatum);
jbyteArray datumToArray(JNIEnv *env, TDB_DATA *fromDatum);

/** Define a successful TDB action to be one whose error code is TDB_SUCCESS or
 *  TDB_ERR_NOEXIST (for actions with the potential to try and access records
 *  that aren't there, such as tdb_exist() and tdb_store()).
 */
bool tdbCheckSuccess(int ecode)
{
  /* We are successful but only for certain definitions of success :P */
  return (TDB_SUCCESS == ecode || TDB_ERR_NOEXIST == ecode);
}
/** tdbCheckSuccess(int) **/


/*
 * Class:     org_greenstone_tdbjava_TDBJava
 * Method:    tdbGetVersion
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_org_greenstone_tdbjava_TDBJava_tdbGetVersion(JNIEnv *env,
						  jclass cls)
{
  return (*env)->NewStringUTF(env, "TDB 1.3.7");
}
/** tdbGetVersion(JNIEnv *, jclass) => jstring **/


/*
 * Class:     org_greenstone_tdbjava_TDBJava
 * Method:    tdbWrapperVersion
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_org_greenstone_tdbjava_TDBJava_tdbWrapperVersion(JNIEnv *env,
						      jclass cls)
{
    return (*env)->NewStringUTF(env, "TDBJava built " __DATE__);
}
/** tdbWrapperVersion(JNIEnv *, jclass) => jstring **/


/*
 * Class:     org_greenstone_tdbjava_TDBJava
 * Method:    tdbOpen
 * Signature: (Ljava/lang/String;II)J
 */
JNIEXPORT jlong JNICALL
Java_org_greenstone_tdbjava_TDBJava_tdbOpen(JNIEnv *env,
					    jobject obj,
					    jstring file_name,
					    jint tdb_flags,
					    jint open_flags)
{
  TDB_CONTEXT *dbf;
  const char *utf_file_name;
  int fcntl_flags = O_RDWR | O_CREAT;
  utf_file_name = (*env)->GetStringUTFChars(env, file_name, 0);
  if (!utf_file_name) {
    return 0;
  }
  if (open_flags == 1) {
    fcntl_flags = O_RDONLY;
  }
  setbuf(stderr, 0);
  dbf = tdb_open((char *) utf_file_name, 0, tdb_flags, fcntl_flags, 0664);
  (*env)->ReleaseStringUTFChars(env, file_name, utf_file_name);
  // notably, the follow exception is the only time TDB_ERR_NOEXIST actually
  // matters
  if (!dbf) {
    TDB_EXCEPTION(env);
    return 0;
  }
  return DBF_TO_JLONG(dbf);
}
/** tdbOpen(JNIEnv *, jobject, jstring, jint, jint) => jlong **/


/*
 * Class:     org_greenstone_tdbjava_TDBJava
 * Method:    tdbClose
 * Signature: (J)V
 */
JNIEXPORT void JNICALL
Java_org_greenstone_tdbjava_TDBJava_tdbClose(JNIEnv *env,
					     jobject obj,
					     jlong dbf)
{
  int ecode;
  CHECK_NOT_NULL_VOID(dbf, env);
  ecode = tdb_close(JLONG_TO_DBF(dbf));
  if (TDB_SUCCESS != ecode) {
    TDB_EXCEPTION(env);
  }
}
/** tdbClose(JNIEnv *, jobject, jlong) => void **/


/*
 * Class:     org_greenstone_tdbjava_TDBJava
 * Method:    tdbStore
 * Signature: (J[B[BZ)V
 */
JNIEXPORT void JNICALL
Java_org_greenstone_tdbjava_TDBJava_tdbStore(JNIEnv *env,
					     jobject obj,
					     jlong dbf,
					     jbyteArray key_array,
					     jbyteArray value_array,
					     jboolean replace)
{
  TDB_DATA key_datum;
  TDB_DATA value_datum;
  int ecode;
  CHECK_NOT_NULL_VOID(dbf, env);
  if (!arrayToDatum(env, key_array, &key_datum)) {
    NULL_PTR_EXCEPTION(env);
    return;
  }
  if (!arrayToDatum(env, value_array, &value_datum)) {
    NULL_PTR_EXCEPTION(env);
    return;
  }
  ecode = tdb_store(JLONG_TO_DBF(dbf), key_datum, value_datum, TDB_REPLACE);
  releaseArrayAbort(env, key_array, &key_datum);
  releaseArrayAbort(env, value_array, &value_datum);
  if (!tdbCheckSuccess(ecode)) {
    TDB_EXCEPTION(env);
  }
}
/** tdbStore(JNIEnv *, jobject, jlong, jbyteArray, jbyteArray, jboolean) => void **/


/*
 * Class:     org_greenstone_tdbjava_TDBJava
 * Method:    tdbFetch
 * Signature: (J[B)[B
 */
JNIEXPORT jbyteArray JNICALL
Java_org_greenstone_tdbjava_TDBJava_tdbFetch(JNIEnv *env,
					     jobject this,
					     jlong dbf,
					     jbyteArray key_array)
{
    TDB_DATA	key_datum;
    TDB_DATA 	value_datum;
    jbyteArray	value_array;
    int ecode;
    CHECK_NOT_NULL(dbf, env);
    if (!arrayToDatum(env, key_array, &key_datum)) {
      NULL_PTR_EXCEPTION(env);
      return 0;
    }
    value_datum = tdb_fetch(JLONG_TO_DBF(dbf), key_datum);
    releaseArrayAbort(env, key_array, &key_datum);
    ecode = tdb_error(JLONG_TO_DBF(dbf));
    if (!tdbCheckSuccess(ecode)) {
      TDB_EXCEPTION(env);
      return 0;
    }
    // no such key case
    if (!value_datum.dptr) {
      return 0;
    }
    value_array = datumToArray(env, &value_datum);
    free(value_datum.dptr);
    return value_array;
}
/** tdbFetch(JNIEnv *, jobject, jlong, jbyteArray) => jbyteArray **/


/*
 * Class:     org_greenstone_tdbjava_TDBJava
 * Method:    tdbExists
 * Signature: (J[B)Z
 */
JNIEXPORT jboolean JNICALL
Java_org_greenstone_tdbjava_TDBJava_tdbExists(JNIEnv *env,
					      jobject obj,
					      jlong dbf,
					      jbyteArray key_array)
{
    TDB_DATA key_datum;
    int	ecode, result;
    CHECK_NOT_NULL(dbf, env);
    if (!arrayToDatum(env, key_array, &key_datum)) {
      NULL_PTR_EXCEPTION(env);
      return JNI_FALSE;
    }
    result = tdb_exists(JLONG_TO_DBF(dbf), key_datum);
    // Gah - tdb_exists can set TDB_ERR_NOEXIST even if it works properly
    ecode = tdb_error(JLONG_TO_DBF(dbf));
    if (!tdbCheckSuccess(ecode)) {
      TDB_EXCEPTION(env);
      return 0;
    }
    releaseArrayAbort(env, key_array, &key_datum);
    return result ? JNI_TRUE : JNI_FALSE;
}
/** tdbExists(JNIEnv *, jobject, jlong, jbyteArray) => jboolean **/


/*
 * Class:     org_greenstone_tdbjava_TDBJava
 * Method:    tdbDelete
 * Signature: (J[B)V
 */
JNIEXPORT void JNICALL
Java_org_greenstone_tdbjava_TDBJava_tdbDelete(JNIEnv *env,
					      jobject obj,
					      jlong dbf,
					      jbyteArray key_array)
{
    TDB_DATA key_datum;
    int ecode;
    CHECK_NOT_NULL_VOID(dbf, env);
    if (!arrayToDatum(env, key_array, &key_datum)) {
      NULL_PTR_EXCEPTION(env);
      return;
    }
    tdb_delete(JLONG_TO_DBF(dbf), key_datum);
    releaseArrayAbort(env, key_array, &key_datum);
    // just to be different, while tdb_delete does return a result value it
    // doesn't reflect any error state (just whether a key/value was found and
    // deleted or not) leaving us to once again get tdb_error()
    ecode = tdb_error(JLONG_TO_DBF(dbf));
    if (!tdbCheckSuccess(ecode)) {
      TDB_EXCEPTION(env);
    }
}
/** tdbDelete(JNIEnv *, jobject, jlong, jbyteArray) => void **/

/*
 * Class:     org_greenstone_tdbjava_TDBJava
 * Method:    tdbFirstKey
 * Signature: (J)[B
 */
JNIEXPORT jbyteArray JNICALL
Java_org_greenstone_tdbjava_TDBJava_tdbFirstKey(JNIEnv *env,
						jobject obj,
						jlong dbf)
{
    TDB_DATA key_datum;
    jbyteArray key_array;
    int ecode;
    CHECK_NOT_NULL(dbf, env);
    key_datum = tdb_firstkey(JLONG_TO_DBF(dbf));
    ecode = tdb_error(JLONG_TO_DBF(dbf));
    if (!tdbCheckSuccess(ecode)) {
      TDB_EXCEPTION(env);
      return 0;
    }
    // no such key case
    if (!key_datum.dptr) {
      return 0;
    }
    key_array = datumToArray(env, &key_datum);
    free(key_datum.dptr);
    return key_array;
}
/** tdbFirstKey(JNIEnv *, jobject, jlong) => jbyteArray **/


/*
 * Class:     org_greenstone_tdbjava_TDBJava
 * Method:    tdbNextKey
 * Signature: (J[B)[B
 */
JNIEXPORT jbyteArray JNICALL
Java_org_greenstone_tdbjava_TDBJava_tdbNextKey(JNIEnv *env,
					       jobject this,
					       jlong dbf,
					       jbyteArray key_array)
{
  TDB_DATA key_datum;
  TDB_DATA next_datum;
  jbyteArray next_array;
  int ecode;
  CHECK_NOT_NULL(dbf, env);
  if (!arrayToDatum(env, key_array, &key_datum)) {
    NULL_PTR_EXCEPTION(env);
    return 0;
  }
  next_datum = tdb_nextkey(JLONG_TO_DBF(dbf), key_datum);
  releaseArrayAbort(env, key_array, &key_datum);
  ecode = tdb_error(JLONG_TO_DBF(dbf));
  if (!tdbCheckSuccess(ecode)) {
    TDB_EXCEPTION(env);
    return 0;
  }
  // no such key case
  if (!next_datum.dptr) {
    return 0;
  }
  next_array = datumToArray(env, &next_datum);
  free(next_datum.dptr);
  return next_array;
}
/** tdbNextKey(JNIEnv *, jobject, jlong, jbyteArray) => jbyteArray **/


/******************************************************************************
 * Following are support functions which aid in interfacing C to Java.
 ******************************************************************************/

/** Create a new Java byte array from a TDB_Data, and return a
 * pointer thereto.  */
jbyteArray datumToArray(JNIEnv *env,
			TDB_DATA *from_datum)
{
  jbyte *jbyte_dptr;
  jbyteArray to_array = 0;
  jsize length;
  if (from_datum->dsize <= 0 || !from_datum->dptr) {
    TDB_EXCEPTION(env);
    return 0;
  }
  to_array = (*env)->NewByteArray(env, from_datum->dsize);
  ASSERT(to_array);
  // We won't be able to meaningfully cast an unsigned int data length larger
  // than the limit of signed integers due to JNI limitations. I bite my thumb
  // at thee, signedness
  if (from_datum->dsize > INT_MAX) {
    TDB_EXCEPTION(env);
    return 0;
  }
  length = (signed int) from_datum->dsize;
  // the following dptr cast *should* be safe as the data arrived in the dptr
  // from a jbyteArray anyway
  jbyte_dptr = (jbyte*) from_datum->dptr;
  (*env)->SetByteArrayRegion(env, to_array, 0, length, jbyte_dptr);
  return to_array;
}
/** datumToArray(JNIEnv *, TDB_DATA) **/


/** Convert a Java byte array to a TDB_DATA object.
 *
 * The Java array is pinned or copied for use in the datum, and must
 * be released after use by releaseBytes.
 *
 * Returns true if the array is non-null and could be pinned.  Otherwise,
 * returns false.
 */
int arrayToDatum(JNIEnv *env,
		 jbyteArray from_array,
		 TDB_DATA *to_datum)
{
  int result = 0;
  jbyte *jbyte_dptr;
  if (from_array) {
    jbyte_dptr = (*env)->GetByteArrayElements(env, from_array, 0);
    // This cast should always be safe as the jbytes are 0-127 (never negative)
    // and the dptr supports values in the range 0-255.
    to_datum->dptr = (unsigned char*) jbyte_dptr;
    jsize data_length = (*env)->GetArrayLength(env, from_array);
    // Excluding the unlikely case of negative lengths, the dsize cast should
    // be okay.
    if (data_length > 0) {
      to_datum->dsize = (unsigned int) data_length;
      result = 1;
    }
  }
  return result;
}
/** arrayToDatum(JNIEnv *, jbyteArray, TDB_DATA *) **/


/** Release a byte array pinned or copied for use in a datum. */
void releaseArray(JNIEnv *env,
		  jbyteArray array,
		  TDB_DATA *from_datum)
{
  jbyte *jbyte_dptr;
  ASSERT(from_datum->dptr);
  // @see datumToArray() for cast justification
  jbyte_dptr = (jbyte *) from_datum->dptr;
  (*env)->ReleaseByteArrayElements(env, array, jbyte_dptr, 0);
  from_datum->dptr = 0;	/* no longer valid */
}
/** releaseArray(JNIEnv *, jbyteArray, TDB_DATA) **/


/** Release a byte array pinned or copied for use in a datum, aborting
 * any changes.  This potentially saves the runtime from having to
 * copy back an unchanged array. */
void releaseArrayAbort(JNIEnv *env,
		       jbyteArray array,
		       TDB_DATA *from_datum)
{
  jbyte *jbyte_dptr;
  ASSERT(from_datum->dptr);
  // @see datumToArray() for cast justification
  jbyte_dptr = (jbyte *) from_datum->dptr;
  (*env)->ReleaseByteArrayElements(env, array, jbyte_dptr, JNI_ABORT);
  from_datum->dptr = 0;	/* no longer valid */
}
/** releaseArrayAbort(JNIEnv *, jbyteArray, TDB_DATA) **/


/** Throw a null pointer exception.
 */
void nullPtrException(JNIEnv *env,
		      const char *file,
		      int line)
{
  jclass exception_class;
  char reason[1024];
  sprintf(reason, "Null pointer exception in TDBJava (%s:%d)", file, line);
  exception_class = (*env)->FindClass(env, "java/lang/NullPointerException");
  ASSERT(exClass);
  (*env)->ThrowNew(env, exception_class, reason);
}
/** nullPtrException(JNIEnv *, const char *, int) **/


/** Translate the TDB error into a Java exception and throw same.
 */
void tdbException(JNIEnv *env,
		  const char *file,
		  int line)
{
  jclass exception_class;
  static char reason[1500];
  static char src_location[500];
  exception_class = (*env)->FindClass(env, "org/greenstone/tdbjava/TDBJavaException");
  ASSERT(exception_class);
  strncpy(reason, strerror(errno), 500);
  sprintf(src_location, " (%s:%d)", file, line);
  strncat(reason, src_location, 495);
  errno = TDB_SUCCESS;	/* this one has been handled */
  (*env)->ThrowNew(env, exception_class, reason);
}
/** tdbException(JNIEnv *, const char *, int) **/
