
#include <Windows.h>


#include <Spectre/Display/Display.h>
#include <Spectre/System/Log.h>
#include "Win32Application.h"
#include "Win32Internal.h"
#include "Win32Display.h"

namespace sp {

#define WND_CLASSNAME "SPECTRE_WIN32_WNDCLASS"

#if defined(_WIN64) && !defined(GWL_USERDATA)
// x64 undefines this. So we define it again.
#define GWL_USERDATA GWLP_USERDATA
#endif /* defined(_WIN64) && !defined(GWL_USERDATA) */

static bool firstTime = true;

Win32Display::Win32Display() :
m_handle		(NULL),
m_icon			(0),
m_cursor		(0),
m_inResizeModalLoop	(false),
m_minSize		(200, 200)
{
}

Win32Display::~Win32Display()
{
	if (m_icon) {
		DestroyIcon(m_icon);
	}

	if (m_handle) {
		DestroyWindow(m_handle);
	}
}

bool Win32Display::create(DisplayDescription description)
{
	DWORD flags;
	Vector2i pos;

	if (firstTime) {
		registerClass();
		firstTime = false;
	}

	// Set window to center and set decoration flags.
	pos = centerWindow(description.mode.width, description.mode.height);
	flags = getWin32Flags(description.decoration);

	// Create window.
	m_handle = CreateWindowExA(0, WND_CLASSNAME, "", flags,
		pos.x, pos.y, description.mode.width, description.mode.height,
		NULL, NULL, GetModuleHandle(NULL), (LPVOID) this);

	if (!m_handle) {
		Log::error("Win32 - Could not create window: %s", Win32GetMessage(GetLastError()));
		return false;
	}

	setSize(description.mode.width, description.mode.height);

	// Store the size for use later.
	m_size = getSize();

	return true;
}

void Win32Display::destroy()
{
	if (m_handle) {
		DestroyWindow(m_handle);
		m_handle = NULL;
	}
}

void* Win32Display::getHandle() const
{
	return m_handle;
}

bool Win32Display::isValid()
{
	return m_handle != NULL;
}

void Win32Display::setSize(unsigned int width, unsigned int height)
{
	int w, h;
	RECT rect = {0, 0, (LONG) width, (LONG) height};
	AdjustWindowRect(&rect, GetWindowLong(m_handle, GWL_STYLE), false);

	w = rect.right - rect.left;
	h = rect.bottom - rect.top;

	::SetWindowPos(m_handle, NULL, 0, 0, w, h, SWP_NOMOVE | SWP_NOZORDER);
}

Vector2u Win32Display::getSize() const
{
	RECT rect;
	Vector2u size;

	if (GetClientRect(m_handle, &rect)) {
		size.x = rect.right - rect.left;
		size.y = rect.bottom - rect.top;
	}
	return size;
}

void Win32Display::setPosition(unsigned int x, unsigned int y)
{
	::SetWindowPos(m_handle, NULL, x, y, 0, 0, SWP_NOSIZE);
}

Vector2u Win32Display::getPosition() const
{
	RECT r;
    GetWindowRect(m_handle, &r);
    return Vector2u(r.left, r.top);
}

void Win32Display::setVisible(bool visible)
{
	::ShowWindow(m_handle, visible ? SW_SHOW : SW_HIDE);
}

void Win32Display::setDecoration(unsigned decoration)
{
	::SetWindowLong(m_handle, GWL_STYLE, getWin32Flags(decoration));
}

void Win32Display::minimize()
{
	::ShowWindow(m_handle, SW_MINIMIZE);
}

void Win32Display::maximize()
{
	::ShowWindow(m_handle, SW_MAXIMIZE);
}

void Win32Display::setCaption(const std::string& caption)
{
	::SetWindowText(m_handle, caption.c_str());
}

void Win32Display::showCursor(bool value)
{
	if (value) {
		m_cursor = ::LoadCursor(NULL, IDC_ARROW);
	} else {
		m_cursor = 0;
	}

	::SetCursor(m_cursor);
}

void Win32Display::grabCursor(bool value)
{
	if (value) {
		RECT rect;
        GetClientRect(m_handle, &rect);
        MapWindowPoints(m_handle, NULL, reinterpret_cast<LPPOINT>(&rect), 2);
        ClipCursor(&rect);
	} else {
		ClipCursor(NULL);
	}
}

void Win32Display::setIcon(unsigned int width, unsigned int height, const uint8_t *pixels)
{
	::HDC hdc;
	::ICONINFO ii;
	::BITMAPV5HEADER bi;
	::HBITMAP bmp_color, bmp_mask;
	unsigned char *bmp_data = NULL;

	::ZeroMemory(&bi, sizeof(bi));
	bi.bV5Size        = sizeof(bi);
	bi.bV5Width       = width;
	bi.bV5Height      = height;
	bi.bV5Planes      = 1;
	bi.bV5BitCount    = 32;
	bi.bV5Compression = BI_RGB;
	bi.bV5CSType      = LCS_WINDOWS_COLOR_SPACE;
	bi.bV5Intent      = LCS_GM_IMAGES;

	hdc = ::GetDC(NULL);
	bmp_color = ::CreateDIBSection(hdc, reinterpret_cast<BITMAPINFO*>(&bi),
		DIB_RGB_COLORS, (void **) &bmp_data, NULL, 0);
	::ReleaseDC(NULL, hdc);

	if (!bmp_color) {
		Log::error("Win32Display::setIcon() - Failed to create RGBA bitmap");
		return;
	}

	bmp_mask = CreateBitmap(width, height, 1, 1, NULL);
	if (!bmp_mask) {
		Log::error("Win32Display::setIcon() - Failed to create mask bitmap");
		::DeleteObject(bmp_color);
		return;
	}

	// pixels are always RGBA, WinApi wants BGRA
	for(int i = 0; i < width * height; i++) {
		bmp_data[i * 4 + 0] = pixels[i * 4 + 2]; // R <- B
		bmp_data[i * 4 + 1] = pixels[i * 4 + 1]; // G <- G
		bmp_data[i * 4 + 2] = pixels[i * 4 + 0]; // B <- R
		bmp_data[i * 4 + 3] = pixels[i * 4 + 3]; // A <- A
	}

	::ZeroMemory(&ii, sizeof(ii));
	ii.fIcon    = TRUE;
	ii.xHotspot = 0;
	ii.yHotspot = 0;
	ii.hbmColor = bmp_color;
	ii.hbmMask  = bmp_mask;

	if (m_icon) {
		::DestroyIcon(m_icon);
	}

	m_icon = CreateIconIndirect(&ii);

	if (m_icon) {
		::SendMessage(m_handle, WM_SETICON, ICON_SMALL, (LPARAM) m_icon);
		::SendMessage(m_handle, WM_SETICON, ICON_BIG, (LPARAM) m_icon);
	} else {
		Log::error("Win32Display::setIcon() - Failed to create icon");
	}

	::DeleteObject(bmp_color);
	::DeleteObject(bmp_mask);
}

void Win32Display::registerClass()
{
	WNDCLASSA wndcl;

	ZeroMemory(&wndcl, sizeof(wndcl));

	wndcl.style		= CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
	wndcl.lpfnWndProc	= Win32Display::staticWndProc;
	wndcl.hInstance		= ::GetModuleHandle(NULL);
	wndcl.lpszClassName	= WND_CLASSNAME;

	::RegisterClass(&wndcl);
}

DWORD Win32Display::getWin32Flags(unsigned int flags)
{
	DWORD win32_flags = WS_VISIBLE;

	if (flags == DisplayDecorate::Empty) {
		win32_flags |= WS_POPUP;
	} else {
		if (flags & DisplayDecorate::Menu) {
			win32_flags |= WS_CAPTION | WS_MINIMIZEBOX;
		}

		if (flags & DisplayDecorate::Resize) {
			win32_flags |= WS_THICKFRAME | WS_MAXIMIZEBOX;
		}

		if (flags & DisplayDecorate::Close) {
			win32_flags |= WS_SYSMENU;
		}
	}
	return win32_flags;
}

Vector2i Win32Display::centerWindow(int width, int height)
{
	Vector2i v;
	v.x = (::GetSystemMetrics(SM_CXSCREEN) - width) / 2;
	v.y = (::GetSystemMetrics(SM_CYSCREEN) - height) / 2;
	return v;
}

void Win32Display::enterFullscreen(DisplayMode mode)
{
	LONG rc;
	::DEVMODEW dev;
	dev.dmSize       = sizeof(dev);
	dev.dmPelsWidth  = mode.width;
	dev.dmPelsHeight = mode.height;
	dev.dmBitsPerPel = mode.bpp;
	dev.dmFields     = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;

	rc = ::ChangeDisplaySettingsW(&dev, CDS_FULLSCREEN);
	if (rc != DISP_CHANGE_SUCCESSFUL) {
		const char *msg;

		switch(rc) {
		case DISP_CHANGE_BADDUALVIEW :
			msg = "The system is DualView capable"; break;
		case DISP_CHANGE_BADFLAGS :
			msg = "Invalid flags given"; break;
		case DISP_CHANGE_BADPARAM :
			msg = "Invalid parameter"; break;
		case DISP_CHANGE_BADMODE :
			msg = "Resolution not supported"; break;
		case DISP_CHANGE_FAILED :
			msg = "Display driver failed to set mode"; break;
		case DISP_CHANGE_NOTUPDATED :
			msg = "Unable to write settings to the registry"; break;
		case DISP_CHANGE_RESTART :
			msg = "System restart required"; break;
		default :
			msg = "Unkown error";
		}

		sp::Log::error("Win32: Failed to switch to fullscreen mode: %s.", msg);
		return;
	}

	::SetWindowLong(m_handle, GWL_STYLE, WS_VISIBLE | WS_POPUP);
	::SetWindowPos(m_handle, HWND_TOP, 0, 0, mode.width, mode.height, SWP_FRAMECHANGED | SWP_SHOWWINDOW);

	grabCursor(true);

	m_fs_mode = mode;
}

void Win32Display::exitFullscreen()
{
	if (!m_fs_mode.empty()) {
		// Restore to previous mode.
		::ChangeDisplaySettingsW(NULL, 0);
		m_fs_mode = DisplayMode();
	}
}

void Win32Display::processResizeMessage(const Vector2u& new_size)
{
	// Check if the size has actually changed.
	if (m_size != new_size) {

		// Update the size and notify the higher layer.
		m_size = new_size;
		onReshape(m_size.x, m_size.y);
	}
}

void Win32Display::processMessage(UINT message, WPARAM wParam, LPARAM lParam)
{
	switch(message) {
	case WM_SETCURSOR :
		if (LOWORD(lParam) == HTCLIENT) {
			::SetCursor(m_cursor);
		}
		break;
	case WM_DESTROY :
		PostQuitMessage(0);
		break;
	case WM_SETFOCUS :
		Log::debug("WM_SETFOCUS");
		if (m_fs_mode.empty() == false) {
			enterFullscreen(m_fs_mode);
		}
		break;
	case WM_KILLFOCUS :
		Log::debug("WM_KILLFOCUS");
		// If in fullscreen mode.
		if (m_fs_mode.empty() == false) {
			// Switch to window mode.
			::ChangeDisplaySettingsW(NULL, 0);

			// also minimize the window to get it out of the way.
			minimize();
		}

		grabCursor(false);
		break;
	case WM_SIZE :

		if (!m_inResizeModalLoop && (wParam == SIZE_MAXIMIZED || wParam == SIZE_RESTORED)) {
			Vector2u size(LOWORD(lParam), HIWORD(lParam));
			processResizeMessage(size);
		}
		break;
	case WM_GETMINMAXINFO :
	{
		MINMAXINFO* info = (MINMAXINFO*)lParam;
		info->ptMinTrackSize.x = m_minSize.x;
		info->ptMinTrackSize.y = m_minSize.y;
		info->ptMaxTrackSize.x = 99999;
		info->ptMaxTrackSize.y = 99999;
		break;
	}
	case WM_ENTERSIZEMOVE :
		m_inResizeModalLoop = true;
		break;
	case WM_EXITSIZEMOVE :
		m_inResizeModalLoop = false;
		processResizeMessage(getSize());
		break;
	}
}

LRESULT CALLBACK Win32Display::staticWndProc(HWND handle, UINT message, WPARAM wParam, LPARAM lParam)
{
	Win32Display *display;

	if (message == WM_NCCREATE) {

		LONG_PTR ptr = (LONG_PTR) ((LPCREATESTRUCT)lParam)->lpCreateParams;
		::SetWindowLongPtr(handle, GWL_USERDATA, ptr);

		display = (Win32Display*) ptr;
	} else {
		display = (Win32Display*) ::GetWindowLongPtr(handle, GWL_USERDATA);
	}

	if (display) {
		display->processMessage(message, wParam, lParam);
	}
	return DefWindowProc(handle, message, wParam, lParam);
}

} // namespace sp
