
--oo/abstract/control: base class for standard controls
--Written by Cosmin Apreutesei. Public Domain.

setfenv(1, require'winapi')
require'winapi.basewindowclass'
require'winapi.comctl'

local anchors_mt = {__tostring = format_anchors}
local function anchors(t)
	return setmetatable(t, anchors_mt)
end

Control = subclass({
	__defaults = {
		anchors = anchors{
			left = true,
			top = true,
			right = false,
			bottom = false,
		},
	},
}, BaseWindow)

function format_anchors(t)
	return
		(t.left   and 'l')..
		(t.top    and 't')..
		(t.right  and 'r')..
		(t.bottom and 'b')
end

function parse_anchors(s)
	if type(s) == 'string' then
		return anchors{
			left   = s:find('l', 1, true) and true or nil,
			top    = s:find('t', 1, true) and true or nil,
			right  = s:find('r', 1, true) and true or nil,
			bottom = s:find('b', 1, true) and true or nil,
		}
	elseif type(s) == 'table' then
		return anchors{
			left   = s.left,
			top    = s.top,
			right  = s.right,
			bottom = s.bottom,
		}
	else
		assert(s == nil)
	end
end

function Control:after___before_create(info, args)
	self.anchors = info.anchors
	--parent is either a window object or a handle. if it's a handle, self.parent will return nil.
	args.parent = info.parent and info.parent.hwnd or info.parent
	args.style = bit.bor(args.style, args.parent and WS_CHILD or WS_POPUP, WS_CLIPSIBLINGS, WS_CLIPCHILDREN)
end

function Control:after___init(info)
	--subclass the control to intercept the messages sent to it.
	self.__prev_proc = ffi.cast('WNDPROC', SetWindowLong(self.hwnd, GWL_WNDPROC, MessageRouter.proc))
end

function Control:__default_proc(WM, wParam, lParam)
	return CallWindowProc(self.__prev_proc, self.hwnd, WM, wParam, lParam)
end

function Control:get_parent()
	return Windows:find(GetParent(self.hwnd))
end

function Control:set_parent(parent)
	local old_parent = self.parent
	if parent and not old_parent then --popup windows can become child windows
		SetWindowStyle(self.hwnd,
			setbits(GetWindowStyle(self.hwnd),
			bit.bor(WS_POPUP, WS_CHILD),
			WS_CHILD))
	end
	SetParent(self.hwnd, parent and parent.hwnd)
	if not parent and old_parent then --child windows can become popup windows (diff. from overlapped)
		SetWindowStyle(self.hwnd,
			setbits(GetWindowStyle(self.hwnd),
			bit.bor(WS_POPUP, WS_CHILD),
			WS_POPUP))
	end
	if (parent and not old_parent) or (not parent and old_parent) then
		SetWindowPos(self.hwnd, nil, 0, 0, 0, 0, SWP_FRAMECHANGED_ONLY)
	end
	--TODO: find out which of the windows is enough to send this message to.
	ChangeUIState(self.hwnd, UIS_INITIALIZE, bit.bor(UISF_HIDEACCEL, UISF_HIDEFOCUS))
	if old_parent then ChangeUIState(old_parent.hwnd, UIS_INITIALIZE, bit.bor(UISF_HIDEACCEL, UISF_HIDEFOCUS)) end
	if parent then ChangeUIState(parent.hwnd, UIS_INITIALIZE, bit.bor(UISF_HIDEACCEL, UISF_HIDEFOCUS)) end
end

--size constraints -----------------------------------------------------------

--clamp x to min..max range, where min and/or max can be nil.
local function clamp(x, min, max)
	return math.min(math.max(x, min or -1/0), max or 1/0)
end

--apply constraints to the movable sides of a rectangle.
function Control:__apply_constraints(r, left, top, right, bottom)
	local min_w, min_h, max_w, max_h = self:__constraints()

	local w1 = clamp(r.w, min_w, max_w)
	local h1 = clamp(r.h, min_h, max_h)

	if top then
		r.y1 = r.y2 - h1
		r.y2 = r.y1 + h1 end
	if bottom then
		r.y2 = r.y1 + h1
	end
	if left then
		r.x1 = r.x2 - w1
		r.x2 = r.x1 + w1
	end
	if right then
		r.x2 = r.x1 + w1
	end

	return r
end

--delphi-style anchors -------------------------------------------------------

local function anchor_dim(self, left, right, enlargement, x, w, adjustment)
	local xofs, wofs
	if right then
		if left then --left and right: resize
			wofs = enlargement + (adjustment or 0)
		else --only right: move
			xofs = enlargement
		end
	end --only left: do nothing; no left/no right: undefined, we choose to do nothing.
	x = xofs and x + xofs
	w = wofs and w + wofs
	return x,w
end

--resize when parent is resized based on anchors.
function Control:after___parent_resizing(wp)

	local pr, r = self.parent.screen_rect, self.rect

	local x, w = anchor_dim(self, self.anchors.left, self.anchors.right,
		pr.x1 + wp.w - pr.x2, r.x1, r.w, self.__anchor_w)
	local y, h = anchor_dim(self, self.anchors.top,  self.anchors.bottom,
		pr.y1 + wp.h - pr.y2, r.y1, r.h, self.__anchor_h)

	--override rect with the changed sides.
	if x then local w = r.w; r.x1 = x; r.x2 = r.x1 + w end
	if y then local h = r.h; r.y1 = y; r.y2 = r.y1 + h end
	if w then r.x2 = r.x1 + w end
	if h then r.y2 = r.y1 + h end

	--apply constraints only on the changed (thus movable) sides of rect.
	self.rect = self:__apply_constraints(r, x, y, w, h)

	--real size might end up different than wanted size in which case we store
	--the difference, so that the original alignment to the parent is preserved
	--on the next resize.
	local rw, rh = self.w, self.h
	if w and rw ~= w then self.__anchor_w = w - rw else self.__anchor_w = nil end
	if h and rh ~= h then self.__anchor_h = h - rh else self.__anchor_h = nil end
end

function Control:get_anchors()
	return self._anchors
end

function Control:set_anchors(anchors)
	self._anchors = parse_anchors(anchors)
end
