// W32LowLevelMonitor.cc --- ActivityMonitor for W32
//
// Copyright (C) 2007, 2010, 2012 Ray Satiro <raysatiro@yahoo.com>
// All rights reserved.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//

/*
Alternate low-level activity monitor.

--
From MSDN, re WH_KEYBOARD_LL & WH_MOUSE_LL:

This hook is called in the context of the thread that
installed it. The call is made by sending a message to
the thread that installed the hook.

Therefore, the thread that installed the hook must
have a message loop.
--

Because a low-level hook is called in the context of
the thread that installed it, we can install the hook
from a time critical thread, and run a message loop.

This should solve the Philips slowdown problem:
1. high priority callback thread returns immediately after
posting a message to the dispatch thread
2. the dispatch thread processes and notifies main thread


LowLevelKeyboardProc Function ():
http://msdn2.microsoft.com/en-us/library/ms644985.aspx

LowLevelMouseProc Function ():
http://msdn2.microsoft.com/en-us/library/ms644986.aspx

Scheduling Priorities (Windows):
http://msdn2.microsoft.com/en-us/library/ms685100.aspx


jay satiro, workrave project, september 2007
*/

#include <assert.h>
#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include <windows.h>

#include "debug.hh"
#include <sstream>
#include "W32LowLevelMonitor.hh"
#include "Harpoon.hh"

W32LowLevelMonitor *W32LowLevelMonitor::singleton = NULL;

W32LowLevelMonitor::thread_struct *W32LowLevelMonitor::dispatch = NULL;
W32LowLevelMonitor::thread_struct *W32LowLevelMonitor::callback = NULL;

volatile HHOOK W32LowLevelMonitor::k_hook = NULL;
volatile HHOOK W32LowLevelMonitor::m_hook = NULL;

HMODULE W32LowLevelMonitor::process_handle = NULL;
/* FYI:
MS type BOOL is type int.
Therefore BOOL functions can return -1.
MSDN notes GetMessage BOOL return is not only 0,1 but also -1.
*/

W32LowLevelMonitor::W32LowLevelMonitor()
{
  TRACE_ENTER("W32LowLevelMonitor::W32LowLevelMonitor");

  if (singleton != NULL)
    {
      TRACE_RETURN(" singleton != NULL ");
      return;
    }

  singleton = this;

  dispatch = new thread_struct;
  dispatch->name = "Dispatch";

  callback = new thread_struct;
  callback->name = "Callback";

  k_hook = NULL;
  m_hook = NULL;

  process_handle = GetModuleHandle(NULL);

  TRACE_EXIT();
}

W32LowLevelMonitor::~W32LowLevelMonitor()
{
  TRACE_ENTER("W32LowLevelMonitor::~W32LowLevelMonitor");

  if (singleton != this)
    {
      TRACE_RETURN(" singleton != this ");
      return;
    }

  terminate();

  delete dispatch;
  delete callback;

  dispatch = NULL;
  callback = NULL;
  singleton = NULL;

  TRACE_EXIT();
}

bool
W32LowLevelMonitor::init()
{
  TRACE_ENTER("W32LowLevelMonitor::init");

  if (singleton != this)
    {
      TRACE_RETURN(" singleton != this ");
      return false;
    }

  terminate();

  dispatch->handle = CreateThread(NULL, 0, thread_Dispatch, this, 0, &dispatch->id);

  if (!wait_for_thread_queue(dispatch))
    {
      terminate();
      TRACE_EXIT();
      return false;
    }

  callback->handle = CreateThread(NULL, 0, thread_Callback, this, 0, &callback->id);

  if (!wait_for_thread_queue(callback))
    {
      terminate();
      TRACE_EXIT();
      return false;
    }

  Harpoon::init(NULL);

  TRACE_EXIT();
  return true;
}

bool
W32LowLevelMonitor::wait_for_thread_queue(thread_struct *thread)
{
  TRACE_ENTER_MSG("W32LowLevelMonitor::wait_for_thread_queue : ", thread->name);

  if (!thread->handle || !thread->id)
    {
      TRACE_RETURN(" thread: creation failed.");
      return false;
    }

  DWORD thread_exit_code;

  do
    {
      thread_exit_code = 0;
      Sleep(1);
      GetExitCodeThread(thread->handle, &thread_exit_code);
      if (thread_exit_code != STILL_ACTIVE)
        {
          TRACE_RETURN(" thread: terminated prematurely.");
          return false;
        }
    }
  while (thread->active == false);

  SetLastError(0);
  BOOL ret = PostThreadMessageW(thread->id, 0xFFFF, 0, 0);
  DWORD gle = GetLastError();

  if (!ret || gle)
    {
      TRACE_MSG(" thread: PostThreadMessage test failed.");
      TRACE_MSG(" thread: PostThreadMessage returned: " << ret);
      TRACE_MSG(" thread: GetLastError returned: " << gle);
      TRACE_EXIT();
      return false;
    }

  TRACE_EXIT();
  return true;
}

void
W32LowLevelMonitor::terminate()
{
  if (singleton != this)
    return;

  unhook();

  terminate_thread(callback);
  terminate_thread(dispatch);

  Harpoon::terminate();
}

void
W32LowLevelMonitor::terminate_thread(thread_struct *thread)
{
  thread->active = false;

  if (thread->id)
    PostThreadMessageW(thread->id, WM_QUIT, 0, 0);

  wait_for_thread_to_exit(thread);

  if (thread->handle != NULL)
    {
      CloseHandle(thread->handle);
      thread->handle = NULL;
    }
  thread->id = 0;
}

void
W32LowLevelMonitor::wait_for_thread_to_exit(thread_struct *thread)
{
  DWORD thread_exit_code = 0;

  if (!thread || !thread->handle)
    return;

  do
    {
      Sleep(1);
      GetExitCodeThread(thread->handle, &thread_exit_code);
    }
  while (thread_exit_code == STILL_ACTIVE);
}

void
W32LowLevelMonitor::unhook()
{
  if (k_hook)
    {
      UnhookWindowsHookEx(k_hook);
      k_hook = NULL;
    }

  if (m_hook)
    {
      UnhookWindowsHookEx(m_hook);
      m_hook = NULL;
    }
}

DWORD WINAPI
W32LowLevelMonitor::thread_Dispatch(LPVOID lpParam)
{
  W32LowLevelMonitor *pThis = (W32LowLevelMonitor *)lpParam;
  return pThis->dispatch_thread();
}

DWORD
W32LowLevelMonitor::dispatch_thread()
{
  dispatch->active = false;

  bool ret = 0;
  MSG msg;

  // It's good practice to force creation of the thread
  // message queue before setting active.
  PeekMessageW(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);
  dispatch->active = true;

  while ((ret = GetMessageW(&msg, NULL, 0, 0) > 0) && dispatch->active)
    {
      msg.message &= 0xFFFF;
      if (msg.message > WM_APP)
        // mouse notification
        {
          switch (msg.message - WM_APP)
            {
            // msg.wParam == x coord
            // msg.lParam == y coord
            case WM_MOUSEMOVE:
              fire_mouse(msg.wParam, msg.lParam, 0);
              break;

            case WM_MOUSEWHEEL:
            case WM_MOUSEHWHEEL:
              fire_mouse(msg.wParam, msg.lParam, 1);
              break;

            case WM_LBUTTONDOWN:
            case WM_MBUTTONDOWN:
            case WM_RBUTTONDOWN:
            case WM_XBUTTONDOWN:
              fire_button(true);
              break;

            case WM_LBUTTONUP:
            case WM_MBUTTONUP:
            case WM_RBUTTONUP:
            case WM_XBUTTONUP:
              fire_button(false);
              break;
            }
        }
      else if (msg.message == WM_APP)
        // keyboard notification
        {
          // kb->flags == msg.wParam
          if (msg.wParam & 0x00000080)
            // Transition state: key released
            fire_keyboard(false);
          else
            // Transition state: key pressed
            fire_action();
        }
    }

  dispatch->active = false;

  // Always return a value != STILL_ACTIVE (259)
  return (DWORD)ret;
}

DWORD WINAPI
W32LowLevelMonitor::thread_Callback(LPVOID lpParam)
{
  W32LowLevelMonitor *pThis = (W32LowLevelMonitor *)lpParam;
  return pThis->time_critical_callback_thread();
}

DWORD
W32LowLevelMonitor::time_critical_callback_thread()
{
  callback->active = false;

  int i = 0;
  MSG msg;

  // An attempt to set the thread priority to
  // THREAD_PRIORITY_TIME_CRITICAL.
  for (i = 0; (!SetThreadPriority(GetCurrentThread(), 15) && (i < 100)); ++i)
    {
      Sleep(1);
    }

  /*
  Do not double check using GetThreadPriority.
  It's possible, throughout this thread's lifetime, that
  an administrative application could change the priority.
  */

  // It's good practice to force creation of the thread
  // message queue before setting active.
  PeekMessageW(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);

  unhook();

  k_hook = SetWindowsHookExW(WH_KEYBOARD_LL, &k_hook_callback, process_handle, 0);
  m_hook = SetWindowsHookExW(WH_MOUSE_LL, &m_hook_callback, process_handle, 0);

  if (!k_hook || !m_hook)
    {
      unhook();
      return (DWORD)0;
    }

  callback->active = true;

  // Message loop. As noted, a hook is called in the
  // context of the thread that installed it. i.e. this one.
  while (GetMessageW(&msg, NULL, 0, 0) && callback->active)
    ;

  unhook();
  callback->active = false;

  // Always return a value != STILL_ACTIVE (259)
  return (DWORD)1;
}

LRESULT CALLBACK
W32LowLevelMonitor::k_hook_callback(int nCode, WPARAM wParam, LPARAM lParam)
{
  DWORD flags = ((_KBDLLHOOKSTRUCT *)lParam)->flags;

  if (!nCode && !(flags & LLKHF_INJECTED))
    // If there is an event, and it's not injected, notify.
    {
      PostThreadMessageW(dispatch->id,  // idThread
                         WM_APP,        // Msg
                         (WPARAM)flags, // wParam
                         (LPARAM)0      // lParam
      );
    }

  return CallNextHookEx((HHOOK)0, nCode, wParam, lParam);
}

LRESULT CALLBACK
W32LowLevelMonitor::m_hook_callback(int nCode, WPARAM wParam, LPARAM lParam)
{
  DWORD flags = ((_MSLLHOOKSTRUCT *)lParam)->flags;

  if (!nCode) // && !( flags & LLMHF_INJECTED ) )
              // If there is an event, and it's not injected, notify.
              // RC: My Wacom tablet driver injects mouse move events...
    {
      PostThreadMessageW(dispatch->id,                              // idThread
                         WM_APP + (DWORD)wParam,                    // Msg
                         (WPARAM)((_MSLLHOOKSTRUCT *)lParam)->pt.x, // wParam
                         (LPARAM)((_MSLLHOOKSTRUCT *)lParam)->pt.y  // lParam
      );
    }

  return CallNextHookEx((HHOOK)0, nCode, wParam, lParam);
}

// POINTTOPOINTS( ( (MSLLHOOKSTRUCT *) lParam )->pt )
