-- for translation
local S = minetest.get_translator("connected_chests")

-- param_tab maps the x and z offset to a param2 value
local param_tab = {
	["-1 0"] = 0,
	 ["1 0"] = 2,
	["0 -1"] = 3,
	 ["0 1"] = 1,
}

-- param_tab2 maps the other way round
local param_tab2 = {}
for n,i in pairs(param_tab) do
	param_tab2[i] = n:split" "
end

local function return_remove_next(allowed_name, add_open)
	local function remove_next(pos, oldnode)
		-- if the left node had an unexpected rotation, the right one can't be
		-- found, in this case simply do nothing
		if oldnode.param2 > 3 then
			return
		end

		-- remove the right one if there is one
		-- (the left one is already removed)
		local x, z = unpack(param_tab2[oldnode.param2])
		pos.x = pos.x-x
		pos.z = pos.z-z
		local right_n = minetest.get_node(pos).name
		if right_n == allowed_name
		or (add_open and right_n == allowed_name .. "_open") then
			minetest.remove_node(pos)
		end
	end
	return remove_next
end


-- used when constructing the left node
local function return_add_next(right_name)
	local function add_next(pos, node)
		node = node or minetest.get_node(pos)
		local par = node.param2

		-- if the left node is set with an unexpected rotation, put the chest
		-- with default rotation
		if par > 3 then
			minetest.log("action",
				node.name .. " with invalid param2 found, pos: " ..
				minetest.pos_to_string(pos) .. ", param2: " .. par)
			node.param2 = 0
			minetest.set_node(pos, node)
			return
		end

		-- put the right chest if possible
		local x, z = unpack(param_tab2[par])
		pos.x = pos.x-x
		pos.z = pos.z-z
		if minetest.get_node(pos).name == "air" then
			minetest.set_node(pos, {name=right_name, param2=par})
		end
	end
	return add_next
end


-- gives information about the positions and param to place the nodes
local function get_pointed_info(pt, name)
	if not pt then
		return
	end
	local pu = minetest.get_pointed_thing_position(pt)
	local pa = minetest.get_pointed_thing_position(pt, true)
	if not pu
	or not pa
	or pu.y ~= pa.y then
		return
	end
	local nd_u = minetest.get_node(pu)
	if nd_u.name ~= name then
		return
	end
	return pu, pa, nd_u.param2
end




local pars = {[0]=2, 3, 0, 1}

local chestdata = {}


-- executed when connecting the chests
local function connect_chests(pu, pa, old_param2, data)
	local metatable = minetest.get_meta(pu):to_table()

	local par = param_tab[pu.x-pa.x.." "..pu.z-pa.z]
	local par_inverted = pars[par]
	if old_param2 == par_inverted then
		pu, pa = pa, pu
		par = par_inverted
	end

	data.on_connect(pu, pa, par, metatable)
end


local tube_to_left, tube_to_left_locked, tube_update, tube_groups
if minetest.global_exists"pipeworks" then
	tube_to_left_locked = {
		insert_object = function(pos, node, stack)
			local x, z = unpack(param_tab2[node.param2])
			return minetest.get_meta{x=pos.x+x, y=pos.y, z=pos.z+z
				}:get_inventory():add_item("main", stack)
		end,
		can_insert = function(pos, node, stack)
			local x, z = unpack(param_tab2[node.param2])
			return minetest.get_meta{x=pos.x+x, y=pos.y, z=pos.z+z
				}:get_inventory():room_for_item("main", stack)
		end,
		connect_sides = {right = 1, back = 1, front = 1, bottom = 1, top = 1}
	}

	tube_to_left = table.copy(tube_to_left_locked)
	tube_to_left.input_inventory = "main"

	tube_update = pipeworks.scan_for_tube_objects

	tube_groups = {tubedevice=1, tubedevice_receiver=1}
else
	function tube_update() end
end


connected_chests = {chestdata = chestdata}
--[[
connected_chests.register_chest(<original_node>, {
	description = <string>, -- The name of the connected chest as shown to the
		-- player, i.e. the node metadata infotext
	get_formspec = function(metatable, pos)
		return <formspec_of_big>
	end,
	lock = true, -- indicates whether a lock should be added to the texture
		-- and has an impact on the tube function
	front = <keyhole_texture>, -- if present, this texture is added to the chest
		-- front
	on_rightclick = <func>, -- sets an on_rightclick (some chests need this)
})
]]

function connected_chests.register_chest(fromname, data)
	chestdata[fromname] = data

	--~ local mod, name = fromname:split":"
	local name_left = fromname .. "_connected_left"
	local name_right = fromname .. "_connected_right"
	data.left = name_left
	data.right = name_right

	local description = data.description
	if not description then
		minetest.log("deprecated",
			"Missing connected chest description for " .. fromname)
		description = "Big " .. minetest.registered_nodes[fromname].description
	end

	-- executed when connecting the chest
	data.on_connect = function(pu, pa, par, metatable)
		minetest.add_node(pu, {name=name_left, param2=par})
		minetest.add_node(pa, {name=name_right, param2=par})

		if not data.add_open_chest then
			metatable.fields.formspec = data.get_formspec(metatable, pu)
		end
		metatable.fields.infotext = description
		local meta = minetest.get_meta(pu)
		meta:from_table(metatable)
		local inv = meta:get_inventory()
		inv:set_size("main", 65)

	end

	-- override the original node to support connecting
	local place_chest = minetest.registered_nodes[fromname].on_place
	local creative_mode = minetest.settings:get_bool"creative_mode"
	minetest.override_item(fromname, {
		on_place = function(itemstack, placer, pointed_thing)
			if not placer
			or not placer:get_player_control().sneak then
				return place_chest(itemstack, placer, pointed_thing)
			end
			local pu, pa, par2 = get_pointed_info(pointed_thing, fromname)
			if not pu then
				return place_chest(itemstack, placer, pointed_thing)
			end
			if minetest.is_protected(pa, placer:get_player_name()) then
				return
			end
			connect_chests(pu, pa, par2, data)
			if not creative_mode then
				itemstack:take_item()
				return itemstack
			end
		end
	})


	-- Adds the big chest nodes

	-- the left one contains inventory
	local chest = {}
	local origdef = minetest.registered_nodes[fromname]
	for i in pairs(origdef) do
		chest[i] = rawget(origdef, i)
	end

	local top = chest.tiles[1]
	local side = chest.tiles[4]
	local top_texture = top .. "^([combine:16x16:5,0=" .. top ..
		"^connected_chests_frame.png^[makealpha:255,126,126)"
	local side_texture = side .. "^([combine:16x16:5,0=" .. side ..
		"^connected_chests_frame.png^[makealpha:255,126,126)"
	local inside_texture


	chest.description = description
	chest.groups = table.copy(chest.groups)
	chest.groups.not_in_creative_inventory = 1
	chest.legacy_facedir_simple = nil
	chest.after_place_node = nil
	chest.on_receive_fields = nil
	if data.on_rightclick then
		chest.on_rightclick = data.on_rightclick
	end

	-- disallow rotating a connected chest using a screwdriver
	function chest.on_rotate()
		return false
	end

	-- copy pipeworks tube data (if requisite)
	if chest.tube then
		chest.tube = table.copy(chest.tube)
		chest.tube.connect_sides = {left = 1, -- no connection to the right.
			back = 1, front = 1, bottom = 1, top = 1}
	end

	if not data.front then
		data.front = "connected_chests_front.png"
		if data.lock then
			data.front = data.front .. "^connected_chests_lock.png"
		end
	end
	chest.tiles = {top_texture, top_texture, "default_obsidian_glass.png",
		side, side_texture.."^[transformFX", side_texture.."^" .. data.front}
	chest.drop = (chest.drop or fromname) .. " 2"
	chest.selection_box = {
		type = "fixed",
		fixed = {
			{-0.5, -0.5, -0.5, 1.5, 0.5, 0.5},
		},
	}
	chest.on_construct = return_add_next(name_right)
	chest.after_destruct = return_remove_next(name_right, data.add_open_chest)

	if data.add_open_chest then
		-- mostly copied from default
		local def_opened = table.copy(chest)

		def_opened.mesh = "connected_chest_open.obj"
		def_opened.drawtype = "mesh"
		def_opened.paramtype = "light"
		for i = 1, #def_opened.tiles do
			if type(def_opened.tiles[i]) == "string" then
				def_opened.tiles[i] =
					{name = def_opened.tiles[i], backface_culling = true}
			elseif def_opened.tiles[i].backface_culling == nil then
				def_opened.tiles[i].backface_culling = true
			end
		end
		def_opened.selection_box = {
			type = "fixed",
			fixed = {-0.5, -0.5, -0.5, 1.5, 3/16, 0.5},
		}
		def_opened.diggable = false
		def_opened.on_blast = function() end
		inside_texture = "default_chest_inside.png^([combine:16x32:5,0=" ..
			"default_chest_inside.png^connected_chests_inside_frame.png^[" ..
			"makealpha:255,126,126)"
		-- TODO, see right chest
		-- here 3 and 4 are swapped and no mirroring is needed…
		def_opened.tiles[3] = def_opened.tiles[4]
		def_opened.tiles[3].name = def_opened.tiles[3].name
		def_opened.tiles[5] = def_opened.tiles[6]

		def_opened.tiles[6] = inside_texture

		minetest.register_node(":" .. name_left.. "_open", def_opened)
	end

	--~ minetest.register_node("connected_chests:chest_left", chest)
	minetest.register_node(":" .. name_left, chest)


	-- the right one is the deco one
	local tiles = {top_texture.."^[transformFX", top_texture.."^[transformFX",
		side, "default_obsidian_glass.png", side_texture, side_texture
		.. "^" .. data.front .. "^[transformFX"}
	local right_def = {
		tiles = tiles,
		paramtype2 = "facedir",
		drop = "",
		pointable = false,
		diggable = false,
		on_construct = function(pos)
			local node = minetest.get_node(pos)

			-- if the right node has an unexpected rotation, try to set it with
			-- a valid one
			if node.param2 > 3 then
				node.param2 = node.param2 % 4
				minetest.set_node(pos, node)
				return
			end

			-- remove it if the left node can't be found
			local x, z = unpack(param_tab2[node.param2])
			local node_left = minetest.get_node{x=pos.x+x, y=pos.y, z=pos.z+z}
			if node_left.name ~= name_left
			or node_left.param2 ~= node.param2 then
				minetest.remove_node(pos)
				return
			end

			-- connect pipework tubes if there are any
			tube_update(pos)
		end,
		after_destruct = function(pos, oldnode)
			-- simply remove the right node if it has an unexpected rotation
			if oldnode.param2 > 3 then
				return
			end

			-- add it back if the left node is still there
			local x, z = unpack(param_tab2[oldnode.param2])
			local node_left = minetest.get_node{x=pos.x+x, y=pos.y, z=pos.z+z}
			if node_left.name == name_left
			and node_left.param2 == oldnode.param2
			and minetest.get_node(pos).name == "air" then
				minetest.set_node(pos, oldnode)
				return
			end

			-- disconnect pipework tubes if there are any
			tube_update(pos)
		end,
		tube = data.lock and tube_to_left_locked or tube_to_left,
		groups = tube_groups,
	}

	if data.add_open_chest then
		local def_opened = table.copy(right_def)

		def_opened.mesh = "connected_chest_open.obj"
		def_opened.drawtype = "mesh"
		def_opened.paramtype = "light"
		for i = 1, #def_opened.tiles do
			if type(def_opened.tiles[i]) == "string" then
				def_opened.tiles[i] =
					{name = def_opened.tiles[i], backface_culling = true}
			elseif def_opened.tiles[i].backface_culling == nil then
				def_opened.tiles[i].backface_culling = true
			end
		end

		-- fix right side, somehow
		def_opened.tiles[4] = def_opened.tiles[3]
		def_opened.tiles[4].name = def_opened.tiles[4].name .. "^[transformFX"

		-- fix front side
		def_opened.tiles[5] = def_opened.tiles[6]

		-- add inside
		def_opened.tiles[6] = inside_texture .. "^[transformFX"

		-- TODO: back side looks like right side

		minetest.register_node(":" .. name_right .. "_open", def_opened)
	end

	minetest.register_node(":" .. name_right, right_def)


	-- LBMs to fix half chests if they occur for some reason
	minetest.register_lbm{
		label = "Connected Chest fixer " .. name_right,
		name = ":" .. name_right .. "_reconnect_lbm",
		nodenames = {name_right},
		run_at_every_load = true,
		action = function(pos, node)
			if node.param2 > 3 then
				node.param2 = node.param2%4
				minetest.set_node(pos, node)
				return
			end
			local x, z = unpack(param_tab2[node.param2])
			local left_node = minetest.get_node{x=pos.x+x, y=pos.y, z=pos.z+z}
			if left_node.name ~= name_left
			or left_node.param2 ~= node.param2 then
				minetest.remove_node(pos)
			end
		end,
	}
	minetest.register_lbm{
		label = "Connected Chest fixer " .. name_left,
		name = ":" .. name_left .. "_reconnect_lbm",
		nodenames = {name_left},
		run_at_every_load = true,
		action = return_add_next(name_right),
	}
end


local function get_chest_formspec(pos)
	local spos = pos.x .. "," .. pos.y .. "," .. pos.z
	local formspec =
		"size[13,9]" ..
		"list[nodemeta:" .. spos .. ";main;0,0;13,5;]" ..
		"list[current_player;main;2.5,5.2;8,4;]" ..
		"listring[nodemeta:" .. spos .. ";main]" ..
		"listring[current_player;main]"
	return formspec
end

local open_chests = {} -- counter for players viewing the chest
local accessed_chests = {} -- position of the chest a player views

-- a hacky way to close open connected default chests
-- vi: vector index of the position of the left chest
local function close_chest(vi)
	local pos = minetest.get_position_from_hash(vi)
	local node = minetest.get_node(pos)
	local is_locked = node.name == "default:chest_locked_connected_left_open"
	if node.name ~= "default:chest_connected_left_open"
	and not is_locked then
		return
	end
	if is_locked then
		node.name = "default:chest_locked_connected_left"
	else
		node.name = "default:chest_connected_left"
	end
	minetest.swap_node(pos, node)

	-- close the right chest
	-- TODO: test for valid rotation
	local x, z = unpack(param_tab2[node.param2])
	pos.x = pos.x-x
	pos.z = pos.z-z
	node = minetest.get_node(pos)
	if is_locked then
		if node.name == "default:chest_locked_connected_right_open" then
			node.name = "default:chest_locked_connected_right"
			minetest.swap_node(pos, node)
		end
	else
		if node.name == "default:chest_connected_right_open" then
			node.name = "default:chest_connected_right"
			minetest.swap_node(pos, node)
		end
	end
	pos.x = pos.x + x * 0.5
	pos.z = pos.z + z * 0.5
	minetest.sound_play("default_chest_close",
		{gain = 0.3, pos = pos, max_hear_distance = 10, pitch = 0.7}, true)
end

-- close all remaining open chest on shutdown
minetest.register_on_shutdown(function()
	for vi in pairs(open_chests) do
		close_chest(vi)
	end
	open_chests = nil
end)

-- close open chests when the last player exits formspec
minetest.register_on_player_receive_fields(function(player, formname, fields)
	if formname ~= "default:chest_connected"
	and formname ~= "default:chest_locked_connected" then
		return
	end
	if not player
	or not fields.quit then
		return
	end
	local pn = player:get_player_name()
	local vi = accessed_chests[pn]
	if not vi then
		minetest.log("warning", pn .. " opened a chest without lid?")
		return
	end
	accessed_chests[pn] = nil

	local cnt = open_chests[vi]
	if cnt == 1 then
		close_chest(vi)
		open_chests[vi] = nil
	else
		open_chests[vi] = cnt-1
	end

	return true
end)

local chest_lid_obstructed = default.chest
	and default.chest.chest_lid_obstructed
connected_chests.register_chest("default:chest", {
	description = S("Big Chest"),
	add_open_chest = true,
	on_rightclick = function(pos, _, player)
		minetest.sound_play("default_chest_open",
			{gain = 0.3, pos = pos, max_hear_distance = 10, pitch = 0.7}, true)

		local vi = minetest.hash_node_position(pos)
		if not open_chests[vi]
		and not chest_lid_obstructed(pos) then
			local left_param2 = minetest.get_node(pos).param2

			-- TODO: test for invalid param2 values
			local x, z = unpack(param_tab2[left_param2])
			local pos_right = {x=pos.x-x, y=pos.y, z=pos.z-z}
			local node = minetest.get_node(pos_right)
			if node.name == "default:chest_connected_right"
			and not chest_lid_obstructed(pos_right) then
				minetest.swap_node(pos, {
					name = "default:chest_connected_left_open",
					param2 = left_param2})
				minetest.swap_node(pos_right, {
					name = "default:chest_connected_right_open",
					param2 = node.param2})
			end
		end

		local pname = player:get_player_name()
		local spec = get_chest_formspec(pos)

		minetest.after(0.2, minetest.show_formspec, pname,
			"default:chest_connected", spec)

		if not accessed_chests[pname] then
			open_chests[vi] = open_chests[vi] or 0
			open_chests[vi] = open_chests[vi]+1

			accessed_chests[pname] = vi
		end
	end
})

connected_chests.register_chest("default:chest_locked", {
	description = S("Big Locked Chest"),
	lock = true,
	add_open_chest = true,
	on_rightclick = function(pos, _, player)
		if not default.can_interact_with_node(player, pos) then
			minetest.sound_play("default_chest_locked", {pos = pos}, true)
			return
		end

		minetest.sound_play("default_chest_open",
			{gain = 0.32, pos = pos, max_hear_distance = 10, pitch = 0.7}, true)

		local vi = minetest.hash_node_position(pos)
		-- TODO: somehow avoid using the chest node names here
		if not open_chests[vi]
		and not chest_lid_obstructed(pos) then
			local left_param2 = minetest.get_node(pos).param2

			-- TODO: test for invalid param2 values
			local x, z = unpack(param_tab2[left_param2])
			local pos_right = {x=pos.x-x, y=pos.y, z=pos.z-z}
			local node = minetest.get_node(pos_right)
			if node.name == "default:chest_locked_connected_right"
			and not chest_lid_obstructed(pos_right) then
				minetest.swap_node(pos_right, {
					name = "default:chest_locked_connected_right_open",
					param2 = node.param2})
				minetest.swap_node(pos, {
					name = "default:chest_locked_connected_left_open",
					param2 = left_param2})
			end
		end

		local pname = player:get_player_name()
		local spec = get_chest_formspec(pos)

		minetest.after(0.2, minetest.show_formspec, pname,
			"default:chest_locked_connected", spec)

		if not accessed_chests[pname] then
			open_chests[vi] = open_chests[vi] or 0
			open_chests[vi] = open_chests[vi]+1

			accessed_chests[pname] = vi
		end
	end
})



-- legacy

-- the default chest lid obstruction function wasn't exposed in minetest 0.4.16
if not chest_lid_obstructed then
	-- copied from default's nodes.lua
	function chest_lid_obstructed(pos)
		local above = {x = pos.x, y = pos.y + 1, z = pos.z}
		local def = minetest.registered_nodes[minetest.get_node(above).name]
		-- allow ladders, signs, wallmounted things and torches to not obstruct
		if def and
				(def.drawtype == "airlike" or
				def.drawtype == "signlike" or
				def.drawtype == "torchlike" or
				(def.drawtype == "nodebox"
					and def.paramtype2 == "wallmounted")) then
			return false
		end
		return true
	end
end

-- once the connected_chests mod supported only default chests and used
-- different node names
minetest.register_alias("connected_chests:chest_left",
	"default:chest_connected_left")
minetest.register_alias("connected_chests:chest_right",
	"default:chest_connected_right")
minetest.register_alias("connected_chests:chest_left_locked",
	"default:chest_locked_connected_left")
minetest.register_alias("connected_chests:chest_right_locked",
	"default:chest_locked_connected_right")
minetest.register_alias("connected_chests:chest_locked_left",
	"default:chest_locked_connected_left")
minetest.register_alias("connected_chests:chest_locked_right",
	"default:chest_locked_connected_right")

if minetest.global_exists("hopper") and hopper.set_extra_container_info then
	local function get_inventory(chest_right_pos)
		local node_right = minetest.get_node(chest_right_pos)
		if node_right.param2 > 3 then
			-- The right connected chest node has an invalid param2 value
			-- Cannot determine the inventory
			return
		end
		local x, z = unpack(param_tab2[node_right.param2])
		local chest_left_pos = {x=chest_right_pos.x+x, y=chest_right_pos.y,
			z=chest_right_pos.z+z}
		local node_left = minetest.get_node(chest_left_pos)
		if node_left.name ~= "default:chest_connected_left"
		and node_left.name ~= "default:chest_connected_left_open" then
			minetest.log("error","The left chest is not a chest: " ..
				node_left.name .. " at " .. vector.to_string(chest_left_pos))
			return
		end
		if node_left.param2 ~= node_right.param2 then
			minetest.log("error", "The chests are pointing in different " ..
				"directions: node_left.param2:" .. node_left.param2 ..
				", node_right.param2:" .. node_right.param2)
			return
		end
		return minetest.get_meta(chest_left_pos):get_inventory()
	end
	hopper:add_container({
		{"top", "default:chest_connected_left", "main"},
		{"bottom", "default:chest_connected_left", "main"},
		{"side", "default:chest_connected_left", "main"},
		{"top", "default:chest_connected_right", "main",
			get_inventory = get_inventory},
		{"bottom", "default:chest_connected_right", "main",
			get_inventory = get_inventory},
		{"side", "default:chest_connected_right", "main",
			get_inventory = get_inventory},
		{"top", "default:chest_connected_left_open", "main"},
		{"bottom", "default:chest_connected_left_open", "main"},
		{"side", "default:chest_connected_left_open", "main"},
		{"top", "default:chest_connected_right_open", "main",
			get_inventory = get_inventory},
		{"bottom", "default:chest_connected_right_open", "main",
			get_inventory = get_inventory},
		{"side", "default:chest_connected_right_open", "main",
			get_inventory = get_inventory},
	})
	local function set_hopper_param2(hopper_pos, chest_left_pos)
		local param2_by_offset = {
			[vector.new(-1, 0, 0):to_string()] = 0,
			[vector.new( 0, 0, 1):to_string()] = 1,
			[vector.new( 1, 0, 0):to_string()] = 2,
			[vector.new( 0, 0,-1):to_string()] = 3,
		}
		local hopper_param2 = param2_by_offset[
			(chest_left_pos - hopper_pos):to_string()]
		if hopper_param2 then
			return hopper_param2
		end
		local x, z = unpack(
			param_tab2[minetest.get_node(chest_left_pos).param2])
		local chest_right_pos = {x=chest_left_pos.x-x, y=chest_left_pos.y,
			z=chest_left_pos.z-z}
		return param2_by_offset[(chest_right_pos - hopper_pos):to_string()]
	end
	hopper:set_extra_container_info({
		{"default:chest_connected_left", set_hopper_param2 = set_hopper_param2},
	})
end
