#include "keyboard_capturer.h"

#include <hidusage.h>

#include "rd_log.h"
#include "windows_input_marker.h"

namespace crossdesk {
namespace {

constexpr wchar_t kRawInputWindowClassName[] =
    L"CrossDeskKeyboardRawInputWindow";

bool PreferSideSpecificVkInjection(int key_code) {
  switch (key_code) {
    case VK_LSHIFT:
    case VK_RSHIFT:
    case VK_LCONTROL:
    case VK_RCONTROL:
    case VK_LMENU:
    case VK_RMENU:
    case VK_LWIN:
    case VK_RWIN:
      return true;
    default:
      return false;
  }
}

}  // namespace

PlatformKeyboardCapturer::PlatformKeyboardCapturer() {}

PlatformKeyboardCapturer::~PlatformKeyboardCapturer() { Unhook(); }

int PlatformKeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
  if (capture_thread_.joinable()) {
    return 0;
  }

  on_key_action_ = on_key_action;
  user_ptr_ = user_ptr;
  {
    std::lock_guard<std::mutex> lock(capture_state_mutex_);
    capture_thread_id_ = 0;
    capture_start_complete_ = false;
    capture_start_succeeded_ = false;
  }

  capture_thread_ = std::thread(&PlatformKeyboardCapturer::RawInputThreadMain, this);

  std::unique_lock<std::mutex> lock(capture_state_mutex_);
  capture_start_condition_.wait(
      lock, [this] { return capture_start_complete_; });
  const bool capture_started = capture_start_succeeded_;
  lock.unlock();

  if (!capture_started) {
    if (capture_thread_.joinable()) {
      capture_thread_.join();
    }
    on_key_action_ = nullptr;
    user_ptr_ = nullptr;
    return -1;
  }
  return 0;
}

int PlatformKeyboardCapturer::Unhook() {
  DWORD capture_thread_id = 0;
  {
    std::lock_guard<std::mutex> lock(capture_state_mutex_);
    capture_thread_id = capture_thread_id_;
  }
  if (capture_thread_id != 0 &&
      !PostThreadMessageW(capture_thread_id, WM_QUIT, 0, 0)) {
    LOG_WARN("Failed to stop keyboard raw input thread, thread_id={}, error={}",
             capture_thread_id, GetLastError());
  }
  if (capture_thread_.joinable()) {
    capture_thread_.join();
  }

  on_key_action_ = nullptr;
  user_ptr_ = nullptr;
  return 0;
}

void PlatformKeyboardCapturer::RawInputThreadMain() {
  const DWORD thread_id = GetCurrentThreadId();

  MSG message{};
  PeekMessageW(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
  const bool capture_started = CreateRawInputWindow();
  {
    std::lock_guard<std::mutex> lock(capture_state_mutex_);
    capture_thread_id_ = thread_id;
    capture_start_succeeded_ = capture_started;
    capture_start_complete_ = true;
  }
  capture_start_condition_.notify_one();

  if (!capture_started) {
    std::lock_guard<std::mutex> lock(capture_state_mutex_);
    capture_thread_id_ = 0;
    return;
  }

  LOG_INFO("Keyboard raw input capture started, thread_id={}", thread_id);
  while (true) {
    const BOOL get_message_result = GetMessageW(&message, nullptr, 0, 0);
    if (get_message_result <= 0) {
      if (get_message_result < 0) {
        LOG_WARN("Keyboard raw input message loop failed, thread_id={}, "
                 "error={}",
                 thread_id, GetLastError());
      }
      break;
    }
    TranslateMessage(&message);
    DispatchMessageW(&message);
  }

  DestroyRawInputWindow();
  {
    std::lock_guard<std::mutex> lock(capture_state_mutex_);
    capture_thread_id_ = 0;
  }
  LOG_INFO("Keyboard raw input capture stopped, thread_id={}", thread_id);
}

bool PlatformKeyboardCapturer::CreateRawInputWindow() {
  const HINSTANCE instance = GetModuleHandleW(nullptr);
  WNDCLASSEXW window_class{};
  window_class.cbSize = sizeof(window_class);
  window_class.lpfnWndProc = &PlatformKeyboardCapturer::RawInputWindowProc;
  window_class.hInstance = instance;
  window_class.lpszClassName = kRawInputWindowClassName;

  if (RegisterClassExW(&window_class) == 0) {
    const DWORD error = GetLastError();
    if (error != ERROR_CLASS_ALREADY_EXISTS) {
      LOG_WARN("Failed to register keyboard raw input window class, error={}",
               error);
      return false;
    }
  }

  raw_input_window_ = CreateWindowExW(
      0, kRawInputWindowClassName, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
      instance, this);
  if (!raw_input_window_) {
    LOG_WARN("Failed to create keyboard raw input window, error={}",
             GetLastError());
    return false;
  }

  RAWINPUTDEVICE keyboard_device{};
  keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC;
  keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD;
  keyboard_device.dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
  keyboard_device.hwndTarget = raw_input_window_;
  if (!RegisterRawInputDevices(&keyboard_device, 1,
                               sizeof(keyboard_device))) {
    LOG_WARN("Failed to register keyboard raw input, error={}", GetLastError());
    DestroyWindow(raw_input_window_);
    raw_input_window_ = nullptr;
    return false;
  }

  raw_input_registered_ = true;
  return true;
}

void PlatformKeyboardCapturer::DestroyRawInputWindow() {
  if (raw_input_registered_) {
    RAWINPUTDEVICE keyboard_device{};
    keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC;
    keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD;
    keyboard_device.dwFlags = RIDEV_REMOVE;
    keyboard_device.hwndTarget = nullptr;
    if (!RegisterRawInputDevices(&keyboard_device, 1,
                                 sizeof(keyboard_device))) {
      LOG_WARN("Failed to unregister keyboard raw input, error={}",
               GetLastError());
    }
    raw_input_registered_ = false;
  }
  if (raw_input_window_) {
    DestroyWindow(raw_input_window_);
    raw_input_window_ = nullptr;
  }
}

LRESULT CALLBACK PlatformKeyboardCapturer::RawInputWindowProc(
    HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
  PlatformKeyboardCapturer* capturer = reinterpret_cast<PlatformKeyboardCapturer*>(
      GetWindowLongPtrW(window, GWLP_USERDATA));
  if (message == WM_NCCREATE) {
    auto* create = reinterpret_cast<CREATESTRUCTW*>(l_param);
    capturer = static_cast<PlatformKeyboardCapturer*>(create->lpCreateParams);
    SetWindowLongPtrW(window, GWLP_USERDATA,
                      reinterpret_cast<LONG_PTR>(capturer));
  } else if (message == WM_INPUT && capturer) {
    capturer->HandleRawInput(reinterpret_cast<HRAWINPUT>(l_param));
  } else if (message == WM_NCDESTROY) {
    SetWindowLongPtrW(window, GWLP_USERDATA, 0);
  }
  return DefWindowProcW(window, message, w_param, l_param);
}

void PlatformKeyboardCapturer::HandleRawInput(HRAWINPUT raw_input_handle) {
  RAWINPUT input{};
  UINT input_size = sizeof(input);
  const UINT bytes_read =
      GetRawInputData(raw_input_handle, RID_INPUT, &input, &input_size,
                      sizeof(RAWINPUTHEADER));
  if (bytes_read == static_cast<UINT>(-1) ||
      bytes_read < sizeof(RAWINPUTHEADER) ||
      input.header.dwType != RIM_TYPEKEYBOARD) {
    return;
  }

  const RAWKEYBOARD& keyboard = input.data.keyboard;
  if (keyboard.VKey == 0xFF ||
      keyboard.ExtraInformation ==
          static_cast<ULONG>(kInjectedKeyboardInputMarker)) {
    return;
  }

  bool is_down = false;
  if (keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN) {
    is_down = true;
  } else if (keyboard.Message != WM_KEYUP &&
             keyboard.Message != WM_SYSKEYUP) {
    return;
  }

  const bool extended = (keyboard.Flags & (RI_KEY_E0 | RI_KEY_E1)) != 0;
  UINT mapped_scan_code = keyboard.MakeCode;
  if ((keyboard.Flags & RI_KEY_E0) != 0) {
    mapped_scan_code |= 0xE000;
  } else if ((keyboard.Flags & RI_KEY_E1) != 0) {
    mapped_scan_code |= 0xE100;
  }
  if (mapped_scan_code == 0xE11D || mapped_scan_code == 0xE02A) {
    return;
  }

  int key_code = static_cast<int>(keyboard.VKey);
  if (key_code == VK_SHIFT || key_code == VK_CONTROL || key_code == VK_MENU) {
    const UINT normalized =
        MapVirtualKeyW(mapped_scan_code, MAPVK_VSC_TO_VK_EX);
    if (normalized != 0) {
      key_code = static_cast<int>(normalized);
    }
  }

  if (on_key_action_) {
    on_key_action_(key_code, is_down, keyboard.MakeCode, extended, user_ptr_);
  }
}

// Apply remote keyboard commands to the local machine.
int PlatformKeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
                                          uint32_t scan_code, bool extended) {
  INPUT input = {0};
  input.type = INPUT_KEYBOARD;
  input.ki.dwExtraInfo =
      static_cast<ULONG_PTR>(kInjectedKeyboardInputMarker);

  const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
  const UINT resolved_scan_code =
      scan_code != 0
          ? static_cast<UINT>(scan_code & 0xFF) | (extended ? 0xE000u : 0u)
          : MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);

  if (scan_code != 0 && !prefer_vk) {
    input.ki.wVk = 0;
    input.ki.wScan = static_cast<WORD>(scan_code & 0xFF);
    input.ki.dwFlags |= KEYEVENTF_SCANCODE;
    if (extended) {
      input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
    }
  } else {
    input.ki.wVk = static_cast<WORD>(key_code);

    if (prefer_vk && resolved_scan_code != 0) {
      input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
      if ((resolved_scan_code & 0xFF00) != 0) {
        input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
      }
    } else if (resolved_scan_code != 0) {
      input.ki.wVk = 0;
      input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
      input.ki.dwFlags |= KEYEVENTF_SCANCODE;
      if ((resolved_scan_code & 0xFF00) != 0) {
        input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
      }
    }
  }

  if (!is_down) {
    input.ki.dwFlags |= KEYEVENTF_KEYUP;
  }

  const UINT sent = SendInput(1, &input, sizeof(INPUT));
  if (sent != 1) {
    LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code,
             is_down, GetLastError());
    return -1;
  }

  return 0;
}
}  // namespace crossdesk
