/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#if (defined(WIN32) || defined(_WIN32)) && !defined(_WIN32_WCE)
#include <windows.h>
#include <Heapapi.h>
#include <log4cxx/nt/nteventlogappender.h>
#if !LOG4CXX_LOGCHAR_IS_UNICHAR
#include <log4cxx/spi/loggingevent.h>
#include <log4cxx/helpers/loglog.h>
#include <log4cxx/level.h>
#include <log4cxx/helpers/stringhelper.h>
#include <log4cxx/helpers/transcoder.h>
#include <log4cxx/helpers/pool.h>
#include <log4cxx/private/appenderskeleton_priv.h>

using namespace LOG4CXX_NS;
using namespace LOG4CXX_NS::spi;
using namespace LOG4CXX_NS::helpers;
using namespace LOG4CXX_NS::nt;

#define priv static_cast<NTEventLogAppenderPrivate*>(m_priv.get())

struct NTEventLogAppender::NTEventLogAppenderPrivate : public AppenderSkeleton::AppenderSkeletonPrivate {
	NTEventLogAppenderPrivate() :
		hEventLog(nullptr),
		pCurrentUserSID(nullptr) {}

	NTEventLogAppenderPrivate( LayoutPtr layout ) :
		AppenderSkeletonPrivate(layout),
		hEventLog(nullptr),
		pCurrentUserSID(nullptr) {}

	// Data
	LogString server;
	LogString log;
	LogString source;
	HANDLE hEventLog;
	SID* pCurrentUserSID;

	void close();
};

class CCtUserSIDHelper
{
	public:
		static bool FreeSid(SID* pSid)
		{
			return ::HeapFree(GetProcessHeap(), 0, (LPVOID)pSid) != 0;
		}

		static bool CopySid(SID * * ppDstSid, SID* pSrcSid)
		{
			bool bSuccess = false;

			DWORD dwLength = ::GetLengthSid(pSrcSid);
			*ppDstSid = (SID*) ::HeapAlloc(GetProcessHeap(),
					HEAP_ZERO_MEMORY, dwLength);

			if (::CopySid(dwLength, *ppDstSid, pSrcSid))
			{
				bSuccess = true;
			}
			else
			{
				FreeSid(*ppDstSid);
			}

			return bSuccess;
		}

		static bool GetCurrentUserSID(SID * * ppSid)
		{
			bool bSuccess = false;

			// Pseudohandle so don't need to close it
			HANDLE hProcess = ::GetCurrentProcess();
			HANDLE hToken = NULL;

			if (::OpenProcessToken(hProcess, TOKEN_QUERY, &hToken))
			{
				// Get the required size
				DWORD tusize = 0;
				GetTokenInformation(hToken, TokenUser, NULL, 0, &tusize);
				TOKEN_USER* ptu = (TOKEN_USER*)new BYTE[tusize];

				if (GetTokenInformation(hToken, TokenUser, (LPVOID)ptu, tusize, &tusize))
				{
					bSuccess = CopySid(ppSid, (SID*)ptu->User.Sid);
				}

				CloseHandle(hToken);
				delete [] ptu;
			}

			return bSuccess;
		}
};

IMPLEMENT_LOG4CXX_OBJECT(NTEventLogAppender)

NTEventLogAppender::NTEventLogAppender() :
	AppenderSkeleton(std::make_unique<NTEventLogAppenderPrivate>())
{
}

NTEventLogAppender::NTEventLogAppender(const LogString& server, const LogString& log, const LogString& source, const LayoutPtr& layout)
	:	AppenderSkeleton(std::make_unique<NTEventLogAppenderPrivate>(layout))
{
	priv->server = server;
	priv->log = log;
	priv->source = source;
	activateOptions();
}

NTEventLogAppender::~NTEventLogAppender()
{
	if (priv->setClosed())
		priv->close();
}


void NTEventLogAppender::close()
{
	if (priv->setClosed())
		priv->close();
}

void NTEventLogAppender::NTEventLogAppenderPrivate::close()
{
	if (this->hEventLog != NULL)
	{
		::DeregisterEventSource(this->hEventLog);
		this->hEventLog = NULL;
	}

	if (this->pCurrentUserSID != NULL)
	{
		CCtUserSIDHelper::FreeSid((::SID*) this->pCurrentUserSID);
		this->pCurrentUserSID = NULL;
	}
}

void NTEventLogAppender::setOption(const LogString& option, const LogString& value)
{
	if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("SERVER"), LOG4CXX_STR("server")))
	{
		priv->server = value;
	}
	else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("LOG"), LOG4CXX_STR("log")))
	{
		priv->log = value;
	}
	else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("SOURCE"), LOG4CXX_STR("source")))
	{
		priv->source = value;
	}
	else
	{
		AppenderSkeleton::setOption(option, value);
	}
}

void NTEventLogAppender::activateOptions( LOG4CXX_ACTIVATE_OPTIONS_FORMAL_PARAMETERS )
{
	if (priv->source.empty())
	{
		LogLog::warn(
			((LogString) LOG4CXX_STR("Source option not set for appender ["))
			+ this->m_priv->name + LOG4CXX_STR("]."));
		return;
	}

	if (priv->log.empty())
	{
		priv->log = LOG4CXX_STR("Application");
	}

	priv->close();

	// current user security identifier
	CCtUserSIDHelper::GetCurrentUserSID((::SID**) &priv->pCurrentUserSID);

	addRegistryInfo();

	LOG4CXX_ENCODE_WCHAR(wsource, priv->source);
	LOG4CXX_ENCODE_WCHAR(wserver, priv->server);
	priv->hEventLog = ::RegisterEventSourceW(
			wserver.empty() ? NULL : wserver.c_str(),
			wsource.c_str());

	if (priv->hEventLog == NULL)
	{
		LogString msg(LOG4CXX_STR("Cannot register NT EventLog -- server: '"));
		msg.append(priv->server);
		msg.append(LOG4CXX_STR("' source: '"));
		msg.append(priv->source);
		LogLog::error(msg);
		LogLog::error(getErrorString(LOG4CXX_STR("RegisterEventSource")));
	}
}

void NTEventLogAppender::append( LOG4CXX_APPEND_FORMAL_PARAMETERS )
{
	if (priv->hEventLog == NULL)
	{
		LogLog::warn(LOG4CXX_STR("NT EventLog not opened."));
		return;
	}

	LogString oss;
	this->m_priv->layout->format(oss, event);
	auto msgs = oss.c_str();
#if LOG4CXX_LOGCHAR_IS_WCHAR
	BOOL bSuccess = ::ReportEventW(
			priv->hEventLog,
			getEventType(event),
			getEventCategory(event),
			0x1000,
			priv->pCurrentUserSID,
			1,
			0,
			(LPCWSTR*) &msgs,
			NULL);
#elif LOG4CXX_LOGCHAR_IS_UTF8
	BOOL bSuccess = ::ReportEventA(
			priv->hEventLog,
			getEventType(event),
			getEventCategory(event),
			0x1000,
			priv->pCurrentUserSID,
			1,
			0,
			(LPCSTR*)&msgs,
			NULL);
#else
	#error NTEventLogAppender is not supported when logchar is unichar
#endif
	if (!bSuccess)
	{
		LogLog::error(getErrorString(LOG4CXX_STR("ReportEvent")));
	}
}

/*
 * Add this source with appropriate configuration keys to the registry.
 */
void NTEventLogAppender::addRegistryInfo()
{
	DWORD disposition = 0;
	::HKEY hkey = 0;
	LogString subkey(LOG4CXX_STR("SYSTEM\\CurrentControlSet\\Services\\EventLog\\"));
	subkey.append(priv->log);
	subkey.append(1, (logchar) 0x5C /* '\\' */);
	subkey.append(priv->source);
	LOG4CXX_ENCODE_WCHAR(wsubkey, subkey);

	long stat = RegCreateKeyExW(HKEY_LOCAL_MACHINE, wsubkey.c_str(), 0, NULL,
			REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL,
			&hkey, &disposition);

	if (stat == ERROR_SUCCESS && disposition == REG_CREATED_NEW_KEY)
	{
		HMODULE hmodule = GetModuleHandleW(L"log4cxx");

		if (hmodule == NULL)
		{
			hmodule = GetModuleHandleW(0);
		}

		wchar_t modpath[_MAX_PATH];
		DWORD modlen = GetModuleFileNameW(hmodule, modpath, _MAX_PATH - 1);

		if (modlen > 0)
		{
			modpath[modlen] = 0;
			RegSetValueExW(hkey, L"EventMessageFile", 0, REG_SZ,
				(LPBYTE) modpath, (DWORD)(wcslen(modpath) * sizeof(wchar_t)));
			RegSetValueExW(hkey, L"CategoryMessageFile", 0, REG_SZ,
				(LPBYTE) modpath, (DWORD)(wcslen(modpath) * sizeof(wchar_t)));
			DWORD typesSupported = 7;
			DWORD categoryCount = 6;
			RegSetValueExW(hkey, L"TypesSupported", 0, REG_DWORD,
				(LPBYTE)&typesSupported, sizeof(DWORD));
			RegSetValueExW(hkey, L"CategoryCount", 0, REG_DWORD,
				(LPBYTE)&categoryCount, sizeof(DWORD));
		}
	}

	RegCloseKey(hkey);
	return;
}

WORD NTEventLogAppender::getEventType(const LoggingEventPtr& event)
{
	int priority = event->getLevel()->toInt();
	WORD type = EVENTLOG_SUCCESS;

	if (priority >= Level::INFO_INT)
	{
		type = EVENTLOG_INFORMATION_TYPE;

		if (priority >= Level::WARN_INT)
		{
			type = EVENTLOG_WARNING_TYPE;

			if (priority >= Level::ERROR_INT)
			{
				type = EVENTLOG_ERROR_TYPE;
			}
		}
	}

	return type;
}

WORD NTEventLogAppender::getEventCategory(const LoggingEventPtr& event)
{
	int priority = event->getLevel()->toInt();
	WORD category = 1;

	if (priority >= Level::DEBUG_INT)
	{
		category = 2;

		if (priority >= Level::INFO_INT)
		{
			category = 3;

			if (priority >= Level::WARN_INT)
			{
				category = 4;

				if (priority >= Level::ERROR_INT)
				{
					category = 5;

					if (priority >= Level::FATAL_INT)
					{
						category = 6;
					}
				}
			}
		}
	}

	return category;
}

LogString NTEventLogAppender::getErrorString(const LogString& function)
{
	enum { MSGSIZE = 5000 };
	Pool p;
	wchar_t* lpMsgBuf = (wchar_t*) p.palloc(MSGSIZE * sizeof(wchar_t));
	DWORD dw = GetLastError();

	FormatMessageW(
		FORMAT_MESSAGE_FROM_SYSTEM,
		NULL,
		dw,
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		lpMsgBuf,
		MSGSIZE, NULL );

	LogString msg(function);
	msg.append(LOG4CXX_STR(" failed with error "));
	StringHelper::toString((size_t) dw, msg);
	msg.append(LOG4CXX_STR(": "));
	Transcoder::decode(lpMsgBuf, msg);

	return msg;
}

void NTEventLogAppender::setSource(const LogString& source)
{
	priv->source.assign(source);
}

const LogString& NTEventLogAppender::getSource() const
{
	return priv->source;
}

void NTEventLogAppender::setLog(const LogString& log)
{
	priv->log.assign(log);
}

const LogString& NTEventLogAppender::getLog() const
{
	return priv->log;
}

void NTEventLogAppender::setServer(const LogString& server)
{
	priv->server.assign(server);
}

const LogString& NTEventLogAppender::getServer() const
{
	return priv->server;
}
#endif // !LOG4CXX_LOGCHAR_IS_UNICHAR

#endif // WIN32
