#include "stdafx.h"
#include "resource.h"

#include "FWatchApp.hpp"

#include "Utility.hpp"

#include "SettingInfoFactory.hpp"
#include "WatchThreadGroup.hpp"
#include "FileIO.hpp"
#include "Crypt.hpp"

#include <assert.h>

#include <sstream>


// 読み込み可能バージョン確認
static LPCTSTR VERSION_SIG1 = _T(";fwatch-version:1.7");
static LPCTSTR VERSION_SIG2 = _T(";fwatch-version:1.8");


//////////////////////////////////////////////

CWatchThreadGroup::CWatchThreadGroup( const bool v_dryrun )
	: modified_(false)
	, dryrun_(v_dryrun)
{
}

void CWatchThreadGroup::ReleaseAllThreads()
{
	// 停止命令をすべてのスレッドに送信(待機はしない。)
	for (CWatchThreadList::iterator ite = watchList_.begin();
		ite != watchList_.end();
		++ite) {
		CWatchThread* pWatchThread = *ite;
		pWatchThread->StopNoWait();
	}

	// すべてのスレッドを破棄する。(個々のスレッドの破棄を待機する)
	for (CWatchThreadList::iterator ite = watchList_.begin();
		ite != watchList_.end();
		++ite) {
		CWatchThread* pWatchThread = *ite;
		delete pWatchThread;
	}

	watchList_.clear();
	modified_ = false;
}

CWatchThreadGroup::~CWatchThreadGroup()
{
	ReleaseAllThreads();
}

bool CWatchThreadGroup::isDryrRun() const
{
	return dryrun_;
}


bool CWatchThreadGroup::LoadInfData(const tstring& InfPath)
{
	if (InfPath.empty()) {
		return false;
	}

	ReleaseAllThreads();

	bool succeeded = false;
	try {
		// ファイルオープン
		CFileIO fileIO(InfPath);
		if (!fileIO.Open(false)) {
			return false;
		}

		// 暗号化解除準備
		CCrypt crypt;
		tstring password = app.GetImpersonatePasswordEncryption();

		const CSettingInfo init;
		bool sigChecked = false;
		bool enableScaped = false;

		tstring line;
		while (fileIO.GetText(line)) {

			// 最初の行のバージョンチェック
			if ( !sigChecked) {
				sigChecked = true;

				tstring sig1(VERSION_SIG1);
				tstring sig2(VERSION_SIG2);
				if ((line.length() >= sig1.length() && line.substr(0, sig1.length()) == sig1) ||
					(line.length() >= sig2.length() && line.substr(0, sig2.length()) == sig2)) {
					// チェックOK
					enableScaped = true;
					continue;
				}
				else if (fileIO.IsUnicode()) {
					// UNICODEの設定ファイルのみバージョンの整合性チェックをする。
					// (MBCS形式の設定ファイルは旧設定ファイルの読み込みを考慮してチェックしない。)
					throw std::runtime_error("incompatible version");
				}
			}

			// コメント行判定
			if (line.length() >= 1 && line[0] == ';') {
				// 先頭文字が「;」であればコメント行とみなしてスキップする。
				continue;
			}


			CSettingInfoFactory factory(init);

			tstring userName;
			int col = 0;
			tstring::size_type lastpos = 0;
			for (;;) {
				const tstring::size_type pos = line.find('\t', lastpos);
				tstring arg;
				if (pos != tstring::npos) {
					arg = line.substr(lastpos, pos - lastpos);
				}
				else {
					arg = line.substr(lastpos);
				}

				// version1.7以降の形式ならばエスケープ文字を受け入れる。
				if (enableScaped) {
					arg = UnescapeUnprintableChars(arg);
				}

				// 項目の読み取り
				switch(col)
				{
				case 0:
					factory.setWatchDir(arg);
					break;

				case 1:
					factory.setWatchFile(arg);
					break;

				case 2:
					factory.setAction(arg);
					break;

				case 3:
					factory.setAppName(arg);
					break;

				case 4:
					factory.setParam(arg);
					break;

				case 5:
					factory.setMaxProcess(atoi(arg));
					break;

				case 6:
					factory.setShowWindow( ShowWindowType::valueOf(atoi(arg)));
					break;

				case 7:
					factory.setWaitWrite(atoi(arg));
					break;

				case 8:
					factory.setTolerance(atoi(arg));
					break;

				case 9:
					factory.setDeletePending(atoi(arg));
					break;

				case 10:
					factory.setLog(arg);
					break;

				case 11:
					factory.setMaxDepth(atoi(arg));
					break;

				case 12:
					factory.setUsingDirNotificationAPI(atoi(arg) != 0);
					break;

				case 13:
					factory.setForceInterval(atoi(arg));
					break;

				case 14:
					factory.setDirNotificationAPIExpirySpan(atoi(arg));
					break;

				case 15:
					factory.setDirNotificationAPIRetryInterval(atoi(arg));
					break;

				case 16:
					factory.setLogLevel(atoi(arg));
					break;

				case 17:
					factory.setAppCurrentDir(arg);
					break;

				case 18:
					factory.setPersist(arg);
					break;

				case 19:
					factory.setArchiveOnly(atoi(arg) != 0);
					break;

				case 20:
					factory.setNotifyOnStart(atoi(arg) != 0);
					break;

				case 21:
					factory.setWatchDelay(atoi(arg));
					break;

				case 22:
					factory.setIgnoreDuplicate(atoi(arg) != 0);
					break;

				case 23:
					factory.setImpersonate(atoi(arg) != 0);
					break;

				case 24:
					{
						factory.setImpersonateUser(arg);
						userName = arg;
					}
					break;

				case 25:
					factory.setImpersonateDomain(arg);
					break;

				case 26:
					{
						if (userName.empty() || arg.empty() || password.empty()) {
							// ユーザまたはパスワード、またはiniのパスワードが空であれば
							// 変換しない。
							if (userName.empty()) {
								// ユーザが空であればパスワードも空とする。
								factory.setImpersonatePassword(_T(""));
							}
							else {
								factory.setImpersonatePassword(arg);
							}
						}
						else {
							// そうでなければ暗号化解除を試行する
							tstring saltPassword = userName;
							saltPassword += _T(":fw:");
							saltPassword += password;
							
							tstring decryptPassword;
							try {
								crypt.decrypt(saltPassword, arg, decryptPassword);
							}
							catch (const CCryptException& exception) {
								DWORD err = exception.GetErrorCode();
								app.showMessageBox(
									MB_ICONERROR | MB_OK,
									NULL,
									IDS_MAINPROC_INFLOADFAILED_DECRYPT,
									ToErrorMessage(err).c_str());
								// パスワード欄を空にして継続する。
								decryptPassword.clear();
							}

							factory.setImpersonatePassword(decryptPassword);
						}
					}
					break;

				case 27:
					factory.setWatchDisabled(atoi(arg) != 0);
					break;

				default:
					break;
				}

				col++;
				lastpos = pos + 1;
				if (pos == std::string::npos) {
					break;
				}
			}

			append(factory.create());
		}
		succeeded = true;
	}
	catch (const CCryptException& v_exception) {
		DWORD err = v_exception.GetErrorCode();
		app.showMessageBox(MB_ICONERROR | MB_OK, NULL, IDS_MAINPROC_INFLOADFAILED, ToErrorMessage(err).c_str());
	}
	catch (const CFileIOException& v_exception) {
		DWORD err = v_exception.GetErrorCode();
		app.showMessageBox(MB_ICONERROR | MB_OK, NULL, IDS_MAINPROC_INFLOADFAILED, ToErrorMessage(err).c_str());
	}
	catch (const std::exception& v_exception) {
		const char *pMes = v_exception.what();
		CA2T mes(pMes ? pMes : "");
		app.showMessageBox(MB_ICONERROR | MB_OK, NULL, IDS_MAINPROC_INFLOADFAILED, (LPCTSTR)mes);
	}

	modified_ = false;
	return succeeded;
}

bool CWatchThreadGroup::isModified() const
{
	return modified_;
}

bool CWatchThreadGroup::SaveInfData(const tstring& InfPath, bool v_bForce) const
{
	if (InfPath.empty()) {
		return false;
	}

	if ( !isModified() && ! v_bForce) {
		return true;
	}

	bool succeeded = false;
	try {
		// 出力先バッファ確保
		tstringstream fout;

		// 暗号化準備
		CCrypt crypt;
		tstring password = app.GetImpersonatePasswordEncryption();
		const CCrypt::algtype* pAlgType = NULL;
		if ( !password.empty()) {
			// パスワードがあれば暗号化アルゴリズムを決定する。
			pAlgType = &crypt.chooseAlgorithm();
		}

		// version
		// 書き込み用バージョン
		tstring versionWriteSig = ToString(_T(";fwatch-version:%s\r\n"),
			app.getFileVersion().c_str());
		fout << versionWriteSig;

		// watch-list
		for (CWatchThreadList::const_iterator ite = watchList_.begin();
			ite != watchList_.end();
			++ite )
		{
			const CWatchThread* pWatchThread = *ite;
			const CSettingInfo& settingInfo = pWatchThread->getSettingInfo();

			LPCTSTR tab = _T("\t");

			// パスワードの暗号化
			tstring encryptedPassword;
			tstring impersonateUser = settingInfo.getImpersonateUser();
			if ( !impersonateUser.empty()) {
				// ユーザが指定されている場合のみパスワードを保存する。
				tstring impersonatePassword = settingInfo.getImpersonatePassword();
				if (pAlgType == NULL) {
					// 暗号化アルゴリズムがなければ平文のまま
					encryptedPassword = impersonatePassword;
				}
				else {
					// パスワードの有無を問わず、暗号化アルゴリズムがあれば暗号化する。
					tstring saltPassword = impersonateUser;
					saltPassword += _T(":fw:");
					saltPassword += password;
					
					crypt.encrypt(saltPassword, *pAlgType, impersonatePassword, encryptedPassword);
				}
			}

			// 項目の書き出し
			fout << EscapeUnprintableChars(settingInfo.getWatchDir())	<< tab;
			fout << EscapeUnprintableChars(settingInfo.getWatchFile())	<< tab;
			fout << EscapeUnprintableChars(settingInfo.getAction())		<< tab;
			fout << EscapeUnprintableChars(settingInfo.getAppName())	<< tab;
			fout << EscapeUnprintableChars(settingInfo.getParam())		<< tab;
			fout << settingInfo.getMaxProcess()	<< tab;
			fout << settingInfo.getShowWindow()	<< tab;
			fout << settingInfo.getWaitWrite()	<< tab;
			fout << settingInfo.getTolerance()	<< tab;
			fout << settingInfo.getDeletePending() << tab;
			fout << EscapeUnprintableChars(settingInfo.getLog())		<< tab;
			fout << settingInfo.getMaxDepth()	<< tab;
			fout << (settingInfo.isUsingDirNotificationAPI() ? 1 : 0)	<< tab;
			fout << settingInfo.getForceInterval()						<< tab;
			fout << settingInfo.getDirNotificationAPIExpirySpan()		<< tab;
			fout << settingInfo.getDirNotificationAPIRetryInterval()	<< tab;
			fout << settingInfo.getLogLevel()	<< tab;
			fout << EscapeUnprintableChars(settingInfo.getAppCurrentDir()) << tab;
			fout << EscapeUnprintableChars(settingInfo.getPersist()) << tab;
			fout << (settingInfo.isArchiveOnly() ? 1 : 0) << tab;
			fout << (settingInfo.isNotifyOnStart() ? 1 : 0) << tab;
			fout << settingInfo.getWatchDelay() << tab;
			fout << (settingInfo.isIgnoreDuplicate() ? 1 : 0) << tab;
			fout << (settingInfo.isImpersonate() ? 1 : 0) << tab;
			fout << EscapeUnprintableChars(settingInfo.getImpersonateUser()) << tab;
			fout << EscapeUnprintableChars(settingInfo.getImpersonateDomain()) << tab;
			fout << EscapeUnprintableChars(encryptedPassword) << tab;
			fout << (settingInfo.isWatchDisabled() ? 1 : 0) << _T("\r\n");
		}

		tstring line = fout.str();

		CFileIO writer(InfPath);
		writer.Open(true);
		writer.PutText(line);

		modified_ = false;
		succeeded = true;
	}
	catch (const CCryptException& v_exception) {
		DWORD err = v_exception.GetErrorCode();
		app.showMessageBox(MB_ICONERROR | MB_OK, NULL, IDS_MAINPROC_INFLOADFAILED, ToErrorMessage(err).c_str());
	}
	catch (const CFileIOException& v_exception) {
		DWORD err = v_exception.GetErrorCode();
		app.showMessageBox(MB_ICONERROR | MB_OK, NULL, IDS_MAINPROC_INFSAVEFAILED, ToErrorMessage(err).c_str());
	}
	catch (const std::exception& v_exception) {
		const char *pMes = v_exception.what();
		CA2T mes(pMes ? pMes : "");
		app.showMessageBox(MB_ICONERROR | MB_OK, NULL, IDS_MAINPROC_INFSAVEFAILED, (LPCTSTR)mes);
	}

	return succeeded;
}

/*!
 * 無効ではない、すべてのスレッドを開始します。
 * すでに開始済みのものは何もしません。
 */
void CWatchThreadGroup::startAll()
{
	for (CWatchThreadList::iterator ite = watchList_.begin();
		ite != watchList_.end();
		++ite)
	{
		CWatchThread* pWatchThread = *ite;
		if ( !pWatchThread->getSettingInfo().isWatchDisabled() && !pWatchThread->isRunning()) {
			pWatchThread->Start();
		}
	}
}

/*!
 * 指定したスレッドを開始します。
 * 既に開始済みであれば何もしません。
 * スレッドがの監視がディセーブル指定であっても開始されます。
 * \return すでに開始されているか開始された場合はtrue、開始できなかった場合はfalse
 */
bool CWatchThreadGroup::start(unsigned int idx)
{
	if (idx >= watchList_.size()) {
		assert(false && "存在しないアイテムにはアクセスできません。");
		return false;
	}

	watchList_[idx]->Start();
	return true;
}

/*!
 * 指定したスレッドを停止します。
 * すでに停止済みであれば何もしません。
 * \return スレッドが既に停止済みであるか停止された場合はtrue、定時的無かった場合はfalse
 */
bool CWatchThreadGroup::stop(unsigned int idx)
{
	if (idx >= watchList_.size()) {
		assert(false && "存在しないアイテムにはアクセスできません。");
		return false;
	}

	watchList_[idx]->Stop();
	return true;
}

unsigned int CWatchThreadGroup::size() const
{
	return static_cast<unsigned int>(watchList_.size());
}

const CSettingInfo& CWatchThreadGroup::getInfo(unsigned int idx) const
{
	//required:
	assert(idx >= 0 && idx < watchList_.size() && "存在しないアイテムにはアクセスできません。");
	
	//do:
	return watchList_[idx]->getSettingInfo();
}

CWatchThread *CWatchThreadGroup::append(const CSettingInfo& v_settingInfo)
{
	CWatchThread* pWatchThread = new CWatchThread(v_settingInfo);
	watchList_.push_back(pWatchThread);
	modified_ = true;
	return pWatchThread;
}

CWatchThread *CWatchThreadGroup::replace(unsigned int idx, const CSettingInfo& v_settingInfo)
{
	//required:
	if (idx >= watchList_.size()) {
		assert(false && "存在しないアイテムにはアクセスできません。");
		return NULL;
	}
	
	//do:

	if (watchList_[idx]->getSettingInfo() == v_settingInfo) {
		// 変更点がない場合
		return watchList_[idx];
	}

	delete watchList_[idx];
	CWatchThread *const pWatchThread = new CWatchThread(v_settingInfo);

	watchList_[idx] = pWatchThread;
	modified_ = true;
	return pWatchThread;
}

bool CWatchThreadGroup::erase(unsigned int idx)
{
	//required:
	if (idx >= watchList_.size()) {
		assert(false && "範囲外です。");
		return false;
	}

	//do:
	CWatchThreadList::iterator ite = watchList_.begin() + idx;
	delete *ite;
	watchList_.erase(ite);
	modified_ = true;
	return true;
}

bool CWatchThreadGroup::isRunning() const
{
	for (CWatchThreadList::const_iterator ite = watchList_.begin();
		ite != watchList_.end();
		++ite)
	{
		const CWatchThread* pWatchThread = *ite;
		if (pWatchThread->isRunning()) {
			return true;
		}
	}
	return false;
}

bool CWatchThreadGroup::isRunning(unsigned int idx) const
{
	//required:
	assert(idx >= 0 && idx < watchList_.size() && "範囲外です。");

	//do:
	return watchList_[idx]->isRunning();
}


void CWatchThreadGroup::ClearLog(const CSettingInfo& settingInfo) const
{
	tstring logPath = settingInfo.getLog();
	if ( !settingInfo.isEnableLogging() || logPath.empty()) {
		// ログ出力しない場合は関係なし
		return;
	}

	// 未使用のロックオブジェクトがあれば消す。
	// (ログファイルの同期に使用しているものがあるので、一括して処理する。)
	// (設定変更でログファイル名が変った場合に、
	// 使われなくなったログファイル名のロックオブジェクトが残るのを防ぐため。)
	app.RemoveUnusedLock();

	// 実行中のスレッドで同一のログを使用しているものがあるか判定する。
	bool alreadyUsed = false;
	for (CWatchThreadList::const_iterator ite = watchList_.begin();
		ite != watchList_.end();
		++ite)
	{
		const CWatchThread* pWatchThread = *ite;
		if (pWatchThread->isRunning()) {
			const CSettingInfo& otherSettingInfo = pWatchThread->getSettingInfo();
			tstring otherLogPath = otherSettingInfo.getLog();
			if (otherSettingInfo.isEnableLogging()) {
				if (_tcsicmp(logPath.c_str(), otherLogPath.c_str()) == 0) {
					alreadyUsed = true;
					break;
				}
			}
		}
	}

	if (!alreadyUsed) {
		// 他の監視スレッドでログが使用されておらず、
		// 設定ファイルにてログが消去指示されている場合はログを削除する。
		if (app.IsDeleteLog()) {
			DeleteFile(logPath.c_str());
		}
	}
}
