/*
 * Java JNI interfaces.
 *
 * This implements a very simple, low-brow, non-OOP interface.
 * It could be improved.
 */

#include <jni.h>
#ifndef _WIN32
#include <langinfo.h>
#endif // _WIN32
#include <locale.h>
#include <stdio.h>
#include <string.h>

#ifdef HAVE_STDATOMIC_H
#include <stdatomic.h>
#endif /* HAVE_STDATOMIC_H */

#include "api-structures.h"
#include "error.h"
#include "jni-client.h"
#include "utilities.h"

/* Make the compiler shut up about the deprecated functions */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"

/* Default to the English language. */
static const char* in_language = "en";

/* Dictionary can be and should be shared by all. */
static Dictionary dict = NULL;
#ifdef HAVE_STDATOMIC_H
static atomic_flag dict_is_init = ATOMIC_FLAG_INIT;
#else /* HAVE_STDATOMIC_H */
static bool dict_is_init = false;
#endif /* HAVE_STDATOMIC_H */

typedef struct
{
	Parse_Options opts, panic_parse_opts;
	Sentence      sent;
	Linkage       linkage;
	int           num_linkages, cur_linkage;
} per_thread_data;

static void setup_panic_parse_options(Parse_Options opts)
{
	parse_options_set_repeatable_rand(opts, false);
	parse_options_set_disjunct_cost(opts, 3.7f);
	parse_options_set_min_null_count(opts, 1);
	parse_options_set_max_null_count(opts, 250);
	parse_options_set_max_parse_time(opts, 60);
	parse_options_set_islands_ok(opts, true);
	parse_options_set_short_length(opts, 6);
	parse_options_set_all_short_connectors(opts, true);
	parse_options_set_linkage_limit(opts, 100);
	parse_options_set_verbosity(opts, 0);
	parse_options_set_spell_guess(opts, false);
	parse_options_set_display_morphology(opts, true);
}

static inline void do_test(void)
{
#if 0
	printf("%d\n", word_contains("said", PAST_TENSE_FORM_MARKER, dict));
	printf("%d\n", word_contains("gave.v", PAST_TENSE_FORM_MARKER, dict));
	printf("%d\n", word_contains("have", PAST_TENSE_FORM_MARKER, dict));
	printf("%d\n", word_contains("had", PAST_TENSE_FORM_MARKER, dict));
#endif
}

/* message: The string is encoded in modified UTF-8, per JNI 1.5 spec. */
static void throwException(JNIEnv *env, const char* message)
{
	char *msg;
	jclass exceptionClazz;
	if ((*env)->ExceptionOccurred(env) != NULL) return;

	msg = (char *) malloc(50+strlen(message));
	strcpy(msg, "link-grammar JNI:\n");
	strcat(msg, message);
	exceptionClazz = (*env)->FindClass(env, "java/lang/RuntimeException");
	if ((*env)->ThrowNew(env, exceptionClazz, msg) != 0)
		(*env)->FatalError(env, "Fatal: link-grammar JNI: Cannot throw");

	// I'm confused. Apparently Java exceptions return ??
	free(msg);
}

// Note that we do NOT offer any kind of protection from having
// another thread access the dictionary while this thread is still
// setting it up. It is up to the user to make sure that this thread
// (more generally, that all threads that call init) return before
// any of the threads call the parse functions.
//
// The only purpose of using the atomic_flag is to prevent two
// different threads from accidentally trying to initialize the
// dict at exactly the same time. (Which does seem like a reasonable
// guarantee to offer).
static void global_init(JNIEnv *env)
{
#ifdef HAVE_STDATOMIC_H
	if (atomic_flag_test_and_set(&dict_is_init)) return;
#else /* HAVE_STDATOMIC_H */
	if (dict_is_init) return;
	dict_is_init = true;
#endif /* HAVE_STDATOMIC_H */

#ifndef _WIN32
	const char *codeset;

	/* Get the locale from the environment...
	 * perhaps we should someday get it from the dictionary ??
	 */
	setlocale(LC_ALL, "");

	/* Everything breaks if the locale is not UTF-8; check for this,
	 * and force  the issue !
	 */
	codeset = nl_langinfo(CODESET);
	if (!strstr(codeset, "UTF") && !strstr(codeset, "utf"))
	{
		prt_error("Warning: JNI: locale %s was not UTF-8; force-setting to en_US.UTF-8\n",
			codeset);
		setlocale(LC_CTYPE, "en_US.UTF-8");
	}
#endif // _WIN32

	dict = dictionary_create_lang(in_language);
	if (!dict) throwException(env, "Error: unable to open dictionary");
	else do_test();

	const char *dict_version = linkgrammar_get_dict_version(dict);
	prt_error("Info: JNI: dictionary language '%s' version %s\n",
		in_language, dict_version);
}

static per_thread_data * init(JNIEnv *env)
{
	global_init(env);

	per_thread_data *ptd = (per_thread_data *)
		malloc(sizeof(per_thread_data));
	memset(ptd, 0, sizeof(per_thread_data));

	ptd->opts = parse_options_create();

	/* Disable repeatable_rand. We'll be using Java primarily to
	 * parse large texts, and we want a good distribution when
	 * linkages overflow (very rare for mature grammars, but crucial
	 * for grammar learning. That is, for the 'any' language.
	 */
	parse_options_set_repeatable_rand(ptd->opts, false);

	/* A cost of 2.7 allows the usual cost-2 connectors, plus the
	 * assorted fractional costs, without going to cost 3.0, which
	 * is used only during panic-parsing.
	 */
	parse_options_set_disjunct_cost(ptd->opts, 2.7f);
	parse_options_set_max_parse_time(ptd->opts, 30);
	parse_options_set_linkage_limit(ptd->opts, 1000);
	parse_options_set_short_length(ptd->opts, 16);
	parse_options_set_verbosity(ptd->opts, 0);
	parse_options_set_spell_guess(ptd->opts, false);
	parse_options_set_display_morphology(ptd->opts, true);

	ptd->panic_parse_opts = parse_options_create();
	setup_panic_parse_options(ptd->panic_parse_opts);

	return ptd;
}

static void finish(per_thread_data *ptd)
{
	if (ptd->sent)
		sentence_delete(ptd->sent);
	ptd->sent = NULL;

	if (ptd->linkage)
		linkage_delete(ptd->linkage);
	ptd->linkage = NULL;

	parse_options_delete(ptd->opts);
	ptd->opts = NULL;

	parse_options_delete(ptd->panic_parse_opts);
	ptd->panic_parse_opts = NULL;

	free(ptd);
}

static TLS per_thread_data * local_ptd = NULL;
static per_thread_data * get_ptd(JNIEnv *env, jclass cls)
{
	if (!local_ptd) local_ptd = init(env);
	return local_ptd;
}

/* ================================================================= */
/* Misc utilities */

static void jParse(JNIEnv *env, per_thread_data *ptd, const char* inputString)
{
	Parse_Options opts = ptd->opts;
	int jverbosity = parse_options_get_verbosity(opts);

	/* Clean up a bit */
	if (ptd->linkage)
		linkage_delete(ptd->linkage);
	ptd->linkage = NULL;

	if (ptd->sent)
		sentence_delete(ptd->sent);

	if (dict == NULL) throwException(env, "jParse: dictionary not open\n");
	if (inputString == NULL) throwException(env, "jParse: no input sentence!\n");
	ptd->sent = sentence_create(inputString, dict);
	ptd->num_linkages = 0;

	if (ptd->sent == NULL)
		return;

	/* First parse with cost 0 or 1 and no null links */
	parse_options_set_disjunct_cost(opts, 2.7f);
	parse_options_set_min_null_count(opts, 0);
	parse_options_set_max_null_count(opts, 0);
	parse_options_reset_resources(opts);

	ptd->num_linkages = sentence_parse(ptd->sent, ptd->opts);

	/* If failed bad. Give up. */
	if (ptd->num_linkages < 0)
	{
		sentence_delete(ptd->sent);
		ptd->sent = NULL;
		return;
	}

	/* If failed, try again with null links */
	if (0 == ptd->num_linkages)
	{
		if (jverbosity > 0) prt_error("Warning: JNI: No complete linkages found.\n");
		parse_options_set_min_null_count(opts, 1);
		parse_options_set_max_null_count(opts, sentence_length(ptd->sent));
		ptd->num_linkages = sentence_parse(ptd->sent, opts);
	}

	if (jverbosity > 0)
	{
		if (parse_options_timer_expired(opts))
			prt_error("Warning: JNI: Timer is expired!\n");

		if (parse_options_memory_exhausted(opts))
			prt_error("Warning: JNI: Memory is exhausted!\n");
	}

	if ((ptd->num_linkages == 0) &&
	    parse_options_resources_exhausted(opts))
	{
		parse_options_print_total_time(opts);
		if (jverbosity > 0) prt_error("Warning: JNI: Entering \"panic\" mode...\n");
		parse_options_reset_resources(ptd->panic_parse_opts);
		parse_options_set_verbosity(ptd->panic_parse_opts, jverbosity);
		ptd->num_linkages = sentence_parse(ptd->sent, ptd->panic_parse_opts);
		if (jverbosity > 0)
		{
			if (parse_options_timer_expired(ptd->panic_parse_opts))
				prt_error("Error: JNI: Panic timer is expired!\n");
		}
	}
}

void unit_test_jparse(JNIEnv *env, const char* inputString)
{
	jParse(env, get_ptd(env, 0), inputString);
}

static void makeLinkage(per_thread_data *ptd)
{
	if (ptd->cur_linkage < ptd->num_linkages)
	{
		if (ptd->linkage)
			linkage_delete(ptd->linkage);

		ptd->linkage = linkage_create(ptd->cur_linkage, ptd->sent, ptd->opts);
	}
}

/* ================================================================ */
/* Java JNI wrappers */

/*
 * Class:      LinkGrammar
 * Method:     getVersion
 * Signature: (I)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_org_linkgrammar_LinkGrammar_getVersion(JNIEnv *env, jclass cls)
{
	const char *s = linkgrammar_get_version();
	jstring j = (*env)->NewStringUTF(env, s);
	return j;
}

JNIEXPORT jstring JNICALL
Java_org_linkgrammar_LinkGrammar_getDictVersion(JNIEnv *env, jclass cls)
{
	init(env);
	const char *s = linkgrammar_get_dict_version(dict);
	jstring j = (*env)->NewStringUTF(env, s);
	return j;
}

JNIEXPORT void JNICALL
Java_org_linkgrammar_LinkGrammar_setMaxParseSeconds(JNIEnv *env, jclass cls, jint maxParseSeconds)
{
	per_thread_data *ptd = get_ptd(env, cls);;
	parse_options_set_max_parse_time(ptd->opts, maxParseSeconds);
}

JNIEXPORT void JNICALL
Java_org_linkgrammar_LinkGrammar_setMaxCost(JNIEnv *env, jclass cls, jdouble maxCost)
{
	per_thread_data *ptd = get_ptd(env, cls);;
	parse_options_set_disjunct_cost(ptd->opts, maxCost);
}

JNIEXPORT void JNICALL
Java_org_linkgrammar_LinkGrammar_setDictionariesPath(JNIEnv *env,
                                              jclass cls, jstring path)
{
	const char *nativePath = (*env)->GetStringUTFChars(env, path, 0);

	// Java passes null pointers as the string "null"
	if (nativePath && strcmp(nativePath, "null"))
	{
		dictionary_set_data_dir(nativePath);
	}
	(*env)->ReleaseStringUTFChars(env, path, nativePath);
}

/*
 * Class:      LinkGrammar
 * Method:     setLanguage
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL
Java_org_linkgrammar_LinkGrammar_setLanguage(JNIEnv *env, jclass cls, jstring str)
{
	const char *tmp = (*env)->GetStringUTFChars(env, str, 0);
	in_language = strdup(tmp);
	(*env)->ReleaseStringUTFChars(env, str, tmp);
}

/*
 * Class:      LinkGrammar
 * Method:     setMaxLinkages
 * Signature: (I)V
 */
JNIEXPORT void JNICALL
Java_org_linkgrammar_LinkGrammar_setMaxLinkages(JNIEnv *env, jclass cls, jint maxLinkages)
{
	per_thread_data *ptd = get_ptd(env, cls);
	parse_options_set_linkage_limit(ptd->opts, maxLinkages);
}

JNIEXPORT jint JNICALL
Java_org_linkgrammar_LinkGrammar_getMaxLinkages(JNIEnv *env, jclass cls)
{
	per_thread_data *ptd = get_ptd(env, cls);
	return parse_options_get_linkage_limit(ptd->opts);
}

/*
 * Class:      LinkGrammar
 * Method:     init
 * Signature: ()V
 */
JNIEXPORT void JNICALL
Java_org_linkgrammar_LinkGrammar_init(JNIEnv *env, jclass cls)
{
	// Force global init, always.
	global_init(env);
	get_ptd(env, cls);
}

/*
 * Class:      LinkGrammar
 * Method:     parse
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL
Java_org_linkgrammar_LinkGrammar_parse(JNIEnv *env, jclass cls, jstring str)
{
	const char *cStr;
	char * tmp;
	per_thread_data *ptd = get_ptd(env, cls);
	cStr = (*env)->GetStringUTFChars(env, str, 0);
	tmp = strdup(cStr);
	jParse(env, ptd, tmp);
	free(tmp);
	(*env)->ReleaseStringUTFChars(env, str, cStr);
}

/*
 * Class:      LinkGrammar
 * Method:     close
 * Signature: ()V
 */
JNIEXPORT void JNICALL
Java_org_linkgrammar_LinkGrammar_close(JNIEnv *env, jclass cls)
{
	if (!local_ptd) return;
	finish(local_ptd);
	local_ptd = NULL;
}

/*
 * Class:      LinkGrammar
 * Method:     finalize
 * Signature: ()V
 */
JNIEXPORT void JNICALL
Java_org_linkgrammar_LinkGrammar_doFinalize(JNIEnv *env, jclass cls)
{
	if (local_ptd) finish(local_ptd);
	local_ptd = NULL;

	// Note that we do NOT offer any protection from having another
	// thread access the dictionary, while this thread is finalizing.
	// It is up to the user to avoid this kind of mistake!
	if (dict) dictionary_delete(dict);
	dict = NULL;
#ifdef HAVE_STDATOMIC_H
	atomic_flag_clear(&dict_is_init);
#else /* HAVE_STDATOMIC_H */
	dict_is_init = false;
#endif /* HAVE_STDATOMIC_H */
}

/*
 * Class:      LinkGrammar
 * Method:     numWords
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_org_linkgrammar_LinkGrammar_getNumWords(JNIEnv *env, jclass cls)
{
	per_thread_data *ptd = get_ptd(env, cls);
	return linkage_get_num_words(ptd->linkage);
}

JNIEXPORT jstring JNICALL
Java_org_linkgrammar_LinkGrammar_getLinkageWord(JNIEnv *env, jclass cls, jint i)
{
	per_thread_data *ptd = get_ptd(env, cls);

	/* Does not need to be freed, points into data structures */
	/* Returns the subscripted word. */
	const char * w = linkage_get_word(ptd->linkage, i);
	jstring j = (*env)->NewStringUTF(env, w);
	return j;
}

JNIEXPORT jstring JNICALL
Java_org_linkgrammar_LinkGrammar_getLinkageDisjunct(JNIEnv *env, jclass cls, jint i)
{
	jstring j;
	per_thread_data *ptd = get_ptd(env, cls);

	/* does not need to be freed, points into data structures */
	/* returns the inflected word. */
	const char * w = linkage_get_disjunct_str(ptd->linkage, i);
	if (NULL == w) j = NULL;
	else j = (*env)->NewStringUTF(env, w);
	return j;
}

/*
 * Class:      LinkGrammar
 * Method:     numSkippedWords
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_org_linkgrammar_LinkGrammar_getNumSkippedWords(JNIEnv *env, jclass cls)
{
	per_thread_data *ptd = get_ptd(env, cls);
	return sentence_null_count(ptd->sent);
}

/*
 * Class:      LinkGrammar
 * Method:     numLinkages
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_org_linkgrammar_LinkGrammar_getNumLinkages(JNIEnv *env, jclass cls)
{
	per_thread_data *ptd = get_ptd(env, cls);
	return sentence_num_valid_linkages(ptd->sent);
}

/*
 * Class:      LinkGrammar
 * Method:     makeLinkage
 * Signature: (I)I
 */
JNIEXPORT void JNICALL
Java_org_linkgrammar_LinkGrammar_makeLinkage(JNIEnv *env, jclass cls, jint i)
{
	per_thread_data *ptd = get_ptd(env, cls);
	ptd->cur_linkage = i;
	makeLinkage(ptd);
}

/*
 * Class:      LinkGrammar
 * Method:     linkageNumViolations
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_org_linkgrammar_LinkGrammar_getLinkageNumViolations(JNIEnv *env, jclass cls)
{
	per_thread_data *ptd = get_ptd(env, cls);
	return sentence_num_violations(ptd->sent, ptd->cur_linkage);
}

/*
 * Class:      LinkGrammar
 * Method:     linkageDisjunctCost
 * Signature: ()I
 */
JNIEXPORT jdouble JNICALL
Java_org_linkgrammar_LinkGrammar_getLinkageDisjunctCost(JNIEnv *env, jclass cls)
{
	per_thread_data *ptd = get_ptd(env, cls);
	return sentence_disjunct_cost(ptd->sent, ptd->cur_linkage);
}

/*
 * Class:      LinkGrammar
 * Method:     linkageLinkCost
 * Signature: ()I
 */
JNIEXPORT jdouble JNICALL
Java_org_linkgrammar_LinkGrammar_getLinkageLinkCost(JNIEnv *env, jclass cls)
{
	per_thread_data *ptd = get_ptd(env, cls);
	return (jdouble) sentence_link_cost(ptd->sent, ptd->cur_linkage);
}

/*
 * Class:      LinkGrammar
 * Method:     getNumLinks
 * Signature: ()I
 */
JNIEXPORT jint JNICALL
Java_org_linkgrammar_LinkGrammar_getNumLinks(JNIEnv *env, jclass cls)
{
	per_thread_data *ptd = get_ptd(env, cls);
	return linkage_get_num_links(ptd->linkage);
}

/*
 * Class:      LinkGrammar
 * Method:     getLinkLWord
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL
Java_org_linkgrammar_LinkGrammar_getLinkLWord(JNIEnv *env, jclass cls, jint i)
{
	per_thread_data *ptd = get_ptd(env, cls);
	return linkage_get_link_lword(ptd->linkage, i);
}

/*
 * Class:      LinkGrammar
 * Method:     getLinkRWord
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL
Java_org_linkgrammar_LinkGrammar_getLinkRWord(JNIEnv *env, jclass cls, jint i)
{
	per_thread_data *ptd = get_ptd(env, cls);
	return linkage_get_link_rword(ptd->linkage, i);
}

/*
 * Class:      LinkGrammar
 * Method:     getLinkLLabel
 * Signature: (I)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_org_linkgrammar_LinkGrammar_getLinkLLabel(JNIEnv *env, jclass cls, jint i)
{
	per_thread_data *ptd = get_ptd(env, cls);
	/* Does not need to be freed, points into linkage */
	const char *s = linkage_get_link_llabel(ptd->linkage, i);
	jstring j = (*env)->NewStringUTF(env, s);
	return j;
}

/*
 * Class:      LinkGrammar
 * Method:     getLinkRLabel
 * Signature: (I)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_org_linkgrammar_LinkGrammar_getLinkRLabel(JNIEnv *env, jclass cls, jint i)
{
	per_thread_data *ptd = get_ptd(env, cls);
	/* Does not need to be freed, points into linkage */
	const char *s = linkage_get_link_rlabel(ptd->linkage, i);
	jstring j = (*env)->NewStringUTF(env, s);
	return j;
}

/*
 * Class:      LinkGrammar
 * Method:     getLinkLabel
 * Signature: (I)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_org_linkgrammar_LinkGrammar_getLinkLabel(JNIEnv *env, jclass cls, jint i)
{
	per_thread_data *ptd = get_ptd(env, cls);
	/* Does not need to be freed, points into linkage */
	const char *s = linkage_get_link_label(ptd->linkage, i);
	jstring j = (*env)->NewStringUTF(env, s);
	return j;
}

/*
 * Class:      LinkGrammar
 * Method:     constituentString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_org_linkgrammar_LinkGrammar_getConstituentString(JNIEnv *env, jclass cls)
{
	per_thread_data *ptd = get_ptd(env, cls);
	/* mode 1 prints a lisp-style string, nicely indented.
	 * mode 2 prints a lisp-style string, but with square brackets.
	 * mode 3 prints a lisp-style string, one one single line.
	 */
	/* char *s = linkage_print_constituent_tree(linkage, 1); */
	char *s = linkage_print_constituent_tree(ptd->linkage, 3);
	jstring j = (*env)->NewStringUTF(env, s);
	linkage_free_constituent_tree_str(s);
	return j;
}

/*
 * Class:      LinkGrammar
 * Method:     linkString
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL
Java_org_linkgrammar_LinkGrammar_getLinkString(JNIEnv *env, jclass cls)
{
	per_thread_data *ptd = get_ptd(env, cls);
	char *s = linkage_print_diagram(ptd->linkage, true, 8100);
	jstring j = (*env)->NewStringUTF(env, s);
	linkage_free_diagram(s);
	return j;
}
