/*
 *    MGPPSearchWrapperImpl.cpp
 *    Copyright (C) 2007 New Zealand Digital Library, http://www.nzdl.org
 *
 *    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.
 */ 
#ifdef __WIN32__
#include <win32cfg.h>
#include <strstream>
#include <sstream>
#else
#ifdef __APPLE__
#include <strstream>
#include <sstream>
#else
#include <sstream>
#endif
#endif

#include <jni.h>

#ifdef __MINGW32__

/* Cross compiling for Windows
   Want the type definitions in *win32* version of jni_md.h but
   this then leads to C-mangled style functions which we *don't*
   want.  The following achieves this */

#undef JNIEXPORT
#undef JNIIMPORT
#undef JNICALL

#define JNIEXPORT
#define JNIIMPORT
#define JNICALL
#endif

#include "org_greenstone_mgpp_MGPPSearchWrapper.h"
#include "MGPPSearchWrapperImpl.h"
#include "GSDLQueryParser.h"
#include "MGQuery.h"

// toggle debugging
//#define _DEBUG
  
MGPPSearchData::MGPPSearchData() {
  indexData = new IndexData();
  queryInfo = new QueryInfo();

  if (queryInfo==NULL) {
    cerr<<"couldn't allocate new query info\n";
    if (indexData!=NULL) {
      delete indexData;
    }
  }

  resetDefaults();
}

MGPPSearchData::~MGPPSearchData() { 
  if (indexData !=NULL) {
    delete indexData;
  }
  if (queryInfo !=NULL) {
    delete queryInfo;
  }
}

void MGPPSearchData::resetDefaults() {
  // set all the default params
  SetCStr(queryInfo->docLevel, "Document"); // the level to search at
  queryInfo->maxDocs = 50;
  queryInfo->sortByRank = true;
  queryInfo->exactWeights = false;
  queryInfo->needRankInfo = true;
  queryInfo->needTermFreqs = true;

  UCArrayClear(level);
  SetCStr(level, "Document"); // the level to return docs at
  defaultStemMethod=0;
  defaultBoolCombine=0;
  maxNumeric = 4;

}

// ********************************************
// initialisation stuff
// ********************************************

// cached ids for java stuff 
jfieldID FID_mgpp_data = NULL; // MGPPSearchData
jfieldID FID_query_result = NULL; // MGPPQueryResult
jmethodID MID_addDoc=NULL; // MGPPQueryResult.addDoc()
jmethodID MID_addTerm=NULL; // MGPPQueryResult.addTerm()
jmethodID MID_setTotalDocs=NULL; // MGPPQueryResult.setTotalDocs()
jmethodID MID_clearResult=NULL; //MGPPQueryResult.clear()
jmethodID MID_setSyntaxError=NULL; // MGPPQueryResult.setSyntaxError()
jclass CID_String=NULL; // class ID of String

/* to access objects and methods on java side, need their field/method ids - 
 this initialises them at the start to avoid recalculating them each time they
 are needed 
Note: the descriptors need to be exactly right, otherwise you get an error 
saying "no such field" but no reference to the fact that it has the right 
name but the wrong type.
Note: apparently the jclass is a local ref and should only work 
in the method that created it. It seems to work ok, but I'll make it
 global cos the book said I should, and it may avoid future hassles.
*/
JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_initIDs (JNIEnv *j_env, jclass j_cls) {
  
  FID_mgpp_data = j_env->GetFieldID(j_cls, "mgpp_data_ptr_", "J"); //a long-"J"
  if (FID_mgpp_data==NULL) {
      cerr <<"MGPP JNI: field mgpp_data_ptr_ not found"<<endl;
  } 

  FID_query_result = j_env->GetFieldID(j_cls, "mgpp_query_result_", "Lorg/greenstone/mgpp/MGPPQueryResult;"); // an object -"L<class name>;"
  if (FID_query_result==NULL) {
      cerr <<"MGPP JNI: field mgpp_query_result_ not found"<<endl;
  }
  // the methods we want to use

  // addDoc(long doc, float rank)
  jclass JC_MGPPQueryResult = j_env->FindClass("org/greenstone/mgpp/MGPPQueryResult");
  MID_addDoc = j_env->GetMethodID(JC_MGPPQueryResult, "addDoc", "(JF)V");
  if (MID_addDoc==NULL) {
      cerr <<"MGPP JNI: addDoc method not found"<<endl;
  }
  // addTerm(String term, String tag, int stem_method, long match_docs, 
  // long term_freq, String[] equiv_terms)
  MID_addTerm = j_env->GetMethodID(JC_MGPPQueryResult, "addTerm", "(Ljava/lang/String;Ljava/lang/String;IJJ[Ljava/lang/String;)V");
  if (MID_addTerm==NULL) {
      cerr <<"MGPP JNI: method addTerm not found"<<endl;
  }
  
  // setTotalDocs(long)
  MID_setTotalDocs = j_env->GetMethodID(JC_MGPPQueryResult, "setTotalDocs", "(J)V");
  if (MID_setTotalDocs==NULL) {
      cerr <<"MGPP JNI: method setTotalDocs not found"<<endl;
  }
  
  MID_clearResult = j_env->GetMethodID(JC_MGPPQueryResult, "clear", "()V");
  if (MID_clearResult==NULL) {
      cerr <<"MGPP JNI: method clear not found"<<endl;
  }
  MID_setSyntaxError = j_env->GetMethodID(JC_MGPPQueryResult, "setSyntaxError", "(Z)V");
  if (MID_clearResult==NULL) {
      cerr <<"MGPP JNI: method setSyntaxError not found"<<endl;
  }

  // get the class for String to use in NewObjectArray in runQuery()
  // FindClass returns a local reference - have to convert it to a global one
  jclass local_CID_String = j_env->FindClass("java/lang/String");
  if (local_CID_String==NULL) {
    cerr <<"MGPP JNI: java String class not found"<<endl;
  } else { 
    /* create a global ref */
    CID_String = (jclass)j_env->NewGlobalRef(local_CID_String);
    /* The local reference is no longer useful */
    j_env->DeleteLocalRef(local_CID_String);
 
    /* Is the global reference created successfully? */
    if (CID_String == NULL) {
      return; /* out of memory exception thrown */
    }
  }
  
}

/* the java side MGPPSearchWrapper has a pointer to a C++ object - MGPPSearchData
   initialise this and set the pointer 
*/
JNIEXPORT jboolean JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_initCppSide (JNIEnv *j_env, jobject j_obj){

#ifdef _DEBUG
  cerr << "**** JNI debugging for GS3. initCppSide: SetLongField()\n";
#endif

  MGPPSearchData * data = new MGPPSearchData();

#ifdef _DEBUG
  fprintf (stderr, "1a. data before SetLongField() is: %ld and as hex: %lX\n", data, data);
  fprintf (stderr, "1b. FID_mgpp_data before SetLongField() is: %ld and as hex: %lX\n", FID_mgpp_data, FID_mgpp_data);
#endif

  j_env->SetLongField(j_obj, FID_mgpp_data, (jlong)data);

#ifdef _DEBUG
  fprintf (stderr, "1a. data after SetLongField() is: %ld and as hex: %lX\n", data, data);
  fprintf (stderr, "1b. FID_mgpp_data after SetLongField() is: %ld and as hex: %lX\n", FID_mgpp_data, FID_mgpp_data);
#endif

  return true;

}

//******************************************
// do a query
// ****************************************

/* load the IndexData - cached for querying
 */
JNIEXPORT jboolean JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_loadIndexData (JNIEnv *j_env, jobject j_obj,  jstring j_index_name) {
#ifdef _DEBUG
  fprintf (stderr, "in loadIndexData\n");
#endif

  jlong data_ptr = j_env->GetLongField(j_obj, FID_mgpp_data);

#ifdef _DEBUG
  fprintf (stderr, "1. data_ptr at start is: %ld and as hex: %lX\n", data_ptr, data_ptr);
#endif

  MGPPSearchData * data = (MGPPSearchData *)data_ptr;

#ifdef __WIN32__
  const char* base_dir = "";
#else
  const char* base_dir = "/";
#endif

  const char * index_name = j_env->GetStringUTFChars( j_index_name, NULL);
  if (index_name==NULL) {
    return false;
  }
  
  jboolean j_result=false;
  
  // why doesn't this complain about const??
  if (data->indexData->LoadData(base_dir, index_name)) {
    j_result=true;
  }
  
  // release any gets
  j_env->ReleaseStringUTFChars(j_index_name, index_name);
  
  return j_result;
}

/* unload the data 
 */
JNIEXPORT jboolean JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_unloadIndexData (JNIEnv *j_env, jobject j_obj) {

#ifdef _DEBUG
  fprintf (stderr, "in unloadIndexData\n");
#endif

  jlong data_ptr = j_env->GetLongField(j_obj, FID_mgpp_data);

#ifdef _DEBUG
  fprintf (stderr, "1. data_ptr at start is: %ld and as hex: %lX\n", data_ptr, data_ptr);
#endif

  MGPPSearchData * data = (MGPPSearchData *)data_ptr;

  data->indexData->UnloadData();
  return true;

}

/* reset the stored settings 
*/
JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_reset (JNIEnv *j_env, jobject j_obj) {

  jlong data_ptr = j_env->GetLongField(j_obj, FID_mgpp_data);
  MGPPSearchData * data = (MGPPSearchData *)data_ptr;
  data->resetDefaults();
  

}
/* do the actual query - the results are written to query_result held on the 
   java side */
JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_runQuery (JNIEnv *j_env, jobject j_obj, jstring j_query){
  
  jthrowable exc; // an exception - check if something funny has happened
  const char *query = j_env->GetStringUTFChars(j_query, NULL);
  if (query==NULL) {
    return; // exception already thrown
  }
  // turn to UCArray for mgpp and then release the string
  UCArray queryArray;
  SetCStr(queryArray, query);
  j_env->ReleaseStringUTFChars(j_query, query);
  
  // the query data
  MGPPSearchData * data = (MGPPSearchData *)j_env->GetLongField(j_obj, FID_mgpp_data);
  
  // the result to write to
  jobject result_ptr = j_env->GetObjectField(j_obj, FID_query_result);
  if (result_ptr==NULL) {
    cerr <<"couldn't access the result to write to"<<endl;
    return;
  }
  
  // clear the result
  j_env->CallVoidMethod(result_ptr, MID_clearResult);
  exc = j_env->ExceptionOccurred(); // this catches the exception I think - it
  //wont be thrown any further
  if (exc) {
    j_env->ExceptionDescribe();
    return;
  }
  // the mgpp QueryResult that we will use
  ExtQueryResult queryResult;

  QueryNode * queryTree = NULL;
  // parse the query string into a tree structure
  queryTree = ParseQuery(queryArray, data->defaultBoolCombine, 
			 data->defaultStemMethod, data->maxNumeric);
  if (queryTree == NULL) {
    // invalid syntax
    j_env->CallVoidMethod(result_ptr, MID_setSyntaxError, true);
    cerr << "MGPPSearchWrapperImpl: invalid query syntax!!\n";
    return; 
  }
  // print the query
  PrintNode (cout, queryTree);
  // finally, do the query
  MGQuery(*(data->indexData), *(data->queryInfo), queryTree, queryResult, data->level);

  delete queryTree;

  // convert queryResult to the java side version
  // use levels rather than docs of ExtQueryResult
  // CallVoidMethod(obj, method id, args to method)
  for (int i=0; i<queryResult.levels.size(); i++) {
    jlong doc = queryResult.levels[i];
    jfloat rank = queryResult.ranks[i];
    j_env->CallVoidMethod(result_ptr, MID_addDoc, doc, rank);
    exc = j_env->ExceptionOccurred();
    if (exc) {
      j_env->ExceptionDescribe();
      return;
    }
    
  }

  // actual num of docs
  jlong total = queryResult.actualNumDocs;
  j_env->CallVoidMethod(result_ptr, MID_setTotalDocs, total);
  exc = j_env->ExceptionOccurred();
  if (exc) {
    j_env->ExceptionDescribe();
    return;
  }

  // the terms
  for (int j=0; j<queryResult.termFreqs.size(); j++) {
    
    TermFreqData tf = queryResult.termFreqs[j];
    jstring term = j_env->NewStringUTF(GetCStr(tf.term));
    jstring tag = j_env->NewStringUTF(GetCStr(tf.tag));
    jint stem = tf.stemMethod;
    jlong match = tf.matchDocs;
    jlong freq = tf.termFreq;
    
    jobjectArray equivs=NULL;
    jstring empty = j_env->NewStringUTF(""); // the initial object to fill the array
    jint num_equivs = tf.equivTerms.size();
    equivs = j_env->NewObjectArray(num_equivs, CID_String, empty);
    if (equivs==NULL) {
      cerr<<"couldn't create object array"<<endl;
      
    } else {
      for (int k=0; k<num_equivs;k++) {
	jstring equiv = j_env->NewStringUTF(GetCStr(tf.equivTerms[k]));
	j_env->SetObjectArrayElement(equivs, k, equiv);
      }
      
      
      j_env->CallVoidMethod(result_ptr, MID_addTerm, term, tag, stem, match, freq, equivs);
      exc = j_env->ExceptionOccurred();
      if (exc) {
	j_env->ExceptionDescribe();
	return;
      }
    }
  }


}

JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_setStem (JNIEnv *j_env, 
					      jobject j_obj, 
					      jboolean j_on) {
  MGPPSearchData * data = (MGPPSearchData *)j_env->GetLongField(j_obj, FID_mgpp_data);
  if (j_on) {
    data->defaultStemMethod |= 2;
  } else {
    data->defaultStemMethod &= 0xd;
  }

}

JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_setAccentFold (JNIEnv *j_env, 
					      jobject j_obj, 
					      jboolean j_on) {
  MGPPSearchData * data = (MGPPSearchData *)j_env->GetLongField(j_obj, FID_mgpp_data);
  if (j_on) {
    data->defaultStemMethod |= 4;
  } else {
    data->defaultStemMethod &= 0xb;
  }
}


JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_setCase (JNIEnv *j_env, 
					      jobject j_obj, 
					      jboolean j_on) {
  MGPPSearchData * data = (MGPPSearchData *)j_env->GetLongField(j_obj, FID_mgpp_data);

  if (j_on) {
    data->defaultStemMethod |= 1;
  } else {
    data->defaultStemMethod &= 0xe;
  }
}

JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_setMaxDocs (JNIEnv *j_env, 
						 jobject j_obj, 
						 jint j_max) {
  MGPPSearchData * data = (MGPPSearchData *)j_env->GetLongField(j_obj, FID_mgpp_data);
  data->queryInfo->maxDocs=j_max;
}

JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_setMaxNumeric (JNIEnv *j_env, 
						 jobject j_obj, 
						 jint j_max) {
  MGPPSearchData * data = (MGPPSearchData *)j_env->GetLongField(j_obj, FID_mgpp_data);
  data->maxNumeric=j_max;
}

JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_setSortByRank (JNIEnv *j_env, 
						    jobject j_obj, 
						    jboolean j_on) {
  MGPPSearchData * data = (MGPPSearchData *)j_env->GetLongField(j_obj, FID_mgpp_data);
  
  data->queryInfo->sortByRank=j_on;
}

JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_setReturnTerms(JNIEnv *j_env, 
						    jobject j_obj, 
						    jboolean j_on) {
  MGPPSearchData * data = (MGPPSearchData *)j_env->GetLongField(j_obj, FID_mgpp_data);
  data->queryInfo->needTermFreqs = j_on;
 
}

JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_setQueryLevel(JNIEnv *j_env, 
						   jobject j_obj, 
						   jstring j_level){
  
  MGPPSearchData * data = (MGPPSearchData *)j_env->GetLongField(j_obj, FID_mgpp_data);

  const char * level = j_env->GetStringUTFChars(j_level, NULL);
  if (level==NULL) {
    return; // exception already thrown
  }
  
  data->queryInfo->docLevel.clear();
  SetCStr(data->queryInfo->docLevel, level);
  
  // release the java stuff
  j_env->ReleaseStringUTFChars(j_level, level);
 
}

JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_setReturnLevel(JNIEnv *j_env, 
						    jobject j_obj, 
						    jstring j_level){
  // print to stderr start of setReturnLevel, print out FID..
  // %ld or %x -> need to print out pointer - 
  // Later consider field containing unsignedlong instead of long
  
#ifdef _DEBUG
  cerr << "In MGPPSearchWrapperImpl.setReturnLevel()\n";
  fprintf (stderr, "1. FID_mgpp_data at start is: %ld and as hex: %lX\n", FID_mgpp_data, FID_mgpp_data);
#endif

  jlong data_ptr = j_env->GetLongField(j_obj, FID_mgpp_data);

#ifdef _DEBUG
  fprintf (stderr, "1a. data_ptr at start is: %ld and as hex: %lX\n", data_ptr, data_ptr);
#endif

  MGPPSearchData * data = (MGPPSearchData *)j_env->GetLongField(j_obj, FID_mgpp_data);

#ifdef _DEBUG
  fprintf (stderr, "2a. FID_mgpp_data after data instantiation: %ld and as hex: %lX\n", FID_mgpp_data, FID_mgpp_data);
  fprintf (stderr, "2b. Pointer value of data upon inst: %ld and as hex: %lX\n", data, data);
#endif

  // print out FID again.. as long decimal and as hex
  // %ld or %x -> need to print out pointer - 
  // in the C code in the Setlong bit in this file
  // And the place on the java side, maybe in the configure 
  //  org.greenstone.gsdl3.service.GS2MGPPSearch.configure(Lorg/w3c/dom/Element;Lorg/w3c/dom/Element;)Z+18
  // find out what value we're SETTING the value of the pointer to
  const char * level = j_env->GetStringUTFChars(j_level, NULL);
  if (level==NULL) {
    return; // exception already thrown
  }

#ifdef _DEBUG
  fprintf (stderr, "3a. FID_mgpp_data after level: %ld and as hex: %lX\n", FID_mgpp_data, FID_mgpp_data);
  fprintf (stderr, "3b. Pointer value of data after level: %ld and as hex: %lX\n", data, data);
#endif
  
  data->level.clear();

#ifdef _DEBUG
  fprintf (stderr, "4a. FID_mgpp_data after data->level.clear(): %ld and as hex: %lX\n", FID_mgpp_data, FID_mgpp_data);
  fprintf (stderr, "4b. Pointer value of data after level.clear(): %ld and as hex: %lX\n", data, data);
#endif

  SetCStr(data->level, level);

#ifdef _DEBUG
  fprintf (stderr, "5a. FID_mgpp_data after SetCStr on data: %ld and as hex: %lX\n", FID_mgpp_data, FID_mgpp_data);
  fprintf (stderr, "5b. Pointer value of data after SetCStr: %ld and as hex: %lX\n", data, data);
#endif
  
  // release the java stuff
  j_env->ReleaseStringUTFChars(j_level, level);

#ifdef _DEBUG
  fprintf (stderr, "5a. FID_mgpp_data at end of setReturnLevel: %ld and as hex: %lX\n", FID_mgpp_data, FID_mgpp_data);
  fprintf (stderr, "5b. Pointer value of data at end of SetReturnLevel: %ld and as hex: %lX\n", data, data);
#endif

}

JNIEXPORT void JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_setMatchMode (JNIEnv *j_env,
						   jobject j_obj,
						   jint j_mode){

  MGPPSearchData * data = (MGPPSearchData *)j_env->GetLongField(j_obj, FID_mgpp_data);
  data->defaultBoolCombine=j_mode;

}

JNIEXPORT jstring JNICALL 
Java_org_greenstone_mgpp_MGPPSearchWrapper_getQueryParams (JNIEnv *j_env,
						     jobject j_obj){

  MGPPSearchData * data = (MGPPSearchData *)j_env->GetLongField(j_obj, FID_mgpp_data);
  
  // print the data to a stringstream, then convert to char*, then to 
  //jstring

  stringstream output;
  output << "Query params:"<<endl
    // need to change this to use platform specific separator for niceness
	 << "index\t\t"<<data->indexData->basePath<<"/"<<data->indexData->filename<<endl
	 <<"search level\t"<<GetCStr(data->queryInfo->docLevel)<<endl
	 <<"result level\t"<<GetCStr(data->level)<<endl
	 <<"casefold\t"<<(data->defaultStemMethod&1)<<endl
	 <<"stem\t\t"<<(data->defaultStemMethod&2)<<endl
	 <<"order by rank\t"<<data->queryInfo->sortByRank<<endl
	 <<"query type\t"<<(data->defaultBoolCombine==1?"all":"some")<<endl
         <<"max docs\t"<<data->queryInfo->maxDocs<<endl<<ends;

  // The following 'one-liner' is determiend by MacOS gcc/clang to be fundamentally
  // not safe.  This is because it will return a pointer to the cstr inside the string
  // returned as output.str(), but that string will be destroyed at the end of the method
  // meaning the 'cstr' is a dangling pointer
  //
  // In actual fact, the way we then take a copy of the cstr and put it into j_result
  // on this occassion means the dangling pointer isn't subsequently used
  //
  // However, let's recode to keep everything above board!
  
  //const char *result = output.str().c_str();
  
  const string result_str = output.str();
  char* result_cstr = new char [result_str.length()+1];
  strcpy (result_cstr, result_str.c_str());
  
  jstring j_result = j_env->NewStringUTF(result_cstr);
  delete (char *)result_cstr;
  return j_result;
}
