-- The main mapgen code.

--[[ THIS FILE WILL BE RUN TWICE!
1) In the global Luanti environment,
2) In the heavily-restricted threaded mapgen environment

This means this file is very restricted and only has
access to functions that are available in both the
global and the mapgen environment. Refer to Luanti's
Lua API documentation for details.

In the global environment, this file runs to expose the
function lzr_mapgen.generate_piece, but it does not call
minetest.register_on_generated.

In the mapgen environment, it uses
minetest.register_on_generated, but it does not modify
the global environment.

The reason this file is called twice is to avoid code
duplication.
]]

-- This variable will be true when we're in the mapgen
-- environment. The mapgen environment does not have
-- access to global variables, so if lzr_mapgen does
-- not exist, we can decude we must be the mapgen
-- environment.
local IS_IN_MAPGEN_ENVIRONMENT = not minetest.global_exists("lzr_mapgen")


--[[ MAPGEN OVERVIEW:
The mapgen has two main zones:

Deep Ocean: The Deep Ocean is a very simple zone and very fast
to generate, it's just a flat layer of water with a flat seabed
way below the surface. Perfect to spawn the ship in.

Islands: Islands is a beautiful zone using 2D Perlin
noise, featuring tropical islands, palms, plants, hills, and
a “hilly” ocean floor. It is derived from the [islands] mod
by TheTermos, released under the MIT License in 2019.

Both zones are separated on the Z axis by DEEP_OCEAN_Z.

There is a hard ugly seam between both zones, so it is
important the game action happens far away from this
seam.
]]


-- Some basic values for mapgen coordinates

-- Water will be at this Y level, all the way down to the ocean floor
local WATER_LEVEL = 1

--[[ Deep ocean-only values ]]
-- The seabed will be at this Y level, all the way down to DEEP_OCEAN_SEASTONE_LEVEL+1
local DEEP_OCEAN_SEABED_LEVEL = -1000
-- The seastone will be at and below this Y level, down to DEEP_OCEAN_CAVESTONE_LEVEL+1
local DEEP_OCEAN_SEASTONE_LEVEL = -1002
-- The cave stone will be at and below this Y level, all the way down to the bottom of the world
local DEEP_OCEAN_CAVESTONE_LEVEL = -2002

--[[ Islands-only values ]]
-- How many dirt, sand or seabed blocks are put below the surface blocks before the stone layer starts
local ISLANDS_GROUND_DEPTH = 2
-- At which Y level the dirt with jungle litter may spawn
local ISLANDS_LITTER_LEVEL = 3
-- Sand generates on the surface at this Y level and below (for the beaches)
local ISLANDS_SAND_LEVEL = 3
-- Seabed generates on the surface at this Y level and below
local ISLANDS_SEABED_LEVEL = -4
-- The cave stone will be at and below this Y level, all the way down to the bottom of the world
local ISLANDS_CAVESTONE_LEVEL = DEEP_OCEAN_CAVESTONE_LEVEL


-- The deep ocean will begin when the Z coordinate is this value or lower.
-- WARNING: This value is duplicated in lzr_globals.
-- When you change it, you MUST also change it in lzr_globals!
local DEEP_OCEAN_Z = -20000

local floor = math.floor
local ceil = math.ceil
local min = math.min
local max = math.max

local convex = false

local mult = 1.0
-- Set the 3D noise parameters for the terrain.


local np_terrain = {
	offset = -11*mult, -- ratio 2:7 or 1:4 ?
	scale = 40*mult,
	spread = {x = 256*mult, y =256*mult, z = 256*mult},
	seed = 1234,
	octaves = convex and 1 or 5,
	persist = 0.38,
	lacunarity = 2.33,
}

local np_var = {
	offset = 0,
	scale = 6*mult,
	spread = {x = 64*mult, y =64*mult, z = 64*mult},
	seed = 567891,
	octaves = 4,
	persist = 0.4,
	lacunarity = 1.89,
}

local np_hills = {
	offset = 2.5, -- off/scale ~ 2:3
	scale = -3.5,
	spread = {x = 64*mult, y =64*mult, z = 64*mult},
	seed = 2345,
	octaves = 3,
	persist = 0.40,
	lacunarity = 2.0,
	flags = "absvalue"
}

local np_cliffs = {
	offset = 0,
	scale = 0.72,
	spread = {x = 180*mult, y =180*mult, z = 180*mult},
	seed = 78901,
	octaves = 2,
	persist = 0.4,
	lacunarity = 2.11,
}

local np_trees = {
	offset = - 0.003,
	scale = 0.008,
	spread = {x = 64, y = 64, z = 64},
	seed = 2,
	octaves = 5,
	persist = 1,
	lacunarity = 1.91,
}

local hills_offset = np_hills.spread.x*0.5
local hills_thresh = floor((np_terrain.scale)*0.5)
local shelf_thresh = floor((np_terrain.scale)*0.5) 
local cliffs_thresh = 10

local function max_height(noiseprm)
	local height = 0
	local scale = noiseprm.scale
	for i=1,noiseprm.octaves do
		height=height + scale
		scale = scale * noiseprm.persist
	end
	return height+noiseprm.offset
end

local function min_height(noiseprm)
	local height = 0
	local scale = noiseprm.scale
	for i=1,noiseprm.octaves do
		height=height - scale
		scale = scale * noiseprm.persist
	end
	return height+noiseprm.offset
end

local base_min = min_height(np_terrain)
local base_max = max_height(np_terrain)
local base_rng = base_max-base_min
local easing_factor = 1/(base_max*base_max*4)
local base_heightmap = {}
local result_heightmap = {}


-- Get the content IDs for the nodes used.

local c_stone = minetest.get_content_id("lzr_core:stone")
local c_cave_stone = minetest.get_content_id("lzr_core:cave_stone")
local c_sand = minetest.get_content_id("lzr_core:sand")
local c_seabed = minetest.get_content_id("lzr_core:seabed")
local c_dirt = minetest.get_content_id("lzr_core:dirt")
local c_dirt_g = minetest.get_content_id("lzr_core:dirt_with_grass")
local c_dirt_l = minetest.get_content_id("lzr_core:dirt_with_jungle_litter")
local c_water = minetest.get_content_id("lzr_core:water_source")



local function get_terrain_height(theight,hheight,cheight)
	-- parabolic gradient
	if theight > 0 and theight < shelf_thresh then
		theight = theight * (theight*theight/(shelf_thresh*shelf_thresh)*0.5 + 0.5)
	end
	-- hills
	if theight > hills_thresh then
		theight = theight + max((theight-hills_thresh) * hheight,0)
		-- cliffs
	elseif theight > 1 and theight < hills_thresh then 
		local clifh = max(min(cheight,1),0) 
		if clifh > 0 then
			clifh = -1*(clifh-1)*(clifh-1) + 1
			theight = theight + (hills_thresh-theight) * clifh * ((theight<2) and theight-1 or 1)
		end
	end
	return theight
end

-- Given an Y coordinate, returns the expected day light level
-- for water at this coordinate, assuming there are no other
-- obstacles
local get_ocean_light_level = function(y)
	local ll = 15 - (WATER_LEVEL - (y-1))
	return max(0, min(15, ll))
end

-- Change the day component of the given original param1
-- light value (0-255) to new_light_day (0-15) and
-- returns the result.
local change_day_light = function(original_light, new_light_day)
	local light = original_light
	local light_night = bit.band(light, 0xF0)
	light = bit.bor(new_light_day, light_night)
	return light
end
 
-- Localise VoxelManip data buffer tables outside the loop,
-- to be re-used for all mapchunks, therefore minimising memory use.
local vm_data = {}
local vm_light_data = {}

-- Generate a piece of the map and set nodes in the specified area.
-- * minp: Minimum position of the area
-- * maxp: Maximum position of the area
-- * vm: VoxelManip object (only required when calling this function in the mapgen environment. Set to nil otherwise)
-- * prot_min: Minimum position of protected area. The protected area will NOT be overwritten by this function
-- * prot_min: Maximum position of protected area
local generate_piece = function(minp, maxp, vm, prot_min, prot_max)
	-- Start time of mapchunk generation.
	local t0 = os.clock()

	if IS_IN_MAPGEN_ENVIRONMENT then
		minetest.log("info", "[lzr_mapgen] Generating piece at " .. minetest.pos_to_string(minp).." ... (mapgen environment)")
	else
		minetest.log("info", "[lzr_mapgen] Generating piece at " .. minetest.pos_to_string(minp).." ... (main environment)")
	end

	local sidelen_x = maxp.x - minp.x + 1
	local sidelen_z = maxp.z - minp.z + 1
	local permapdims3d = {x = sidelen_x, y = sidelen_z}

	local clear = IS_IN_MAPGEN_ENVIRONMENT ~= true

	-- voxelmanip stuff
	if not vm then
		vm = minetest.get_voxel_manip(minp, maxp)
	end
	local emin, emax = vm:get_emerged_area()

	local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax}
	vm:get_data(vm_data)
	vm:get_light_data(vm_light_data)

	-- If we are fully outside the islands area,
	-- we can switch to the ultra-fast deep ocean
	-- algorithm.
	local generate_islands = maxp.z > DEEP_OCEAN_Z
	local isln_terrain = nil
	local isln_var = nil
	local isln_hills = nil
	local isln_cliffs = nil
	local isln_trees = nil

	if generate_islands then

		-- base terrain
		local nobj_terrain = minetest.get_perlin_map(np_terrain, permapdims3d)
		isln_terrain = nobj_terrain:get_2d_map({x=minp.x,y=minp.z})

		-- base variation
		local nobj_var = minetest.get_perlin_map(np_var, permapdims3d)
		isln_var = nobj_var:get_2d_map({x=minp.x,y=minp.z})

		-- hills
		local nobj_hills = minetest.get_perlin_map(np_hills, permapdims3d)
		isln_hills = nobj_hills:get_2d_map({x=minp.x+hills_offset,y=minp.z+hills_offset})

		-- cliffs
		local nobj_cliffs = minetest.get_perlin_map(np_cliffs, permapdims3d)
		isln_cliffs = nobj_cliffs:get_2d_map({x=minp.x,y=minp.z})

		-- trees
		local nobj_trees = minetest.get_perlin_map(np_trees, permapdims3d)
		isln_trees = nobj_trees:get_2d_map({x=minp.x,y=minp.z})

		for z = 1, sidelen_z do
			base_heightmap[z]={}
			result_heightmap[z]={}
			for x = 1, sidelen_x do
				if not isln_terrain[z][x] then
					minetest.log("error", "[lzr_mapgen] isln_terrain["..z.."]["..x.."] is nil!")
					minetest.log("error", "[lzr_mapgen] LEN Z="..#isln_terrain)
					for i=1, #isln_terrain do
						minetest.log("error", "[lzr_mapgen] LEN X["..i.."]="..#isln_terrain[i])
					end
					minetest.log("error", "[lzr_mapgen] sidelen_x="..sidelen_x)
					minetest.log("error", "[lzr_mapgen] sidelen_z="..sidelen_z)
					return
				end
				if not isln_var[z][x] then
					minetest.log("error", "[lzr_mapgen] isln_var["..z.."]["..x.."] is nil!")
					minetest.log("error", "[lzr_mapgen] VAR LEN Z="..#isln_terrain)
					minetest.log("error", "[lzr_mapgen] VAR LEN X="..#isln_terrain[1])
					minetest.log("error", "[lzr_mapgen] sidelen_x="..sidelen_x)
					minetest.log("error", "[lzr_mapgen] sidelen_z="..sidelen_z)
					minetest.log("error", "[lzr_mapgen] isln_var="..dump(isln_var))
					return
				end
				local theight = isln_terrain[z][x] + (convex and isln_var[z][x] or 0)
				local hheight = isln_hills[z][x]
				local cheight = isln_cliffs[z][x]
				base_heightmap[z][x]=theight
				result_heightmap[z][x]=get_terrain_height(theight,hheight,cheight)
			end
		end

	end

	for z = minp.z, maxp.z do
		for y = minp.y, maxp.y do
			for x = minp.x, maxp.x do
				local mpos = vector.new(x, y, z)
				if not prot_min or not vector.in_area(mpos, prot_min, prot_max) then
					local vi = area:index(x, y, z)
					-- Deep ocean
					if z <= DEEP_OCEAN_Z then
						if y <= DEEP_OCEAN_CAVESTONE_LEVEL then
							vm_data[vi] = c_cave_stone
						elseif y <= DEEP_OCEAN_SEASTONE_LEVEL then
							vm_data[vi] = c_stone
						elseif y <= DEEP_OCEAN_SEABED_LEVEL then
							vm_data[vi] = c_seabed
						elseif y <= WATER_LEVEL then
							vm_data[vi] = c_water
							-- We calculate the light level for the ocean manually
							-- because calc_lighting apparently doesn't calculate
							-- the lighting of the ocean.
							-- FIXME: Remove the manual manual calculation entirely when we
							-- figured out a way to fix ocean lighting with calc_lighting alone
							vm_light_data[vi] = change_day_light(vm_light_data[vi], get_ocean_light_level(y))
						elseif clear then
							vm_data[vi] = minetest.CONTENT_AIR
						end
					-- Islands mapgen
					else
						local bheight = base_heightmap[z-minp.z+1][x-minp.x+1]
						local theight = result_heightmap[z-minp.z+1][x-minp.x+1]

						local dirt
						if (theight >= ISLANDS_LITTER_LEVEL and theight < hills_thresh and isln_trees[z-minp.z+1][x-minp.x+1] > 0) then
							dirt = c_dirt_l
						else
							dirt = c_dirt_g
						end

						if y < theight then
							if y <= ISLANDS_CAVESTONE_LEVEL then
								vm_data[vi] = c_cave_stone
							elseif y <= theight - ISLANDS_GROUND_DEPTH then
								vm_data[vi] = c_stone
							else
								if theight <= ISLANDS_SEABED_LEVEL then
									vm_data[vi] = c_seabed
								elseif theight <= ISLANDS_SAND_LEVEL then
									vm_data[vi] = c_sand
								else
									vm_data[vi] = c_dirt
								end
							end
						elseif y == ceil(theight) then
							if y <= ISLANDS_SEABED_LEVEL then
								vm_data[vi] = c_seabed
							elseif y <= ISLANDS_SAND_LEVEL then
								vm_data[vi] = c_sand
							else
								vm_data[vi] = dirt
							end
						elseif y <= WATER_LEVEL then
							vm_data[vi] = c_water
							-- Calculate lighting manually (see above)
							vm_light_data[vi] = change_day_light(vm_light_data[vi], get_ocean_light_level(y))
						elseif clear then
							vm_data[vi] = minetest.CONTENT_AIR
						end
					end
				end
			end
		end
	end

	vm:set_data(vm_data)
	vm:set_light_data(vm_light_data)

	if IS_IN_MAPGEN_ENVIRONMENT then
		if generate_islands then
			minetest.generate_decorations(vm, minp, maxp)
		end
		vm:calc_lighting(minp, maxp)
	else
		if generate_islands then
			minetest.generate_decorations(vm, minp, maxp)
		end
		vm:write_to_map(true)
	end

	-- Print generation time of this piece
	local chugent = ceil((os.clock() - t0) * 1000)
	if IS_IN_MAPGEN_ENVIRONMENT then
		minetest.log("info", "[lzr_mapgen] Generation time for piece at " .. minetest.pos_to_string(minp)..": " .. chugent .. " ms (mapgen environment)")
	else
		minetest.log("info", "[lzr_mapgen] Generation time for piece at " .. minetest.pos_to_string(minp)..": " .. chugent .. " ms (main environment)")
	end
end


if IS_IN_MAPGEN_ENVIRONMENT then
	minetest.log("action", "[lzr_mapgen] mapgen script successfully run in mapgen environment")
	-- On generated function for the mapgen environment

	minetest.register_on_generated(function(vmanip, minp, maxp)
		generate_piece(minp, maxp, vmanip)
	end)
else
	minetest.log("action", "[lzr_mapgen] mapgen script successfully run in global environment")
	-- Expose generate_piece to the global environment when this file does not
	-- run as mapgen thrad
	lzr_mapgen.generate_piece = generate_piece
end
