--[[

  Nether mod for minetest

  All the Dungeon related functions used by the biomes-based mapgen are here.


  Copyright (C) 2021 Treer

  Permission to use, copy, modify, and/or distribute this software for
  any purpose with or without fee is hereby granted, provided that the
  above copyright notice and this permission notice appear in all copies.

  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
  WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
  WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR
  BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
  OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
  SOFTWARE.

]]--


-- We don't need to be gen-notified of temples because only dungeons will be generated
-- if a biome defines the dungeon nodes
minetest.set_gen_notify({dungeon = true})


-- Content ids

local c_air              = minetest.get_content_id("air")
local c_netherrack       = minetest.get_content_id("nether:rack")
local c_netherrack_deep  = minetest.get_content_id("nether:rack_deep")
local c_crystaldark      = minetest.get_content_id("nether:geode")
local c_dungeonbrick     = minetest.get_content_id("nether:brick")
local c_dungeonbrick_alt = minetest.get_content_id("nether:brick_cracked")
local c_netherbrick_slab = minetest.get_content_id("nether:slab_brick")
local c_netherfence      = minetest.get_content_id("nether:fence_nether_brick")
local c_glowstone        = minetest.get_content_id("nether:glowstone")
local c_glowstone_deep   = minetest.get_content_id("nether:glowstone_deep")
local c_lava_source      = minetest.get_content_id("default:lava_source")


-- Misc math functions

-- avoid needing table lookups each time a common math function is invoked
local math_max, math_min  = math.max, math.min


-- Dungeon excavation functions

function is_dungeon_brick(node_id)
	return node_id == c_dungeonbrick or node_id == c_dungeonbrick_alt
end



nether.mapgen.build_dungeon_room_list = function(data, area)

	local result = {}

	-- Unfortunately gennotify only returns dungeon rooms, not corridors.
	-- We don't need to check for temples because only dungeons are generated in biomes
	-- that define their own dungeon nodes.
	local gennotify = minetest.get_mapgen_object("gennotify")
	local roomLocations = gennotify["dungeon"] or {}

	-- Excavation should still know to stop if a cave or corridor has removed the dungeon wall.
	-- See MapgenBasic::generateDungeons in mapgen.cpp for max room sizes.
	local maxRoomSize = 18
	local maxRoomRadius = math.ceil(maxRoomSize / 2)

	local xStride, yStride, zStride = 1, area.ystride, area.zstride
	local minEdge, maxEdge = area.MinEdge, area.MaxEdge

	for _, roomPos in ipairs(roomLocations) do

		if area:containsp(roomPos) then -- this safety check does not appear to be necessary, but lets make it explicit

			local room_vi = area:indexp(roomPos)
			--data[room_vi] = minetest.get_content_id("default:torch") -- debug

			local startPos = vector.new(roomPos)
			if roomPos.y + 1 <= maxEdge.y and data[room_vi + yStride] == c_air then
				-- The roomPos coords given by gennotify are at floor level, but whenever possible we
				-- want to be performing searches a node higher than floor level to avoids dungeon chests.
				startPos.y = startPos.y + 1
				room_vi = area:indexp(startPos)
			end

			local bound_min_x = math_max(minEdge.x, roomPos.x - maxRoomRadius)
			local bound_min_y = math_max(minEdge.y, roomPos.y - 1) -- room coords given by gennotify are on the floor
			local bound_min_z = math_max(minEdge.z, roomPos.z - maxRoomRadius)

			local bound_max_x = math_min(maxEdge.x, roomPos.x + maxRoomRadius)
			local bound_max_y = math_min(maxEdge.y, roomPos.y + maxRoomSize) -- room coords given by gennotify are on the floor
			local bound_max_z = math_min(maxEdge.z, roomPos.z + maxRoomRadius)

			local room_min = vector.new(startPos)
			local room_max = vector.new(startPos)

			local vi = room_vi
			while room_max.y < bound_max_y and data[vi + yStride] == c_air do
				room_max.y = room_max.y + 1
				vi = vi + yStride
			end

			vi = room_vi
			while room_min.y > bound_min_y and data[vi - yStride] == c_air do
				room_min.y = room_min.y - 1
				vi = vi - yStride
			end

			vi = room_vi
			while room_max.z < bound_max_z and data[vi + zStride] == c_air do
				room_max.z = room_max.z + 1
				vi = vi + zStride
			end

			vi = room_vi
			while room_min.z > bound_min_z and data[vi - zStride] == c_air do
				room_min.z = room_min.z - 1
				vi = vi - zStride
			end

			vi = room_vi
			while room_max.x < bound_max_x and data[vi + xStride] == c_air do
				room_max.x = room_max.x + 1
				vi = vi + xStride
			end

			vi = room_vi
			while room_min.x > bound_min_x and data[vi - xStride] == c_air do
				room_min.x = room_min.x - 1
				vi = vi - xStride
			end

			local roomInfo = vector.new(roomPos)
			roomInfo.minp = room_min
			roomInfo.maxp = room_max
			result[#result + 1] = roomInfo
		end
	end

	return result;
end

-- Only partially excavates dungeons, the rest is left as an exercise for the player ;)
-- (Corridors and the parts of rooms which extend beyond the emerge boundary will remain filled)
nether.mapgen.excavate_dungeons = function(data, area, rooms)

	local vi, node_id

	-- any air from the native mapgen has been replaced by netherrack, but
	-- we don't want this inside dungeons, so fill dungeon rooms with air
	for _, roomInfo in ipairs(rooms) do

		local room_min = roomInfo.minp
		local room_max = roomInfo.maxp

		for z = room_min.z, room_max.z do
			for y = room_min.y, room_max.y do
				vi = area:index(room_min.x, y, z)
				for x = room_min.x, room_max.x do
					node_id = data[vi]
					if node_id == c_netherrack or node_id == c_netherrack_deep or node_id == c_crystaldark then data[vi] = c_air end
					vi = vi + 1
				end
			end
		end
	end

	-- clear netherrack from dungeon stairways
	if #rooms > 0 then
		local stairPositions = minetest.find_nodes_in_area(area.MinEdge, area.MaxEdge, minetest.registered_biomes["nether_caverns"].node_dungeon_stair)
		for _, stairPos in ipairs(stairPositions) do
			vi = area:indexp(stairPos)
			for i = 1, 4 do
				if stairPos.y + i > area.MaxEdge.y then break end
				vi = vi + area.ystride
				node_id = data[vi]
				-- searching forward of the stairs could also be done
				if node_id == c_netherrack or node_id == c_netherrack_deep or node_id == c_crystaldark then data[vi] = c_air end
			end
		end
	end
end

-- Since we already know where all the rooms and their walls are, and have all the nodes stored
-- in a voxelmanip already, we may as well add a little Nether flair to the dungeons found here.
nether.mapgen.decorate_dungeons = function(data, area, rooms)

	local xStride, yStride, zStride = 1, area.ystride, area.zstride
	local minEdge, maxEdge = area.MinEdge, area.MaxEdge

	for _, roomInfo in ipairs(rooms) do

		local room_min, room_max = roomInfo.minp, roomInfo.maxp
		local room_size = vector.distance(room_min, room_max)

		if room_size > 10 then
			local room_seed = roomInfo.x + 3 * roomInfo.z + 13 * roomInfo.y
			local window_y  = roomInfo.y + math_min(2, room_max.y - roomInfo.y - 1)
			local roomWidth  = room_max.x - room_min.x + 1
			local roomLength = room_max.z - room_min.z + 1

			if room_seed % 3 == 0 and room_max.y < maxEdge.y then
				-- Glowstone chandelier (feel free to replace with a fancy schematic)
				local vi = area:index(roomInfo.x, room_max.y + 1, roomInfo.z)
				if is_dungeon_brick(data[vi]) then data[vi] = c_glowstone end

			elseif room_seed % 4 == 0 and room_min.y > minEdge.y
				   and room_min.x > minEdge.x and room_max.x < maxEdge.x
				   and room_min.z > minEdge.z and room_max.z < maxEdge.z then
				-- lava well (feel free to replace with a fancy schematic)
				local vi = area:index(roomInfo.x, room_min.y, roomInfo.z)
				if is_dungeon_brick(data[vi - yStride]) then
					data[vi - yStride] = c_lava_source
					if data[vi - zStride] == c_air then data[vi - zStride] = c_netherbrick_slab end
					if data[vi + zStride] == c_air then data[vi + zStride] = c_netherbrick_slab end
					if data[vi - xStride] == c_air then data[vi - xStride] = c_netherbrick_slab end
					if data[vi + xStride] == c_air then data[vi + xStride] = c_netherbrick_slab end
				end
			end

			-- Barred windows
			if room_seed % 7 < 5 and roomWidth >= 5 and roomLength >= 5
			   and window_y >= minEdge.y and window_y + 1 <= maxEdge.y
			   and room_min.x > minEdge.x and room_max.x < maxEdge.x
			   and room_min.z > minEdge.z and room_max.z < maxEdge.z then
				--data[area:indexp(roomInfo)] = minetest.get_content_id("default:mese_post_light") -- debug

				-- Glass panes can't go in the windows because we aren't setting param data.
				-- Until a Nether glass is added, every window will be made of netherbrick fence rather
				-- than material depending on room_seed.
				local window_node = c_netherfence
				--if c_netherglass ~= nil and room_seed % 20 >= 12 then window_node = c_netherglass end

				local function placeWindow(vi, viOutsideOffset, windowNo)
					if is_dungeon_brick(data[vi]) and is_dungeon_brick(data[vi + yStride]) then
						data[vi] = window_node

						if room_seed % 19 == windowNo then
							-- place a glowstone light behind the window
							local node_id = data[vi + viOutsideOffset]
							if node_id == c_netherrack then
								data[vi + viOutsideOffset] = c_glowstone
							elseif node_id == c_netherrack_deep then
								data[vi + viOutsideOffset] = c_glowstone_deep
							end
						end
					end
				end

				local vi_min = area:index(room_min.x - 1, window_y, roomInfo.z)
				local vi_max = area:index(room_max.x + 1, window_y, roomInfo.z)
				local locations = {-zStride, zStride, -zStride + yStride, zStride + yStride}
				for i, offset in ipairs(locations) do
					placeWindow(vi_min + offset, -1, i)
					placeWindow(vi_max + offset,  1, i + #locations)
				end
				vi_min = area:index(roomInfo.x, window_y, room_min.z - 1)
				vi_max = area:index(roomInfo.x, window_y, room_max.z + 1)
				locations = {-xStride, xStride, -xStride + yStride, xStride + yStride}
				for i, offset in ipairs(locations) do
					placeWindow(vi_min + offset, -zStride, i + #locations * 2)
					placeWindow(vi_max + offset,  zStride, i + #locations * 3)
				end
			end

			-- pillars or mezzanine floor
			if room_seed % 43 > 10 and roomWidth >= 6 and roomLength >= 6 then

				local pillar_vi    = {}
				local pillarHeight = 0
				local wallDist = 1 + math.floor((roomWidth + roomLength) / 14)

				local roomHeight = room_max.y - room_min.y
				if roomHeight >= 7 then
					-- mezzanine floor
					local mezzMax = {
						x = room_min.x + math.floor(roomWidth / 7 * 4),
						y = room_min.y + math.floor(roomHeight / 5 * 3),
						z = room_max.z
					}

					pillarHeight = mezzMax.y - room_min.y - 1
					pillar_vi = {
						area:index(mezzMax.x, room_min.y, room_min.z + wallDist),
						area:index(mezzMax.x, room_min.y, room_max.z - wallDist),
					}

					if is_dungeon_brick(data[pillar_vi[1] - yStride]) and is_dungeon_brick(data[pillar_vi[2] - yStride]) then
						-- The floor of the dungeon looks like it exists (i.e. not erased by nether
						-- cavern), so add the mezzanine floor
						for z = 0, roomLength - 1 do
							local vi = area:index(room_min.x, mezzMax.y, room_min.z + z)
							for x = room_min.x, mezzMax.x do
								if data[vi] == c_air then data[vi] = c_dungeonbrick end
								vi = vi + 1
							end
						end
					end

				elseif roomHeight >= 4 then
					-- 4 pillars
					pillarHeight = roomHeight
					pillar_vi = {
						area:index(room_min.x + wallDist, room_min.y, room_min.z + wallDist),
						area:index(room_min.x + wallDist, room_min.y, room_max.z - wallDist),
						area:index(room_max.x - wallDist, room_min.y, room_min.z + wallDist),
						area:index(room_max.x - wallDist, room_min.y, room_max.z - wallDist)
					}
				end

				for i = #pillar_vi, 1, -1 do
					if not is_dungeon_brick(data[pillar_vi[i] - yStride]) then
						-- there's no dungeon floor under this pillar so skip it, it's probably been cut away by nether cavern.
						table.remove(pillar_vi, i)
					end
				end
				for y = 0, pillarHeight do
					for _, vi in ipairs(pillar_vi) do
						if data[vi + y * yStride] == c_air then data[vi + y * yStride] = c_dungeonbrick end
					end
				end
			end

			-- Weeds on the floor once Nether weeds are added
		end
	end
end
