local S = minetest.get_translator("unified_inventory")
local F = minetest.formspec_escape
local ui = unified_inventory

-- This pair of encoding functions is used where variable text must go in
-- button names, where the text might contain formspec metacharacters.
-- We can escape button names for the formspec, to avoid screwing up
-- form structure overall, but they then don't get de-escaped, and so
-- the input we get back from the button contains the formspec escaping.
-- This is a game engine bug, and in the anticipation that it might be
-- fixed some day we don't want to rely on it.  So for safety we apply
-- an encoding that avoids all formspec metacharacters.

function ui.mangle_for_formspec(str)
	return string.gsub(str, "([^A-Za-z0-9])", function (c) return string.format("_%d_", string.byte(c)) end)
end
function ui.demangle_for_formspec(str)
	return string.gsub(str, "_([0-9]+)_", function (v) return string.char(v) end)
end

-- Get the player-specific unified_inventory style
function ui.get_per_player_formspec(player_name)
	local draw_lite_mode = ui.lite_mode and not minetest.check_player_privs(player_name, {ui_full=true})

	local style = table.copy(draw_lite_mode and ui.style_lite or ui.style_full)
	style.is_lite_mode = draw_lite_mode
	return style
end

-- Creates an item image or regular image button with a tooltip
local function formspec_button(ui_peruser, name, image, offset, pos, scale, label)
	local element = 'image_button'
	if minetest.registered_items[image] then
		element = 'item_image_button'
	elseif image:find(":", 1, true) then
		image = "unknown_item.png"
	end
	local spc = (1-scale)*ui_peruser.btn_size/2
	local size = ui_peruser.btn_size*scale
	return string.format("%s[%f,%f;%f,%f;%s;%s;]", element,
		(offset.x or offset[1]) + ( ui_peruser.btn_spc * (pos.x or pos[1]) ) + spc,
		(offset.y or offset[2]) + ( ui_peruser.btn_spc * (pos.y or pos[2]) ) + spc,
		size, size, image, name) ..
		string.format("tooltip[%s;%s]", name, F(label or name))
end

-- Add registered buttons (tabs)
local function formspec_tab_buttons(player, formspec, style)
	local n = #formspec + 1

	-- Main buttons

	local filtered_inv_buttons = {}

	for _, def in pairs(ui.buttons) do
		if not (style.is_lite_mode and def.hide_lite) then
			if def.condition == nil or def.condition(player) or not ui.hide_disabled_buttons then
				table.insert(filtered_inv_buttons, def)
			end
		end
	end

	local needs_scrollbar = #filtered_inv_buttons > style.main_button_cols * style.main_button_rows

	formspec[n] = ("scroll_container[%g,%g;%g,%g;tabbtnscroll;vertical]"):format(
		style.main_button_x, style.main_button_y, -- position
		style.main_button_cols * style.btn_spc, style.main_button_rows -- size
	)
	n = n + 1

	for i, def in pairs(filtered_inv_buttons) do
		local pos_x =           ((i - 1) % style.main_button_cols) * style.btn_spc
		local pos_y = math.floor((i - 1) / style.main_button_cols) * style.btn_spc

		if def.type == "image" then
			if (def.condition == nil or def.condition(player)) then
				formspec[n] = string.format("image_button[%g,%g;%g,%g;%s;%s;]",
					pos_x, pos_y, style.btn_size, style.btn_size,
					F(def.image),
					F(def.name))
				formspec[n+1] = "tooltip["..F(def.name)..";"..(def.tooltip or "").."]"
				n = n+2

			else
				formspec[n] = string.format("image[%g,%g;%g,%g;%s^[colorize:#808080:alpha]",
					pos_x, pos_y, style.btn_size, style.btn_size,
					def.image)
				n = n+1
			end
		end
	end
	formspec[n] = "scroll_container_end[]"
	if needs_scrollbar then
		local total_rows = math.ceil(#filtered_inv_buttons / style.main_button_cols)
		formspec[n+1] = ("scrollbaroptions[max=%i;arrows=hide]"):format(
			-- This calculation is not 100% accurate but "good enough"
			(total_rows - style.main_button_rows) * style.btn_spc * 10
		)
		formspec[n+2] = ("scrollbar[%g,%g;0.4,%g;vertical;tabbtnscroll;0]"):format(
			style.main_button_x + style.main_button_cols * style.btn_spc - 0.1, -- x pos
			style.main_button_y, -- y pos
			style.main_button_rows * style.btn_spc -- height
		)
		formspec[n+3] = "scrollbaroptions[max=1000;arrows=default]"
	end
end

-- Add category GUI elements (top right)
local function formspec_add_categories(player, formspec, ui_peruser)
	local player_name = player:get_player_name()
	local n = #formspec + 1

	local categories_pos = {
		ui_peruser.page_x,
		ui_peruser.page_y-ui_peruser.btn_spc-0.5
	}
	local categories_scroll_pos = {
		ui_peruser.page_x,
		ui_peruser.form_header_y - (ui_peruser.is_lite_mode and 0 or 0.2)
	}

	formspec[n] = string.format("background9[%f,%f;%f,%f;%s;false;16]",
		ui_peruser.page_x-0.15, categories_scroll_pos[2],
		(ui_peruser.btn_spc * ui_peruser.pagecols) + 0.2, 1.4 + (ui_peruser.is_lite_mode and 0 or 0.2),
		"ui_smallbg_9_sliced.png")
	n = n + 1

	formspec[n] = string.format("label[%f,%f;%s]",
		ui_peruser.page_x,
		ui_peruser.form_header_y + (ui_peruser.is_lite_mode and 0.3 or 0.2), F(S("Category:")))
	n = n + 1

	local scroll_offset = 0
	local category_count = #ui.category_list
	if category_count > ui_peruser.pagecols then
		scroll_offset = ui.current_category_scroll[player_name]
	end

	for index, category in ipairs(ui.category_list) do
		local column = index - scroll_offset
		if column > 0 and column <= ui_peruser.pagecols then
			local scale = 0.8
			if ui.current_category[player_name] == category.name then
				scale = 1
			end
			formspec[n] = formspec_button(ui_peruser, "category_"..category.name, category.symbol, categories_pos,
				{column-1, 0}, scale, category.label)
			n = n + 1
		end
	end
	if category_count > ui_peruser.pagecols and scroll_offset > 0 then
		-- prev
		formspec[n] = formspec_button(ui_peruser, "prev_category", "ui_left_icon.png", categories_scroll_pos,
			{ui_peruser.pagecols - 2, 0}, 0.8, S("Scroll categories left"))
		n = n + 1
	end
	if category_count > ui_peruser.pagecols and category_count - scroll_offset > ui_peruser.pagecols then
		-- next
		formspec[n] = formspec_button(ui_peruser, "next_category", "ui_right_icon.png", categories_scroll_pos,
			{ui_peruser.pagecols - 1, 0}, 0.8, S("Scroll categories right"))
	end
end

local function formspec_add_search_box(player, formspec, ui_peruser)
	local player_name = player:get_player_name()
	local n = #formspec + 1

	formspec[n] = "field_close_on_enter[searchbox;false]"

	formspec[n+1] = string.format("field[%f,%f;%f,%f;searchbox;;%s]",
		ui_peruser.page_buttons_x, ui_peruser.page_buttons_y,
		ui_peruser.searchwidth - 0.1, ui_peruser.btn_size,
		F(ui.current_searchbox[player_name]))
	formspec[n+2] = string.format("image_button[%f,%f;%f,%f;ui_search_icon.png;searchbutton;]",
		ui_peruser.page_buttons_x + ui_peruser.searchwidth, ui_peruser.page_buttons_y,
		ui_peruser.btn_size,ui_peruser.btn_size)
	formspec[n+3] = "tooltip[searchbutton;" ..F(S("Search")) .. "]"
	formspec[n+4] = string.format("image_button[%f,%f;%f,%f;ui_reset_icon.png;searchresetbutton;]",
		ui_peruser.page_buttons_x + ui_peruser.searchwidth + ui_peruser.btn_spc,
		ui_peruser.page_buttons_y,
		ui_peruser.btn_size, ui_peruser.btn_size)
	formspec[n+5] = "tooltip[searchresetbutton;"..F(S("Reset search and display everything")).."]"

	if ui.activefilter[player_name] ~= "" then
		formspec[n+6] = string.format("label[%f,%f;%s: %s]",
			ui_peruser.page_x, ui_peruser.page_y - 0.25,
			F(S("Filter")), F(ui.activefilter[player_name]))
	end
end

local function formspec_add_item_browser(player, formspec, ui_peruser)
	local player_name = player:get_player_name()
	local n = #formspec + 1

	-- Controls to flip items pages

	local btnlist = {
		{ "ui_skip_backward_icon.png", "start_list", S("First page") },
		{ "ui_doubleleft_icon.png",    "rewind3",    S("Back three pages") },
		{ "ui_left_icon.png",          "rewind1",    S("Back one page") },
		{ "ui_right_icon.png",         "forward1",   S("Forward one page") },
		{ "ui_doubleright_icon.png",   "forward3",   S("Forward three pages") },
		{ "ui_skip_forward_icon.png",  "end_list",   S("Last page") },
	}

	if ui_peruser.is_lite_mode then
		btnlist[2] = nil
		btnlist[5] = nil
	end

	local bn = 0
	for _, b in pairs(btnlist) do
		formspec[n] =  string.format("image_button[%f,%f;%f,%f;%s;%s;]",
			ui_peruser.page_buttons_x + ui_peruser.btn_spc*bn,
			ui_peruser.page_buttons_y + ui_peruser.btn_spc,
			ui_peruser.btn_size, ui_peruser.btn_size,
			b[1],b[2])
		formspec[n+1] = "tooltip["..b[2]..";"..F(b[3]).."]"
		bn = bn + 1
		n = n + 2
	end

	-- Items list
	if #ui.filtered_items_list[player_name] == 0 then
		local no_matches = S("No matching items")
		if ui_peruser.is_lite_mode then
			no_matches = S("No matches.")
		end

		formspec[n] = "label["..ui_peruser.page_x..","..(ui_peruser.page_y+0.15)..";" .. F(no_matches) .. "]"
		return
	end

	local dir = ui.active_search_direction[player_name]
	local list_index = ui.current_index[player_name]
	local page2 = math.floor(list_index / (ui_peruser.items_per_page) + 1)
	local pagemax = math.floor(
		(#ui.filtered_items_list[player_name] - 1)
			/ (ui_peruser.items_per_page) + 1)
	for y = 0, ui_peruser.pagerows - 1 do
		for x = 0, ui_peruser.pagecols - 1 do
			local name = ui.filtered_items_list[player_name][list_index]
			local item = minetest.registered_items[name]
			if item then
				-- Clicked on current item: Flip crafting direction
				if name == ui.current_item[player_name] then
					local cdir = ui.current_craft_direction[player_name]
					if cdir == "recipe" then
						dir = "usage"
					elseif cdir == "usage" then
						dir = "recipe"
					end
				else
				-- Default: use active search direction by default
					dir = ui.active_search_direction[player_name]
				end

				local button_name = "item_button_" .. dir .. "_"
					.. ui.mangle_for_formspec(name)
				formspec[n] = ("item_image_button[%f,%f;%f,%f;%s;%s;]"):format(
					ui_peruser.page_x + x * ui_peruser.btn_spc,
					ui_peruser.page_y + y * ui_peruser.btn_spc,
					ui_peruser.btn_size, ui_peruser.btn_size,
					name, button_name
				)
				local tooltip = item.description
				if item.mod_origin then
					-- "mod_origin" may not be specified for items that were
					-- registered in a callback (during or before ServerEnv init)
					tooltip = tooltip .. " [" .. item.mod_origin .. "]"
				end
				formspec[n + 1] = ("tooltip[%s;%s]"):format(
					button_name, minetest.formspec_escape(tooltip)
				)
				n = n + 2
			end
			list_index = list_index + 1
		end
	end
	formspec[n] = "style[page_number;content_offset=0]"
	formspec[n + 1] = string.format("image_button[%f,%f;%f,0.4;;page_number;%s: %s;false;false;]",
		ui_peruser.page_buttons_x,
		ui_peruser.page_buttons_y + ui_peruser.btn_spc * 2 - 0.1,
		ui_peruser.btn_spc * (bn - 1) + ui_peruser.btn_size,
		F(S("Page")), S("@1 of @2",page2,pagemax))
end

function ui.get_formspec(player, page)

	if not player then
		return ""
	end

	local player_name = player:get_player_name()
	local ui_peruser = ui.get_per_player_formspec(player_name)

	ui.current_page[player_name] = page
	local pagedef = ui.pages[page]

	if not pagedef then
		return "" -- Invalid page name
	end

	local fs = {
		"formspec_version[4]",
		"size["..ui_peruser.formw..","..ui_peruser.formh.."]",
		pagedef.formspec_prepend and "" or "no_prepend[]",
		ui.standard_background
	}

	local perplayer_formspec = ui.get_per_player_formspec(player_name)
	local fsdata = pagedef.get_formspec(player, perplayer_formspec)

	fs[#fs + 1] = fsdata.formspec

	formspec_tab_buttons(player, fs, ui_peruser)

	if fsdata.draw_inventory ~= false then
		-- Player inventory
		fs[#fs + 1] = "listcolors[#00000000;#00000000]"
		fs[#fs + 1] = ui_peruser.standard_inv
	end

	if fsdata.draw_item_list == false then
		return table.concat(fs, "")
	end

	formspec_add_categories(player, fs, ui_peruser)
	formspec_add_search_box(player, fs, ui_peruser)
	formspec_add_item_browser(player, fs, ui_peruser)

	return table.concat(fs)
end

function ui.set_inventory_formspec(player, page)
	if player then
		player:set_inventory_formspec(ui.get_formspec(player, page))
	end
end

function ui.is_itemdef_listable(def)
	return (not def.groups.not_in_creative_inventory
			or def.groups.not_in_creative_inventory == 0)
		and def.description
		and def.description ~= ""
end

--apply filter to the inventory list (create filtered copy of full one)
function ui.apply_filter(player, filter, search_dir)
	if not player then
		return false
	end
	local player_name = player:get_player_name()

	-- Whether to show uncraftable items
	local fprefilter = function(_)
		return true
	end
	if ui.hide_uncraftable_items and not ui.is_creative(player_name) then
		fprefilter = function(name)
			return ui.get_recipe_list(name)
		end
	end

	local registered_items = minetest.registered_items
	local lfilter = string.lower(filter)
	local ffilter

	if lfilter:sub(1, 6) == "group:" then
		-- Group filter: all groups of the item must match
		local groups = lfilter:sub(7):split(",")
		ffilter = function(name)
			local def = registered_items[name]
			if not def then
				return false
			end

			for _, group in ipairs(groups) do
				if not def.groups[group]
				or def.groups[group] <= 0 then
					return false
				end
			end
			return true
		end
	else
		-- Name filter: fuzzy match item names and descriptions
		local player_info = minetest.get_player_information(player_name)
		local lang = player_info and player_info.lang_code or ""

		ffilter = function(name)
			local def = registered_items[name]
			if not def then
				return false
			end

			local lname = string.lower(name)
			local ldesc = string.lower(def.description)
			local llocaldesc = minetest.get_translated_string
				and string.lower(minetest.get_translated_string(lang, def.description))
			return string.find(lname, lfilter, 1, true) or string.find(ldesc, lfilter, 1, true)
				or llocaldesc and string.find(llocaldesc, lfilter, 1, true)
		end
	end

	local filtered_items = {}

	local category = ui.current_category[player_name] or 'all'
	if category == 'all' then
		for _, name in ipairs(ui.items_list) do
			if fprefilter(name) and ffilter(name) then
				table.insert(filtered_items, name)
			end
		end
	elseif category == 'uncategorized' then
		for _, name in ipairs(ui.items_list) do
			if not ui.find_category(name)
					and fprefilter(name)
					and ffilter(name) then
				table.insert(filtered_items, name)
			end
		end
	else
		-- Any other category is selected
		for name, exists in pairs(ui.registered_category_items[category]) do
			if exists
					and fprefilter(name)
					and ffilter(name) then
				table.insert(filtered_items, name)
			end
		end
	end
	table.sort(filtered_items)

	ui.filtered_items_list_size[player_name] = #filtered_items
	ui.filtered_items_list[player_name] = filtered_items
	ui.current_index[player_name] = 1
	ui.activefilter[player_name] = filter
	ui.active_search_direction[player_name] = search_dir
	ui.set_inventory_formspec(player, ui.current_page[player_name])
end

-- Inform players about potential visual issues
minetest.register_on_joinplayer(function(player)
	local player_name = player:get_player_name()
	local info = minetest.get_player_information(player_name)
	if info and (info.formspec_version or 0) < 4 then
		minetest.chat_send_player(player_name, S("Unified Inventory: Your game version is too old"
			.. " and does not support the GUI requirements. You might experience visual issues."))
	end
end)
