#define TDA_EXPORT // This is an instruction for the tdakernel.h
                   // that we are compiling tdakernel.cpp,
                   // so the functions have to be exported, not imported.
#include "tdakernel.h"
  // Function prototypes that will be provided by the native part of TDA Kernel (i.e., by this library).
  // Here, in tdakernel.cpp, we will implement these native functions.

#include "lv_lumii_tda_util_ParentProcessWaiter.h"
  // generated by javah; we will implement the native
  // waitForTheParentProcessToTerminate() function here

  // Function prototypes generated by javah.
  // These are helper functions residing in the native (i.e., non-Java)
  // part of the TDA kernel. They will be called by TDAKernel.java.
  // Here, in tdakernel.cpp, we will implement these native functions.

#ifdef TDA_USE_TAO_CORBA
#include "tdakernel_stub_corba.h"
#endif
#include "tdakernel_stub_pipe.h"
#include "tdakernel_stub_jni.h"
#include "tdakernel_stub_shared_memory.h"

//#include "tdakernel_corba_server_launcher.h"
//#include "tdakernel_pipe_server_launcher.h"
#include "tdakernel_jni_launcher.h"

#include <string>
#include <vector>
using namespace std;

#include <string.h>
#include <stdlib.h>
#include <dirent.h>

#if defined(_WIN32) || defined(_WIN64) // Windows-specific stuff for a DLL

  #define TDA_MAX_SHORT_PATH 4096

  wstring _thisLibraryFullFileNameW;
  wstring _thisLibraryFullFileNameW_8_3;
  string _thisLibraryFullFileName_8_3;
  wstring _thisLibraryFullPathW; // without trailing backslash
  string _thisLibraryFullPath_utf8; // without trailing backslash
  const char* _thisLibraryFullPath_utf8_cstr;
  wstring _thisLibraryFullPathW_8_3; // without trailing backslash
  string _thisLibraryFullPath_8_3; // without trailing backslash

  bool pathsDone = false;


  #include <windows.h>
  TDAEXTERN int WINAPI DllMain(HINSTANCE hinst, unsigned long reason, void* lpReserved)
  {
    if ((reason == DLL_PROCESS_ATTACH) && (!pathsDone)) {
      WCHAR bufferW[TDA_MAX_SHORT_PATH];

      /// thisLibraryFullFileNameW ///
      GetLastError(); // setting last error to zero
      int result = GetModuleFileNameW(hinst, bufferW, TDA_MAX_SHORT_PATH-1);
      if ((result == 0) || (GetLastError() == ERROR_INSUFFICIENT_BUFFER))
        return 0;

      _thisLibraryFullFileNameW = bufferW;

      /// thisLibraryFullPathW ///
      _thisLibraryFullPathW = _thisLibraryFullFileNameW;
      size_t i = _thisLibraryFullPathW.find_last_of(L"\\");
      if (i >= _thisLibraryFullPathW.length())
        return 0;
      _thisLibraryFullPathW.erase(i);

      /// thisLibraryFullFileNameW_8_3 ///
      if (GetShortPathNameW(
                 (L"\\\\?\\"+_thisLibraryFullFileNameW).c_str(),
                 bufferW, TDA_MAX_SHORT_PATH-1)==0) {
    	  _thisLibraryFullFileNameW_8_3 = _thisLibraryFullFileNameW;
             //return 0;
      }
      else
          _thisLibraryFullFileNameW_8_3 = bufferW;



      if (_thisLibraryFullFileNameW_8_3.substr(0, 4)==wstring(L"\\\\?\\"))
      	  _thisLibraryFullFileNameW_8_3.erase(0, 4);
      //wprintf(L"in tdakernel dll %s 4\n",_thisLibraryFullFileNameW_8_3.c_str());

      /// thisLibraryFullFileName_8_3 ///
      _thisLibraryFullFileName_8_3.resize(_thisLibraryFullFileNameW_8_3.length());
      for (unsigned int i=0; i<_thisLibraryFullFileName_8_3.length(); i++)
    	  _thisLibraryFullFileName_8_3[i] = (char)(_thisLibraryFullFileNameW_8_3[i]);

      /// thisLibraryFullPathW_8_3 ///
      if (GetShortPathNameW(
                 (L"\\\\?\\"+_thisLibraryFullPathW).c_str(),
                 bufferW, TDA_MAX_SHORT_PATH-1)==0) {
    	  _thisLibraryFullPathW_8_3 = _thisLibraryFullPathW;
             //return 0;
      }
      else
    	  _thisLibraryFullPathW_8_3 = bufferW;

      if (_thisLibraryFullPathW_8_3.substr(0, 4)==wstring(L"\\\\?\\"))
      	  _thisLibraryFullPathW_8_3.erase(0, 4);
      //wprintf(L"in tdakernel dll %s 5 \n",_thisLibraryFullPathW_8_3.c_str());

      /// thisLibraryFullPath_8_3 ///
      _thisLibraryFullPath_8_3.resize(_thisLibraryFullPathW_8_3.length());
      for (unsigned int i=0; i<_thisLibraryFullPath_8_3.length(); i++)
        _thisLibraryFullPath_8_3[i] = (char)(_thisLibraryFullPathW_8_3[i]);

      _thisLibraryFullPath_utf8 = _thisLibraryFullPath_8_3; // todo
      _thisLibraryFullPath_utf8_cstr = _thisLibraryFullPath_utf8.c_str();

      pathsDone = true;
      return 1;
    }
    return 1;
  }
#else // *NIX-specific stuff
  string _thisLibraryFullPath_utf8;
  const char* _thisLibraryFullPath_utf8_cstr;

  #include <sys/wait.h>
  #include <dlfcn.h>
  #include <unistd.h>
  __attribute__((constructor))
  void OnLoad()
  {
    Dl_info info;
    dladdr((const void*)OnLoad, &info);

#ifdef __MACH__
    char buffer[2048];
    char *curDir = getcwd(buffer, sizeof(buffer));
    _thisLibraryFullPath_utf8 = curDir;
#else
    char *curDir = get_current_dir_name();
    _thisLibraryFullPath_utf8 = curDir;
    free(curDir);
#endif

    if (strlen(info.dli_fname)>0) {
    	// adding the relative filename (with possible relative path)...
    	if (info.dli_fname[0]=='/') {
    		_thisLibraryFullPath_utf8 = info.dli_fname;
    	}
    	else {
    		_thisLibraryFullPath_utf8 += "/";
    		_thisLibraryFullPath_utf8 += info.dli_fname;
    		for (;;) {
    			// replacing: "/./" => "/"
    			unsigned int i = _thisLibraryFullPath_utf8.find("/./");
    			if (i >= _thisLibraryFullPath_utf8.length())
    				break;
    			_thisLibraryFullPath_utf8.erase(i,2);
    		}
    	}
    	// deleting the file name from the path...
    	unsigned int i = _thisLibraryFullPath_utf8.find_last_of('/');
    	_thisLibraryFullPath_utf8 = _thisLibraryFullPath_utf8.substr(0, i);

    	_thisLibraryFullPath_utf8_cstr = _thisLibraryFullPath_utf8.c_str();
    }
  }

#endif

__thread string *returnStr = NULL;
const char *TDA_CreateReturnString(const char *s)
{
    if (s == NULL)
      return NULL;
    else {
      if (returnStr == NULL)
        returnStr = new string();
      if (returnStr == NULL)
        return NULL;
      *returnStr = s;
      return returnStr->c_str();
    }
}

TDAEXTERN const char* TDACALL TDA_GetTDAKernelLibraryPath()
{
	return _thisLibraryFullPath_utf8_cstr;
}

#if defined(_WIN32) || defined(_WIN64)
TDAEXTERN const wchar_t* TDACALL TDA_GetTDAKernelLibraryPathW()
{
	return _thisLibraryFullPathW.c_str();
}
#endif

// FUNCTIONS FOR CREATING NEW TDA KERNELS AND ACCESSING EXISTING ONES //

TDAEXTERN void* TDACALL TDA_GetTDAKernelReference(const char *protocolOrUri)
{
	// Checking the protocol...
	if (strcmp(protocolOrUri, "jni") == 0) {
		JavaVM *jvm = NULL;
		jobject pKernel = createJniTDAKernel(&jvm);
		if (pKernel == NULL) {
			return NULL;
		}
		else {
			JniStub *retVal = new JniStub(jvm, pKernel, true); // JniStub will obtain its own JNI global reference to the kernel
			JNIEnv *env = NULL;
			jvm->AttachCurrentThread((void**)&env, NULL);
			if (env)
				env->DeleteLocalRef(pKernel);
			return retVal;
		}
	}
	if (strcmp(protocolOrUri, "pipe") == 0) {
		void* hTDAProcess = TDA_LaunchPipedJavaProcess(NULL, "lv.lumii.tda.kernel.TDAKernelPipeServer", NULL);
		if (hTDAProcess == NULL)
			return NULL;
	  	PipeStub *tdaKernel = new PipeStub(hTDAProcess);
	  	return tdaKernel;
	}
	if (strcmp(protocolOrUri, "shared_memory") == 0) {
		char memoryName[50];
		sprintf(memoryName, "TDA_SHARED_MEMORY_%u", TDA_GetCurrentProcessID());
	  	void* hMemory = TDA_CreateSharedMemory(memoryName, TDA_SHARED_MEMORY_SIZE);
	  	if (hMemory == NULL)
	  		return NULL;

		const char *args[2];
		args[0] = memoryName;
		args[1] = NULL;

		void* hTDAProcess = TDA_LaunchPipedJavaProcess(NULL, "lv.lumii.tda.kernel.TDAKernelSharedMemoryServer", args);
		if (hTDAProcess == NULL) {
			TDA_CloseSharedMemory(hMemory);
			return NULL;
		}

		SharedMemoryStub *tdaKernel = new SharedMemoryStub(hMemory, hTDAProcess);
		return tdaKernel;
	}
    #ifdef TDA_USE_TAO_CORBA
	if (strcmp(protocolOrUri, "corba") == 0) {
		HANDLE hProcess = NULL;
		string ior = startTDAKernelCorbaServer(hProcess);
		if ((ior.length() == 0) || (hProcess == NULL) || (hProcess == INVALID_HANDLE_VALUE)) {
			fprintf(stderr, "TDA Kernel library error: Could not start TDA Kernel CORBA server process.\n");
			return NULL;
		}

		CorbaStub *tdaKernel = new CorbaStub(hProcess);
		if (!tdaKernel->initializeFromIOR(ior.c_str())) {
		  delete tdaKernel;
		  return NULL;
		}
		else
		  return tdaKernel;
	}
    #endif

	// Checking the URI...
	if (strncmp(protocolOrUri, "jni:", 4) == 0) {
		JavaVM *jvm = NULL;
		const char* uuid = protocolOrUri+4;
		jobject pKernel = findJniTDAKernelByUUID(uuid, &jvm);
		if (pKernel == NULL)
			return NULL;
		else {
			JniStub *retVal = new JniStub(jvm, pKernel, false); // JniStub will obtain its own JNI global reference to the kernel
			JNIEnv *env = NULL;
			jvm->AttachCurrentThread((void**)&env, NULL);
			if (env)
				env->DeleteLocalRef(pKernel);
			return retVal;
		}
	}

#if defined(_WIN32) || defined(_WIN64) // currently, shared memory is supported only on the Windows platform
	if (strncmp(protocolOrUri, "shared_memory:", 14) == 0) {
		const char *memoryName = protocolOrUri+14;
		void* hMemory = TDA_CreateSharedMemory(memoryName, TDA_SHARED_MEMORY_SIZE);
		if (hMemory == NULL)
			return NULL;

		SharedMemoryStub *tdaKernel = new SharedMemoryStub(hMemory, NULL);
		return tdaKernel;
	}
	else
		return NULL;
#endif

	return NULL;
}

TDAEXTERN bool TDACALL TDA_FreeTDAKernelReference(void *tdaKernel)
{
	if (tdaKernel == NULL)
		return false;
	delete (StubBase*)tdaKernel;
	return true;
}

#if defined(_WIN32) || defined(_WIN64)
unsigned int TDA_GetParentProcessID()
{
	HINSTANCE hDLL = LoadLibrary("Kernel32.dll");
	if (!hDLL) {
		fprintf(stderr, "TDA Kernel library error: Kernel32.dll could not be loaded.\n");
		return 0;
	}

	typedef struct tagPROCESSENTRY32 {
	  DWORD     dwSize;
	  DWORD     cntUsage;
	  DWORD     th32ProcessID;
	  ULONG* 	th32DefaultHeapID;
	  DWORD     th32ModuleID;
	  DWORD     cntThreads;
	  DWORD     th32ParentProcessID;
	  LONG      pcPriClassBase;
	  DWORD     dwFlags;
	  TCHAR     szExeFile[MAX_PATH];
	} PROCESSENTRY32, *LPPROCESSENTRY32;


	HANDLE (WINAPI *lpfCreateToolhelp32Snapshot)(DWORD, DWORD); // from Psapi.dll
	BOOL (WINAPI *lpfProcess32First)(HANDLE, LPPROCESSENTRY32); // from Kernel32.dll
	BOOL (WINAPI *lpfProcess32Next)(HANDLE, LPPROCESSENTRY32);  // from Kernel32.dll
	lpfCreateToolhelp32Snapshot = (HANDLE (WINAPI *)(DWORD, DWORD)) GetProcAddress(hDLL, "CreateToolhelp32Snapshot");
	lpfProcess32First = (BOOL (WINAPI *)(HANDLE, LPPROCESSENTRY32)) GetProcAddress(hDLL, "Process32First");
	lpfProcess32Next = (BOOL (WINAPI *)(HANDLE, LPPROCESSENTRY32)) GetProcAddress(hDLL, "Process32Next");

	if ((lpfCreateToolhelp32Snapshot == NULL)
	  ||(lpfProcess32First == NULL)
	  ||(lpfProcess32Next == NULL)) {
		fprintf(stderr, "TDA Kernel library error: One of the Windows API functions lpfCreateToolhelp32Snapshot, lpfProcess32First, and lpfProcess32Next not found.\n");
		FreeLibrary(hDLL);
		return 0;
	}

	PROCESSENTRY32 processEntry = { sizeof(PROCESSENTRY32) };

	DWORD thisProcessId = GetCurrentProcessId();

	HANDLE hSnapShot = lpfCreateToolhelp32Snapshot(0x00000002/*TH32CS_SNAPPROCESS*/, 0);
	bool ok = lpfProcess32First(hSnapShot, &processEntry);
	while (ok) {
		if (processEntry.th32ProcessID == thisProcessId) {
			DWORD retVal = processEntry.th32ParentProcessID;
			CloseHandle(hSnapShot);
			FreeLibrary(hDLL);
			return retVal;
		}

		ok = lpfProcess32Next(hSnapShot, &processEntry);
	}
	CloseHandle(hSnapShot);
	FreeLibrary(hDLL);
	return 0; //???
}

unsigned int TDA_GetCurrentProcessID()
{
	return GetCurrentProcessId();
}


#else

#include <sys/types.h>
#include <unistd.h>
unsigned int TDA_GetParentProcessID()
{
	return getppid();
}

unsigned int TDA_GetCurrentProcessID()
{
	return getpid();
}

#endif

///// NATIVE FUNCTIONS USED BY JAVA PART OF TDA KERNEL /////

//in *NIX:
//#include <sys/types.h>
//#include <unistd.h>
//pid_t parentProcessId = getppid();

JNIEXPORT void JNICALL Java_lv_lumii_tda_util_ParentProcessWaiter_waitForTheParentProcessToTerminate
  (JNIEnv *env, jclass jcls)
{
	unsigned int ppid = TDA_GetParentProcessID();
	#if defined(_WIN32) || defined(_WIN64)
	if (ppid != 0) {
		HANDLE hParentProcess = OpenProcess(SYNCHRONIZE, FALSE/*do not inherit handles*/, ppid);
		// waiting for the parent process to terminate...
		WaitForSingleObject(hParentProcess, INFINITE);
		CloseHandle(hParentProcess);
	}
	#else
	waitpid(ppid, NULL, 0);
	#endif
}

JNIEXPORT jlong JNICALL Java_lv_lumii_tda_kernel_TDAKernel_CreateMutex
  (JNIEnv *env, jclass j_cl, jstring j_mutexName)
{
  #if defined(_WIN32) || defined(_WIN64) // Windows-specific
    const jchar *w_mutexName = env->GetStringChars(j_mutexName, NULL);
    if (w_mutexName == NULL)
      return 0;


    HANDLE hMutex = CreateMutexW(NULL, true, (const wchar_t*)w_mutexName);

    env->ReleaseStringChars(j_mutexName, w_mutexName);

    if (hMutex == NULL)
      return 0;

    if (GetLastError() == ERROR_ALREADY_EXISTS) {
      CloseHandle(hMutex);
      return 0;
    }

    return (jlong)hMutex;
  #else
    return 1;
  #endif
}

JNIEXPORT jboolean JNICALL Java_lv_lumii_tda_kernel_TDAKernel_DestroyMutex
  (JNIEnv *env, jclass j_cl, jlong h)
{
  #if defined(_WIN32) || defined(_WIN64) // Windows-specific
    return (ReleaseMutex((HANDLE)h) && CloseHandle((HANDLE)h));
  #else
    return true;
  #endif
}

JNIEXPORT jlong JNICALL Java_lv_lumii_tda_kernel_TDAKernel_getAddressOfJavaObject
  (JNIEnv *env, jclass j_cl, jobject j_obj)
{
  jobject globalRef = env->NewGlobalRef(j_obj);
  return (jlong)globalRef;
}

JNIEXPORT jobject JNICALL Java_lv_lumii_tda_kernel_TDAKernel_getJavaObjectByAddress
  (JNIEnv *env, jclass j_cl, jlong address)
{
  return env->NewLocalRef((jobject)address);
}

JNIEXPORT void JNICALL Java_lv_lumii_tda_kernel_TDAKernel_freeJavaObjectAddress
  (JNIEnv *env, jclass j_cl, jlong address)
{
  env->DeleteGlobalRef((jobject)address);
}

