#include "capture_window.h"
#include <godot_cpp/variant/utility_functions.hpp>

#include <chrono>
#include <thread>
#include <iomanip>
#include <sstream>

#include <d3d11.h>
#include <dxgi1_2.h>
#include <wrl/client.h>

#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")


//#define PROFILER_ENABLED

using namespace godot;

namespace microtaur
{

class AcceleratedWindowCapturer
{
public:
  AcceleratedWindowCapturer() {
    init();
  }

  void init()
  {
    D3D_FEATURE_LEVEL featureLevel = D3D_FEATURE_LEVEL_11_0;
    auto hr = D3D11CreateDevice(
      nullptr,
      D3D_DRIVER_TYPE_HARDWARE,
      nullptr,
      0,
      &featureLevel,
      1,
      D3D11_SDK_VERSION,
      &m_device,
      nullptr,
      &m_context
    );
    if (FAILED(hr)) {
      reset();
      return;
    }

    IDXGIDevice* dxgiDevice = nullptr;
    hr = m_device->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast<void**>(&dxgiDevice));
    if (FAILED(hr)) {
      reset();
      return;
    }

    IDXGIAdapter* dxgiAdapter = nullptr;
    hr = dxgiDevice->GetParent(__uuidof(IDXGIAdapter), reinterpret_cast<void**>(&dxgiAdapter));
    if (FAILED(hr)) {
      reset();
      return;
    }
    dxgiDevice->Release();

    IDXGIOutput* dxgiOutput = nullptr;
    hr = dxgiAdapter->EnumOutputs(1, &dxgiOutput); // TODO: screen choose
    if (FAILED(hr)) {
      reset();
      return;
    }
    dxgiAdapter->Release();

    IDXGIOutput1* dxgiOutput1 = nullptr;
    hr = dxgiOutput->QueryInterface(__uuidof(IDXGIOutput1), reinterpret_cast<void**>(&dxgiOutput1));
    if (FAILED(hr)) {
      reset();
      return;
    }
    dxgiOutput->Release();

    // Get desktop duplication
    hr = dxgiOutput1->DuplicateOutput(m_device.Get(), &m_duplication);
    if (FAILED(hr)) {
      reset();
      return;
    }
    dxgiOutput1->Release();

  }

  Frame nextFrame()
  {
    IDXGIResource* desktopResource = nullptr;
    DXGI_OUTDUPL_FRAME_INFO frameInfo;

    if (m_frameAcquired) {
      m_duplication->ReleaseFrame();
      m_frameAcquired = false;
    }

    HRESULT hr = m_duplication->AcquireNextFrame(INFINITE, &frameInfo, &desktopResource);
    if (FAILED(hr)) {
      if (hr == DXGI_ERROR_WAIT_TIMEOUT) {
        // TODO: maybe adjust this value?
        std::this_thread::sleep_for(std::chrono::milliseconds(33));
        return {};
      }

      reset();
      return { m_width, m_height, m_buffer };
    }
    m_frameAcquired = true;

    // Get the DXGI surface
    hr = desktopResource->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void**>(m_desktopTexture.GetAddressOf()));
    if (FAILED(hr)) {
      desktopResource->Release();
      m_duplication->ReleaseFrame();
      return {};
    }

    // Create a staging texture if that's necessary
    D3D11_TEXTURE2D_DESC desc;
    m_desktopTexture->GetDesc(&desc);

    if (!m_stagingTexture || m_width != desc.Width || m_height == desc.Height) {
      m_width = desc.Width;
      m_height = desc.Height;

      desc.Usage = D3D11_USAGE_STAGING;
      desc.BindFlags = 0;
      desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
      desc.MiscFlags = 0;
      m_device->CreateTexture2D(&desc, nullptr, &m_stagingTexture);

      if (!m_stagingTexture) {
        desktopResource->Release();
        m_duplication->ReleaseFrame();
        return {};
      }
    }

    m_context->CopyResource(m_stagingTexture.Get(), m_desktopTexture.Get());
    desktopResource->Release();

    D3D11_MAPPED_SUBRESOURCE mappedResource;
    hr = m_context->Map(m_stagingTexture.Get(), 0, D3D11_MAP_READ, 0, &mappedResource);
    if (FAILED(hr)) {
      m_stagingTexture->Release();
      return { m_width, m_height, m_buffer };
    }

    // Convert BGRA to YUV420
    const auto yuvSize = m_width * m_height * 3 / 2;
    if (m_buffer.size() != yuvSize) {
      m_buffer.resize(yuvSize);
    }

    rgbToYuv(mappedResource, m_width, m_height);

    // Unmap and release the staging texture
    m_context->Unmap(m_stagingTexture.Get(), 0);

    return {m_width, m_height, m_buffer};
  }

  void rgbToYuv(D3D11_MAPPED_SUBRESOURCE mappedResource, size_t width, size_t height)
  {
    auto rgb = static_cast<uint8_t*>(mappedResource.pData);

    size_t upos = width * height;
    size_t vpos = upos + upos / 4;
    size_t i = 0;

    for (size_t line = 0; line < height; ++line) {
      if (!(line % 2)) {
        for (size_t x = 0; x < width; x += 2) {
          uint8_t b = rgb[4 * i];
          uint8_t g = rgb[4 * i + 1];
          uint8_t r = rgb[4 * i + 2];

          m_buffer[i++] = ((66 * r + 129 * g + 25 * b) >> 8) + 16;

          m_buffer[upos++] = ((-38 * r + -74 * g + 112 * b) >> 8) + 128;
          m_buffer[vpos++] = ((112 * r + -94 * g + -18 * b) >> 8) + 128;

          b = rgb[4 * i];
          g = rgb[4 * i + 1];
          r = rgb[4 * i + 2];

          m_buffer[i++] = ((66 * r + 129 * g + 25 * b) >> 8) + 16;
        }
      } else {
        for (size_t x = 0; x < width; x += 1) {
          uint8_t b = rgb[4 * i];
          uint8_t g = rgb[4 * i + 1];
          uint8_t r = rgb[4 * i + 2];

          m_buffer[i++] = ((66 * r + 129 * g + 25 * b) >> 8) + 16;
        }
      }
    }
  }

  void reset()
  {
    m_stagingTexture.Reset();
    m_desktopTexture.Reset();
    m_duplication.Reset();
    m_context.Reset();
    m_device.Reset();

    m_width = m_height = 0;
    m_frameAcquired = false;

    init();
  }

private:
  Microsoft::WRL::ComPtr<ID3D11Device> m_device;
  Microsoft::WRL::ComPtr<ID3D11DeviceContext> m_context;
  Microsoft::WRL::ComPtr<IDXGIOutputDuplication> m_duplication;
  Microsoft::WRL::ComPtr<ID3D11Texture2D> m_desktopTexture;
  Microsoft::WRL::ComPtr<ID3D11Texture2D> m_stagingTexture;

  std::vector<uint8_t> m_buffer;
  bool m_frameAcquired{ false };

  size_t m_width{};
  size_t m_height{};
};


WindowCapturer::WindowCapturer()
  : m_impl(std::make_unique<AcceleratedWindowCapturer>())
{
}

WindowCapturer::~WindowCapturer()
{
}

Frame WindowCapturer::capture(size_t id)
{
  return m_impl->nextFrame();
}

}