------------
-- Mapgen --
------------

-- Settings

local water_nest_spawning = minetest.settings:get("water_nest_spawning") or true

local water_cavern_spawning = minetest.settings:get("water_cavern_spawning") or true

local water_nest_spawn_rate = tonumber(minetest.settings:get("water_nest_spawn_rate")) or 128

local water_cavern_spawn_rate = tonumber(minetest.settings:get("water_cavern_spawn_rate")) or 40

---------------------
-- Local Variables --
---------------------

local S = waterdragon.S
local random = math.random

local c_wet_stone = minetest.get_content_id("waterdragon:stone_wet")
local c_soil_wet = minetest.get_content_id("waterdragon:soil_wet")
local c_bones_water = minetest.get_content_id("waterdragon:bone_pile_wet")
local c_eggs_pure_water = minetest.get_content_id("waterdragon:egg_pure_water")
local c_eggs_rare_water = minetest.get_content_id("waterdragon:egg_rare_water")

local c_air = minetest.get_content_id("air")
local c_ignore = minetest.get_content_id("ignore")

local c_gold = c_air
if minetest.registered_nodes["default:water_source"] then
	c_gold = minetest.get_content_id("default:water_source")
end
local c_steel = c_air
if minetest.registered_nodes["default:water_source"] then
	c_steel = minetest.get_content_id("default:water_source")
end

local np_nest = {
	offset = 0,
	scale = 1,
	spread = { x = 30, y = 30, z = 30 },
	seed = -40901,
	octaves = 3,
	persist = 0.67
}

local walkable_nodes = {}

minetest.register_on_mods_loaded(function()
	for name in pairs(minetest.registered_nodes) do
		if name ~= "air" and name ~= "ignore" then
			if minetest.registered_nodes[name].walkable then
				table.insert(walkable_nodes, name)
			end
		end
	end
end)

---------------------
-- Local Utilities --
---------------------

local function get_nearest_player(pos)
	local closest_player
	local dist
	for _, player in pairs(minetest.get_connected_players()) do
		local player_pos = player:get_pos()
		if player_pos
			and (not dist
				or dist > vector.distance(pos, player_pos)) then
			dist = vector.distance(pos, player_pos)
			closest_player = player
		end
	end
	return dist or 100, closest_player
end

local function is_cold_biome(pos)
	local data = minetest.get_biome_data(pos)
	return data.heat < 45 and data.humidity < 75
end

local function is_scottish_biome(pos)
	local data = minetest.get_biome_data(pos)
	return data.heat < 30 and data.humidity < 60
end

local function is_warm_biome(pos)
	local data = minetest.get_biome_data(pos)
	return data.heat > 60 and data.humidity < 80
end

------------------
-- VM Functions --
------------------

-- Nests --

local function generate_pure_water_dragon_nest(minp, maxp, area, data)
	local gender = "male"

	if random(2) < 2 then
		gender = "female"
	end

	local min_y = minp.y
	local max_y = maxp.y

	local min_x = minp.x
	local max_x = maxp.x
	local min_z = minp.z
	local max_z = maxp.z

	local center_x = math.floor((min_x + max_x) / 2)
	local center_y = math.floor((min_y + max_y) / 2)
	local center_z = math.floor((min_z + max_z) / 2)
	local pos = {
		x = center_x,
		y = center_y,
		z = center_z
	}

	local surface = false -- y of above surface node
	for y = max_y, 2, -1 do
		local vi = area:index(center_x, y, center_z)
		if data[vi] ~= c_air then -- if node solid
			break
		elseif data[vi] == c_air
			and data[area:index(center_x, y - 1, center_z)] ~= c_air
			and data[area:index(center_x, y - 1, center_z)] ~= c_ignore then
			surface = y
			break
		end
	end

	if not surface
		or surface - 6 < min_y then
		return
	end

	center_y = surface

	local sidelen = max_x - min_x + 1
	local chulens = { x = sidelen, y = sidelen, z = sidelen }
	local minposxyz = { x = min_x, y = min_y, z = min_z }

	local nvals_nest = minetest.get_perlin_map(np_nest, chulens):get3dMap_flat(minposxyz)

	local nixyz = 1
	for z = min_z, max_z do
		for y = min_y, max_y do
			local vi = area:index(min_x, y, z)
			for x = min_x, max_x do
				local noise = (nvals_nest[nixyz] + 1) * 7.88
				local height = math.abs(y - center_y)
				local dist_slope = (noise - height) * 0.77
				local distance = vector.distance({ x = x, y = y, z = z }, { x = center_x, y = center_y, z = center_z }) -
				dist_slope
				-- Create Nest
				if distance < 15 - (height * 0.66)
					and distance > 4 + ((y - center_y) * 2) then
					data[vi] = c_wet_stone
				elseif distance < 15 - (height * 0.66)
					and distance < 4 + ((y - center_y) * 2)
					and data[vi] ~= c_wet_stone then
					data[vi] = c_air
				end
				-- Create platform to stop floating Nests
				if distance < 15 - (height * 0.33)
					and distance > 4 + ((y - center_y) * 1.5)
					and y < center_y then
					data[vi] = c_wet_stone
				end
				-- Create wet Soil around nest
				if distance > 13 - (height * 0.66)
					and distance < 19 - (height * 0.66)
					and data[vi] ~= c_air
					and random(8) < 2 then
					data[vi] = c_soil_wet
				end
				local bi = area:index(x, y - 1, z)
				-- Create scattered loot
				local loot_chance = 30
				if gender == "male" then
					loot_chance = 12
				end
				if data[bi] == c_wet_stone
					and data[vi] == c_air then
					if random(loot_chance) < 2 then
						data[vi] = c_gold
					elseif random(24) < 2 then
						data[vi] = c_bones_water
					end
				end
				-- Create scattered Eggs
				local egg_chance = 30
				if gender == "male" then
					egg_chance = 12
				end
				if data[bi] == c_wet_stone
					and data[vi] == c_air then
					if random(egg_chance) < 2 then
						data[vi] = c_steel
					elseif random(24) < 2 then
						data[vi] = c_eggs_pure_water
					end
				end
				-- Create stone pillars
				if data[bi] == c_wet_stone
					and data[vi] == c_air
					and random(80) < 2 then
					local pillar_height = random(4, 6)
					for i = -1, pillar_height do
						local pil_i = area:index(x, y + i, z)
						data[pil_i] = c_wet_stone
					end
				end
				nixyz = nixyz + 1
				vi = vi + 1
			end
		end
	end

	minetest.after(0.2, function()
		minetest.add_node({ x = center_x, y = center_y, z = center_z }, { name = "waterdragon:spawn_node" })
		local meta = minetest.get_meta({ x = center_x, y = center_y, z = center_z })
		meta:set_string("mob", "waterdragon:pure_water_dragon")
		meta:set_string("gender", gender)
		local _, closest_player = get_nearest_player(pos)
		if closest_player then
			local name = closest_player:get_player_name()
			local inv = minetest.get_inventory({ type = "player", name = name })
			if waterdragon.contains_book(inv) then
				waterdragon.add_page(inv, "waterdragons")
			end
		end
	end)
end

local function generate_rare_water_dragon_nest(minp, maxp, area, data)
	local gender = "male"

	if random(2) < 2 then
		gender = "female"
	end

	local min_y = minp.y
	local max_y = maxp.y

	local min_x = minp.x
	local max_x = maxp.x
	local min_z = minp.z
	local max_z = maxp.z

	local center_x = math.floor((min_x + max_x) / 2)
	local center_y = math.floor((min_y + max_y) / 2)
	local center_z = math.floor((min_z + max_z) / 2)
	local pos = {
		x = center_x,
		y = center_y,
		z = center_z
	}

	local surface = false -- y of above surface node
	for y = max_y, 2, -1 do
		local vi = area:index(center_x, y, center_z)
		if data[vi] ~= c_air then -- if node solid
			break
		elseif data[vi] == c_air
			and data[area:index(center_x, y - 1, center_z)] ~= c_air
			and data[area:index(center_x, y - 1, center_z)] ~= c_ignore then
			surface = y
			break
		end
	end

	if not surface
		or surface - 6 < min_y then
		return
	end

	center_y = surface

	local sidelen = max_x - min_x + 1
	local chulens = { x = sidelen, y = sidelen, z = sidelen }
	local minposxyz = { x = min_x, y = min_y, z = min_z }

	local nvals_nest = minetest.get_perlin_map(np_nest, chulens):get3dMap_flat(minposxyz)

	local nixyz = 1
	for z = min_z, max_z do
		for y = min_y, max_y do
			local vi = area:index(min_x, y, z)
			for x = min_x, max_x do
				local noise = (nvals_nest[nixyz] + 1) * 7.88
				local height = math.abs(y - center_y)
				local dist_slope = (noise - height) * 0.77
				local distance = vector.distance({ x = x, y = y, z = z }, { x = center_x, y = center_y, z = center_z }) -
				dist_slope
				-- Create Nest
				if distance < 15 - (height * 0.66)
					and distance > 4 + ((y - center_y) * 2) then
					data[vi] = c_wet_stone
				elseif distance < 15 - (height * 0.66)
					and distance < 4 + ((y - center_y) * 2)
					and data[vi] ~= c_wet_stone then
					data[vi] = c_air
				end
				-- Create platform to stop floating Nests
				if distance < 15 - (height * 0.33)
					and distance > 4 + ((y - center_y) * 1.5)
					and y < center_y then
					data[vi] = c_wet_stone
				end
				-- Create Wet Soil around nest
				if distance > 13 - (height * 0.66)
					and distance < 19 - (height * 0.66)
					and data[vi] ~= c_air
					and random(8) < 2 then
					data[vi] = c_soil_wet
				end
				local bi = area:index(x, y - 1, z)
				-- Create scattered loot
				local loot_chance = 30
				if gender == "male" then
					loot_chance = 12
				end
				if data[bi] == c_wet_stone
					and data[vi] == c_air then
					if random(loot_chance) < 2 then
						data[vi] = c_steel
					elseif random(24) < 2 then
						data[vi] = c_bones_water
					end
				end
				-- Create scattered Eggs
				local egg_chance = 30
				if gender == "male" then
					egg_chance = 12
				end
				if data[bi] == c_wet_stone
					and data[vi] == c_air then
					if random(egg_chance) < 2 then
						data[vi] = c_steel
					elseif random(24) < 2 then
						data[vi] = c_eggs_rare_water
					end
				end
				-- Create stone pillars
				if data[bi] == c_wet_stone
					and data[vi] == c_air
					and random(80) < 2 then
					local pillar_height = random(4, 6)
					for i = -1, pillar_height do
						local pil_i = area:index(x, y + i, z)
						data[pil_i] = c_wet_stone
					end
				end
				nixyz = nixyz + 1
				vi = vi + 1
			end
		end
	end

	minetest.after(0.2, function()
		minetest.add_node({ x = center_x, y = center_y, z = center_z }, { name = "waterdragon:spawn_node" })
		local meta = minetest.get_meta({ x = center_x, y = center_y, z = center_z })
		meta:set_string("mob", "waterdragon:rare_water_dragon")
		meta:set_string("gender", gender)
		local _, closest_player = get_nearest_player(pos)
		if closest_player then
			local name = closest_player:get_player_name()
			local inv = minetest.get_inventory({ type = "player", name = name })
			if waterdragon.contains_book(inv) then
				waterdragon.add_page(inv, "waterdragons")
			end
		end
	end)
end

-- Nests --

local function generate_pure_water_dragon_cavern(minp, maxp, area, data)
	local gender = "male"

	if random(2) < 2 then
		gender = "female"
	end

	local min_y = minp.y
	local max_y = maxp.y

	local min_x = minp.x
	local max_x = maxp.x
	local min_z = minp.z
	local max_z = maxp.z

	local center_x = math.floor((min_x + max_x) / 2)
	local center_y = math.floor((min_y + max_y) / 2)
	local center_z = math.floor((min_z + max_z) / 2)
	local pos = {
		x = center_x,
		y = center_y,
		z = center_z
	}

	local sidelen = max_x - min_x + 1
	local chulens = { x = sidelen, y = sidelen, z = sidelen }
	local minposxyz = { x = min_x, y = min_y, z = min_z }

	local nvals_nest = minetest.get_perlin_map(np_nest, chulens):get3dMap_flat(minposxyz)

	local nixyz = 1
	for z = min_z, max_z do
		for y = min_y, max_y do
			local vi = area:index(min_x, y, z)
			for x = min_x, max_x do
				local noise = (nvals_nest[nixyz] + 1) * 3.33
				local height = math.abs(y - center_y)
				local distance = vector.distance({ x = x, y = y, z = z }, { x = center_x, y = center_y, z = center_z }) -
				noise
				-- Create Nest
				if distance < 33 + (4 - ((height * 0.15) * (height * 0.4))) then
					data[vi] = c_wet_stone
					if distance < 29 + (4 - (height * 0.15) * (height * 0.4)) then
						data[vi] = c_air
					end
				end
				-- Create Stalactites
				local bi = area:index(x, y - 1, z)
				if y > center_y
					and data[vi] == c_wet_stone
					and data[bi] == c_air then
					if random(18) == 1 then
						local len = random(3, 6)
						for i = 1, len do
							data[area:index(x, y - i, z)] = c_wet_stone
						end
					end
				end
				-- Create scattered loot
				local loot_chance = 30
				if gender == "male" then
					loot_chance = 12
				end
				if data[bi] == c_wet_stone
					and data[vi] == c_air then
					if random(loot_chance) < 2 then
						data[vi] = c_gold
					elseif random(24) < 2 then
						data[vi] = c_bones_water
					end
				end
				nixyz = nixyz + 1
				vi = vi + 1
			end
		end
	end

	minetest.after(0.2, function()
		minetest.add_node({ x = center_x, y = center_y, z = center_z }, { name = "waterdragon:spawn_node" })
		local meta = minetest.get_meta({ x = center_x, y = center_y, z = center_z })
		meta:set_string("mob", "waterdragon:pure_water_dragon")
		meta:set_string("gender", gender)
		local _, closest_player = get_nearest_player(pos)
		if closest_player then
			local name = closest_player:get_player_name()
			local inv = minetest.get_inventory({ type = "player", name = name })
			if waterdragon.contains_book(inv) then
				waterdragon.add_page(inv, "waterdragons")
			end
		end
	end)
end

-- Rare Water Nest --

local function generate_rare_water_dragon_cavern(minp, maxp, area, data)
	local gender = "male"

	if random(2) < 2 then
		gender = "female"
	end

	local min_y = minp.y
	local max_y = maxp.y

	local min_x = minp.x
	local max_x = maxp.x
	local min_z = minp.z
	local max_z = maxp.z

	local center_x = math.floor((min_x + max_x) / 2)
	local center_y = math.floor((min_y + max_y) / 2)
	local center_z = math.floor((min_z + max_z) / 2)
	local pos = {
		x = center_x,
		y = center_y,
		z = center_z
	}

	local sidelen = max_x - min_x + 1
	local chulens = { x = sidelen, y = sidelen, z = sidelen }
	local minposxyz = { x = min_x, y = min_y, z = min_z }

	local nvals_nest = minetest.get_perlin_map(np_nest, chulens):get3dMap_flat(minposxyz)

	local nixyz = 1
	for z = min_z, max_z do
		for y = min_y, max_y do
			local vi = area:index(min_x, y, z)
			for x = min_x, max_x do
				local noise = (nvals_nest[nixyz] + 1) * 3.33
				local height = math.abs(y - center_y)
				local distance = vector.distance({ x = x, y = y, z = z }, { x = center_x, y = center_y, z = center_z }) -
				noise
				-- Create Nest
				if distance < 33 + (4 - ((height * 0.15) * (height * 0.4))) then
					data[vi] = c_wet_stone
					if distance < 29 + (4 - (height * 0.15) * (height * 0.4)) then
						data[vi] = c_air
					end
				end
				-- Create Stalactites
				local bi = area:index(x, y - 1, z)
				if y > center_y
					and data[vi] == c_wet_stone
					and data[bi] == c_air then
					if random(18) < 3 then
						local len = random(3, 6)
						for i = 1, len do
							data[area:index(x, y - i, z)] = c_wet_stone
						end
					end
				end
				-- Create scattered loot
				local loot_chance = 30
				if gender == "male" then
					loot_chance = 12
				end
				if data[bi] == c_wet_stone
					and data[vi] == c_air then
					if random(loot_chance) < 2 then
						data[vi] = c_steel
					elseif random(24) < 2 then
						data[vi] = c_bones_water
					end
				end
				nixyz = nixyz + 1
				vi = vi + 1
			end
		end
	end

	minetest.after(0.2, function()
		minetest.add_node({ x = center_x, y = center_y, z = center_z }, { name = "waterdragon:spawn_node" })
		local meta = minetest.get_meta({ x = center_x, y = center_y, z = center_z })
		meta:set_string("mob", "waterdragon:rare_water_dragon")
		meta:set_string("gender", gender)
		local _, closest_player = get_nearest_player(pos)
		if closest_player then
			local name = closest_player:get_player_name()
			local inv = minetest.get_inventory({ type = "player", name = name })
			if waterdragon.contains_book(inv) then
				waterdragon.add_page(inv, "waterdragons")
			end
		end
	end)
end

--------------
-- Spawning --
--------------

waterdragon.register_on_spawn("waterdragon:pure_water_dragon", function(self, pos)
	self.nest_pos = pos
	self:memorize("nest_pos", self.nest_pos)
end)

waterdragon.register_on_spawn("waterdragon:rare_water_dragon", function(self, pos)
	self.nest_pos = pos
	self:memorize("nest_pos", self.nest_pos)
end)

----------------
-- Generation --
----------------

local function average(tbl)
	local sum = 0
	for _, v in pairs(tbl) do -- Get the sum of all numbers in t
		sum = sum + v
	end
	return sum / #tbl
end

minetest.register_on_generated(function(minp, maxp)
	local min_y, max_y = minp.y, maxp.y
	local min_x, max_x = minp.x, maxp.x
	local min_z, max_z = minp.z, maxp.z

	local center_x = math.floor((min_x + max_x) / 2)
	local center_y = math.floor((min_y + max_y) / 2)
	local center_z = math.floor((min_z + max_z) / 2)

	local pos = { x = center_x, y = center_y, z = center_z }

	local vm, emin, emax = minetest.get_mapgen_object("voxelmanip")
	local area = VoxelArea:new { MinEdge = emin, MaxEdge = emax }
	local data = vm:get_data()

	if water_nest_spawning
		and random(water_nest_spawn_rate) < 2 then
		local heights = {}

		for z = min_z + 8, max_z - 7, 8 do
			for x = min_x + 8, max_x - 7, 8 do
				for y = min_y, max_y do
					local vi = area:index(x, y, z)
					local vi_name = minetest.get_name_from_content_id(data[vi])
					if not waterdragon.get_node_def(vi_name).walkable then
						table.insert(heights, y)
						break
					end
				end
			end
		end

		if water_scottish_nest_spawn_rate and math.random(water_scottish_nest_spawn_rate) < 2 then
			local heights = {}

			for z = min_z + 8, max_z - 7, 8 do
				for x = min_x + 8, max_x - 7, 8 do
					for y = min_y, max_y do
						local vi = area:index(x, y, z)
						local vi_name = minetest.get_name_from_content_id(data[vi])
						if not waterdragon.get_node_def(vi_name).walkable then
							table.insert(heights, y)
							break
						end
					end
				end
			end

			local avg_height = average(heights)

			if avg_height > 4 then
				pos.y = avg_height

				local heightmap = minetest.get_mapgen_object("heightmap")

				if heightmap and #heightmap > 0 then
					pos.y = heightmap[math.floor(#heightmap / 2)]
				end

				if is_scottish_biome(pos) then
					generate_scottish_dragon_nest(minp, maxp, area, data)
					vm:set_data(data)
					vm:set_lighting({ day = 0, night = 0 })
					vm:calc_lighting()
					vm:update_liquids()
					vm:write_to_map()
				end
			end
		end

		local avg_height = average(heights)

		if avg_height > 4 then
			pos.y = avg_height

			local heightmap = minetest.get_mapgen_object("heightmap")

			if heightmap
				and #heightmap > 0 then
				pos.y = heightmap[math.floor(#heightmap / 2)]
			end

			if is_cold_biome(pos) then
				generate_rare_water_dragon_nest(minp, maxp, area, data)
				vm:set_data(data)
				vm:set_lighting({ day = 0, night = 0 })
				vm:calc_lighting()
				vm:update_liquids()
				vm:write_to_map()
			elseif is_warm_biome(pos) then
				generate_pure_water_dragon_nest(minp, maxp, area, data)
				vm:set_data(data)
				vm:set_lighting({ day = 0, night = 0 })
				vm:calc_lighting()
				vm:update_liquids()
				vm:write_to_map()
			end
		end
	elseif water_cavern_spawning
		and random(water_cavern_spawn_rate) < 2
		and max_y < 0 then
		if is_cold_biome(pos) then
			generate_rare_water_dragon_cavern(minp, maxp, area, data)
			vm:set_data(data)
			vm:set_lighting({ day = 0, night = 0 })
			vm:calc_lighting()
			vm:update_liquids()
			vm:write_to_map()
		elseif is_warm_biome(pos) then
			generate_pure_water_dragon_cavern(minp, maxp, area, data)
			vm:set_data(data)
			vm:set_lighting({ day = 0, night = 0 })
			vm:calc_lighting()
			vm:update_liquids()
			vm:write_to_map()
		end
	end
end)

minetest.register_node("waterdragon:scottish_dragon_dragonstone_block", {
	description = S("Scottish Dragonstone Block"),
	tiles = { "waterdragon_scottish_dragon_forge_bottom.png" },
	groups = { cracky = 1 },
	is_ground_content = false,
	sounds = default.node_sound_stone_defaults(),
})

function generate_scottish_dragon_nest(minp, maxp, area, data)
	local min_y = minp.y
	local max_y = maxp.y

	local min_x = minp.x
	local max_x = maxp.x
	local min_z = minp.z
	local max_z = maxp.z

	local center_x = math.floor((min_x + max_x) / 2)
	local center_y = math.floor((min_y + max_y) / 2)
	local center_z = math.floor((min_z + max_z) / 2)
	local pos = {
		x = center_x,
		y = center_y,
		z = center_z
	}

	local surface = false
	for y = max_y, 2, -1 do
		local vi = area:index(center_x, y, center_z)
		if data[vi] ~= c_air then
			break
		elseif data[vi] == c_air
			and data[area:index(center_x, y - 1, center_z)] ~= c_air
			and data[area:index(center_x, y - 1, center_z)] ~= c_ignore then
			surface = y
			break
		end
	end

	if not surface
		or surface - 6 < min_y then
		return
	end

	center_y = surface

	local sidelen = max_x - min_x + 1
	local chulens = { x = sidelen, y = sidelen, z = sidelen }
	local minposxyz = { x = min_x, y = min_y, z = min_z }

	local nvals_nest = minetest.get_perlin_map(np_nest, chulens):get3dMap_flat(minposxyz)

	local c_dragonstone = minetest.get_content_id("waterdragon:scottish_dragon_dragonstone_block")

	local nixyz = 1
	for z = min_z, max_z do
		for y = min_y, max_y do
			local vi = area:index(min_x, y, z)
			for x = min_x, max_x do
				local noise = (nvals_nest[nixyz] + 1) * 7.88
				local height = math.abs(y - center_y)
				local dist_slope = (noise - height) * 0.77
				local distance = vector.distance({ x = x, y = y, z = z }, { x = center_x, y = center_y, z = center_z }) -
				dist_slope

				if distance < 15 - (height * 0.66)
					and distance > 4 + ((y - center_y) * 2) then
					data[vi] = c_dragonstone
				elseif distance < 15 - (height * 0.66)
					and distance < 4 + ((y - center_y) * 2)
					and data[vi] ~= c_dragonstone then
					data[vi] = c_air
				end

				if distance < 15 - (height * 0.33)
					and distance > 4 + ((y - center_y) * 1.5)
					and y < center_y then
					data[vi] = c_dragonstone
				end


				nixyz = nixyz + 1
				vi = vi + 1
			end
		end
	end
end
