
--oo/controls/menu: standard menu control
--Written by Cosmin Apreutesei. Public Domain.

setfenv(1, require'winapi')
require'winapi.vobject'
require'winapi.itemlist'
require'winapi.handlelist'
require'winapi.menu'

Menus = HandleList'hmenu' --singleton to track menu objects by their hwnd

MenuItemList = class(ItemList)

function MenuItemList:__init(menu, items)
	self.menu = menu
	self.hmenu = menu.hmenu
	self.handlers = {} --{i = handler|false}
	self:add_items(items)
end

local types_bitmask = bitmask{
	menu_break_bar = MFT_MENUBARBREAK, --vertical break with a bar
	menu_break = MFT_MENUBREAK,        --vertical break without a bar
	separator = MFT_SEPARATOR,
	owner_draw = MFT_OWNERDRAW,
	radio_check = MFT_RADIOCHECK,
	rtl = MFT_RIGHTORDER,
	right_align = MFT_RIGHTJUSTIFY,   --apply to this item and all subsequent items (only for menu bar items)
}

local states_bitmask = bitmask{
	checked = MFS_CHECKED,
	enabled = negate(MFS_DISABLED),
	highlight = MFS_HILITE,
	is_default = MFS_DEFAULT,
}

local function mkitem(item)
	return {
		text = item.text,
		submenu = item.submenu and item.submenu.hmenu,
		bitmap = item.bitmap,
		checked_bitmap = item.checked_bitmap,
		unchecked_bitmap = item.unchecked_bitmap,
		type = types_bitmask:set(0, item),
		state = states_bitmask:set(0, item),
	}
end

function MenuItemList:add(i, item)
	if not item then i,item = nil,i end --i is optional
	i = i or #self.handlers+1
	item = item or {}
	InsertMenuItem(self.hmenu, i, mkitem(item), true)
	table.insert(self.handlers, i, item.on_click or false)
	self.menu:__redraw()
	return i
end

function MenuItemList:remove(i)
	RemoveMenuItem(self.hmenu, i, true)
	table.remove(self.handlers, i)
	self.menu:__redraw()
end

function MenuItemList:set(i, item)
	SetMenuItemInfo(self.hmenu, i, mkitem(item), true)
	self.handlers[i] = item.on_click or false
	self.menu:__redraw()
end

function MenuItemList:get(i)
	local item = GetMenuItemInfo(self.hmenu, i, true)
	item.cch = item.cch + 1
	local buf = ffi.new('WCHAR[?]', item.cch)
	item.dwTypeData = buf
	item = GetMenuItemInfo(self.hmenu, i, true, item)
	return update({
			text = item.text,
			submenu = Menus:find(item.submenu),
			bitmap = item.bitmap,
			checked_bitmap = item.checked_bitmap,
			unchecked_bitmap = item.unchecked_bitmap,
			on_click = self.handlers[i],
		},
		types_bitmask:get(item.type),
		states_bitmask:get(item.state)
	)
end

function MenuItemList:setchecked(i, checked)
	CheckMenuItem(self.hmenu, i, true, checked)
end

function MenuItemList:checked(i)
	return bit.band(GetMenuState(self.hmenu, i, true), MF_CHECKED) ~= 0
end

function MenuItemList:setenabled(i, enabled)
	EnableMenuItem(self.hmenu, i, true, enabled)
end

function MenuItemList:enabled(i)
	local item = GetMenuItemInfo(self.hmenu, i, true)
	return states_bitmask:getbit(item.state, 'enabled')
end

function MenuItemList:get_count()
	return GetMenuItemCount(self.hmenu)
end


Menu = class(VObject)

local style_bitmask = bitmask{
	nocheck = MNS_NOCHECK,
	modeless = MNS_MODELESS,
	drag_and_drop = MNS_DRAGDROP,
	auto_dismiss = MNS_AUTODISMISS,
	notify_by_pos = MNS_NOTIFYBYPOS,
}

function Menu:__create_hmenu()
	self.type = 'menu'
	return CreateMenu()
end

function Menu:__init(info)
	info = info or {}
	self.hmenu = self:__create_hmenu()
	Menus:add(self)
	self.items = MenuItemList(self, info.items)

	local style = info
	info = MENUINFO(info)
	info.style = style_bitmask:set(MNS_NOTIFYBYPOS, style)
	SetMenuInfo(self.hmenu, info)
end

function Menu:free()
	DestroyMenu(self.hmenu)
	Menus:remove(self)
	self.hmenu = nil
	self.items.hmenu = nil
end

function Menu.__get_vproperty(class, self, k)
	if MENUINFO.fields[k] then --publish info fields individually
		return GetMenuInfo(self.hmenu)[k]
	elseif style_bitmask.fields[k] then --publish style fields individually
		return style_bitmask:get(GetMenuInfo(self.hmenu).style)
	end
	return Menu.__index.__get_vproperty(class, self, k)
end

function Menu.__set_vproperty(class, self, k, v)
	if MENUINFO.fields[k] then --publish info fields individually
		info = MENUINFO()
		info[k] = v
		SetMenuInfo(self.hmenu, info)
	elseif style_bitmask.fields[k] then --publish style fields individually
		info = GetMenuInfo(self.hmenu)
		info.style = style_bitmask:setbit(info.style, k, v)
		SetMenuInfo(self.hmenu, info)
	else
		Menu.__index.__set_vproperty(class, self, k, v)
	end
end

function Menu:__redraw() end --stub; used by MenuItemList methods

function Menu:get_info()
	local info = MENUINFO:collect(GetMenuInfo(self.hmenu))
	info.style = style_bitmask:get(info.style)
	return info
end

function Menu:set_info(info)
	info = MENUINFO(info)
	info.style = style_bitmask:set(MNS_NOTIFYBYPOS, info.style)
	SetMenuInfo(self.hmenu, info)
end

function Menu:popup(window, x, y, TPM) --TODO: crack TPM
	local r = RECT(x, y, x, y)
	MapWindowRect(window.hwnd, nil, r)
	return TrackPopupMenu(self.hmenu, window.hwnd, r.x, r.y, bit.bor(flags(TPM), TPM_RETURNCMD))
end

function Menu:WM_MENUCOMMAND(i) --pseudo-message from the owner window further routed to individual item handlers
	local handler = self.items.handlers[i]
	if handler then handler(self) end
end


MenuBar = class(Menu)

function MenuBar:__create_hmenu()
	self.type = 'menubar'
	return CreateMenuBar()
end

function MenuBar:__redraw() --used by MenuItemList methods
	if not self.hwnd then return end
	DrawMenuBar(self.hwnd)
end

function MenuBar:__set_window(window) --used by Window:set_menu()
	self.hwnd = window and window.hwnd
	self:__redraw()
end

--showcase

if not ... then
	require'winapi.showcase'
	local win = ShowcaseWindow()

	win.menu = MenuBar()

	win.menu.items:add{
		text = '&File',
		submenu = Menu{
			items = {
				{text = '&Close', on_click = function() win:close() end},
			},
		},
	}

	local helpmenu = Menu()
	helpmenu.items:add{text = '&About', on_click = function() end}

	win.menu.items:add{text = '&Help', submenu = helpmenu}

	MessageLoop()
end
