local S = minetest.get_translator("pipeworks")
local has_digilines = minetest.get_modpath("digilines")

local function set_wielder_formspec(def, meta)
	local width, height = def.wield_inv.width, def.wield_inv.height
	local offset = 5.22 - width * 0.625
	local size = "10.2,"..(6.5 + height * 1.25 + (has_digilines and 1.25 or 0))
	local list_bg = ""
	if minetest.get_modpath("i3") or minetest.get_modpath("mcl_formspec") then
		list_bg = "style_type[box;colors=#666]"
		for i=0, height-1 do
			for j=0, width-1 do
				list_bg = list_bg.."box["..offset+(i*1.25)..","..1.25+(j*1.25)..";1,1;]"
			end
		end
	end
	local inv_offset = 1.5 + height * 1.25
	local fs = "formspec_version[2]size["..size.."]"..
		pipeworks.fs_helpers.get_prepends(size)..list_bg..
		"item_image[0.5,0.3;1,1;"..def.name.."_off]"..
		"label[1.75,0.8;"..minetest.formspec_escape(def.description).."]"..
		"list[context;"..def.wield_inv.name..";"..offset..",1.25;"..width..","..height..";]"
	if has_digilines then
		fs = fs.."field[1.5,"..inv_offset..";5,0.8;channel;"..S("Channel")..";${channel}]"..
			"button_exit[6.5,"..inv_offset..";2,0.8;save;"..S("Save").."]"..
			pipeworks.fs_helpers.get_inv(inv_offset + 1.25).."listring[]"
	else
		fs = fs..pipeworks.fs_helpers.get_inv(inv_offset).."listring[]"
	end
	meta:set_string("formspec", fs)
	meta:set_string("infotext", def.description)
end

local function wielder_action(def, pos, node, index)
	local meta = minetest.get_meta(pos)
	local inv = meta:get_inventory()
	local list = inv:get_list(def.wield_inv.name)
	local wield_index
	if index then
		if list[index] and (def.wield_hand or not list[index]:is_empty()) then
			wield_index = index
		end
	else
		for i, stack in ipairs(list) do
			if not stack:is_empty() then
				wield_index = i
				break
			end
		end
	end
	if not wield_index and not def.wield_hand then
		return
	end
	local dir = minetest.facedir_to_dir(node.param2)
	local fakeplayer = fakelib.create_player({
		name = meta:get_string("owner"),
		direction = vector.multiply(dir, -1),
		position = pos,
		inventory = inv,
		wield_index = wield_index or 1,
		wield_list = def.wield_inv.name,
	})
	-- Under and above positions are intentionally switched.
	local pointed = {
		type = "node",
		under = vector.subtract(pos, dir),
		above = vector.subtract(pos, vector.multiply(dir, 2)),
	}
	def.action(fakeplayer, pointed)
	if def.eject_drops then
		for i, stack in ipairs(inv:get_list("main")) do
			if not stack:is_empty() then
				pipeworks.tube_inject_item(pos, pos, dir, stack)
				inv:set_stack("main", i, ItemStack(""))
			end
		end
	end
end

local function wielder_on(def, pos, node)
	if node.name ~= def.name.."_off" then
		return
	end
	node.name = def.name.."_on"
	minetest.swap_node(pos, node)
	wielder_action(def, pos, node)
end

local function wielder_off(def, pos, node)
	if node.name == def.name.."_on" then
		node.name = def.name.."_off"
		minetest.swap_node(pos, node)
	end
end

local function wielder_digiline_action(def, pos, channel, msg)
	local meta = minetest.get_meta(pos)
	local set_channel = meta:get_string("channel")
	if channel ~= set_channel then
		return
	end
	if type(msg) ~= "table" then
		if type(msg) == "string" then
			if msg:sub(1, 8) == "activate" then
				msg = {command = "activate", slot = tonumber(msg:sub(9))}
			end
		else
			return
		end
	end
	if msg.command == "activate" then
		local node = minetest.get_node(pos)
		local index = type(msg.slot) == "number" and msg.slot or nil
		wielder_action(def, pos, node, index)
	end
end

function pipeworks.register_wielder(def)
	for _,state in ipairs({"off", "on"}) do
		local groups = {
			snappy = 2, choppy = 2, oddly_breakable_by_hand = 2,
			mesecon = 2, tubedevice = 1, tubedevice_receiver = 1,
			axey = 1, handy = 1, pickaxey = 1,
			not_in_creative_inventory = state == "on" and 1 or nil
		}
		minetest.register_node(def.name.."_"..state, {
			description = def.description,
			tiles = def.tiles[state],
			paramtype2 = "facedir",
			groups = groups,
			is_ground_content = false,
			_mcl_hardness = 0.6,
			_sound_def = {
				key = "node_sound_stone_defaults",
			},
			drop = def.name.."_off",
			mesecons = {
				effector = {
					rules = pipeworks.rules_all,
					action_on = function(pos, node)
						wielder_on(def, pos, node)
					end,
					action_off = function(pos, node)
						wielder_off(def, pos, node)
					end,
				},
			},
			digilines = {
				receptor = {},
				effector = {
					action = function(pos, _, channel, msg)
						wielder_digiline_action(def, pos, channel, msg)
					end,
				},
			},
			tube = {
				can_insert = function(pos, node, stack, direction)
					if def.eject_drops then
						-- Prevent ejected items from being inserted
						local dir = vector.multiply(minetest.facedir_to_dir(node.param2), -1)
						if vector.equals(direction, dir) then
							return false
						end
					end
					local inv = minetest.get_meta(pos):get_inventory()
					return inv:room_for_item(def.wield_inv.name, stack)
				end,
				insert_object = function(pos, node, stack)
					local inv = minetest.get_meta(pos):get_inventory()
					return inv:add_item(def.wield_inv.name, stack)
				end,
				input_inventory = def.wield_inv.name,
				connect_sides = def.connect_sides,
				can_remove = function(pos, node, stack)
					return stack:get_count()
				end,
			},
			on_construct = function(pos)
				local meta = minetest.get_meta(pos)
				local inv = meta:get_inventory()
				inv:set_size(def.wield_inv.name, def.wield_inv.width * def.wield_inv.height)
				if def.eject_drops then
					inv:set_size("main", 32)
				end
				set_wielder_formspec(def, meta)
			end,
			after_place_node = function(pos, placer)
				pipeworks.scan_for_tube_objects(pos)
				if not placer then
					return
				end
				local node = minetest.get_node(pos)
				node.param2 = minetest.dir_to_facedir(placer:get_look_dir(), true)
				minetest.set_node(pos, node)
				minetest.get_meta(pos):set_string("owner", placer:get_player_name())
			end,
			after_dig_node = function(pos, oldnode, oldmetadata, digger)
				for _,stack in ipairs(oldmetadata.inventory[def.wield_inv.name] or {}) do
					if not stack:is_empty() then
						minetest.add_item(pos, stack)
					end
				end
				pipeworks.scan_for_tube_objects(pos)
			end,
			on_rotate = pipeworks.on_rotate,
			allow_metadata_inventory_put = function(pos, listname, index, stack, player)
				if not pipeworks.may_configure(pos, player) then return 0 end
				return stack:get_count()
			end,
			allow_metadata_inventory_take = function(pos, listname, index, stack, player)
				if not pipeworks.may_configure(pos, player) then return 0 end
				return stack:get_count()
			end,
			allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
				if not pipeworks.may_configure(pos, player) then return 0 end
				return count
			end,
			on_receive_fields = function(pos, _, fields, sender)
				if not fields.channel or not pipeworks.may_configure(pos, sender) then
					return
				end
				minetest.get_meta(pos):set_string("channel", fields.channel)
			end,
		})
	end
	table.insert(pipeworks.ui_cat_tube_list, def.name.."_off")
end

local function get_tiles(name, stateful)
	local tiles = {on = {}, off = {}}
	for _,state in ipairs({"off", "on"}) do
		for _,side in ipairs({"top", "bottom", "side2", "side1", "back", "front"}) do
			local suffix = stateful[side] and "_"..state or ""
			table.insert(tiles[state], "pipeworks_"..name.."_"..side..suffix..".png")
		end
	end
	return tiles
end

if pipeworks.enable_node_breaker then
	pipeworks.register_wielder({
		name = "pipeworks:nodebreaker",
		description = S("Node Breaker"),
		tiles = get_tiles("nodebreaker", {top = 1, bottom = 1, side2 = 1, side1 = 1, front = 1}),
		connect_sides = {top = 1, bottom = 1, left = 1, right = 1, back = 1},
		wield_inv = {name = "pick", width = 1, height = 1},
		wield_hand = true,
		eject_drops = true,
		action = function(fakeplayer, pointed)
			local stack = fakeplayer:get_wielded_item()
			local old_stack = ItemStack(stack)
			local item_def = minetest.registered_items[stack:get_name()]
			if item_def.on_use then
				stack = item_def.on_use(stack, fakeplayer, pointed) or stack
				fakeplayer:set_wielded_item(stack)
			else
				local node = minetest.get_node(pointed.under)
				local node_def = minetest.registered_nodes[node.name]
				if not node_def or not node_def.on_dig then
					return
				end
				-- Check if the tool can dig the node
				local tool = stack:get_tool_capabilities()
				if not minetest.get_dig_params(node_def.groups, tool).diggable then
					-- Try using hand if tool can't dig the node
					local hand = ItemStack():get_tool_capabilities()
					if not minetest.get_dig_params(node_def.groups, hand).diggable then
						return
					end
				end
				-- This must only check for false, because `on_dig` returning nil is the same as returning true.
				if node_def.on_dig(pointed.under, node, fakeplayer) == false then
					return
				end
				local sound = node_def.sounds and node_def.sounds.dug
				if sound then
					minetest.sound_play(sound, {pos = pointed.under}, true)
				end
				stack = fakeplayer:get_wielded_item()
			end
			if stack:get_name() == old_stack:get_name() then
				-- Don't mechanically wear out tool
				if stack:get_wear() ~= old_stack:get_wear() and stack:get_count() == old_stack:get_count()
						and (item_def.wear_represents == nil or item_def.wear_represents == "mechanical_wear") then
					fakeplayer:set_wielded_item(old_stack)
				end
			elseif not stack:is_empty() then
				-- Tool got replaced by something else, treat it as a drop.
				fakeplayer:get_inventory():add_item("main", stack)
				fakeplayer:set_wielded_item("")
			end
		end,
	})
	minetest.register_alias("technic:nodebreaker_off", "pipeworks:nodebreaker_off")
	minetest.register_alias("technic:nodebreaker_on", "pipeworks:nodebreaker_on")
	minetest.register_alias("technic:node_breaker_off", "pipeworks:nodebreaker_off")
	minetest.register_alias("technic:node_breaker_on", "pipeworks:nodebreaker_on")
	minetest.register_alias("auto_tree_tap:off", "pipeworks:nodebreaker_off")
	minetest.register_alias("auto_tree_tap:on", "pipeworks:nodebreaker_on")
end

if pipeworks.enable_deployer then
	pipeworks.register_wielder({
		name = "pipeworks:deployer",
		description = S("Deployer"),
		tiles = get_tiles("deployer", {front = 1}),
		connect_sides = {back = 1},
		wield_inv = {name = "main", width = 3, height = 3},
		action = function(fakeplayer, pointed)
			local stack = fakeplayer:get_wielded_item()
			local def = minetest.registered_items[stack:get_name()]
			if def and def.on_place then
				local new_stack, placed_pos = def.on_place(stack, fakeplayer, pointed)
				fakeplayer:set_wielded_item(new_stack or stack)
				-- minetest.item_place_node doesn't play sound to the placer
				local sound = placed_pos and def.sounds and def.sounds.place
				local name = fakeplayer:get_player_name()
				if sound and name ~= "" then
					minetest.sound_play(sound, {pos = placed_pos, to_player = name}, true)
				end
			end
		end,
	})
	minetest.register_alias("technic:deployer_off", "pipeworks:deployer_off")
	minetest.register_alias("technic:deployer_on", "pipeworks:deployer_on")
end

if pipeworks.enable_dispenser then
	-- Override minetest.item_drop to negate its hardcoded offset
	-- when the dropper is a fake player.
	local item_drop = minetest.item_drop
	-- luacheck: ignore 122
	function minetest.item_drop(stack, dropper, pos)
		if dropper and dropper.is_fake_player then
			pos = vector.new(pos.x, pos.y - 1.2, pos.z)
		end
		return item_drop(stack, dropper, pos)
	end
	pipeworks.register_wielder({
		name = "pipeworks:dispenser",
		description = S("Dispenser"),
		tiles = get_tiles("dispenser", {front = 1}),
		connect_sides = {back = 1},
		wield_inv = {name = "main", width = 3, height = 3},
		action = function(fakeplayer)
			local stack = fakeplayer:get_wielded_item()
			local def = minetest.registered_items[stack:get_name()]
			if def and def.on_drop then
				local pos = fakeplayer:get_pos()
				fakeplayer:set_wielded_item(def.on_drop(stack, fakeplayer, pos) or stack)
			end
		end,
	})
end