
--oo/windows/window: overlapping (aka top-level) windows
--Written by Cosmin Apreutesei. Public Domain.

setfenv(1, require'winapi')
require'winapi.basewindowclass'
require'winapi.menuclass'
require'winapi.color'
require'winapi.cursor'
require'winapi.waitemlistclass'

Window = subclass({
	__class_style_bitmask = bitmask{ --only static, frame styles here
		closeable = negate(CS_NOCLOSE), --enable close button and ALT+F4
		dropshadow = CS_DROPSHADOW, --only for non-movable windows
		own_dc = CS_OWNDC, --for opengl or other purposes
		receive_double_clicks = CS_DBLCLKS, --receive double click messages
	},
	__style_bitmask = bitmask{ --only static, frame styles here
		border = WS_BORDER, 		--a frameless window is one without WS_BORDER, WS_DLGFRAME, WS_SIZEBOX and WS_EX_WINDOWEDGE
		frame = WS_DLGFRAME,    --for the titlebar to appear you need both WS_BORDER and WS_DLGFRAME
		minimizable = WS_MINIMIZEBOX,
		maximizable = WS_MAXIMIZEBOX,
		resizeable = WS_SIZEBOX,  --needs WS_DLGFRAME
		sysmenu = WS_SYSMENU,   --not setting this hides all buttons
		vscroll = WS_VSCROLL,
		hscroll = WS_HSCROLL,
		clip_children = WS_CLIPCHILDREN,
		clip_siblings = WS_CLIPSIBLINGS,
		child = WS_CHILD, --needed for windows with WS_EX_TOOLWINDOW + WS_EX_NOACTIVATE!
		popup = WS_POPUP,
	},
	__style_ex_bitmask = bitmask{
		topmost = WS_EX_TOPMOST,
		window_edge = WS_EX_WINDOWEDGE,  --needs to be the same as WS_DLGFRAME
		dialog_frame = WS_EX_DLGMODALFRAME, --double border and no system menu icon!
		help_button = WS_EX_CONTEXTHELP, --only shown if both minimize and maximize buttons are hidden
		tool_window = WS_EX_TOOLWINDOW,
		transparent = WS_EX_TRANSPARENT, --makes clicks go through where alpha == 255
		layered = WS_EX_LAYERED, --setting this makes a completely frameless window regardless of other styles
		control_parent = WS_EX_CONTROLPARENT, --recurse when looking for the next control with WS_TABSTOP
		activable = negate(WS_EX_NOACTIVATE), --don't activate and don't show on taskbar (but see notes in window.lua!)
		taskbar_button = WS_EX_APPWINDOW, --force showing a button on taskbar for this window
	},
	__defaults = {
		--class style bits
		closeable = true,
		dropshadow = false,
		own_dc = false,
		receive_double_clicks = true,
		--window style bits
		border = true,
		frame = true,
		minimizable = true,
		maximizable = true,
		resizeable = true, --...and has a 3px resizing border
		sysmenu = true,
		vscroll = false,
		hscroll = false,
		clip_children = true,
		clip_siblings = true,
		child = false,
		--window ex style bits
		topmost = false,
		window_edge = true,
		dialog_frame = false,
		help_button = false,
		tool_window = false,
		transparent = false,
		layered = false,
		control_parent = true,
		activable = true,
		taskbar_button = false,
		--class properties
		background = COLOR_WINDOW,
		cursor = LoadCursor(IDC_ARROW),
		--window properties
		title = '',
		x = CW_USEDEFAULT,
		y = CW_USEDEFAULT,
		w = CW_USEDEFAULT,
		h = CW_USEDEFAULT,
		autoquit = false,
		menu = nil,
		--behavior
		remember_maximized_pos = false, --see below for explanation of this flag
	},
	__init_properties = {
		'menu',
		'autoquit', --quit the app when the window closes
	},
	__wm_handler_names = index{
		on_close = WM_CLOSE,
		on_unminimizing = WM_QUERYOPEN, --return false to prevent.
		on_get_minmax_info = WM_GETMINMAXINFO,
		--system changes
		on_query_end_session = WM_QUERYENDSESSION,
		on_end_session = WM_ENDSESSION,
		on_system_color_change = WM_SYSCOLORCHANGE,
		on_settings_change = WM_SETTINGCHANGE,
		on_device_mode_change = WM_DEVMODECHANGE,
		on_fonts_change = WM_FONTCHANGE,
		on_time_change = WM_TIMECHANGE,
		on_spooler_change = WM_SPOOLERSTATUS,
		on_input_language_change = WM_INPUTLANGCHANGE,
		on_user_change = WM_USERCHANGED,
		on_display_change = WM_DISPLAYCHANGE,
		on_mouse_activate = WM_MOUSEACTIVATE, --clicked on window's client area
	},
	__wm_syscommand_handler_names = index{
		on_minimizing = SC_MINIMIZE, --before minimize; return false to prevent.
		on_maximizing = SC_MAXIMIZE, --before maximize; return false to prevent.
		on_menu_key   = SC_KEYMENU,  --get the 'f' in Alt+F if there's a `&File` menu.
		on_restoring  = SC_RESTORE,
	},
}, BaseWindow)

--instantiating --------------------------------------------------------------

local n = 0
local function gen_classname()
	n = n + 1
	return 'Window'..n
end

function Window:__info_style(info)
	local style = Window.__index.__info_style(self, info)
	--NOTE: WS_MINIMIZE and WS_MAXIMIZE flags don't work together: combining
	--them makes ShowWindow(SW_RESTORE) have no effect. Instead, when both are
	--needed, we set WS_MINIMIZE only and then set self.restore_to_maximized.
	return bit.bor(style,
		info.minimized and WS_MINIMIZE or 0,
		info.maximized and not info.minimized and WS_MAXIMIZE or 0)
end

function Window:__before_create(info, args)
	Window.__index.__before_create(self, info, args)

	local class_args = {}
	class_args.name = gen_classname()
	local class_style = bit.bor(
		--send WM_PAINT when shrinking the window too, otherwise WM_PAINT is
		--sent only when enlarging the window!
		CS_HREDRAW, CS_VREDRAW
	)
	class_args.style = self.__class_style_bitmask:set(class_style, info)
	class_args.proc = MessageRouter.proc
	class_args.icon = info.icon
	class_args.small_icon = info.small_icon
	class_args.cursor = info.cursor
	class_args.background = info.background
	args.class = RegisterClass(class_args)

	args.parent = info.owner and info.owner.hwnd
	args.text = info.title

	--properties affecting the maximized size and position
	self.remember_maximized_pos = info.remember_maximized_pos
	self.__state.maximized_pos = info.maximized_pos
	self.__state.maximized_size = info.maximized_size

	self.__winclass = args.class --for unregistering
	self.__winclass_style = class_args.style --for checking
end

function Window:__after_create(info, args)

	self:__check_class_style(self.__winclass_style)
	self.__winclass_style = nil --we're done with this

	--when WS_MINIMIZED is present we don't want to set WS_MAXIMIZED.
	if info.maximized and info.minimized then
		self.restore_to_maximized = true
	end

	self.accelerators = WAItemList(self)
end

--destroying -----------------------------------------------------------------

function Window:close()
	CloseWindow(self.hwnd)
end

function Window:WM_NCDESTROY()
	Window.__index.WM_NCDESTROY(self)

	--free the menu bar, if any.
	if self.menu then
		self.menu:free()
	end

	--post a message to unregister the window's class after the window is destroyed.
	PostMessage(nil, WM_UNREGISTER_CLASS, self.__winclass)

	--post WM_QUIT to stop the message loop, if autoquit is set.
	if self.autoquit then
		PostQuitMessage()
	end
end

--properties -----------------------------------------------------------------

Window.get_title = BaseWindow.get_text
Window.set_title = BaseWindow.set_text

function Window:get_owner()
	return Windows:find(GetWindowOwner(self.hwnd))
end

function Window:set_owner(owner)
	SetWindowOwner(self.hwnd, owner and owner.hwnd)
end

--activation -----------------------------------------------------------------

function Window:get_active() return GetActiveWindow() == self.hwnd end
function Window:get_foreground() return GetForegroundWindow() == self.hwnd end
function Window:activate() SetActiveWindow(self.hwnd) end

--this is different than activate() in that the window flashes in the taskbar
--if its thread is not currently the active thread.
function Window:setforeground()
	SetForegroundWindow(self.hwnd)
end

function Window:WM_ACTIVATE(flag, minimized, other_hwnd)
	if flag == 'active' or flag == 'clickactive' then
		if self.on_activate then
			self:on_activate(Windows:find(other_hwnd))
		end
	elseif flag == 'inactive' then
		local other = Windows:find(other_hwnd)
		if self.on_deactivate then
			self:on_deactivate(other)
		end
	end
end

function Window:WM_ACTIVATEAPP(flag, other_thread_id)
	if flag == 'active' then
		if self.on_activate_app then
			self:on_activate_app(other_thread_id)
		end
	elseif flag == 'inactive' then
		if self.on_deactivate_app then
			self:on_deactivate_app(other_thread_id)
		end
	end
end

--NOTE: windows with WS_EX_TOOLBOX don't receive this event.
--NOTE: windows with WS_EX_NOACTIVATE do receive this event.
function Window:WM_NCACTIVATE(flag, update_hrgn)
	if flag == 'active' then
		if self.on_nc_activate then
			self:on_nc_activate(update_hrgn)
		end
	elseif flag == 'inactive' then
		if self.on_nc_deactivate then
			self:on_nc_deactivate(update_hrgn)
		end
	end
end

--NOTE: only sent when the user clicks on the window's client area.
function Window:WM_MOUSEACTIVATE()
	if not self.activable then
		return MA_NOACTIVATE
	end
end

--constraints & maximized size and position ----------------------------------

function Window:WM_GETMINMAXINFO(info)

	--compute and apply any size constraints.
	local min_w, min_h, max_w, max_h = self:__constraints()

	if min_w then info.ptMinTrackSize.w = min_w end
	if min_h then info.ptMinTrackSize.h = min_h end
	if max_w then info.ptMaxTrackSize.w = max_w end
	if max_h then info.ptMaxTrackSize.h = max_h end

	--maximize to last position.
	if self.__maximized_pos then
		info.ptMaxPosition = self.__maximized_pos
	end

	--maximize to user position.
	if self.maximized_pos then
		info.ptMaxPosition = self.maximized_pos
	end

	--maximize to user size.
	if self.maximized_size then
		info.ptMaxSize = self.maximized_size
	end
end

function Window:WM_WINDOWPOSCHANGED(wp)
	--NOTE: A maximized window becomes movable if its size is smaller than
	--the entire screen (WinXP only, in Win7+ it is unmaximized when moved).
	--A window can have such smaller maximized size if constrained.
	--But when such a window is maximized, in absence of a programmer-supplied
	--maximized_pos, it always moves to the top-left corner of the screen,
	--which is lame. A much better option is to remember the last maximized
	--position and restore to that position instead, when maximized again.
	--Which is what we do here.
	if self.remember_maximized_pos and not getbit(wp.flags, SWP_NOMOVE) then
		if self.maximized and not self.minimized then
			self.__maximized_pos = POINT(self.__maximized_pos)
			self.__maximized_pos.x = wp.x
			self.__maximized_pos.y = wp.y
		end
	end
end

--window state ---------------------------------------------------------------

--NOTE: minimized state is preserved between hide() and show() calls.
function Window:get_minimized()
	return IsIconic(self.hwnd)
end

--NOTE: when a maximized window is minimized, the maximized flag becomes false,
--and the restore_to_maximized flag becomes true.
function Window:get_maximized()
	return IsZoomed(self.hwnd)
end

--override show() for non activable windows.
function Window:show(SW, async)
	if not self.activable then
		if SW == SW_SHOWNORMAL then
			SW = SW_SHOWNOACTIVATE
		elseif SW == nil or SW == true or SW == SW_SHOW then
			SW = SW_SHOWNA
		end
	end
	BaseWindow.show(self, SW, async)
end

--minimize (or show minimized if hidden) and deactivate or not.
function Window:minimize(deactivate, async)
	self:show(deactivate == false and SW_SHOWMINIMIZED or SW_MINIMIZE, async)
end

--maximize (or show maximized if hidden) and activate.
--NOTE: can't maximize without activating; WM_COMMAND/SC_MAXIMIZE also activates.
function Window:maximize(_, async)
	self:show(SW_SHOWMAXIMIZED, async)
end

--restore to normal state (or show in normal state) and activate or not.
function Window:shownormal(activate, async)
	self:show(activate == false and SW_SHOWNOACTIVATE or SW_SHOWNORMAL, async)
end

--restore to last state and activate:
-- 1) if minimized, restore to normal or maximized state.
-- 2) if maximized, restore to normal state.
--NOTE: retore-to-maximized doesn't work with async=true.
function Window:restore(_, async)
	self:show(SW_RESTORE, async)
end

--normal rectangle -----------------------------------------------------------

--normal_rect is the frame rectangle in normal state.
--it can be get/set any time without affecting the current state of the window.

function Window:get_normal_rect()
	return GetWindowPlacement(self.hwnd).rcNormalPosition
end

--clamp with optional min and max, where min takes precedence over max.
local function clamp(x, min, max)
	if max and min and max < min then max = min end
	if min then x = math.max(x, min) end
	if max then x = math.min(x, max) end
	return x
end

function Window:set_normal_rect(...) --x1,y1,x2,y2 or rect
	local wp = GetWindowPlacement(self.hwnd)
	local r = RECT(...)

	--must apply constraints manually if maximized.
	if self.maximized then
		local minw, minh, maxw, maxh = self:__constraints()
		if minw or minh or maxw or maxh then
			r.x2 = r.x1 + clamp(r.w, minw, maxw)
			r.y2 = r.y1 + clamp(r.h, minh, maxh)
		end
	end

	wp.rcNormalPosition = r
	if not self.visible then wp.showCmd = SW_HIDE end --don't show it if hidden!
	SetWindowPlacement(self.hwnd, wp)
end

--restore state --------------------------------------------------------------

--control the behavior of the next call to restore().
--NOTE: only works when the window is minimized!

function Window:get_restore_to_maximized()
	local wp = GetWindowPlacement(self.hwnd)
	if wp.showCmd == SW_SHOWMINIMIZED then
		return getbit(wp.flags, WPF_RESTORETOMAXIMIZED)
	end
end

function Window:set_restore_to_maximized(yes)
	local wp = GetWindowPlacement(self.hwnd)

	if wp.showCmd ~= SW_SHOWMINIMIZED then return end

	wp.flags = yes and
		bit.bor(wp.flags, WPF_RESTORETOMAXIMIZED) or
		bit.band(wp.flags, bit.bnot(WPF_RESTORETOMAXIMIZED))

	--NOTE: wp.showCmd is SW_SHOWMINIMIZED even when the window is hidden,
	--so calling SetWindowPlacement() will show the window which we don't want.
	if not self.visible then
		wp.showCmd = SW_HIDE
	end

	SetWindowPlacement(self.hwnd, wp)
end

--z order --------------------------------------------------------------------

function Window:set_topmost(topmost)
	SetWindowPos(self.hwnd, topmost and HWND_TOPMOST or HWND_NOTOPMOST,
		0, 0, 0, 0, SWP_ZORDER_CHANGED_ONLY)
end

function Window:send_to_back(relto)
	local topmost = self.topmost
	local relto_hwnd = relto and relto.hwnd or (self.topmost and HWND_NOTOPMOST or HWND_BOTTOM)
	SetWindowPos(self.hwnd, relto_hwnd, 0, 0, 0, 0, SWP_ZORDER_CHANGED_ONLY)
	if topmost then
		--self.topmost = true
	end
end

function Window:bring_to_front(relto)
	local relto_hwnd = relto and GetPrevSibling(relto.hwnd) or (self.topmost and HWND_TOPMOST or HWND_TOP)
	SetWindowPos(self.hwnd, relto_hwnd, 0, 0, 0, 0, SWP_ZORDER_CHANGED_ONLY)
end

--menus ----------------------------------------------------------------------

function Window:get_menu()
	return Menus:find(GetMenu(self.hwnd))
end

function Window:set_menu(menu)
	if self.menu then self.menu:__set_window(nil) end
	SetMenu(self.hwnd, menu and menu.hmenu)
	if menu then menu:__set_window(self) end
end

function Window:WM_MENUCOMMAND(menu, i)
	menu = Menus:find(menu)
	if menu.WM_MENUCOMMAND then menu:WM_MENUCOMMAND(i) end
end

--rendering ------------------------------------------------------------------

function Window:WM_CTLCOLORSTATIC(wParam, lParam)
	 --TODO: fix group box
	 do return end
	 local hBackground = CreateSolidBrush(RGB(0, 0, 0))
	 local hdc = ffi.cast('HDC', wParam)
    SetBkMode(hdc, OPAQUE)
    SetTextColor(hdc, RGB(100, 100, 0))
	 return tonumber(hBackground)
end

--accelerators ---------------------------------------------------------------

function Window:WM_COMMAND(kind, id, ...)
	if kind == 'accelerator' then
		self.accelerators:WM_COMMAND(id) --route message to individual accelerators
	end
	Window.__index.WM_COMMAND(self, kind, id, ...)
end

--showcase -------------------------------------------------------------------

if not ... then
require'winapi.icon'
require'winapi.font'

local c = Window{title = 'Main',
	border = true, frame = true, window_edge = true, resizeable = true, control_parent = true,
	help_button = true, maximizable = false, minimizable = false, maximized = true,
	autoquit = true, w = 500, h = 300, visible = false}
c:show()

c.cursor = LoadCursor(IDC_HAND)
c.icon = LoadIconFromInstance(IDI_INFORMATION)

print('shown     ', c.visible, c.minimized, c.maximized)
c:maximize()
print('maximized ', c.visible, c.minimized, c.maximized)
c:minimize()
print('minimized ', c.visible, c.minimized, c.maximized)
c:show()
print('shown     ', c.visible, c.minimized, c.maximized)
c:restore()
print('restored  ', c.visible, c.minimized, c.maximized)
c:shownormal()
print('shownormal', c.visible, c.minimized, c.maximized)

local c3 = Window{topmost = true, title='Topmost', h = 300, w = 300, resizeable = false}

local c2 = Window{title = 'Owned by Main', frame = true, w = 500, h = 100, visible = true, owner = c,
							--taskbar_button = true --force a button on taskbar even when owned
							}
c2.min_w=200; c2.min_h=200
c2.max_w=300; c2.max_h=300

local c4 = Window{x = 400, y = 400, w = 400, h = 200,
						border = true,
						frame = false,
						window_edge = false,
						--dialog_frame = false,
						resizeable = false,
						owner = c,
						}

function c:on_key_down(vk, flags)
	print('WM_KEYDOWN', vk, flags)
end

function c:on_key_down_char(char, flags)
	print('WM_CHAR', char, flags)
end

function c:on_lbutton_double_click()
	print'double clicked'
end

c.__wantallkeys = true

c3:minimize()
c3:activate()
c3:minimize()

MessageLoop()

end

