local F = minetest.formspec_escape
local S = minetest.get_translator("lzr_getitem")

lzr_getitem = {}

local detached_inventories = {}

-- Per-player lists (indexed by player name)
local current_pages = {} -- current page number
local current_max_pages = {} -- current max. page number
local current_searches = {} -- current search string
local current_item_lists = {} -- currently active list of items

local SLOTS_W = 10
local SLOTS_H = 5
local SLOTS = SLOTS_W * SLOTS_H

local all_items_list_editor -- cached list of all items (editor)
local all_items_list_dev -- cached list of all items (dev mode)

local function allow_check(player, msg_disallowed)
	if player and player:is_player() then
		local name = player:get_player_name()
		local state = lzr_gamestate.get_state()
		if state == lzr_gamestate.EDITOR or state == lzr_gamestate.DEV or minetest.check_player_privs(name, "give") then
			return -1
		else
			minetest.chat_send_player(name, msg_disallowed)
			minetest.close_formspec(name, "lzr_getitem:getitem")
			return 0
		end
	else
		return -1
	end
end

-- Create detached inventories
local function add_detached_inventories(player)
	local name = player:get_player_name()
	local inv_items = minetest.create_detached_inventory("lzr_getitem_items_"..name, {
		allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
			return 0
		end,
		allow_put = function(inv, listname, index, stack, player)
			return 0
		end,
		allow_take = function(inv, listname, index, stack, player)
			return allow_check(player, S("You can only take items in the level editor, in development mode, or when you have the ‘give’ privilege."))
		end,
	}, name)
	local inv_trash = minetest.create_detached_inventory("lzr_getitem_trash_"..name, {
		allow_take = function(inv, listname, index, stack, player)
			return 0
		end,
		allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
			return 0
		end,
		allow_put = function(inv, listname, index, stack, player)
			local ret = allow_check(player, S("You can only trash items in the level editor, in development mode, or when you have the ‘give’ privilege."))
			if ret == -1 then
				return stack:get_count()
			else
				return 0
			end
		end,
		on_put = function(inv, listname, index, stack, player)
			inv:set_list(listname, {})
		end,
	}, name)
	inv_trash:set_size("main", 1)
	detached_inventories[name] = { items = inv_items, trash = inv_trash }
end

local sort_items_by_type = function(item1, item2)
	--[[ Sort items in this order:
	* Tools
	* Craftitems
	* Nodes
	* items from this mod (come last) ]]
	local def1 = minetest.registered_items[item1]
	local def2 = minetest.registered_items[item2]
	local tool1 = def1.type == "tool"
	local tool2 = def2.type == "tool"
	local craftitem1 = def1.type == "craft"
	local craftitem2 = def2.type == "craft"
	local node1 = def1.type == "node"
	local node2 = def2.type == "node"
	if tool1 and not tool2 then
		return true
	elseif not tool1 and tool2 then
		return false
	elseif craftitem1 and not craftitem2 then
		return true
	elseif not craftitem1 and craftitem2 then
		return false
	elseif node1 and not node2 then
		return true
	elseif not node1 and node2 then
		return false
	else
		return item1 < item2
	end
end

local collect_items = function(devmode, filter, lang_code)
	local items = {}
	if filter then
		filter = string.trim(filter)
		filter = string.lower(filter) -- to make sure the search is case-insensitive
	end
	for itemstring, def in pairs(minetest.registered_items) do
		local advanced_items
		if devmode then
			advanced_items = true
		else
			advanced_items = minetest.get_item_group(itemstring, "not_in_creative_inventory") == 0
		end
		if itemstring ~= "" and itemstring ~= "unknown" and itemstring ~= "ignore" and advanced_items then
			if filter and lang_code then
				local desc = ItemStack(itemstring):get_description()
				local matches
				-- First, try to match original description
				if desc ~= "" then
					local ldesc = string.lower(desc)
					matches = string.find(ldesc, filter, 1, true) ~= nil
					-- Second, try to match translated description
					if not matches then
						local tdesc = minetest.get_translated_string(lang_code, desc)
						if tdesc ~= "" then
							tdesc = string.lower(tdesc)
							matches = string.find(tdesc, filter, 1, true) ~= nil
						end
					end
					-- Third, try to match translated short description
					if not matches then
						local sdesc = ItemStack(itemstring):get_short_description()
						if sdesc ~= "" then
							sdesc = minetest.get_translated_string(lang_code, sdesc)
							sdesc = string.lower(sdesc)
							matches = string.find(sdesc, filter, 1, true) ~= nil
						end
					end

				end
				-- Fourth, try to match itemstring
				if not matches then
					matches = string.find(itemstring, filter, 1, true) ~= nil
				end

				-- If item was matched, add to item list
				if matches then
					table.insert(items, itemstring)
				end
			else
				table.insert(items, itemstring)
			end
		end
	end
	table.sort(items, sort_items_by_type)

	return items
end

local function update_inventory(devmode, name)
	local search = current_searches[name] or ""
	local items
	if search == "" then
		if devmode then
			items = all_items_list_dev
		else
			items = all_items_list_editor
		end
	else
		local lang_code = minetest.get_player_information(name).lang_code
		items = collect_items(devmode, search, lang_code)
	end
	local max_page = math.ceil(#items / SLOTS)
	current_max_pages[name] = max_page

	local inv = detached_inventories[name].items
	inv:set_size("main", #items)
	inv:set_list("main", items)
	if not current_pages[name] then
		current_pages[name] = 1
	end
	if current_pages[name] > max_page then
		current_pages[name] = max_page
	end
	if current_pages[name] < 1 then
		current_pages[name] = 1
	end
end

local function get_formspec(page, name)
	local start = 0 + (page-1)*SLOTS
	if not name then
		return ""
	end
	local player = minetest.get_player_by_name(name)
	local playerinvsize = player:get_inventory():get_size("main")
	local hotbarsize = player:hud_get_hotbar_itemcount()
	local pinv_w, pinv_h, pinv_x
	pinv_w = hotbarsize
	pinv_h = math.ceil(playerinvsize / pinv_w)
	pinv_w = math.min(pinv_w, 10)
	pinv_h = math.min(pinv_w, 4)
	pinv_x = 0
	if pinv_w < 9 then
		pinv_x = 1.5
	elseif pinv_w == 9 then
		pinv_x = 0.75
	end

	local pagestr = ""
	local max_page = current_max_pages[name]
	if max_page > 1 then
		-- Previous page button in item selection
		pagestr = "image_button[0,6.1;1,1;lzr_gui_arrow_left.png;lzr_getitem_prev;]"..
		-- Next page button in item selection
		"image_button[1.2,6.1;1,1;lzr_gui_arrow_right.png;lzr_getitem_next;]"..
		--~ Page counter in item selection. @1 = current page, @2 = total pages
		"label[0,5.9;"..F(S("Page: @1/@2", page, max_page)).."]"
	end

	local search_text = current_searches[name] or ""

	local inventory_list
	if current_max_pages[name] > 0 then
		inventory_list = "list[detached:lzr_getitem_items_"..name..";main;0,0;"..SLOTS_W..","..SLOTS_H..";"..start.."]"
	else
		inventory_list = "label[2.5,2.5;"..F(S("No items found.")).."]"
		if search_text ~= "" then
			inventory_list = inventory_list .. "button[2.5,3.25;3,0.8;search_button_reset_big;"..F(S("Reset search")).."]"
		end
	end

	local back_button
	if lzr_util.is_show_inventory_formspec_supported(player) then
		back_button = "style[back;noclip=true]"..
			--~ Button: Back to inventory menu
			"button[11.1,-0.6;2,0.6;back;"..F(S("Back")).."]"
	else
		back_button = ""
	end

	return "formspec_version[7]size[13.1,12.7]"..
	"style_type[list;spacing=0.25,0.15]"..
	"container[0.4,0.4]"..
	inventory_list ..
	"list[current_player;main;"..pinv_x..",7.5;"..pinv_w..","..pinv_h..";]" ..
	--~ Inventory: Trash item slot that destroys items dragged into it
	"label[11.25,5.9;"..F(S("Trash:")).."]" ..
	"list[detached:lzr_getitem_trash_"..name..";main;11.25,6.1;1,1]" ..
	"field[2.45,6.2;5.3,0.8;search;;"..F(search_text).."]" ..
	"field_close_on_enter[search;false]" ..
	--~ Button: Search for item
	"button[7.9,6.1;2.0,1;search_button_start;"..F(S("Search")).."]" ..
	-- Reset search button label in item selection
	"image_button[10,6.1;1,1;lzr_getitem_reset.png;search_button_reset;]" ..
	"tooltip[search_button_reset;"..F(S("Reset search")).."]" ..
	pagestr ..
	"container_end[]"..
	back_button..
	"listring[detached:lzr_getitem_items_"..name..";main]"..
	"listring[current_player;main]"..
	"listring[detached:lzr_getitem_trash_"..name..";main]"
end

lzr_getitem.show_formspec = function(name)
	local state = lzr_gamestate.get_state()
	if state ~= lzr_gamestate.EDITOR and state ~= lzr_gamestate.DEV and not minetest.check_player_privs(name, "give") and not lzr_game then
		minetest.chat_send_player(name, S("This is only available in the level editor, or when you have the 'give' privilege."))
		minetest.close_formspec(name, "lzr_getitem:getitem")
		return false
	end
	local page = current_pages[name]
	local form = get_formspec(page, name)
	minetest.show_formspec(name, "lzr_getitem:getitem", form)
	return true
end

minetest.register_on_player_receive_fields(function(player, formname, fields)
	if formname == "lzr_getitem:getitem" then
		local name = player:get_player_name()
		local state = lzr_gamestate.get_state()
		if state ~= lzr_gamestate.EDITOR and state ~= lzr_gamestate.DEV and not minetest.check_player_privs(name, "give") then
			minetest.chat_send_player(name, S("This is only available in the level editor, or when you have the 'give' privilege."))
			minetest.close_formspec(name, "lzr_getitem:getitem")
			return
		end
		local page = current_pages[name]
		local old_page = page
		-- Next page or previous page
		if fields.lzr_getitem_next or fields.lzr_getitem_prev then
			if fields.lzr_getitem_next then
				page = page + 1
			elseif fields.lzr_getitem_prev then
				page = page - 1
			end
			-- Handle page change
			if page < 1 then
				page = 1
			end
			local max_page = current_max_pages[name]
			if page > max_page then
				page = max_page
			end
			if page ~= old_page then
				current_pages[name] = page
				lzr_getitem.show_formspec(name)
			end
			return
		-- Search
		elseif (fields.search_button_start or (fields.key_enter and fields.key_enter_field == "search")) and fields.search then
			current_searches[name] = fields.search
			update_inventory(lzr_gamestate.get_state() == lzr_gamestate.DEV, name)
			lzr_getitem.show_formspec(name, fields.search)
			return
		-- Reset search
		elseif (fields.search_button_reset or fields.search_button_reset_big) then
			current_searches[name] = ""
			update_inventory(lzr_gamestate.get_state() == lzr_gamestate.DEV, name)
			lzr_getitem.show_formspec(name)
			return
		end
		-- Return to player formspec if pressing the close button
		if fields.back then
			lzr_util.show_inventory_formspec_if_supported(player)
			if not lzr_util.is_show_inventory_formspec_supported(player) then
				minetest.close_formspec(name, "lzr_getitem:getitem")
			end
		end
	end
end)

minetest.register_on_mods_loaded(function()
	all_items_list_dev = collect_items(true)
	all_items_list_editor = collect_items(false)
end)

minetest.register_on_joinplayer(function(player)
	local name = player:get_player_name()
	current_searches[name] = ""
	current_pages[name] = 1
	current_max_pages[name] = 0
	add_detached_inventories(player)
	update_inventory(false, name)
end)

lzr_gamestate.register_on_enter_state(function(new_state)
	if new_state == lzr_gamestate.EDITOR or new_state == lzr_gamestate.DEV then
		local name = "singleplayer"
		if minetest.get_player_by_name(name) then
			update_inventory(new_state == lzr_gamestate.DEV, name)
		end
	end
end)

minetest.register_on_leaveplayer(function(player)
	local name = player:get_player_name()
	current_pages[name] = nil
	current_max_pages[name] = nil
	current_searches[name] = nil
	current_item_lists[name] = nil
end)
