/*
* Library of functions for the Search4j utility
*/

#include "libsearch4j.h"

#ifdef WINDOWS
//minimal launch4j stuff
#define NO_JAVA_FOUND 0
#define FOUND_JRE 1
#define FOUND_SDK 2

//windows functions
#define popen _popen
#define pclose _pclose
#define strcmp _stricmp

#endif /* WINDOWS */

void replace_all ( std::string & str, std::string const & pattern, std::string const & replacement ) {

	std::string::size_type start = str.find( pattern, 0 );

	while ( start != str.npos ) {
		str.replace( start, pattern.size(), replacement );
		start = str.find( pattern, start+replacement.size() );
	}

}


int process( string command, bool render ) {

	#ifdef WINDOWS

	STARTUPINFO si;
	PROCESS_INFORMATION pi;
	memset(&pi, 0, sizeof(pi));
	memset(&si, 0, sizeof(si));
	si.cb = sizeof(si);

	DWORD dwExitCode = -1;
	char* cmd = (char*)command.c_str();

	bool result;
	if ( render ) {
		result = CreateProcess(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
	} else {
		result = CreateProcess(NULL, cmd, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
	}

	if ( result ) {
		WaitForSingleObject(pi.hProcess, INFINITE);
		GetExitCodeProcess(pi.hProcess, &dwExitCode);
		CloseHandle(pi.hThread);
		CloseHandle(pi.hProcess);
	}
	return dwExitCode;

	#else

	return system( command.c_str() );

	#endif
}



int process_and_catch_output( string command, string& output ) {
	
	FILE *pPipe;

	char cmd[1024];
	strcpy(cmd, command.c_str());
	strcat(cmd, " 2>&1" );

	//if ( verbose ) cout << "process(): running '" << command << "'" << endl;
#ifdef WINDOWS
	if( (pPipe = _popen( cmd, "rt" )) == NULL ) {
		_pclose( pPipe );
#else
	if( (pPipe = popen( cmd, "r" )) == NULL ) {
		pclose( pPipe );
#endif
		return -1;
	}
	//if ( verbose ) cout << "started process" << endl;

	/* Read pipe until end of file. */
	while( !feof( pPipe ) ) {
		char psBuffer[512];
		//if ( verbose ) cout << "get some data" << endl;
		if( fgets( psBuffer, sizeof(psBuffer), pPipe ) != NULL ) {
			
			//if ( verbose ) cout << "got: " << psBuffer << endl;
			output.append( psBuffer );
			
		} else {
			//if ( verbose ) cout << "none left" << endl;
		}
		
	}

	/* Close pipe and return return value of pPipe. */
#ifdef WINDOWS
	int code = _pclose( pPipe );
#else
	int code = pclose( pPipe );
#endif

	return code;

}

//Jvm class methods

Jvm::Jvm() 
  : era_(1), major_(0), minor_(0), update_(0),
    javaHome_(), 
    isJdk_(false), healthy_(false)
{}

Jvm::~Jvm() 
{
  // javaHome automatically destructed
}
 

void Jvm::setJavaHome( string jh ) {
	healthy_ = true;
	javaHome_ = jh;
	setVersionFromExeOuput();
	setIsJdkFromJavacPresence();
}

string Jvm::getJavaHome() {
	return javaHome_;
}

string Jvm::getExecutable() {
	string exec = "";
	exec.append( javaHome_ );
	
	#ifdef WINDOWS
	exec.append( "\\bin\\java.exe" );
	#endif

	#ifndef WINDOWS
	exec.append( "/bin/java" );
	#endif
	
	return exec;
}

#ifdef WINDOWS
string Jvm::getWinExecutable() {
	string exec = "";
	exec.append( javaHome_ );
	exec.append( "\\bin\\javaw.exe" );
	return exec;
}
#endif


string Jvm::getVersion() {
	stringstream ss;
	ss << era_ << "." << major_ << "." << minor_ << "_" << update_;
	return ss.str();
}

bool Jvm::check() {
	return healthy_;
}

bool Jvm::setVersionFromString( string version ) {
	era_ = atoi( version.substr(0,1).c_str() );
	if (version[3] == '.') {
	  // Parsing something like 1.8.0_24
	  major_ = atoi( version.substr(2,1).c_str() );
	  if(version.length() >= 5) {
	    minor_ = atoi( version.substr(4,1).c_str() );
	  } else {
	    minor_ = 0;
	  }
	  if(version.length() >= 7) {
	    update_ = atoi( version.substr(6,2).c_str() );
	  } else {
	    update_ = 0;
	  }
	}
	else {
	  // Parsing something like 1.8.0_242
	  major_ = atoi( version.substr(2,2).c_str() );
	  if(version.length() >= 6) {
	    minor_ = atoi( version.substr(5,1).c_str() );
	  } else {
	    minor_ = 0;
	  }
	  if(version.length() >= 8) {
	    update_ = atoi( version.substr(7,2).c_str() );
	  } else {
	    update_ = 0;
	  }
	  
	}
	
	return true;
}

int Jvm::compare( Jvm otherJvm ) {
	//era  
	if ( era_ > otherJvm.getEra() )
		return 1;
	else if ( era_ < otherJvm.getEra() )
		return -1;
	
	//major
	if ( major_ > otherJvm.getMajor() )
		return 1;
	else if ( major_ < otherJvm.getMajor() )
		return -1;
		
	//minor
	if ( minor_ > otherJvm.getMinor() )
		return 1;
	else if ( minor_ < otherJvm.getMinor() )
		return -1;

	//update
	if ( update_ > otherJvm.getUpdate() )
		return 1;
	else if ( update_ < otherJvm.getUpdate() )
		return -1;

	//all the same so far, must be exactly the same
	return 0;

}

int  Jvm::getEra()    { return era_; }
int  Jvm::getMajor()  { return major_; }
int  Jvm::getMinor()  { return minor_; }
int  Jvm::getUpdate() { return update_; }

bool Jvm::getIsJdk()  { return isJdk_; }
bool Jvm::getIsJre()  { return !isJdk_; }

void Jvm::setVersionFromExeOuput() {
	string command = "", output = "";
	command.append( "\"" );
	command.append( getExecutable() );
	command.append( "\"" );
	command.append(" -version");

	//cerr << "command: " << command << endl;
	int result = process_and_catch_output( command, output );
	//cerr << "output: " << output << endl;
	
	if ( result == 0 ) {
	  // 22 is the length of sentinel string plus typical version value
	  int max_search_length = output.length() - 22;
	  int caret = 0;
	  bool found = false;
	  while (caret < max_search_length && !found) {
	    const int to_match_len = 8;
	    if ( strcmp( output.substr( caret, to_match_len ).c_str() , " version" ) == 0 ) {
	      if (output[caret+to_match_len+3] == '.') {
		// Version of the form 1.8.0_24
		era_ = atoi( output.substr(caret + to_match_len + 2,1).c_str() );
		major_ = atoi( output.substr(caret + to_match_len + 4,1).c_str() );
		minor_ = atoi( output.substr(caret + to_match_len + 6,1).c_str() );
		update_ = atoi( output.substr(caret + to_match_len + 8,2).c_str() );
	      }
	      else {
		// Version of the form 11.0.6
		era_ = 1;
		major_ = atoi( output.substr(caret + to_match_len + 2,2).c_str() );
		minor_ = atoi( output.substr(caret + to_match_len + 5,1).c_str() );
		update_ = atoi( output.substr(caret + to_match_len + 7,2).c_str() );

	      }
	      found = true;
	    }
	    else {
	      caret++;
	    }
	  }
	  if (!found) {
	    healthy_ = false;
	  }
	}
	else {
	  healthy_ = false;
	}
}
void Jvm::setIsJdkFromJavacPresence() {
	string javacFile = "";
	javacFile.append( javaHome_ );

	#ifdef WINDOWS
	javacFile.append( "\\bin\\javac.exe" );
	#endif
	#ifndef WINDOWS
	javacFile.append( "/bin/javac" );
	#endif
	struct stat stFileInfo;
	isJdk_ = ( stat(javacFile.c_str(),&stFileInfo) == 0 );
}

//end of Jvm class methods


#ifdef WINDOWS
int regSearch(HKEY hKey, const char* keyName, int searchType, char* foundJavaVer ) {
	DWORD x = 0;
	unsigned long size = 1024;
	FILETIME time;
	char buffer[1024] = {0};

	int foundJava = NO_JAVA_FOUND;

	while (RegEnumKeyEx(
				hKey,			// handle to key to enumerate
				x++,			// index of subkey to enumerate
				buffer,			// address of buffer for subkey name
				&size,			// address for size of subkey buffer
				NULL,			// reserved
				NULL,			// address of buffer for class string
				NULL,			// address for size of class buffer
				&time) == ERROR_SUCCESS) {
		strcpy(foundJavaVer, buffer);
		foundJava = searchType;
		size = 1024;
	}
	
	return foundJava;
	
}
#endif


char* get_dirname (const char* full_file_path)
{
  char* parent_dir = NULL;
  
  if (full_file_path != NULL) {
  
    char* full_file_path_dup = strdup(full_file_path);
#ifdef WINDOWS
    char *p = strrchr(full_file_path_dup, '\\');
#else
    char *p = strrchr(full_file_path_dup, '/');
#endif

    if (p != NULL) {
      // truncate the string at the point where the dir-separator was found
      p[0] = 0; 
      parent_dir = full_file_path_dup;
    }
    else {
      // no dir-separator found
      free(full_file_path_dup);
      full_file_path_dup = NULL;
    }
  }
      
  return parent_dir;
}

/*
* function to find java
* implements the logic drawn on the dl lab whiteboard in feb 08
* return a Jvm object which represents the jvm on disk
*/
bool find( Jvm &jvm, bool use_minimum, const Jvm& minimum, bool jreOnly, bool jdkOnly, string phint, string hint, bool verbose ) {
	
	if ( verbose ) cout << "Searching for a JVM" << endl;
	
	char *javaHomeEnv = NULL; // use below would benefit from more careful application of strdup()/free()
	bool jvmFound = false;
	
	if ( !jvmFound ) {
		
		//try the priority hint
		if ( verbose ) cout << " - trying priority hint: ";
		if ( strcmp(phint.c_str(),"") != 0  ) {
			if ( verbose ) cout << "(" << phint << ") ";
			jvm.setJavaHome( phint );
			if ( jvm.check() ) {
				if ( use_minimum ) {
					if ( jvm.compare( minimum ) >= 0 ) {
						jvmFound = true;
					}
				} else {
					jvmFound = true;
				}
			}
			if ( (jreOnly && !jvm.getIsJre() ) || (jdkOnly && !jvm.getIsJdk()) ) {
				jvmFound = false;
			}

		}
		if ( verbose ) { if( jvmFound ) cout << "yes" << endl; else cout << "no" << endl; }
	}

	if ( !jvmFound ) {
		
		//try JAVA_HOME
		if ( verbose ) cout << " - trying JAVA_HOME: ";
		javaHomeEnv = getenv( "JAVA_HOME" );
		if ( javaHomeEnv != NULL ) {
			if ( verbose ) cout << "(" << javaHomeEnv << ") ";
			jvm.setJavaHome( javaHomeEnv );
			if ( jvm.check() ) {
				if ( use_minimum ) {
					if ( jvm.compare( minimum ) >= 0 ) {
						jvmFound = true;
					}
				} else {
					jvmFound = true;
				}
			}
			if ( (jreOnly && !jvm.getIsJre() ) || (jdkOnly && !jvm.getIsJdk()) ) {
				jvmFound = false;
			}

		}
		if ( verbose ) { if( jvmFound ) cout << "yes" << endl; else cout << "no" << endl; }
	}
	
	if ( !jvmFound ) {
		
		//try JRE_HOME
		if ( verbose ) cout << " - trying JRE_HOME: ";
		javaHomeEnv = getenv( "JRE_HOME" );
		if ( javaHomeEnv != NULL ) {
			if ( verbose ) cout << "(" << javaHomeEnv << ") ";
			jvm.setJavaHome( javaHomeEnv );
			if ( jvm.check() ) {
				if ( use_minimum ) {
					if ( jvm.compare( minimum ) >= 0 ) {
						jvmFound = true;
					}
				} else {
					jvmFound = true;
				}
			}
			if ( (jreOnly && !jvm.getIsJre() ) || (jdkOnly && !jvm.getIsJdk()) ) {
				jvmFound = false;
			}

		}
		if ( verbose ) { if( jvmFound ) cout << "yes" << endl; else cout << "no" << endl; }
	}

	#ifdef WINDOWS
	if ( !jvmFound ) {

		//try the registry - this code based on launch4j code
		char foundJavaVer[8192] = {0};
		int foundJava = NO_JAVA_FOUND;
		
		if ( verbose ) cout << " - trying the registry: "; cout.flush();
		HKEY hKey;
		const char jre[] = "SOFTWARE\\JavaSoft\\Java Runtime Environment";
		const char sdk[] = "SOFTWARE\\JavaSoft\\Java Development Kit";
		
		if ( RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT(jre), 0, KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS, &hKey) == ERROR_SUCCESS ) {
			foundJava = regSearch(hKey, jre, FOUND_JRE, foundJavaVer);
			RegCloseKey(hKey);
		}
		
		if ( RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT(sdk), 0, KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS, &hKey) == ERROR_SUCCESS ) {
			foundJava = regSearch(hKey, sdk, FOUND_SDK, foundJavaVer);
			RegCloseKey(hKey);
		}
		
		if ( foundJava != NO_JAVA_FOUND ) {
			char path[1024] = {0};
			char keyBuffer[1024];
			unsigned long datatype;
			unsigned long bufferlength = 1024;
			if (foundJava == FOUND_JRE)	{
				strcpy(keyBuffer, jre);
			} else {
				strcpy(keyBuffer, sdk);
			}
			
			strcat(keyBuffer, "\\");
			strcat(keyBuffer, foundJavaVer);
			
			if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT(keyBuffer), 0, KEY_QUERY_VALUE, &hKey) == ERROR_SUCCESS) {
				unsigned char buffer[1024] = {0};
				if (RegQueryValueEx(hKey, "JavaHome", NULL, &datatype, buffer, &bufferlength) == ERROR_SUCCESS) {
					int i = 0;
					do {
						path[i] = buffer[i];
					} while (path[i++] != 0);
					if (foundJava == FOUND_SDK) {
						strcat(path, "\\jre");
					}
					if ( verbose ) cerr << "path: " << path << endl ;
					jvm.setJavaHome( path );
					if ( jvm.check() ) {
						if ( use_minimum ) {
							if ( jvm.compare( minimum ) >= 0 ) {
								jvmFound = true;
							}
						} else {
							jvmFound = true;
						}
					}
					if ( (jreOnly && !jvm.getIsJre() ) || (jdkOnly && !jvm.getIsJdk()) ) {
						jvmFound = false;
					}
				}
				RegCloseKey(hKey);
			}
		}
		if ( verbose ) { if( jvmFound ) cout << "yes" << endl; else cout << "no" << endl; }
	}
	#endif

	if ( !jvmFound ) {
		
		//try the hint
		if ( verbose ) cout << " - trying hint: ";
		if ( strcmp(hint.c_str(),"") != 0  ) {
			if ( verbose ) cout << "(" << hint << ") ";
			jvm.setJavaHome( hint );
			if ( jvm.check() ) {
				if ( use_minimum ) {
					if ( jvm.compare( minimum ) >= 0 ) {
						jvmFound = true;
					}
				} else {
					jvmFound = true;
				}
			}
			if ( (jreOnly && !jvm.getIsJre() ) || (jdkOnly && !jvm.getIsJdk()) ) {
				jvmFound = false;
			}

		}
		if ( verbose ) { if( jvmFound ) cout << "yes" << endl; else cout << "no" << endl; }
	}



	if ( !jvmFound ) {		
		//look for it on PATH 
		if ( verbose ) cout << " - Looking on PATH (possibly yielding a pseudo, but passable, JAVA_HOME such as /usr): ";

#ifdef WINDOWS
		// The following is the DOS equivalent to 'which java', however note that
		// 'where' returns a list of all possible matches (one per line)
		const char* cmd = "where java 2>nul";
#else
		const char* cmd = "which java 2>/dev/null"; 
#endif
		char full_path_java[512] = { '\0' }; // empty string

		// ****
		// Consider replacing the following code with a call to process_and_catch_output(command, output)
		// but would need extending to have extra (optional) argument 'only_first_list' which defaults to 'false'
		
		FILE* PIN = popen(cmd,"r");
		if (PIN == NULL) {
		  cerr << "Failed to run the command to locate 'java' on PATH" << endl;
		  cerr << "Command run was: " << cmd << endl;
		}
		else {
		  // Only need the first line of output

		  // Returns number of chars returned.  OK to ignore here
		  if (fgets(full_path_java, sizeof(full_path_java), PIN) == NULL) {
		    // Not strictly necessary (as already initialized to be the empty string when declared)
		    // but done to void the warning g++ gives about not checking the return value
		    full_path_java[0] = '\0';
		  }
		}
		
		// Not all implmentations of 'which' are coded to return 0 if a program match is found
		// => safer just to look to see if a non-empty string is returned

		int ignore_exit_val = pclose(PIN);
		
		if ( strcmp(full_path_java,"") != 0) {
		  // go two directories up from where 'java' was found

		  char* parent_dir = get_dirname(full_path_java);
		  char* parent_parent_dir = get_dirname(parent_dir); 
		  
		  javaHomeEnv = parent_parent_dir; 

		  if (parent_dir != NULL) {
		    free(parent_dir);
		  }
		  
		  // Logic from here same as for other search4j java/javac testing
		  if (javaHomeEnv != NULL) {
		    if ( verbose ) cout << "(" << javaHomeEnv << ") ";
		    jvm.setJavaHome( javaHomeEnv );
		    if ( jvm.check() ) {
		      if ( use_minimum ) {
			if ( jvm.compare( minimum ) >= 0 ) {
			  jvmFound = true;
			}
		      } else {
			jvmFound = true;
		      }
		    }
		    if ( (jreOnly && !jvm.getIsJre() ) || (jdkOnly && !jvm.getIsJdk()) ) {
		      jvmFound = false;
		    }		  
		  }
		}
		if ( verbose ) { if( jvmFound ) cout << "yes" << endl; else cout << "no" << endl; }
	}

	
	return jvmFound;
}
