 /*****************************************************************************
 * InstallMgr functions to be made into something usefully exposed by
 * master Glassey
 *
 */


#ifndef EXCLUDEZLIB
extern "C" {
#include <untgz.h>
}
#endif

#include <installmgr.h>
#include <filemgr.h>
#include <utilstr.h>

#include <fcntl.h>

#include <swmgr.h>
#include <swmodule.h>
#include <swversion.h>
#include <swlog.h>
#include <dirent.h>

#include <stdio.h>
#include <map>

#ifdef CURLAVAILABLE
#include <curlftpt.h>
#else
#include <ftplibftpt.h>
#endif

SWORD_NAMESPACE_START

namespace {

void removeTrailingSlash(SWBuf &buf) {
	int len = buf.size();
	if ((buf[len-1] == '/')
	 || (buf[len-1] == '\\'))
		buf.size(len-1);
}

};


using std::map;

const int InstallMgr::MODSTAT_OLDER            = 0x001;
const int InstallMgr::MODSTAT_SAMEVERSION      = 0x002;
const int InstallMgr::MODSTAT_UPDATED          = 0x004;
const int InstallMgr::MODSTAT_NEW              = 0x008;
const int InstallMgr::MODSTAT_CIPHERED         = 0x010;
const int InstallMgr::MODSTAT_CIPHERKEYPRESENT = 0x020;

// override this method and provide your own custom FTPTransport subclass
// here we try a couple defaults if sword was compiled with support for them.
// see these classes for examples of how to make your own
FTPTransport *InstallMgr::createFTPTransport(const char *host, StatusReporter *statusReporter) {
#ifdef CURLAVAILABLE
	return new CURLFTPTransport(host, statusReporter);
#else
	return new FTPLibFTPTransport(host, statusReporter);
#endif
}




InstallMgr::InstallMgr(const char *privatePath, StatusReporter *sr) {
	statusReporter = sr;
	this->privatePath = 0;
	this->transport = 0;
	stdstr(&(this->privatePath), privatePath);
	if (this->privatePath) {
		int len = strlen(this->privatePath);
		if ((this->privatePath[len-1] == '/')
		 || (this->privatePath[len-1] == '\\'))
			this->privatePath[len-1] = 0;
	}
	SWBuf confPath = (SWBuf)privatePath + "/InstallMgr.conf";
	FileMgr::createParent(confPath.c_str());
	
	installConf = new SWConfig(confPath.c_str());

	SectionMap::iterator sourcesSection;
	ConfigEntMap::iterator sourceBegin;
	ConfigEntMap::iterator sourceEnd;

	sources.clear();
	
	setFTPPassive(stricmp((*installConf)["General"]["PassiveFTP"].c_str(), "false")!=0);

	sourcesSection = installConf->Sections.find("Sources");
	if (sourcesSection != installConf->Sections.end()) {
		sourceBegin = sourcesSection->second.lower_bound("FTPSource");
		sourceEnd = sourcesSection->second.upper_bound("FTPSource");

		while (sourceBegin != sourceEnd) {
			InstallSource *is = new InstallSource("FTP", sourceBegin->second.c_str());
			sources[is->caption] = is;
			SWBuf parent = (SWBuf)privatePath + "/" + is->source + "/file";
			FileMgr::createParent(parent.c_str());
			is->localShadow = (SWBuf)privatePath + "/" + is->source;
			sourceBegin++;
		}
	}

	defaultMods.clear();
	sourcesSection = installConf->Sections.find("General");
	if (sourcesSection != installConf->Sections.end()) {
		sourceBegin = sourcesSection->second.lower_bound("DefaultMod");
		sourceEnd = sourcesSection->second.upper_bound("DefaultMod");

		while (sourceBegin != sourceEnd) {
			defaultMods.insert(sourceBegin->second.c_str());
			sourceBegin++;
		}
	}
}


InstallMgr::~InstallMgr() {
	delete [] privatePath;
	delete installConf;

	for (InstallSourceMap::iterator it = sources.begin(); it != sources.end(); ++it) {
		delete it->second;
	}
}


void InstallMgr::terminate() { if (transport) transport->terminate(); }

int InstallMgr::removeModule(SWMgr *manager, const char *moduleName) {
	SectionMap::iterator module;
	ConfigEntMap::iterator fileBegin;
	ConfigEntMap::iterator fileEnd, entry;

	// save our own copy, cuz when we remove the module from the SWMgr
	// it's likely we'll free the memory passed to us in moduleName
	SWBuf modName = moduleName;
	module = manager->config->Sections.find(modName);

	if (module != manager->config->Sections.end()) {
		// to be sure all files are closed
		// this does not remove the .conf information from SWMgr
		manager->deleteModule(modName);
			
		fileBegin = module->second.lower_bound("File");
		fileEnd = module->second.upper_bound("File");

		SWBuf modFile;
		SWBuf modDir;
		entry = module->second.find("AbsoluteDataPath");
		modDir = entry->second.c_str();
		removeTrailingSlash(modDir);
		if (fileBegin != fileEnd) {	// remove each file
			while (fileBegin != fileEnd) {
				modFile = modDir;
				modFile += "/";
				modFile += fileBegin->second.c_str();
				//remove file
				FileMgr::removeFile(modFile.c_str());
				fileBegin++;
			}
		}
		else {	//remove all files in DataPath directory

			DIR *dir;
			struct dirent *ent;
			ConfigEntMap::iterator entry;

			FileMgr::removeDir(modDir.c_str());

			if ((dir = opendir(manager->configPath))) {	// find and remove .conf file
				rewinddir(dir);
				while ((ent = readdir(dir))) {
					if ((strcmp(ent->d_name, ".")) && (strcmp(ent->d_name, ".."))) {
						modFile = manager->configPath;
						removeTrailingSlash(modFile);
						modFile += "/";
						modFile += ent->d_name;
						SWConfig *config = new SWConfig(modFile.c_str());
						if (config->Sections.find(modName) != config->Sections.end()) {
							delete config;
							FileMgr::removeFile(modFile.c_str());
						}
						else	delete config;
					}
				}
				closedir(dir);
			}
		}
		return 0;
	}
	return 1;
}


int InstallMgr::ftpCopy(InstallSource *is, const char *src, const char *dest, bool dirTransfer, const char *suffix) {
	int retVal = 0;
	FTPTransport *trans = createFTPTransport(is->source, statusReporter);
	transport = trans; // set classwide current transport for other thread terminate() call
	trans->setPassive(passive);
	
	SWBuf urlPrefix = (SWBuf)"ftp://" + is->source;

	// let's be sure we can connect.  This seems to be necessary but sucks
//	SWBuf url = urlPrefix + is->directory.c_str() + "/"; //dont forget the final slash
//	if (trans->getURL("swdirlist.tmp", url.c_str())) {
//		 SWLog::getSystemLog()->logDebug("FTPCopy: failed to get dir %s\n", url.c_str());
//		 return -1;
//	}

	   
	if (dirTransfer) {
		SWBuf dir = (SWBuf)is->directory.c_str();
		removeTrailingSlash(dir);
		dir += (SWBuf)"/" + src; //dont forget the final slash

		retVal = trans->copyDirectory(urlPrefix, dir, dest, suffix);


	}
	else {
		SWTRY {
			SWBuf url = urlPrefix + is->directory.c_str();
			removeTrailingSlash(url);
			url += (SWBuf)"/" + src; //dont forget the final slash
			if (trans->getURL(dest, url.c_str())) {
				SWLog::getSystemLog()->logDebug("FTPCopy: failed to get file %s", url.c_str());
				retVal = -1;
			}
		}
		SWCATCH (...) {
			retVal = -1;
		}
	}
	SWTRY {
		FTPTransport *deleteMe = trans;
		// do this order for threadsafeness
		// (see terminate())
		trans = transport = 0;
		delete deleteMe;
	}
	SWCATCH (...) {}
	return retVal;
}


int InstallMgr::installModule(SWMgr *destMgr, const char *fromLocation, const char *modName, InstallSource *is) {
	SectionMap::iterator module, section;
	ConfigEntMap::iterator fileBegin;
	ConfigEntMap::iterator fileEnd;
	ConfigEntMap::iterator entry;
	SWBuf sourceDir;
	SWBuf buffer;
	bool aborted = false;
	bool cipher = false;
	DIR *dir;
	struct dirent *ent;
	SWBuf modFile;

	SWLog::getSystemLog()->logDebug("***** InstallMgr::installModule\n");
	if (fromLocation)
		SWLog::getSystemLog()->logDebug("***** fromLocation: %s \n", fromLocation);
	SWLog::getSystemLog()->logDebug("***** modName: %s \n", modName);

	if (is)
		sourceDir = (SWBuf)privatePath + "/" + is->source;
	else	sourceDir = fromLocation;

	removeTrailingSlash(sourceDir);
	sourceDir += '/';

	SWMgr mgr(sourceDir.c_str());
	
	module = mgr.config->Sections.find(modName);

	if (module != mgr.config->Sections.end()) {
	
		entry = module->second.find("CipherKey");
		if (entry != module->second.end())
			cipher = true;
		
		//
		// This first check is a method to allow a module to specify each
		// file that needs to be copied
		//
		fileEnd = module->second.upper_bound("File");
		fileBegin = module->second.lower_bound("File");

		if (fileBegin != fileEnd) {	// copy each file
			if (is) {
				while (fileBegin != fileEnd) {	// ftp each file first
					buffer = sourceDir + fileBegin->second.c_str();
					if (ftpCopy(is, fileBegin->second.c_str(), buffer.c_str())) {
						aborted = true;
						break;	// user aborted
					}
					fileBegin++;
				}
				fileBegin = module->second.lower_bound("File");
			}

			if (!aborted) {
				// DO THE INSTALL
				while (fileBegin != fileEnd) {
					SWBuf sourcePath = sourceDir;
					sourcePath += fileBegin->second.c_str();
					SWBuf dest = destMgr->prefixPath;
					removeTrailingSlash(dest);
					dest += '/';
					dest += fileBegin->second.c_str();
					FileMgr::copyFile(sourcePath.c_str(), dest.c_str());

					fileBegin++;
				}
			}
			//---------------

			if (is) {
				fileBegin = module->second.lower_bound("File");
				while (fileBegin != fileEnd) {	// delete each tmp ftp file
					buffer = sourceDir + fileBegin->second.c_str();
					FileMgr::removeFile(buffer.c_str());
					fileBegin++;
				}
			}
		}

		// This is the REAL install code, the above code I don't think has
		// ever been used
		//
		// Copy all files in DataPath directory
		// 
		else {
			ConfigEntMap::iterator entry;

			entry = module->second.find("AbsoluteDataPath");
			if (entry != module->second.end()) {
				SWBuf absolutePath = entry->second.c_str();
				SWBuf relativePath = absolutePath;
				entry = module->second.find("PrefixPath");
				if (entry != module->second.end()) {
					relativePath << strlen(entry->second.c_str());
				}
				else {
					relativePath << strlen(mgr.prefixPath);
				}
				SWLog::getSystemLog()->logDebug("***** mgr.prefixPath: %s \n", mgr.prefixPath);
				SWLog::getSystemLog()->logDebug("***** destMgr->prefixPath: %s \n", destMgr->prefixPath);
				SWLog::getSystemLog()->logDebug("***** absolutePath: %s \n", absolutePath.c_str());
				SWLog::getSystemLog()->logDebug("***** relativePath: %s \n", relativePath.c_str());

				if (is) {
					if (ftpCopy(is, relativePath.c_str(), absolutePath.c_str(), true)) {
						aborted = true;	// user aborted
					}
				}
				if (!aborted) {
					SWBuf destPath = (SWBuf)destMgr->prefixPath + relativePath;
					FileMgr::copyDir(absolutePath.c_str(), destPath.c_str());
				}
				if (is) {		// delete tmp ftp files
//					mgr->deleteModule(modName);
					FileMgr::removeDir(absolutePath.c_str());
				}
			}
		}
		if (!aborted) {
			SWBuf confDir = sourceDir + "mods.d/";
			if ((dir = opendir(confDir.c_str()))) {	// find and copy .conf file
				rewinddir(dir);
				while ((ent = readdir(dir))) {
					if ((strcmp(ent->d_name, ".")) && (strcmp(ent->d_name, ".."))) {
						modFile = confDir;
						modFile += ent->d_name;
						SWConfig *config = new SWConfig(modFile.c_str());
						if (config->Sections.find(modName) != config->Sections.end()) {
							SWBuf targetFile = destMgr->configPath; //"./mods.d/";
							removeTrailingSlash(targetFile);
							targetFile += "/";
							targetFile += ent->d_name;
							FileMgr::copyFile(modFile.c_str(), targetFile.c_str());
							if (cipher) {
								if (getCipherCode(modName, config)) {
									SWMgr newDest(destMgr->prefixPath);
									removeModule(&newDest, modName);
									aborted = true;
								}
								else {
									config->Save();
									FileMgr::copyFile(modFile.c_str(), targetFile.c_str());
								}
							}
						}
						delete config;
					}
				}
				closedir(dir);
			}
		}
		return (aborted) ? -1 : 0;
	}
	return 1;
}


// override this and provide an input mechanism to allow your users
// to enter the decipher code for a module.
// return true you added the cipher code to the config.
// default to return 'aborted'
bool InstallMgr::getCipherCode(const char *modName, SWConfig *config) {
	return false;

/* a sample implementation, roughly taken from the windows installmgr

	SectionMap::iterator section;
	ConfigEntMap::iterator entry;
	SWBuf tmpBuf;
	section = config->Sections.find(modName);
	if (section != config->Sections.end()) {
		entry = section->second.find("CipherKey");
		if (entry != section->second.end()) {
			entry->second = GET_USER_INPUT();
			config->Save();

			// LET'S SHOW THE USER SOME SAMPLE TEXT FROM THE MODULE
			SWMgr *mgr = new SWMgr();
			SWModule *mod = mgr->Modules[modName];
			mod->setKey("Ipet 2:12");
			tmpBuf = mod->StripText();
			mod->setKey("gen 1:10");
			tmpBuf += "\n\n";
			tmpBuf += mod->StripText();
			SOME_DIALOG_CONTROL->SETTEXT(tmpBuf.c_str());
			delete mgr;

			// if USER CLICKS OK means we should return true
			return true;
		}
	}
	return false;
*/

}


int InstallMgr::refreshRemoteSource(InstallSource *is) {
	SWBuf root = (SWBuf)privatePath + (SWBuf)"/" + is->source.c_str();
	removeTrailingSlash(root);
	SWBuf target = root + "/mods.d";
	int errorCode = -1; //0 means successful
	
	FileMgr::removeDir(target.c_str());

	if (!FileMgr::existsDir(target))
		FileMgr::createPathAndFile(target+"/globals.conf");

#ifndef EXCLUDEZLIB
	SWBuf archive = root + "/mods.d.tar.gz";
	
	errorCode = ftpCopy(is, "mods.d.tar.gz", archive.c_str(), false);
	if (!errorCode) { //sucessfully downloaded the tar,gz of module configs
		FileDesc *fd = FileMgr::getSystemFileMgr()->open(archive.c_str(), FileMgr::RDONLY);
		untargz(fd->getFd(), root.c_str());
		FileMgr::getSystemFileMgr()->close(fd);
	}
	else if (!term) //if the tar.gz download was canceled don't continue with another download
#endif
	errorCode = ftpCopy(is, "mods.d", target.c_str(), true, ".conf"); //copy the whole directory
	
	is->flush();
	return errorCode;
}


bool InstallMgr::isDefaultModule(const char *modName) {
	return defaultMods.count(modName);
}

/************************************************************************
 * getModuleStatus - compare the modules of two SWMgrs and return a 
 * 	vector describing the status of each.  See MODSTAT_*
 */
map<SWModule *, int> InstallMgr::getModuleStatus(const SWMgr &base, const SWMgr &other) {
	map<SWModule *, int> retVal;
	SWBuf targetVersion;
	SWBuf sourceVersion;
	SWBuf softwareVersion;
	bool cipher;
	bool keyPresent;
	int modStat;
	
	for (ModMap::const_iterator mod = other.Modules.begin(); mod != other.Modules.end(); mod++) {
	
		modStat = 0;

		cipher = false;
		keyPresent = false;
		
		const char *v = mod->second->getConfigEntry("CipherKey");
		if (v) {
			cipher = true;
			keyPresent = *v;
		}
		
		targetVersion = "0.0";
		sourceVersion = "1.0";
		softwareVersion = (const char *)SWVersion::currentVersion;
		
		v = mod->second->getConfigEntry("Version");
		if (v) sourceVersion = v;

		v = mod->second->getConfigEntry("MinimumVersion");
		if (v) softwareVersion = v;

		const SWModule *baseMod = base.getModule(mod->first);
		if (baseMod) {
			targetVersion = "1.0";
			v = baseMod->getConfigEntry("Version");
			if (v) targetVersion = v;
			modStat |= (SWVersion(sourceVersion.c_str()) > SWVersion(targetVersion.c_str())) ? MODSTAT_UPDATED : (SWVersion(sourceVersion.c_str()) < SWVersion(targetVersion.c_str())) ? MODSTAT_OLDER : MODSTAT_SAMEVERSION;
		}
		else modStat |= MODSTAT_NEW;

		if (cipher) modStat |= MODSTAT_CIPHERED;
		if (keyPresent) modStat |= MODSTAT_CIPHERKEYPRESENT;
		retVal[mod->second] = modStat;
	}
	return retVal;
}


InstallSource::InstallSource(const char *type, const char *confEnt) {
	this->type = type;
	mgr = 0;
	userData = 0;
	if (confEnt) {
		char *buf = 0;
		stdstr(&buf, confEnt);

		caption = strtok(buf, "|");
		source = strtok(0, "|");
		directory = strtok(0, "|");
		removeTrailingSlash(directory);
		delete [] buf;
	}
}


InstallSource::~InstallSource() {
	if (mgr)
		delete mgr;
}


void InstallSource::flush() {
	if (mgr) {
		delete mgr;
		mgr = 0;
	}
}


SWMgr *InstallSource::getMgr() {
	if (!mgr)
		// ..., false = don't augment ~home directory.
		mgr = new SWMgr(localShadow.c_str(), true, 0, false, false);
	return mgr;
}


SWORD_NAMESPACE_END

