local set_fs = i3.files.api()
local singleplayer = core.is_singleplayer()

local fmt, search, table_merge, is_group, extract_groups, item_has_groups, apply_recipe_filters =
	i3.need("fmt", "search", "table_merge", "is_group", "extract_groups", "item_has_groups", "apply_recipe_filters")

local POLL_FREQ = 0.25
local HUD_TIMER_MAX = 1.5

local function array_diff(t1, t2)
	local hash = {}

	for i = 1, #t1 do
		local v = t1[i]
		hash[v] = true
	end

	for i = 1, #t2 do
		local v = t2[i]
		hash[v] = nil
	end

	local diff, c = {}, 0

	for i = 1, #t1 do
		local v = t1[i]
		if hash[v] then
			c = c + 1
			diff[c] = v
		end
	end

	return diff
end

local function get_filtered_items(player, data)
	local items, known, c = {}, 0, 0

	for i = 1, #i3.init_items do
		local item = i3.init_items[i]
		local recipes = i3.recipes_cache[item]
		local usages = i3.usages_cache[item]

		recipes = #apply_recipe_filters(recipes or {}, player)
		usages = #apply_recipe_filters(usages or {}, player)

		if recipes > 0 or usages > 0 then
			c = c + 1
			items[c] = item

			if data then
				known = known + recipes + usages
			end
		end
	end

	if data then
		data.known_recipes = known
	end

	return items
end

local function item_in_inv(item, inv_items)
	local inv_items_size = #inv_items

	if is_group(item) then
		local groups = extract_groups(item)

		for i = 1, inv_items_size do
			local def = core.registered_items[inv_items[i]]

			if def then
				if item_has_groups(def.groups, groups) then
					return true
				end
			end
		end
	else
		for i = 1, inv_items_size do
			if inv_items[i] == item then
				return true
			end
		end
	end
end

local function recipe_in_inv(rcp, inv_items)
	for _, item in pairs(rcp.items) do
		if not item_in_inv(item, inv_items) then return end
	end

	return true
end

local function progressive_filter(recipes, player)
	if not recipes then
		return {}
	end

	local name = player:get_player_name()
	local data = i3.data[name]

	if #data.inv_items == 0 then
		return {}
	end

	local filtered, c = {}, 0

	for i = 1, #recipes do
		local recipe = recipes[i]
		if recipe_in_inv(recipe, data.inv_items) then
			c = c + 1
			filtered[c] = recipe
		end
	end

	return filtered
end

local item_lists = {"main", "craft", "craftpreview"}

local function get_inv_items(player)
	local inv = player:get_inventory()
	local stacks = {}

	for i = 1, #item_lists do
		local list = inv:get_list(item_lists[i])
		table_merge(stacks, list)
	end

	local inv_items, c = {}, 0

	for i = 1, #stacks do
		local stack = stacks[i]

		if not stack:is_empty() then
			local name = stack:get_name()
			if core.registered_items[name] then
				c = c + 1
				inv_items[c] = name
			end
		end
	end

	return inv_items
end

local function init_hud(player, data)
	data.hud = {
		bg = player:hud_add {
			hud_elem_type = "image",
			position      = {x = 0.78, y = 1},
			alignment     = {x = 1,    y = 1},
			scale         = {x = 370,  y = 112},
			text          = "i3_bg.png",
			z_index       = 0xDEAD,
		},

		book = player:hud_add {
			hud_elem_type = "image",
			position      = {x = 0.79, y = 1.02},
			alignment     = {x = 1,    y = 1},
			scale         = {x = 4,    y = 4},
			text          = "i3_book.png",
			z_index       = 0xDEAD,
		},

		text = player:hud_add {
			hud_elem_type = "text",
			position      = {x = 0.84, y = 1.04},
			alignment     = {x = 1,    y = 1},
			number        = 0xffffff,
			text          = "",
			z_index       = 0xDEAD,
			style         = 1,
		},
	}
end

local function show_hud_success(player, data)
	-- It'd better to have an engine function `hud_move` to only need
	-- 2 calls for the notification's back and forth.

	local hud_info_bg = player:hud_get(data.hud.bg)
	local dt = 0.016

	if hud_info_bg.position.y <= 0.9 then
		data.show_hud = false
		data.hud_timer = (data.hud_timer or 0) + dt
	end

	if data.show_hud then
		for _, def in pairs(data.hud) do
			local hud_info = player:hud_get(def)

			player:hud_change(def, "position", {
				x = hud_info.position.x,
				y = hud_info.position.y - (dt / 5)
			})
		end

		player:hud_change(data.hud.text, "text",
			fmt("%u new recipe%s unlocked!", data.discovered, data.discovered > 1 and "s" or ""))

	elseif data.show_hud == false then
		if data.hud_timer >= HUD_TIMER_MAX then
			for _, def in pairs(data.hud) do
				local hud_info = player:hud_get(def)

				player:hud_change(def, "position", {
					x = hud_info.position.x,
					y = hud_info.position.y + (dt / 5)
				})
			end

			if hud_info_bg.position.y >= 1 then
				data.show_hud = nil
				data.hud_timer = nil
			end
		end
	end
end

-- Workaround. Need an engine call to detect when the contents of
-- the player inventory changed, instead.
local function poll_new_items()
	local players = core.get_connected_players()

	for i = 1, #players do
		local player = players[i]
		local name = player:get_player_name()
		local data = i3.data[name]
		if not data then return end

		local inv_items = get_inv_items(player)
		local diff = array_diff(inv_items, data.inv_items)

		if #diff > 0 then
			data.inv_items = table_merge(diff, data.inv_items)
			local oldknown = data.known_recipes or 0
			local items = get_filtered_items(player, data)
			data.discovered = data.known_recipes - oldknown

			if data.show_hud == nil and data.discovered > 0 then
				data.show_hud = true
			end

			data.items_raw = items
			data.current_itab = 1

			search(data)
			set_fs(player)
		end
	end

	core.after(POLL_FREQ, poll_new_items)
end

poll_new_items()

if singleplayer then
	core.register_globalstep(function()
		local name = "singleplayer"
		local player = core.get_player_by_name(name)
		local data = i3.data[name]

		if data and data.show_hud ~= nil then
			show_hud_success(player, data)
		end
	end)
end

i3.add_recipe_filter("Default progressive filter", progressive_filter)

core.register_on_joinplayer(function(player)
	local name = player:get_player_name()
	local data = i3.data[name]
	if not data then return end

	data.inv_items = data.inv_items or {}
	data.known_recipes = data.known_recipes or 0

	local oldknown = data.known_recipes
	local items = get_filtered_items(player, data)
	data.discovered = data.known_recipes - oldknown

	data.items_raw = items
	search(data)

	if singleplayer then
		init_hud(player, data)

		if data.show_hud == nil and data.discovered > 0 then
			data.show_hud = true
		end
	end
end)
