local S = minetest.get_translator(minetest.get_current_modname())

local SCAN_2_MAP_CHUNKS = true -- slower but helps to find more suitable places

-- Localize functions for better performance
local abs = math.abs
local ceil = math.ceil
local floor = math.floor
local max = math.max
local min = math.min
local random = math.random
local dist = vector.distance
local add = vector.add
local mul = vector.multiply
local sub = vector.subtract

-- Setup
local W_MIN, W_MAX			= 4, 23
local H_MIN, H_MAX			= 5, 23
local N_MIN, N_MAX			= 6, (W_MAX-2) * (H_MAX-2)
local TRAVEL_X, TRAVEL_Y, TRAVEL_Z	= 8, 1, 8
local LIM_MIN, LIM_MAX			= mcl_vars.mapgen_edge_min, mcl_vars.mapgen_edge_max
local PLAYER_COOLOFF, MOB_COOLOFF	= 3, 14 -- for this many seconds they won't teleported again
local TOUCH_CHATTER_TIME		= 1 -- prevent multiple teleportation attempts caused by multiple portal touches, for this number of seconds
local CHATTER_US			= TOUCH_CHATTER_TIME * 1000000
local DELAY				= 3 -- seconds before teleporting in Nether portal in Survival mode (4 minus ABM interval time)
local DISTANCE_MAX			= 128
local PORTAL				= "mcl_portals:portal"
local OBSIDIAN				= "mcl_core:obsidian"
local O_Y_MIN, O_Y_MAX			= max(mcl_vars.mg_overworld_min, -31), min(mcl_vars.mg_overworld_max, 2048)
local N_Y_MIN, N_Y_MAX			= mcl_vars.mg_bedrock_nether_bottom_min, mcl_vars.mg_bedrock_nether_top_min - H_MIN

-- Alpha and particles
local node_particles_allowed = minetest.settings:get("mcl_node_particles") or "none"
local node_particles_levels = { none=0, low=1, medium=2, high=3 }
local PARTICLES = node_particles_levels[node_particles_allowed]

-- Table of objects (including players) which recently teleported by a
-- Nether portal. Those objects have a brief cooloff period before they
-- can teleport again. This prevents annoying back-and-forth teleportation.
local cooloff = {}
function mcl_portals.nether_portal_cooloff(object)
	return cooloff[object]
end

local chatter = {}

local queue = {}
local chunks = {}

local storage = mcl_portals.storage
local exits = {}
local keys = minetest.deserialize(storage:get_string("nether_exits_keys") or "return {}") or {}
for _, key in pairs(keys) do
	local n = tonumber(key)
	if n then
		exits[key] = minetest.deserialize(storage:get_string("nether_exits_"..key) or "return {}") or {}
	end
end
minetest.register_on_shutdown(function()
	local keys={}
	for key, data in pairs(exits) do
		storage:set_string("nether_exits_"..tostring(key), minetest.serialize(data))
		keys[#keys+1] = key
	end
	storage:set_string("nether_exits_keys", minetest.serialize(keys))
end)

local get_node = mcl_vars.get_node
local set_node = minetest.set_node
local registered_nodes = minetest.registered_nodes
local is_protected = minetest.is_protected
local find_nodes_in_area = minetest.find_nodes_in_area
local find_nodes_in_area_under_air = minetest.find_nodes_in_area_under_air
local log = minetest.log
local pos_to_string = minetest.pos_to_string
local is_area_protected = minetest.is_area_protected
local get_us_time = minetest.get_us_time

local dimension_to_teleport = { nether = "overworld", overworld = "nether" }

local limits = {
	nether = {
		pmin = {x=LIM_MIN, y = N_Y_MIN, z = LIM_MIN},
		pmax = {x=LIM_MAX, y = N_Y_MAX, z = LIM_MAX},
	},
	overworld = {
		pmin = {x=LIM_MIN, y = O_Y_MIN, z = LIM_MIN},
		pmax = {x=LIM_MAX, y = O_Y_MAX, z = LIM_MAX},
	},
}

-- This function registers exits from Nether portals.
-- Incoming verification performed: two nodes must be portal nodes, and an obsidian below them.
-- If the verification passes - position adds to the table and saves to mod storage on exit.
local function add_exit(p)
	if not p or not p.y or not p.z or not p.x then return end
	local x, y, z = floor(p.x), floor(p.y), floor(p.z)
	local p = {x = x, y = y, z = z}
	if get_node({x=x,y=y-1,z=z}).name ~= OBSIDIAN or get_node(p).name ~= PORTAL or get_node({x=x,y=y+1,z=z}).name ~= PORTAL then return end
	local k = floor(z/256) * 256 + floor(x/256)
	if not exits[k] then
		exits[k]={}
	end
	local e = exits[k]
	for i = 1, #e do
		local t = e[i]
		if t and t.x == p.x and t.y == p.y and t.z == p.z then
			return
		end
	end
	e[#e+1] = p
	log("action", "[mcl_portals] Exit added at " .. pos_to_string(p))
end

-- This function removes Nether portals exits.
local function remove_exit(p)
	if not p or not p.y or not p.z or not p.x then return end
	local x, y, z = floor(p.x), floor(p.y), floor(p.z)
	local k = floor(z/256) * 256 + floor(x/256)
	if not exits[k] then return end
	local p = {x = x, y = y, z = z}
	local e = exits[k]
	if e then
		for i, t in pairs(e) do
			if t and t.x == x and t.y == y and t.z == z then
				e[i] = nil
				log("action", "[mcl_portals] Nether portal removed from " .. pos_to_string(p))
				return
			end
		end
	end
end

-- This functon searches Nether portal nodes whitin distance specified
local function find_exit(p, dx, dy, dz)
	if not p or not p.y or not p.z or not p.x then return end
	local dx, dy, dz = dx or DISTANCE_MAX, dy or DISTANCE_MAX, dz or DISTANCE_MAX
	if dx < 1 or dy < 1 or dz < 1 then return false end

    --y values aren't used
	local x = floor(p.x)
    --local y = floor(p.y)
    local z = floor(p.z)

	local x1 = x-dx+1
    --local y1 = y-dy+1
    local z1 = z-dz+1

    local x2 = x+dx-1
    --local y2 = y+dy-1
    local z2 = z+dz-1

	local k1x, k2x = floor(x1/256), floor(x2/256)
	local k1z, k2z = floor(z1/256), floor(z2/256)

	local t, d
	for kx = k1x, k2x do for kz = k1z, k2z do
		local k = kz*256 + kx
		local e = exits[k]
		if e then
			for _, t0 in pairs(e) do
				local d0 = dist(p, t0)
				if not d or d>d0 then
					d = d0
					t = t0
					if d==0 then return t end
				end
			end
		end
	end end

	if t and abs(t.x-p.x) <= dx and abs(t.y-p.y) <= dy and abs(t.z-p.z) <= dz then
		return t
	end
end


-- Ping-Pong the coordinate for Fast Travelling, https://git.minetest.land/Wuzzy/MineClone2/issues/795#issuecomment-11058
local function ping_pong(x, m, l1, l2)
	if x < 0 then
		return	 l1 + abs(((x*m+l1) % (l1*4)) - (l1*2)), 	floor(x*m/l1/2)  + ((ceil(x*m/l1)+1)%2) * ((x*m)%l1)/l1
	end
	return		 l2 - abs(((x*m+l2) % (l2*4)) - (l2*2)), 	floor(x*m/l2/2) + (floor(x*m/l2)%2) * ((x*m)%l2)/l2
end

local function get_target(p)
	if p and p.y and p.x and p.z then
		local x, z = p.x, p.z
		local y, d = mcl_worlds.y_to_layer(p.y)
		local o1, o2 -- y offset
		if y then
			if d=="nether" then
				x, o1 = ping_pong(x, TRAVEL_X, LIM_MIN, LIM_MAX)
				z, o2 = ping_pong(z, TRAVEL_Z, LIM_MIN, LIM_MAX)
				y = floor(y * TRAVEL_Y + (o1+o2) / 16 * LIM_MAX)
				y = min(max(y + O_Y_MIN, O_Y_MIN), O_Y_MAX)
			elseif d=="overworld" then
				x, y, z = floor(x / TRAVEL_X + 0.5), floor(y / TRAVEL_Y + 0.5), floor(z / TRAVEL_Z + 0.5)
				y = min(max(y + N_Y_MIN, N_Y_MIN), N_Y_MAX)
			end
			return {x=x, y=y, z=z}, d
		end
	end
end

-- Destroy a nether portal.  Connected portal nodes are searched and removed
-- using 'bulk_set_node'.  This function is called from 'after_destruct' of
-- nether portal nodes.  The flag 'destroying_portal' is used to avoid this
-- function being called recursively through callbacks in 'bulk_set_node'.
local destroying_portal = false
local function destroy_nether_portal(pos, node)
	if destroying_portal then
		return
	end
	destroying_portal = true

	local orientation = node.param2
	local checked_tab = { [minetest.hash_node_position(pos)] = true }
	local nodes = { pos }

	local function check_remove(pos)
		local h = minetest.hash_node_position(pos)
		if checked_tab[h] then
			return
		end

		local node = minetest.get_node(pos)
		if node and node.name == PORTAL and (orientation == nil or node.param2 == orientation) then
			table.insert(nodes, pos)
			checked_tab[h] = true
		end
	end

	local i = 1
	while i <= #nodes do
		pos = nodes[i]
		if orientation == 0 then
			check_remove({x = pos.x - 1, y = pos.y, z = pos.z})
			check_remove({x = pos.x + 1, y = pos.y, z = pos.z})
		else
			check_remove({x = pos.x, y = pos.y, z = pos.z - 1})
			check_remove({x = pos.x, y = pos.y, z = pos.z + 1})
		end
		check_remove({x = pos.x, y = pos.y - 1, z = pos.z})
		check_remove({x = pos.x, y = pos.y + 1, z = pos.z})
		remove_exit(pos)
		i = i + 1
	end

	minetest.bulk_set_node(nodes, { name = "air" })
	destroying_portal = false
end

local on_rotate
if minetest.get_modpath("screwdriver") then
	on_rotate = screwdriver.disallow
end

minetest.register_node(PORTAL, {
	description = S("Nether Portal"),
	_doc_items_longdesc = S("A Nether portal teleports creatures and objects to the hot and dangerous Nether dimension (and back!). Enter at your own risk!"),
	_doc_items_usagehelp = S("Stand in the portal for a moment to activate the teleportation. Entering a Nether portal for the first time will also create a new portal in the other dimension. If a Nether portal has been built in the Nether, it will lead to the Overworld. A Nether portal is destroyed if the any of the obsidian which surrounds it is destroyed, or if it was caught in an explosion."),

	tiles = {
		"blank.png",
		"blank.png",
		"blank.png",
		"blank.png",
		{
			name = "mcl_portals_portal.png",
			animation = {
				type = "vertical_frames",
				aspect_w = 16,
				aspect_h = 16,
				length = 1.25,
			},
		},
		{
			name = "mcl_portals_portal.png",
			animation = {
				type = "vertical_frames",
				aspect_w = 16,
				aspect_h = 16,
				length = 1.25,
			},
		},
	},
	drawtype = "nodebox",
	paramtype = "light",
	paramtype2 = "facedir",
	sunlight_propagates = true,
	use_texture_alpha = minetest.features.use_texture_alpha_string_modes and "blend" or true,
	walkable = false,
	buildable_to = false,
	is_ground_content = false,
	drop = "",
	light_source = 11,
	post_effect_color = {a = 180, r = 51, g = 7, b = 89},
	node_box = {
		type = "fixed",
		fixed = {
			{-0.5, -0.5, -0.1,  0.5, 0.5, 0.1},
		},
	},
	groups = { creative_breakable = 1, portal = 1, not_in_creative_inventory = 1 },
	sounds = mcl_sounds.node_sound_glass_defaults(),
	after_destruct = destroy_nether_portal,
	on_rotate = on_rotate,

	_mcl_hardness = -1,
	_mcl_blast_resistance = 0,
})

local function light_frame(x1, y1, z1, x2, y2, z2, name, node, node_frame)
	local orientation = 0
	if x1 == x2 then
		orientation = 1
	end
	local pos = {}
	local node = node or {name = PORTAL, param2 = orientation}
	local node_frame = node_frame or {name = OBSIDIAN}
	for x = x1 - 1 + orientation, x2 + 1 - orientation do
		pos.x = x
		for z = z1 - orientation, z2 + orientation do
			pos.z = z
			for y = y1 - 1, y2 + 1 do
				pos.y = y
				local frame = (x < x1) or (x > x2) or (y < y1) or (y > y2) or (z < z1) or (z > z2)
				if frame then
					set_node(pos, node_frame)
				else
					set_node(pos, node)
					add_exit({x=pos.x, y=pos.y-1, z=pos.z})
				end
			end
		end
	end
end

--Build arrival portal
function build_nether_portal(pos, width, height, orientation, name, clear_before_build)
	local width, height, orientation = width or W_MIN - 2, height or H_MIN - 2, orientation or random(0, 1)

	if clear_before_build then
		light_frame(pos.x, pos.y, pos.z, pos.x + (1 - orientation) * (width - 1), pos.y + height - 1, pos.z + orientation * (width - 1), name, {name="air"}, {name="air"})
	end
	light_frame(pos.x, pos.y, pos.z, pos.x + (1 - orientation) * (width - 1), pos.y + height - 1, pos.z + orientation * (width - 1), name)

	-- Build obsidian platform:
	for x = pos.x - orientation, pos.x + orientation + (width - 1) * (1 - orientation), 1 + orientation do
		for z = pos.z - 1 + orientation, pos.z + 1 - orientation + (width - 1) * orientation, 2 - orientation do
			local pp = {x = x, y = pos.y - 1, z = z}
			local pp_1 = {x = x, y = pos.y - 2, z = z}
			local nn = get_node(pp).name
			local nn_1 = get_node(pp_1).name
			if ((nn=="air" and nn_1 == "air") or not registered_nodes[nn].is_ground_content) and not is_protected(pp, name) then
				set_node(pp, {name = OBSIDIAN})
			end
		end
	end

	log("action", "[mcl_portals] Destination Nether portal generated at "..pos_to_string(pos).."!")

	return pos
end

function mcl_portals.spawn_nether_portal(pos, rot, pr, name)
	if not pos then return end
	local o = 0
	if rot then
		if rot == "270" or rot=="90" then
			o = 1
		elseif rot == "random" then
			o = random(0,1)
		end
	end
	build_nether_portal(pos, nil, nil, o, name, true)
end

-- Teleportation cooloff for some seconds, to prevent back-and-forth teleportation
local function stop_teleport_cooloff(o)
	cooloff[o] = nil
	chatter[o] = nil
end

local function teleport_cooloff(obj)
	cooloff[obj] = true
	if obj:is_player() then
		minetest.after(PLAYER_COOLOFF, stop_teleport_cooloff, obj)
	else
		minetest.after(MOB_COOLOFF, stop_teleport_cooloff, obj)
	end
end

local function finalize_teleport(obj, exit)
	if not obj or not exit or not exit.x or not exit.y or not exit.z then return end

	local objpos = obj:get_pos()
	if not objpos then return end

	local is_player = obj:is_player()
	local name
	if is_player then
		name = obj:get_player_name()
	end
	local _, dim = mcl_worlds.y_to_layer(exit.y)


	-- If player stands, player is at ca. something+0.5 which might cause precision problems, so we used ceil for objpos.y
	objpos = {x = floor(objpos.x+0.5), y = ceil(objpos.y), z = floor(objpos.z+0.5)}
	if get_node(objpos).name ~= PORTAL then return end

	-- THIS IS A TEMPORATY CODE SECTION FOR COMPATIBILITY REASONS -- 1 of 2 -- TODO: Remove --
	-- Old worlds have no exits indexed - adding the exit to return here:
	add_exit(objpos)
	-- TEMPORATY CODE SECTION ENDS HERE --


	-- Enable teleportation cooloff for some seconds, to prevent back-and-forth teleportation
	teleport_cooloff(obj)

	-- Teleport
	obj:set_pos(exit)

	if is_player then
		mcl_worlds.dimension_change(obj, dim)
		minetest.sound_play("mcl_portals_teleport", {pos=exit, gain=0.5, max_hear_distance = 16}, true)
		log("action", "[mcl_portals] player "..name.." teleported to Nether portal at "..pos_to_string(exit)..".")
		if dim == "nether" then
			awards.unlock(obj:get_player_name(), "mcl:theNether")
		end
	else
		log("action", "[mcl_portals] entity teleported to Nether portal at "..pos_to_string(exit)..".")
	end
end

local function create_portal_2(pos1, name, obj)
	local orientation = 0
	local pos2 = {x = pos1.x + 3, y = pos1.y + 3, z = pos1.z + 3}
	local nodes = find_nodes_in_area(pos1, pos2, {"air"})
	if #nodes == 64 then
		orientation = random(0,1)
	else
		pos2.x = pos2.x - 1
		nodes = find_nodes_in_area(pos1, pos2, {"air"})
		if #nodes == 48 then
			orientation = 1
		end
	end
	local exit = build_nether_portal(pos1, W_MIN-2, H_MIN-2, orientation, name)
	finalize_teleport(obj, exit)
	local cn = mcl_vars.get_chunk_number(pos1)
	chunks[cn] = nil
	if queue[cn] then
		for next_obj, _ in pairs(queue[cn]) do
			if next_obj ~= obj then
				finalize_teleport(next_obj, exit)
			end
		end
		queue[cn] = nil
	end
end

local function get_lava_level(pos, pos1, pos2)
	if pos.y > -1000 then
		return max(min(mcl_vars.mg_lava_overworld_max, pos2.y-1), pos1.y+1)
	end
	return max(min(mcl_vars.mg_lava_nether_max, pos2.y-1), pos1.y+1)
end

local function ecb_scan_area_2(blockpos, action, calls_remaining, param)
	if calls_remaining and calls_remaining > 0 then return end
	local pos, pos1, pos2, name, obj = param.pos, param.pos1, param.pos2, param.name or "", param.obj
	local pos0, distance
	local lava = get_lava_level(pos, pos1, pos2)

	-- THIS IS A TEMPORATY CODE SECTION FOR COMPATIBILITY REASONS -- 2 of 2 -- TODO: Remove --
	-- Find portals for old worlds (new worlds keep them all in the table):
	local portals = find_nodes_in_area(pos1, pos2, {PORTAL})
	if portals and #portals>0 then
		for _, p in pairs(portals) do
			add_exit(p)
		end
		local exit = find_exit(pos)
		if exit then
			finalize_teleport(obj, exit)
		end
		return
	end
	-- TEMPORATY CODE SECTION ENDS HERE --


	local nodes = find_nodes_in_area_under_air(pos1, pos2, {"group:building_block"})
	if nodes then
		local nc = #nodes
		log("action", "[mcl_portals] Area for destination Nether portal emerged! Found " .. tostring(nc) .. " nodes under the air around "..pos_to_string(pos))
		if nc > 0 then
			for i=1,nc do
				local node = nodes[i]
				local node1 = {x=node.x,   y=node.y+1, z=node.z  }
				local node2 = {x=node.x+2, y=node.y+3, z=node.z+2}
				local nodes2 = find_nodes_in_area(node1, node2, {"air"})
				if nodes2 then
					local nc2 = #nodes2
					if nc2 == 27 and not is_area_protected(node, node2, name) then
						local distance0 = dist(pos, node)
						if distance0 < 2 then
							log("action", "[mcl_portals] found space at pos "..pos_to_string(node).." - creating a portal")
							create_portal_2(node1, name, obj)
							return
						end
						if not distance or (distance0 < distance) or (distance0 < distance-1 and node.y > lava and pos0.y < lava) then
							log("verbose", "[mcl_portals] found distance "..tostring(distance0).." at pos "..pos_to_string(node))
							distance = distance0
							pos0 = {x=node1.x, y=node1.y, z=node1.z}
						end
					end
				end
			end
		end
	end
	if distance then -- several nodes of air might be better than lava lake, right?
		log("action", "[mcl_portals] using backup pos "..pos_to_string(pos0).." to create a portal")
		create_portal_2(pos0, name, obj)
		return
	end

	if param.next_chunk_1 and param.next_chunk_2 and param.next_pos then
		local pos1, pos2, p = param.next_chunk_1, param.next_chunk_2, param.next_pos
		if p.x >= pos1.x and p.x <= pos2.x and p.y >= pos1.y and p.y <= pos2.y and p.z >= pos1.z and p.z <= pos2.z then
			log("action", "[mcl_portals] Making additional search in chunk below, because current one doesn't contain any air space for portal, target pos "..pos_to_string(p))
			minetest.emerge_area(pos1, pos2, ecb_scan_area_2, {pos = p, pos1 = pos1, pos2 = pos2, name=name, obj=obj})
			return
		end
	end

	log("action", "[mcl_portals] found no space, reverting to target pos "..pos_to_string(pos).." - creating a portal")
	if pos.y < lava then
		pos.y = lava + 1
	else
		pos.y = pos.y + 1
	end
	create_portal_2(pos, name, obj)
end

local function create_portal(pos, limit1, limit2, name, obj)
	local cn = mcl_vars.get_chunk_number(pos)
	if chunks[cn] then
		local q = queue[cn] or {}
		q[obj] = true
		queue[cn] = q
		return
	end
	chunks[cn] = true

	-- we need to emerge the area here, but currently (mt5.4/mcl20.71) map generation is slow
	-- so we'll emerge single chunk only: 5x5x5 blocks, 80x80x80 nodes maximum
	-- and maybe one more chunk from below if (SCAN_2_MAP_CHUNKS = true)

	local pos1 = add(mul(mcl_vars.pos_to_chunk(pos), mcl_vars.chunk_size_in_nodes), mcl_vars.central_chunk_offset_in_nodes)
	local pos2 = add(pos1, mcl_vars.chunk_size_in_nodes - 1)

	if not SCAN_2_MAP_CHUNKS then
		if limit1 and limit1.x and limit1.y and limit1.z then
			pos1 = {x = max(min(limit1.x, pos.x), pos1.x), y = max(min(limit1.y, pos.y), pos1.y), z = max(min(limit1.z, pos.z), pos1.z)}
		end
		if limit2 and limit2.x and limit2.y and limit2.z  then
			pos2 = {x = min(max(limit2.x, pos.x), pos2.x), y = min(max(limit2.y, pos.y), pos2.y), z = min(max(limit2.z, pos.z), pos2.z)}
		end
		minetest.emerge_area(pos1, pos2, ecb_scan_area_2, {pos = vector.new(pos), pos1 = pos1, pos2 = pos2, name=name, obj=obj})
		return
	end

	-- Basically the copy of code above, with minor additions to continue the search in single additional chunk below:
	local next_chunk_1 = {x = pos1.x, y = pos1.y - mcl_vars.chunk_size_in_nodes, z = pos1.z}
	local next_chunk_2 = add(next_chunk_1, mcl_vars.chunk_size_in_nodes - 1)
	local next_pos = {x = pos.x, y=max(next_chunk_2.y, limit1.y), z = pos.z}
	if limit1 and limit1.x and limit1.y and limit1.z then
		pos1 = {x = max(min(limit1.x, pos.x), pos1.x), y = max(min(limit1.y, pos.y), pos1.y), z = max(min(limit1.z, pos.z), pos1.z)}
		next_chunk_1 = {x = max(min(limit1.x, next_pos.x), next_chunk_1.x), y = max(min(limit1.y, next_pos.y), next_chunk_1.y), z = max(min(limit1.z, next_pos.z), next_chunk_1.z)}
	end
	if limit2 and limit2.x and limit2.y and limit2.z  then
		pos2 = {x = min(max(limit2.x, pos.x), pos2.x), y = min(max(limit2.y, pos.y), pos2.y), z = min(max(limit2.z, pos.z), pos2.z)}
		next_chunk_2 = {x = min(max(limit2.x, next_pos.x), next_chunk_2.x), y = min(max(limit2.y, next_pos.y), next_chunk_2.y), z = min(max(limit2.z, next_pos.z), next_chunk_2.z)}
	end
	minetest.emerge_area(pos1, pos2, ecb_scan_area_2, {pos = vector.new(pos), pos1 = pos1, pos2 = pos2, name=name, obj=obj, next_chunk_1 = next_chunk_1, next_chunk_2 = next_chunk_2, next_pos = next_pos})
end

local function available_for_nether_portal(p)
	local nn = get_node(p).name
	local obsidian = nn == OBSIDIAN
	if nn ~= "air" and minetest.get_item_group(nn, "fire") ~= 1 then
		return false, obsidian
	end
	return true, obsidian
end

local function check_and_light_shape(pos, orientation)
	local stack = {{x = pos.x, y = pos.y, z = pos.z}}
	local node_list = {}
	local index_list = {}
	local node_counter = 0
	-- Search most low node from the left (pos1) and most right node from the top (pos2)
	local pos1 = {x = pos.x, y = pos.y, z = pos.z}
	local pos2 = {x = pos.x, y = pos.y, z = pos.z}

	local kx, ky, kz = pos.x - 1999, pos.y - 1999, pos.z - 1999
	while #stack > 0 do
		local i = #stack
		local x, y, z = stack[i].x, stack[i].y, stack[i].z
		local k = (x-kx)*16000000 + (y-ky)*4000 + z-kz
		if index_list[k] then
			stack[i] = nil -- Already checked, skip it
		else
			local good, obsidian = available_for_nether_portal(stack[i])
			if obsidian then
				stack[i] = nil
			else
				if (not good) or (node_counter >= N_MAX) then
					return false
				end
				node_counter = node_counter + 1
				node_list[node_counter] = {x = x, y = y, z = z}
				index_list[k] = true
				stack[i].y = y - 1
				stack[i + 1] = {x = x, y = y + 1, z = z}
				if orientation == 0 then
					stack[i + 2] = {x = x - 1, y = y, z = z}
					stack[i + 3] = {x = x + 1, y = y, z = z}
				else
					stack[i + 2] = {x = x, y = y, z = z - 1}
					stack[i + 3] = {x = x, y = y, z = z + 1}
				end
				if (y < pos1.y) or (y == pos1.y and (x < pos1.x or z < pos1.z)) then
					pos1 = {x = x, y = y, z = z}
				end
				if (x > pos2.x or z > pos2.z) or (x == pos2.x and z == pos2.z and y > pos2.y) then
					pos2 = {x = x, y = y, z = z}
				end
			end
		end
	end

	if node_counter < N_MIN then
		return false
	end

	-- Limit rectangles width and height
	if abs(pos2.x - pos1.x + pos2.z - pos1.z) + 3 > W_MAX or abs(pos2.y - pos1.y) + 3 > H_MAX then
		return false
	end

	for i = 1, node_counter do
		local node_pos = node_list[i]
		minetest.set_node(node_pos, {name = PORTAL, param2 = orientation})
		add_exit(node_pos)
	end
	return true
end

-- Attempts to light a Nether portal at pos
-- Pos can be any of the inner part.
-- The frame MUST be filled only with air or any fire, which will be replaced with Nether portal blocks.
-- If no Nether portal can be lit, nothing happens.
-- Returns true if portal created
function mcl_portals.light_nether_portal(pos)
	-- Only allow to make portals in Overworld and Nether
	local dim = mcl_worlds.pos_to_dimension(pos)
	if dim ~= "overworld" and dim ~= "nether" then
		return false
	end
	local orientation = random(0, 1)
	for orientation_iteration = 1, 2 do
		if check_and_light_shape(pos, orientation) then
			return true
		end
		orientation = 1 - orientation
	end
	return false
end

-- Teleport function
local function teleport_no_delay(obj, pos)
	local is_player = obj:is_player()
	if (not is_player and not obj:get_luaentity()) or cooloff[obj] then return end

	local objpos = obj:get_pos()
	if not objpos then return end

	-- If player stands, player is at ca. something+0.5 which might cause precision problems, so we used ceil for objpos.y
	objpos = {x = floor(objpos.x+0.5), y = ceil(objpos.y), z = floor(objpos.z+0.5)}
	if get_node(objpos).name ~= PORTAL then return end

	local target, dim = get_target(objpos)
	if not target then return end

	local name
	if is_player then
		name = obj:get_player_name()
	end

	local exit = find_exit(target)
	if exit then
		finalize_teleport(obj, exit)
	else
		dim = dimension_to_teleport[dim]
		-- need to create arrival portal
		create_portal(target, limits[dim].pmin, limits[dim].pmax, name, obj)
	end
end

local function prevent_portal_chatter(obj)
	local time_us = get_us_time()
	local ch = chatter[obj] or 0
	chatter[obj] = time_us
	minetest.after(TOUCH_CHATTER_TIME, function(o)
		if o and chatter[o] and get_us_time() - chatter[o] >= CHATTER_US then
			chatter[o] = nil
		end
	end, obj)
	return time_us - ch > CHATTER_US
end

local function animation(player, playername)
	local ch = chatter[player] or 0
	if cooloff[player] or get_us_time() - ch < CHATTER_US then
		local pos = player:get_pos()
		if not pos then
			return
		end
		minetest.add_particlespawner({
			amount = 1,
			minpos = {x = pos.x - 0.1, y = pos.y + 1.4, z = pos.z - 0.1},
			maxpos = {x = pos.x + 0.1, y = pos.y + 1.6, z = pos.z + 0.1},
			minvel = 0,
			maxvel = 0,
			minacc = 0,
			maxacc = 0,
			minexptime = 0.1,
			maxexptime = 0.2,
			minsize = 5,
			maxsize = 15,
			collisiondetection = false,
			texture = "mcl_particles_nether_portal_t.png",
			playername = playername,
		})
		minetest.after(0.3, animation, player, playername)
	end
end

local function teleport(obj, portal_pos)
	local name = ""
	if obj:is_player() then
		name = obj:get_player_name()
		animation(obj, name)
	end

	if cooloff[obj] then return end

	if minetest.is_creative_enabled(name) then
		teleport_no_delay(obj, portal_pos)
		return
	end

	minetest.after(DELAY, teleport_no_delay, obj, portal_pos)
end

minetest.register_abm({
	label = "Nether portal teleportation and particles",
	nodenames = {PORTAL},
	interval = 1,
	chance = 1,
	action = function(pos, node)
		local o = node.param2		-- orientation
		local d = random(0, 1)	-- direction
		local time = random() * 1.9 + 0.5
		local velocity, acceleration
		if o == 1 then
			velocity	= {x = random() * 0.7 + 0.3,	y = random() - 0.5,	z = random() - 0.5}
			acceleration	= {x = random() * 1.1 + 0.3,	y = random() - 0.5,	z = random() - 0.5}
		else
			velocity	= {x = random() - 0.5,		y = random() - 0.5,	z = random() * 0.7 + 0.3}
			acceleration	= {x = random() - 0.5,		y = random() - 0.5,	z = random() * 1.1 + 0.3}
		end
		local distance = add(mul(velocity, time), mul(acceleration, time * time / 2))
		if d == 1 then
			if o == 1 then
				distance.x	= -distance.x
				velocity.x	= -velocity.x
				acceleration.x	= -acceleration.x
			else
				distance.z	= -distance.z
				velocity.z	= -velocity.z
				acceleration.z	= -acceleration.z
			end
		end
		distance = sub(pos, distance)
		for _, obj in pairs(minetest.get_objects_inside_radius(pos, 15)) do
			if obj:is_player() then
				minetest.add_particlespawner({
					amount = PARTICLES + 1,
					minpos = distance,
					maxpos = distance,
					minvel = velocity,
					maxvel = velocity,
					minacc = acceleration,
					maxacc = acceleration,
					minexptime = time,
					maxexptime = time,
					minsize = 0.3,
					maxsize = 1.8,
					collisiondetection = false,
					texture = "mcl_particles_nether_portal.png",
					playername = obj:get_player_name(),
				})
			end
		end
		for _, obj in pairs(minetest.get_objects_inside_radius(pos, 1)) do	--maikerumine added for objects to travel
			local lua_entity = obj:get_luaentity()				--maikerumine added for objects to travel
			if (obj:is_player() or lua_entity) and prevent_portal_chatter(obj) then
				teleport(obj, pos)
			end
		end
	end,
})


--[[ ITEM OVERRIDES ]]

local longdesc = registered_nodes[OBSIDIAN]._doc_items_longdesc
longdesc = longdesc .. "\n" .. S("Obsidian is also used as the frame of Nether portals.")
local usagehelp = S("To open a Nether portal, place an upright frame of obsidian with a width of at least 4 blocks and a height of 5 blocks, leaving only air in the center. After placing this frame, light a fire in the obsidian frame. Nether portals only work in the Overworld and the Nether.")

minetest.override_item(OBSIDIAN, {
	_doc_items_longdesc = longdesc,
	_doc_items_usagehelp = usagehelp,
	after_destruct = function(pos, node)
		local function check_remove(pos, orientation)
			local node = get_node(pos)
			if node and node.name == PORTAL then
				minetest.remove_node(pos)
			end
		end

		-- check each of 6 sides of it and destroy every portal
		check_remove({x = pos.x - 1, y = pos.y, z = pos.z})
		check_remove({x = pos.x + 1, y = pos.y, z = pos.z})
		check_remove({x = pos.x, y = pos.y, z = pos.z - 1})
		check_remove({x = pos.x, y = pos.y, z = pos.z + 1})
		check_remove({x = pos.x, y = pos.y - 1, z = pos.z})
		check_remove({x = pos.x, y = pos.y + 1, z = pos.z})
	end,

	_on_ignite = function(user, pointed_thing)
		local x, y, z = pointed_thing.under.x, pointed_thing.under.y, pointed_thing.under.z
		-- Check empty spaces around obsidian and light all frames found:
		local portals_placed =
				mcl_portals.light_nether_portal({x = x - 1, y = y, z = z}) or mcl_portals.light_nether_portal({x = x + 1, y = y, z = z}) or
				mcl_portals.light_nether_portal({x = x, y = y - 1, z = z}) or mcl_portals.light_nether_portal({x = x, y = y + 1, z = z}) or
				mcl_portals.light_nether_portal({x = x, y = y, z = z - 1}) or mcl_portals.light_nether_portal({x = x, y = y, z = z + 1})
		if portals_placed then
			log("action", "[mcl_portals] Nether portal activated at "..pos_to_string({x=x,y=y,z=z})..".")
			if minetest.get_modpath("doc") then
				doc.mark_entry_as_revealed(user:get_player_name(), "nodes", PORTAL)

				-- Achievement for finishing a Nether portal TO the Nether
				local dim = mcl_worlds.pos_to_dimension({x=x, y=y, z=z})
				if minetest.get_modpath("awards") and dim ~= "nether" and user:is_player() then
					awards.unlock(user:get_player_name(), "mcl:buildNetherPortal")
				end
			end
			return true
		else
			return false
		end
	end,
})