/**
 * =========================================================================
 * File        : mahaf.cpp
 * Project     : 0 A.D.
 * Description : user-mode interface to Aken driver
 * =========================================================================
 */

// license: GPL; see lib/license.txt

#include "precompiled.h"

#include "win.h"
#include <winioctl.h>
#include "aken/aken.h"
#include "wutil.h"
#include "lib/module_init.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
    BOOL ok = DeviceIoControl(hAken, (DWORD)IOCTL_AKEN_READ_PORT, &in, sizeof(in), &out, sizeof(out), &bytesReturned, ovl);
    if(!ok)
    {
        WARN_WIN32_ERR;
        return 0;
    }

    debug_assert(bytesReturned == sizeof(out));
    const u32 value = out.value;
    return value;
}

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

u16 mahaf_ReadPort16(u16 port)
{
    const u32 value = ReadPort(port, 2);
    debug_assert(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);
}


volatile void* mahaf_MapPhysicalMemory(uintptr_t physicalAddress, size_t numBytes)
{
    // WinXP introduced checks that ensure we don't re-map pages with
    // incompatible attributes. without this, mapping physical pages risks
    // disaster due to TLB corruption, so disable it for safety.
    if(wutil_WindowsVersion() < WUTIL_VERSION_XP)
        return 0;

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

    DWORD bytesReturned;
    LPOVERLAPPED ovl = 0;   // synchronous
    BOOL ok = DeviceIoControl(hAken, (DWORD)IOCTL_AKEN_MAP, &in, sizeof(in), &out, sizeof(out), &bytesReturned, ovl);
    if(!ok)
    {
        WARN_WIN32_ERR;
        return 0;
    }

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


void mahaf_UnmapPhysicalMemory(volatile void* virtualAddress)
{
    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);
}


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

static SC_HANDLE OpenServiceControlManager()
{
    LPCSTR machineName = 0; // local
    LPCSTR databaseName = 0;    // default
    SC_HANDLE hSCM = OpenSCManager(machineName, databaseName, SC_MANAGER_ALL_ACCESS);
    if(!hSCM)
    {
        // administrator privileges are required. note: installing the
        // service and having it start automatically would allow
        // Least-Permission accounts to use it, but is too invasive and
        // thus out of the question.

        // make sure the error is as expected, otherwise something is afoot.
        if(GetLastError() != ERROR_ACCESS_DENIED)
            debug_assert(0);

        return 0;
    }

    return hSCM;    // success
}


static void UninstallDriver()
{
    SC_HANDLE hSCM = OpenServiceControlManager();
    if(!hSCM)
        return;
    SC_HANDLE hService = OpenService(hSCM, AKEN_NAME, SERVICE_ALL_ACCESS);
    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();
        if(err != ERROR_SERVICE_NOT_ACTIVE && err != ERROR_SERVICE_CANNOT_ACCEPT_CTRL)
            debug_assert(0);
    }

    // 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);
}


static void StartDriver(const char* driverPathname)
{
    const SC_HANDLE hSCM = OpenServiceControlManager();
    if(!hSCM)
        return;

    SC_HANDLE hService = OpenService(hSCM, AKEN_NAME, SERVICE_ALL_ACCESS);

#if 1
    // during development, we want to ensure the newest build is used, so
    // unload and re-create the service if it's running/installed.
    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)
    {
        LPCSTR startName = 0;   // LocalSystem
        hService = CreateService(hSCM, AKEN_NAME, AKEN_NAME,
            SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
            driverPathname, 0, 0, 0, startName, 0);
        debug_assert(hService != 0);
    }

    // start service
    {
        DWORD numArgs = 0;
        BOOL ok = StartService(hService, numArgs, 0);
        if(!ok)
        {
            // if it wasn't already running, starting failed
            if(GetLastError() != ERROR_SERVICE_ALREADY_RUNNING)
                WARN_IF_FALSE(0);
        }
    }

    CloseServiceHandle(hService);
    CloseServiceHandle(hSCM);
}


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

static void GetDriverPathname(char* driverPathname, size_t maxChars)
{
    const char* const bits = Is64BitOs()? "64" : "";
#ifdef NDEBUG
    const char* const debug = "";
#else
    const char* const debug = "d";
#endif
    sprintf_s(driverPathname, maxChars, "%s\\aken%s%s.sys", win_exe_dir, bits, debug);
}


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

static ModuleInitState initState;

bool mahaf_Init()
{
    if(!ModuleShouldInitialize(&initState))
        return true;

    char driverPathname[PATH_MAX];
    GetDriverPathname(driverPathname, ARRAY_SIZE(driverPathname));

    StartDriver(driverPathname);

    DWORD shareMode = 0;
    hAken = CreateFile("\\\\.\\Aken", GENERIC_READ, shareMode, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    if(hAken == INVALID_HANDLE_VALUE)
    {
        ModuleSetError(&initState);
        return false;
    }

    return true;
}


void mahaf_Shutdown()
{
    if(!ModuleShouldShutdown(&initState))
        return;

    CloseHandle(hAken);
    hAken = INVALID_HANDLE_VALUE;

    UninstallDriver();
}