
local c_air = minetest.get_content_id("air")
local c_ignore = minetest.get_content_id("ignore")
local c_bedrock = minetest.get_content_id("bedrock:bedrock")
local c_obsidian = minetest.get_content_id("default:obsidian")
local c_stone = minetest.get_content_id("default:stone")

-- Localize for performance.
local random = math.random
local abs = math.abs
local floor = math.floor
local max = math.max
local min = math.min
local sin = math.sin
local cos = math.cos
local tan = math.tan
local distance = vector.distance
local CID = minetest.get_content_id

local vm_data = {}
local minp = {x=-31000, z=-31000}
local maxp = {x=31000, z=31000}
local step = 100

local SPHERES_EVERYWHERE = false -- Mainly for debugging.
local ALL_SPHERES = {}
local MIN_RADIUS = 7
local MAX_RADIUS = 50
local MIN_Y = -10
local MAX_Y = 10
local SPHERE_SEED = 4718
local SPHERE_WALL = 4
local SPHERE_INNER = 7
local SPHERE_MISSHAPE = 4
local SPHERE_CONTENTS = {
	{CID("default:obsidian"), CID("default:obsidian"), CID("default:obsidian")},
	{CID("default:obsidian"), CID("default:obsidian"), CID("default:obsidian")},
	{CID("default:obsidian"), CID("default:obsidian"), CID("air")},
	{CID("default:obsidian"), CID("default:obsidian"), CID("air")},
	{CID("default:lava_source"), CID("default:lava_source"), CID("default:lava_source")},
	{CID("default:water_source"), CID("default:water_source"), CID("default:water_source")},
	{CID("cavestuff:glow_white_crystal"), CID("quartz:block"), CID("air")},
	{CID("darkage:darkdirt"), CID("default:dirt"), CID("default:dirt")},
}

--------------------------------------------------------------------------------
-- Pregenerate sphere information for the entire realm!
do
	local perlin = minetest.get_perlin({
		offset = 0,
		scale = 1,
		spread = {x=1024, y=1024, z=1024},
		seed = 7752,
		octaves = 2,
		persist = 0.5,
		lacunarity = 2,
	})

	-- For sphere positions and counts.
	local pr1 = PcgRandom(SPHERE_SEED + 1)

	-- For sphere contents.
	local pr2 = PcgRandom(SPHERE_SEED + 2)

	-- Sphere radii.
	local pr3 = PcgRandom(SPHERE_SEED + 3)

	for x = minp.x, maxp.x, step do
		for z = minp.z, maxp.z, step do
			local count = pr1:next(1, 3)
			for k = 1, count do
				local rad = pr3:next(MIN_RADIUS, pr3:next(floor(MAX_RADIUS / 2), MAX_RADIUS))
				local yoff = pr1:next(MIN_Y, MAX_Y)
				local px = pr1:next(x - (step / 2), x + (step / 2))
				local pz = pr1:next(z - (step / 2), z + (step / 2))
				local pos2d = {x=px, y=pz}
				local contents = SPHERE_CONTENTS[pr2:next(1, #SPHERE_CONTENTS)]

				if abs(perlin:get_2d(pos2d)) < 0.2 or SPHERES_EVERYWHERE then
					ALL_SPHERES[#ALL_SPHERES + 1] = {
						pos_x = px,
						pos_z = pz,
						radius = rad,
						y_offset = yoff,
						cid_1 = contents[1],
						cid_2 = contents[2],
						cid_3 = contents[3],
					}
				end
			end
		end
	end
end
--print('num total spheres: ' .. #ALL_SPHERES)
--------------------------------------------------------------------------------

-- Get which spheres intersect this map chunk.
function sw.get_spheres(minp, maxp, get_height, radius_override)
	local maxrad = radius_override or MAX_RADIUS
	local minx = minp.x - maxrad
	local miny = minp.y - maxrad
	local minz = minp.z - maxrad
	local maxx = maxp.x + maxrad
	local maxy = maxp.y + maxrad
	local maxz = maxp.z + maxrad

	local got = {}
	local count = #ALL_SPHERES

	for k = 1, count do
		local data = ALL_SPHERES[k]
		if data.pos_x >= minx and data.pos_x <= maxx then
			if data.pos_z >= minz and data.pos_z <= maxz then
				--print('matching x/z sphere')
				-- Find ground level at the center of each sphere.
				-- Bake Y-level info the first time we generate it.
				local y_level = data.y_level
				if not y_level then
					data.y_level = get_height(data.pos_x, data.pos_z)
					y_level = data.y_level
				end
				--print('y_level: ' .. y_level)

				if y_level >= miny and y_level <= maxy then
					local d = table.copy(data)
					got[#got + 1] = d
				end
			end
		end
	end

	return got
end

sw.create_3d_noise("sphereshear", {
	offset = 0,
	scale = 1,
	spread = {x=16, y=16, z=16},
	seed = 7718,
	octaves = 1,
	persist = 0.5,
	lacunarity = 2,
})

function sw.generate_spheres(vm, minp, maxp, seed, ystart, yend, get_height)
	local x1 = maxp.x
	local y1 = maxp.y
	local z1 = maxp.z
	local x0 = minp.x
	local y0 = minp.y
	local z0 = minp.z

  -- Spheres only generate on surface chunks.
  --if y0 >= (get_height(x0, z0) + 250) or y1 <= (get_height(x0, z0) - 250) then
	--	return
	--end

	local spheres = sw.get_spheres(minp, maxp, get_height)
	--print('spheres: ' .. #spheres)
	if #spheres == 0 then
		return
	end

	local emin, emax = vm:get_emerged_area()
	local area = VoxelArea:new({MinEdge=emin, MaxEdge=emax})
	local pr = PseudoRandom(seed + 5928)

	vm:get_data(vm_data)

	-- Compute side lengths.
	-- Note: noise maps use overgeneration coordinates/sizes.
	-- This is to support horizontal shearing.
	local side_len_x = ((emax.x-emin.x)+1)
	local side_len_y = ((emax.y-emin.y)+1)
	local side_len_z = ((emax.z-emin.z)+1)
	local sides2D = {x=side_len_x, y=side_len_z}
	local sides3D = {x=side_len_x, y=side_len_y, z=side_len_z}
	local bp2d = {x=emin.x, y=emin.z}
	local bp3d = {x=emin.x, y=emin.y, z=emin.z}

	local sphereshear = sw.get_3d_noise(bp3d, sides3D, "sphereshear")

	for k = 1, #spheres do
		local data = spheres[k]

		local sphere_x = data.pos_x
		local sphere_y = data.y_level + data.y_offset
		local sphere_z = data.pos_z
		local sphere_rad = data.radius

		local minbox = {
			x = sphere_x - sphere_rad - 6,
			y = sphere_y - sphere_rad - 6,
			z = sphere_z - sphere_rad - 6,
		}
		local maxbox = {
			x = sphere_x + sphere_rad + 6,
			y = sphere_y + sphere_rad + 6,
			z = sphere_z + sphere_rad + 6,
		}

		local p1 = vector.new(sphere_x, sphere_y, sphere_z)
		local p2 = {x=0, y=0, z=0}

		local ncid_1 = c_obsidian
		local ncid_3 = data.cid_3

		-- For each dimension, do bounds checks as early as possible.
		for z = z0, z1 do
			if z >= minbox.z and z <= maxbox.z then
			for x = x0, x1 do
				if x >= minbox.x and x <= maxbox.x then
				for y = y0, y1 do
					if y >= ystart and y <= yend and y >= minbox.y and y <= maxbox.y then
						local vp = area:index(x, y, z)
						local cid = vm_data[vp]

						if cid == c_air then
							p2.x = x
							p2.y = y
							p2.z = z

							local n3d = area:index(x, y, z)
							local nrad = abs(sphereshear[n3d]) * SPHERE_MISSHAPE
							local D = floor(distance(p1, p2))

							local sphere = false
							local shell_outer = floor(sphere_rad + nrad)
							local shell_inner = floor(sphere_rad + nrad - SPHERE_WALL)
							local shell_center = floor(sphere_rad + nrad - SPHERE_INNER)

							if D <= shell_outer then
								sphere = true
							end

							if sphere then
								if D >= shell_inner then
									vm_data[vp] = ncid_1
								elseif D >= shell_center then
									if pr:next(1, 3) >= 2 then
										vm_data[vp] = data.cid_2
									else
										vm_data[vp] = data.cid_1
									end
								else
									vm_data[vp] = ncid_3
								end
							end
						end
					end
				end -- Y
				end
			end -- X
			end
		end -- Z
	end

	vm:set_data(vm_data)
end
