-- Craft Lookup
-- Copyright 2017 Justin Law

local mod, private = ...
local tablelib = private.tablelib

-- Variables
mod.contexts = {} -- Information the mod uses to track players. Contexts accessed by player's name.
mod.database = nil -- Table containing navigation information of items and crafting recipes.

function private.print(playername, msg)
	assert(type(playername) == "string")
	assert(type(msg) == "string")
	
	minetest.chat_send_player(playername, "[craft_lookup] "..tostring(msg))
end

-- Backend Functions
function mod.catalog_items()
	if mod.database == nil then
		-- Create skeleton database structure.
		local itemlist = {}
		local grouplist = {}
		local database = {
			items = itemlist,
			groups = grouplist,
		}
		
		local get_group_data, add_ingredient_record
		function get_group_data(groupname)
			local r = grouplist[groupname]
			if r == nil then
				r = {
					members = {},
					recipes_ingredient = {},
				}
				grouplist[groupname] = r
			end
			return r
		end
		-- Function for adding recipes other items are ingredients of.
		function add_ingredient_record(input_itemname, recipe)
			local is_group, groupname = mod.is_group(input_itemname)
			if not is_group then
				-- Create item data if it's not there
				local data2 = itemlist[input_itemname]
				if data2 == nil then
					data2 = {
						name = input_itemname,
						recipes_ingredient = {},
						not_complete = true,
					}
					itemlist[input_itemname] = data2
				end
				-- Add this recipe to the item data.
				if tablelib.indexof(data2.recipes_ingredient, recipe) == nil then
					table.insert(data2.recipes_ingredient, recipe)
				end
			else
				-- Add this recipe to the relevant group data, and let existing
				-- members know they are used in a recipe.
				local data2 = get_group_data(groupname)
				if tablelib.indexof(data2.recipes_ingredient, recipe) == nil then
					table.insert(data2.recipes_ingredient, recipe)
					for k, v in pairs(data2.members) do
						if tablelib.indexof(v.recipes_ingredient, recipe) == nil then
							table.insert(v.recipes_ingredient, recipe)
						end
					end
				end
			end
		end
		
		-- Construct recipe information from everything registered to the minetest engine.
		for k,v in pairs(minetest.registered_items) do -- k = item name, v = definition
			if v.groups and (v.groups.not_in_creative_inventory or 0) < 1
				and v.description and v.description ~= "" then
				local data = {
					name = k,
					def = v,
					recipes_result = minetest.get_all_craft_recipes(k) or {},
					recipes_ingredient = ((itemlist[k] ~= nil and itemlist[k].not_complete) and itemlist[k].recipes_ingredient or {}),
					not_complete = nil,
				}
				
				local not_encountered_yet = (itemlist[k] == nil)

				-- Record self in group data.
				for k2, v2 in pairs(v.groups) do -- k2 = group name, v2 = group level
					if v2 ~= nil and v2 > 0 then
						get_group_data(k2).members[k] = data -- Add self
						
						-- Make group membership imply recipe involvement.
						for i, recipe in ipairs(grouplist[k2].recipes_ingredient) do
							add_ingredient_record(k, recipe)
						end
					end
				end
				
				--[[
				If itemlist[k] was nil before the above loop, and if it is
				not nil after the above loop, this means two things:
				- No recipe that requires k exactly was encountered up until
				  this point.
				- k is part of at least one group that is used in a recipe which
				  has been seen before this point.
				At this point, itemlist[k] has a list of recipes that can use k
				as an ingredient due to its group assocations because
				add_ingredient_record creates itemlist[k] if it is nil. However,
				the local variable 'data', which replaces itemlist[k] at the end
				of this loop, still has an empty 'recipes_ingredient' table, and
				we need to update it here or else all of the recipes that use k
				will not be recorded, resulting in no recipes showing up for k
				in-game.
				]]--
				if not_encountered_yet and itemlist[k] then
					data.recipes_ingredient = itemlist[k].recipes_ingredient
				end

				-- Reconstruct fuel recipe in recipes_ingredient if one is around
				local fuel_output_info, fuel_decremented_input =
					minetest.get_craft_result({method = "fuel", width = 1, items = {ItemStack(k)}})
				if fuel_output_info.time > 0 then
					table.insert(data.recipes_ingredient, {
						type = "fuel",
						method = "fuel",
						burntime = fuel_output_info.time,
						items = {k},
						width = 1,
					})
				end
				
				-- Add recipes other items are ingredients of.
				for k2, v2 in pairs(data.recipes_result) do -- v2 = recipe
					for k3,v3 in pairs(v2.items) do
						add_ingredient_record(v3, v2)
					end
				end
				
				-- Add self to catalog
				itemlist[k] = data
			end
		end

		-- Handle recipes which contain input items that must belong to multiple
		-- groups at the same time, such as 'group:dye,color_green'
		for k, v in pairs(database.groups) do
			if string.find(k, ",") then
				local g = string.split(k, ",")
				local first = g[1]
				table.remove(g, 1)

				-- Check if members of the first required group also belong to
				-- the rest of the groups
				for k2, v2 in pairs(database.groups[first].members) do
					local add = true
					for _, v3 in pairs(g) do
						if not v2.def.groups[v3] then
							add = false
							break
						end
					end
					-- If an item is part of every group, then it can be used as
					-- an input item for any recipes that specify that group
					-- combination
					if add then
						v.members[k2] = v2
						for _, v3 in pairs(v.recipes_ingredient) do
							table.insert(v2.recipes_ingredient, v3)
						end
					end
				end
			end
		end

		-- Clean up unfinished item defs.
		for k, v in pairs(itemlist) do
			if v.not_complete then
				itemlist[k] = nil
			end
		end

		for k, v in pairs(database.items) do
			local as_ingredient = v.recipes_ingredient

			table.sort(as_ingredient, function(a, b)
				-- Fuel recipes should be last
				if a.method and a.method == "fuel" then
					return false
				elseif b.method and b.method == "fuel" and
						not (a.method and a.method == "fuel") then
					return true
				end

				-- Recipe output strings can have multiple components such as
				-- the output quantity, but all we want is the name
				local a_name = string.split(a.output, " ", false, 1)[1]
				local b_name = string.split(b.output, " ", false, 1)[1]

				-- Recommended items go before other items
				if mod.recommends[a_name] and not mod.recommends[b_name] then
					return true
				elseif not mod.recommends[a_name] and mod.recommends[b_name] then
					return false
				end
				
				local a_def = minetest.registered_items[a_name]
				local b_def = minetest.registered_items[b_name]

				-- Stairs go after other items since they are so many of them
				local first_stair = a_def.groups and a_def.groups.stair
				local second_stair = b_def.groups and b_def.groups.stair
				if first_stair and not second_stair then
					return false
				elseif second_stair and not first_stair then
					return true
				end

				-- Same with slabs
				local first_slab = a_def.groups and a_def.groups.slab
				local second_slab = b_def.groups and b_def.groups.slab
				if first_slab and not second_slab then
					return false
				elseif second_slab and not first_slab then
					return true
				end

				-- Same with fences
				local first_fence = a_def.groups and a_def.groups.fence
				local second_fence = b_def.groups and b_def.groups.fence
				if first_fence and not second_fence then
				    return false
				elseif second_fence and not first_fence then
				    return true
				end
				
				-- Same with mese posts; this is mainly for when players are
				-- going through recipes that require the mese crystal
				-- There is no group for this though so just use string matching
				local first_post = string.find(a_name, ":mese_post_light")
				local second_post = string.find(b_name, ":mese_post_light")
				if first_post and not second_post then
					return false
				elseif second_post and not first_post then
					return true
				end

				return a_name < b_name
			end)
		end

		mod.database = database
	end
end
function mod.is_group(itemstring)
	assert(type(itemstring) == "string")
	
	local r, r2 = false, nil
	if string.sub(itemstring, 1, 6) == "group:" then
		r, r2 = true, string.sub(assert(itemstring), 7)
	end
	return r, r2
end
function mod.is_visible_to(playercontext, itemname)
	return not mod.progressive_mode or mod.discovery.knows(playercontext.playername, itemname)
end

-- List Processing
function mod.filterby_group(deflist, groupname)
	local r, all = {}, true
	for k, v in pairs(deflist) do
		if (v.def.groups[groupname] or 0) > 0 then
			r[v.name] = v
		else
			all = false
		end
	end
	return (all and deflist or r), all
end
function mod.filterby_deflist(deflist, deflist2)
	-- You can merely use names and booleans key-value-pairs in deflist2.
	local r, all = {}, true
	for k,v in pairs(deflist) do
		if deflist2[v.name] then
			r[v.name] = v
		else
			all = false
		end
	end
	return (all and deflist or r), all
end
function mod.filterby_searchterm(deflist, searchterm)
	local r, all
	local is_group, groupname = mod.is_group(searchterm)
	if is_group then
		r, all = mod.filterby_group(deflist, groupname), false
	else
		r, all = {}, true
		if searchterm ~= nil and searchterm ~= "" then
			searchterm = string.lower(searchterm)
			for k,v in pairs(deflist) do
				local desc = v.def._tt_original_description or
				             v.def.description or ""
				if string.match(string.lower(v.name), searchterm) or
						string.match(string.lower(desc), searchterm) then
					r[v.name] = v
				else all = false
				end
			end
		else
			for k,v in pairs(deflist) do
				r[v.name] = v
			end
		end
	end
	return (all and deflist or r), all
end
function mod.filterby_searchtype(deflist, searchtype, playercontext)
	if type(deflist) ~= "table" then
		error("deflist unexpected type. Expected table.")
	end
	local r
	if searchtype == 1 then
		r = deflist
	elseif searchtype == 2 then
		r = mod.filterby_deflist(deflist, minetest.registered_nodes)
	elseif searchtype == 3 then
		r = mod.filterby_deflist(deflist, minetest.registered_tools)
	elseif searchtype == 4 then
		r = mod.filterby_deflist(deflist, minetest.registered_craftitems)
	elseif searchtype == 5 then -- Only situation where playercontext is required.
		if tablelib.indexof(mod.contexts, playercontext) ~= nil then
			r = {}
			local list = mod.to_named_deflist(deflist)
			local player = minetest.get_player_by_name(playercontext.playername)
			local playerinv = player:get_inventory()
			for i, v in ipairs(playerinv:get_list("main")) do
				if v ~= nil and v:get_count() > 0 and list[v:get_name()] ~= nil then
					table.insert(r, list[v:get_name()])
				end
			end
		else
			error("Can't filter by inventory (5) without knowing whoes inventory to use.")
		end
	else
		error("searchtype is unexpected value.")
	end
	return r
end
function mod.filterby_player(deflist, playercontext)
	local r, all = {}, true
	for k,v in pairs(deflist) do
		if mod.is_visible_to(playercontext, k) then
			r[v.name] = v
		else
			all = false
		end
	end
	return (all and deflist or r), all
end
function mod.to_indexed_deflist(deflist)
	local r = {}
	for k, v in pairs(deflist) do
		table.insert(r, v)
	end
	table.sort(r, function(defa, defb)
		return defa.name < defb.name
	end)
	return r
end
function mod.to_named_deflist(deflist)
	local r = {}
	for k, v in pairs(deflist) do
		r[v.name] = v
	end
	return r
end

minetest.register_on_joinplayer(function(player)
	-- Ensures the craft database is created before a player does anything.
	mod.catalog_items()
	
	-- Set up an environment specific to the player.
	local playername = player:get_player_name()
	mod.contexts[playername] = {
		playername = playername,
		known_items = {}, -- Item name is key and boolean is value
		gui_list_search_term = "",
		gui_list_search_type = 1,
		gui_list_search_cache = nil, -- Indexed
		gui_list_page = 1,
		gui_recipe_item = nil,
		gui_recipe_page = 1,
		gui_recipe_mode = 1, -- 1 = As output, 2 = As ingredient
	}
end)
minetest.register_on_leaveplayer(function(player)
	mod.contexts[player:get_player_name()] = nil
end)
minetest.after(0.01, function()
	mod.catalog_items()
end)
