local utils, mod_storage = ...
local S = utils.S



if utils.hopper_supported then



local hopper_interval = 1.0



local hopper_list = minetest.deserialize (mod_storage:get_string ("hopper_list"))
if type (hopper_list) ~= "table" then
	hopper_list = {}
end



local function add_hopper_to_list (pos)
	hopper_list[minetest.pos_to_string (pos, 0)] = true

	mod_storage:set_string ("hopper_list", minetest.serialize (hopper_list))
end



local function remove_hopper_from_list (pos)
	hopper_list[minetest.pos_to_string (pos, 0)] = nil

	mod_storage:set_string ("hopper_list", minetest.serialize (hopper_list))
end



local input_dir =
{
	[0] = { x = 0, y = 1, z = 0 },
	{ x = 0, y = 0, z = 1 },
	{ x = 0, y = 0, z = -1 },
	{ x = 1, y = 0, z = 0 },
	{ x = -1, y = 0, z = 0 },
	{ x = 0, y = -1, z = 0 }
}

local function get_input_dir (node)
	return input_dir[math.floor (node.param2 / 4)]
end



local function get_output_dir (node)
	if node then
		if node.name == "lwcomponents:hopper" then
			return vector.multiply (get_input_dir (node), -1)
		elseif node.name == "lwcomponents:hopper_horz" then
			return minetest.facedir_to_dir (node.param2)
		end
	end

	return nil
end



local function get_drop (pos)
	local objs = minetest.get_objects_inside_radius (pos, 1)

	for _, obj in pairs (objs) do
		local obj_pos = (obj.get_pos and obj:get_pos ())

		if obj_pos and utils.is_drop (obj) then
			obj_pos = vector.round (obj_pos)

			if vector.equals (pos, obj_pos) then
				local stack = ItemStack (obj:get_luaentity ().itemstring)

				if stack and not stack:is_empty () then
					stack:set_count (1)

					return stack, obj
				end
			end
		end
	end
end



local function take_drop (obj)
	if utils.is_drop (obj) then
		local stack = ItemStack (obj:get_luaentity ().itemstring)

		if stack and not stack:is_empty () then
			stack:set_count (stack:get_count () - 1)

			if stack:is_empty () then
				obj:get_luaentity().itemstring = ""
				obj:remove()
			else
				obj:get_luaentity().itemstring = stack:to_string ()
			end
		end
	end
end



local function next_item_to_take (src_pos, src_node, src_inv_name)
	if not src_inv_name or not minetest.registered_nodes[src_node.name] then
		return
	end

	local src_meta = minetest.get_meta (src_pos)
	local src_inv = (src_meta and src_meta:get_inventory ()) or nil

	if src_inv then
		local slots = src_inv:get_size (src_inv_name)

		for slot = 1, slots, 1 do
			local stack = src_inv:get_stack (src_inv_name, slot)

			if stack and not stack:is_empty () then
				stack:set_count (1)

				return stack, slot
			end
		end
	end
end



local function take_item (src_pos, src_inv_name, slot)
	local src_meta = minetest.get_meta (src_pos)
	local src_inv = (src_meta and src_meta:get_inventory ()) or nil

	if src_inv then
		local stack = src_inv:get_stack (src_inv_name, slot)

		if stack and not stack:is_empty () then
			stack:set_count (stack:get_count () - 1)

			src_inv:set_stack (src_inv_name, slot, stack)
		end
	end
end



local function call_allow_metadata_inventory_put (def, pos, listname, index, stack, placer)
	if def.allow_metadata_inventory_put and placer then
		local result, count = pcall (def.allow_metadata_inventory_put,
											  pos, listname, index, stack, placer)

		if result then
			return count
		end
	end

	return utils.settings.default_stack_max
end



local function call_on_metadata_inventory_put (def, pos, listname, index, stack, placer)
	if def.on_metadata_inventory_put and placer then
		pcall (def.on_metadata_inventory_put, pos, listname, index, stack, placer)
	end
end



local function place_item (dest_pos, dest_node, dest_inv_name, stack, placer)
	local dest_def = minetest.registered_nodes[dest_node.name]

	if not dest_inv_name or not dest_def then
		return
	end

	local dest_meta = minetest.get_meta (dest_pos)
	local dest_inv = (dest_meta and dest_meta:get_inventory ()) or nil

	if dest_inv then
		local slots = dest_inv:get_size (dest_inv_name)

		-- find existing stack
		for slot = 1, slots, 1 do
			local inv_stack = dest_inv:get_stack (dest_inv_name, slot)

			if inv_stack and not inv_stack:is_empty () and
				utils.is_same_item (inv_stack, stack) and
				inv_stack:get_count () < inv_stack:get_stack_max () and
				(call_allow_metadata_inventory_put (dest_def, dest_pos, dest_inv_name, slot, stack, placer) > 0) then

				inv_stack:set_count (inv_stack:get_count () + 1)
				dest_inv:set_stack (dest_inv_name, slot, inv_stack)

				call_on_metadata_inventory_put (dest_def, dest_pos, dest_inv_name, slot, stack, placer)

				return true
			end
		end

		-- find empty slot
		for slot = 1, slots, 1 do
			local inv_stack = dest_inv:get_stack (dest_inv_name, slot)

			if not inv_stack or inv_stack:is_empty () and
				(call_allow_metadata_inventory_put (dest_def, dest_pos, dest_inv_name, slot, stack, placer) > 0) then

				dest_inv:set_stack (dest_inv_name, slot, stack)

				call_on_metadata_inventory_put (dest_def, dest_pos, dest_inv_name, slot, stack, placer)

				return true
			end
		end
	end

	return false
end



local function get_player_object (pos)
	local meta = minetest.get_meta (pos)
	local placer_name = "<unknown>"

	if meta then
		placer_name = meta:get_string ("placer_name")
		local placer = minetest.get_player_by_name (placer_name)

		if placer then
			return placer
		end

		if placer_name == "" then
			placer_name = "<unknown>"
		end
	end

	return utils.get_dummy_player (true, placer_name)
end



local function run_hopper_action (pos)
	local node = utils.get_far_node (pos)
	local dest_dir = get_output_dir (node)

	if dest_dir then
		local dest_pos = vector.add (pos, dest_dir)
		local dest_node = utils.get_far_node (dest_pos)

		if dest_node then
			local registered_dest_invs = hopper.get_registered_inventories_for (dest_node.name)

			if registered_dest_invs then
				local placer = get_player_object (pos)
				local src_pos = vector.add (pos, get_input_dir (node))
				local drop
				local stack
				local slot = nil
				local src_inv_name = nil

				stack, drop = get_drop (src_pos)

				if not stack then
					local src_node = utils.get_far_node (src_pos)

					if src_node then
						local registered_src_invs = hopper.get_registered_inventories_for (src_node.name)

						if registered_src_invs then
							src_inv_name = registered_src_invs["top"]
							stack, slot = next_item_to_take (src_pos, src_node, src_inv_name)
						end
					end
				end

				if stack then
					local dest_side = (node.name == "lwcomponents:hopper" and "bottom") or "side"
					local dest_inv_name = registered_dest_invs[dest_side]

					if place_item (dest_pos, dest_node, dest_inv_name, stack, placer) then
						if drop then
							take_drop (drop)
						else
							take_item (src_pos, src_inv_name, slot)
						end
					end
				end
			end
		end
	end
end



local function on_construct (pos)
	add_hopper_to_list (pos)
end



local function on_destruct (pos)
	remove_hopper_from_list (pos)
end



local function after_place_node (pos, placer, itemstack, pointed_thing)
	local meta = minetest.get_meta (pos)

	meta:set_string ("placer_name", (placer and placer:get_player_name ()) or "")

	-- If return true no item is taken from itemstack
	return false
end



local function on_place (itemstack, placer, pointed_thing)
	if pointed_thing and pointed_thing.type == "node" then
		local stack = ItemStack (itemstack)
		local dir = vector.direction (pointed_thing.above, pointed_thing.under)

		if dir.y == 0 then
			minetest.item_place (ItemStack ("lwcomponents:hopper_horz"), placer, pointed_thing)

			if not utils.is_creative (placer) then
				stack:set_count (stack:get_count () - 1)
			end

			return stack
		end
	end

	return minetest.item_place (itemstack, placer, pointed_thing)
end



minetest.register_node ("lwcomponents:hopper", {
   description = S("Conduit Hopper"),
   tiles = { "lwcomponents_hopper_top.png", "lwcomponents_hopper_vert_spout.png",
				 "lwcomponents_hopper_side.png", "lwcomponents_hopper_side.png",
				 "lwcomponents_hopper_side.png", "lwcomponents_hopper_side.png" },
   drawtype = "nodebox",
   node_box = {
      type = "fixed",
		fixed = {
			{-0.3125, -0.5, -0.3125, 0.3125, -0.25, 0.3125}, -- spout_vert
			{-0.375, -0.375, -0.375, 0.375, 0.3125, 0.375}, -- body
			{-0.5, 0, -0.5, -0.3125, 0.5, 0.5}, -- funnel_1
			{-0.5, 0, 0.3125, 0.5, 0.5, 0.5}, -- funnel_2
			{-0.5, 0, -0.5, 0.5, 0.5, -0.3125}, -- funnel_3
			{0.3125, 0, -0.5, 0.5, 0.5, 0.5}, -- funnel_4
		}
   },
   selection_box = {
      type = "fixed",
		fixed = {
			{-0.3125, -0.5, -0.3125, 0.3125, -0.25, 0.3125}, -- spout_vert
			{-0.375, -0.375, -0.375, 0.375, 0.3125, 0.375}, -- body
			{-0.5, 0, -0.5, 0.5, 0.5, 0.5}, -- funnel
		}
   },
   collision_box = {
      type = "fixed",
		fixed = {
			{-0.375, -0.375, -0.375, 0.375, 0.3125, 0.375}, -- body
			{-0.5, 0, -0.5, 0.5, 0.5, 0.5}, -- funnel
			{-0.3125, -0.3125, 0.5, 0.3125, 0.3125, 0.3125}, -- spout_side
		}
   },
	paramtype = "light",
	param1 = 0,
	paramtype2 = "facedir",
	param2 = 0,
	drop = "lwcomponents:hopper",
	groups = { cracky = 3 },

   on_construct = on_construct,
   on_destruct = on_destruct,
	after_place_node = after_place_node,
	on_place = on_place,
})



minetest.register_node ("lwcomponents:hopper_horz", {
   description = S("Conduit Hopper"),
   tiles = { "lwcomponents_hopper_top.png", "lwcomponents_hopper_bottom.png",
				 "lwcomponents_hopper_side.png", "lwcomponents_hopper_side.png",
				 "lwcomponents_hopper_side_spout.png", "lwcomponents_hopper_side.png" },
   drawtype = "nodebox",
   node_box = {
      type = "fixed",
		fixed = {
			{-0.375, -0.375, -0.375, 0.375, 0.3125, 0.375}, -- body
			{-0.5, 0, -0.5, -0.3125, 0.5, 0.5}, -- funnel_1
			{-0.5, 0, 0.3125, 0.5, 0.5, 0.5}, -- funnel_2
			{-0.5, 0, -0.5, 0.5, 0.5, -0.3125}, -- funnel_3
			{0.3125, 0, -0.5, 0.5, 0.5, 0.5}, -- funnel_4
			{-0.3125, -0.3125, 0.5, 0.3125, 0.3125, 0.3125}, -- spout_side
		}
   },
   selection_box = {
      type = "fixed",
		fixed = {
			{-0.375, -0.375, -0.375, 0.375, 0.3125, 0.375}, -- body
			{-0.5, 0, -0.5, 0.5, 0.5, 0.5}, -- funnel
			{-0.3125, -0.3125, 0.5, 0.3125, 0.3125, 0.3125}, -- spout_side
		}
   },
   collision_box = {
      type = "fixed",
		fixed = {
			{-0.375, -0.375, -0.375, 0.375, 0.3125, 0.375}, -- body
			{-0.5, 0, -0.5, 0.5, 0.5, 0.5}, -- funnel
			{-0.3125, -0.3125, 0.5, 0.3125, 0.3125, 0.3125}, -- spout_side
		}
   },
	paramtype = "light",
	param1 = 0,
	paramtype2 = "facedir",
	param2 = 0,
	drop = "lwcomponents:hopper",
	groups = { cracky = 3, not_in_creative_inventory = 1 },

   on_construct = on_construct,
   on_destruct = on_destruct,
	after_place_node = after_place_node,
})



local function run_hoppers ()
	for spos, _ in pairs (hopper_list) do
		run_hopper_action (minetest.string_to_pos (spos))
	end

	minetest.after (hopper_interval, run_hoppers)
end



minetest.register_on_mods_loaded (function ()
	minetest.after (3.0, run_hoppers)
end)



end -- utils.hopper_supported
