/* Copyright (c) 2010 Wildfire Games
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * user-mode interface to Aken driver
 */

#include "precompiled.h"
#include "lib/sysdep/os/win/mahaf.h"

#include "lib/config2.h"
#include "lib/module_init.h"

#include "lib/sysdep/os/win/wutil.h"
#include <winioctl.h>
#include "lib/sysdep/os/win/aken/aken.h"
#include "lib/sysdep/os/win/wversion.h"

static HANDLE hAken = INVALID_HANDLE_VALUE;	// handle to Aken driver


//-----------------------------------------------------------------------------
// ioctl wrappers
//-----------------------------------------------------------------------------

static u32 ReadPort(u16 port, u8 numBytes)
{
	AkenReadPortIn in;
	in.port = (USHORT)port;
	in.numBytes = (UCHAR)numBytes;
	AkenReadPortOut out;

	DWORD bytesReturned;
	LPOVERLAPPED ovl = 0;	// synchronous
	const BOOL ok = DeviceIoControl(hAken, (DWORD)IOCTL_AKEN_READ_PORT, &in, sizeof(in), &out, sizeof(out), &bytesReturned, ovl);
	WARN_RETURN_0_IF_FALSE(ok);

	ENSURE(bytesReturned == sizeof(out));
	return out.value;
}

u8 mahaf_ReadPort8(u16 port)
{
	const u32 value = ReadPort(port, 1);
	ENSURE(value <= 0xFF);
	return (u8)(value & 0xFF);
}

u16 mahaf_ReadPort16(u16 port)
{
	const u32 value = ReadPort(port, 2);
	ENSURE(value <= 0xFFFF);
	return (u16)(value & 0xFFFF);
}

u32 mahaf_ReadPort32(u16 port)
{
	const u32 value = ReadPort(port, 4);
	return value;
}


static void WritePort(u16 port, u32 value, u8 numBytes)
{
	AkenWritePortIn in;
	in.value = (DWORD32)value;
	in.port  = (USHORT)port;
	in.numBytes = (UCHAR)numBytes;

	DWORD bytesReturned;	// unused but must be passed to DeviceIoControl
	LPOVERLAPPED ovl = 0;	// synchronous
	BOOL ok = DeviceIoControl(hAken, (DWORD)IOCTL_AKEN_WRITE_PORT, &in, sizeof(in), 0, 0u, &bytesReturned, ovl);
	WARN_IF_FALSE(ok);
}

void mahaf_WritePort8(u16 port, u8 value)
{
	WritePort(port, (u32)value, 1);
}

void mahaf_WritePort16(u16 port, u16 value)
{
	WritePort(port, (u32)value, 2);
}

void mahaf_WritePort32(u16 port, u32 value)
{
	WritePort(port, value, 4);
}


bool mahaf_IsPhysicalMappingDangerous()
{
	// pre-XP versions don't prevent re-mapping pages with incompatible
	// attributes, which may lead to disaster due to TLB corruption.
	if(wversion_Number() < WVERSION_XP)
		return true;

	return false;
}


volatile void* mahaf_MapPhysicalMemory(uintptr_t physicalAddress, size_t numBytes)
{
	ENSURE(!mahaf_IsPhysicalMappingDangerous());

	AkenMapIn in;
	in.physicalAddress = (DWORD64)physicalAddress;
	in.numBytes        = (DWORD64)numBytes;
	AkenMapOut out;

	DWORD bytesReturned;
	LPOVERLAPPED ovl = 0;	// synchronous
	const BOOL ok = DeviceIoControl(hAken, (DWORD)IOCTL_AKEN_MAP, &in, sizeof(in), &out, sizeof(out), &bytesReturned, ovl);
	WARN_RETURN_0_IF_FALSE(ok);

	ENSURE(bytesReturned == sizeof(out));
	volatile void* virtualAddress = (volatile void*)(uintptr_t)out.virtualAddress;
	return virtualAddress;
}


void mahaf_UnmapPhysicalMemory(volatile void* virtualAddress)
{
	ENSURE(!mahaf_IsPhysicalMappingDangerous());

	AkenUnmapIn in;
	in.virtualAddress = (DWORD64)virtualAddress;

	DWORD bytesReturned;	// unused but must be passed to DeviceIoControl
	LPOVERLAPPED ovl = 0;	// synchronous
	BOOL ok = DeviceIoControl(hAken, (DWORD)IOCTL_AKEN_UNMAP, &in, sizeof(in), 0, 0u, &bytesReturned, ovl);
	WARN_IF_FALSE(ok);
}


static u64 ReadRegister(DWORD ioctl, u64 reg)
{
	AkenReadRegisterIn in;
	in.reg = reg;
	AkenReadRegisterOut out;

	DWORD bytesReturned;
	LPOVERLAPPED ovl = 0;	// synchronous
	const BOOL ok = DeviceIoControl(hAken, ioctl, &in, sizeof(in), &out, sizeof(out), &bytesReturned, ovl);
	WARN_RETURN_0_IF_FALSE(ok);

	ENSURE(bytesReturned == sizeof(out));
	return out.value;
}

u64 mahaf_ReadModelSpecificRegister(u64 reg)
{
	return ReadRegister((DWORD)IOCTL_AKEN_READ_MSR, reg);
}

u64 mahaf_ReadPerformanceMonitoringCounter(u64 reg)
{
	return ReadRegister((DWORD)IOCTL_AKEN_READ_PMC, reg);
}

void mahaf_WriteModelSpecificRegister(u64 reg, u64 value)
{
	AkenWriteRegisterIn in;
	in.reg = reg;
	in.value = value;

	DWORD bytesReturned;	// unused but must be passed to DeviceIoControl
	LPOVERLAPPED ovl = 0;	// synchronous
	BOOL ok = DeviceIoControl(hAken, (DWORD)IOCTL_AKEN_WRITE_MSR, &in, sizeof(in), 0, 0u, &bytesReturned, ovl);
	WARN_IF_FALSE(ok);
}


//-----------------------------------------------------------------------------
// driver installation
//-----------------------------------------------------------------------------

// @param access need not include SC_MANAGER_CONNECT ("implicitly specified")
static SC_HANDLE OpenServiceControlManager(DWORD access)
{
	LPCWSTR machineName = 0;	// local
	LPCWSTR databaseName = 0;	// default
	SC_HANDLE hSCM = OpenSCManagerW(machineName, databaseName, access);
	if(!hSCM)
	{
		// ensure no other problems arose
		ENSURE(GetLastError() == ERROR_ACCESS_DENIED);

		// administrator privileges are required for SC_MANAGER_CREATE_SERVICE.
		// this is a problem on Vista / Win7, so users will have to use the
		// separate aken_install.bat

		return 0;
	}

	return hSCM;	// success
}


static void UninstallDriver()
{
	SC_HANDLE hSCM = OpenServiceControlManager(SC_MANAGER_ENUMERATE_SERVICE);
	if(!hSCM)
		return;
	SC_HANDLE hService = OpenServiceW(hSCM, AKEN_NAME, SERVICE_STOP|SERVICE_INTERROGATE);
	if(!hService)
		return;

	// stop service
	SERVICE_STATUS serviceStatus;
	if(!ControlService(hService, SERVICE_CONTROL_STOP, &serviceStatus))
	{
		// if the problem wasn't that the service is already stopped,
		// something actually went wrong.
		const DWORD err = GetLastError();
		ENSURE(err == ERROR_SERVICE_NOT_ACTIVE || err == ERROR_SERVICE_CANNOT_ACCEPT_CTRL);
	}

	// delete service
	BOOL ok;
	ok = DeleteService(hService);
	WARN_IF_FALSE(ok);
	ok = CloseServiceHandle(hService);
	WARN_IF_FALSE(ok);

	ok = CloseServiceHandle(hSCM);
	WARN_IF_FALSE(ok);
}


#if CONFIG2_MAHAF_ATTEMPT_DRIVER_START

static void StartDriver(const OsPath& driverPathname)
{
	const SC_HANDLE hSCM = OpenServiceControlManager(SC_MANAGER_CREATE_SERVICE);
	if(!hSCM)
	{
		ENSURE(GetLastError() == ERROR_ACCESS_DENIED);
		SetLastError(0);
		return;
	}

	SC_HANDLE hService = OpenServiceW(hSCM, AKEN_NAME, SERVICE_START);

	// during development, we want to ensure the newest build is used, so
	// unload and re-create the service if it's running/installed.
	// as of 2008-03-24 no further changes to Aken are pending, so this is
	// disabled (thus also avoiding trouble when running multiple instances)
#if 0
	if(hService)
	{
		BOOL ok = CloseServiceHandle(hService);
		WARN_IF_FALSE(ok);
		hService = 0;
		UninstallDriver();
	}
#endif	

	// create service (note: this just enters the service into SCM's DB;
	// no error is raised if the driver binary doesn't exist etc.)
	if(!hService)
	{
		LPCWSTR startName = 0;	// LocalSystem
		// NB: Windows 7 seems to insist upon backslashes (i.e. external_file_string)
		hService = CreateServiceW(hSCM, AKEN_NAME, AKEN_NAME,
			SERVICE_START, SERVICE_KERNEL_DRIVER, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
			OsString(driverPathname).c_str(), 0, 0, 0, startName, 0);
		ENSURE(hService != 0);
	}

	{
		DWORD numArgs = 0;
		BOOL ok = StartService(hService, numArgs, 0);
		if(!ok)
		{
			switch(GetLastError())
			{
			case ERROR_SERVICE_ALREADY_RUNNING:
				// ok, no action needed
				break;
			case ERROR_ACCESS_DENIED:
				// Win7, can't start service; must use aken_install.bat
				break;
			case ERROR_INVALID_IMAGE_HASH:
				// Win7 x86 rejects our code signing certificate; must enable
				// "test signing" mode via aken_install.bat
				break;
			default:
				// unexpected problem
				DEBUG_WARN_ERR(ERR::LOGIC);
				break;
			}
		}
	}

	CloseServiceHandle(hService);
	CloseServiceHandle(hSCM);
}


static bool Is64BitOs()
{
#if OS_WIN64
	return true;
#else
	return wutil_IsWow64();
#endif
}

static OsPath DriverPathname()
{
	const char* const bits = Is64BitOs()? "64" : "";
#ifdef NDEBUG
	const char* const debug = "";
#else
	const char* const debug = "d";
#endif
	char filename[PATH_MAX];
	sprintf_s(filename, ARRAY_SIZE(filename), "aken%s%s.sys", bits, debug);
	return wutil_ExecutablePath() / filename;
}

#endif // CONFIG2_MAHAF_ATTEMPT_DRIVER_START


//-----------------------------------------------------------------------------

static Status Init()
{
	WinScopedPreserveLastError s;

	if(wutil_HasCommandLineArgument(L"-wNoMahaf"))
		return ERR::NOT_SUPPORTED;	// NOWARN

#if CONFIG2_MAHAF_ATTEMPT_DRIVER_START
	{
		const OsPath driverPathname = DriverPathname();
		StartDriver(driverPathname);
	}
#endif

	{
		const DWORD shareMode = 0;
		hAken = CreateFileW(L"\\\\.\\Aken", GENERIC_READ, shareMode, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
		if(hAken == INVALID_HANDLE_VALUE)
		{
			// GetLastError() is ERROR_FILE_NOT_FOUND if the driver isn't running,
			// but this is also reached when a handle has already been opened
			// (e.g. by a second instance of the same program) - in which case we must
			// indicate failure so that clients won't engage in unsynchronized ring 0 operations.
			SetLastError(0);
			return ERR::INVALID_HANDLE;	// NOWARN (happens often due to security restrictions)
		}
	}

	return INFO::OK;
}

static void Shutdown()
{
	CloseHandle(hAken);
	hAken = INVALID_HANDLE_VALUE;

	UninstallDriver();
}


static ModuleInitState initState;

Status mahaf_Init()
{
	return ModuleInit(&initState, Init);
}

void mahaf_Shutdown()
{
	ModuleShutdown(&initState, Shutdown);
}
