/*
 * This file Copyright (C) 2015 Mnemosyne LLC
 *
 * It may be used under the GNU GPL versions 2 or 3
 * or any future license endorsed by Mnemosyne LLC.
 *
 * $Id$
 */

#include <process.h> /* _beginthreadex () */

#include <windows.h>

#include <libtransmission/transmission.h>
#include <libtransmission/error.h>
#include <libtransmission/log.h>
#include <libtransmission/utils.h>

#include "daemon.h"

/***
****
***/

#ifndef SERVICE_ACCEPT_PRESHUTDOWN
 #define SERVICE_ACCEPT_PRESHUTDOWN 0x00000100
#endif
#ifndef SERVICE_CONTROL_PRESHUTDOWN
 #define SERVICE_CONTROL_PRESHUTDOWN 0x0000000F
#endif

static const dtr_callbacks * callbacks = NULL;
static void * callback_arg = NULL;

static const LPCWSTR service_name = L"TransmissionDaemon";

static SERVICE_STATUS_HANDLE status_handle = NULL;
static DWORD current_state = SERVICE_STOPPED;
static HANDLE service_thread = NULL;
static HANDLE service_stop_thread = NULL;

/***
****
***/

static void
set_system_error (tr_error   ** error,
                  DWORD         code,
                  const char  * message)
{
  char * const system_message = tr_win32_format_message (code);
  tr_error_set (error, code, "%s (0x%08lx): %s", message, code, system_message);
  tr_free (system_message);
}

static void
do_log_system_error (const char   * file,
                     int            line,
                     tr_log_level   level,
                     DWORD          code,
                     const char   * message)
{
  char * const system_message = tr_win32_format_message (code);
  tr_logAddMessage (file, line, level, "[dtr_daemon] %s (0x%08lx): %s", message, code, system_message);
  tr_free (system_message);
}

#define log_system_error(level, code, message) \
  do \
    { \
      const DWORD local_code = (code); \
      if (tr_logLevelIsActive ((level))) \
        do_log_system_error (__FILE__, __LINE__, (level), local_code, (message)); \
    } \
  while (0)

/***
****
***/

static BOOL WINAPI
handle_console_ctrl (DWORD control_type)
{
  (void) control_type;

  callbacks->on_stop (callback_arg);
  return TRUE;
}

static void
update_service_status (DWORD new_state,
                       DWORD win32_exit_code,
                       DWORD service_specific_exit_code,
                       DWORD check_point,
                       DWORD wait_hint)
{
  SERVICE_STATUS status;
  status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
  status.dwCurrentState = new_state;
  status.dwControlsAccepted = new_state != SERVICE_RUNNING ? 0 :
    SERVICE_ACCEPT_PRESHUTDOWN | SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PARAMCHANGE;
  status.dwWin32ExitCode = service_specific_exit_code == 0 ? win32_exit_code : ERROR_SERVICE_SPECIFIC_ERROR;
  status.dwServiceSpecificExitCode = service_specific_exit_code;
  status.dwCheckPoint = check_point;
  status.dwWaitHint = wait_hint;

  if (SetServiceStatus (status_handle, &status))
    current_state = new_state;
  else
    log_system_error (TR_LOG_DEBUG, GetLastError (), "SetServiceStatus() failed");
}

static unsigned int __stdcall
service_stop_thread_main (void * param)
{
  callbacks->on_stop (callback_arg);

  const DWORD sleep_time = 500;
  DWORD wait_time = (DWORD) (UINT_PTR) param;
  for (DWORD checkpoint = 2; WaitForSingleObject (service_thread, sleep_time) == WAIT_TIMEOUT; ++checkpoint)
    {
      wait_time = wait_time >= sleep_time ? wait_time - sleep_time : 0;
      update_service_status (SERVICE_STOP_PENDING, NO_ERROR, 0, checkpoint, MAX (wait_time, sleep_time * 2));
    }

  return 0;
}

static void
stop_service (void)
{
  if (service_stop_thread != NULL)
    {
      return;
    }

  const DWORD wait_time = 30 * 1000;

  update_service_status (SERVICE_STOP_PENDING, NO_ERROR, 0, 1, wait_time);

  service_stop_thread = (HANDLE) _beginthreadex (NULL, 0, &service_stop_thread_main,
                                                 (LPVOID) (UINT_PTR) wait_time, 0, NULL);
  if (service_stop_thread == NULL)
    {
      log_system_error (TR_LOG_DEBUG, GetLastError (), "_beginthreadex() failed, trying to stop synchronously");
      service_stop_thread_main ((LPVOID) (UINT_PTR) wait_time);
    }
}

static DWORD WINAPI
handle_service_ctrl (DWORD  control_code,
                     DWORD  event_type,
                     LPVOID event_data,
                     LPVOID context)
{
  (void) event_type;
  (void) event_data;
  (void) context;

  switch (control_code)
    {
      case SERVICE_CONTROL_PRESHUTDOWN:
      case SERVICE_CONTROL_SHUTDOWN:
      case SERVICE_CONTROL_STOP:
        stop_service ();
        return NO_ERROR;

      case SERVICE_CONTROL_PARAMCHANGE:
        callbacks->on_reconfigure (callback_arg);
        return NO_ERROR;

      case SERVICE_CONTROL_INTERROGATE:
        update_service_status (current_state, NO_ERROR, 0, 0, 0);
        return NO_ERROR;
    }

  return ERROR_CALL_NOT_IMPLEMENTED;
}

static unsigned int __stdcall
service_thread_main (void * context)
{
  (void) context;

  return callbacks->on_start (callback_arg, false);
}

static VOID WINAPI
service_main (DWORD    argc,
              LPWSTR * argv)
{
  (void) argc;
  (void) argv;

  status_handle = RegisterServiceCtrlHandlerExW (service_name, &handle_service_ctrl, NULL);
  if (status_handle == NULL)
    {
      log_system_error (TR_LOG_ERROR, GetLastError (), "RegisterServiceCtrlHandlerEx() failed");
      return;
    }

  update_service_status (SERVICE_START_PENDING, NO_ERROR, 0, 1, 1000);

  service_thread = (HANDLE) _beginthreadex (NULL, 0, &service_thread_main, NULL, 0, NULL);
  if (service_thread == NULL)
    {
      log_system_error (TR_LOG_ERROR, GetLastError (), "_beginthreadex() failed");
      return;
    }

  update_service_status (SERVICE_RUNNING, NO_ERROR, 0, 0, 0);

  if (WaitForSingleObject (service_thread, INFINITE) != WAIT_OBJECT_0)
    log_system_error (TR_LOG_ERROR, GetLastError (), "WaitForSingleObject() failed");

  if (service_stop_thread != NULL)
    {
      WaitForSingleObject (service_stop_thread, INFINITE);
      CloseHandle (service_stop_thread);
    }

  DWORD exit_code;
  if (!GetExitCodeThread (service_thread, &exit_code))
    exit_code = 1;

  CloseHandle (service_thread);

  update_service_status (SERVICE_STOPPED, NO_ERROR, exit_code, 0, 0);
}

/***
****
***/

bool
dtr_daemon (const dtr_callbacks  * cb,
            void                 * cb_arg,
            bool                   foreground,
            int                  * exit_code,
            tr_error            ** error)
{
  callbacks = cb;
  callback_arg = cb_arg;

  *exit_code = 1;

  if (foreground)
    {
      if (!SetConsoleCtrlHandler (&handle_console_ctrl, TRUE))
        {
          set_system_error (error, GetLastError (), "SetConsoleCtrlHandler() failed");
          return false;
        }

      *exit_code = cb->on_start (cb_arg, true);
    }
  else
    {
      const SERVICE_TABLE_ENTRY service_table[] =
        {
          { (LPWSTR) service_name, &service_main },
          { NULL, NULL }
        };

      if (!StartServiceCtrlDispatcherW (service_table))
        {
          set_system_error (error, GetLastError (), "StartServiceCtrlDispatcher() failed");
          return false;
        }

      *exit_code = 0;
    }

  return true;
}
