#include <string.h>
#include <jni.h>
#include "org_clearsilver_HDF.h"

#include "cs_config.h"
#include "util/neo_err.h"
#include "util/neo_misc.h"
#include "util/neo_str.h"
#include "util/neo_hdf.h"
#include "cgi/cgi.h"
#include "cgi/cgiwrap.h"
#include "cgi/date.h"
#include "cgi/html.h"

#include "j_neo_util.h"

void throwException(JNIEnv *env, const char* class_name, const char *message) {
  jclass ex_class = (*env)->FindClass(env, class_name);
  if (ex_class == NULL) {
    // Unable to find proper class!
    return;
  }
  (*env)->ThrowNew(env, ex_class, message);
}

void throwNullPointerException(JNIEnv *env, const char *message) {
  throwException(env, "java/lang/NullPointerException", message);
}

void throwRuntimeException(JNIEnv *env, const char *message) {
  throwException(env, "java/lang/RuntimeException", message);
}

void throwIOException(JNIEnv *env, const char *message) {
  throwException(env, "java/io/IOException", message);
}

void throwFileNotFoundException(JNIEnv *env, const char *message) {
  throwException(env, "java/io/FileNotFoundException", message);
}

void throwOutOfMemoryError(JNIEnv *env, const char *message) {
  throwException(env, "java/lang/OutOfMemoryError", message);
}

// Throws a runtime exception back to the Java VM appropriate for the type of
// error and frees the NEOERR that is passed in.
// TODO: throw more specific exceptions for errors like NERR_IO and NERR_NOMEM
int jNeoErr(JNIEnv *env, NEOERR *err) {
  STRING str;

  string_init(&str);
  if (nerr_match(err, NERR_PARSE)) {
    nerr_error_string(err, &str);
    throwRuntimeException(env, str.buf);
  } else if (nerr_match(err, NERR_IO)) {
    nerr_error_string(err, &str);
    throwIOException(env, str.buf);
  } else if (nerr_match(err, NERR_NOMEM)) {
    nerr_error_string(err, &str);
    throwOutOfMemoryError(env, str.buf);
  } else {
    nerr_error_traceback(err, &str);
    throwRuntimeException(env, str.buf);
  }

  nerr_ignore(&err);  // free err, otherwise it would leak
  string_clear(&str);

  return 0;
}

JNIEXPORT jlong JNICALL Java_org_clearsilver_HDF__1init(
    JNIEnv *env, jclass objClass) {
  HDF *hdf = NULL;
  NEOERR *err;

  err = hdf_init(&hdf);
  if (err != STATUS_OK) {
    return jNeoErr(env, err);
  }
  return (jlong) hdf;
}

JNIEXPORT void JNICALL Java_org_clearsilver_HDF__1dealloc(
    JNIEnv *env, jclass objClass, jlong hdf_obj_ptr) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  hdf_destroy(&hdf);
}

JNIEXPORT jint JNICALL Java_org_clearsilver_HDF__1getIntValue(
    JNIEnv *env, jclass objClass, jlong hdf_obj_ptr, jstring j_hdfname,
     jint default_value) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  int r;
  const char *hdfname;

  if (!j_hdfname) {
    throwNullPointerException(env, "hdfname argument was null");
    return 0;
  }

  hdfname = (*env)->GetStringUTFChars(env,j_hdfname, 0);

  r = hdf_get_int_value(hdf, hdfname, default_value);

  (*env)->ReleaseStringUTFChars(env,j_hdfname,hdfname);
  return r;
}

JNIEXPORT jstring JNICALL Java_org_clearsilver_HDF__1getValue(
    JNIEnv *env, jclass objClass, jlong hdf_obj_ptr, jstring j_hdfname,
    jstring j_default_value) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  const char *r;
  const char *hdfname;
  const char *default_value;
  jstring retval;

  if (!j_hdfname) {
    throwNullPointerException(env, "hdfname argument was null");
    return 0;
  }
  hdfname = (*env)->GetStringUTFChars(env,j_hdfname,0);
  if (!j_default_value) {
    default_value = NULL;
  } else {
    default_value = (*env)->GetStringUTFChars(env, j_default_value, 0);
  }

  r = hdf_get_value(hdf, hdfname, default_value);

  (*env)->ReleaseStringUTFChars(env, j_hdfname, hdfname);
  retval = (r ? (*env)->NewStringUTF(env, r) : 0);
  if (default_value) {
    (*env)->ReleaseStringUTFChars(env, j_default_value, default_value);
  }
  return retval;
}

JNIEXPORT void JNICALL Java_org_clearsilver_HDF__1setValue(
    JNIEnv *env, jclass objClass,
    jlong hdf_obj_ptr, jstring j_hdfname, jstring j_value) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  NEOERR *err;
  const char *hdfname;
  const char *value;

  if (!j_hdfname) {
    throwNullPointerException(env, "hdfname argument was null");
    return;
  }
  hdfname = (*env)->GetStringUTFChars(env, j_hdfname, 0);
  if (j_value) {
    value = (*env)->GetStringUTFChars(env, j_value, 0);
  } else {
    value = NULL;
  }
  err = hdf_set_value(hdf, hdfname, value);

  (*env)->ReleaseStringUTFChars(env, j_hdfname, hdfname);
  if (value) {
    (*env)->ReleaseStringUTFChars(env, j_value, value);
  }

  if (err != STATUS_OK) {
    // Throw an exception
    jNeoErr(env, err);
  }
}

JNIEXPORT void JNICALL Java_org_clearsilver_HDF__1removeTree(
    JNIEnv *env, jclass objClass,
    jlong hdf_obj_ptr, jstring j_hdfname) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  NEOERR *err;
  const char *hdfname;

  if (!j_hdfname) {
    throwNullPointerException(env, "hdfname argument was null");
    return;
  }
  hdfname = (*env)->GetStringUTFChars(env, j_hdfname, 0);
  err = hdf_remove_tree(hdf, hdfname);

  (*env)->ReleaseStringUTFChars(env, j_hdfname, hdfname);

  if (err != STATUS_OK) {
    // Throw an exception
    jNeoErr(env, err);
  }
}

JNIEXPORT void JNICALL Java_org_clearsilver_HDF__1setSymLink(
    JNIEnv *env, jclass objClass,
    jlong hdf_obj_ptr, jstring j_hdf_name_src, jstring j_hdf_name_dest) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  NEOERR *err;
  const char *hdf_name_src;
  const char *hdf_name_dest;

  if (!j_hdf_name_src) {
    throwNullPointerException(env, "hdf_name_src argument was null");
    return;
  }
  hdf_name_src = (*env)->GetStringUTFChars(env, j_hdf_name_src, 0);

  if (!j_hdf_name_dest) {
    throwNullPointerException(env, "hdf_name_dest argument was null");
    return;
  }
  hdf_name_dest = (*env)->GetStringUTFChars(env, j_hdf_name_dest, 0);

  err = hdf_set_symlink(hdf, hdf_name_src, hdf_name_dest);

  (*env)->ReleaseStringUTFChars(env, j_hdf_name_src, hdf_name_src);
  (*env)->ReleaseStringUTFChars(env, j_hdf_name_dest, hdf_name_dest);

  if (err != STATUS_OK) {
    // Throw an exception
    jNeoErr(env, err);
  }
}

JNIEXPORT jstring JNICALL Java_org_clearsilver_HDF__1dump(
    JNIEnv *env, jclass objClass,
    jlong hdf_obj_ptr) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  NEOERR *err;
  STRING str;
  jstring retval;

  string_init(&str);
  err = hdf_dump_str(hdf, NULL, 0, &str);
  if (err != STATUS_OK) {
    // Throw an exception
    jNeoErr(env, err);
    retval = NULL;
  } else {
    retval =  (*env)->NewStringUTF(env,str.buf);
  }
  string_clear(&str);

  return retval;
}

NEOERR *jni_fileload_cb(void *ctx, HDF *hdf, const char *filename,
    char **contents) {
  FILELOAD_INFO *info = (FILELOAD_INFO *)ctx;
  jstring filename_str;
  jstring loaded_string;
  const char *c_loaded_string;

  // We assume that the hdf passed back is actually the hdf we already
  // have...
  if (hdf != info->hdf)
    return nerr_raise(NERR_ASSERT,
        "jni_fileload_cb: passed HDF pointer doesn't match hdf_obj_ptr");

  filename_str = (*info->env)->NewStringUTF(info->env, filename);
  (*info->env)->NewLocalRef(info->env, filename_str);

  loaded_string = (*info->env)->CallObjectMethod(info->env, info->fl_obj,
      info->fl_method, filename_str);
  if ((*info->env)->ExceptionCheck(info->env)) {
    (*info->env)->ExceptionDescribe(info->env);
    (*info->env)->ExceptionClear(info->env);
    return nerr_raise(NERR_ASSERT,
        "jni_fileload_cb: HDF.fileLoad returned exception, see STDERR");
  }
  c_loaded_string = (*info->env)->GetStringUTFChars(info->env, loaded_string,
      0);
  if (c_loaded_string) {
    *contents = strdup(c_loaded_string);
  }
  (*info->env)->ReleaseStringUTFChars(info->env, loaded_string,
                                      c_loaded_string);

  return STATUS_OK;
}

JNIEXPORT jboolean JNICALL Java_org_clearsilver_HDF__1readFile(
    JNIEnv *env, jobject objClass, jlong hdf_obj_ptr, jstring j_filename,
    jboolean use_cb) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  NEOERR *err;
  const char *filename;
  jboolean retval;
  FILELOAD_INFO fl_info;

  if (use_cb == JNI_TRUE) {
    jclass hdfClass;

    fl_info.env = env;
    fl_info.fl_obj = objClass;
    fl_info.hdf = hdf;

    hdfClass = (*env)->GetObjectClass(env, objClass); 
    if (hdfClass == NULL) return JNI_FALSE;

    fl_info.fl_method = (*env)->GetMethodID(env, hdfClass,
        "fileLoad", "(Ljava/lang/String;)Ljava/lang/String;");
    if (fl_info.fl_method == NULL) return JNI_FALSE;

    hdf_register_fileload(hdf, &fl_info, jni_fileload_cb);
  }

  filename = (*env)->GetStringUTFChars(env, j_filename, 0);
  err = hdf_read_file(hdf, filename);
  (*env)->ReleaseStringUTFChars(env, j_filename, filename);
  if (use_cb == JNI_TRUE) hdf_register_fileload(hdf, NULL, NULL);
  if (err != STATUS_OK) {
    // Throw an exception.  jNeoErr handles all types of errors other than
    // NOT_FOUND, since that can mean different things in different contexts.
    // In this context, it means "file not found".
    if (nerr_match(err, NERR_NOT_FOUND)) {
      STRING str;
      string_init(&str);
      nerr_error_string(err, &str);
      throwFileNotFoundException(env, str.buf);
      string_clear(&str);
    } else {
      jNeoErr(env, err);
    }
  }
  retval = (err == STATUS_OK);
  return retval;
}

JNIEXPORT jboolean JNICALL Java_org_clearsilver_HDF__1writeFile(
    JNIEnv *env, jobject objClass, jlong hdf_obj_ptr, jstring j_filename) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  NEOERR *err;
  const char *filename;
  jboolean retval;

  filename = (*env)->GetStringUTFChars(env, j_filename, 0);
  err = hdf_write_file(hdf, filename);
  (*env)->ReleaseStringUTFChars(env, j_filename, filename);
  if (err != STATUS_OK) {
    jNeoErr(env, err);
  }
  retval = (err == STATUS_OK);
  return retval;
}

JNIEXPORT jboolean JNICALL Java_org_clearsilver_HDF__1writeFileAtomic(
    JNIEnv *env, jobject objClass, jlong hdf_obj_ptr, jstring j_filename) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  NEOERR *err;
  const char *filename;
  jboolean retval;

  filename = (*env)->GetStringUTFChars(env, j_filename, 0);
  err = hdf_write_file_atomic(hdf, filename);
  (*env)->ReleaseStringUTFChars(env, j_filename, filename);
  if (err != STATUS_OK) {
    jNeoErr(env, err);
  }
  retval = (err == STATUS_OK);
  return retval;
}

JNIEXPORT jboolean JNICALL Java_org_clearsilver_HDF__1readString(
    JNIEnv *env, jobject objClass, jlong hdf_obj_ptr, jstring j_data) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  NEOERR *err;
  const char *data;
  jboolean retval;

  data = (*env)->GetStringUTFChars(env, j_data, 0);
  err = hdf_read_string(hdf, data);
  (*env)->ReleaseStringUTFChars(env, j_data, data);
  if (err != STATUS_OK) {
    jNeoErr(env, err);
  }
  retval = (err == STATUS_OK);
  return retval;
}

JNIEXPORT jstring JNICALL Java_org_clearsilver_HDF__1writeString(
    JNIEnv *env, jclass objClass, jlong hdf_obj_ptr) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  NEOERR *err;
  char *output = NULL;
  jstring retval = NULL;

  err = hdf_write_string(hdf, &output);
  if (err != STATUS_OK) {
    jNeoErr(env, err);
  } else if (output) {
    retval = (*env)->NewStringUTF(env, output);
    free(output);
  }
  return retval;
}

JNIEXPORT jlong JNICALL Java_org_clearsilver_HDF__1getObj(
    JNIEnv *env, jclass objClass, jlong hdf_obj_ptr, jstring j_hdf_path) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  HDF *obj_hdf = NULL;
  const char *hdf_path;

  hdf_path = (*env)->GetStringUTFChars(env, j_hdf_path, 0);
  obj_hdf = hdf_get_obj(hdf, (char*)hdf_path);
  (*env)->ReleaseStringUTFChars(env, j_hdf_path, hdf_path);
  return (jlong)obj_hdf;
}

JNIEXPORT jlong JNICALL Java_org_clearsilver_HDF__1getChild(
    JNIEnv *env, jclass objClass, jlong hdf_obj_ptr, jstring j_hdf_path) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  HDF *obj_hdf = NULL;
  const char *hdf_path;

  hdf_path = (*env)->GetStringUTFChars(env, j_hdf_path, 0);
  obj_hdf = hdf_get_child(hdf, (char*)hdf_path);
  (*env)->ReleaseStringUTFChars(env, j_hdf_path, hdf_path);
  return (jlong)obj_hdf;
}

JNIEXPORT jlong JNICALL Java_org_clearsilver_HDF__1objChild(
    JNIEnv *env, jclass objClass, jlong hdf_obj_ptr) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  HDF *child_hdf = NULL;

  child_hdf = hdf_obj_child(hdf);
  return (jlong)child_hdf;
}

JNIEXPORT jlong JNICALL Java_org_clearsilver_HDF__1objNext(
    JNIEnv *env, jclass objClass, jlong hdf_obj_ptr) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  HDF *next_hdf = NULL;

  next_hdf = hdf_obj_next(hdf);
  return (jlong)next_hdf;
}

JNIEXPORT jstring JNICALL Java_org_clearsilver_HDF__1objName(
    JNIEnv *env, jclass objClass, jlong hdf_obj_ptr) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  char *name;
  jstring retval = NULL;

  name = hdf_obj_name(hdf);
  if (name != NULL) {
    retval =  (*env)->NewStringUTF(env, name);
  }
  return retval;
}

JNIEXPORT jstring JNICALL Java_org_clearsilver_HDF__1objValue(
    JNIEnv *env, jclass objClass, jlong hdf_obj_ptr) {
  HDF *hdf = (HDF *)hdf_obj_ptr;
  char *name;
  jstring retval = NULL;

  name = hdf_obj_value(hdf);
  if (name != NULL) {
    retval =  (*env)->NewStringUTF(env, name);
  }
  return retval;
}

JNIEXPORT void JNICALL Java_org_clearsilver_HDF__1copy
  (JNIEnv *env, jclass objClass, jlong hdf_dest_ptr, jstring j_hdf_path, 
   jlong hdf_src_ptr) {
  HDF *dest = (HDF *)hdf_dest_ptr;
  HDF *src = (HDF *)hdf_src_ptr;
  const char *hdf_path;

  hdf_path = (*env)->GetStringUTFChars(env, j_hdf_path, 0);
  hdf_copy(dest, hdf_path, src);
  (*env)->ReleaseStringUTFChars(env, j_hdf_path, hdf_path);
}