---------------------------
-- SmartFS: Smart Formspecs
-- License: CC0 or WTFPL
--    by Rubenwardy
---------------------------

local smartfs = {
	_fdef = {},
	_edef = {},
	_ldef = {},
	opened = {},
	inv = {}
}

local function boolToStr(v)
	return v and "true" or "false"
end

-- the smartfs() function
function smartfs.__call(self, name)
	return smartfs.get(name)
end

function smartfs.get(name)
	return smartfs._fdef[name]
end

------------------------------------------------------
-- Smartfs Interface -  Creates a new form and adds elements to it by running the function. Use before Minetest loads. (like minetest.register_node)
------------------------------------------------------
-- Register forms and elements
function smartfs.create(name, onload)
	assert(not smartfs._fdef[name],
			"SmartFS - (Error) Form "..name.." already exists!")
	assert(not smartfs.loaded or smartfs._loaded_override,
			"SmartFS - (Error) Forms should be declared while the game loads.")

	smartfs._fdef[name] = {
		form_setup_callback = onload,
		name = name,
		show = smartfs._show_,
		attach_to_node = smartfs._attach_to_node_
	}

	return smartfs._fdef[name]
end

------------------------------------------------------
-- Smartfs Interface - Creates a new element type
------------------------------------------------------
function smartfs.element(name, data)
	assert(not smartfs._edef[name],
			"SmartFS - (Error) Element type "..name.." already exists!")

	assert(data.onCreate, "element requires onCreate method")
	smartfs._edef[name] = data
	return smartfs._edef[name]
end

------------------------------------------------------
-- Smartfs Interface - Creates a dynamic form. Returns state
------------------------------------------------------
function smartfs.dynamic(name,player)
	if not smartfs._dynamic_warned then
		smartfs._dynamic_warned = true
		minetest.log("warning", "SmartFS - (Warning) On the fly forms are being used. May cause bad things to happen")
	end
	local statelocation = smartfs._ldef.player._make_state_location_(player)
	local state = smartfs._makeState_({name=name}, nil, statelocation, player)
	smartfs.opened[player] = state
	return state
end

------------------------------------------------------
-- Smartfs Interface - Returns the name of an installed and supported inventory mod that will be used above, or nil
------------------------------------------------------
function smartfs.inventory_mod()
	if minetest.global_exists("unified_inventory") then
		return "unified_inventory"
	elseif minetest.global_exists("inventory_plus") then
		return "inventory_plus"
	else
		return nil
	end
end

------------------------------------------------------
-- Smartfs Interface - Adds a form to an installed advanced inventory. Returns true on success.
------------------------------------------------------
function smartfs.add_to_inventory(form, icon, title)
	local ldef
	local invmod = smartfs.inventory_mod()
	if  invmod then
		ldef = smartfs._ldef[invmod]
	else
		return false
	end
	return ldef.add_to_inventory(form, icon, title)
end

------------------------------------------------------
-- Smartfs Interface - Set the form as players inventory
------------------------------------------------------
function smartfs.set_player_inventory(form)
	smartfs._ldef.inventory.set_inventory(form)
end
------------------------------------------------------
-- Smartfs Interface - Allows you to use smartfs.create after the game loads. Not recommended!
------------------------------------------------------
function smartfs.override_load_checks()
	smartfs._loaded_override = true
end

------------------------------------------------------
-- Smartfs formspec locations
------------------------------------------------------
-- Unified inventory plugin
smartfs._ldef.unified_inventory = {
	add_to_inventory = function(form, icon, title)
		unified_inventory.register_button(form.name, {
			type = "image",
			image = icon,
		})
		unified_inventory.register_page(form.name, {
			get_formspec = function(player, formspec)
				local name = player:get_player_name()
				local statelocation = smartfs._ldef.unified_inventory._make_state_location_(name)
				local state = smartfs._makeState_(form, nil, statelocation, name)
				if form.form_setup_callback(state) ~= false then
					smartfs.inv[name] = state
					return {formspec = state:_buildFormspec_(false)}
				else
					return nil
				end
			end
		})
	end,
	_make_state_location_ = function(player)
		return {
			type = "inventory",
			player = player,
			_show_ = function(state)
				unified_inventory.set_inventory_formspec(minetest.get_player_by_name(state.location.player), state.def.name)
			end
		}
	end
}

-- Inventory plus plugin
smartfs._ldef.inventory_plus = {
	add_to_inventory = function(form, icon, title)
		minetest.register_on_joinplayer(function(player)
			inventory_plus.register_button(player, form.name, title)
		end)
		minetest.register_on_player_receive_fields(function(player, formname, fields)
			if formname == "" and fields[form.name] then
				local name = player:get_player_name()
				local statelocation = smartfs._ldef.inventory_plus._make_state_location_(name)
				local state = smartfs._makeState_(form, nil, statelocation, name)
				if form.form_setup_callback(state) ~= false then
					smartfs.inv[name] = state
					state:show()
				end
			end
		end)
	end,
	_make_state_location_ = function(player)
		return {
			type = "inventory",
			player = player,
			_show_ = function(state)
				inventory_plus.set_inventory_formspec(minetest.get_player_by_name(state.location.player), state:_buildFormspec_(true))
			end
		}
	end
}

-- Show to player
smartfs._ldef.player = {
	_make_state_location_ = function(player)
		return {
			type = "player",
			player = player,
			_show_ = function(state)
				if not state._show_queued then
					state._show_queued = true
					minetest.after(0, function(state)
						if state then
							state._show_queued = nil
							if (not state.closed) and (not state.obsolete) then
								minetest.show_formspec(state.location.player, state.def.name, state:_buildFormspec_(true))
							end
						end
					end, state) -- state given as reference. Maybe additional updates are done in the meantime or the form is obsolete
				end
			end
		}
	end
}

-- Standalone inventory
smartfs._ldef.inventory = {
	set_inventory = function(form)
		if sfinv and sfinv.enabled then
			sfinv.enabled = nil
		end
		minetest.register_on_joinplayer(function(player)
			local name = player:get_player_name()
			local statelocation = smartfs._ldef.inventory._make_state_location_(name)
			local state = smartfs._makeState_(form, nil, statelocation, name)
			if form.form_setup_callback(state) ~= false then
				smartfs.inv[name] = state
				state:show()
			end
		end)
		minetest.register_on_leaveplayer(function(player)
			local name = player:get_player_name()
			smartfs.inv[name].obsolete = true
			smartfs.inv[name] = nil
		end)
	end,
	_make_state_location_ = function(name)
		return {
			type = "inventory",
			player = name,
			_show_ = function(state)
				if not state._show_queued then
					state._show_queued = true
					minetest.after(0, function(state)
						if state then
							state._show_queued = nil
							local player = minetest.get_player_by_name(state.location.player)
							--print("smartfs formspec:", state:_buildFormspec_(true))
							player:set_inventory_formspec(state:_buildFormspec_(true))
						end
					end, state)
				end
			end
		}
	end
}

-- Node metadata
smartfs._ldef.nodemeta = {
	_make_state_location_ = function(nodepos)
		return {
			type = "nodemeta",
			pos = nodepos,
			_show_ = function(state)
				if not state._show_queued then
					state._show_queued = true
					minetest.after(0, function(state)
						if state then
							state._show_queued = nil
							local meta = minetest.get_meta(state.location.pos)
							meta:set_string("formspec", state:_buildFormspec_(true))
							meta:set_string("smartfs_name", state.def.name)
							meta:mark_as_private("smartfs_name")
						end
					end, state)
				end
			end,
		}
	end
}

-- Sub-container (internally used)
smartfs._ldef.container = {
	_make_state_location_ = function(element)
		local self = {
			type = "container",
			containerElement = element,
			parentState = element.root
		}
		if self.parentState.location.type == "container" then
			self.rootState = self.parentState.location.rootState
		else
			self.rootState = self.parentState
		end
		return self
	end
}

------------------------------------------------------
-- Minetest Interface - on_receive_fields callback can be used in minetest.register_node for nodemeta forms
------------------------------------------------------
function smartfs.nodemeta_on_receive_fields(nodepos, formname, fields, sender, params)
	local meta = minetest.get_meta(nodepos)
	local nodeform = meta:get_string("smartfs_name")
	if not nodeform then
		print("SmartFS - (Warning) smartfs.nodemeta_on_receive_fields for node without smarfs data")
		return false
	end

	-- get the currentsmartfs state
	local opened_id = minetest.pos_to_string(nodepos)
	local state
	local form = smartfs.get(nodeform)
	if not smartfs.opened[opened_id] or      -- If opened first time
			smartfs.opened[opened_id].def.name ~= nodeform or -- Or form is changed
			smartfs.opened[opened_id].obsolete then
		local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos)
		state = smartfs._makeState_(form, params, statelocation)
		if smartfs.opened[opened_id] then
			smartfs.opened[opened_id].obsolete = true
		end
		smartfs.opened[opened_id] = state
		form.form_setup_callback(state)
	else
		state = smartfs.opened[opened_id]
	end

	-- Set current sender check for multiple users on node
	local name
	if sender then
		name = sender:get_player_name()
		state.players:connect(name)
	end

	-- take the input
	state:_sfs_on_receive_fields_(name, fields)

	-- Reset form if all players disconnected
	if sender and not state.players:get_first() and not state.obsolete then
		local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos)
		local resetstate = smartfs._makeState_(form, params, statelocation)
		if form.form_setup_callback(resetstate) ~= false then
			resetstate:show()
		end
		smartfs.opened[opened_id] = nil
	end
end

------------------------------------------------------
-- Minetest Interface - on_player_receive_fields callback in case of inventory or player
------------------------------------------------------
minetest.register_on_player_receive_fields(function(player, formname, fields)
	local name = player:get_player_name()
	if smartfs.opened[name] and smartfs.opened[name].location.type == "player" then
		if smartfs.opened[name].def.name == formname then
			local state = smartfs.opened[name]
			state:_sfs_on_receive_fields_(name, fields)

			-- disconnect player if form closed
			if not state.players:get_first() then
				smartfs.opened[name].obsolete = true
				smartfs.opened[name] = nil
			end
		end
	elseif smartfs.inv[name] and smartfs.inv[name].location.type == "inventory" then
		local state = smartfs.inv[name]
		state:_sfs_on_receive_fields_(name, fields)
	end
	return false
end)

------------------------------------------------------
-- Minetest Interface - Notify loading of smartfs is done
------------------------------------------------------
minetest.after(0, function()
	smartfs.loaded = true
end)

------------------------------------------------------
-- Form Interface [linked to form:show()] - Shows the form to a player
------------------------------------------------------
function smartfs._show_(form, name, params)
	assert(form)
	assert(type(name) == "string", "smartfs: name needs to be a string")
	assert(minetest.get_player_by_name(name), "player does not exist")
	local statelocation = smartfs._ldef.player._make_state_location_(name)
	local state = smartfs._makeState_(form, params, statelocation, name)
	if form.form_setup_callback(state) ~= false then
		if smartfs.opened[name] then -- set maybe previous form to obsolete
			smartfs.opened[name].obsolete = true
		end
		smartfs.opened[name] = state
		state:show()
	end
	return state
end

------------------------------------------------------
-- Form Interface [linked to form:attach_to_node()] - Attach a formspec to a node meta
------------------------------------------------------
function smartfs._attach_to_node_(form, nodepos, params)
	assert(form)
	assert(nodepos and nodepos.x)

	local statelocation = smartfs._ldef.nodemeta._make_state_location_(nodepos)
	local state = smartfs._makeState_(form, params, statelocation)
	if form.form_setup_callback(state) ~= false then
		local opened_id = minetest.pos_to_string(nodepos)
		if smartfs.opened[opened_id] then -- set maybe previous form to obsolete
			smartfs.opened[opened_id].obsolete = true
		end
		state:show()
	end
	return state
end

------------------------------------------------------
-- Smartfs Framework - create a form object (state)
------------------------------------------------------
function smartfs._makeState_(form, params, location, newplayer)
	------------------------------------------------------
	-- State - -- Object to manage players
	------------------------------------------------------
	local function _make_players_(newplayer)
		local self = {
			_list = {}
		}
		function self.connect(self, player)
			self._list[player] = true
		end
		function self.disconnect(self, player)
			self._list[player] = nil
		end
		function self.get_first(self)
			return next(self._list)
		end
		if newplayer then
			self:connect(newplayer)
		end
		return self
	end


	------------------------------------------------------
	-- State - create returning state object
	------------------------------------------------------
	return {
		_ele = {},
		def = form,
		players = _make_players_(newplayer),
		location = location,
		is_inv = (location.type == "inventory"), -- obsolete. Please use location.type="inventory" instead
		player = newplayer, -- obsolete. Please use location.player
		param = params or {},
		get = function(self,name)
			return self._ele[name]
		end,
		close = function(self)
			self.closed = true
		end,
		getSize = function(self)
			return self._size
		end,
		size = function(self,w,h)
			self._size = {w=w,h=h}
		end,
		setSize = function(self,w,h)
			self._size = {w=w,h=h}
		end,
		getNamespace = function(self)
			local ref = self
			local namespace = ""
			while ref.location.type == "container" do
				namespace = ref.location.containerElement.name.."#"..namespace
				ref = ref.location.parentState -- step near to the root
			end
			return namespace
		end,
		_buildFormspec_ = function(self,size)
			local res = ""
			if self._size and size then
				res = "size["..self._size.w..","..self._size.h.."]"
			end
			for key,val in pairs(self._ele) do
				if val:getVisible() then
					res = res .. val:getBackgroundString() .. val:build() .. val:getTooltipString()
				end
			end
			return res
		end,
		show = location._show_,
		_get_element_recursive_ = function(self, field)
			local topfield
			for z in field:gmatch("[^#]+") do
				topfield = z
				break
			end
			local element = self._ele[topfield]
			if element and field == topfield then
				return element
			elseif element then
				if element._getSubElement_ then
					local rel_field = string.sub(field, string.len(topfield)+2)
					return element:_getSubElement_(rel_field)
				else
					return element
				end
			else
				return nil
			end
		end,
		-- process onInput hook for the state
		_sfs_process_oninput_ = function(self, fields, player)
			if self._onInput then
				self:_onInput(fields, player)
			end
			-- recursive all onInput hooks on visible containers
			for elename, eledef in pairs(self._ele) do
				if eledef.getContainerState and eledef:getVisible() then
					eledef:getContainerState():_sfs_process_oninput_(fields, player)
				end
			end
		end,
		-- Receive fields and actions from formspec
		_sfs_on_receive_fields_ = function(self, player, fields)

			local fields_todo = {}
			for field, value in pairs(fields) do
				local element = self:_get_element_recursive_(field)
				if element then
					fields_todo[field] = { element = element, value = value }
				end
			end

			for field, todo in pairs(fields_todo) do
				todo.element:setValue(todo.value)
			end

			self:_sfs_process_oninput_(fields, player)

			for field, todo in pairs(fields_todo) do
				if todo.element.submit then
					todo.element:submit(todo.value, player)
				end
			end
			-- handle key_enter
			if fields.key_enter and fields.key_enter_field then
				local element = self:_get_element_recursive_(fields.key_enter_field)
				if element and element.submit_key_enter then
					element:submit_key_enter(fields[fields.key_enter_field], player)
				end
			end

			if not fields.quit and not self.closed and not self.obsolete then
				self:show()
			else
				self.players:disconnect(player)
				if not fields.quit and self.closed and not self.obsolete then
					--closed by application (without fields.quit). currently not supported, see: https://github.com/minetest/minetest/pull/4675
					minetest.show_formspec(player,"","size[5,1]label[0,0;Formspec closing not yet created!]")
				end
			end
			return true
		end,
		onInput = function(self, func)
			self._onInput = func -- (fields, player)
		end,
		load = function(self,file)
			local file = io.open(file, "r")
			if file then
				local table = minetest.deserialize(file:read("*all"))
				if type(table) == "table" then
					if table.size then
						self._size = table.size
					end
					for key,val in pairs(table.ele) do
						self:element(val.type,val)
					end
					return true
				end
			end
			return false
		end,
		save = function(self,file)
			local res = {ele={}}

			if self._size then
				res.size = self._size
			end

			for key,val in pairs(self._ele) do
				res.ele[key] = val.data
			end

			local file = io.open(file, "w")
			if file then
				file:write(minetest.serialize(res))
				file:close()
				return true
			end
			return false
		end,
		setparam = function(self,key,value)
			if not key then return end
			self.param[key] = value
			return true
		end,
		getparam = function(self,key,default)
			if not key then return end
			return self.param[key] or default
		end,
		element = function(self,typen,data)
			local type = smartfs._edef[typen]
			assert(type, "Element type "..typen.." does not exist!")
			assert(not self._ele[data.name], "Element "..data.name.." already exists")

			data.type = typen
			local ele = {
				name = data.name,
				root = self,
				data = data,
				remove = function(self)
					self.root._ele[self.name] = nil
				end,
				setPosition = function(self,x,y)
					self.data.pos = {x=x,y=y}
				end,
				getPosition = function(self)
					return self.data.pos
				end,
				setSize = function(self,w,h)
					self.data.size = {w=w,h=h}
				end,
				getSize = function(self)
					return self.data.size
				end,
				setVisible = function(self, visible)
					if visible == nil then
						self.data.visible = true
					else
						self.data.visible = visible
					end
				end,
				getVisible = function(self)
					return self.data.visible
				end,
				getAbsName = function(self)
					return self.root:getNamespace()..self.name
				end,
				setBackground = function(self, image)
					self.data.background = image
				end,
				getBackground = function(self)
					return self.data.background
				end,
				getBackgroundString = function(self)
					if self.data.background then
						local size = self:getSize()
						if size then
							return "background["..
									self.data.pos.x..","..self.data.pos.y..";"..
									size.w..","..size.h..";"..
									self.data.background.."]"
						else
							return ""
						end
					else
						return ""
					end
				end,
				setValue = function(self, value)
					self.data.value = value
				end,
				setTooltip = function(self,text)
					self.data.tooltip = minetest.formspec_escape(text)
				end,
				getTooltip = function(self)
					return self.data.tooltip
				end,
				getTooltipString = function(self)
					if self.data.tooltip then
						return "tooltip["..self:getAbsName()..";"..self:getTooltip().."]"
					else
						return ""
					end
				end,
			}

			ele.data.visible = true --visible by default

			for key, val in pairs(type) do
				ele[key] = val
			end

			self._ele[data.name] = ele

			type.onCreate(ele)

			return self._ele[data.name]
		end,

		------------------------------------------------------
		-- State - Element Constructors
		------------------------------------------------------
		button = function(self, x, y, w, h, name, text, exitf)
			return self:element("button", {
				pos    = {x=x,y=y},
				size   = {w=w,h=h},
				name   = name,
				value  = text,
				closes = exitf or false
			})
		end,
		image_button = function(self, x, y, w, h, name, text, image, exitf)
			return self:element("button", {
				pos    = {x=x,y=y},
				size   = {w=w,h=h},
				name   = name,
				value  = text,
				image  = image,
				closes = exitf or false
			})
		end,
		item_image_button = function(self, x, y, w, h, name, text, item, exitf)
			return self:element("button", {
				pos    = {x=x,y=y},
				size   = {w=w,h=h},
				name   = name,
				value  = text,
				item   = item,
				closes = exitf or false
			})
		end,
		label = function(self, x, y, name, text)
			return self:element("label", {
				pos   = {x=x,y=y},
				name  = name,
				value = text,
				vertical = false
			})
		end,
		vertlabel = function(self, x, y, name, text)
			return self:element("label", {
				pos   = {x=x,y=y},
				name  = name,
				value = text,
				vertical = true
			})
		end,
		toggle = function(self, x, y, w, h, name, list)
			return self:element("toggle", {
				pos  = {x=x, y=y},
				size = {w=w, h=h},
				name = name,
				id   = 1,
				list = list
			})
		end,
		field = function(self, x, y, w, h, name, label)
			return self:element("field", {
				pos   = {x=x, y=y},
				size  = {w=w, h=h},
				name  = name,
				value = "",
				label = label
			})
		end,
		pwdfield = function(self, x, y, w, h, name, label)
			local res = self:element("field", {
				pos   = {x=x, y=y},
				size  = {w=w, h=h},
				name  = name,
				value = "",
				label = label
			})
			res:isPassword(true)
			return res
		end,
		textarea = function(self, x, y, w, h, name, label)
			local res = self:element("field", {
				pos   = {x=x, y=y},
				size  = {w=w, h=h},
				name  = name,
				value = "",
				label = label
			})
			res:isMultiline(true)
			return res
		end,
		image = function(self, x, y, w, h, name, img)
			return self:element("image", {
				pos   = {x=x, y=y},
				size  = {w=w, h=h},
				name  = name,
				value = img,
				imgtype = "image"
			})
		end,
		background = function(self, x, y, w, h, name, img)
			return self:element("image", {
				pos   = {x=x, y=y},
				size  = {w=w, h=h},
				name  = name,
				background = img,
				imgtype  = "background"
			})
		end,
		item_image = function(self, x, y, w, h, name, img)
			return self:element("image", {
				pos   = {x=x, y=y},
				size  = {w=w, h=h},
				name  = name,
				value = img,
				imgtype  = "item"
			})
		end,
		checkbox = function(self, x, y, name, label, selected)
			return self:element("checkbox", {
				pos   = {x=x, y=y},
				name  = name,
				value = selected,
				label = label
			})
		end,
		listbox = function(self, x, y, w, h, name, selected, transparent)
			return self:element("list", {
				pos         = {x=x, y=y},
				size        = {w=w, h=h},
				name        = name,
				selected    = selected,
				transparent = transparent
			})
		end,
		dropdown = function(self, x, y, w, h, name, selected)
			return self:element("dropdown", {
				pos         = {x=x, y=y},
				size        = {w=w, h=h},
				name        = name,
				selected    = selected
			})
		end,
		inventory = function(self, x, y, w, h, name)
			return self:element("inventory", {
				pos  = {x=x, y=y},
				size = {w=w, h=h},
				name = name
			})
		end,
		container = function(self, x, y, name, relative)
			return self:element("container", {
				pos  = {x=x, y=y},
				name = name,
				relative = false
			})
		end,
		view = function(self, x, y, name, relative)
			return self:element("container", {
				pos  = {x=x, y=y},
				name = name,
				relative = true
			})
		end,
	}
end

-----------------------------------------------------------------
-------------------------  ELEMENTS  ----------------------------
-----------------------------------------------------------------

smartfs.element("button", {
	onCreate = function(self)
		assert(self.data.pos and self.data.pos.x and self.data.pos.y, "button needs valid pos")
		assert(self.data.size and self.data.size.w and self.data.size.h, "button needs valid size")
		assert(self.name, "button needs name")
		assert(self.data.value, "button needs label")
	end,
	build = function(self)
		local specstring
		if self.data.image then
			if self.data.closes then
				specstring = "image_button_exit["
			else
				specstring = "image_button["
			end
		elseif self.data.item then
			if self.data.closes then
				specstring = "item_image_button_exit["
			else
				specstring = "item_image_button["
			end
		else
			if self.data.closes then
				specstring = "button_exit["
			else
				specstring = "button["
			end
		end

		specstring = specstring ..
				self.data.pos.x..","..self.data.pos.y..";"..
				self.data.size.w..","..self.data.size.h..";"
		if self.data.image then
			specstring = specstring..self.data.image..";"
		elseif self.data.item then
			specstring = specstring..self.data.item..";"
		end
		specstring = specstring..self:getAbsName()..";"..
				minetest.formspec_escape(self.data.value).."]"
		return specstring
	end,
	submit = function(self, field, player)
		if self._click then
			self:_click(self.root, player)
		end
	end,
	onClick = function(self,func)
		self._click = func
	end,
	click = function(self,func)
		self._click = func
	end,
	setText = function(self,text)
		self:setValue(text)
	end,
	getText = function(self)
		return self.data.value
	end,
	setImage = function(self,image)
		self.data.image = image
		self.data.item = nil
	end,
	getImage = function(self)
		return self.data.image
	end,
	setItem = function(self,item)
		self.data.item = item
		self.data.image = nil
	end,
	getItem = function(self)
		return self.data.item
	end,
	setClose = function(self,bool)
		self.data.closes = bool
	end,
	getClose = function(self)
		return self.data.closes or false
	end
})

smartfs.element("toggle", {
	onCreate = function(self)
		assert(self.data.pos and self.data.pos.x and self.data.pos.y, "toggle needs valid pos")
		assert(self.data.size and self.data.size.w and self.data.size.h, "toggle needs valid size")
		assert(self.name, "toggle needs name")
		assert(self.data.list, "toggle needs data")
	end,
	build = function(self)
		return "button["..
			self.data.pos.x..","..self.data.pos.y..
			";"..
			self.data.size.w..","..self.data.size.h..
			";"..
			self:getAbsName()..
			";"..
			minetest.formspec_escape(self.data.list[self.data.id])..
			"]"
	end,
	submit = function(self, field, player)
		self.data.id = self.data.id + 1
		if self.data.id > #self.data.list then
			self.data.id = 1
		end
		if self._tog then
			self:_tog(self.root, player)
		end
	end,
	onToggle = function(self,func)
		self._tog = func
	end,
	setId = function(self,id)
		self.data.id = id
	end,
	getId = function(self)
		return self.data.id
	end,
	getText = function(self)
		return self.data.list[self.data.id]
	end
})

smartfs.element("label", {
	onCreate = function(self)
		assert(self.data.pos and self.data.pos.x and self.data.pos.y, "label needs valid pos")
		assert(self.data.value, "label needs text")
	end,
	build = function(self)
		local specstring
		if self.data.vertical then
			specstring = "vertlabel["
		else
			specstring = "label["
		end
		return specstring..
			self.data.pos.x..","..self.data.pos.y..
			";"..
			minetest.formspec_escape(self.data.value)..
			"]"
	end,
	setText = function(self,text)
		self:setValue(text)
	end,
	getText = function(self)
		return self.data.value
	end
})

smartfs.element("field", {
	onCreate = function(self)
		assert(self.data.pos and self.data.pos.x and self.data.pos.y, "field needs valid pos")
		assert(self.data.size and self.data.size.w and self.data.size.h, "field needs valid size")
		assert(self.name, "field needs name")
		self.data.value = self.data.value or ""
		self.data.label = self.data.label or ""
	end,
	build = function(self)
		if self.data.ml then
			return "textarea["..
				self.data.pos.x..","..self.data.pos.y..
				";"..
				self.data.size.w..","..self.data.size.h..
				";"..
				self:getAbsName()..
				";"..
				minetest.formspec_escape(self.data.label)..
				";"..
				minetest.formspec_escape(self.data.value)..
				"]"
		elseif self.data.pwd then
			return "pwdfield["..
				self.data.pos.x..","..self.data.pos.y..
				";"..
				self.data.size.w..","..self.data.size.h..
				";"..
				self:getAbsName()..
				";"..
				minetest.formspec_escape(self.data.label)..
				"]"..
				self:getCloseOnEnterString()
		else
			return "field["..
				self.data.pos.x..","..self.data.pos.y..
				";"..
				self.data.size.w..","..self.data.size.h..
				";"..
				self:getAbsName()..
				";"..
				minetest.formspec_escape(self.data.label)..
				";"..
				minetest.formspec_escape(self.data.value)..
				"]"..
				self:getCloseOnEnterString()
		end
	end,
	setLabel = function(self,text)
		self.data.label = text
	end,
	getLabel = function(self)
		return self.data.label
	end,
	setText = function(self,text)
		self:setValue(text)
	end,
	getText = function(self)
		return self.data.value
	end,
	isPassword = function(self,bool)
		self.data.pwd = bool
	end,
	isMultiline = function(self,bool)
		self.data.ml = bool
	end,
	getCloseOnEnterString = function(self)
		if self.close_on_enter == nil then
			return ""
		else
			return "field_close_on_enter["..self:getAbsName()..";"..tostring(self.close_on_enter).."]"
		end
	end,
	setCloseOnEnter = function(self, value)
		self.close_on_enter = value
	end,
	getCloseOnEnter = function(self)
		return self.close_on_enter
	end,
	submit_key_enter = function(self, field, player)
		if self._key_enter then
			self:_key_enter(self.root, player)
		end
	end,
	onKeyEnter = function(self,func)
		self._key_enter = func
	end,
})

smartfs.element("image", {
	onCreate = function(self)
		assert(self.data.pos and self.data.pos.x and self.data.pos.y, "image needs valid pos")
		assert(self.data.size and self.data.size.w and self.data.size.h, "image needs valid size")
		self.data.value = self.data.value or ""
	end,
	build = function(self)
		if self.data.imgtype == "background" then
			return "" -- handled in _buildFormspec_ trough getBackgroundString()
		elseif self.data.imgtype == "item" then
			return "item_image["..
				self.data.pos.x..","..self.data.pos.y..
				";"..
				self.data.size.w..","..self.data.size.h..
				";"..
				self.data.value..
				"]"
		else
			return "image["..
				self.data.pos.x..","..self.data.pos.y..
				";"..
				self.data.size.w..","..self.data.size.h..
				";"..
				self.data.value..
				"]"
		end
	end,
	setImage = function(self,text)
		if self.data.imgtype == "background" then
			self.data.background = text
		else
			self:setValue(text)
		end
	end,
	getImage = function(self)
		if self.data.imgtype == "background" then
			return self.data.background
		else
			return self.data.value
		end
	end
})

smartfs.element("checkbox", {
	onCreate = function(self)
		assert(self.data.pos and self.data.pos.x and self.data.pos.y, "checkbox needs valid pos")
		assert(self.name, "checkbox needs name")
		self.data.value = minetest.is_yes(self.data.value)
		self.data.label = self.data.label or ""
	end,
	build = function(self)
		return "checkbox["..
			self.data.pos.x..","..self.data.pos.y..
			";"..
			self:getAbsName()..
			";"..
			minetest.formspec_escape(self.data.label)..
			";" .. boolToStr(self.data.value) .."]"
	end,
	submit = function(self, field, player)
		-- call the toggle function if defined
		if self._tog then
			self:_tog(self.root, player)
		end
	end,
	setValue = function(self, value)
		self.data.value = minetest.is_yes(value)
	end,
	getValue = function(self)
		return self.data.value
	end,
	onToggle = function(self,func)
		self._tog = func
	end,
})

smartfs.element("list", {
	onCreate = function(self)
		assert(self.data.pos and self.data.pos.x and self.data.pos.y, "list needs valid pos")
		assert(self.data.size and self.data.size.w and self.data.size.h, "list needs valid size")
		assert(self.name, "list needs name")
		self.data.value = minetest.is_yes(self.data.value)
		self.data.items = self.data.items or {}
	end,
	build = function(self)
		if not self.data.items then
			self.data.items = {}
		end
		local escaped = {}
		for i, v in ipairs(self.data.items) do
			escaped[i] = minetest.formspec_escape(v)
		end
		return "textlist["..
				self.data.pos.x..","..self.data.pos.y..
				";"..
				self.data.size.w..","..self.data.size.h..
				";"..
				self:getAbsName()..
				";"..
				table.concat(escaped, ",")..
				";"..
				tostring(self.data.selected or "")..
				";"..
				tostring(self.data.transparent or "false").."]"
	end,
	submit = function(self, field, player)
		local _type = string.sub(field,1,3)
		local index = tonumber(string.sub(field,5))
		self.data.selected = index
		if _type == "CHG" and self._click then
			self:_click(self.root, index, player)
		elseif _type == "DCL" and self._doubleClick then
			self:_doubleClick(self.root, index, player)
		end
	end,
	onClick = function(self, func)
		self._click = func
	end,
	click = function(self, func)
		self._click = func
	end,
	onDoubleClick = function(self, func)
		self._doubleClick = func
	end,
	doubleclick = function(self, func)
		self._doubleClick = func
	end,
	addItem = function(self, item)
		table.insert(self.data.items, item)
		-- return the index of item. It is the last one
		return #self.data.items
	end,
	removeItem = function(self,idx)
		table.remove(self.data.items,idx)
	end,
	getItem = function(self, idx)
		return self.data.items[idx]
	end,
	popItem = function(self)
		local item = self.data.items[#self.data.items]
		table.remove(self.data.items)
		return item
	end,
	clearItems = function(self)
		self.data.items = {}
	end,
	setSelected = function(self,idx)
		self.data.selected = idx
	end,
	getSelected = function(self)
		return self.data.selected
	end,
	getSelectedItem = function(self)
		return self:getItem(self:getSelected())
	end,
})

smartfs.element("dropdown", {
	onCreate = function(self)
		assert(self.data.pos and self.data.pos.x and self.data.pos.y, "dropdown needs valid pos")
		assert(self.data.size and self.data.size.w and self.data.size.h, "dropdown needs valid size")
		assert(self.name, "dropdown needs name")
		self.data.items = self.data.items or {}
		self.data.selected = self.data.selected or 1
		self.data.value = ""
	end,
	build = function(self)
		return "dropdown["..
			self.data.pos.x..","..self.data.pos.y..
			";"..
			self.data.size.w..","..self.data.size.h..
			";"..
			self:getAbsName()..
			";"..
			table.concat(self.data.items, ",")..
			";"..
			tostring(self:getSelected())..
			"]"
	end,
	submit = function(self, field, player)
		self:getSelected()
		if self._select then
			self:_select(self.root, field, player)
		end
	end,
	onSelect = function(self, func)
		self._select = func
	end,
	addItem = function(self, item)
		table.insert(self.data.items, item)
		if #self.data.items == self.data.selected then
			self.data.value = item
		end
		-- return the index of item. It is the last one
		return #self.data.items
	end,
	removeItem = function(self,idx)
		table.remove(self.data.items,idx)
	end,
	getItem = function(self, idx)
		return self.data.items[idx]
	end,
	popItem = function(self)
		local item = self.data.items[#self.data.items]
		table.remove(self.data.items)
		return item
	end,
	clearItems = function(self)
		self.data.items = {}
	end,
	setSelected = function(self,idx)
		self.data.selected = idx
		self.data.value = self:getItem(idx) or ""
	end,
	setSelectedItem = function(self,itm)
		for idx, item in ipairs(self.data.items) do
			if item == itm then
				self.data.selected = idx
				self.data.value = item
			end
		end
	end,
	getSelected = function(self)
		self.data.selected = 1
		if #self.data.items > 1 then
			for i = 1, #self.data.items do
				if self.data.items[i] == self.data.value then
					self.data.selected = i
				end
			end
		end
		return self.data.selected
	end,
	getSelectedItem = function(self)
		return self.data.value
	end,
})

smartfs.element("inventory", {
	onCreate = function(self)
		assert(self.data.pos and self.data.pos.x and self.data.pos.y, "list needs valid pos")
		assert(self.data.size and self.data.size.w and self.data.size.h, "list needs valid size")
		assert(self.name, "list needs name")
	end,
	build = function(self)
		return "list["..
			(self.data.invlocation or "current_player") ..
			";"..
			self.name..    --no namespacing
			";"..
			self.data.pos.x..","..self.data.pos.y..
			";"..
			self.data.size.w..","..self.data.size.h..
			";"..
			(self.data.index or "") ..
			"]"
	end,
	-- available inventory locations
	-- "current_player": Player to whom the menu is shown
	-- "player:<name>": Any player
	-- "nodemeta:<X>,<Y>,<Z>": Any node metadata
	-- "detached:<name>": A detached inventory
	-- "context" does not apply to smartfs, since there is no node-metadata as context available
	setLocation = function(self,invlocation)
		self.data.invlocation = invlocation
	end,
	getLocation = function(self)
		return self.data.invlocation or "current_player"
	end,
	usePosition = function(self, pos)
		self.data.invlocation = string.format("nodemeta:%d,%d,%d", pos.x, pos.y, pos.z)
	end,
	usePlayer = function(self, name)
		self.data.invlocation = "player:" .. name
	end,
	useDetached = function(self, name)
		self.data.invlocation = "detached:" .. name
	end,
	setIndex = function(self,index)
		self.data.index = index
	end,
	getIndex = function(self)
		return self.data.index
	end
})

smartfs.element("code", {
	onCreate = function(self)
		self.data.code = self.data.code or ""
	end,
	build = function(self)
		if self._build then
			self:_build()
		end

		return self.data.code
	end,
	submit = function(self, field, player)
		if self._sub then
			self:_sub(self.root, field, player)
		end
	end,
	onSubmit = function(self,func)
		self._sub = func
	end,
	onBuild = function(self,func)
		self._build = func
	end,
	setCode = function(self,code)
		self.data.code = code
	end,
	getCode = function(self)
		return self.data.code
	end
})

smartfs.element("container", {
	onCreate = function(self)
		assert(self.data.pos and self.data.pos.x and self.data.pos.y, "container needs valid pos")
		assert(self.name, "container needs name")
		local statelocation = smartfs._ldef.container._make_state_location_(self)
		self._state = smartfs._makeState_(nil, self.root.param, statelocation)
	end,

	-- redefinitions. The size is not handled by data.size but by container-state:size
	setSize = function(self,w,h)
		self:getContainerState():setSize(w,h)
	end,
	getSize = function(self)
		return self:getContainerState():getSize()
	end,

	-- element interface methods
	build = function(self)
		if self.data.relative ~= true then
			return "container["..self.data.pos.x..","..self.data.pos.y.."]"..
					self:getContainerState():_buildFormspec_(false)..
					"container_end[]"
		else
			return self:getContainerState():_buildFormspec_(false)
		end
	end,
	getContainerState = function(self)
		return self._state
	end,
	_getSubElement_ = function(self, field)
		return self:getContainerState():_get_element_recursive_(field)
	end,
})

return smartfs