
local mer = fluidity.external.ref
local S = core.get_translator("melterns")

tool_station = {}

local tool_list_cache = nil
function tool_station.get_tool_type_list(ix, iy, mx)
	local formspec = ""
	local x        = 0
	local y        = 0

	formspec = formspec..("button[%f,%f;1,1;anvil;" .. S("Anvil") .. "]"):format(x + ix, y + iy)
	x = x + 1.25

	for t, tool in pairs(tinkering.tools) do
		local toolmod = tool.mod_name or "tinkering"
		formspec = formspec.. ("item_image_button[%f,%f;1,1;%s;%s;]"):format(x + ix, y + iy, toolmod..":steel_"..t, t)
		formspec = formspec.. ("tooltip[%s;%s]"):format(t, tool.description)
		x = x + 1.25
		if x >= mx then
			y = y + 1.25
			x = 0
		end
	end

	return formspec
end

function tool_station.get_formspec(comp_list)
	if not tool_list_cache then
		tool_list_cache = tool_station.get_tool_type_list(11.75, 0.375, 6.25)
	end

	local w = 1
	local h = 0

	local x = 2.125
	local y = 0.75

	local til = ""

	if comp_list then
		for _,comp in pairs(comp_list) do
			local img = tinkering.components[comp].image .. "^[colorize:#1e1e1e:255"
			til = til .. "image[" .. x .. "," .. y .. ";1,1;".. img .. "]"
			y = y + 1.25
			h = h + 1

			if y > 3.25 then
				y = 0
				x = x + 1.25
			end

			if h > 3 then
				h = 3
				w = w + 1
			end
		end
	else
		h = 3
		w = 3
	end

	return "formspec_version[4]size[18,10.45]"..
		"label[0.375,0.375;"..S("Tool Station").."]"..
		mer.get_itemslot_bg(2.125, 0.75, w, h) ..
		til..
		"list[context;input;2.125,0.75;" .. w .. "," .. h .. ";]"..
		mer.get_itemslot_bg(7.125, 2, 1, 1) ..
		"list[context;output;7.125,2;1,1;]"..
		"image[5.875,2;1,1;"..mer.gui_furnace_arrow.."^[transformR270]"..
		mer.gui_player_inv()..
		tool_list_cache..
		"listring[current_player;main]"..
		"listring[context;input]"..
		"listring[current_player;main]"..
		"listring[context;output]"..
		"listring[current_player;main]"
end

local function get_metalgroup(groups)
	if not groups then return nil end
	for g in pairs(groups) do
		if g:find("material_") == 1 then
			return g:gsub("^material_", "")
		end
	end
	return nil
end

-- Get tool components from specified stacks
function tool_station.get_types(list, tool_type)
	local tool = tinkering.tools[tool_type]
	if not tool then return nil end

	local result = {}
	local items_required = {}
	local components = {}

	for _,stack in pairs(list) do
		if not result then break end
		local stack_name = stack:get_name()
		for tt, ty in pairs(tool.components) do
			if not result then break end
			local in_grp = core.get_item_group(stack_name, "tc_"..ty) > 0
			if in_grp then
				if components[tt] == nil then
					local mtg = get_metalgroup(core.registered_items[stack_name].groups)
					if mtg ~= nil then
						result[tt] = mtg

						if not items_required[stack_name] then
							items_required[stack_name] = 0
						end

						items_required[stack_name] = items_required[stack_name] + 1
						components[tt] = true
					end
				else
					-- Don't allow multiple components of the same type to avoid confusion
					result = nil
					items_required = nil
					components = {}
					break
				end
			end
		end
	end

	return result, items_required
end

function tool_station.get_tool(list)
	local tool_fnd  = nil
	local tool_type = nil
	for _,stack in pairs(list) do
		local stack_name = stack:get_name()
		if core.get_item_group(stack_name, "tinker_tool") > 0 then
			if tool_fnd == nil then
				local itemdef = stack:get_definition()
				if itemdef._is_broken then
					local broken_stack = stack
					stack = ItemStack(itemdef._unbroken_name)
					local meta = stack:get_meta()
					meta:from_table(broken_stack:get_meta():to_table())
					meta:set_string("description", meta:get_string("description_non_broken"))
					meta:set_string("description_non_broken", "")
					meta:set_tool_capabilities(core.deserialize(meta:get_string("capabilities_non_broken"), true))
					meta:set_string("capabilities_non_broken", "")
					stack:set_wear(65535)
				end
				for t in pairs(tinkering.tools) do
					if core.get_item_group(stack_name, "tinker_"..t) > 0 then
						tool_type = t
						break
					end
				end
				tool_fnd = stack
			else
				-- Don't allow multiple tools in the repair grid at the same time to avoid confusion
				tool_fnd = nil
				break
			end
		end
	end
	return tool_fnd, tool_type
end

local function decode_meta(s)
	local t = {}
	for k, v in string.gmatch(s, "(%w+)=(%w+)") do
		t[k] = v
	end
	return t
end

local function find_material(stack)
	-- Meltables
	for metal,list in pairs(fluidity.melts) do
		for type,stacks in pairs(list) do
			for _,st in pairs(stacks) do
				if st == stack then
					return metal, type
				end
			end
		end
	end

	-- Grouped
	for mat,iv in pairs(tinkering.materials) do
		if iv.base == "group" and core.get_item_group(stack, iv.default) > 0 then
			return mat, "block"
		elseif stack == iv.default then
			return mat, "ingot"
		end
	end

	-- Modifiers
	for name,iv in pairs(tinkering.modifiers) do
		if iv.base == "group" and core.get_item_group(stack, iv.default) > 0 then
			return name, "modifier"
		elseif stack == iv.default then
			return name, "modifier"
		end
	end

	return nil
end

local function get_materials_in_list(list, skip)
	local result = {}
	for _,stack in pairs(list) do
		local stack_name = stack:get_name()
		if stack_name ~= "" and stack_name ~= skip then
			local material, type = find_material(stack_name)
			if material then
				if result[material] then
					result[material].count = result[material].count + stack:get_count()
				else
					result[material] = {stack = stack_name, type = type, count = stack:get_count()}
				end
			end
		end
	end

	return result
end

local function match_materials(list1, materials)
	local matches = {}
	for name,type in pairs(materials) do
		if list1[type] then
			matches[type] = list1[type]
		end
	end

	-- Return nothing if there are materials not suitable
	for name in pairs(list1) do
		if not matches[name] then
			matches = {}
			break
		end
	end
	return matches
end

local function take_from_list(list, item, list2)
	local item_broken = item .. "_broken"
	for _,stack in pairs(list) do
		local stack_name = stack:get_name()
		if stack_name == item or stack_name == item_broken then
			stack:clear()
		elseif list2[stack_name] then
			if list2[stack_name] > stack:get_count() then
				list2[stack_name] = list2[stack_name] - stack:get_count()
				stack:clear()
			else
				stack:set_count(stack:get_count() - list2[stack_name])
				list2[stack_name] = 0
			end
		end
	end
	return list
end

local function can_apply_modifier(mod_list, mod_name, max_mods)
	if max_mods == 0 then
		return 0
	end

	local can_apply_count = 0
	local definition = tinkering.modifiers[mod_name].modifier

	local has_mod = false
	for _,minfo in pairs(mod_list) do
		if minfo.name == mod_name then
			has_mod = true
			if minfo.count < definition.count then
				can_apply_count = definition.count - minfo.count
				break
			end
		end
	end

	-- If the modifier can only be added once, prevent it from being added to additional slots
	if has_mod and definition.count == 1 then
		return 0
	end

	-- If the mod list is empty, or there are still modifier slots available,
	-- return the full count.
	if #mod_list == 0 or (can_apply_count == 0 and #mod_list < max_mods) then
		return definition.count
	end

	return can_apply_count
end

local function apply_modifier(mod_list, mod_name, count)
	local leftover = count
	local definition = tinkering.modifiers[mod_name].modifier
	for _,minfo in pairs(mod_list) do
		if minfo.name == mod_name then
			local new_count = minfo.count + count
			if new_count > definition.count then
				leftover = new_count - definition.count
				new_count = definition.count
			end

			minfo.count = new_count
		end
	end

	if leftover > 0 then
		table.insert(mod_list, {
			name = mod_name,
			count = leftover
		})
	end

	return mod_list
end

local function material_list_to_modifiers(list, mod_list, max_mods)
	local modified = false
	local complete_list = table.copy(mod_list)
	local materials_to_take = {}

	for mat, stat in pairs(list) do
		for name in pairs(tinkering.modifiers) do
			if mat == name then
				local can_apply_count = can_apply_modifier(complete_list, name, max_mods)
				if can_apply_count > 0 then
					local m_count = math.min(stat.count, can_apply_count)
					complete_list = apply_modifier(complete_list, name, m_count)
					materials_to_take[stat.stack] = m_count
					modified = true
				end
			end
		end
	end

	return modified, complete_list, materials_to_take
end

local function handle_take_output(pos, listname)
	local meta = core.get_meta(pos)
	local inv  = meta:get_inventory()

	local tooltype = meta:get_string("tool_type")
	local list     = inv:get_list(listname)

	if tooltype ~= "" then
		local types, items = tool_station.get_types(list, tooltype)
		if not types then return end
		local res = {}

		for _,stack in pairs(list) do
			local stack_name = stack:get_name()
			if items[stack_name] then
				if not res[stack_name] then
					res[stack_name] = items[stack_name]
				end

				if res[stack_name] > 0 then
					if stack:get_count() > res[stack_name] then
						stack:set_count(stack:get_count() - res[stack_name])
						res[stack_name] = 0
					else
						res[stack_name] = res[stack_name] - stack:get_count()
						stack:clear()
					end
				end
			end
		end

		inv:set_list(listname, list)
	else
		local tool, tool_type_ = tool_station.get_tool(list)
		if tool then
			local comp_mats = tool:get_meta():get_string("materials")
			if comp_mats and comp_mats ~= "" then
				local materials = decode_meta(comp_mats)
				-- Material list found, now we can start doing repair work, replacing a component or applying modifiers
				local mat_grid = get_materials_in_list(list, tool:get_name())
				local modifiers, max_modifiers = tinkering.read_tool_modifiers(tool)
				local modified, _, to_take = material_list_to_modifiers(mat_grid, modifiers, max_modifiers)

				-- Find components to remove
				local for_removal = {}
				local removed_types = {}
				local repair = true
				local tool_comps = tinkering.tools[tool_type_].components
				for mat, stat in pairs(mat_grid) do
					for name, comp in pairs(tool_comps) do
						if stat.type == comp and not removed_types[comp] then
							for_removal[stat.stack] = 1
							removed_types[comp] = true
							repair = false
						end
					end
				end

				if modified then
					inv:set_list(listname, take_from_list(list, tool:get_name(), to_take))
					repair = true
				end

				if not modified and not repair then
					inv:set_list(listname, take_from_list(list, tool:get_name(), for_removal))
				end

				if not modified and tool:get_wear() ~= 0 and repair then
					local matches = match_materials(mat_grid, materials)
					local repair_cap = 0
					for mat, stat in pairs(matches) do
						repair_cap = repair_cap + math.min(stat.count, 3)
					end

					if repair_cap > 0 then
						local _take = 1
						for i = 1, repair_cap do
							local tool_wear = 65535 - tool:get_wear()
							local repair_cnt = (0.33 * 65535) * i
							local new_wear = 65535 - (tool_wear + repair_cnt)
							if new_wear > 0 then
								_take = _take + 1
							end
						end

						local to_take = {}
						local exch = _take

						for type, c in pairs(matches) do
							if not to_take[c.stack] then to_take[c.stack] = 0 end
							if c.count < exch then
								to_take[c.stack] = to_take[c.stack] + c.count
								exch = exch - 1
							else
								to_take[c.stack] = to_take[c.stack] + exch
								break
							end
						end

						inv:set_list(listname, take_from_list(list, tool:get_name(), to_take))
					end
				end
			end
		end
	end
end

local function on_timer(pos, elapsed)
	local meta    = core.get_meta(pos)
	local inv     = meta:get_inventory()
	local refresh = false

	local output = nil

	-- Get selected tool type
	local tool_type = meta:get_string("tool_type")
	local list      = inv:get_list("input")

	if tool_type ~= "" then
		local results = tool_station.get_types(list, tool_type)
		if results then
			-- Attempt to create the tool with the provided materials
			local tool_res = tinkering.create_tool(tool_type, results, true)
			if tool_res then
				output = tool_res
			end
		end
		meta:set_string("formspec", tool_station.get_formspec(tinkering.tools[tool_type].components))
	else
		local tool, tool_type_ = tool_station.get_tool(list)
		if tool then
			local comp_mats = tool:get_meta():get_string("materials")
			if comp_mats and comp_mats ~= "" then
				local materials = decode_meta(comp_mats)
				-- Material list found, now we can start doing repair work, replacing a component or applying modifiers
				local mat_grid = get_materials_in_list(list, tool:get_name())
				local modifiers, max_modifiers = tinkering.read_tool_modifiers(tool)
				local modified, new_modifiers, to_take = material_list_to_modifiers(mat_grid, modifiers, max_modifiers)

				-- Find components to replace
				local comp_repl = {}
				local repair = true
				local tool_comps = tinkering.tools[tool_type_].components
				for mat, stat in pairs(mat_grid) do
					if comp_repl == nil then break end
					for name, comp in pairs(tool_comps) do
						if stat.type == comp then
							if comp_repl[name] then
								-- Dont allow multiple of the same component to avoid confusion
								comp_repl = nil
								break
							else
								comp_repl[name] = mat
							end
							repair = false
						end
					end
				end

				if modified then
					local tool_res = tinkering.create_tool(
						tool_type_,
						materials,
						true,
						nil,
						{
							wear = tool:get_wear(),
							initial_metadata = tool:get_meta():to_table()
						},
						new_modifiers
					)
					if tool_res then
						output = tool_res
					end
				end

				if not modified and not repair and comp_repl then
					-- Add non-replacement materials back
					for i,v in pairs(materials) do
						if not comp_repl[i] then
							comp_repl[i] = v
						end
					end

					local tool_res = tinkering.create_tool(
						tool_type_,
						comp_repl,
						true,
						nil,
						{
							wear = tool:get_wear(),
							initial_metadata = tool:get_meta():to_table()
						},
						modifiers
					)
					if tool_res then
						output = tool_res
					end
				end

				-- Attempt to repair tool with provided items
				if not modified and tool:get_wear() ~= 0 and repair then
					local matches = match_materials(mat_grid, materials)
					local repair_cap = 0
					for mat, stat in pairs(matches) do
						repair_cap = repair_cap + math.min(stat.count, 3)
					end

					if repair_cap > 0 then
						local tool_wear = 65535 - tool:get_wear()
						local repair_cnt = (0.33 * 65535) * repair_cap
						local new_wear = 65535 - (tool_wear + repair_cnt)

						if new_wear < 0 then
							new_wear = 0
						end

						local modifiers = tinkering.read_tool_modifiers(tool)
						local tool_res = tinkering.create_tool(
							tool_type_,
							materials,
							true,
							nil,
							{
								wear = new_wear,
								initial_metadata = tool:get_meta():to_table()
							},
							modifiers
						)
						if tool_res then
							output = tool_res
						end
					end
				end
			end
		end
		meta:set_string("formspec", tool_station.get_formspec())
	end

	if output then
		inv:set_list("output", {output})
	else
		inv:set_list("output", {})
	end

	return refresh
end

local function allow_metadata_inventory_put (pos, listname, index, stack, player)
	if core.is_protected(pos, player:get_player_name()) then
		return 0
	end

	if listname == "output" then
		return 0
	end

	return stack:get_count()
end

local function allow_metadata_inventory_move (pos, from_list, from_index, to_list, to_index, count, player)
	local meta = core.get_meta(pos)
	local inv = meta:get_inventory()
	local stack = inv:get_stack(from_list, from_index)
	return allow_metadata_inventory_put(pos, to_list, to_index, stack, player)
end

local function allow_metadata_inventory_take (pos, listname, index, stack, player)
	if core.is_protected(pos, player:get_player_name()) then
		return 0
	end

	return stack:get_count()
end

local function on_construct(pos)
	local meta = core.get_meta(pos)
	meta:set_string("formspec", tool_station.get_formspec())

	-- Create inventory
	local inv = meta:get_inventory()
	inv:set_size('input', 9)
	inv:set_size('output', 1)

	-- Set tool type meta
	meta:set_string("tool_type", "")
end

local function on_take(pos, listname, index, stack, player)
	local inv = core.get_meta(pos):get_inventory()

	if listname == "output" then
		handle_take_output(pos, "input")
	end

	core.get_node_timer(pos):start(0.02)
end

local function can_dig(pos, player)
	local meta = core.get_meta(pos)
	local inv = meta:get_inventory()
	return inv:is_empty("input") and inv:is_empty("output")
end

local function on_receive_fields(pos, formname, fields, sender)
	if sender and core.is_protected(pos, sender:get_player_name()) then
		return 0
	end

	local meta = core.get_meta(pos)
	if fields["anvil"] then
		meta:set_string("tool_type", "")
	else
		for name,_ in pairs(fields) do
			if tinkering.tools[name] then
				meta:set_string("tool_type", name)
				break
			end
		end
	end

	core.get_node_timer(pos):start(0.02)
end

core.register_node("tinkering:tool_station", {
	description = S("Tool Station"),
	tiles = {
		"tinkering_workbench_top.png", "tinkering_bench_bottom.png",
		"tinkering_bench_side.png",    "tinkering_bench_side.png",
		"tinkering_bench_side.png",    "tinkering_bench_side.png"
	},
	drawtype = "nodebox",
	paramtype = "light",
	node_box = tinkering.bench,

	on_construct = on_construct,
	legacy_facedir_simple = true,
	is_ground_content = false,
	sounds = fluidity.external.sounds.node_sound_wood,

	can_dig = can_dig,
	on_timer = on_timer,
	on_construct = on_construct,
	on_receive_fields = on_receive_fields,

	on_metadata_inventory_move = function(pos)
		core.get_node_timer(pos):start(0.05)
	end,
	on_metadata_inventory_put = function(pos)
		core.get_node_timer(pos):start(0.05)
	end,
	on_metadata_inventory_take = on_take,

	allow_metadata_inventory_put  = allow_metadata_inventory_put,
	allow_metadata_inventory_take = allow_metadata_inventory_take,
	allow_metadata_inventory_move = allow_metadata_inventory_move,

	groups = {choppy = 3, axey = 1, oddly_breakable_by_hand = 1},

	_mcl_hardness = 1,
	_mcl_blast_resistance = 1,
})
