/*---------------------------------------------------------*\
| main_Windows.cpp                                          |
|                                                           |
|   Entry point for the OpenRGB application on Windows      |
|   Differentiate between service and application startup   |
|   Housekeeping for Windows service management             |
|                                                           |
|   This file is part of the OpenRGB project                |
|   SPDX-License-Identifier: GPL-2.0-or-later               |
\*---------------------------------------------------------*/

#include <algorithm>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <thread>

#include <QApplication>

#include "cli.h"
#include "startup.h"
#include "LogManager.h"
#include "NetworkServer.h"
#include "ResourceManager.h"
#include "StringUtils.h"

using namespace std::chrono_literals;

static int common_main(int argc, char* argv[]);

static void WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv);
static void ReportServiceStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint);

static char                  service_name[]             = "OpenRGB";
static SERVICE_TABLE_ENTRY   service_dispatch_table[]   = { { service_name, ServiceMain }, { NULL, NULL } };
static DWORD                 service_checkpoint         = 1;
static SERVICE_STATUS_HANDLE service_status_handle;
static SERVICE_STATUS        service_status;

static bool                  started_as_service;
static volatile bool         service_stop_requested;
static bool                  have_console;

static std::mutex            service_status_mutex;

/*---------------------------------------------------------*\
| Detection progress goes from 0 to 100 twice, passing an   |
| estimate based on that confuses the service manager       |
\*---------------------------------------------------------*/
static int                   detection_pass;
static unsigned int          lastpercent                = 101;

/*---------------------------------------------------------*\
| ServiceStartupProgress                                    |
|                                                           |
|   Report detection progress when running as a service     |
\*---------------------------------------------------------*/
static void ServiceStartupProgress(void*)
{
    unsigned int percent = ResourceManager::get()->GetDetectionPercent();
    unsigned int estimate;

    percent = std::clamp(percent, 0u, 100u);

    if(lastpercent > percent)
    {
        detection_pass += 1;
    }

    lastpercent = percent;

    switch(detection_pass)
    {
        case 0:
            percent = 0;
            break;
        case 1:
            percent = percent * 4 / 5;
            break;
        case 2:
            percent = percent / 5 + 80;
            break;
        default:
            percent = 100;
            break;
    }

    estimate = (100 - percent) / 5 + 10;

    ReportServiceStatus(SERVICE_START_PENDING, NO_ERROR, estimate * 1000);
}

/*---------------------------------------------------------*\
| ReportServiceStatus                                       |
|                                                           |
|   As of writing this (24H2) there are 7 possible service  |
|   states:                                                 |
|       SERVICE_START_PENDING, SERVICE_RUNNING,             |
|       SERVICE_STOP_PENDING, SERVICE_STOPPED,              |
|       SERVICE_PAUSE_PENDING, SERVICE_PAUSED,              |
|       SERVICE_CONTINUE_PENDING                            |
|                                                           |
|   We don't accept pause requests, so only start_pending   |
|   through stopped are possible.                           |
|                                                           |
|   Control requests can arrive asynchronously so we take   |
|   some precautions here not to report an out of order     |
|   status.                                                 |
\*---------------------------------------------------------*/
static void ReportServiceStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint)
{
    static DWORD last_state = SERVICE_START_PENDING;

    LOG_TRACE("ReportServiceStatus(%lu, %lu, %lu), PID %lu Thread %lu", dwCurrentState, dwWin32ExitCode, dwWaitHint, GetCurrentProcessId(), GetCurrentThreadId());

    service_status_mutex.lock();

    switch(last_state)
    {
        case SERVICE_RUNNING:
            /*---------------------------------------------*\
            | Don't go back to SERVICE_START_PENDING        |
            \*---------------------------------------------*/
            if(dwCurrentState == SERVICE_START_PENDING)
            {
                service_status_mutex.unlock();
                return;
            }
            break;

        case SERVICE_STOP_PENDING:
            /*---------------------------------------------*\
            | Don't go back to starting or running          |
            \*---------------------------------------------*/
            if((dwCurrentState != SERVICE_STOP_PENDING) && (dwCurrentState != SERVICE_STOPPED))
            {
                service_status_mutex.unlock();
                return;
            }
            break;

        case SERVICE_STOPPED:
            /*---------------------------------------------*\
            | Don't call SetServiceStatus anymore after     |
            | SERVICE_STOPPED has been reported             |
            \*---------------------------------------------*/
            service_status_mutex.unlock();
            return;
    }

    switch(dwCurrentState)
    {
        case SERVICE_START_PENDING:
        case SERVICE_STOP_PENDING:
            service_status.dwControlsAccepted = 0;
            service_status.dwCheckPoint = service_checkpoint;
            service_checkpoint += 1;
            break;

        case SERVICE_RUNNING:
        case SERVICE_STOPPED:
            service_status.dwCheckPoint = 0;
            service_status.dwControlsAccepted =
                // SERVICE_ACCEPT_POWEREVENT   |
                SERVICE_ACCEPT_STOP         |
                SERVICE_ACCEPT_PRESHUTDOWN;
            break;

        default:
            /*---------------------------------------------*\
            | Shouldn't happen                              |
            \*---------------------------------------------*/
            service_status_mutex.unlock();
            return;
    }

    last_state                      = dwCurrentState;
    service_status.dwCurrentState   = dwCurrentState;
    service_status.dwWin32ExitCode  = dwWin32ExitCode;
    service_status.dwWaitHint       = dwWaitHint;

    SetServiceStatus(service_status_handle, &service_status);

    service_status_mutex.unlock();

    return;
}

/*---------------------------------------------------------*\
| ServiceControlHandler                                     |
|                                                           |
|   Handler function to register with                       |
|   RegisterServiceControlHandlerEx, to handle service      |
|   control operations (stop/shutdown).                     |
\*---------------------------------------------------------*/
static DWORD WINAPI ServiceControlHandler(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
    DWORD retval = NO_ERROR;

    LOG_TRACE("ServiceControlHandler(%lu, %lu, %p, %p) called, PID %lu Thread %lu", dwControl, dwEventType, lpEventData, lpContext, GetCurrentProcessId(), GetCurrentThreadId());

    switch(dwControl)
    {
        /*-------------------------------------------------*\
        | TODO either this or                               |
        | RegisterSuspendResumeNotification somewhere       |
        \*-------------------------------------------------*/
        // case SERVICE_CONTROL_POWEREVENT:
        case SERVICE_CONTROL_INTERROGATE:
            /*---------------------------------------------*\
            | Should do nothing but return NO_ERROR         |
            \*---------------------------------------------*/
            break;

        case SERVICE_CONTROL_STOP:
        case SERVICE_CONTROL_PRESHUTDOWN:
            ReportServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 10000);
            service_stop_requested = true;
            break;

        default:
            retval = ERROR_CALL_NOT_IMPLEMENTED;
            break;
    }

    return retval;
}

/*---------------------------------------------------------*\
| LogEvent                                                  |
|                                                           |
|   Log an event to the service log when running as a       |
|   service.  Only used for startup and shutdown events.    |
\*---------------------------------------------------------*/
static void LogEvent(const char *fmt, ...)
{
    va_list args1;
    va_list args2;

    /*-----------------------------------------------------*\
    | Handle variable arguments                             |
    \*-----------------------------------------------------*/
    va_start(args1, fmt);
    va_copy(args2, args1);

    /*-----------------------------------------------------*\
    | Determine string length and allocate buffer           |
    \*-----------------------------------------------------*/
    int     len = vsnprintf(NULL, 0, fmt, args1);
    char *  buf = (char *)malloc(len + 1);

    /*-----------------------------------------------------*\
    | Fill the string buffer                                |
    \*-----------------------------------------------------*/
    va_end(args1);
    vsnprintf(buf, len + 1, fmt, args2);
    va_end(args2);

    /*-----------------------------------------------------*\
    | If we somehow ended up here with a working console    |
    | then just print it                                    |
    \*-----------------------------------------------------*/
    if(have_console)
    {
        printf("%s\n", buf);
        free(buf);
        return;
    }

    /*-----------------------------------------------------*\
    | Otherwise, register event source with our service     |
    | name and report event to the event log                |
    \*-----------------------------------------------------*/
    HANDLE event_source = RegisterEventSource(NULL, service_name);

    if(event_source)
    {
        LPCSTR strings[2] = { service_name, buf };
        ReportEvent(
            event_source,
            EVENTLOG_ERROR_TYPE,
            0,
            1, // error #
            NULL,
            2,
            0,
            strings,
            NULL);
        DeregisterEventSource(event_source);
    }
    else
    {
        /*-------------------------------------------------*\
        | No console, no logmanager, no eventlog, no more   |
        | ideas. Sometimes it's OK to give up.              |
        \*-------------------------------------------------*/
    }

    /*-----------------------------------------------------*\
    | Free the buffer                                       |
    \*-----------------------------------------------------*/
    free(buf);
}

/*---------------------------------------------------------*\
| LogError                                                  |
|                                                           |
|   Log an error event when running as a service            |
\*---------------------------------------------------------*/
static void LogError(const char *fn, DWORD lasterror)
{
    std::string message = std::system_category().message(lasterror);
    LogEvent("%s failed with error code %lu \"%s\"", fn, lasterror, message.c_str());
}

/*---------------------------------------------------------*\
| ServiceMain                                               |
|                                                           |
|   Figures out whether we are started as a service, calls  |
|   common_main one way or another.                         |
\*---------------------------------------------------------*/
static void WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv)
{
    /*-----------------------------------------------------*\
    | Initialize service status                             |
    \*-----------------------------------------------------*/
    service_status.dwServiceType                = SERVICE_WIN32_OWN_PROCESS;
    service_status.dwServiceSpecificExitCode    = 0;

    started_as_service                          = true;
    service_status_handle                       = RegisterServiceCtrlHandlerEx(service_name, ServiceControlHandler, NULL);

    /*-----------------------------------------------------*\
    | Exit if service status handle is invalid              |
    \*-----------------------------------------------------*/
    if(service_status_handle == 0)
    {
        LogError("RegisterServiceCtrlHandlerEx", GetLastError());
        return;
    }

    /*-----------------------------------------------------*\
    | Report service status start pending                   |
    \*-----------------------------------------------------*/
    ReportServiceStatus(SERVICE_START_PENDING, NO_ERROR, 30000);

    /*-----------------------------------------------------*\
    | Perform common main processing                        |
    \*-----------------------------------------------------*/
    int exitval = common_main(dwArgc, lpszArgv);

    /*-----------------------------------------------------*\
    | Log if exit was unsuccessful                          |
    \*-----------------------------------------------------*/
    if(exitval != EXIT_SUCCESS)
    {
        LogEvent("OpenRGB finishing with exit code %d", exitval);
    }

    /*-----------------------------------------------------*\
    | Report service status stopped                         |
    \*-----------------------------------------------------*/
    ReportServiceStatus(SERVICE_STOPPED, NO_ERROR, 0);
}

/*---------------------------------------------------------*\
| main                                                      |
|                                                           |
|   Entry point, checks if started as a service and then    |
|   calls the common main processing                        |
\*---------------------------------------------------------*/
int main(int argc, char* argv[])
{
    started_as_service = false;

    /*-----------------------------------------------------*\
    | This will call ServiceMain() if we are started as a   |
    | service                                               |
    \*-----------------------------------------------------*/
    bool sscd = StartServiceCtrlDispatcher(service_dispatch_table);

    /*-----------------------------------------------------*\
    | ... returns a specific error code otherwise           |
    \*-----------------------------------------------------*/
    DWORD lasterror = GetLastError();

    if(sscd)
    {
        if(started_as_service)
        {
            /*---------------------------------------------*\
            | StartServiceCtrlDispatcher reported success,  |
            | ServiceMain was reached. Don't log anything,  |
            | it's the normal exit path when a service is   |
            | stopped.                                      |
            \*---------------------------------------------*/
            return EXIT_SUCCESS;
        }
        else
        {
            /*---------------------------------------------*\
            | Should not happen                             |
            \*---------------------------------------------*/
            LogEvent("StartServiceCtrlDispatcher returned true, but ServiceMain has not been reached.");
            return EXIT_FAILURE;
        }
    }

    /*-----------------------------------------------------*\
    | StartServiceCtrlDispatcher reported failure.          |
    | First, check that the error isn't the one returned    |
    | when it's called from an app                          |
    \*-----------------------------------------------------*/
    if(lasterror != ERROR_FAILED_SERVICE_CONTROLLER_CONNECT)
    {
        LogError("StartServiceCtrlDispatcher", lasterror);
        return EXIT_FAILURE;
    }

    /*-----------------------------------------------------*\
    | Now we are quite sure we are not started as a service |
    \*-----------------------------------------------------*/
    if(started_as_service)
    {
        /*-------------------------------------------------*\
        | ... so this should not happen                     |
        \*-------------------------------------------------*/
        LogEvent("StartServiceCtrlDispatcher returned false, but ServiceMain has been reached.");
        return EXIT_FAILURE;
    }

    /*-----------------------------------------------------*\
    | Code that needs to be run at startup when we are      |
    | started as an app and not as a service goes here.     |
    \*-----------------------------------------------------*/

    /*-----------------------------------------------------*\
    | Attach console output                                 |
    \*-----------------------------------------------------*/
    if(AttachConsole(ATTACH_PARENT_PROCESS))
    {
        /*-------------------------------------------------*\
        | We are running under some terminal context;       |
        | otherwise leave the GUI and CRT alone             |
        \*-------------------------------------------------*/
        freopen("CONIN$",  "r", stdin);
        freopen("CONOUT$", "w", stdout);
        freopen("CONOUT$", "w", stderr);
        have_console = true;
    }

    return common_main(argc, argv);
}

/*---------------------------------------------------------*\
| InitializeTimerResolutionThreadFunction (Win32)           |
|                                                           |
|   On Windows, the default timer resolution is 15.6ms.     |
|   For higher accuracy delays, the timer resolution should |
|   be set to a shorter interval.  The shortest interval    |
|   that can be set is 0.5ms.                               |
\*---------------------------------------------------------*/
void InitializeTimerResolutionThreadFunction()
{
    /*-----------------------------------------------------*\
    | NtSetTimerResolution function pointer type            |
    \*-----------------------------------------------------*/
    typedef unsigned int NTSTATUS;
    typedef NTSTATUS (*NTSETTIMERRESOLUTION)(ULONG DesiredResolution, BOOLEAN SetResolution, PULONG CurrentResolution);

    /*-----------------------------------------------------*\
    | PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION = 4, |
    | isn't defined in Win10 headers                        |
    \*-----------------------------------------------------*/
    PROCESS_POWER_THROTTLING_STATE PowerThrottlingState { PROCESS_POWER_THROTTLING_CURRENT_VERSION, 4, 0 };

    ULONG                CurrentResolution;
    HMODULE              NtDllHandle;
    NTSETTIMERRESOLUTION NtSetTimerResolution;

    /*-----------------------------------------------------*\
    | Load ntdll.dll and get pointer to NtSetTimerResolution|
    \*-----------------------------------------------------*/
    NtDllHandle             = LoadLibrary("ntdll.dll");
    NtSetTimerResolution    = (NTSETTIMERRESOLUTION)GetProcAddress(NtDllHandle, "NtSetTimerResolution");

    /*-----------------------------------------------------*\
    | Windows 11 requires                                   |
    | PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION      |
    \*-----------------------------------------------------*/
    SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &PowerThrottlingState, sizeof(PowerThrottlingState));

    /*-----------------------------------------------------*\
    | Call NtSetTimerResolution to set timer resolution to  |
    | 0.5ms every 500ms                                     |
    \*-----------------------------------------------------*/
    while(1)
    {
        NtSetTimerResolution(5000, TRUE, &CurrentResolution);

        std::this_thread::sleep_for(500ms);
    }
}

/*---------------------------------------------------------*\
| WaitWhileServerOnline                                     |
|                                                           |
|   Wait while NetworkServer is online and return only when |
|   it has shut down                                        |
\*---------------------------------------------------------*/
static void WaitWhileServerOnline(NetworkServer* srv)
{
    while(srv->GetOnline())
    {
        std::this_thread::sleep_for(1s);
        if(service_stop_requested)
        {
            srv->StopServer();
        }
    };
}

/*---------------------------------------------------------*\
| common_main                                               |
|                                                           |
|   Common entry functionality after determining whether we |
|   were started as a service or not.                       |
\*---------------------------------------------------------*/
static int common_main(int argc, char* argv[])
{
    unsigned int ret_flags;

    if(started_as_service)
    {
        /*-------------------------------------------------*\
        | Passing command line arguments to a service is    |
        | difficult, can cause all kinds of trouble and     |
        | doesn't have a way to warn about them             |
        \*-------------------------------------------------*/
        ret_flags = RET_FLAG_START_SERVER | RET_FLAG_NO_AUTO_CONNECT;

        /*-------------------------------------------------*\
        | Get the path to the executable and create a       |
        | directory called service_config there, use this   |
        | as the service's configuration directory          |
        \*-------------------------------------------------*/
        WCHAR exe_path_wchar[MAX_PATH];
        GetModuleFileNameW(NULL, exe_path_wchar, MAX_PATH);

        std::string exe_path_string = StringUtils::wstring_to_string(std::wstring(exe_path_wchar));

        filesystem::path exe_path(exe_path_string);
        filesystem::path config_path = exe_path.remove_filename() / filesystem::u8path("service_config");

        filesystem::create_directories(config_path);

        ResourceManager::get()->SetConfigurationDirectory(config_path);
    }
    else
    {
        /*-------------------------------------------------*\
        | Perform CLI pre-detection processing to get       |
        | return flags                                      |
        \*-------------------------------------------------*/
        ret_flags = cli_pre_detection(argc, argv);
    }

    /*-----------------------------------------------------*\
    | Start timer resolution correction thread              |
    \*-----------------------------------------------------*/
    std::thread * InitializeTimerResolutionThread;
    InitializeTimerResolutionThread = new std::thread(InitializeTimerResolutionThreadFunction);
    InitializeTimerResolutionThread->detach();

    /*-----------------------------------------------------*\
    | Initialize ResourceManager                            |
    \*-----------------------------------------------------*/
    ResourceManager::get()->Initialize(
        !(ret_flags & RET_FLAG_NO_AUTO_CONNECT),
        !(ret_flags & RET_FLAG_NO_DETECT),
        ret_flags & RET_FLAG_START_SERVER,
        ret_flags & RET_FLAG_CLI_POST_DETECTION);

    /*-----------------------------------------------------*\
    | If running as a service, register the service startup |
    | progress callback with ResourceManager to report      |
    | startup progress based on detection progress.         |
    \*-----------------------------------------------------*/
    if(started_as_service)
    {
        ResourceManager::get()->RegisterDetectionProgressCallback(ServiceStartupProgress, NULL);
    }

    /*-----------------------------------------------------*\
    | Perform application startup and run the application.  |
    | This call returns only when the GUI application is    |
    | closing or if not running the GUI.                    |
    \*-----------------------------------------------------*/
    int exitval = startup(argc, argv, ret_flags);

    /*-----------------------------------------------------*\
    | If running as a service, unregister the service       |
    | startup progress callback when shutting down.         |
    \*-----------------------------------------------------*/
    if(started_as_service)
    {
        ResourceManager::get()->UnregisterDetectionProgressCallback(ServiceStartupProgress, NULL);
    }

    /*-----------------------------------------------------*\
    | If started in headless server mode, wait until server |
    | shuts down before closing application.                |
    \*-----------------------------------------------------*/
    if((ret_flags & RET_FLAG_START_SERVER) && !(ret_flags & RET_FLAG_START_GUI))
    {
        NetworkServer* server = ResourceManager::get()->GetServer();
        if(server && server->GetOnline())
        {
            if(started_as_service)
            {
                ReportServiceStatus(SERVICE_RUNNING, NO_ERROR, 0);
            }
            WaitWhileServerOnline(server);
        }
    }

    /*-----------------------------------------------------*\
    | Perform ResourceManager cleanup before exiting        |
    \*-----------------------------------------------------*/
    ResourceManager::get()->Cleanup();

    LOG_TRACE("OpenRGB finishing with exit code %d", exitval);

    return exitval;
}
