worldedit = worldedit or {}

--wip: test the entire API again to make sure it works
--wip: remove env parameter where no longer needed in chat commands module

--modifies positions `pos1` and `pos2` so that each component of `pos1` is less than or equal to its corresponding conent of `pos2`, returning two new positions
worldedit.sort_pos = function(pos1, pos2)
	pos1 = {x=pos1.x, y=pos1.y, z=pos1.z}
	pos2 = {x=pos2.x, y=pos2.y, z=pos2.z}
	if pos1.x > pos2.x then
		pos2.x, pos1.x = pos1.x, pos2.x
	end
	if pos1.y > pos2.y then
		pos2.y, pos1.y = pos1.y, pos2.y
	end
	if pos1.z > pos2.z then
		pos2.z, pos1.z = pos1.z, pos2.z
	end
	return pos1, pos2
end

--determines the volume of the region defined by positions `pos1` and `pos2`, returning the volume
worldedit.volume = function(pos1, pos2)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
	return (pos2.x - pos1.x + 1) * (pos2.y - pos1.y + 1) * (pos2.z - pos1.z + 1)
end

--sets a region defined by positions `pos1` and `pos2` to `nodename`, returning the number of nodes filled
worldedit.set = function(pos1, pos2, nodename)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)

	local size = {x=pos2.x - pos1.x, y=pos2.y - pos1.y, z=pos2.z - pos1.z}
	local nodes = {}

	--fill nodes table with node to be set
	local node = {name=nodename, param1=0, param2=0}
	for i = 1, (size.x * size.y * size.z) do
		nodes[i] = node
	end

	minetest.place_schematic(pos1, {size=size, data=nodes})
	return worldedit.volume(pos1, pos2)
end

--replaces all instances of `searchnode` with `replacenode` in a region defined by positions `pos1` and `pos2`, returning the number of nodes replaced
worldedit.replace = function(pos1, pos2, searchnode, replacenode)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)

	local node = {name=replacenode}
	local add_node = minetest.add_node
	local nodes = minetest.find_nodes_in_area(pos1, pos2, searchnode)
	for _, pos in ipairs(nodes) do
		add_node(pos, node)
	end
	return #nodes
end

--replaces all nodes other than `searchnode` with `replacenode` in a region defined by positions `pos1` and `pos2`, returning the number of nodes replaced
worldedit.replaceinverse = function(pos1, pos2, searchnode, replacenode)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)

	local pos = {x=pos1.x, y=0, z=0}
	local node = {name=replacenode}
	local get_node, add_node = minetest.get_node, minetest.add_node
	local count = 0
	while pos.x <= pos2.x do
		pos.y = pos1.y
		while pos.y <= pos2.y do
			pos.z = pos1.z
			while pos.z <= pos2.z do
				local name = get_node(pos).name
				if name ~= "ignore" and name ~= searchnode then
					add_node(pos, node)
					count = count + 1
				end
				pos.z = pos.z + 1
			end
			pos.y = pos.y + 1
		end
		pos.x = pos.x + 1
	end
	return count
end

--copies the region defined by positions `pos1` and `pos2` along the `axis` axis ("x" or "y" or "z") by `amount` nodes, returning the number of nodes copied
worldedit.copy = function(pos1, pos2, axis, amount, env)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
	if env == nil then env = minetest.env end

	--wip: copy slice by slice using schematic method in the copy axis and transfer metadata in separate loop (and if the amount is greater than the length in the axis, copy whole thing at a time)
	if amount < 0 then
		local pos = {x=pos1.x, y=0, z=0}
		while pos.x <= pos2.x do
			pos.y = pos1.y
			while pos.y <= pos2.y do
				pos.z = pos1.z
				while pos.z <= pos2.z do
					local node = env:get_node(pos) --obtain current node
					local meta = env:get_meta(pos):to_table() --get meta of current node
					local value = pos[axis] --store current position
					pos[axis] = value + amount --move along axis
					env:add_node(pos, node) --copy node to new position
					env:get_meta(pos):from_table(meta) --set metadata of new node
					pos[axis] = value --restore old position
					pos.z = pos.z + 1
				end
				pos.y = pos.y + 1
			end
			pos.x = pos.x + 1
		end
	else
		local pos = {x=pos2.x, y=0, z=0}
		while pos.x >= pos1.x do
			pos.y = pos2.y
			while pos.y >= pos1.y do
				pos.z = pos2.z
				while pos.z >= pos1.z do
					local node = minetest.env:get_node(pos) --obtain current node
					local meta = env:get_meta(pos):to_table() --get meta of current node
					local value = pos[axis] --store current position
					pos[axis] = value + amount --move along axis
					minetest.env:add_node(pos, node) --copy node to new position
					env:get_meta(pos):from_table(meta) --set metadata of new node
					pos[axis] = value --restore old position
					pos.z = pos.z - 1
				end
				pos.y = pos.y - 1
			end
			pos.x = pos.x - 1
		end
	end
	return worldedit.volume(pos1, pos2)
end

--moves the region defined by positions `pos1` and `pos2` along the `axis` axis ("x" or "y" or "z") by `amount` nodes, returning the number of nodes moved
worldedit.move = function(pos1, pos2, axis, amount, env)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
	if env == nil then env = minetest.env end

	--wip: move slice by slice using schematic method in the move axis and transfer metadata in separate loop (and if the amount is greater than the length in the axis, copy whole thing at a time and erase original after, using schematic method)
	if amount < 0 then
		local pos = {x=pos1.x, y=0, z=0}
		while pos.x <= pos2.x do
			pos.y = pos1.y
			while pos.y <= pos2.y do
				pos.z = pos1.z
				while pos.z <= pos2.z do
					local node = env:get_node(pos) --obtain current node
					local meta = env:get_meta(pos):to_table() --get metadata of current node
					env:remove_node(pos)
					local value = pos[axis] --store current position
					pos[axis] = value + amount --move along axis
					env:add_node(pos, node) --move node to new position
					env:get_meta(pos):from_table(meta) --set metadata of new node
					pos[axis] = value --restore old position
					pos.z = pos.z + 1
				end
				pos.y = pos.y + 1
			end
			pos.x = pos.x + 1
		end
	else
		local pos = {x=pos2.x, y=0, z=0}
		while pos.x >= pos1.x do
			pos.y = pos2.y
			while pos.y >= pos1.y do
				pos.z = pos2.z
				while pos.z >= pos1.z do
					local node = env:get_node(pos) --obtain current node
					local meta = env:get_meta(pos):to_table() --get metadata of current node
					env:remove_node(pos)
					local value = pos[axis] --store current position
					pos[axis] = value + amount --move along axis
					env:add_node(pos, node) --move node to new position
					env:get_meta(pos):from_table(meta) --set metadata of new node
					pos[axis] = value --restore old position
					pos.z = pos.z - 1
				end
				pos.y = pos.y - 1
			end
			pos.x = pos.x - 1
		end
	end
	return worldedit.volume(pos1, pos2)
end

--duplicates the region defined by positions `pos1` and `pos2` along the `axis` axis ("x" or "y" or "z") `count` times, returning the number of nodes stacked
worldedit.stack = function(pos1, pos2, axis, count, env)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
	local length = pos2[axis] - pos1[axis] + 1
	if count < 0 then
		count = -count
		length = -length
	end
	local amount = 0
	local copy = worldedit.copy
	for i = 1, count do
		amount = amount + length
		copy(pos1, pos2, axis, amount, env)
	end
	return worldedit.volume(pos1, pos2)
end

--scales the region defined by positions `pos1` and `pos2` by an factor of positive integer `factor` with `pos1` as the origin, returning the number of nodes scaled, the new scaled position 1, and the new scaled position 2
worldedit.scale = function(pos1, pos2, factor)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)

	--prepare schematic of large node
	local get_node, get_meta, place_schematic = minetest.get_node, minetest.get_meta, minetest.place_schematic
	local placeholder_node = {name="", param1=0, param2=0}
	local nodes = {}
	for i = 1, size ^ 3 do
		nodes[i] = placeholder_node
	end
	local schematic = {size={x=size, y=size, z=size}, data=nodes}

	local pos = {x=pos2.x, y=0, z=0}
	local bigpos = {x=0, y=0, z=0}
	size = factor - 1
	while pos.x >= pos1.x do
		pos.y = pos2.y
		while pos.y >= pos1.y do
			pos.z = pos2.z
			while pos.z >= pos1.z do
				local node = get_node(pos) --obtain current node
				local meta = get_meta(pos):to_table() --get meta of current node

				local value = pos[axis] --store current position
				local posx, posy, posz = pos1.x + (pos.x - pos1.x) * factor, pos1.y + (pos.y - pos1.y) * factor, pos1.z + (pos.z - pos1.z) * factor

				--create large node
				placeholder_node[1], placeholder_node[3] = node.name, node.param2
				bigpos.x, bigpos.y, bigpos.z = posx, posy, posz
				place_schematic(bigpos, schematic)
				for x = 0, size do --fill in large node meta
					for y = 0, size do
						for z = 0, size do
							bigpos.x, bigpos.y, bigpos.z = posx + x, posy + y, posz + z
							get_meta(bigpos):from_table(meta) --set metadata of new node
						end
					end
				end
				pos.z = pos.z - 1
			end
			pos.y = pos.y - 1
		end
		pos.x = pos.x - 1
	end
	local newpos2 = {x=pos1.x + (pos2.x - pos1.x) * factor + size, y=pos1.y + (pos2.y - pos1.y) * factor + size, z=pos1.z + (pos2.z - pos1.z) * factor + size}
	return worldedit.volume(pos1, pos2), pos1, newpos2
end

--transposes a region defined by the positions `pos1` and `pos2` between the `axis1` and `axis2` axes, returning the number of nodes transposed, the new transposed position 1, and the new transposed position 2
worldedit.transpose = function(pos1, pos2, axis1, axis2, env)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)

	local compare
	local extent1, extent2 = pos2[axis1] - pos1[axis1], pos2[axis2] - pos1[axis2]

	if extent1 > extent2 then
		compare = function(extent1, extent2)
			return extent1 > extent2
		end
	else
		compare = function(extent1, extent2)
			return extent1 < extent2
		end
	end

	--calculate the new position 2 after transposition
	local newpos2 = {x=pos2.x, y=pos2.y, z=pos2.z}
	newpos2[axis1] = pos1[axis1] + extent2
	newpos2[axis2] = pos1[axis2] + extent1

	local pos = {x=pos1.x, y=0, z=0}
	local get_node, get_meta, add_node = minetest.get_node, minetest.get_meta, minetest.add_node
	while pos.x <= pos2.x do
		pos.y = pos1.y
		while pos.y <= pos2.y do
			pos.z = pos1.z
			while pos.z <= pos2.z do
				local extent1, extent2 = pos[axis1] - pos1[axis1], pos[axis2] - pos1[axis2]
				if compare(extent1, extent2) then --transpose only if below the diagonal
					local node1 = get_node(pos)
					local meta1 = get_meta(pos):to_table()
					local value1, value2 = pos[axis1], pos[axis2] --save position values
					pos[axis1], pos[axis2] = pos1[axis1] + extent2, pos1[axis2] + extent1 --swap axis extents
					local node2 = get_node(pos)
					local meta2 = get_meta(pos):to_table()
					add_node(pos, node1)
					get_meta(pos):from_table(meta1)
					pos[axis1], pos[axis2] = value1, value2 --restore position values
					add_node(pos, node2)
					get_meta(pos):from_table(meta2)
				end
				pos.z = pos.z + 1
			end
			pos.y = pos.y + 1
		end
		pos.x = pos.x + 1
	end
	return worldedit.volume(pos1, pos2), pos1, newpos2
end

--flips a region defined by the positions `pos1` and `pos2` along the `axis` axis ("x" or "y" or "z"), returning the number of nodes flipped
worldedit.flip = function(pos1, pos2, axis, env)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)

	--wip: flip the region slice by slice along the flip axis using schematic method
	local pos = {x=pos1.x, y=0, z=0}
	local start = pos1[axis] + pos2[axis]
	pos2[axis] = pos1[axis] + math.floor((pos2[axis] - pos1[axis]) / 2)
	if env == nil then env = minetest.env end
	while pos.x <= pos2.x do
		pos.y = pos1.y
		while pos.y <= pos2.y do
			pos.z = pos1.z
			while pos.z <= pos2.z do
				local node1 = env:get_node(pos)
				local meta1 = env:get_meta(pos):to_table()
				local value = pos[axis]
				pos[axis] = start - value
				local node2 = env:get_node(pos)
				local meta2 = env:get_meta(pos):to_table()
				env:add_node(pos, node1)
				env:get_meta(pos):from_table(meta1)
				pos[axis] = value
				env:add_node(pos, node2)
				env:get_meta(pos):from_table(meta2)
				pos.z = pos.z + 1
			end
			pos.y = pos.y + 1
		end
		pos.x = pos.x + 1
	end
	return worldedit.volume(pos1, pos2)
end

--rotates a region defined by the positions `pos1` and `pos2` by `angle` degrees clockwise around axis `axis` (90 degree increment), returning the number of nodes rotated
worldedit.rotate = function(pos1, pos2, axis, angle, env)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)

	local axis1, axis2
	if axis == "x" then
		axis1, axis2 = "z", "y"
	elseif axis == "y" then
		axis1, axis2 = "x", "z"
	else --axis == "z"
		axis1, axis2 = "y", "x"
	end
	angle = angle % 360

	local count
	if angle == 90 then
		worldedit.flip(pos1, pos2, axis1, env)
		count, pos1, pos2 = worldedit.transpose(pos1, pos2, axis1, axis2, env)
	elseif angle == 180 then
		worldedit.flip(pos1, pos2, axis1, env)
		count = worldedit.flip(pos1, pos2, axis2, env)
	elseif angle == 270 then
		worldedit.flip(pos1, pos2, axis2, env)
		count, pos1, pos2 = worldedit.transpose(pos1, pos2, axis1, axis2, env)
	end
	return count, pos1, pos2
end

--rotates all oriented nodes in a region defined by the positions `pos1` and `pos2` by `angle` degrees clockwise (90 degree increment) around the Y axis, returning the number of nodes oriented
worldedit.orient = function(pos1, pos2, angle, env)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
	local registered_nodes = minetest.registered_nodes

	local wallmounted = {
		[90]={[0]=0, [1]=1, [2]=5, [3]=4, [4]=2, [5]=3},
		[180]={[0]=0, [1]=1, [2]=3, [3]=2, [4]=5, [5]=4},
		[270]={[0]=0, [1]=1, [2]=4, [3]=5, [4]=3, [5]=2}
	}
	local facedir = {
		[90]={[0]=1, [1]=2, [2]=3, [3]=0},
		[180]={[0]=2, [1]=3, [2]=0, [3]=1},
		[270]={[0]=3, [1]=0, [2]=1, [3]=2}
	}

	angle = angle % 360
	if angle == 0 then
		return 0
	end
	local wallmounted_substitution = wallmounted[angle]
	local facedir_substitution = facedir[angle]

	local count = 0
	local get_node, get_meta, add_node = minetest.get_node, minetest.get_meta, minetest.add_node
	local pos = {x=pos1.x, y=0, z=0}
	while pos.x <= pos2.x do
		pos.y = pos1.y
		while pos.y <= pos2.y do
			pos.z = pos1.z
			while pos.z <= pos2.z do
				local node = get_node(pos)
				local def = registered_nodes[node.name]
				if def then
					if def.paramtype2 == "wallmounted" then
						node.param2 = wallmounted_substitution[node.param2]
						local meta = get_meta(pos):to_table()
						add_node(pos, node)
						get_meta(pos):from_table(meta)
						count = count + 1
					elseif def.paramtype2 == "facedir" then
						node.param2 = facedir_substitution[node.param2]
						local meta = get_meta(pos):to_table()
						add_node(pos, node)
						get_meta(pos):from_table(meta)
						count = count + 1
					end
				end
				pos.z = pos.z + 1
			end
			pos.y = pos.y + 1
		end
		pos.x = pos.x + 1
	end
	return count
end

--fixes the lighting in a region defined by positions `pos1` and `pos2`, returning the number of nodes updated
worldedit.fixlight = function(pos1, pos2, env)
	local pos1, pos2 = worldedit.sort_pos(pos1, pos2)
	local nodes = minetest.find_nodes_in_area(pos1, pos2, "air")
	local dig_node = minetest.dig_node
	for _, pos in ipairs(nodes) do
		dig_node(pos)
	end
	return #nodes
end