#pragma comment (lib, "Setupapi.lib")

#include "json.hpp"
#include "base64.h"
#include "sgfplib.h"     // Secugen SDK header
#include <atomic>
#include <exception>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <napi.h>
#include <stdexcept>
#include <map>
#include <string>
#include <thread>
#include <typeinfo>
#include <windows.h>
#include <Dbt.h>
#include <initguid.h>     // Include for GUID definitions
#include <winbio_ioctl.h> // Include for device interface GUIDs
#include <setupapi.h>     // Include for SetupDiGetDeviceInterfaceDetail
#include <cfgmgr32.h>     // Include for CM_Get_Device_ID

#include "loguru.hpp"
#include "utils.h"

#define WM_USER_SGCLOSEDEVICE (WM_USER + 1) // Custom message to close device
#define WM_USER_SGCONNECTDEVICE (WM_USER + 2) // Custom message to connect device
#define WM_USER_SGREMOVED (WM_USER + 3) // Custom message for device removal

GUID BiometricDeviceGuid = GUID_DEVINTERFACE_BIOMETRIC_READER;

// Global SGFPM object
HSGFPM g_hFpm;
std::atomic<bool> g_autoOnEnabled(false);
std::atomic<bool> g_capturing(false);
DWORD g_deviceId = USB_AUTO_DETECT;
SGDeviceInfoParam g_deviceInfo;
HDEVNOTIFY g_DeviceNotify = nullptr;
std::map<std::string, std::string> deviceInfoCache;

HWND g_message_window = nullptr; // Hidden window handle to receive messages

Napi::Value NlohmmanJsonToNapiValue(const Napi::Env &env,
                                    const nlohmann::json &json) {
  if (json.is_null()) {
    return env.Null();
  } else if (json.is_boolean()) {
    return Napi::Boolean::New(env, json.get<bool>());
  } else if (json.is_number_integer()) {
    return Napi::Number::New(env, json.get<int>());
  } else if (json.is_number_float()) {
    return Napi::Number::New(env, json.get<double>());
  } else if (json.is_string()) {
    return Napi::String::New(env, json.get<std::string>());
  } else if (json.is_array()) {
    Napi::Array arr = Napi::Array::New(env, json.size());
    for (size_t i = 0; i < json.size(); i++) {
      arr.Set(i, NlohmmanJsonToNapiValue(env, json[i]));
    }
    return arr;
  } else if (json.is_object()) {
    Napi::Object obj = Napi::Object::New(env);
    for (auto it = json.begin(); it != json.end(); ++it) {
      obj.Set(it.key(), NlohmmanJsonToNapiValue(env, it.value()));
    }
    return obj;
  } else {
    return env.Null();
  }
}

BOOL DoRegisterDeviceInterfaceToHwnd(IN GUID InterfaceClassGuid, IN HWND hWnd,
                                     OUT HDEVNOTIFY *hDeviceNotify) {
  DEV_BROADCAST_DEVICEINTERFACE NotificationFilter;

  ZeroMemory(&NotificationFilter, sizeof(NotificationFilter));
  NotificationFilter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
  NotificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
  NotificationFilter.dbcc_classguid = InterfaceClassGuid;

  *hDeviceNotify = RegisterDeviceNotification(
      hWnd,                       // events recipient
      &NotificationFilter,        // type of device
      DEVICE_NOTIFY_WINDOW_HANDLE // type of recipient handle
  );

  if (NULL == *hDeviceNotify) {
    DLOG_F(ERROR, "Error: %d", GetLastError());
    return FALSE;
  }

  return TRUE;
}

std::string GetDeviceFriendlyName(PDEV_BROADCAST_DEVICEINTERFACE lpdbv) {
  // Get Device Information Set
  HDEVINFO hDevInfo = SetupDiGetClassDevs(
      &BiometricDeviceGuid,                 // Use BiometricDeviceGuid
      NULL,                                 // Enumerator (use NULL for default)
      NULL,                                 // Parent window (can be NULL)
      DIGCF_PRESENT | DIGCF_DEVICEINTERFACE // Get currently present devices
  );

  if (hDevInfo == INVALID_HANDLE_VALUE) {
    DWORD lastError = GetLastError();
    DLOG_F(ERROR, "SetupDiGetClassDevs failed: %d", lastError);
    return "Unknown device";
  }

  SP_DEVICE_INTERFACE_DATA devInterfaceData = {0};
  devInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);

  // Iterate through device interfaces to find a match with the symbolic link
  DWORD memberIndex = 0;
  while (SetupDiEnumDeviceInterfaces(hDevInfo, NULL, &BiometricDeviceGuid,
                                     memberIndex, &devInterfaceData)) {
    // Get the required size for the device interface detail data
    DWORD detailDataSize = 0;
    SetupDiGetDeviceInterfaceDetail(hDevInfo, &devInterfaceData, NULL, 0,
                                    &detailDataSize, NULL);

    if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
      DLOG_F(INFO, "Detail data size: %d", detailDataSize);

      // Allocate buffer for device interface detail data
      PSP_DEVICE_INTERFACE_DETAIL_DATA detailData =
          (PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(detailDataSize);
      detailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);

      // Get the device interface detail data
      if (SetupDiGetDeviceInterfaceDetail(hDevInfo, &devInterfaceData,
                                          detailData, detailDataSize, NULL,
                                          NULL)) {
        std::wstring ws(detailData->DevicePath);
        std::string devicePath(ws.begin(), ws.end());
        DLOG_F(INFO, "Device interface detail data found: %s",
               devicePath.c_str());
        // Compare the device path from WM_DEVICECHANGE with the one from
        // SetupDiGetDeviceInterfaceDetail
        if (_wcsicmp(lpdbv->dbcc_name, detailData->DevicePath) == 0) {
          DLOG_F(INFO, "Device path match found");
          // Get Device Info Data using the matching devInterfaceData
          SP_DEVINFO_DATA devInfoData = {0};
          devInfoData.cbSize = sizeof(SP_DEVINFO_DATA);

          if (SetupDiEnumDeviceInfo(hDevInfo, memberIndex, &devInfoData)) {
            DLOG_F(INFO, "Device info data found");
            // Try retrieving the device description as well
            DWORD descriptionSize = 0;
            SetupDiGetDeviceRegistryPropertyW(hDevInfo, &devInfoData,
                                              SPDRP_DEVICEDESC, NULL, NULL, 0,
                                              &descriptionSize);

            std::vector<WCHAR> description(descriptionSize);
            if (SetupDiGetDeviceRegistryPropertyW(
                    hDevInfo, &devInfoData, SPDRP_DEVICEDESC, NULL,
                    (PBYTE)description.data(), descriptionSize, NULL)) {
              std::wstring ws(description.data());
              std::string deviceDesc(ws.begin(), ws.end());
              DLOG_F(INFO, "Device Description: %s", deviceDesc.c_str());

              free(detailData);
              SetupDiDestroyDeviceInfoList(hDevInfo);
              return deviceDesc;
            } else {
              DWORD lastError = GetLastError();
              DLOG_F(ERROR,
                     "SetupDiGetDeviceRegistryPropertyW (description) "
                     "failed: %d",
                     lastError);
            }

            // Get Friendly Name using Device Instance ID
            DWORD friendlyNameSize = 0;
            SetupDiGetDeviceRegistryPropertyW(hDevInfo, &devInfoData,
                                              SPDRP_FRIENDLYNAME, NULL, NULL, 0,
                                              &friendlyNameSize);

            std::vector<WCHAR> friendlyName(friendlyNameSize);
            if (SetupDiGetDeviceRegistryPropertyW(
                    hDevInfo, &devInfoData, SPDRP_FRIENDLYNAME, NULL,
                    (PBYTE)friendlyName.data(), friendlyNameSize, NULL)) {
              // Convert friendly name to std::string
              std::wstring ws(friendlyName.data());
              std::string finalName(ws.begin(), ws.end());

              DLOG_F(INFO, "Friendly name found: %s", finalName.c_str());

              free(detailData);
              SetupDiDestroyDeviceInfoList(hDevInfo);
              return finalName;
            } else {
              DWORD lastError = GetLastError();
              DLOG_F(ERROR,
                     "SetupDiGetDeviceRegistryPropertyW (friendlyName) "
                     "failed: %d",
                     lastError);
            }
          }
        }
        free(detailData);
      }
    }
    memberIndex++;
  }

  // Clean Up Device Information Set
  SetupDiDestroyDeviceInfoList(hDevInfo);

  return "Unknown device";
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam,
                            LPARAM lParam) {
  PDEV_BROADCAST_HDR lpdb;
  DLOG_F(INFO, "WindowProc: %d", uMsg);

  switch (uMsg) {
  case WM_CREATE:
    DLOG_F(INFO, "WM_CREATE received");
    if (!DoRegisterDeviceInterfaceToHwnd(BiometricDeviceGuid, hwnd,
                                         &g_DeviceNotify)) {
      // Terminate on failure.
      DLOG_F(ERROR, "DoRegisterDeviceInterfaceToHwnd failed");
      PostMessage(hwnd, WM_USER_SGCLOSEDEVICE, 0, 0);
    } else {
      DLOG_F(INFO, "DoRegisterDeviceInterfaceToHwnd succeeded");
    }
    break;
  case WM_CLOSE:
    if (!UnregisterDeviceNotification(g_DeviceNotify)) {
      DLOG_F(ERROR, "UnregisterDeviceNotification failed");
    }
    DestroyWindow(hwnd);
    break;
  case WM_DEVICECHANGE:
    lpdb = (PDEV_BROADCAST_HDR)lParam;
    if (lpdb->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) {
      PDEV_BROADCAST_DEVICEINTERFACE lpdbv =
          (PDEV_BROADCAST_DEVICEINTERFACE)lpdb;
      std::wstring ws(lpdbv->dbcc_name);
      std::string devicePath(ws.begin(), ws.end());

      std::string friendlyName = [lpdbv](std::string devicePath) {
        if (deviceInfoCache.find(devicePath) != deviceInfoCache.end()) {
          return deviceInfoCache[devicePath];
        } else {
          std::string friendlyName = GetDeviceFriendlyName(lpdbv);
          if (friendlyName == "SecuGen fingerprint device") {
            deviceInfoCache[devicePath] = friendlyName;
          }
          return friendlyName;
        }
      }(devicePath);

      if (wParam == DBT_DEVICEARRIVAL &&
          friendlyName == "SecuGen fingerprint device") {
        PostMessage(hwnd, WM_USER_SGCONNECTDEVICE, 0, 0);
      } else if (wParam == DBT_DEVICEREMOVECOMPLETE) {
        PostMessage(hwnd, WM_USER_SGREMOVED, 0, 0);
      }
    }
    break;
  default:
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
  }

  return TRUE;
}

// Function to handle Auto-On events (runs in a separate thread)
void autoOnThread(Napi::ThreadSafeFunction tsfn) {
  const wchar_t *windowClassName = L"SecugenHandlerHiddenWindowClass";
  DWORD err;
  bool fingerOn = false; // Keep track of finger state
  BYTE *templateBuffer = nullptr;
  bool closeDeviceOnCleanup = false;

  try {
    // Create Hidden Window to receive messages
    WNDCLASSEX wx = {};
    wx.cbSize = sizeof(WNDCLASSEX);
    wx.lpfnWndProc = WindowProc;
    wx.hInstance = GetModuleHandle(nullptr);
    wx.lpszClassName = windowClassName;
    RegisterClassEx(&wx);

    g_message_window =
        CreateWindowEx(0, windowClassName, NULL, 0, 0, 0, 0, 0, HWND_MESSAGE,
                       NULL, GetModuleHandle(nullptr), NULL);

    if (g_message_window == NULL) {
      DWORD lastError = GetLastError();
      tsfn.BlockingCall([lastError](Napi::Env env, Napi::Function jsCallback) {
        Napi::String errorMessage = Napi::String::New(
            env, "Error creating hidden window: " + std::to_string(lastError));
        jsCallback.Call({Napi::String::New(env, "error"), errorMessage});
      });
      return;
    }

    SGFPM_SetLedOn(g_hFpm, TRUE);

    // Enable Auto-On
    err = SGFPM_EnableAutoOnEvent(g_hFpm, TRUE, g_message_window, 0);
    if (err != SGFDX_ERROR_NONE) {
      tsfn.BlockingCall([err](Napi::Env env, Napi::Function jsCallback) {
        Napi::String errorMessage =
            Napi::String::New(env, Utils::SGFPMErrorToMessage(err));
        jsCallback.Call({
            Napi::String::New(env, "error"),
            errorMessage,
        });
      });
      return;
    }

    g_autoOnEnabled = true;

    // Message Loop
    MSG msg = {};
    while (GetMessage(&msg, NULL, 0, 0) > 0) {
      TranslateMessage(&msg);
      DispatchMessage(&msg);

      DLOG_F(INFO, "Message: %d", msg.message);

      if (msg.message == WM_APP_SGAUTOONEVENT) {
        WORD isFinger = msg.wParam;

        if (isFinger == SGDEVEVNET_FINGER_ON && !fingerOn) { // Finger ON
          tsfn.BlockingCall([](Napi::Env env, Napi::Function jsCallback) {
            jsCallback.Call({Napi::String::New(env, "fingerOn")});
          });

          // Blink LED
          SGFPM_SetLedOn(g_hFpm, FALSE);

          // Capture image and create template
          std::vector<BYTE> imageData(
              g_deviceInfo.ImageWidth *
              g_deviceInfo.ImageHeight); // Allocate image buffer

          err = SGFPM_GetImage(g_hFpm, imageData.data());

          if (err != SGFDX_ERROR_NONE) {
            tsfn.BlockingCall([err](Napi::Env env, Napi::Function jsCallback) {
              Napi::Value payload = NlohmmanJsonToNapiValue(
                  env,
                  {
                      {"ok", false},
                      {"code", Utils::SGErrortoString((SGFDxErrorCode)err)},
                      {"message", Utils::SGFPMErrorToMessage(err)},
                  });
              jsCallback.Call({Napi::String::New(env, "capture"), payload});
            });
            goto END_OF_FINGER_ON;
          }

          // Get image quality
          DWORD image_quality;
          DWORD maxTemplateSize;

          SGFPM_GetImageQuality(g_hFpm, g_deviceInfo.ImageWidth,
                                g_deviceInfo.ImageHeight, imageData.data(),
                                &image_quality);

          if (image_quality <= 40) {
            tsfn.BlockingCall([](Napi::Env env, Napi::Function jsCallback) {
              Napi::Value payload = NlohmmanJsonToNapiValue(
                  env, {
                           {"ok", false},
                           {"code",
                            Utils::CustomErrorCodeToString(
                                Utils::CustomErrorCode::CAPTURE_LOW_QUALITY)},
                           {"message",
                            Utils::CustomErrorCodeToMessage(
                                Utils::CustomErrorCode::CAPTURE_LOW_QUALITY)},
                       });
              jsCallback.Call({Napi::String::New(env, "capture"), payload});
            });
            goto END_OF_FINGER_ON;
          }

          // Create template
          err = SGFPM_GetMaxTemplateSize(g_hFpm, &maxTemplateSize);
          templateBuffer = new BYTE[maxTemplateSize];

          SGFingerInfo finger_info;
          finger_info.FingerNumber = SG_FINGPOS_RI;
          finger_info.ImageQuality = (WORD)image_quality;
          finger_info.ImpressionType = SG_IMPTYPE_LP;
          finger_info.ViewNumber = 0;

          err = SGFPM_CreateTemplate(g_hFpm, &finger_info, imageData.data(),
                                     templateBuffer);

          if (err != SGFDX_ERROR_NONE) {
            tsfn.BlockingCall([err](Napi::Env env, Napi::Function jsCallback) {
              Napi::Value payload = NlohmmanJsonToNapiValue(
                  env, {{"ok", false},
                        {"code", Utils::SGErrortoString((SGFDxErrorCode)err)},
                        {"message", Utils::SGFPMErrorToMessage(err)}});
              jsCallback.Call({Napi::String::New(env, "capture"), payload});
            });
            goto END_OF_FINGER_ON;
          }

          // Send template to JS
          tsfn.BlockingCall([imageData, image_quality, templateBuffer,
                             maxTemplateSize](Napi::Env env,
                                              Napi::Function jsCallback) {
            Napi::Value payload = NlohmmanJsonToNapiValue(
                env,
                {{"ok", true},
                 {"raw_image",
                  {{"base64", base64_encode(imageData.data(),
                                            g_deviceInfo.ImageWidth *
                                                g_deviceInfo.ImageHeight)},
                   {"width", g_deviceInfo.ImageWidth},
                   {"height", g_deviceInfo.ImageHeight}}},
                 {"template",
                  {{"base64", base64_encode(templateBuffer, maxTemplateSize)},
                   {"quality", image_quality}}}});

            jsCallback.Call({Napi::String::New(env, "capture"), payload});
          });

        END_OF_FINGER_ON:
          if (templateBuffer != nullptr) {
            delete[] templateBuffer;
            templateBuffer = nullptr;
          }
          SGFPM_SetLedOn(g_hFpm, TRUE);
          fingerOn = true;
        } else if (isFinger == SGDEVEVNET_FINGER_OFF &&
                   fingerOn) { // Finger OFF
          tsfn.BlockingCall([](Napi::Env env, Napi::Function jsCallback) {
            jsCallback.Call({Napi::String::New(env, "fingerOff")});
          });

          fingerOn = false;
        }
      } else if (msg.message == WM_QUIT) {
        DLOG_F(INFO, "Received WM_QUIT message");
        break;
      } else if (msg.message == WM_USER_SGCLOSEDEVICE ||
                 msg.message == WM_USER_SGREMOVED) {
        DLOG_F(INFO, "Received WM_USER_SGCLOSEDEVICE message");
        closeDeviceOnCleanup = true;
        break;
      }
    }
  } catch (...) {
    DLOG_F(INFO, "Exception ocurred");
  }

  // Cleanup
  if (templateBuffer != nullptr) {
    DLOG_F(INFO, "Deleting template buffer");
    delete[] templateBuffer;
    templateBuffer = nullptr;
  }

  if (g_hFpm != nullptr) {
    DLOG_F(INFO, "Disabling Auto-On");
    SGFPM_EnableAutoOnEvent(g_hFpm, FALSE, g_message_window, 0);
    SGFPM_SetLedOn(g_hFpm, FALSE);

    if (closeDeviceOnCleanup) {
      DLOG_F(INFO, "Closing device on cleanup");
      SGFPM_CloseDevice(g_hFpm);
      SGFPM_Terminate(g_hFpm);
      g_hFpm = nullptr;
    }
  }
  if (g_message_window != nullptr) {
    DLOG_F(INFO, "Destroying message window");
    DestroyWindow(g_message_window);
    UnregisterClass(windowClassName, GetModuleHandle(nullptr));
    g_message_window = nullptr;
  }

  g_autoOnEnabled = false;

  // Release the ThreadSafeFunction, allowing it to release its associated
  // resources
  tsfn.Release();
}

Napi::Value GetDeviceInfo(const Napi::CallbackInfo &info) {
  Napi::Env env = info.Env();

  if (g_hFpm == nullptr) {
    nlohmann::json error = {
        {"ok", false},
        {"code", Utils::CustomErrorCodeToString(
                     Utils::CustomErrorCode::DEVICE_NOT_OPENED)},
        {"message", Utils::CustomErrorCodeToMessage(
                        Utils::CustomErrorCode::DEVICE_NOT_OPENED)}};
    Napi::Error::New(env, error.dump()).ThrowAsJavaScriptException();
    return env.Null();
  }

  SGDeviceInfoParam device_info;
  memset(&device_info, 0x00, sizeof(device_info));
  DWORD error = SGFPM_GetDeviceInfo(g_hFpm, &device_info);

  if (error != SGFDX_ERROR_NONE) {
    nlohmann::json error = {
        {"ok", false},
        {"code", Utils::SGErrortoString((SGFDxErrorCode)error)},
        {"message", Utils::SGFPMErrorToMessage(error)}};
    return NlohmmanJsonToNapiValue(env, error);
  }

  return NlohmmanJsonToNapiValue(env, {{"ok", true},
                                       {"deviceId", device_info.DeviceID},
                                       {"imageWidth", device_info.ImageWidth},
                                       {"imageHeight", device_info.ImageHeight},
                                       {"contrast", device_info.Contrast},
                                       {"brightness", device_info.Brightness},
                                       {"gain", device_info.Gain},
                                       {"imageDpi", device_info.ImageDPI}});
}

Napi::Value OpenDevice(const Napi::CallbackInfo &info) {
  Napi::Env env = info.Env();
  DWORD err;

  if (g_hFpm != nullptr) {
    return NlohmmanJsonToNapiValue(
        env, {{"ok", false},
              {"code", Utils::CustomErrorCodeToString(
                           Utils::CustomErrorCode::DEVICE_NOT_OPENED)},
              {"message", Utils::CustomErrorCodeToMessage(
                              Utils::CustomErrorCode::DEVICE_NOT_OPENED)}});
  }

  err = SGFPM_Create(&g_hFpm);
  if (err != SGFDX_ERROR_NONE) {
    return NlohmmanJsonToNapiValue(
        env, {{"ok", false},
              {"code", Utils::SGErrortoString((SGFDxErrorCode)err)},
              {"message", Utils::SGFPMErrorToMessage(err)}});
  }

  err = SGFPM_Init(g_hFpm, SG_DEV_AUTO);
  if (err != SGFDX_ERROR_NONE) {
    SGFPM_Terminate(g_hFpm);
    g_hFpm = nullptr;

    return NlohmmanJsonToNapiValue(
        env, {{"ok", false},
              {"code", Utils::SGErrortoString((SGFDxErrorCode)err)},
              {"message", Utils::SGFPMErrorToMessage(err)}});
  }

  err = SGFPM_OpenDevice(g_hFpm, g_deviceId);
  if (err != SGFDX_ERROR_NONE) {
    SGFPM_Terminate(g_hFpm);
    g_hFpm = nullptr;

    return NlohmmanJsonToNapiValue(
        env, {{"ok", false},
              {"code", Utils::SGErrortoString((SGFDxErrorCode)err)},
              {"message", Utils::SGFPMErrorToMessage(err)}});
  }

  memset(&g_deviceInfo, 0x00, sizeof(g_deviceInfo));
  err = SGFPM_GetDeviceInfo(g_hFpm, &g_deviceInfo);

  if (err != SGFDX_ERROR_NONE) {
    SGFPM_CloseDevice(g_hFpm);
    SGFPM_Terminate(g_hFpm);
    g_hFpm = nullptr;

    return NlohmmanJsonToNapiValue(
        env, {{"ok", false},
              {"code", Utils::SGErrortoString((SGFDxErrorCode)err)},
              {"message", Utils::SGFPMErrorToMessage(err)}});
  }

  return NlohmmanJsonToNapiValue(env, {{"ok", true}});
}

Napi::Value CloseDevice(const Napi::CallbackInfo &info) {
  Napi::Env env = info.Env();

  if (g_hFpm == nullptr) {
    return NlohmmanJsonToNapiValue(
        env, {{"ok", false},
              {"code", Utils::CustomErrorCodeToString(
                           Utils::CustomErrorCode::DEVICE_NOT_OPENED)},
              {"message", Utils::CustomErrorCodeToMessage(
                              Utils::CustomErrorCode::DEVICE_NOT_OPENED)}});
  }

  if (g_autoOnEnabled == true) {
    DLOG_F(INFO, "Auto-On is active. Stopping Auto-On");
    PostMessage(g_message_window, WM_USER_SGCLOSEDEVICE, 0,
                0); // Post custom message to close device

    while (g_autoOnEnabled == true) {
      std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    DLOG_F(INFO, "Auto-On stopped");

    return NlohmmanJsonToNapiValue(env, {{"ok", true}});
  } else if (g_capturing == true) {
    DLOG_F(INFO, "Capture is active. Stopping capture");

    while (g_capturing == true) {
      std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    DLOG_F(INFO, "Capture stopped");
  }

  DLOG_F(INFO, "Closing device");

  std::this_thread::sleep_for(std::chrono::milliseconds(500));

  SGFPM_CloseDevice(g_hFpm);
  SGFPM_Terminate(g_hFpm);
  g_hFpm = nullptr;
  g_deviceInfo = {};

  return NlohmmanJsonToNapiValue(env, {{"ok", true}});
}

// Function to start the Auto-On thread
Napi::Value StartAutoOn(const Napi::CallbackInfo &info) {
  Napi::Env env = info.Env();
  Napi::Function jsCallback = info[0].As<Napi::Function>();

  if (g_hFpm == nullptr) {
    Napi::Error::New(env, "Device is not opened").ThrowAsJavaScriptException();
    return env.Undefined();
  }

  if (g_autoOnEnabled) {
    DLOG_F(INFO, "Auto-On is already active");
    return env.Undefined();
  }

  DLOG_F(INFO, "Starting Auto-On thread");

  auto tsfn =
      Napi::ThreadSafeFunction::New(env, jsCallback, "AutoOnEvent", 0, 1);

  std::thread autoOnThread(autoOnThread, tsfn);
  autoOnThread.detach();

  return env.Undefined();
}

// Function to stop Auto-On
Napi::Value StopAutoOn(const Napi::CallbackInfo &info) {
  if (!g_autoOnEnabled) {
    DLOG_F(INFO, "Auto-On is not active");
    return info.Env().Undefined();
  }

  PostMessage(g_message_window, WM_QUIT, 0, 0); // Post WM_QUIT message

  while (g_autoOnEnabled == true) {
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
  }

  return info.Env().Undefined();
}

Napi::Value CompareTemplates(const Napi::CallbackInfo &info) {
  Napi::Env env = info.Env();

  std::string firstTemplate;
  std::string secondTemplate;

  std::string firstTemplateBuffer;
  std::string secondTemplateBuffer;

  DWORD security_level;
  BOOL matched;
  DWORD err;

  Napi::Object payload = Napi::Object::New(env);
  payload.Set("ok", false);
  payload.Set("code", Utils::CustomErrorCodeToString(
                          Utils::CustomErrorCode::UNKNOWN_ERROR));
  payload.Set("message", Utils::CustomErrorCodeToMessage(
                             Utils::CustomErrorCode::UNKNOWN_ERROR));

  if (info.Length() < 2) {
    Napi::TypeError::New(env, "Wrong number of arguments")
        .ThrowAsJavaScriptException();
    return env.Null();
  }

  if (!info[0].IsString()) {
    Napi::TypeError::New(
        env, "You need to pass the first template as a base64 encoded string.")
        .ThrowAsJavaScriptException();
    return env.Null();
  }

  if (!info[1].IsString()) {
    Napi::TypeError::New(
        env, "You need to pass the second template as a base64 encoded string.")
        .ThrowAsJavaScriptException();
    return env.Null();
  }

  if (g_hFpm == nullptr) {
    payload.Set("code", Utils::CustomErrorCodeToString(
                            Utils::CustomErrorCode::DEVICE_NOT_OPENED));
    payload.Set("message", Utils::CustomErrorCodeToMessage(
                               Utils::CustomErrorCode::DEVICE_NOT_OPENED));
    goto CompareTemplates_Exit;
  }

  firstTemplate = info[0].As<Napi::String>().Utf8Value();
  secondTemplate = info[1].As<Napi::String>().Utf8Value();

  firstTemplateBuffer = base64_decode(firstTemplate, false);
  secondTemplateBuffer = base64_decode(secondTemplate, false);

  security_level = SL_NORMAL;
  matched = false;
  err = SGFPM_MatchTemplate(g_hFpm, (BYTE *)firstTemplateBuffer.c_str(),
                            (BYTE *)secondTemplateBuffer.c_str(),
                            security_level, &matched);

  if (err != SGFDX_ERROR_NONE) {
    payload.Set("code", Utils::SGErrortoString((SGFDxErrorCode)err));
    payload.Set("message", Utils::SGFPMErrorToMessage(err));
    goto CompareTemplates_Exit;
  }

  payload.Set("ok", true);
  payload.Delete("code");
  payload.Delete("message");
  payload.Set("isMatch", Napi::Boolean::New(env, matched));

CompareTemplates_Exit:
  return payload;
}

class GetImageWorker : public Napi::AsyncWorker {
public:
  GetImageWorker(Napi::Function &callback, Napi::Number &timeout) : Napi::AsyncWorker(callback) {
    if (timeout.IsNumber()) {
      m_timeout = timeout.As<Napi::Number>().Uint32Value();
    } else {
      m_timeout = 10000;
    }
  }
  ~GetImageWorker() {
    if (templateBuffer != NULL) {
      delete[] templateBuffer;
    }
  }

  void Execute() {
    SGFingerInfo finger_info;

    DWORD imageWidth = g_deviceInfo.ImageWidth;
    DWORD imageHeight = g_deviceInfo.ImageHeight;
    DWORD minAcceptingQuality = 40;

    SGFPM_SetLedOn(g_hFpm, TRUE);

    m_imageData.resize(imageWidth * imageHeight);
    DWORD err = SGFPM_GetImageEx(g_hFpm, m_imageData.data(), m_timeout, NULL, minAcceptingQuality);

    if (err != SGFDX_ERROR_NONE) {
      DLOG_F(ERROR, "Failed to capture image: %s",
             Utils::SGFPMErrorToMessage(err).c_str());
      return SetError(
          nlohmann::json({{"ok", false},
                          {"code", Utils::SGErrortoString((SGFDxErrorCode)err)},
                          {"message", Utils::SGFPMErrorToMessage(err)}})
              .dump());
    }

    err = SGFPM_GetImageQuality(g_hFpm, imageWidth, imageHeight,
                                m_imageData.data(), &image_quality);
    if (err != SGFDX_ERROR_NONE) {
      return SetError(
          nlohmann::json({{"ok", false},
                          {"code", Utils::SGErrortoString((SGFDxErrorCode)err)},
                          {"message", Utils::SGFPMErrorToMessage(err)}})
              .dump());
    }

    if (image_quality <= 40) {
      return SetError(
          nlohmann::json({{"ok", false},
                          {"code", Utils::CustomErrorCodeToString(
                                       Utils::CustomErrorCode::CAPTURE_LOW_QUALITY)},
                          {"message", Utils::CustomErrorCodeToMessage(
                                         Utils::CustomErrorCode::CAPTURE_LOW_QUALITY)}})
              .dump());
    }

    // Create template from captured image
    err = SGFPM_GetMaxTemplateSize(g_hFpm, &maxTemplateSize);

    if (err != SGFDX_ERROR_NONE) {
      return SetError(
          nlohmann::json({{"ok", false},
                          {"code", Utils::SGErrortoString((SGFDxErrorCode)err)},
                          {"message", Utils::SGFPMErrorToMessage(err)}})
              .dump());
    }

    if (templateBuffer != NULL) {
      delete[] templateBuffer;
    }

    templateBuffer = new BYTE[maxTemplateSize];

    // Set information about template
    finger_info.FingerNumber = SG_FINGPOS_RI;
    finger_info.ImageQuality = (WORD)image_quality;
    finger_info.ImpressionType = SG_IMPTYPE_LP;
    finger_info.ViewNumber = 0;

    err = SGFPM_CreateTemplate(g_hFpm, &finger_info, m_imageData.data(),
                               templateBuffer);

    if (err != SGFDX_ERROR_NONE) {
      delete[] templateBuffer;
      return SetError(
          nlohmann::json({{"ok", false},
                          {"code", Utils::SGErrortoString((SGFDxErrorCode)err)},
                          {"message", Utils::SGFPMErrorToMessage(err)}})
              .dump());
    }
  }

  void OnOK() {
    SGFPM_SetLedOn(g_hFpm, FALSE);
    g_capturing = false;
    Napi::Value payload = NlohmmanJsonToNapiValue(
        Env(), {{"ok", true},
                {"raw_image",
                 {{"base64", base64_encode(m_imageData.data(),
                                           g_deviceInfo.ImageWidth *
                                               g_deviceInfo.ImageHeight)},
                  {"width", g_deviceInfo.ImageWidth},
                  {"height", g_deviceInfo.ImageHeight}}},
                {"template",
                 {{"base64", base64_encode(templateBuffer, maxTemplateSize)},
                  {"quality", image_quality}}}});

    Callback().Call({Env().Null(), payload});
  }

  void OnError(const Napi::Error &e) {
    SGFPM_SetLedOn(g_hFpm, FALSE);
    g_capturing = false;

    DLOG_F(ERROR, "GetImageWorker error: %s", e.Message().c_str());

    nlohmann::json error = nlohmann::json::parse(e.Message());
    Callback().Call({NlohmmanJsonToNapiValue(Env(), error), Env().Null()});
  }

private:
  DWORD m_timeout;
  std::vector<BYTE> m_imageData; // Allocate image buffer
  DWORD image_quality;
  BYTE *templateBuffer = nullptr;
  DWORD maxTemplateSize;
};

Napi::Value CaptureAndCreateTemplate(const Napi::CallbackInfo &info) {
  Napi::Env env = info.Env();

  if (info.Length() < 2) {
    Napi::TypeError::New(env, "Wrong number of arguments")
        .ThrowAsJavaScriptException();
    return env.Null();
  }

  Napi::Function callback = info[0].As<Napi::Function>();
  Napi::Number timeout = info[1].As<Napi::Number>();

  if (g_hFpm == nullptr) {
    Napi::Error::New(info.Env(), "Device is not opened")
        .ThrowAsJavaScriptException();
    return info.Env().Undefined();
  }

  if (g_autoOnEnabled) {
    Napi::Error::New(info.Env(), "Auto-On is currently active. Please turn off "
                                 "before executing this function.")
        .ThrowAsJavaScriptException();
    return info.Env().Undefined();
  }
  g_capturing = true;

  GetImageWorker *worker = new GetImageWorker(callback, timeout);
  worker->Queue();
  return info.Env().Undefined();
}

Napi::Value IsAutoOnActive(const Napi::CallbackInfo &info) {
  return Napi::Boolean::New(info.Env(), g_autoOnEnabled);
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "openDevice"),
              Napi::Function::New(env, OpenDevice));
  exports.Set(Napi::String::New(env, "closeDevice"),
              Napi::Function::New(env, CloseDevice));
  exports.Set(Napi::String::New(env, "getDeviceInfo"),
              Napi::Function::New(env, GetDeviceInfo));
  exports.Set(Napi::String::New(env, "startAutoOn"),
              Napi::Function::New(env, StartAutoOn));
  exports.Set(Napi::String::New(env, "stopAutoOn"),
              Napi::Function::New(env, StopAutoOn));
  exports.Set(Napi::String::New(env, "captureAndCreateTemplate"),
              Napi::Function::New(env, CaptureAndCreateTemplate));
  exports.Set(Napi::String::New(env, "compareTemplates"),
              Napi::Function::New(env, CompareTemplates));
  exports.Set(Napi::String::New(env, "isAutoOnActive"),
              Napi::Function::New(env, IsAutoOnActive));

  return exports;
}

NODE_API_MODULE(secugen, Init)
