local vec = vector.new
local ItemStack = ItemStack
local loadstring = loadstring
local reg_items = core.registered_items
local translate = core.get_translated_string
local sort, concat, insert = table.sort, table.concat, table.insert
local min, floor, ceil = math.min, math.floor, math.ceil
local fmt, find, match, gmatch, sub, split, lower, upper =
	string.format, string.find, string.match, string.gmatch,
	string.sub, string.split, string.lower, string.upper

if not core.registered_privileges.creative then
	core.register_privilege("creative", {
		description = "Allow player to use creative inventory",
		give_to_singleplayer = false,
		give_to_admin = false,
	})
end

local old_is_creative_enabled = core.is_creative_enabled

function core.is_creative_enabled(name)
	if name == "" then
		return old_is_creative_enabled(name)
	end

	return core.check_player_privs(name, {creative = true}) or old_is_creative_enabled(name)
end

local S = core.get_translator"i3"
local FS = function(...) return core.formspec_escape(S(...)) end

local function is_num(x)
	return type(x) == "number"
end

local function is_str(x)
	return type(x) == "string"
end

local function is_table(x)
	return type(x) == "table"
end

local function is_func(x)
	return type(x) == "function"
end

local function true_str(str)
	return is_str(str) and str ~= ""
end

local function true_table(x)
	return is_table(x) and next(x)
end

local function reset_compression(data)
	data.alt_items = nil
	data.expand = ""
end

local function msg(name, str)
	local prefix = "[i3]"
	return core.chat_send_player(name, fmt("%s %s", core.colorize("#ff0", prefix), str))
end

local function err(str)
	return core.log("error", str)
end

local function round(num, decimal)
	local mul = 10 ^ decimal
	return floor(num * mul + 0.5) / mul
end

local function toupper(str)
	return str:gsub("%f[%w]%l", upper):gsub("_", " ")
end

local function utf8_len(str)
	local c = 0

	for _ in str:gmatch"[%z\1-\127\194-\244][\128-\191]*" do -- Arguably working duct-tape code
		c++
	end

	return c
end

local function get_bag_description(data, stack)
	local desc = translate(data.lang_code, stack:get_description())
	      desc = split(desc, "(")[1] or desc
	      desc = toupper(desc:trim())

	return desc
end

local function search(data)
	reset_compression(data)

	local filter = data.filter
	local opt = "^(.-)%+([%w_]+)=([%w_,]+)"
	local search_filter = next(i3.search_filters) and match(filter, opt)
	local filters = {}

	if search_filter then
		search_filter = search_filter:trim()

		for filter_name, values in gmatch(filter, sub(opt, 6)) do
			if i3.search_filters[filter_name] then
				values = split(values, ",")
				filters[filter_name] = values
			end
		end
	end

	local filtered_list, c = {}, 0

	for i = 1, #data.items_raw do
		local item = data.items_raw[i]
		local def = reg_items[item]
		local desc = lower(translate(data.lang_code, def.description)) or ""
		local search_in = fmt("%s %s", item, desc)
		local temp, j, to_add = {}, 1

		if search_filter then
			for filter_name, values in pairs(filters) do
				if values then
					local func = i3.search_filters[filter_name]
					to_add = (j > 1 and temp[item] or j == 1) and
						func(item, values) and (search_filter == "" or
						find(search_in, search_filter, 1, true))

					if to_add then
						temp[item] = true
					end

					j++
				end
			end
		else
			local ok = true

			for keyword in gmatch(filter, "%S+") do
				if not find(search_in, keyword, 1, true) then
					ok = nil
					break
				end
			end

			if ok then
				to_add = true
			end
		end

		if to_add then
			c++
			filtered_list[c] = item
		end
	end

	data.items = filtered_list
end

local function table_replace(t, val, new)
	for k, v in pairs(t) do
		if v == val then
			t[k] = new
		end
	end
end

local function table_merge(t1, t2, hash)
	t1 = t1 or {}
	t2 = t2 or {}

	if hash then
		for k, v in pairs(t2) do
			t1[k] = v
		end
	else
		local c = #t1

		for i = 1, #t2 do
			c++
			t1[c] = t2[i]
		end
	end

	return t1
end

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++
			diff[c] = v
		end
	end

	return diff
end

local function table_eq(t1, t2)
	local ty1, ty2 = type(t1), type(t2)
	if ty1 ~= ty2 then return end

	if ty1 ~= "table" and ty2 ~= "table" then
		return t1 == t2
	end

	for k, v in pairs(t1) do
		local v2 = t2[k]
		if v2 == nil or not table_eq(v, v2) then return end
	end

	for k, v in pairs(t2) do
		local v1 = t1[k]
		if v1 == nil or not table_eq(v1, v) then return end
	end

	return true
end

local function clean_name(item)
	if sub(item, 1, 1) == ":" or sub(item, 1, 1) == " " or sub(item, 1, 1) == "_" then
		item = sub(item, 2)
	end

	return item
end

local function is_group(item)
	return sub(item, 1, 6) == "group:"
end

local function extract_groups(str)
	return split(sub(str, 7), ",")
end

local function item_has_groups(item_groups, groups)
	for i = 1, #groups do
		local group = groups[i]
		if (item_groups[group] or 0) == 0 then return end
	end

	return true
end

local function valid_item(def)
	return def and def.groups.not_in_creative_inventory ~= 1 and
		def.description and def.description ~= ""
end

local function get_group_stereotype(group)
	local stereotype = i3.group_stereotypes[group]
	local def = reg_items[stereotype]

	if valid_item(def) then
		return stereotype
	end
end

local function groups_to_items(groups)
	local names = {}

	for name, def in pairs(reg_items) do
		if valid_item(def) and item_has_groups(def.groups, groups) then
			insert(names, name)
		end
	end

	sort(names)

	return names
end

local function is_cube(drawtype)
	return drawtype == "normal" or drawtype == "liquid" or
		sub(drawtype, 1, 9) == "glasslike" or
		sub(drawtype, 1, 8) == "allfaces"
end

local function get_cube(tiles)
	if not true_table(tiles) then
		return "i3_blank.png"
	end

	local top = tiles[1] or "i3_blank.png"
	if is_table(top) then
		top = top.name or top.image
	end

	local left = tiles[3] or top or "i3_blank.png"
	if is_table(left) then
		left = left.name or left.image
	end

	local right = tiles[5] or left or "i3_blank.png"
	if is_table(right) then
		right = right.name or right.image
	end

	return core.inventorycube(top, left, right)
end

local function apply_recipe_filters(recipes, player)
	for _, filter in pairs(i3.recipe_filters) do
		recipes = filter(recipes, player)
	end

	return recipes
end

local function recipe_filter_set()
	return next(i3.recipe_filters)
end

local function compression_active(data)
	return data.collapse and not recipe_filter_set() and data.filter == ""
end

local function compressible(item, data)
	return compression_active(data) and i3.compress_groups[item]
end

local function is_fav(data)
	for i = 1, #data.favs do
		if data.favs[i] == data.query_item then
			return i
		end
	end
end

local function sort_by_category(data)
	reset_compression(data)
	local items = data.items_raw

	if data.filter ~= "" then
		search(data)
		items = data.items
	end

	local new = {}

	for i = 1, #items do
		local item = items[i]
		local tab = i3.minitabs[data.itab]
		local to_add = tab.sorter(item, data)

		if to_add then
			insert(new, item)
		end
	end

	data.items = new
end

local function spawn_item(player, stack)
	local dir     = player:get_look_dir()
	local ppos    = player:get_pos()
	      ppos.y  = ppos.y + player:get_properties().eye_height
	local look_at = ppos + dir

	core.add_item(look_at, stack)
end

local function get_recipes(player, item)
	item = core.registered_aliases[item] or item
	local recipes = i3.recipes_cache[item]
	local usages = i3.usages_cache[item]

	if recipes then
		recipes = apply_recipe_filters(recipes, player)
	end

	local no_recipes = not recipes or #recipes == 0
	if no_recipes and not usages then return end
	usages = apply_recipe_filters(usages, player)
	local no_usages = not usages or #usages == 0

	return not no_recipes and recipes or nil,
	       not no_usages  and usages  or nil
end

local function get_stack(player, stack)
	local inv = player:get_inventory()

	if inv:room_for_item("main", stack) then
		inv:add_item("main", stack)
	else
		spawn_item(player, stack)
	end
end

local function get_group_items(name)
	local groups = extract_groups(name)
	return i3.groups[name:sub(7)].items or groups_to_items(groups)
end

local function craft_stack(player, data, craft_rcp)
	local inv = player:get_inventory()
	local rcp_usg = craft_rcp and "recipe" or "usage"
	local rcp_def = rcp_usg == "recipe" and data.recipes[data.rnum] or data.usages[data.unum]
	local output = craft_rcp and data.recipes[data.rnum].output or data.usages[data.unum].output
	      output = ItemStack(output)
	local stackname, stackcount, stackmax = output:get_name(), output:get_count(), output:get_stack_max()
	local scrbar_val = data[fmt("scrbar_%s", craft_rcp and "rcp" or "usg")] or 1

	for name, count in pairs(data.crafting_counts[rcp_usg].rcp) do
		local items = {[name] = count}

		if is_group(name) then
			items = {}
			local item_groups = get_group_items(name)
			local remaining = count

			for _, item in ipairs(item_groups) do
				for _name, _count in pairs(data.crafting_counts[rcp_usg].inv) do
					if item == _name and remaining > 0 then
						local c = min(remaining, _count)
						items[item] = c
						remaining -= c
					end

					if remaining == 0 then break end
				end
			end
		end

		for item, v in pairs(items) do
			for _ = 1, v * scrbar_val do
				inv:remove_item("main", item)

				for _, pair in ipairs(rcp_def.replacements or {}) do
					local old_name, new_name = unpack(pair)

					if is_group(old_name) then
						local item_groups = get_group_items(old_name)

						for _, it in ipairs(item_groups) do
							if item == it then
								get_stack(player, ItemStack(new_name))
							end
						end
					elseif item == old_name then
						get_stack(player, ItemStack(new_name))
					end
				end
			end
		end
	end

	local count = stackcount * scrbar_val
	local iter = ceil(count / stackmax)
	local leftover = count

	for _ = 1, iter do
		local c = min(stackmax, leftover)
		local stack = ItemStack(fmt("%s %s", stackname, c))
		get_stack(player, stack)
		leftover -= stackmax
	end
end

local function play_sound(name, sound, volume)
	core.sound_play(sound, {to_player = name, gain = volume}, true)
end

local function safe_teleport(player, pos)
	local name = player:get_player_name()
	play_sound(name, "i3_teleport", 0.8)

	local vel = player:get_velocity()
	player:add_velocity(-vel)

	local p = vec(pos)
	      p.y += 0.25

	player:set_pos(p)
end

local function sorter(inv, data, mode)
	sort(inv, function(a, b)
		if mode == 1 then
			a = translate(data.lang_code, a:get_short_description())
			b = translate(data.lang_code, b:get_short_description())
		else
			a, b = a:get_count(), b:get_count()
		end

		if data.reverse_sorting then
			return a > b
		end

		return a < b
	end)
end

local function pre_sorting(list, start_i)
	local new_inv, special = {}, {}

	for i = start_i, #list do
		local stack = list[i]
		local empty = stack:is_empty()
		local meta  = stack:get_meta():to_table()
		local wear  = stack:get_wear() > 0

		if not empty then
			if next(meta.fields) or wear then
				insert(special, stack)
			else
				insert(new_inv, stack)
			end
		end
	end

	new_inv = table_merge(new_inv, special)
	return new_inv
end

local function compress_items(list, start_i)
	local hash, new_inv, special = {}, {}, {}

	for i = start_i, #list do
		local stack    = list[i]
		local name     = stack:get_name()
		local count    = stack:get_count()
		local stackmax = stack:get_stack_max()
		local empty    = stack:is_empty()
		local meta     = stack:get_meta():to_table()
		local wear     = stack:get_wear() > 0

		if not empty then
			if next(meta.fields) or wear or count >= stackmax then
				insert(special, stack)
			else
				hash[name] = hash[name] or 0
				hash[name] += count
			end
		end
	end

	for name, count in pairs(hash) do
		local stackmax = ItemStack(name):get_stack_max()
		local iter = ceil(count / stackmax)
		local leftover = count

		for _ = 1, iter do
			insert(new_inv, ItemStack(fmt("%s %u", name, min(stackmax, leftover))))
			leftover -= stackmax
		end
	end

	new_inv = table_merge(new_inv, special)
	return new_inv
end

local function sort_inventory(player, data)
	local inv = player:get_inventory()
	local list = inv:get_list"main"
	local size = inv:get_size"main"
	local start_i = data.ignore_hotbar and (data.hotbar_len + 1) or 1

	list = data.inv_compress and compress_items(list, start_i) or pre_sorting(list, start_i)

	local new_inv = i3.sorting_methods[data.sort].func(list, data)
	if not new_inv then return end

	if not data.ignore_hotbar then
		inv:set_list("main", new_inv)
		return
	end

	for i = start_i, size do
		local index = i - start_i + 1
		inv:set_stack("main", i, new_inv[index] or "")
	end
end

local function reset_data(data)
	data.filter        = ""
	data.expand        = ""
	data.pagenum       = 1
	data.rnum          = 1
	data.unum          = 1
	data.scrbar_rcp    = 1
	data.scrbar_usg    = 1
	data.query_item    = nil
	data.enable_search = nil
	data.goto_page     = nil
	data.recipes       = nil
	data.usages        = nil
	data.crafting_rcp  = nil
	data.crafting_usg  = nil
	data.alt_items     = nil
	data.confirm_trash = nil
	data.show_settings = nil
	data.show_setting  = "home"
	data.items         = data.items_raw

	if data.itab and data.itab > 1 then
		sort_by_category(data)
	end
end

local function add_hud_waypoint(player, name, pos, color, image)
	return player:hud_add {
		hud_elem_type = image and "image_waypoint" or "waypoint",
		name = name,
		text = image or "m",
		scale = {x = 5, y = 5},
		world_pos = pos,
		number = color,
		image = image,
		z_index = -300,
	}
end

local function init_hud_notif(player)
	return {
		bg = player:hud_add {
			hud_elem_type = "image",
			position      = {x = 0,   y = 1},
			offset        = {x = 10,  y = 0},
			alignment     = {x = 1,   y = 1},
			scale         = {x = 0.6, y = 0.6},
			text          = "i3_bg_notif.png",
			z_index       = 0xDEAD,
		},

		img = player:hud_add {
			hud_elem_type = "image",
			position      = {x = 0,  y = 1},
			offset        = {x = 20, y = 20},
			alignment     = {x = 1,  y = 1},
			scale         = {x = 1,  y = 1},
			text          = "",
			z_index       = 0xDEAD,
		},

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

local function get_detached_inv(name, player_name)
	return core.get_inventory {
		type = "detached",
		name = fmt("i3_%s_%s", name, player_name)
	}
end

local function update_inv_size(player, data)
	data.hotbar_len = data.legacy_inventory and 8 or 9
	data.inv_size = 4 * data.hotbar_len

	local inv = player:get_inventory()
	inv:set_size("main", data.inv_size)

	player:hud_set_hotbar_itemcount(data.hotbar_len)

	core.after(0, function()
		player:hud_set_hotbar_image(data.legacy_inventory and (core.global_exists("default") and "gui_hotbar.png" or "") or "i3_hotbar.png")
	end)
end

-- Much faster implementation of `unpack`
local function createunpack(n)
	local ret = {"local t = ... return "}

	for k = 2, n do
		ret[2 + (k - 2) * 4] = "t["
		ret[3 + (k - 2) * 4] = k - 1
		ret[4 + (k - 2) * 4] = "]"

		if k ~= n then
			ret[5 + (k - 2) * 4] = ","
		end
	end

	return loadstring(concat(ret))
end

local newunpack = createunpack(33)

-------------------------------------------------------------------------------

local _ = {
	-- Groups
	is_group = is_group,
	extract_groups = extract_groups,
	item_has_groups = item_has_groups,
	groups_to_items = groups_to_items,
	get_group_stereotype = get_group_stereotype,

	-- Compression
	compressible = compressible,
	compression_active = compression_active,

	-- Sorting
	search = search,
	sorter = sorter,
	get_recipes = get_recipes,
	sort_inventory = sort_inventory,
	sort_by_category = sort_by_category,
	recipe_filter_set = recipe_filter_set,
	apply_recipe_filters = apply_recipe_filters,

	-- Type checks
	is_fav = is_fav,
	is_str = is_str,
	is_num = is_num,
	is_func = is_func,
	true_str = true_str,
	true_table = true_table,

	-- Console
	err = err,
	msg = msg,

	-- Misc. functions
	is_cube = is_cube,
	get_cube = get_cube,
	ItemStack = ItemStack,
	valid_item = valid_item,
	spawn_item = spawn_item,
	clean_name = clean_name,
	play_sound = play_sound,
	reset_data = reset_data,
	safe_teleport = safe_teleport,
	init_hud_notif = init_hud_notif,
	add_hud_waypoint = add_hud_waypoint,

	-- Core functions
	clr = core.colorize,
	slz = core.serialize,
	dslz = core.deserialize,
	ESC = core.formspec_escape,
	draw_cube = core.inventorycube,
	get_group = core.get_item_group,
	pos_to_str = core.pos_to_string,
	str_to_pos = core.string_to_pos,
	check_privs = core.check_player_privs,
	get_player_by_name = core.get_player_by_name,
	get_connected_players = core.get_connected_players,

	-- Inventory
	get_stack = get_stack,
	craft_stack = craft_stack,
	update_inv_size = update_inv_size,
	get_detached_inv = get_detached_inv,
	get_bag_description = get_bag_description,
	create_inventory = core.create_detached_inventory,

	-- Registered items
	reg_items = core.registered_items,
	reg_nodes = core.registered_nodes,
	reg_tools = core.registered_tools,
	reg_aliases = core.registered_aliases,
	reg_entities = core.registered_entities,
	reg_craftitems = core.registered_craftitems,

	-- i18n
	S = S,
	FS = FS,
	translate = core.get_translated_string,

	-- String
	sub = string.sub,
	find = string.find,
	fmt = string.format,
	upper = string.upper,
	lower = string.lower,
	split = string.split,
	match = string.match,
	gmatch = string.gmatch,
	toupper = toupper,
	utf8_len = utf8_len,

	-- Table
	maxn = table.maxn,
	sort = table.sort,
	copy = table.copy,
	concat = table.concat,
	insert = table.insert,
	remove = table.remove,
	indexof = table.indexof,
	unpack = newunpack,
	is_table = is_table,
	table_merge = table_merge,
	table_replace = table_replace,
	table_eq = table_eq,
	array_diff = array_diff,

	-- Math
	round = round,
	abs = math.abs,
	min = math.min,
	max = math.max,
	ceil = math.ceil,
	floor = math.floor,
	random = math.random,

	-- Vectors
	vec = vector.new,
	vec_round = vector.round,
}

function i3.get(...)
	local t = {}

	for i, var in ipairs{...} do
		t[i] = _[var]
	end

	return newunpack(t)
end
