#include "stdafx.h"

#include "FWatchApp.hpp"
#include "Logger.hpp"

#include "Utility.hpp"

#include <vector>
#include <string>
#include <memory>

#include <assert.h>

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


int FileLogger::GetLogThreshold() const
{
	return logThreshold_;
}

void FileLogger::SetLogThreshold(int logLevel)
{
	logThreshold_ = logLevel;
}


FileLogger::FileLogger()
	: enabled_(false)
	, logThreshold_(0)
	, lastWrite_(FILETIME())
	, hLogFile_(INVALID_HANDLE_VALUE)
	, logCloseSpan_(0)
	, lockTryLimit_(0)
{
	init();
}

FileLogger::FileLogger(LPCTSTR logPath)
	: enabled_(logPath && *logPath)
	, logThreshold_(0)
	, lastWrite_(FILETIME())
	, hLogFile_(INVALID_HANDLE_VALUE)
	, logPath_(logPath)
	, logCloseSpan_(0)
	, lockTryLimit_(0)
{
	init();
}

void FileLogger::init()
{
	lockTryLimit_ = (int) app.GetLong(_T("CONFIG"), _T("LOGEXCLUSIVELOCK"), 30);
	logCloseSpan_ = app.GetLogCloseSpan();
}

FileLogger::~FileLogger() throw()
{
	Close();
}

bool FileLogger::IsOpened() const
{
	return hLogFile_ != INVALID_HANDLE_VALUE;
}

bool FileLogger::Open()
{
	// すでに開いていれば、何もしない。
	if (IsOpened()) {
		return true;
	}

	// ログが無効であるか、ログファイルが指定されていなければ何もしない
	LPCTSTR logpath = GetLogPath();
	if ( !IsEnableLogging() || !logpath || !*logpath) {
		return false;
	}

	// ファイルを追記モードで開く。なければ作成する。
	// 読み書き共有モードで、同一ログファイル名で複数スレッドからアクセスできるようにする。
	hLogFile_ = ::CreateFile(
		logpath,
		GENERIC_WRITE,
		FILE_SHARE_READ | FILE_SHARE_WRITE,
		NULL,
		OPEN_ALWAYS,
		FILE_ATTRIBUTE_NORMAL,
		NULL
		);
	return hLogFile_ != INVALID_HANDLE_VALUE;
}

void FileLogger::Close()
{
	if (hLogFile_ != INVALID_HANDLE_VALUE) {
		LogMessage(10, NULL, 0, _T("close log."));
		::CloseHandle(hLogFile_);
		hLogFile_ = INVALID_HANDLE_VALUE;
	}
}

LPCTSTR FileLogger::GetLogPath() const throw()
{
	return logPath_.c_str();
}

void FileLogger::SetLogPath(LPCTSTR logPath) throw()
{
	logPath_ = logPath ? logPath : _T("");
}

void FileLogger::SetEnableLogging(bool enabled) throw()
{
	enabled_ = enabled;
}

bool FileLogger::IsEnableLogging() const throw()
{
	return enabled_ && logPath_.length() > 0;
}


/*!
 * ログファイルに書き込む。
 * ログが有効になっていないか、ログファイル名が空の場合は出力しない。
 * また、ログの書き込みに失敗してもエラーは返さない。
 * ログファイルはログを書き込むたびに追記モードでオープンして書き込み後即座にクローズされる。
 * 書き込みはログファイル名が複数の監視スレッドで同一である場合を想定して書き込みは排他ロックがかけられる。
 * \param logLevel ログレベル
 * \param systime タイムスタンプ、nullの場合は現在時刻が使用される。
 * \param errcode Win32のエラーコード、0以外の場合はログに出力される。
 * \param mes メッセージ1、nullの場合は空文字
 */
void FileLogger::LogMessage(int logLevel, const SYSTEMTIME* systime, DWORD errcode, LPCTSTR mes)
{
	// 出力するログのログレベルが閾値と等しいか小さい(優先度が高い)場合のみ出力する。
	if (logLevel > logThreshold_) {
		return;
	}

	// ファイルを開く(必要なら)
	if (!Open()) {
		// 開けなかった場合、もしくはログ出力しない場合はただちに戻る。
		return;
	}

	// ロック開始
	// (同一プロセスにおける排他処理、デストラクタの実行でロック解除される。)
	tstring lockname = _T("lock:");
	lockname += GetLogPath();
	ToLower(lockname);

	std::auto_ptr<LockObject> lock(app.GetLockObject(lockname));

	// ログファイルの日付が変ったらローテートする。
	if (app.IsRotateLog()) {
		RotateLog();
	}

	// ファイルを排他ロックする
	// (同一ログファイルに対する多重書き込み時に重ならないようにするため。)
	// (Windows9xでは無効)
	FileLockObject fileLock(hLogFile_, lockTryLimit_);

	// メッセージを構築しログファイルに出力する。
	InternalLogMessage(logLevel, systime, errcode, mes);


	// 都度閉じる指示があれば、毎回閉じる。
	if (logCloseSpan_ == 0) {
		// 再帰になるのでCloseメソッドは呼ばない。
		::CloseHandle(hLogFile_);
		hLogFile_ = INVALID_HANDLE_VALUE;
	}
	else {
		// 都度閉じなければフラッシュする。
		FlushFileBuffers(hLogFile_);
	}

	// 最後に書き込んだ時刻をセットする。
	GetSystemTimeAsFileTime(&lastWrite_);
}

/*!
 * 内部用のログ出力メソッド。
 * ログメッセージの構築とファイル出力を行う。
 * 排他ロック制御やファイルのオープン処理は呼び出しもとで処理されている前提としている。
 */
void FileLogger::InternalLogMessage(int logLevel, const SYSTEMTIME* systime, DWORD errcode, LPCTSTR mes)
{
	// 出力するログのログレベルが閾値と等しいか小さい(優先度が高い)場合のみ出力する。
	// (しかし、ログファイルがオープンされていなければ何もしない。)
	if (logLevel > logThreshold_ || hLogFile_ == INVALID_HANDLE_VALUE) {
		return;
	}

#ifdef _UNICODE
	// UNICODEの場合でファイルの先頭であればBOMを書く (UTF-16 メモ帳互換)
	ULARGE_INTEGER siz;
	siz.LowPart = GetFileSize(hLogFile_, &siz.HighPart);
	if (siz.LowPart != -1 || GetLastError() == NO_ERROR) {
		// エラーではない
		if (siz.QuadPart == 0) {
			// ファイルサイズがゼロである
			unsigned char bom[] = {0xff, 0xfe};
			DWORD wd;
			WriteFile(hLogFile_, (LPVOID) bom, 2, &wd, NULL);
		}
	}
#endif

	// メッセージが省略された場合は空文字とする。
	if (mes == NULL) {
		mes = _T("");
	}

	// 時刻を省略した場合は現在時刻を取得する.
	SYSTEMTIME tmp = {0};
	if (systime == NULL) {
		GetLocalTime(&tmp);
		systime = &tmp;
	}

	// ログメッセージバッファ
	int bufsiz = lstrlen(mes) + 64;
	if (bufsiz < 512) {
		bufsiz = 512;
	}
	std::vector<TCHAR> buf(bufsiz);

	// ログメッセージを構築する.
	HRESULT hr;
	hr = StringCbPrintf(
		&buf[0],
		bufsiz * sizeof(TCHAR),
		_T("*%04d/%02d/%02d %02d:%02d:%02d.%03d (%d) [%04x] %s\r\n"),
		systime->wYear,
		systime->wMonth,
		systime->wDay,
		systime->wHour,
		systime->wMinute,
		systime->wSecond,
		systime->wMilliseconds,
		logLevel,
		::GetCurrentThreadId(),
		mes
		);
	if (FAILED(hr)) {
		assert(false && "メッセージの構築に失敗しました。");
		return;
	}

	// 末尾にシークする.
	::SetFilePointer(hLogFile_, 0, NULL, FILE_END);

	// 書き込み
	DWORD wd;
	WriteFile(hLogFile_, &buf[0], sizeof(TCHAR) * lstrlen(&buf[0]), &wd, NULL);

	// エラーコードからエラーメッセージの生成
	if (errcode != 0) {
		tstring errmsg = ToErrorMessage(errcode);
		size_t bufsiz2 = errmsg.length() + 64;
		std::vector<TCHAR> buf2(bufsiz2);
		hr = StringCbPrintf(
			&buf2[0],
			bufsiz2 * sizeof(TCHAR),
			_T("ERROR CODE=%d %s\r\n"),
			errcode,
			errmsg.c_str()
			);
		if (FAILED(hr)) {
			assert(false && "エラーメッセージの構築に失敗しました。");
		}
		WriteFile(hLogFile_, &buf2[0], sizeof(TCHAR) * lstrlen(&buf2[0]), &wd, NULL);
	}
}

/*!
 * メッセージIDによるログファイルに書き込む。
 * それ以外は他のメソッドと同様である。
 * \param logLevel ログレベル
 * \param systime タイムスタンプ、nullの場合は現在時刻が使用される。
 * \param errcode エラーコード
 * \param messageID メッセージID
 */
void FileLogger::LogMessage(int logLevel, const SYSTEMTIME* systime, DWORD errcode, DWORD messageID, ...)
{
	// 出力するログのログレベルが閾値と等しいか小さい(優先度が高い)場合のみ出力する。
	if (logLevel > logThreshold_) {
		return;
	}

	// ログが無効であるか、ログファイルが指定されていなければ何もしない
	LPCTSTR logpath = GetLogPath();
	if ( !IsEnableLogging() || !logpath || !*logpath) {
		return;
	}

	// メッセージをフォーマットしたあと、ログライタに送る
	va_list marker;
	va_start(marker, messageID);

	tstring format = app.LoadStringFormatV(messageID, marker);
	LogMessage(logLevel, systime, errcode,format.c_str());
}

/*!
 * ログの日付が変更されていればローテートします。
 * ファイルが開かれてなければ開きます。
 * 自動的に閉じることはありません。
 */
void FileLogger::RotateLog()
{
	if (!Open()) {
		// ファイルを開けないか、ログ出力しない場合は何もしない。
	}

	// ファイルの日付が本日でなければバックアップする。
	FILETIME lastModified = {0};
	if (GetFileTime(hLogFile_, NULL, NULL, &lastModified)) {

		// 日付が読み取れた場合のみ。
		FILETIME localLastModified = {0};
		FileTimeToLocalFileTime(&lastModified, &localLastModified);
		SYSTEMTIME today = {0}, fileTime = {0};
		GetLocalTime(&today);
		FileTimeToSystemTime(&localLastModified, &fileTime);

		if (today.wYear != fileTime.wYear
			|| today.wMonth != fileTime.wMonth
			|| today.wDay != fileTime.wDay) {

			// コピーする前に現在時刻に設定する。
			SystemTimeToFileTime(&today, &localLastModified);
			LocalFileTimeToFileTime(&localLastModified, &lastModified);
			SetFileTime(hLogFile_, NULL, &lastModified, &lastModified);

			// ファイルの日付が本日でないのでbakファイルを作成する。
			LPCTSTR logpath = GetLogPath();
			assert(logpath && "ログファイル名は必須です。");

			tstring bakFile = logpath;
			bakFile += _T(".bak");

			if (CopyFile(logpath, bakFile.c_str(), FALSE)) {

				// コピーに成功したら、このファイルを切り詰める
				SetFilePointer(hLogFile_, 0, NULL, FILE_BEGIN);
				SetEndOfFile(hLogFile_);
			}
		}
	}
}

const FILETIME& FileLogger::GetLastWrite() const
{
	return lastWrite_;
}

void FileLogger::Tick(const FILETIME &ftNow)
{
	if (logCloseSpan_ > 0 && IsOpened()) {
		if (IsExpired(ftNow, logCloseSpan_, lastWrite_)) {
			Close();
		}
	}
}


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


FileLockObject::FileLockObject(HANDLE hFile, int maxretry)
	: hFile_(hFile)
	, locked_(false)
{
	assert(hFile_ && hFile_ != INVALID_HANDLE_VALUE && "ファイルハンドルが不正です");

	// ファイルを排他ロックする
	// (同一ログファイルに対する多重書き込み時に重ならないようにするため。)
	// (Windows9xでは無効)

	ZeroMemory(&overlapped_, sizeof(OVERLAPPED));
	overlapped_.Offset = 0;
	overlapped_.OffsetHigh = 0;
	overlapped_.hEvent = NULL;

	locked_ = false;
	for (int retry = 0; retry < maxretry; retry++) {
		if (LockFileEx(hFile_, LOCKFILE_FAIL_IMMEDIATELY | LOCKFILE_EXCLUSIVE_LOCK, 0, (DWORD) -1, (DWORD) -1, &overlapped_)) {
			locked_ = true;
			break;
		}
		DWORD err = GetLastError();
		if (err != ERROR_IO_PENDING) {
			break;
		}
		Sleep(100);
	}
}

FileLockObject::~FileLockObject()
{
	Release();
}

void FileLockObject::Release()
{
	if (locked_) {
		locked_ = false;
		UnlockFileEx(hFile_, 0, (DWORD) -1, (DWORD) -1, &overlapped_);
	}
}
