//=======================================================================
//
//  Copyright (c) 1998-2001 Microsoft Corporation.  All Rights Reserved.
//
//  File:   FreeLog.cpp
//
//  Owner:  KenSh
//
//  Description:
//
//      Runtime logging for use in both checked and free builds.
//
//=======================================================================

#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <malloc.h>
#include "FreeLog.h"
#include <MISTSAFE.h>

#ifndef _countof
#define _countof(ar) (sizeof(ar)/sizeof((ar)[0]))
#endif

// Unicode files start with the 2 bytes { FF FE }.
// This is the little-endian version of those 2 bytes.
#define UNICODE_FILE_HEADER 0xFEFF

#define MUTEX_TIMEOUT       1000    // Don't wait more than 1 second to write to logfile
#define MAX_MUTEX_WAITS     4       // Don't keep trying after this many failures

#define LOG_FILE_BIG_SIZE   50000   // Don't bother trimming if file is smaller than this
#define LOG_LINES_TRIM_FROM 1000    // Start trimming if more than this many lines
#define LOG_LINES_TRIM_TO   750     // Trim til the log file is this many lines

#define LOG_LEVEL_SUCCESS   0
#define LOG_LEVEL_FAILURE   1

#define MAX_MSG_LENGTH (MAX_PATH + 20)
#define MAX_ERROR_LENGTH 128

static const TCHAR c_szUnknownModuleName[] = _T("?");

// Local functions
void LogMessageExV(UINT nLevel, DWORD dwError, LPCSTR pszFormatA, va_list args);





//============================================================================
//
// Private CFreeLogging class to keep track of log file resources
//
//============================================================================

class CFreeLogging
{
public:
	CFreeLogging(LPCTSTR pszModuleName, LPCTSTR pszLogFileName);
	~CFreeLogging();

	void WriteLine(LPCTSTR pszText, UINT nLevel, DWORD dwError);

private:
	inline static HANDLE CreateMutex(LPCTSTR pszMutexName);
	inline HANDLE OpenLogFile(LPCTSTR pszFileName);
	inline void CloseLogFile();
	void TrimLogFile();

	BOOL AcquireMutex();
	void ReleaseMutex();

private:
	HANDLE m_hFile;
	HANDLE m_hMutex;
	int m_cLinesWritten;
	int m_cFailedWaits;
	LPTSTR m_pszModuleName;
};
CFreeLogging* g_pFreeLogging;

//============================================================================
//
// Public functions
//
//============================================================================

void InitFreeLogging(LPCTSTR pszModuleName, LPCTSTR pszLogFileName)
{
	if (g_pFreeLogging == NULL)
	{
		g_pFreeLogging = new CFreeLogging(pszModuleName, pszLogFileName);
	}
}

void TermFreeLogging()
{
	delete g_pFreeLogging;
	g_pFreeLogging = NULL;
}

void LogMessage(LPCSTR pszFormatA, ...)
{
	va_list arglist;
	va_start(arglist, pszFormatA);
	LogMessageExV(LOG_LEVEL_SUCCESS, 0, pszFormatA, arglist);
	va_end(arglist);
}

void LogError(DWORD dwError, LPCSTR pszFormatA, ...)
{
	va_list arglist;
	va_start(arglist, pszFormatA);
	LogMessageExV(LOG_LEVEL_FAILURE, dwError, pszFormatA, arglist);
	va_end(arglist);
}


void LogMessageExV(UINT nLevel, DWORD dwError, LPCSTR pszFormatA, va_list args)
{
	if (g_pFreeLogging != NULL)
	{
		char szBufA[MAX_MSG_LENGTH];
		
		size_t nRem=0;
		StringCchVPrintfExA(szBufA, _countof(szBufA), NULL, &nRem, MISTSAFE_STRING_FLAGS, pszFormatA, args);
		int cchA = _countof(szBufA) - nRem;

#ifdef UNICODE
		WCHAR szBufW[MAX_MSG_LENGTH];
		MultiByteToWideChar(CP_ACP, 0, szBufA, cchA+1, szBufW, _countof(szBufW));
		g_pFreeLogging->WriteLine(szBufW, nLevel, dwError);
#else
		g_pFreeLogging->WriteLine(szBufA, nLevel, dwError);
#endif
	}
}

//============================================================================
//
// CFreeLogging implementation
//
//============================================================================

CFreeLogging::CFreeLogging(LPCTSTR pszModuleName, LPCTSTR pszLogFileName)
	: m_cFailedWaits(0),
	  m_cLinesWritten(0)
{
	m_pszModuleName = _tcsdup(pszModuleName);
	if (m_pszModuleName == NULL)
		m_pszModuleName = (LPTSTR)c_szUnknownModuleName;

	m_hMutex = CreateMutex(pszLogFileName);
	m_hFile = OpenLogFile(pszLogFileName);
}

CFreeLogging::~CFreeLogging()
{
	CloseLogFile();
	if (m_hMutex != NULL)
		CloseHandle(m_hMutex);

	if (m_pszModuleName != c_szUnknownModuleName)
		free(m_pszModuleName);
}

inline HANDLE CFreeLogging::CreateMutex(LPCTSTR pszMutexName)
{
	// Create a mutex in the global namespace (works across TS sessions)
	HANDLE hMutex = ::CreateMutex(NULL, FALSE, pszMutexName);
	return hMutex;
}

inline HANDLE CFreeLogging::OpenLogFile(LPCTSTR pszLogFileName)
{
	HANDLE hFile = INVALID_HANDLE_VALUE;

	TCHAR szPath[MAX_PATH+1];

	int cch = GetWindowsDirectory(szPath, _countof(szPath)-1);
		
	if(cch >0)
	{
		if (szPath[cch-1] != _T('\\'))
			szPath[cch++] = _T('\\');

		HRESULT hr = StringCchCopyEx(szPath + cch, _countof(szPath)-cch, pszLogFileName, NULL, NULL, MISTSAFE_STRING_FLAGS);

		if(FAILED(hr))
			return hFile;

		hFile = CreateFile(szPath, GENERIC_READ | GENERIC_WRITE, 
						FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, 
						OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	}


#ifdef UNICODE
	if (hFile != INVALID_HANDLE_VALUE)
	{
		if (AcquireMutex())
		{
			//
			// Check for the unicode header { FF FE }
			//
			WORD wHeader = 0;
			DWORD cbRead;
			(void)ReadFile(hFile, &wHeader, sizeof(wHeader), &cbRead, NULL);

			//
			// Write the header if there isn't one. This may be due to the
			// file being newly created, or to an ANSI-formatted file.
			//
			if (wHeader != UNICODE_FILE_HEADER)
			{
				SetFilePointer(hFile, 0, NULL, FILE_BEGIN);

				DWORD cbWritten;
				wHeader = UNICODE_FILE_HEADER;
				WriteFile(hFile, &wHeader, sizeof(wHeader), &cbWritten, NULL);
				SetEndOfFile(hFile);
			}

			ReleaseMutex();
		}
	}
#endif

	return hFile;
}

inline void CFreeLogging::CloseLogFile()
{
	if (m_hFile != INVALID_HANDLE_VALUE)
	{
		// Trim old stuff from the log before closing the file
		TrimLogFile();

		CloseHandle(m_hFile);
		m_hFile = INVALID_HANDLE_VALUE; 
	}
}

BOOL CFreeLogging::AcquireMutex()
{
	// In rare case where mutex not created, we allow file operations
	// with no synchronization
	if (m_hMutex == NULL)
		return TRUE;

	// Don't keep waiting if we've been blocked in the past
	if (m_cFailedWaits >= MAX_MUTEX_WAITS)
		return FALSE;

	BOOL fResult = TRUE;
	if (WaitForSingleObject(m_hMutex, MUTEX_TIMEOUT) != WAIT_OBJECT_0)
	{
		fResult = FALSE;
		m_cFailedWaits++;
	}

	return fResult;
}

void CFreeLogging::ReleaseMutex()
{
	if (m_hMutex != NULL) // Note: AcquireMutex succeeds even if m_hMutex is NULL
	{
		::ReleaseMutex(m_hMutex);
	}
}

void CFreeLogging::WriteLine(LPCTSTR pszText, UINT nLevel, DWORD dwError)
{
	if (m_hFile != INVALID_HANDLE_VALUE)
	{
		DWORD cbText = lstrlen(pszText) * sizeof(TCHAR);

		if (AcquireMutex())
		{
			DWORD cbWritten;

			SetFilePointer(m_hFile, 0, NULL, FILE_END);

			//
			// Write time/date/module as a prefix
			//
			//     2001-05-03 13:49:01  21:49:01   CDM      Failed   Loading module (Error 0x00000005: Access is denied.)
			//
			// NOTE: ISO 8601 format for date/time. Local time first, then GMT.
			//
			TCHAR szPrefix[60];
			SYSTEMTIME sysTime, gmtTime;
			GetLocalTime(&sysTime);
			GetSystemTime(&gmtTime);
			LPCTSTR pszStatus = (nLevel == LOG_LEVEL_SUCCESS) ? _T("Success") : _T("Error  ");

			StringCchPrintfEx(szPrefix, _countof(szPrefix), NULL, NULL, MISTSAFE_STRING_FLAGS,
				_T("%04d-%02d-%02d %02d:%02d:%02d  %02d:%02d:%02d   %s   %-13s  "),
					sysTime.wYear, sysTime.wMonth, sysTime.wDay, 
					sysTime.wHour, sysTime.wMinute, sysTime.wSecond,
					gmtTime.wHour, gmtTime.wMinute, gmtTime.wSecond,
					pszStatus, m_pszModuleName);
			
			WriteFile(m_hFile, szPrefix, lstrlen(szPrefix) * sizeof(TCHAR), &cbWritten, NULL);

			//
			// Write the message followed by error info (if any) and a newline
			//
			WriteFile(m_hFile, pszText, cbText, &cbWritten, NULL);

			if (nLevel != LOG_LEVEL_SUCCESS)
			{
				TCHAR szError[MAX_ERROR_LENGTH];
				HRESULT hr=S_OK;
				size_t nRem=0;

				// nRem contains the remaining characters in the buffer including the null terminator
				// To get the number of characters written in to the buffer we use
				// int cchErrorPrefix = _countof(szError) - nRem;

				StringCchPrintfEx(szError, _countof(szError), NULL, &nRem, MISTSAFE_STRING_FLAGS, _T(" (Error 0x%08X: "), dwError);

				// Get the number of characters written in to the buffer
				int cchErrorPrefix = _countof(szError) - nRem;
				int cchErrorText = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwError, 0, 
												 szError + cchErrorPrefix, _countof(szError) - cchErrorPrefix - 1, NULL);
				int cchError = cchErrorPrefix + cchErrorText;
				cchError -= 2; // backup past ": " or "\r\n"

				StringCchCopyEx(szError + cchError, _countof(szError)-cchError, _T(")"), NULL, NULL, MISTSAFE_STRING_FLAGS);

				WriteFile(m_hFile, szError, (cchError + 1) * sizeof(TCHAR), &cbWritten, NULL);
			}

			WriteFile(m_hFile, _T("\r\n"), 2 * sizeof(TCHAR), &cbWritten, NULL);

			//
			// If we've written a ton of stuff, trim now rather than waiting
			// for the module to unload. (This check is only for how much this
			// module has written, not how big the log file itself is.)
			//
			if (++m_cLinesWritten > LOG_LINES_TRIM_FROM)
			{
				TrimLogFile();
				m_cLinesWritten = LOG_LINES_TRIM_TO;
			}

			ReleaseMutex();
		}
	}
}

// Checks the size of the log file, and trims it if necessary.
void CFreeLogging::TrimLogFile()
{
	if (AcquireMutex())
	{
		DWORD cbFile = GetFileSize(m_hFile, NULL);

		if (cbFile > LOG_FILE_BIG_SIZE)
		{
			DWORD cbFileNew = cbFile;

			//
			// Create a memory-mapped file so we can use memmove
			//
			HANDLE hMapping = CreateFileMapping(m_hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
			if (hMapping != NULL)
			{
				LPTSTR pszFileStart = (LPTSTR)MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, 0);
				if (pszFileStart != NULL)
				{
					LPTSTR pszEnd = (LPTSTR)((LPBYTE)pszFileStart + cbFile);

					LPTSTR pszTextStart = pszFileStart;
			#ifdef UNICODE
					pszTextStart++; // skip the 2-byte header
			#endif

					//
					// Count newlines
					//
					int cLines = 0;
					for (LPTSTR pch = pszTextStart; pch < pszEnd; )
					{
						if (*pch == _T('\n'))
							cLines++;

						// REVIEW: in Ansi builds should we call CharNextExA?
						//   If so, what code page is the log file in?
						pch++;
					}

					if (cLines > LOG_LINES_TRIM_FROM)
					{
						int cTrimLines = cLines - LOG_LINES_TRIM_TO;
						for (pch = pszTextStart; pch < pszEnd; )
						{
							if (*pch == _T('\n'))
								cTrimLines--;

							// REVIEW: in Ansi builds should we call CharNextExA?
							//   If so, what code page is the log file in?
							pch++;

							if (cTrimLines <= 0)
								break;
						}

						// Move more recent data to beginning of file
						int cchMove = (int)(pszEnd - pch);
						memmove(pszTextStart, pch, cchMove * sizeof(TCHAR));
						cbFileNew = (cchMove * sizeof(TCHAR));

			#ifdef UNICODE
						cbFileNew += sizeof(WORD);
			#endif
					}
					UnmapViewOfFile(pszFileStart);
				}
				CloseHandle(hMapping);

				if (cbFileNew != cbFile)
				{
					// Truncate the file, now that we've moved data as needed
					SetFilePointer(m_hFile, cbFileNew, NULL, FILE_BEGIN);
					SetEndOfFile(m_hFile);
				}
			}
		}

		ReleaseMutex();
	}
}