local luaentity = pipeworks.luaentity
local enable_max_limit = minetest.settings:get_bool("pipeworks_enable_items_per_tube_limit")
local max_tube_limit = tonumber(minetest.settings:get("pipeworks_max_items_per_tube")) or 30
if enable_max_limit == nil then enable_max_limit = true end

function pipeworks.tube_inject_item(pos, start_pos, velocity, item, owner)
	-- Take item in any format
	local stack = ItemStack(item)
	local obj = luaentity.add_entity(pos, "pipeworks:tubed_item")
	obj:set_item(stack:to_string())
	obj.start_pos = vector.copy(start_pos)
	obj:set_velocity(velocity)
	obj.owner = owner
	obj.tags = {}
	return obj
end

function pipeworks.tube_inject_direct(pos, start_pos, to_pos, velocity, item, owner) -- function needed because monitoring pipeworks.tube_inject_item
	local node = sbz_api.get_or_load_node(to_pos)
	local stack = ItemStack(item)
	if node and core.registered_nodes[node.name] then
		local def = core.registered_nodes[node.name]
		if def.tube and def.tube.insert_object then
			local leftover = def.tube.insert_object(to_pos, node, stack, velocity, owner)
			stack = leftover
			if stack:is_empty() then return false end
		end
	end
	return pipeworks.tube_inject_item(pos, start_pos, velocity, stack, owner) -- leftover
end

-- adding two tube functions
-- can_remove(pos,node,stack,dir) returns the maximum number of items of that stack that can be removed
-- remove_items(pos,node,stack,dir,count) removes count items and returns them
-- both optional w/ sensible defaults and fallback to normal allow_* function

local default_adjlist = { { x = 0, y = 0, z = 1 }, { x = 0, y = 0, z = -1 }, { x = 0, y = 1, z = 0 }, { x = 0, y = -1, z = 0 }, { x = 1, y = 0, z = 0 }, { x = -1, y = 0, z = 0 } }

function pipeworks.notvel(tbl, vel)
	local tbl2 = {}
	for _, val in ipairs(tbl) do
		if val.x ~= -vel.x or val.y ~= -vel.y or val.z ~= -vel.z then table.insert(tbl2, val) end
	end
	return tbl2
end

local tube_item_count = {}

minetest.register_globalstep(function(dtime)
	if not luaentity.entities then
		return
	end
	tube_item_count = {}

	-- start removing entities after there are too much
	local too_many_entities = 1000

	local count = 0
	for _, entity in pairs(luaentity.entities) do
		if entity.name == "pipeworks:tubed_item" then
			local h = minetest.hash_node_position(vector.round(entity._pos))
			if h ~= nil and not minetest.is_nan(h) then
				count = count + 1
				tube_item_count[h] = (tube_item_count[h] or 0) + 1
			else
				entity:remove()
			end
		end
	end
	if count > too_many_entities then
		local to_remove = (count - too_many_entities)
		local removed = 0
		for _, entity in pairs(luaentity.entities) do
			if entity.name == "pipeworks:tubed_item" then
				entity:remove()
			end
			removed = removed + 1
			if removed >= to_remove then
				break
			end
		end
	end
end)



-- tube overload mechanism:
-- when the tube's item count (tracked in the above tube_item_count table)
-- exceeds the limit configured per tube, replace it with a broken one.

function pipeworks.break_tube(pos)
	local node = minetest.get_node(pos)
	local meta = minetest.get_meta(pos)
	meta:set_string("the_tube_was", minetest.serialize(node))
	minetest.swap_node(pos, { name = "pipeworks:broken_tube_1" })
	core.get_meta(pos):set_string("infotext", "Broken Tube")
	pipeworks.scan_for_tube_objects(pos)
end

local crunch_tube = function(pos, cnode, cmeta)
	if enable_max_limit then
		local h = minetest.hash_node_position(pos)
		local itemcount = tube_item_count[h] or 0
		if itemcount > max_tube_limit then
			pipeworks.logger("Warning - a tube at " ..
				minetest.pos_to_string(pos) .. " broke due to too many items (" .. itemcount .. ")")
			pipeworks.break_tube(pos)
		end
	end
end



local adjlist_cache = {}
-- compatibility behaviour for the existing can_go() callbacks,
-- which can only specify a list of possible positions.
-- the fact that minetest.deserialize used to be in this function just not cached hurts my soul
local function go_next_compat(pos, cnode, cmeta, cycledir, vel, stack, owner, tags)
	local next_positions = {}
	local max_priority = 0
	local can_go

	local def = minetest.registered_nodes[cnode.name]
	if def and def.tube and def.tube.can_go then
		can_go = def.tube.can_go(pos, cnode, vel, stack, tags)
	else
		local adjlist_string = minetest.get_meta(pos):get_string("adjlist")
		local adjlist
		if adjlist_string ~= "" then
			adjlist = adjlist_cache[adjlist_string]
			if not adjlist then
				adjlist_cache[adjlist_string] = core.deserialize(adjlist_string)
				adjlist = adjlist_cache[adjlist_string]
			end
		else
			adjlist = default_adjlist
		end
		if not adjlist then
			adjlist = default_adjlist
		end

		can_go = pipeworks.notvel(adjlist, vel)
	end
	-- can_go() is expected to return an array-like table of candidate offsets.
	-- for each one, look at the node at that offset and determine if it can accept the item.
	-- also note the prioritisation:
	-- if any tube is found with a greater priority than previously discovered,
	-- then the valid positions are reset and and subsequent positions under this are skipped.
	-- this has the effect of allowing only equal priorities to co-exist.
	for _, vect in ipairs(can_go) do
		local npos = vector.add(pos, vect)
		pipeworks.load_position(npos)
		local node = minetest.get_node(npos)
		local reg_node = minetest.registered_nodes[node.name]
		if reg_node then
			local tube_def = reg_node.tube
			local tubedevice = minetest.get_item_group(node.name, "tubedevice")
			local tube_priority = (tube_def and tube_def.priority) or 100
			if tubedevice > 0 and tube_priority >= max_priority then
				if not tube_def or not tube_def.can_insert or
					tube_def.can_insert(npos, node, stack, vect, owner) then
					if tube_priority > max_priority then
						max_priority = tube_priority
						next_positions = {}
					end
					next_positions[#next_positions + 1] = { pos = npos, vect = vect }
				end
			end
		end
	end

	-- indicate not found if no valid rules were picked up,
	-- and don't change the counter.
	if not next_positions[1] then
		return cycledir, false, nil, nil
	end

	-- otherwise rotate to the next output direction and return that
	local n = (cycledir % (#next_positions)) + 1
	local new_velocity = vector.multiply(next_positions[n].vect, vel.speed)
	return n, true, new_velocity, nil
end




-- function called by the on_step callback of the pipeworks tube luaentity.
-- the routine is passed the current node position, velocity, itemstack,
-- and owner name.
-- returns three values:
-- * a boolean "found destination" status;
-- * a new velocity vector that the tubed item should use, or nil if not found;
-- * a "multi-mode" data table (or nil if N/A) where a stack was split apart.
--	if this is not nil, the luaentity spawns new tubed items for each new fragment stack,
--	then deletes itself (i.e. the original item stack).
local function go_next(pos, velocity, stack, owner, tags)
	local cnode = minetest.get_node(pos)
	if cnode.name == "ignore" then
		return false, velocity, nil
	end
	local cmeta = minetest.get_meta(pos)
	local speed = math.abs(velocity.x + velocity.y + velocity.z)
	if speed == 0 then
		speed = 1
	end
	local vel = { x = velocity.x / speed, y = velocity.y / speed, z = velocity.z / speed, speed = speed }
	if speed >= 4.1 then
		speed = 4
	elseif speed >= 1.1 then
		speed = speed - 0.1
	else
		speed = 1
	end
	vel.speed = speed

	crunch_tube(pos, cnode, cmeta)
	-- cycling of outputs:
	-- an integer counter is kept in each pipe's metadata,
	-- which allows tracking which output was previously chosen.
	-- note reliance on get_int returning 0 for uninitialised.
	local cycledir = cmeta:get_int("tubedir")

	-- pulled out and factored out into go_next_compat() above.
	-- n is the new value of the cycle counter.
	local n, found, new_velocity, multimode = go_next_compat(pos, cnode, cmeta, cycledir, vel, stack, owner, tags)

	-- if not using output cycling,
	-- don't update the field so it stays the same for the next item.
	if pipeworks.enable_cyclic_mode then
		cmeta:set_int("tubedir", n)
	end
	return found, new_velocity, multimode
end



minetest.register_entity("pipeworks:tubed_item", {
	initial_properties = {
		hp_max = 1,
		physical = false,
		collisionbox = { 0.1, 0.1, 0.1, 0.1, 0.1, 0.1 },
		visual = "wielditem",
		visual_size = { x = pipeworks.tube_size, y = pipeworks.tube_size },
		textures = { "" },
		spritediv = { x = 1, y = 1 },
		initial_sprite_basepos = { x = 0, y = 0 },
		is_visible = false,
	},

	physical_state = false,

	from_data = function(self, itemstring)
		local stack = ItemStack(itemstring)
		--[[
		local itemtable = stack:to_table()
		local itemname = nil
		if itemtable then
			itemname = stack:to_table().name
		end
		local item_texture = nil
		local item_type = ""
		if minetest.registered_items[itemname] then
			item_texture = minetest.registered_items[itemname].inventory_image
			item_type = minetest.registered_items[itemname].type
		end
		--]]
		self.object:set_properties({
			is_visible = true,
			textures = { stack:get_name() }
		})

		local def = stack:get_definition()
		self.object:set_yaw((def and def.type == "node") and 0 or math.pi * 0.25)
	end,

	get_staticdata = luaentity.get_staticdata,
	on_activate = function(self, staticdata) -- Legacy code, should be replaced later by luaentity.on_activate
		if staticdata == "" or staticdata == nil then
			return
		end
		if staticdata == "toremove" then
			self.object:remove()
			return
		end
		local item = minetest.deserialize(staticdata)
		pipeworks.tube_inject_item(self.object:get_pos(), item.start_pos, item.velocity, item.itemstring)
		self.object:remove()
	end,
})

minetest.register_entity("pipeworks:color_entity", {
	initial_properties = {
		hp_max = 1,
		physical = false,
		collisionbox = { 0.1, 0.1, 0.1, 0.1, 0.1, 0.1 },
		visual = "cube",
		visual_size = { x = 3.5, y = 3.5, z = 3.5 }, -- todo: find correct size
		textures = { "" },
		is_visible = false,
	},

	physical_state = false,

	from_data = function(self, color)
		local t = "pipeworks_color_" .. color .. ".png"
		local prop = {
			is_visible = true,
			visual = "cube",
			textures = { t, t, t, t, t, t } -- todo: textures
		}
		self.object:set_properties(prop)
	end,

	get_staticdata = luaentity.get_staticdata,
	on_activate = luaentity.on_activate,
})

-- see below for usage:
-- determine if go_next returned a multi-mode set.
local is_multimode = function(v)
	return (type(v) == "table") and (v.__multimode)
end

luaentity.register_entity("pipeworks:tubed_item", {
	itemstring = '',
	item_entity = nil,
	color_entity = nil,
	color = nil,
	start_pos = nil,
	tags = nil,

	set_item = function(self, item)
		local itemstring = ItemStack(item):to_string() -- Accept any input format
		if self.itemstring == itemstring then
			return
		end
		if self.item_entity then
			self:remove_attached_entity(self.item_entity)
		end
		self.itemstring = itemstring
		self.item_entity = self:add_attached_entity("pipeworks:tubed_item", itemstring)
	end,

	set_color = function(self, color)
		if self.color == color then
			return
		end
		self.color = color
		if self.color_entity then
			self:remove_attached_entity(self.color_entity)
		end
		if color then
			self.color_entity = self:add_attached_entity("pipeworks:color_entity", color)
		else
			self.color_entity = nil
		end
	end,

	on_step = function(self, dtime)
		local pos = self:get_pos()
		if self.start_pos == nil then
			self.start_pos = vector.round(pos)
			self:set_pos(pos)
		end

		local velocity = self:get_velocity()

		local moved = false
		local speed = math.abs(velocity.x + velocity.y + velocity.z)
		if speed == 0 then
			speed = 1
			moved = true
		end
		local vel = { x = velocity.x / speed, y = velocity.y / speed, z = velocity.z / speed, speed = speed }
		local moved_by = vector.distance(pos, self.start_pos)

		if moved_by >= 1 then
			self.start_pos = vector.add(self.start_pos, vel)
			moved = true
		end

		if not moved then
			return
		end

		local stack = ItemStack(self.itemstring)

		pipeworks.load_position(self.start_pos)
		local node = minetest.get_node(self.start_pos)
		if minetest.get_item_group(node.name, "tubedevice_receiver") == 1 then
			local oldstack = stack
			local leftover
			local def = minetest.registered_nodes[node.name]
			if def.tube and def.tube.insert_object then
				leftover = def.tube.insert_object(self.start_pos, node, stack, vel, self.owner)
			else
				leftover = stack
			end
			if leftover:is_empty() then
				self:remove()
				return
			end
			velocity = vector.multiply(velocity, -1)
			-- this is to prevent massive amounts of tubed entities with wielders, added in sbz
			local to_return = vector.add(self.start_pos, velocity)
			local to_return_node = core.get_node(to_return)
			local to_return_def = core.registered_nodes[to_return_node.name]
			if to_return_def.tube and to_return_def.tube.can_insert and oldstack:equals(leftover) then
				local can_insert = to_return_def.tube.can_insert(to_return, to_return_node, stack, velocity, self.owner)
				if not can_insert then -- drop itself
					local dropped_item = minetest.add_item(self.start_pos, stack)
					if dropped_item then
						dropped_item:set_velocity(vector.multiply(velocity, 2))
						self:remove()
					end
					return
				end
			end

			self:set_pos(vector.subtract(self.start_pos, vector.multiply(vel, moved_by - 1)))
			self:set_velocity(velocity)
			self:set_item(leftover:to_string())
			return
		end

		local tags
		if pipeworks.enable_item_tags then
			tags = self.tags or {}
		end
		local found_next, new_velocity, multimode = go_next(self.start_pos, velocity, stack, self.owner, tags) -- todo: color
		if pipeworks.enable_item_tags then
			self.tags = #tags > 0 and tags or nil
		end
		local rev_vel = vector.multiply(velocity, -1)
		local rev_dir = vector.direction(self.start_pos, vector.add(self.start_pos, rev_vel))
		local rev_node = minetest.get_node(vector.round(vector.add(self.start_pos, rev_dir)))
		local tube_present = minetest.get_item_group(rev_node.name, "tubedevice") == 1
		if not found_next then
			if pipeworks.drop_on_routing_fail or not tube_present or
				minetest.get_item_group(rev_node.name, "tube") ~= 1 then
				-- Using add_item instead of item_drop since this makes pipeworks backward
				-- compatible with Minetest 0.4.13.
				-- Using item_drop here makes Minetest 0.4.13 crash.
				local dropped_item = minetest.add_item(self.start_pos, stack)
				if dropped_item then
					dropped_item:set_velocity(vector.multiply(velocity, 5))
					self:remove()
				end
				return
			else
				velocity = vector.multiply(velocity, -1)
				self:set_pos(vector.subtract(self.start_pos, vector.multiply(vel, moved_by - 1)))
				self:set_velocity(velocity)
			end
		elseif is_multimode(multimode) then
			-- create new stacks according to returned data.
			local s = self.start_pos
			for _, split in ipairs(multimode) do
				pipeworks.tube_inject_item(s, s, split.velocity, split.itemstack, self.owner)
			end
			-- remove ourself now the splits are sent
			self:remove()
			return
		end

		if new_velocity and not vector.equals(velocity, new_velocity) then
			local nvelr = math.abs(new_velocity.x + new_velocity.y + new_velocity.z)
			self:set_pos(vector.add(self.start_pos, vector.multiply(new_velocity, (moved_by - 1) / nvelr)))
			self:set_velocity(new_velocity)
		end
	end
})

if minetest.get_modpath("mesecons_mvps") then
	mesecon.register_mvps_unmov("pipeworks:tubed_item")
	mesecon.register_mvps_unmov("pipeworks:color_entity")
	mesecon.register_on_mvps_move(function(moved_nodes)
		local moved = {}
		for _, n in ipairs(moved_nodes) do
			moved[minetest.hash_node_position(n.oldpos)] = vector.subtract(n.pos, n.oldpos)
		end
		for _, entity in pairs(luaentity.entities) do
			if entity.name == "pipeworks:tubed_item" then
				local pos = entity:get_pos()
				local rpos = vector.round(pos)
				local dir = moved[minetest.hash_node_position(rpos)]
				if dir then
					entity:set_pos(vector.add(pos, dir))
					entity.start_pos = vector.add(entity.start_pos, dir)
				end
			end
		end
	end)
end
