#include <d3d9.h>
#include <d3d11.h>
#include <dxgi.h>

#include "graphics-hook.h"
#include "d3d9-patches.hpp"

#include <detours.h>

typedef HRESULT(STDMETHODCALLTYPE *present_t)(IDirect3DDevice9 *, CONST RECT *,
					      CONST RECT *, HWND,
					      CONST RGNDATA *);
typedef HRESULT(STDMETHODCALLTYPE *present_ex_t)(IDirect3DDevice9Ex *,
						 CONST RECT *, CONST RECT *,
						 HWND, CONST RGNDATA *, DWORD);
typedef HRESULT(STDMETHODCALLTYPE *present_swap_t)(IDirect3DSwapChain9 *,
						   CONST RECT *, CONST RECT *,
						   HWND, CONST RGNDATA *,
						   DWORD);
typedef HRESULT(STDMETHODCALLTYPE *reset_t)(IDirect3DDevice9 *,
					    D3DPRESENT_PARAMETERS *);
typedef HRESULT(STDMETHODCALLTYPE *reset_ex_t)(IDirect3DDevice9 *,
					       D3DPRESENT_PARAMETERS *,
					       D3DDISPLAYMODEEX *);

typedef HRESULT(WINAPI *createfactory1_t)(REFIID, void **);

present_t RealPresent = NULL;
present_ex_t RealPresentEx = NULL;
present_swap_t RealPresentSwap = NULL;
reset_t RealReset = NULL;
reset_ex_t RealResetEx = NULL;

struct d3d9_data {
	HMODULE d3d9;
	IDirect3DDevice9 *device; /* do not release */
	uint32_t cx;
	uint32_t cy;
	D3DFORMAT d3d9_format;
	DXGI_FORMAT dxgi_format;
	bool using_shtex;

	/* shared texture */
	IDirect3DSurface9 *d3d9_copytex;
	ID3D11Device *d3d11_device;
	ID3D11DeviceContext *d3d11_context;
	ID3D11Resource *d3d11_tex;
	struct shtex_data *shtex_info;
	HANDLE handle;
	int patch;

	/* shared memory */
	IDirect3DSurface9 *copy_surfaces[NUM_BUFFERS];
	IDirect3DQuery9 *queries[NUM_BUFFERS];
	struct shmem_data *shmem_info;
	bool texture_mapped[NUM_BUFFERS];
	volatile bool issued_queries[NUM_BUFFERS];
	uint32_t pitch;
	int cur_tex;
	int copy_wait;
};

static struct d3d9_data data = {};

static void d3d9_free()
{
	capture_free();

	if (data.using_shtex) {
		if (data.d3d11_tex)
			data.d3d11_tex->Release();
		if (data.d3d11_context)
			data.d3d11_context->Release();
		if (data.d3d11_device)
			data.d3d11_device->Release();
		if (data.d3d9_copytex)
			data.d3d9_copytex->Release();
	} else {
		for (size_t i = 0; i < NUM_BUFFERS; i++) {
			if (data.copy_surfaces[i]) {
				if (data.texture_mapped[i])
					data.copy_surfaces[i]->UnlockRect();
				data.copy_surfaces[i]->Release();
			}
			if (data.queries[i])
				data.queries[i]->Release();
		}
	}

	memset(&data, 0, sizeof(data));

	hlog("----------------- d3d9 capture freed -----------------");
}

static DXGI_FORMAT d3d9_to_dxgi_format(D3DFORMAT format)
{
	switch (format) {
	case D3DFMT_A2B10G10R10:
		return DXGI_FORMAT_R10G10B10A2_UNORM;
	case D3DFMT_A8R8G8B8:
		return DXGI_FORMAT_B8G8R8A8_UNORM;
	case D3DFMT_X8R8G8B8:
		return DXGI_FORMAT_B8G8R8X8_UNORM;
	}

	return DXGI_FORMAT_UNKNOWN;
}

const static D3D_FEATURE_LEVEL feature_levels[] = {
	D3D_FEATURE_LEVEL_11_0,
	D3D_FEATURE_LEVEL_10_1,
	D3D_FEATURE_LEVEL_10_0,
};

static inline bool shex_init_d3d11()
{
	PFN_D3D11_CREATE_DEVICE create_device;
	createfactory1_t create_factory;
	D3D_FEATURE_LEVEL level_used;
	IDXGIFactory *factory;
	IDXGIAdapter *adapter;
	HMODULE d3d11;
	HMODULE dxgi;
	HRESULT hr;

	d3d11 = load_system_library("d3d11.dll");
	if (!d3d11) {
		hlog("d3d9_init: Failed to load D3D11");
		return false;
	}

	dxgi = load_system_library("dxgi.dll");
	if (!dxgi) {
		hlog("d3d9_init: Failed to load DXGI");
		return false;
	}

	create_factory =
		(createfactory1_t)GetProcAddress(dxgi, "CreateDXGIFactory1");
	if (!create_factory) {
		hlog("d3d9_init: Failed to get CreateDXGIFactory1 address");
		return false;
	}

	create_device = (PFN_D3D11_CREATE_DEVICE)GetProcAddress(
		d3d11, "D3D11CreateDevice");
	if (!create_device) {
		hlog("d3d9_init: Failed to get D3D11CreateDevice address");
		return false;
	}

	hr = create_factory(__uuidof(IDXGIFactory1), (void **)&factory);
	if (FAILED(hr)) {
		hlog_hr("d3d9_init: Failed to create factory object", hr);
		return false;
	}

	hr = factory->EnumAdapters(0, &adapter);
	factory->Release();

	if (FAILED(hr)) {
		hlog_hr("d3d9_init: Failed to get adapter", hr);
		return false;
	}

	hr = create_device(adapter, D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0,
			   feature_levels,
			   sizeof(feature_levels) / sizeof(D3D_FEATURE_LEVEL),
			   D3D11_SDK_VERSION, &data.d3d11_device, &level_used,
			   &data.d3d11_context);
	adapter->Release();

	if (FAILED(hr)) {
		hlog_hr("d3d9_init: Failed to create D3D11 device", hr);
		return false;
	}

	return true;
}

static inline bool d3d9_shtex_init_shtex()
{
	IDXGIResource *res;
	HRESULT hr;

	D3D11_TEXTURE2D_DESC desc = {};
	desc.Width = data.cx;
	desc.Height = data.cy;
	desc.Format = data.dxgi_format;
	desc.MipLevels = 1;
	desc.ArraySize = 1;
	desc.SampleDesc.Count = 1;
	desc.Usage = D3D11_USAGE_DEFAULT;
	desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
	desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;

	hr = data.d3d11_device->CreateTexture2D(
		&desc, nullptr, (ID3D11Texture2D **)&data.d3d11_tex);
	if (FAILED(hr)) {
		hlog_hr("d3d9_shtex_init_shtex: Failed to create D3D11 texture",
			hr);
		return false;
	}

	hr = data.d3d11_tex->QueryInterface(__uuidof(IDXGIResource),
					    (void **)&res);
	if (FAILED(hr)) {
		hlog_hr("d3d9_shtex_init_shtex: Failed to query IDXGIResource",
			hr);
		return false;
	}

	hr = res->GetSharedHandle(&data.handle);
	res->Release();

	if (FAILED(hr)) {
		hlog_hr("d3d9_shtex_init_shtex: Failed to get shared handle",
			hr);
		return false;
	}

	return true;
}

static inline bool d3d9_shtex_init_copytex()
{
	struct d3d9_offsets offsets = global_hook_info->offsets.d3d9;
	uint8_t *patch_addr = nullptr;
	BOOL *p_is_d3d9 = nullptr;
	uint8_t saved_data[MAX_PATCH_SIZE];
	size_t patch_size = 0;
	BOOL was_d3d9ex = false;
	IDirect3DTexture9 *tex;
	DWORD protect_val;
	HRESULT hr;

	if (offsets.d3d9_clsoff && offsets.is_d3d9ex_clsoff) {
		uint8_t *device_ptr = (uint8_t *)(data.device);
		uint8_t *d3d9_ptr =
			*(uint8_t **)(device_ptr + offsets.d3d9_clsoff);
		p_is_d3d9 = (BOOL *)(d3d9_ptr + offsets.is_d3d9ex_clsoff);
	} else {
		patch_addr = get_d3d9_patch_addr(data.d3d9, data.patch);
	}

	if (p_is_d3d9) {
		was_d3d9ex = *p_is_d3d9;
		*p_is_d3d9 = true;

	} else if (patch_addr) {
		patch_size = patch[data.patch].size;
		VirtualProtect(patch_addr, patch_size, PAGE_EXECUTE_READWRITE,
			       &protect_val);
		memcpy(saved_data, patch_addr, patch_size);
		memcpy(patch_addr, patch[data.patch].data, patch_size);
	}

	hr = data.device->CreateTexture(data.cx, data.cy, 1,
					D3DUSAGE_RENDERTARGET, data.d3d9_format,
					D3DPOOL_DEFAULT, &tex, &data.handle);

	if (p_is_d3d9) {
		*p_is_d3d9 = was_d3d9ex;

	} else if (patch_addr && patch_size) {
		memcpy(patch_addr, saved_data, patch_size);
		VirtualProtect(patch_addr, patch_size, protect_val,
			       &protect_val);
	}

	if (FAILED(hr)) {
		hlog_hr("d3d9_shtex_init_copytex: Failed to create shared texture",
			hr);
		return false;
	}

	hr = tex->GetSurfaceLevel(0, &data.d3d9_copytex);
	tex->Release();

	if (FAILED(hr)) {
		hlog_hr("d3d9_shtex_init_copytex: Failed to get surface level",
			hr);
		return false;
	}

	return true;
}

static bool d3d9_shtex_init(HWND window)
{
	data.using_shtex = true;

	if (!shex_init_d3d11()) {
		return false;
	}
	if (!d3d9_shtex_init_shtex()) {
		return false;
	}
	if (!d3d9_shtex_init_copytex()) {
		return false;
	}
	if (!capture_init_shtex(&data.shtex_info, window, data.cx, data.cy,
				data.dxgi_format, false,
				(uintptr_t)data.handle)) {
		return false;
	}

	hlog("d3d9 shared texture capture successful");
	return true;
}

static bool d3d9_shmem_init_buffers(size_t buffer)
{
	HRESULT hr;

	hr = data.device->CreateOffscreenPlainSurface(
		data.cx, data.cy, data.d3d9_format, D3DPOOL_SYSTEMMEM,
		&data.copy_surfaces[buffer], nullptr);
	if (FAILED(hr)) {
		hlog_hr("d3d9_shmem_init_buffers: Failed to create surface",
			hr);
		return false;
	}

	if (buffer == 0) {
		D3DLOCKED_RECT rect;
		hr = data.copy_surfaces[buffer]->LockRect(&rect, nullptr,
							  D3DLOCK_READONLY);
		if (FAILED(hr)) {
			hlog_hr("d3d9_shmem_init_buffers: Failed to lock "
				"buffer",
				hr);
			return false;
		}

		data.pitch = rect.Pitch;
		data.copy_surfaces[buffer]->UnlockRect();
	}

	hr = data.device->CreateQuery(D3DQUERYTYPE_EVENT,
				      &data.queries[buffer]);
	if (FAILED(hr)) {
		hlog_hr("d3d9_shmem_init_buffers: Failed to create query", hr);
		return false;
	}

	return true;
}

static bool d3d9_shmem_init(HWND window)
{
	data.using_shtex = false;

	for (size_t i = 0; i < NUM_BUFFERS; i++) {
		if (!d3d9_shmem_init_buffers(i)) {
			return false;
		}
	}
	if (!capture_init_shmem(&data.shmem_info, window, data.cx, data.cy,
				data.pitch, data.dxgi_format, false)) {
		return false;
	}

	hlog("d3d9 memory capture successful");
	return true;
}

static bool d3d9_get_swap_desc(D3DPRESENT_PARAMETERS &pp)
{
	IDirect3DSwapChain9 *swap = nullptr;
	HRESULT hr;

	hr = data.device->GetSwapChain(0, &swap);
	if (FAILED(hr)) {
		hlog_hr("d3d9_get_swap_desc: Failed to get swap chain", hr);
		return false;
	}

	hr = swap->GetPresentParameters(&pp);
	swap->Release();

	if (FAILED(hr)) {
		hlog_hr("d3d9_get_swap_desc: Failed to get "
			"presentation parameters",
			hr);
		return false;
	}

	return true;
}

static bool d3d9_init_format_backbuffer(HWND &window)
{
	IDirect3DSurface9 *back_buffer = nullptr;
	D3DPRESENT_PARAMETERS pp;
	D3DSURFACE_DESC desc;
	HRESULT hr;

	if (!d3d9_get_swap_desc(pp)) {
		return false;
	}

	hr = data.device->GetRenderTarget(0, &back_buffer);
	if (FAILED(hr)) {
		return false;
	}

	hr = back_buffer->GetDesc(&desc);
	back_buffer->Release();

	if (FAILED(hr)) {
		hlog_hr("d3d9_init_format_backbuffer: Failed to get "
			"backbuffer descriptor",
			hr);
		return false;
	}

	data.d3d9_format = desc.Format;
	data.dxgi_format = d3d9_to_dxgi_format(desc.Format);
	window = pp.hDeviceWindow;

	data.cx = desc.Width;
	data.cy = desc.Height;

	return true;
}

static bool d3d9_init_format_swapchain(HWND &window)
{
	D3DPRESENT_PARAMETERS pp;

	if (!d3d9_get_swap_desc(pp)) {
		return false;
	}

	data.dxgi_format = d3d9_to_dxgi_format(pp.BackBufferFormat);
	data.d3d9_format = pp.BackBufferFormat;
	window = pp.hDeviceWindow;

	data.cx = pp.BackBufferWidth;
	data.cy = pp.BackBufferHeight;

	return true;
}

static void d3d9_init(IDirect3DDevice9 *device)
{
	IDirect3DDevice9Ex *d3d9ex = nullptr;
	bool has_d3d9ex_bool_offset =
		global_hook_info->offsets.d3d9.d3d9_clsoff &&
		global_hook_info->offsets.d3d9.is_d3d9ex_clsoff;
	bool success;
	HWND window = nullptr;
	HRESULT hr;

	data.d3d9 = get_system_module("d3d9.dll");
	data.device = device;

	hr = device->QueryInterface(__uuidof(IDirect3DDevice9Ex),
				    (void **)&d3d9ex);
	if (SUCCEEDED(hr)) {
		d3d9ex->Release();
		data.patch = -1;
	} else if (!has_d3d9ex_bool_offset) {
		data.patch = get_d3d9_patch(data.d3d9);
	} else {
		data.patch = -1;
	}

	if (!d3d9_init_format_backbuffer(window)) {
		if (!d3d9_init_format_swapchain(window)) {
			return;
		}
	}

	if (global_hook_info->force_shmem ||
	    (!d3d9ex && data.patch == -1 && !has_d3d9ex_bool_offset)) {
		success = d3d9_shmem_init(window);
	} else {
		success = d3d9_shtex_init(window);
	}

	if (!success)
		d3d9_free();
}

static inline HRESULT get_backbuffer(IDirect3DDevice9 *device,
				     IDirect3DSurface9 **surface)
{
	static bool use_backbuffer = false;
	static bool checked_exceptions = false;

	if (!checked_exceptions) {
		if (_strcmpi(get_process_name(), "hotd_ng.exe") == 0)
			use_backbuffer = true;
		checked_exceptions = true;
	}

	if (use_backbuffer) {
		return device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO,
					     surface);
	} else {
		return device->GetRenderTarget(0, surface);
	}
}

static inline void d3d9_shtex_capture(IDirect3DSurface9 *backbuffer)
{
	HRESULT hr;

	hr = data.device->StretchRect(backbuffer, nullptr, data.d3d9_copytex,
				      nullptr, D3DTEXF_NONE);
	if (FAILED(hr))
		hlog_hr("d3d9_shtex_capture: StretchRect failed", hr);
}

static void d3d9_shmem_capture_copy(int i)
{
	IDirect3DSurface9 *target = data.copy_surfaces[i];
	D3DLOCKED_RECT rect;
	HRESULT hr;

	if (!data.issued_queries[i]) {
		return;
	}
	if (data.queries[i]->GetData(0, 0, 0) != S_OK) {
		return;
	}

	data.issued_queries[i] = false;

	hr = target->LockRect(&rect, nullptr, D3DLOCK_READONLY);
	if (SUCCEEDED(hr)) {
		data.texture_mapped[i] = true;
		shmem_copy_data(i, rect.pBits);
	}
}

static inline void d3d9_shmem_capture(IDirect3DSurface9 *backbuffer)
{
	int next_tex;
	HRESULT hr;

	next_tex = (data.cur_tex + 1) % NUM_BUFFERS;
	d3d9_shmem_capture_copy(next_tex);

	if (data.copy_wait < NUM_BUFFERS - 1) {
		data.copy_wait++;
	} else {
		IDirect3DSurface9 *src = backbuffer;
		IDirect3DSurface9 *dst = data.copy_surfaces[data.cur_tex];

		if (shmem_texture_data_lock(data.cur_tex)) {
			dst->UnlockRect();
			data.texture_mapped[data.cur_tex] = false;
			shmem_texture_data_unlock(data.cur_tex);
		}

		hr = data.device->GetRenderTargetData(src, dst);
		if (FAILED(hr)) {
			hlog_hr("d3d9_shmem_capture: GetRenderTargetData "
				"failed",
				hr);
		}

		data.queries[data.cur_tex]->Issue(D3DISSUE_END);
		data.issued_queries[data.cur_tex] = true;
	}

	data.cur_tex = next_tex;
}

static void d3d9_capture(IDirect3DDevice9 *device,
			 IDirect3DSurface9 *backbuffer)
{
	if (capture_should_stop()) {
		d3d9_free();
	}
	if (capture_should_init()) {
		d3d9_init(device);
	}
	if (capture_ready()) {
		if (data.device != device) {
			d3d9_free();
			return;
		}

		if (data.using_shtex)
			d3d9_shtex_capture(backbuffer);
		else
			d3d9_shmem_capture(backbuffer);
	}
}

/* this is used just in case Present calls PresentEx or vise versa. */
static int present_recurse = 0;

static inline void present_begin(IDirect3DDevice9 *device,
				 IDirect3DSurface9 *&backbuffer)
{
	HRESULT hr;

	if (!present_recurse) {
		hr = get_backbuffer(device, &backbuffer);
		if (FAILED(hr)) {
			hlog_hr("d3d9_shmem_capture: Failed to get "
				"backbuffer",
				hr);
		}

		if (!global_hook_info->capture_overlay) {
			d3d9_capture(device, backbuffer);
		}
	}

	present_recurse++;
}

static inline void present_end(IDirect3DDevice9 *device,
			       IDirect3DSurface9 *backbuffer)
{
	present_recurse--;

	if (!present_recurse) {
		if (global_hook_info->capture_overlay) {
			if (!present_recurse)
				d3d9_capture(device, backbuffer);
		}

		if (backbuffer)
			backbuffer->Release();
	}
}

static bool hooked_reset = false;
static void setup_reset_hooks(IDirect3DDevice9 *device);

static HRESULT STDMETHODCALLTYPE hook_present(IDirect3DDevice9 *device,
					      CONST RECT *src_rect,
					      CONST RECT *dst_rect,
					      HWND override_window,
					      CONST RGNDATA *dirty_region)
{
	IDirect3DSurface9 *backbuffer = nullptr;

	if (!hooked_reset)
		setup_reset_hooks(device);

	present_begin(device, backbuffer);

	const HRESULT hr = RealPresent(device, src_rect, dst_rect,
				       override_window, dirty_region);

	present_end(device, backbuffer);

	return hr;
}

static HRESULT STDMETHODCALLTYPE hook_present_ex(
	IDirect3DDevice9Ex *device, CONST RECT *src_rect, CONST RECT *dst_rect,
	HWND override_window, CONST RGNDATA *dirty_region, DWORD flags)
{
	IDirect3DSurface9 *backbuffer = nullptr;

	if (!hooked_reset)
		setup_reset_hooks(device);

	present_begin(device, backbuffer);

	const HRESULT hr = RealPresentEx(device, src_rect, dst_rect,
					 override_window, dirty_region, flags);

	present_end(device, backbuffer);

	return hr;
}

static HRESULT STDMETHODCALLTYPE hook_present_swap(
	IDirect3DSwapChain9 *swap, CONST RECT *src_rect, CONST RECT *dst_rect,
	HWND override_window, CONST RGNDATA *dirty_region, DWORD flags)
{
	IDirect3DSurface9 *backbuffer = nullptr;
	IDirect3DDevice9 *device = nullptr;

	if (!present_recurse) {
		IDirect3DDevice9 *temp;
		if (SUCCEEDED(swap->GetDevice(&temp))) {
			device = temp;
		}
	}

	if (device) {
		if (!hooked_reset)
			setup_reset_hooks(device);

		present_begin(device, backbuffer);
	}

	const HRESULT hr = RealPresentSwap(
		swap, src_rect, dst_rect, override_window, dirty_region, flags);

	if (device) {
		present_end(device, backbuffer);
		device->Release();
	}

	return hr;
}

static HRESULT STDMETHODCALLTYPE hook_reset(IDirect3DDevice9 *device,
					    D3DPRESENT_PARAMETERS *params)
{
	if (capture_active())
		d3d9_free();

	return RealReset(device, params);
}

static HRESULT STDMETHODCALLTYPE hook_reset_ex(IDirect3DDevice9 *device,
					       D3DPRESENT_PARAMETERS *params,
					       D3DDISPLAYMODEEX *dmex)
{
	if (capture_active())
		d3d9_free();

	return RealResetEx(device, params, dmex);
}

static void setup_reset_hooks(IDirect3DDevice9 *device)
{
	IDirect3DDevice9Ex *d3d9ex = nullptr;
	uintptr_t *vtable = *(uintptr_t **)device;
	HRESULT hr;

	DetourTransactionBegin();

	RealReset = (reset_t)vtable[16];
	DetourAttach((PVOID *)&RealReset, hook_reset);

	hr = device->QueryInterface(IID_PPV_ARGS(&d3d9ex));
	if (SUCCEEDED(hr)) {
		RealResetEx = (reset_ex_t)vtable[132];
		DetourAttach((PVOID *)&RealResetEx, hook_reset_ex);

		d3d9ex->Release();
	}

	const LONG error = DetourTransactionCommit();
	const bool success = error == NO_ERROR;
	if (success) {
		hlog("Hooked IDirect3DDevice9::Reset");
		if (RealResetEx)
			hlog("Hooked IDirect3DDevice9Ex::ResetEx");
		hooked_reset = true;
	} else {
		RealReset = nullptr;
		RealResetEx = nullptr;
	}
}

typedef HRESULT(WINAPI *d3d9create_ex_t)(UINT, IDirect3D9Ex **);

static bool manually_get_d3d9_addrs(HMODULE d3d9_module, void **present_addr,
				    void **present_ex_addr,
				    void **present_swap_addr)
{
	d3d9create_ex_t create_ex;
	D3DPRESENT_PARAMETERS pp;
	HRESULT hr;

	IDirect3DDevice9Ex *device;
	IDirect3D9Ex *d3d9ex;

	hlog("D3D9 values invalid, manually obtaining");

	create_ex = (d3d9create_ex_t)GetProcAddress(d3d9_module,
						    "Direct3DCreate9Ex");
	if (!create_ex) {
		hlog("Failed to load Direct3DCreate9Ex");
		return false;
	}
	if (FAILED(create_ex(D3D_SDK_VERSION, &d3d9ex))) {
		hlog("Failed to create D3D9 context");
		return false;
	}

	memset(&pp, 0, sizeof(pp));
	pp.Windowed = 1;
	pp.SwapEffect = D3DSWAPEFFECT_FLIP;
	pp.BackBufferFormat = D3DFMT_A8R8G8B8;
	pp.BackBufferCount = 1;
	pp.hDeviceWindow = (HWND)dummy_window;
	pp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;

	hr = d3d9ex->CreateDeviceEx(
		D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, dummy_window,
		D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_NOWINDOWCHANGES,
		&pp, NULL, &device);
	d3d9ex->Release();

	if (SUCCEEDED(hr)) {
		uintptr_t *vtable = *(uintptr_t **)device;
		IDirect3DSwapChain9 *swap;

		*present_addr = (void *)vtable[17];
		*present_ex_addr = (void *)vtable[121];

		hr = device->GetSwapChain(0, &swap);
		if (SUCCEEDED(hr)) {
			vtable = *(uintptr_t **)swap;
			*present_swap_addr = (void *)vtable[3];

			swap->Release();
		}

		device->Release();
	} else {
		hlog("Failed to create D3D9 device");
		return false;
	}

	return true;
}

bool hook_d3d9(void)
{
	HMODULE d3d9_module = get_system_module("d3d9.dll");
	uint32_t d3d9_size;
	void *present_addr = nullptr;
	void *present_ex_addr = nullptr;
	void *present_swap_addr = nullptr;

	if (!d3d9_module) {
		return false;
	}

	d3d9_size = module_size(d3d9_module);

	if (global_hook_info->offsets.d3d9.present < d3d9_size &&
	    global_hook_info->offsets.d3d9.present_ex < d3d9_size &&
	    global_hook_info->offsets.d3d9.present_swap < d3d9_size) {

		present_addr = get_offset_addr(
			d3d9_module, global_hook_info->offsets.d3d9.present);
		present_ex_addr = get_offset_addr(
			d3d9_module, global_hook_info->offsets.d3d9.present_ex);
		present_swap_addr = get_offset_addr(
			d3d9_module,
			global_hook_info->offsets.d3d9.present_swap);
	} else {
		if (!dummy_window) {
			return false;
		}

		if (!manually_get_d3d9_addrs(d3d9_module, &present_addr,
					     &present_ex_addr,
					     &present_swap_addr)) {
			hlog("Failed to get D3D9 values");
			return true;
		}
	}

	if (!present_addr && !present_ex_addr && !present_swap_addr) {
		hlog("Invalid D3D9 values");
		return true;
	}

	DetourTransactionBegin();

	if (present_swap_addr) {
		RealPresentSwap = (present_swap_t)present_swap_addr;
		DetourAttach((PVOID *)&RealPresentSwap, hook_present_swap);
	}
	if (present_ex_addr) {
		RealPresentEx = (present_ex_t)present_ex_addr;
		DetourAttach((PVOID *)&RealPresentEx, hook_present_ex);
	}
	if (present_addr) {
		RealPresent = (present_t)present_addr;
		DetourAttach((PVOID *)&RealPresent, hook_present);
	}

	const LONG error = DetourTransactionCommit();
	const bool success = error == NO_ERROR;
	if (success) {
		if (RealPresentSwap)
			hlog("Hooked IDirect3DSwapChain9::Present");
		if (RealPresentEx)
			hlog("Hooked IDirect3DDevice9Ex::PresentEx");
		if (RealPresent)
			hlog("Hooked IDirect3DDevice9::Present");
		hlog("Hooked D3D9");
	} else {
		RealPresentSwap = nullptr;
		RealPresentEx = nullptr;
		RealPresent = nullptr;
		hlog("Failed to attach Detours hook: %ld", error);
	}

	return success;
}
