// FWatch.cpp : アプリケーション用のエントリ ポイントの定義
//

#include "stdafx.h"
#include "FWatchApp.hpp"
#include "resource.h"

#include "FWatchUI.hpp"
#include "ServiceEntry.hpp"
#include "Utility.hpp"
#include "StringExpander.hpp"

#include <assert.h>
#include <locale.h>

#include <vector>

#include <Commdlg.h>
#include <shlobj.h>

#include <htmlhelp.h>
#pragma comment(lib, "Htmlhelp.lib")

#include <strsafe.h>
#pragma comment(lib, "strsafe.lib")



LPCTSTR ResourceAccessor::getApplicationName() const
{
	return NULL;
}

HWND ResourceAccessor::getMainHWnd() const
{
	return NULL;
}

int ResourceAccessor::LoadString(UINT uMessageID, LPTSTR buffer, int nBufferMax) const
{
	return ::LoadString(getInstanceHandle(), uMessageID, buffer, nBufferMax);
}

tstring ResourceAccessor::LoadString(UINT uMessageID) const
{
	TCHAR buffer[4096] = {0};
	::LoadString(getInstanceHandle(), uMessageID, buffer, 4096);
	return buffer;
}

int ResourceAccessor::LoadInteger(UINT uMessageID) const
{
	return atoi(LoadString( uMessageID ).c_str());
}

tstring ResourceAccessor::LoadStringFormat(UINT uMessageID, ...) const
{
	va_list marker;
	va_start(marker, uMessageID);

	return LoadStringFormatV(uMessageID, marker);
}

tstring ResourceAccessor::LoadStringFormatV(UINT uMessageID, va_list marker) const
{
	const tstring format = LoadString(uMessageID);
	return ToStringV(format.c_str(), marker);
}

int ResourceAccessor::showMessageBox(UINT uType, LPCTSTR caption, UINT uMessageID, ...) const
{
	va_list marker;
	va_start(marker, uMessageID);

	const tstring message = LoadStringFormatV(uMessageID, marker);

	if (caption == NULL) {
		caption = getApplicationName();
	}

	return ::MessageBox(NULL, message.c_str(), caption, uType);
}

int ResourceAccessor::showMessageBox(HWND hWnd, UINT uType, LPCTSTR caption, UINT uMessageID, ...) const
{
	va_list marker;
	va_start(marker, uMessageID);

	const tstring message = LoadStringFormatV(uMessageID, marker);

	if (caption == NULL) {
		caption = getApplicationName();
	}

	return ::MessageBox(hWnd, message.c_str(), caption, uType);
}


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

Configuration::	Configuration()
	: nameMatcherIgnoreCase_(false)
	, deleteLog_(false)
	, rotateLog_(false)
	, logCloseSpan_(0)
	, currentDirSetToWatchDir_(false)
{
}

void Configuration::initCache()
{
	nameMatcherIgnoreCase_ = GetLong(_T("CONFIG"), _T("NAMEMATCHER_IGNORE_CASE"), 1) != 0;
	deleteLog_ = GetLong(_T("CONFIG"), _T("DELETELOG"), 0) != 0;
	rotateLog_ = GetLong(_T("CONFIG"), _T("ROTATELOG"), 1) != 0;
	currentDirSetToWatchDir_ = GetLong(_T("CONFIG"), _T("CURDIRECTORY"), 1) != 0;
	logCloseSpan_ = GetLong(_T("CONFIG"), _T("LOGCLOSESPAN"), 100);
	impersonatePasswordEncryption_ = GetString(_T("SECURITY"), _T("IMPERSONATE_PASSWORD_ENCRYPTION"), _T(""));
}

bool Configuration::IsNameMatcherIgnoreCase() const
{
	return nameMatcherIgnoreCase_;
}

bool Configuration::IsDeleteLog() const
{
	return deleteLog_;
}

bool Configuration::IsRotateLog() const
{
	return rotateLog_;
}

bool Configuration::IsCurrentDirSetToWatchDir() const
{
	return currentDirSetToWatchDir_;
}

long Configuration::GetLogCloseSpan() const
{
	return logCloseSpan_;
}

tstring Configuration::GetImpersonatePasswordEncryption() const
{
	return impersonatePasswordEncryption_;
}


tstring Configuration::GetString(LPCTSTR appName, LPCTSTR name, LPCTSTR defValue) const
{
	assert(appName && name && "appName, nameにnullは指定できません。");

	int bufsiz = 1024;
	std::vector<TCHAR> buf(bufsiz);
	::GetPrivateProfileString(appName, name, defValue, &buf[0], bufsiz, getIniPath().c_str());
	return tstring(&buf[0]);
}

long Configuration::GetLong(LPCTSTR appName, LPCTSTR name, int defValue) const
{
	tstring value = GetString(appName, name, _T(""));
	if (value.length() > 0) {
		return _tstol(value.c_str());
	}
	return defValue;
}


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

VersionInfoProvider::VersionInfoProvider()
{
	tstring moduleFileName = GetModuleFileName();

	DWORD siz = ::GetFileVersionInfoSize(moduleFileName.c_str(), 0);
	if (siz > 0) {
		std::vector<unsigned char> buf(siz + 1);
		LPVOID pBuf = &buf[0];

		if (::GetFileVersionInfo(moduleFileName.c_str(), 0, siz, pBuf)) {

			LPCTSTR strFileVersion = NULL;
			LPCTSTR strLegalCopyright = NULL;
			LPCTSTR strComments = NULL;

			UINT lenFileVersion = 0;
			UINT lenLegalCopyright = 0;
			UINT lenComments;
			
			VerQueryValue(pBuf, _T("\\StringFileInfo\\041104b0\\FileVersion" ),
				(LPVOID*) &strFileVersion, &lenFileVersion);
			VerQueryValue(pBuf, _T("\\StringFileInfo\\041104b0\\LegalCopyright" ),
				(LPVOID*) &strLegalCopyright, &lenLegalCopyright);
			VerQueryValue(pBuf, _T("\\StringFileInfo\\041104b0\\Comments" ),
				(LPVOID*) &strComments, &lenComments);
			
			assert(strFileVersion != NULL && "ファイルバージョンが取得できません。");
			assert(strLegalCopyright != NULL && "コピーライトが取得できません。");
			assert(strComments != NULL && "コメントが取得できません。");

			// バージョン情報をドット編集する.
			std::vector<unsigned int> versions;
			LPCTSTR pVer = strFileVersion;
			while (*pVer) {
				unsigned int ver = _tstoi(pVer);
				versions.push_back(ver);
				while (*pVer && !(*pVer == ',' || *pVer == '.')) {
					// カンマまたはピリオドまでスキップ
					pVer++;
				}
				if (*pVer) {
					pVer++;
				}
			}
			versions.resize(4);

			// バージョン情報をメンバ変数に設定する.
			fileVersion_ = ToString(_T("%d.%d.%d.%d"),
				versions[0], versions[1], versions[2], versions[3]);
			legalCopyright_ = strLegalCopyright;
			versionComment_ = strComments;
		}
	}
}

const tstring& VersionInfoProvider::getFileVersion() const
{
	return fileVersion_;
}

const tstring& VersionInfoProvider::getLegalCopyright() const
{
	return legalCopyright_;
}

const tstring& VersionInfoProvider::getVersionComment() const
{
	return versionComment_;
}


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



FWatchApp::FWatchApp()
	: hMainWnd_( NULL )
	, cl_( false )
{
}

FWatchApp::~FWatchApp()
{
}

HINSTANCE FWatchApp::getInstanceHandle() const
{
	//required:
	assert(hInstance_ != NULL && "まだ初期化されていません。");
	
	//do:
	return hInstance_;
}

LPCTSTR FWatchApp::getApplicationName() const
{
	return applicationName_.c_str();
}

HWND FWatchApp::getMainHWnd() const
{
	return hMainWnd_;
}

CWatchThreadGroup& FWatchApp::getCL()
{
	return cl_;
}

void FWatchApp::initDefaultPath()
{
	// ヘルプファイルの位置
	helpFile_ = GetBaseFileNameWithExtention(_T("chm"));

	// デフォルトの設定ファイル
	iniPath_ = GetBaseFileNameWithExtention(_T("ini"));

	// デフォルトの監視リストファイル
	infPath_ = GetBaseFileNameWithExtention(_T("lst"));

	// モジュールディレクトリ
	moduleDir_ = EnsureLast(GetDirName(GetBaseFileNameWithExtention(NULL)), _T("\\"));

	// APPDATA環境変数からユーザ別設定ファイル格納先(C:\Users\XXXXX\AppData\Roaming\fwatch.ini\)を決定する。
	tstring appData;
	if (GetEnvironmentVariable(_T("APPDATA"), appData)) {
		TCHAR szUserDataDir[MAX_PATH] = {0};
		PathCombine(szUserDataDir, appData.c_str(), GetBaseName(iniPath_).c_str());
		PathAddBackslash(szUserDataDir);
		userDataDir_ = szUserDataDir;
	}
	if (userDataDir_.empty()) {
		userDataDir_ = moduleDir_;
	}

	// ユーザ別設定ファイル格納先にiniファイルがあれば、それを優先する。
	tstring userIniPath = userDataDir_ + GetBaseName(iniPath_);
	if (PathFileExists(userIniPath.c_str())) {
		iniPath_ = userIniPath;
	}

	// iniファイルのディレクトリ
	iniDir_ = EnsureLast(GetDirName(iniPath_), _T("\\"));

	// ユーザ別設定ファイル格納先にiniまたはlstファイルがあれば、
	// ユーザ別設定ファイル格納先に監視リストファイルがあるものとする。
	tstring userInfPath = userDataDir_ + GetBaseName(infPath_);
	if (PathFileExists(userIniPath.c_str()) || PathFileExists(userInfPath.c_str())) {
		infPath_ = userInfPath;
	}

	// infファイルのディレクトリ
	infDir_ = EnsureLast(GetDirName(infPath_), _T("\\"));
}

void FWatchApp::setAppConfVariablesTo(PropertyStringExpander& varExpander, bool useInfDir)
{
	varExpander.addProperty(_T("INIDIR"), iniDir_);
	varExpander.addProperty(_T("CONFDIR"), userDataDir_);
	varExpander.addProperty(_T("MODULEDIR"), moduleDir_);
	if (useInfDir) {
		varExpander.addProperty(_T("LSTDIR"), infDir_);
	}
}

int FWatchApp::run(HINSTANCE, LPCTSTR v_lpCmdLine)
{
	//required:
	assert(v_lpCmdLine != NULL && "v_lpCmdLineにnullは指定できません。");

	//do:

	// ロケールの設定
	setlocale(LC_ALL, "");

	// 基本設定の初期化
	hInstance_ = ::GetModuleHandle(NULL);
	applicationName_ = app.LoadString(IDS_APP_TITLE);

	// デフォルトのパス
	initDefaultPath();

	// パラメーターの取得
	const int cmdLineBufSiz = lstrlen(v_lpCmdLine) + 1;
	std::vector<TCHAR> cmdLineBuf(cmdLineBufSiz);
	_tcscpy_s(&cmdLineBuf[0], cmdLineBufSiz, v_lpCmdLine);

	const int argvMx = 16;
	LPTSTR argv[argvMx];
	const int argc = GetCmdArgs(&cmdLineBuf[0], argv, argvMx);

	// 起動モード
	enum CMDTYPE
	{
		CMDTYPE_NONE     = 0,
		CMDTYPE_REGIST   = 1,
		CMDTYPE_UNREGIST = 2,
		CMDTYPE_NORMAL   = 3,
		CMDTYPE_SERVICE  = 4,
		CMDTYPE_UNKNOWN  = 9
	} cmdtyp = CMDTYPE_NONE;

	// ファイルダイアログでユーザに対話的にinfファイルを選ぶ場合
	bool bChooseInfFile = false;

	// オプションの解析
	int idx;
	for (idx = 0 ; idx < argc ; idx++) {
		if (argv[idx][0] == '/') {
			// サービス関連
			if ( !_tcsicmp(argv[idx], _T("/service")))			cmdtyp = CMDTYPE_REGIST;
			else if ( !_tcsicmp(argv[idx], _T("/unregister")))	cmdtyp = CMDTYPE_UNREGIST;
			else if ( !_tcsicmp(argv[idx], _T("/noservice")))	cmdtyp = CMDTYPE_NORMAL;
			else if ( !_tcsicmp(argv[idx], _T("/runservice")))	cmdtyp = CMDTYPE_SERVICE;

			// オプションに設定ファイルが指定されていれば設定ファイルの差し替え
			else if (_tcsstr(argv[idx], _T("/ini:")) == argv[idx]) {
				LPCTSTR iniPathInArg = argv[idx] + 5; // 「/ini:」分進めるために+5
				if (::GetFileAttributes(iniPathInArg) == -1) {
					showMessageBox(MB_ICONERROR | MB_OK, NULL, IDS_OPTION_INI_NOTFOUND, iniPathInArg);
					return 1;
				}
				iniPath_ = iniPathInArg;
				iniDir_ = EnsureLast(GetDirName(iniPath_), _T("\\"));
			}

			// ファイルダイアログによる選択
			else if (_tcsstr(argv[idx], _T("/nodefault")) == argv[idx]) {
				if (cmdtyp != CMDTYPE_SERVICE) {
					// サービス時にはダイアログは開けないので無視する。
					// そうでなければユーザによる対話的な選択とする。
					bChooseInfFile = true;
				}
			}

			else {
				showMessageBox(MB_ICONERROR | MB_OK, NULL, IDS_OPTION_FAIL, argv[idx]);
				return 1;
			}
		}
	}

	// 設定ファイル上のデフォルトの監視リストファイル位置を取得する。
	tstring defaultInfPath = GetString(_T("CONFIG"), _T("DEFAULT_LST_FILE"), _T(""));
	if ( !defaultInfPath.empty()) {
		PropertyStringExpander varExpander;
		setAppConfVariablesTo(varExpander, false);
		tstring defInfPath = StrTrim(varExpander.expandString(defaultInfPath));
		if ( !defInfPath.empty()) {
			if (defInfPath.find('*') != tstring::npos) {
				// アスタリスクがある場合は手動選択
				bChooseInfFile = true;

			} else {
				infPath_ = defInfPath;
				infDir_ = EnsureLast(GetDirName(infPath_), _T("\\"));
			}
		}
	}

	// オプションでない最初の引数が存在すれば、監視リストファイルとして採用する。
	for (idx=0; idx < argc; idx++) {
		if (argv[idx][0] != '/') {
			tstring tmp = StrTrim(tstring(argv[idx]));
			if ( !tmp.empty()) {
				infPath_ = tmp;
				infDir_ = EnsureLast(GetDirName(infPath_), _T("\\"));
			}
			break;
		}
	}

	// 手動で監視リストファイルを選択する。
	if (bChooseInfFile) {
		OPENFILENAME  ofn = {0};
		const DWORD bufsiz = MAX_PATH * 2;
		TCHAR szChooseInfPath[bufsiz] = {0};
		ofn.lStructSize = sizeof(OPENFILENAME);
		ofn.lpstrFilter = _T("lst(*.lst)\0*.lst\0All Files\0*.*\0");
		ofn.lpstrFile = szChooseInfPath;
		ofn.nMaxFile = bufsiz;
		ofn.lpstrInitialDir = iniDir_.c_str();
		ofn.lpstrDefExt = _T("lst");
		ofn.Flags = OFN_ENABLESIZING | OFN_HIDEREADONLY;
		if (!GetOpenFileName(&ofn)) {
			// キャンセルされた場合
			return 2;
		}
		infPath_ = ofn.lpstrFile;
		infDir_ = EnsureLast(GetDirName(infPath_), _T("\\"));
	}

	// 設定ファイルのメモリへの読み込み
	initCache();


	// パラメーターをチェックし、サービスとして登録するか検査します。
	if (cmdtyp == CMDTYPE_REGIST) {
		// サービスとして登録して終了
		return RegistService();
	}
	else if (cmdtyp == CMDTYPE_UNREGIST) {
		// サービスの登録解除して終了
		return UnregistService();
	}

	// サービスの準備
	ServiceInit();

	BOOL bStartService = FALSE;
	const BOOL bRegistService = IsRegistService(bStartService);

	if (cmdtyp == CMDTYPE_SERVICE) {
		// サービスプロセスの起動引数の場合 (サービス登録時に設定される引数)
		if ( !bRegistService) {
			// サービスとして登録されていない場合
			app.showMessageBox(MB_SERVICE_NOTIFICATION | MB_ICONERROR | MB_OK, NULL, IDS_MAINPROC_SERVICENOREG);
		}
		else if (bStartService) {
			// サービスが既に実行されている場合
			app.showMessageBox(MB_SERVICE_NOTIFICATION | MB_ICONERROR | MB_OK, NULL, IDS_SERVICE_RUNNING);
		}
		else if ( !StartService()) {
			// サービスが起動できなかった場合
			app.showMessageBox(MB_SERVICE_NOTIFICATION | MB_ICONERROR | MB_OK, NULL, IDS_SERVICE_STARTERROR);
		}
	}
	else {
		// サービスプロセスとして起動されていない場合 (通常のプロセスとして実行)
		FWatchUI ui;
		if ( !ui.create()) {
			// UIの構築に失敗
			app.showMessageBox(MB_ICONERROR, NULL, IDS_UI_CREATIONFAILED);
		}
		else {
			// スタンドアロンで監視の開始
			cl_.LoadInfData(getInfPath());
			cl_.startAll();

			// 起動時の画面状態
			DWORD showOnStartup = GetLong(_T("CONFIG"), _T("SHOWONSTARTUP"), 0);
			if ((showOnStartup == 1 && !cl_.isRunning()) || (showOnStartup >= 2)) {
				ui.showEditDialog();
			}

			// ヘルプの準備
			DWORD dwCookie = 0;
			HtmlHelp(NULL, NULL, HH_INITIALIZE, (DWORD_PTR) &dwCookie);

			// メッセージループ
			MSG msg;
			while (GetMessage(&msg, NULL, 0, 0)) {
				if (!HtmlHelp(NULL, NULL, HH_PRETRANSLATEMESSAGE, (DWORD_PTR) &msg)) {
					TranslateMessage(&msg);
					DispatchMessage(&msg);
				}
			}

			// ヘルプ終了
			HtmlHelp(NULL, NULL, HH_UNINITIALIZE, (DWORD) dwCookie);

			// 監視終了
			cl_.ReleaseAllThreads();
		}
	}

	return 0;
}

const tstring& FWatchApp::getInfPath() const
{
	return infPath_;
}

const tstring& FWatchApp::getInfDir() const
{
	return infDir_;
}

const tstring& FWatchApp::getIniPath() const
{
	return iniPath_;
}

const tstring& FWatchApp::getIniDir() const
{
	return iniDir_;
}

void FWatchApp::showHelp(HWND hWnd) const
{
	HtmlHelp(hWnd, helpFile_.c_str(), HH_DISPLAY_TOC, NULL);
}

void FWatchApp::showContextHelp(HWND hWnd, UINT helpid, POINT pt) const
{
	RECT margin = {8, 8, 8, 8};
	HH_POPUP popup = {0};
	popup.cbStruct = sizeof(HH_POPUP);
	popup.hinst = NULL;
	popup.idString = helpid;
	popup.pt = pt;
	popup.clrForeground = (COLORREF) -1;
	popup.clrBackground = (COLORREF) -1;
	popup.rcMargins = margin;
	popup.pszFont = _T("MS UI Gothic,10");

	tstring popupText = _T("::/popup.txt");
	HtmlHelp(hWnd, (helpFile_ + popupText).c_str(), HH_DISPLAY_TEXT_POPUP, (DWORD_PTR) &popup);
}

FWatchApp app;


/*!
 * エントリポイント
 * \param hInstance インスタンス
 * \return 終了コード
 */
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int)
{
	// デバッグフラグ(_DEBUG時のみ有効)
	// ヒープの破壊が確認された場合は検出されたヒープの割り当て番号を
	// _crtBreakAllocをデバッグモードのウオッチで使用する.
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_CHECK_ALWAYS_DF | _CRTDBG_LEAK_CHECK_DF);

	// ランタイムの初期設定
	setlocale(LC_ALL, "");

	// COMの初期化
	if (FAILED(::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED))) {
		app.showMessageBox(MB_ICONERROR | MB_OK, NULL, IDS_MAINPROC_COINITFAILED);
		return 1;
	}

	// コマンドラインの取得
	LPCTSTR lpCmdLine = GetCommandLine();
	lpCmdLine = GetToken(NULL, lpCmdLine, 0); // 最初の要素(実行ファイル名)を読み飛ばす.

	// メイン
	const int result = app.run(hInstance, lpCmdLine);

	// COMの初期化解除
	::CoUninitialize();
	return result;
}
