files/lua/CLRForm.lua
require "CLRPackage"
import "System.Windows.Forms"
import "System.Drawing"
local Directory = luanet.import_type("System.IO.Directory")
local Path = luanet.import_type("System.IO.Path")
local File = luanet.import_type("System.IO.File")
local append = table.insert
local ferr = io.stderr -- for debugging

----------- some generally useful functions -----------------
--- Can be used to set multiple properties of an object, by supplying a table.
-- e.g. set(button,{Text="Click Me",Dock = DockStyle.Fill})
function set (obj,props)
    for k,v in pairs(props) do
		if type(k) == 'string' then
			obj[k] = v
		end
	end
end

--- works like AddRange, except it takes a table of controls
-- e.g add_controls(form,{button1,button2})
function add_controls (ctrl,ctrls)
    for k,v in pairs(ctrls) do
	    ctrl.Controls:Add(v)
	end
end

function ShowMessageBox (caption,icon)
	icon = icon or MessageBoxIcon.Information
    MessageBox.Show(caption,arg[0],MessageBoxButtons.OK,icon)
end

function ShowError (caption)
    ShowMessageBox(caption,MessageBoxIcon.Error)
end

---------- Utility function for creating classes -------------
--- Does single-inheritance and _delegation_
function class(base)
	local c = {}     -- a new class instance, which is the metatable for all objects of this type
	local mt = {}   -- a metatable for the class instance
	local userdata_base
	if base == nil then
	--nada
	elseif type(base) == 'table' then
		-- our new class is a shallow copy of the base class!
		for i,v in pairs(base) do
			c[i] = v
		end
		c._base = base
		-- inherit the 'not found' handler, if present
		if c._handler then mt.__index = c._handler end
	end
	-- the class will be the metatable for all its objects,
	-- and they will look up their methods in it.
	c.__index = c

	-- expose a ctor which can be called by <classname>(<args>)
	mt.__call = function(class_tbl,...)
		local obj= {}
		setmetatable(obj,c)
		-- nice alias for the base class ctor (which you have to call explicitly if you have a ctor)
		if base then c.super = base._init end
		if c._init then
			c._init(obj,...)
		else
			-- make sure that any stuff from the base class is initialized!
			if base and base._init then
				base._init(obj,...)
			end
		end
		return obj
	end

	-- Call Class.catch to set a handler for methods/properties not found in the class!
	c.catch = function(handler)
		c._handler = handler
		mt.__index = handler
	end
	c._init = ctor
	c.is_a = function(self,klass)
		local m = getmetatable(self)
		if not m then return false end --*can't be an object!
		while m do
			if m == klass then return true end
			m = rawget(m,'_base')
		end
		return false
	end
	c.class_of = function(obj)
		return c.is_a(obj,c)
	end
	-- any object can have a specified delegate which is called with unrecognized methods
	-- if _handler exists and obj[key] is nil, then pass onto handler!
	c.delegate = function(self,obj)
        local me = self
		mt.__index = function(tbl,key)
            -- handling fields!
            local getter = rawget(c,"Get_"..key)
            if getter then return getter(me) end
            getter = rawget(me,"_"..key)
            if getter then return getter end
			local method = obj[key]
			if method then
				-- it exists in the delegate! First check if it's callable
				if type(method) == 'function' or getmetatable(method).__call then
					return function(self,...)
						return method(obj,...)
					end
				else -- otherwise, just return
					return method
				end
			elseif self._handler then
				return self._handler(tbl,key)
			end
		end
		c.__newindex = function(self,key,val)
            local setter = rawget(c,"Set_"..key)
            if setter then
                setter(self,val)
            else
                obj[key] = val
            end
		end
	end
	setmetatable(c,mt)
	return c
end

----------- Creating Menus -----------------------------

local ShortcutType = Shortcut.F1:GetType()

local function parse_shortcut (s)
    local res
    if pcall(function()  -- we have to catch the exception!
        res = Enum.Parse(ShortcutType,s,false)
    end) then return res end
end

local function add_menu_items (item,tbl)
    for i = 1,#tbl,2 do
        item.MenuItems:Add(create_menu_item(tbl[i],tbl[i+1]))
    end
end

function create_menu_item (label,action)
    local item = MenuItem()
    local shortcut = label:match('%((%w+)%)')
    if shortcut then
        local shortcut = parse_shortcut(shortcut)
        if shortcut then item.Shortcut = shortcut end
        label = label:match('(.+)%(')
    end
    item.Text = label
    if type(action) == 'function' then
        item.Click:Add(action)
    else
        add_menu_items(item,action)
    end
    return item
end

function main_menu (tbl)
    local mm = MainMenu()
    add_menu_items(mm,tbl)
    return mm
end

function popup_menu (tbl)
    local mm = ContextMenu()
    add_menu_items(mm,tbl)
    return mm
end

-- a useful function for creating menu callbacks to methods of a given object.
function method (obj,fun)
    return function()
		fun(obj)
	end
end

local function populate_control (form,tbl)
	set(form,tbl)
	if #tbl > 0 then -- has an array part, containing controls
		if #tbl == 1 then
			table.insert(tbl,1,"Fill")
		end
		local i = 1
		while i <= #tbl do
			local c = tbl[i]
			local dock
			if type(c) == 'string' then
				dock = c
				c = tbl[i+1]
				i = i + 1
				c.Dock = DockStyle[dock]
			end
			form.Controls:Add(c)
			i = i + 1
		end
	end
    return form
end

function LuaForm (tbl)
	return populate_control(Form(),tbl)
end

function LuaPanel (tbl)
	return populate_control(Panel(),tbl)
end


---------------- Stream Layout --------------
StreamLayout = class()

function StreamLayout:_init(panel)
    self.xsep = 10
	self.X = self.xsep
	self.Y = self.xsep
	self.panel = panel
	self.newline = true
	self.maxX = 0
	self.maxHeight = 0
	self.labels = {}
	self.panel:SuspendLayout()
end

function StreamLayout:Add(c,like)
    if like then self.X = like.Left end
	c.Location = Point(self.X,self.Y)
	self.panel.Controls:Add(c)
	self.X = self.X + c.Width + self.xsep
	self.maxX = math.max(self.maxX,self.X)
	self.maxHeight = math.max(self.maxHeight,c.Height)
	if self.newline then
		self.firstC = c
		self.newline = false
		self.maxHeight = 0
	end
end

function StreamLayout:AddRow(lbl,...)
	local row = {...}
    if lbl then
		local label = Label()
		label.AutoSize = true
		label.Text = lbl
		row.label = label
		append(self.labels,row)
		self:Add(label)
	end
	for i,c in ipairs(row) do
		self:Add(c)
	end
	self:NextRow()
end

function StreamLayout:Height()
	return self.Y + self.maxHeight + self.xsep
end

function StreamLayout:Width()
    return self.maxX
end

function StreamLayout:NextRow()
    self.Y = self:Height()
	self.X = self.xsep
	self.newline = true
end

function StreamLayout:Finish ()
    local width = 0
	for i,row in ipairs(self.labels) do
		width = math.max(width,row.label.Width)
	end
	if width > 0 then -- i.e there is an explicit row of labels
		for i,row in ipairs(self.labels) do
			local lbl = row.label
			for j,c in ipairs(row) do
				c.Left = c.Left + (width - lbl.Width)
				self.maxX = math.max(self.maxX,c.Left+c.Width+self.xsep)
			end
		end
	end
	self.panel:ResumeLayout(false)
end

LayoutForm = class()

function LayoutForm:_init ()
	self.form = Form()
	self.layout = StreamLayout(self.form)
	self.hasButtons = false
	self.ok = false
	self.cancel = false
	self.finishedLayout = false
	-- this method can only be called once we've set up our own fields!
	self:delegate(self.form)
	self.FormBorderStyle = FormBorderStyle.FixedDialog
	self.MaximizeBox = false
	self.MinimizeBox = false
end

function LayoutForm:AddControl(c)
    self.layout:Add(c)
end

function LayoutForm:AddControlRow(lbl,...)
    self.layout:AddRow(lbl,...)
end

function LayoutForm:AddTextBoxRow(lbl)
	local textBox = TextBox()
	self:AddControlRow(lbl,textBox)
	return textBox
end

function LayoutForm:Btn (title,res)
    local b = Button()
	b.Text = title
	if res == DialogResult.OK then
		self.AcceptButton = b
	elseif res == DialogResult.Cancel then
		self.CancelButton = b
	end
	self.layout:Add(b)
	self.hasButtons = true
	return b
end

function LayoutForm:OkBtn (title)
    return self:Btn(title,DialogResult.OK)
end

function LayoutForm:CancelBtn (title)
    return self:Btn(title,DialogResult.Cancel)
end

function LayoutForm:NextRow()
    self.layout:NextRow()
end

function LayoutForm:OkCancel ()
	if not self.layout.newline then self:NextRow() end
	self.ok = self:OkBtn "OK"
	self.cancel = self:CancelBtn "Cancel"
end

function LayoutForm:OnOK()
	return true
end

function LayoutForm:CenterControls (...)
    local w = 0
	local ctrls = {...}
	for _,c in ipairs(ctrls) do
	    w = w + c.Width
	end
	local diff = (self.layout:Width() - w)/(#ctrls + 1)
	local xx = diff
	for _,c in ipairs(ctrls) do
	    c.Left = xx
		xx = xx + c.Width + diff
	end
end

function LayoutForm:FinishLayout()
    if not self.hasButtons then
		self:OkCancel()
		self:CenterControls(self.ok,self.cancel)
		self.ok.Click:Add(function()
			if self:OnOK() then
				self.DialogResult = DialogResult.OK
			else
				self.DialogResult = DialogResult.None
			end
		end)
	end
	local layout = self.layout
	layout:Finish()
	self.ClientSize = Size(layout:Width(), layout:Height())
	self.finishedLayout = true
end

function LayoutForm:ShowDialogOK ()
    if not self.finishedLayout then
		self:FinishLayout()
	end
	return self:ShowDialog() == DialogResult.OK
end

------------------- Converters ------------------------------
-- These classes convert values between controls and Lua values, and provide basic verification,
-- like ensuring that a string is a valid number, for instance.
-- They provide an appropriate control for editing the particular value.
Converter = class()

function Converter:Control ()
    self.box = TextBox()
	return self.box
end

function Converter:Read (c)
    return c.Text
end

function Converter:Write (c,text)
    c.Text = text
end

NumberConverter = class(Converter)

function NumberConverter:Read (c)
	local txt = c.Text
    local value = tonumber(txt)
	if not value then return nil, "Cannot convert '"..txt.."' to a number" end
	return value
end

BoolConverter = class(Converter)

function BoolConverter:Control ()
    return CheckBox()
end

function BoolConverter:Read (c)
    return c.Checked
end

function BoolConverter:Write (c,val)
    c.Checked = val
end

ListConverter = class(Converter)

function ListConverter:_init (list)
    self.list = list
end

function ListConverter:Control ()
	local c = ComboBox()
	if not self.list.Editable then
		c.DropDownStyle = ComboBoxStyle.DropDownList
	end
	for i,item in ipairs(self.list) do
		c.Items:Add(item)
	end
	return c
end

function ListConverter:Read (c)
    local val = c.SelectedItem
	if not val then val = c.Text end
	return val
end

function ListConverter:Write (c,val)
    c.SelectedItem = val
end

FileConverter = class(Converter)

function FileConverter:_init (reading,mask)
	-- the filter is in a simplified form like 'Lua Files (*.lua)|C# Files (*.cs)"
	-- this will expand it into the required form.
    self.filter = mask:gsub("%((.-)%)",function(pat)
		return "("..pat..")|"..pat
	end)
	self.reading = reading
end

-- ExtraControl is an optional method which gives a converter the opportunity of adding another
-- control to the row after the primary control. In this case, we create a file browser button.
function FileConverter:ExtraControl ()
    local btn = Button()
	local box = self.box
	btn.Width = 30
	btn.Text = ".."
	btn.Click:Add(function()
		-- if possible, open the file browser in the same directory as the filename
		local path = self:Read(box)
		if not File.Exists(path) then
			path = Directory.GetCurrentDirectory()
		else
			path = Path.GetDirectoryName(path)
		end
		-- depending on whether we want a file to read or write ("Save as"), pick the file dialog.
		local filebox
		if self.reading then filebox = OpenFileDialog
		else filebox = SaveFileDialog 	end
		local dlg = filebox()
		dlg.Filter = self.filter
		dlg.InitialDirectory = path
		if dlg:ShowDialog() == DialogResult.OK then
			self:Write(box,dlg.FileName)
		end
	end)
	return btn
end

-- Note an important convention: this converter puts the full file path in the text box's Tag field,

function FileConverter:Write (c,val)
    c.Text = Path.GetFileName(val)
	c.Tag = val
end

function FileConverter:Read (c)
    return c.Tag
end

-- there are then two subclasses, depending if you want to open a file for reading or writing.

FileIn = class(FileConverter)

function FileIn:_init (mask)
    self:super(true,mask)
end

FileOut = class(FileConverter)

function FileOut:_init (mask)
    self:super(false,mask)
end

local converters = {
	number = NumberConverter(),
	string = Converter(),
	boolean = BoolConverter(),
}

function Converter.AddConverter (typename,conv)
    converters[typename] = conv
end

---------------- AutoVarDialog ------------------------------

local function callable (method)
	local mt = getmetatable(method)
	return type(method) == 'function' or (mt and mt.__call)
end

local function simple_list (nxt)
    return type(nxt) == 'table' and #nxt > 0
end

AutoVarDialog = class(LayoutForm)

function AutoVarDialog:_init (tbl)
	self.rows = {}
	self.T = tbl.Object or _G
	self.verify = tbl.Verify
	self.verify_exists = tbl.Verify ~= nil
	--end of local fields; NOW we can initialize the form!
	self.super(self)
	self.Text = tbl.Text or "untitled"
	local i,n = 1,#tbl
    while i <= n do
		local converter,constraint,extra
		local lbl = tbl[i]
		local var = tbl[i+1]
		local value = self.T[var]
		local vtype = type(value)
		-- is there a particular default or constraint set?
		if i+1 < n then
			local nxt = tbl[i+2]
			if type(nxt) ~= 'string' then
				if callable(nxt) then
					constraint = nxt
				elseif simple_list(nxt) then
					-- have been given a list of possible values
					converter = ListConverter(nxt)
				elseif Converter.class_of(nxt) then
					converter = nxt
				else
					ShowError("Unknown converter or verify function: "..nxt)
					return
				end
				i = i + 1
			end
		end
		if not converter then
			-- use a default converter appropriate to this type
			converter = converters[vtype]
			if not converter then
				ShowError("Cannot find a converter for type: "..vtype)
				return
			end
		end
		local c = converter:Control()
	    self:AddControlRow(lbl,c,converter.ExtraControl and converter:ExtraControl())
		converter:Write(c,value)
		append(self.rows,{cntrl=c,converter=converter,var=var, constraint=constraint})
		i = i + 2
	end
end

function AutoVarDialog:OnOK ()
	local T = {}
    for _,t in ipairs(self.rows) do
		local value,err = t.converter:Read(t.cntrl)
		if not err and t.constraint then
			err = t.constraint(value)
		end
		if err then
			ShowError(err)
			t.cntrl:Focus()
			return false
		end
		T[t.var] = value
	end
	 -- a function to verify the fields has been supplied
	if self.verify_exists then
		local err = self.verify(T)
		if err then
			ShowError(err)
			return false
		end
	end
	-- NOW we can finally copy the changed values into the target table!
	for k,v in pairs(T) do
	    self.T[k] = v
	end
    return true
end

function Match (pat,err)
    return function (s)
		--ferr:write(',',s,',',pat,'\n')
		if not s:find(pat) then return err end
	end
end

function Range (x1,x2)
	if not x2 then -- unbound upper range
		return function(x)
			if x < x1 then return "Must be greater than "..x1 end
		end
	elseif not x1 then -- unbound lower range
		return function(x)
			if x > x2 then return "Must be less than "..x2 end
		end
	else
		return function(x)
			if x < x1 or x > x2 then return "Must be in range "..x1.." to "..x2 end
		end
	end
end

NonBlank = Match ('%S+','Must be a non-blank string')
Word = Match('^%w+$','Must be a word')

--- A useful function for prompting a user for a single value.
-- returns a non-nil value if the user clicks on OK or presses <enter>.
function PromptForString (caption,prompt,default)
	local tbl = {val = default or ""}
    local form = AutoVarDialog {Text = caption, Object = tbl;
		prompt,"val"
	}
	if form:ShowDialogOK() then
		return tbl.val
	end
end
