#include "screen_capturer_gdi.h"

#include <algorithm>
#include <chrono>
#include <string>
#include <vector>

#include <display_stream_id.h>
#include "libyuv.h"
#include "rd_log.h"

namespace crossdesk {

namespace {
std::string WideToUtf8(const std::wstring& wstr) {
  if (wstr.empty()) return {};
  int size_needed = WideCharToMultiByte(
      CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
  std::string result(size_needed, 0);
  WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(),
                      size_needed, nullptr, nullptr);
  return result;
}

std::string GetDisplayLabel(const std::wstring& wide_name) {
  std::string name = WideToUtf8(wide_name);
  constexpr char kDevicePrefix[] = "\\\\.\\";
  if (name.rfind(kDevicePrefix, 0) == 0) {
    name.erase(0, sizeof(kDevicePrefix) - 1);
  }
  return name;
}
}  // namespace

ScreenCapturerGdi::ScreenCapturerGdi() {}
ScreenCapturerGdi::~ScreenCapturerGdi() {
  Stop();
  Destroy();
}

BOOL CALLBACK ScreenCapturerGdi::EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
                                                 LPARAM data) {
  auto displays = reinterpret_cast<std::vector<DisplayInfo>*>(data);
  MONITORINFOEX mi{};
  mi.cbSize = sizeof(MONITORINFOEX);
  if (GetMonitorInfo(hMonitor, &mi)) {
    std::string name = GetDisplayLabel(mi.szDevice);
    bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
    DisplayInfo info((void*)hMonitor, name, is_primary, mi.rcMonitor.left,
                     mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom);
    if (is_primary)
      displays->insert(displays->begin(), info);
    else
      displays->push_back(info);
  }
  return TRUE;
}

void ScreenCapturerGdi::EnumerateDisplays() {
  display_info_list_.clear();
  EnumDisplayMonitors(nullptr, nullptr, EnumMonitorProc,
                      (LPARAM)&display_info_list_);
}

int ScreenCapturerGdi::Init(const int fps, cb_desktop_data cb) {
  fps_ = fps;
  callback_ = cb;
  if (!callback_) {
    LOG_ERROR("GDI: callback is null");
    return -1;
  }
  EnumerateDisplays();
  if (display_info_list_.empty()) {
    LOG_ERROR("GDI: no displays found");
    return -2;
  }
  monitor_index_ = 0;
  initial_monitor_index_ = monitor_index_;
  return 0;
}

int ScreenCapturerGdi::Destroy() {
  Stop();
  if (nv12_frame_) {
    delete[] nv12_frame_;
    nv12_frame_ = nullptr;
    nv12_width_ = 0;
    nv12_height_ = 0;
  }
  return 0;
}

int ScreenCapturerGdi::Start(bool show_cursor) {
  if (running_) return 0;
  show_cursor_ = show_cursor;
  paused_ = false;
  running_ = true;
  thread_ = std::thread([this]() { CaptureLoop(); });
  return 0;
}

int ScreenCapturerGdi::Stop() {
  if (!running_) return 0;
  running_ = false;
  if (thread_.joinable()) thread_.join();
  return 0;
}

int ScreenCapturerGdi::Pause(int monitor_index) {
  paused_ = true;
  return 0;
}

int ScreenCapturerGdi::Resume(int monitor_index) {
  paused_ = false;
  return 0;
}

int ScreenCapturerGdi::SwitchTo(int monitor_index) {
  if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
    LOG_ERROR("GDI: invalid monitor index {}", monitor_index);
    return -1;
  }
  monitor_index_ = monitor_index;
  LOG_INFO("GDI: switched to monitor {}:{}", monitor_index_.load(),
           display_info_list_[monitor_index_].name);
  return 0;
}

int ScreenCapturerGdi::ResetToInitialMonitor() {
  if (display_info_list_.empty()) return -1;
  int target = initial_monitor_index_;
  if (target < 0 || target >= (int)display_info_list_.size()) return -1;
  monitor_index_ = target;
  LOG_INFO("GDI: reset to initial monitor {}:{}", monitor_index_.load(),
           display_info_list_[monitor_index_].name);
  return 0;
}

void ScreenCapturerGdi::CaptureLoop() {
  int interval_ms = fps_ > 0 ? (1000 / fps_) : 16;
  HDC screen_dc = GetDC(nullptr);
  while (running_) {
    if (paused_) {
      std::this_thread::sleep_for(std::chrono::milliseconds(10));
      continue;
    }
    if (!screen_dc) {
      std::this_thread::sleep_for(std::chrono::milliseconds(10));
      continue;
    }

    int idx = monitor_index_.load();
    if (idx < 0 || idx >= static_cast<int>(display_info_list_.size())) {
      LOG_ERROR("GDI: CaptureLoop invalid monitor_index {} (list size {})",
                idx, display_info_list_.size());
      std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
      continue;
    }
    const auto& di = display_info_list_[idx];
    int left = di.left;
    int top = di.top;
    int width = di.width & ~1;
    int height = di.height & ~1;
    if (width <= 0 || height <= 0) {
      std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
      continue;
    }

    BITMAPINFO bmi{};
    bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmi.bmiHeader.biWidth = width;
    bmi.bmiHeader.biHeight = -height;
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 32;
    bmi.bmiHeader.biCompression = BI_RGB;

    void* bits = nullptr;
    HDC mem_dc = CreateCompatibleDC(screen_dc);
    HBITMAP dib =
        CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
    HGDIOBJ old = SelectObject(mem_dc, dib);

    BitBlt(mem_dc, 0, 0, width, height, screen_dc, left, top,
           SRCCOPY | CAPTUREBLT);

    if (show_cursor_) {
      CURSORINFO ci{};
      ci.cbSize = sizeof(CURSORINFO);
      if (GetCursorInfo(&ci) && ci.flags == CURSOR_SHOWING && ci.hCursor) {
        POINT pt = ci.ptScreenPos;
        int cx = pt.x - left;
        int cy = pt.y - top;
        if (cx >= -64 && cy >= -64 && cx < width + 64 && cy < height + 64) {
          DrawIconEx(mem_dc, cx, cy, ci.hCursor, 0, 0, 0, nullptr, DI_NORMAL);
        }
      }
    }

    int stride_argb = width * 4;
    int nv12_size = width * height * 3 / 2;
    if (!nv12_frame_ || nv12_width_ != width || nv12_height_ != height) {
      delete[] nv12_frame_;
      nv12_frame_ = new unsigned char[nv12_size];
      nv12_width_ = width;
      nv12_height_ = height;
    }

    libyuv::ARGBToNV12(static_cast<const uint8_t*>(bits), stride_argb,
                       nv12_frame_, width, nv12_frame_ + width * height, width,
                       width, height);

    if (callback_) {
      const std::string stream_id = MakeDisplayStreamId(idx);
      callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str());
    }

    SelectObject(mem_dc, old);
    DeleteObject(dib);
    DeleteDC(mem_dc);

    std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
  }
  ReleaseDC(nullptr, screen_dc);
}

}  // namespace crossdesk
