/*
 * Copyright 2011-2013 Branimir Karadzic. All rights reserved.
 * License: http://www.opensource.org/licenses/BSD-2-Clause
 */

#include <bx/bx.h>

#if BX_PLATFORM_WINDOWS

#include "entry_p.h"

#include <bx/countof.h>
#include <bx/uint32_t.h>
#include <bx/thread.h>

#include <windowsx.h>

#define DEFAULT_WIDTH 1280
#define DEFAULT_HEIGHT 720

#define WM_USER_SET_WINDOW_SIZE     (WM_USER+0)
#define WM_USER_TOGGLE_WINDOW_FRAME (WM_USER+1)
#define WM_USER_MOUSE_LOCK          (WM_USER+2)

extern int _main_(int _argc, char** _argv);

namespace entry
{
	struct TranslateKeyModifiers
	{
		int m_vk;
		Modifier::Enum m_modifier;
	};

	static const TranslateKeyModifiers s_translateKeyModifiers[8] =
	{
		{ VK_LMENU,    Modifier::LeftAlt    },
		{ VK_RMENU,    Modifier::RightAlt   },
		{ VK_LCONTROL, Modifier::LeftCtrl   },
		{ VK_RCONTROL, Modifier::RightCtrl  },
		{ VK_LSHIFT,   Modifier::LeftShift  },
		{ VK_RSHIFT,   Modifier::RightShift },
		{ VK_LWIN,     Modifier::LeftMeta   },
		{ VK_RWIN,     Modifier::RightMeta  },
	};

	static uint8_t translateKeyModifiers()
	{
		uint8_t modifiers = 0;
		for (uint32_t ii = 0; ii < countof(s_translateKeyModifiers); ++ii)
		{
			const TranslateKeyModifiers& tkm = s_translateKeyModifiers[ii];
			modifiers |= 0 > GetKeyState(tkm.m_vk) ? tkm.m_modifier : Modifier::None;
		}
		return modifiers;
	}

	static uint8_t s_translateKey[256];

	static Key::Enum translateKey(WPARAM _wparam)
	{
		return (Key::Enum)s_translateKey[_wparam&0xff];
	}

	struct MainThreadEntry
	{
		int m_argc;
		char** m_argv;

		static int32_t threadFunc(void* _userData);
	};

	struct Context
	{
		Context()
			: m_frame(true)
			, m_init(false)
			, m_exit(false)
		{
			memset(s_translateKey, 0, sizeof(s_translateKey) );
			s_translateKey[VK_ESCAPE]    = Key::Esc;
			s_translateKey[VK_RETURN]    = Key::Return;
			s_translateKey[VK_TAB]       = Key::Tab;
			s_translateKey[VK_BACK]      = Key::Backspace;
			s_translateKey[VK_SPACE]     = Key::Space;
			s_translateKey[VK_UP]        = Key::Up;
			s_translateKey[VK_DOWN]      = Key::Down;
			s_translateKey[VK_LEFT]      = Key::Left;
			s_translateKey[VK_RIGHT]     = Key::Right;
			s_translateKey[VK_PRIOR]     = Key::PageUp;
			s_translateKey[VK_NEXT]      = Key::PageUp;
			s_translateKey[VK_HOME]      = Key::Home;
			s_translateKey[VK_END]       = Key::End;
			s_translateKey[VK_SNAPSHOT]  = Key::Print;
			s_translateKey[VK_OEM_PLUS]  = Key::Plus;
			s_translateKey[VK_OEM_MINUS] = Key::Minus;
			s_translateKey[VK_F1]        = Key::F1;
			s_translateKey[VK_F2]        = Key::F2;
			s_translateKey[VK_F3]        = Key::F3;
			s_translateKey[VK_F4]        = Key::F4;
			s_translateKey[VK_F5]        = Key::F5;
			s_translateKey[VK_F6]        = Key::F6;
			s_translateKey[VK_F7]        = Key::F7;
			s_translateKey[VK_F8]        = Key::F8;
			s_translateKey[VK_F9]        = Key::F9;
			s_translateKey[VK_F10]       = Key::F10;
			s_translateKey[VK_F11]       = Key::F11;
			s_translateKey[VK_F12]       = Key::F12;
			s_translateKey[VK_NUMPAD0]   = Key::NumPad0;
			s_translateKey[VK_NUMPAD1]   = Key::NumPad1;
			s_translateKey[VK_NUMPAD2]   = Key::NumPad2;
			s_translateKey[VK_NUMPAD3]   = Key::NumPad3;
			s_translateKey[VK_NUMPAD4]   = Key::NumPad4;
			s_translateKey[VK_NUMPAD5]   = Key::NumPad5;
			s_translateKey[VK_NUMPAD6]   = Key::NumPad6;
			s_translateKey[VK_NUMPAD7]   = Key::NumPad7;
			s_translateKey[VK_NUMPAD8]   = Key::NumPad8;
			s_translateKey[VK_NUMPAD9]   = Key::NumPad9;
			s_translateKey['0']          = Key::Key0;
			s_translateKey['1']          = Key::Key1;
			s_translateKey['2']          = Key::Key2;
			s_translateKey['3']          = Key::Key3;
			s_translateKey['4']          = Key::Key4;
			s_translateKey['5']          = Key::Key5;
			s_translateKey['6']          = Key::Key6;
			s_translateKey['7']          = Key::Key7;
			s_translateKey['8']          = Key::Key8;
			s_translateKey['9']          = Key::Key9;
			s_translateKey['A']          = Key::KeyA;
			s_translateKey['B']          = Key::KeyB;
			s_translateKey['C']          = Key::KeyC;
			s_translateKey['D']          = Key::KeyD;
			s_translateKey['E']          = Key::KeyE;
			s_translateKey['F']          = Key::KeyF;
			s_translateKey['G']          = Key::KeyG;
			s_translateKey['H']          = Key::KeyH;
			s_translateKey['I']          = Key::KeyI;
			s_translateKey['J']          = Key::KeyJ;
			s_translateKey['K']          = Key::KeyK;
			s_translateKey['L']          = Key::KeyL;
			s_translateKey['M']          = Key::KeyM;
			s_translateKey['N']          = Key::KeyN;
			s_translateKey['O']          = Key::KeyO;
			s_translateKey['P']          = Key::KeyP;
			s_translateKey['Q']          = Key::KeyQ;
			s_translateKey['R']          = Key::KeyR;
			s_translateKey['S']          = Key::KeyS;
			s_translateKey['T']          = Key::KeyT;
			s_translateKey['U']          = Key::KeyU;
			s_translateKey['V']          = Key::KeyV;
			s_translateKey['W']          = Key::KeyW;
			s_translateKey['X']          = Key::KeyX;
			s_translateKey['Y']          = Key::KeyY;
			s_translateKey['Z']          = Key::KeyZ;
		}

		int32_t main(int _argc, char** _argv)
		{
			HINSTANCE instance = (HINSTANCE)GetModuleHandle(NULL);

			WNDCLASSEX wnd;
			memset(&wnd, 0, sizeof(wnd) );
			wnd.cbSize = sizeof(wnd);
			wnd.lpfnWndProc = DefWindowProc;
			wnd.hInstance = instance;
			wnd.hIcon = LoadIcon(instance, IDI_APPLICATION);
			wnd.hCursor = LoadCursor(instance, IDC_ARROW);
			wnd.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
			wnd.lpszClassName = "bgfx_letterbox";
			wnd.hIconSm = LoadIcon(instance, IDI_APPLICATION);
			RegisterClassExA(&wnd);

			memset(&wnd, 0, sizeof(wnd) );
			wnd.cbSize = sizeof(wnd);
			wnd.style = CS_HREDRAW | CS_VREDRAW;
			wnd.lpfnWndProc = wndProc;
			wnd.hInstance = instance;
			wnd.hIcon = LoadIcon(instance, IDI_APPLICATION);
			wnd.hCursor = LoadCursor(instance, IDC_ARROW);
			wnd.lpszClassName = "bgfx";
			wnd.hIconSm = LoadIcon(instance, IDI_APPLICATION);
			RegisterClassExA(&wnd);

			HWND hwnd = CreateWindowA("bgfx_letterbox"
				, "BGFX"
				, WS_POPUP|WS_SYSMENU
				, -32000
				, -32000
				, 0
				, 0
				, NULL
				, NULL
				, instance
				, 0
				);

			m_hwnd = CreateWindowA("bgfx"
				, "BGFX"
				, WS_OVERLAPPEDWINDOW|WS_VISIBLE
				, 0
				, 0
				, DEFAULT_WIDTH
				, DEFAULT_HEIGHT
				, hwnd
				, NULL
				, instance
				, 0
				);

			bgfx::winSetHwnd(m_hwnd);

			adjust(DEFAULT_WIDTH, DEFAULT_HEIGHT, true);
			m_oldWidth = DEFAULT_WIDTH;
			m_oldHeight = DEFAULT_HEIGHT;

			MainThreadEntry mte;
			mte.m_argc = _argc;
			mte.m_argv = _argv;

			bx::Thread thread;
			thread.init(mte.threadFunc, &mte);
			m_init = true;

			MSG msg;
			msg.message = WM_NULL;

			while (!m_exit)
			{
				WaitMessage();

				while (0 != PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE) )
				{
					TranslateMessage(&msg);
					DispatchMessage(&msg);
				}
			}

			thread.shutdown();

			DestroyWindow(m_hwnd);
			DestroyWindow(hwnd);

			return 0;
		}

		LRESULT process(HWND _hwnd, UINT _id, WPARAM _wparam, LPARAM _lparam)
		{
			if (m_init)
			{
				switch (_id)
				{
				case WM_USER_SET_WINDOW_SIZE:
					{
						uint32_t width = GET_X_LPARAM(_lparam);
						uint32_t height = GET_Y_LPARAM(_lparam);
						adjust(width, height, true);
					}
					break;

				case WM_USER_TOGGLE_WINDOW_FRAME:
					{
						if (m_frame)
						{
							m_oldWidth = m_width;
							m_oldHeight = m_height;
						}
						adjust(m_oldWidth, m_oldHeight, !m_frame);
					}
					break;

				case WM_USER_MOUSE_LOCK:
					setMouseLock(!!_lparam);
					break;

				case WM_DESTROY:
					break;

				case WM_QUIT:
				case WM_CLOSE:
					m_exit = true;
					m_eventQueue.postExitEvent();
					break;

				case WM_SIZING:
					{
						RECT clientRect;
						GetClientRect(_hwnd, &clientRect);
						uint32_t width = clientRect.right-clientRect.left;
						uint32_t height = clientRect.bottom-clientRect.top;

						RECT& rect = *(RECT*)_lparam;
						uint32_t frameWidth = rect.right-rect.left - width;
						uint32_t frameHeight = rect.bottom-rect.top - height;

						switch (_wparam)
						{
						case WMSZ_LEFT:
						case WMSZ_RIGHT:
							{
								float aspectRatio = 1.0f/m_aspectRatio;
								width = bx::uint32_max(DEFAULT_WIDTH/4, width);
								height = uint32_t(float(width)*aspectRatio);
							}
							break;

						default:
							{
								float aspectRatio = m_aspectRatio;
								height = bx::uint32_max(DEFAULT_HEIGHT/4, height);
								width = uint32_t(float(height)*aspectRatio);
							}
							break;
						}

						rect.right = rect.left + width + frameWidth;
						rect.bottom = rect.top + height + frameHeight;

						SetWindowPos(_hwnd
							, HWND_TOP
							, rect.left
							, rect.top
							, (rect.right-rect.left)
							, (rect.bottom-rect.top)
							, SWP_SHOWWINDOW
							);
					}
					return 0;

				case WM_SIZE:
					{
						m_width = GET_X_LPARAM(_lparam);
						m_height = GET_Y_LPARAM(_lparam);
						m_eventQueue.postSizeEvent(m_width, m_height);
					}
					break;

				case WM_SYSCOMMAND:
					switch (_wparam)
					{
					case SC_MINIMIZE:
					case SC_RESTORE:
						{
							HWND parent = GetWindow(_hwnd, GW_OWNER);
							if (NULL != parent)
							{
								PostMessage(parent, _id, _wparam, _lparam);
							}
						}
					}
					break;

				case WM_MOUSEMOVE:
					{
						int32_t mx = GET_X_LPARAM(_lparam);
						int32_t my = GET_Y_LPARAM(_lparam);

						if (m_mouseLock)
						{
							mx -= m_mx;
							my -= m_my;

							if (0 == mx
							&&  0 == my)
							{
								break;
							}

							setMousePos(m_mx, m_my);
						}

						m_eventQueue.postMouseEvent(mx, my);
					}
					break;

				case WM_LBUTTONDOWN:
				case WM_LBUTTONUP:
				case WM_LBUTTONDBLCLK:
					{
						int32_t mx = GET_X_LPARAM(_lparam);
						int32_t my = GET_Y_LPARAM(_lparam);
						m_eventQueue.postMouseEvent(mx, my, MouseButton::Left, _id == WM_LBUTTONDOWN);
					}
					break;

				case WM_MBUTTONDOWN:
				case WM_MBUTTONUP:
				case WM_MBUTTONDBLCLK:
					{
						int32_t mx = GET_X_LPARAM(_lparam);
						int32_t my = GET_Y_LPARAM(_lparam);
						m_eventQueue.postMouseEvent(mx, my, MouseButton::Middle, _id == WM_MBUTTONDOWN);
					}
					break;

				case WM_RBUTTONUP:
				case WM_RBUTTONDOWN:
				case WM_RBUTTONDBLCLK:
					{
						int32_t mx = GET_X_LPARAM(_lparam);
						int32_t my = GET_Y_LPARAM(_lparam);
						m_eventQueue.postMouseEvent(mx, my, MouseButton::Right, _id == WM_RBUTTONDOWN);
					}
					break;

				case WM_KEYDOWN:
				case WM_SYSKEYDOWN:
				case WM_KEYUP:
				case WM_SYSKEYUP:
					{
						uint8_t modifiers = translateKeyModifiers();
						Key::Enum key = translateKey(_wparam);
						m_eventQueue.postKeyEvent(key, modifiers, _id == WM_KEYDOWN || _id == WM_SYSKEYDOWN);
					}
					break;

				default:
					break;
				}
			}

			return DefWindowProc(_hwnd, _id, _wparam, _lparam);
		}

		void adjust(uint32_t _width, uint32_t _height, bool _windowFrame)
		{
			m_width = _width;
			m_height = _height;
			m_aspectRatio = float(_width)/float(_height);

			ShowWindow(m_hwnd, SW_SHOWNORMAL);
			RECT rect;
			RECT newrect = {0, 0, (LONG)_width, (LONG)_height};
			DWORD style = WS_POPUP|WS_SYSMENU;

			if (m_frame)
			{
				GetWindowRect(m_hwnd, &m_rect);
				m_style = GetWindowLong(m_hwnd, GWL_STYLE);
			}

			if (_windowFrame)
			{
				rect = m_rect;
				style = m_style;
			}
			else
			{
#if defined(__MINGW32__)
				rect = m_rect;
				style = m_style;
#else
				HMONITOR monitor = MonitorFromWindow(m_hwnd, MONITOR_DEFAULTTONEAREST);
				MONITORINFO mi;
				mi.cbSize = sizeof(mi);
				GetMonitorInfo(monitor, &mi);
				newrect = mi.rcMonitor;
				rect = mi.rcMonitor;
#endif // !defined(__MINGW__)
			}

			SetWindowLong(m_hwnd, GWL_STYLE, style);
			AdjustWindowRect(&newrect, style, FALSE);
			UpdateWindow(m_hwnd);

			if (rect.left == -32000
			||  rect.top == -32000)
			{
				rect.left = 0;
				rect.top = 0;
			}

			int32_t left = rect.left;
			int32_t top = rect.top;
			int32_t width = (newrect.right-newrect.left);
			int32_t height = (newrect.bottom-newrect.top);

			if (!_windowFrame)
			{
				float aspectRatio = 1.0f/m_aspectRatio;
				width = bx::uint32_max(DEFAULT_WIDTH/4, width);
				height = uint32_t(float(width)*aspectRatio);

				left = newrect.left+(newrect.right-newrect.left-width)/2;
				top = newrect.top+(newrect.bottom-newrect.top-height)/2;
			}

			HWND parent = GetWindow(m_hwnd, GW_OWNER);
			if (NULL != parent)
			{
				if (_windowFrame)
				{
					SetWindowPos(parent
						, HWND_TOP
						, -32000
						, -32000
						, 0
						, 0
						, SWP_SHOWWINDOW
						);
				}
				else
				{
					SetWindowPos(parent
						, HWND_TOP
						, newrect.left
						, newrect.top
						, newrect.right-newrect.left
						, newrect.bottom-newrect.top
						, SWP_SHOWWINDOW
						);
				}
			}

			SetWindowPos(m_hwnd
				, HWND_TOP
				, left
				, top
				, width
				, height
				, SWP_SHOWWINDOW
				);

			ShowWindow(m_hwnd, SW_RESTORE);

			m_frame = _windowFrame;
		}

		void setMousePos(int32_t _mx, int32_t _my)
		{
			POINT pt = { _mx, _my };
			ClientToScreen(m_hwnd, &pt);
			SetCursorPos(pt.x, pt.y);
		}

		void setMouseLock(bool _lock)
		{
			if (_lock != m_mouseLock)
			{
				if (_lock)
				{
					m_mx = m_width/2;
					m_my = m_height/2;
					ShowCursor(false);
					setMousePos(m_mx, m_my);
				}
				else
				{
					setMousePos(m_mx, m_my);
					ShowCursor(true);
				}

				m_mouseLock = _lock;
			}
		}

		static LRESULT CALLBACK wndProc(HWND _hwnd, UINT _id, WPARAM _wparam, LPARAM _lparam);

		EventQueue m_eventQueue;

		HWND m_hwnd;
		RECT m_rect;
		DWORD m_style;
		uint32_t m_width;
		uint32_t m_height;
		uint32_t m_oldWidth;
		uint32_t m_oldHeight;
		float m_aspectRatio;

		int32_t m_mx;
		int32_t m_my;

		bool m_frame;
		bool m_mouseLock;
		bool m_init;
		bool m_exit;

	};

	static Context s_ctx;

	LRESULT CALLBACK Context::wndProc(HWND _hwnd, UINT _id, WPARAM _wparam, LPARAM _lparam)
	{
		return s_ctx.process(_hwnd, _id, _wparam, _lparam);
	}

	const Event* poll()
	{
		return s_ctx.m_eventQueue.poll();
	}

	void release(const Event* _event)
	{
		s_ctx.m_eventQueue.release(_event);
	}

	void setWindowSize(uint32_t _width, uint32_t _height)
	{
		PostMessage(s_ctx.m_hwnd, WM_USER_SET_WINDOW_SIZE, 0, (_height<<16) | (_width&0xffff) );
	}

	void toggleWindowFrame()
	{
		PostMessage(s_ctx.m_hwnd, WM_USER_TOGGLE_WINDOW_FRAME, 0, 0);
	}

	void setMouseLock(bool _lock)
	{
		PostMessage(s_ctx.m_hwnd, WM_USER_MOUSE_LOCK, 0, _lock);
	}

	int32_t MainThreadEntry::threadFunc(void* _userData)
	{
		MainThreadEntry* self = (MainThreadEntry*)_userData;
		int32_t result = _main_(self->m_argc, self->m_argv);
		PostMessage(s_ctx.m_hwnd, WM_QUIT, 0, 0);
		return result;
	}

} // namespace entry

int main(int _argc, char** _argv)
{
	using namespace entry;
	return s_ctx.main(_argc, _argv);
}

#endif // BX_PLATFORM_WINDOWS
