--[[
Describes the falling/eroding effect for slopes
--]]

--[[
Pick replacement, node and area
--]]

-- Manage color for param2
-- @param replacement the replacement table
-- @param source the name or id of the node being transformed
-- @param dest the name or id of the new shape
-- @param param2_source the param2 value before transformation
-- @param param2_dest the param2 value for facedir (if any) after transformation
-- @return a new param2 value for dest node with color if necessary
local function manage_param2_color(replacement, source, dest, param2_source, param2_dest)
	if not replacement._colored_source then
		return param2_dest
	end
	if dest == replacement.source then
		-- param2_source will hold a 'color' value
		if source == replacement.source then
			-- from 'color' to 'color'
			return param2_source
		else
			-- from 'color' to 'colorfacedir'
			local new_color_index = replacement._color_convert(param2_source, true) % 8
			return param2_dest + (new_color_index * 32)
		end
	else
		-- param2_source will hold a 'colorfacedir' value
		if dest == replacement.source then
			-- from 'colorfacedir' to 'color'
			local old_color = math.floor(param2_source / 32)
			return replacement._color_convert(old_color, false) % 256
		else
			-- from 'colorfacedir' to an other 'colorfacedir'
			local color = math.floor(param2_source / 32)
			return param2_dest + (color * 32)
		end
	end
end

--- {Private} Pick a replacement node.
-- @param type The replacement shape. Either 'block', 'straight', 'ic' or 'oc'
-- @param name The name (or id for area) of the node to replace.
-- @param old_param2 The current value of param2 for the node to replace
-- @param param2 Facedir value to orient the new node.
-- @param for_area True when picking for an area, changes the parameter types
-- @return node {name=new_name, param2=new_param2} or area data {id=new_id, param2_data=new_param2}
-- or nil if dest node is not found.
local function pick_replacement(slope_type, name, old_param2, param2, for_area)
	local replacement
	if for_area then
		replacement = naturalslopeslib.get_replacement_id(name)
	else
		replacement = naturalslopeslib.get_replacement(name)
	end
	if not replacement then return nil end
	local dest_node_name = nil
	if slope_type == 'block' and replacement.source then
		dest_node_name = replacement.source
	elseif slope_type == 'pike' and replacement.pike then
		dest_node_name = replacement.pike
	elseif slope_type == 'straight' and replacement.straight then
		dest_node_name = replacement.straight
	elseif slope_type == 'ic' and replacement.inner then
		dest_node_name = replacement.inner
	elseif slope_type == 'oc' and replacement.outer then
		dest_node_name = replacement.outer
	end
	if dest_node_name then
		if param2 == nil then param2 = 0 end
		local color_param2 = manage_param2_color(replacement, name, dest_node_name, old_param2, param2)
		if for_area then
			return {id = dest_node_name, param2_data = color_param2}
		else
			return {name = dest_node_name, param2 = color_param2}
		end
	end
	return nil
end


--[[
Surrounding checks and get replacement
--]]

--- Check if a node is considered empty to switch shape.
-- @param pos The position to check
function naturalslopeslib.is_free_for_shape_update(pos)
	if not pos then return nil end
	local node = minetest.get_node_or_nil(pos)
	if node == nil then
		return nil
	end
	return node.name == 'air'
end

local air_id = minetest.get_content_id('air')
function naturalslopeslib.area_is_free_for_shape_update(area, data, index)
	if not area:containsi(index) then
		return nil
	end
	return data[index] == air_id
end
-- Deprecated name
naturalslopeslib.area_is_free_for_erosion = naturalslopeslib.area_is_free_for_shape_update

--- Get the replacement node according to it's surroundings.
-- @param pos The position of the node or index with VoxelArea.
-- @param node The node at that position or content id with VoxelArea.
-- @param area The VoxelArea, nil for single position update.
-- @param data Data from VoxelManip, nil for single position update.
-- @param param2_data Param2 data from VoxelManip, nil for single position update.
-- @return A node to use with minetest.set_node
-- or a table with id and param2_data if called with an area.
-- Nil if no replacement is found or a neighbour cannot be read.
function naturalslopeslib.get_replacement_node(pos, node, area, data, param2_data)
	-- Set functions and data according to update mode: single or VoxelManip
	local is_free = nil
	local new_pos = nil
	local replacement = nil
	local node_name = nil -- Either name or id
	local for_area = false
	local old_param2 = 0
	if area then
		for_area = true
		is_free = function (at_index) -- always use with new_pos
			return naturalslopeslib.area_is_free_for_shape_update(area, data, at_index)
		end
		new_pos = function(add) -- Get new index from current with add position
			local area_pos = area:position(pos)
			return area:indexp(vector.add(area_pos, add))
		end
		node_name = node
		old_param2 = param2_data[pos]
	else
		is_free = naturalslopeslib.is_free_for_shape_update
		new_pos = function(add) return vector.add(pos, add) end
		node_name = node.name
		old_param2 = node.param2
	end
	local is_ground -- ground or ceiling node
	local pointing_y = -1
	-- If there's something above and below, get back to full block
	local above_free = is_free(new_pos({x=0, y=1, z=0}))
	local below_free = is_free(new_pos({x=0, y=-1, z=0}))
	if above_free == nil or below_free == nil then
		return nil
	end
	if above_free and not below_free then
		is_ground = true
		pointing_y = 1
	elseif below_free and not above_free then
		is_ground = false
		pointing_y = 5
	else -- nothing below and above
		return pick_replacement("block", node_name, old_param2, 0, for_area)
	end
	-- Check blocks around
	local airXP = is_free(new_pos({x=1, y=0, z=0}))
	if airXP == nil then return nil end
	local airXM = is_free(new_pos({x=-1, y=0, z=0}))
	if airXM == nil then return nil end
	local airZP = is_free(new_pos({x=0, y=0, z=1}))
	if airZP == nil then return nil end
	local airZM = is_free(new_pos({x=0, y=0, z=-1}))
	if airZM == nil then return nil end
	local free_neighbors = 0
	for index, free in next, {airXP, airXM, airZP, airZM} do
		if free then free_neighbors = free_neighbors + 1 end
	end
	-- For four or three free neighbors, pike (slab)
	if free_neighbors == 4 or free_neighbors == 3 then
		local param2 = 0
		if is_ground == false then param2 = 20 end
		return pick_replacement("pike", node_name, old_param2, param2, for_area)
	-- For two free neighbors
	elseif free_neighbors == 2 then
		-- at opposite sides, block
		local param2
		if (airXP and airXM) or (airZP and airZM) then
			return pick_replacement('block', node_name, old_param2, 0, for_area)
		-- side by side, outer corner
		elseif (airXP and airZP) then
			if is_ground then param2 = 3 else param2 = 22 end
			return pick_replacement("oc", node_name, old_param2, param2, for_area)
		elseif (airXP and airZM) then
			if is_ground then param2 = 0 else param2 = 21 end
			return pick_replacement("oc", node_name, old_param2, param2, for_area)
		elseif (airXM and airZP) then
			if is_ground then param2 = 2 else param2 = 23 end
			return pick_replacement("oc", node_name, old_param2, param2, for_area)
		elseif (airXM and airZM) then
			if is_ground then param2 = 1 else param2 = 20 end
			return pick_replacement("oc", node_name, old_param2, param2, for_area)
		end
	-- For one free neighbor, straight slope
	elseif free_neighbors == 1 then
		local param2 = 0
		if airXP then if is_ground then param2 = 3 else param2 = 15 end
		elseif airXM then if is_ground then param2 = 1 else param2 = 17 end
		elseif airZP then if is_ground then param2 = 2 else param2 = 6 end
		elseif airZM then if is_ground then param2 = 0 else param2 = 8 end
		end
		return pick_replacement("straight", node_name, old_param2, param2, for_area)
	-- For no free neighbor check for a free diagonal for an inner corner
	-- or fully surrounded for a rebuild
	else
		local airXPZP = is_free(new_pos({x=1, y=0, z=1}))
		local airXPZM = is_free(new_pos({x=1, y=0, z=-1}))
		local airXMZP = is_free(new_pos({x=-1, y=0, z=1}))
		local airXMZM = is_free(new_pos({x=-1, y=0, z=-1}))
		local param2
		if airXPZP and not airXPZM and not airXMZP and not airXMZM then
			if is_ground then param2 = 3 else param2 = 15 end
			return pick_replacement("ic", node_name, old_param2, param2, for_area)
		elseif not airXPZP and airXPZM and not airXMZP and not airXMZM then
			if is_ground then param2 = 0 else param2 = 8 end
			return pick_replacement("ic", node_name, old_param2, param2, for_area)
		elseif not airXPZP and not airXPZM and airXMZP and not airXMZM then
			if is_ground then param2 = 2 else param2 = 23 end
			return pick_replacement("ic", node_name, old_param2, param2, for_area)
		elseif not airXPZP and not airXPZM and not airXMZP and airXMZM then
			if is_ground then param2 = 1 else param2 = 17 end
			return pick_replacement("ic", node_name, old_param2, param2, for_area)
		else
			return pick_replacement('block', node_name, old_param2, 0, for_area)
		end
	end
end


--[[
Do the replacement
--]]

-- Do shape update when random roll passes on a single node.
function naturalslopeslib.chance_update_shape(pos, node, factor, type)
	if factor == nil then factor = 1 end
	local replacement = naturalslopeslib.get_replacement(node.name)
	if not replacement then return false end
	local chance_factor = 1
	if type == "mapgen" or type == "stomp" or type == "place" or type == "time" then
		chance_factor = replacement.chance_factors[type]
	end
	if (math.random() * (replacement.chance * factor * chance_factor)) < 1.0 then
		return naturalslopeslib.update_shape(pos, node)
	end
	return false
end

--- Try to update the shape of a node according to it's surroundings.
-- @param pos The position of the node.
-- @param node The node at that position.
-- @return True if the node was updated, false otherwise.
function naturalslopeslib.update_shape(pos, node)
	local replacement = naturalslopeslib.get_replacement_node(pos, node)
	if replacement and (replacement.name ~= node.name or node.param2 ~= replacement.param2) then
		minetest.set_node(pos, replacement)
		return true
	else
		return false
	end
end

local function get_edges(minp, maxp)
	-- corner000 = minp
	local corner001 = {x = minp.x, y = minp.y, z = maxp.z}
	local corner010 = {x = minp.x, y = maxp.y, z = minp.z}
	local corner011 = {x = minp.x, y = maxp.y, z = maxp.z}
	local corner100 = {x = maxp.x, y = minp.y, z = minp.z}
	local corner101 = {x = maxp.x, y = minp.y, z = maxp.z}
	local corner110 = {x = maxp.x, y = maxp.y, z = minp.z}
	-- corner111 = maxp
	return { -- min pos, max pos, normal[x, y ,z]
		-- The 8 corners
		{minp,      minp,      {-1, -1, -1}},
		{corner001, corner001, {-1, -1,  1}},
		{corner010, corner010, {-1,  1, -1}},
		{corner011, corner011, {-1,  1,  1}},
		{corner100, corner100, { 1, -1, -1}},
		{corner101, corner101, { 1, -1,  1}},
		{corner110, corner110, { 1,  1, -1}},
		{maxp,      maxp,      { 1,  1,  1}},
		-- The 8 segments
		{{x = minp.x + 1, y = minp.y, z = minp.z}, {x = maxp.x - 1, y = minp.y, z = minp.z}, { 0, -1, -1}},
		{{x = minp.x + 1, y = maxp.y, z = minp.z}, {x = maxp.x - 1, y = maxp.y, z = minp.z}, { 0,  1, -1}},
		{{x = minp.x, y = minp.y + 1, z = minp.z}, {x = minp.x, y = maxp.y - 1, z = minp.z}, {-1,  0, -1}},
		{{x = maxp.x, y = minp.y + 1, z = minp.z}, {x = maxp.x, y = maxp.y - 1, z = minp.z}, { 1,  0, -1}},
		{{x = minp.x + 1, y = minp.y, z = maxp.z}, {x = maxp.x - 1, y = minp.y, z = maxp.z}, { 0, -1, 1}},
		{{x = minp.x + 1, y = maxp.y, z = maxp.z}, {x = maxp.x - 1, y = maxp.y, z = maxp.z}, { 0,  1, 1}},
		{{x = minp.x, y = minp.y + 1, z = maxp.z}, {x = minp.x, y = maxp.y - 1, z = maxp.z}, { -1, 0, 1}},
		{{x = maxp.x, y = minp.y + 1, z = maxp.z}, {x = maxp.x, y = maxp.y - 1, z = maxp.z}, {  1, 0, 1}},
		-- The 6 faces
		{{x = minp.x + 1, y = minp.y, z = minp.z + 1}, {x = maxp.x - 1, y = minp.y, z = maxp.z - 1}, {  0, -1,  0}},
		{{x = minp.x + 1, y = maxp.y, z = minp.z + 1}, {x = maxp.x - 1, y = maxp.y, z = maxp.z - 1}, {  0,  1,  0}},
		{{x = minp.x, y = minp.y + 1, z = minp.z + 1}, {x = minp.x, y = maxp.y - 1, z = maxp.z - 1}, { -1,  0,  0}},
		{{x = maxp.x, y = minp.y + 1, z = minp.z + 1}, {x = maxp.x, y = maxp.y - 1, z = maxp.z - 1}, {  1,  0,  0}},
		{{x = minp.x + 1, y = minp.y + 1, z = minp.z}, {x = maxp.x - 1, y = maxp.y - 1, z = minp.z}, {  0,  0, -1}},
		{{x = minp.x + 1, y = minp.y + 1, z = maxp.z}, {x = maxp.x - 1, y = maxp.y - 1, z = maxp.z}, {  0,  0,  1}}
	}
end

--- Massive shape update with VoxelManip.
-- @param minp Lower boundary of area.
-- @param mapx Higher boundary of area.
-- @param factor Factor for chance (0.1 means 10 times more likely to update)
-- @param skip (optional) Don't parse all nodes, skip randomly skip/2 to skip nodes
-- @param progressive_edges (optional) When true, edges are generated progressively (default)
-- @param type (optional) Transformation type for chance factor.
-- at every loop.
function naturalslopeslib.area_chance_update_shape(minp, maxp, factor, skip, progressive_edges, type)
	if not skip then skip = 0 end
	if progressive_edges == nil then progressive_edges = true end
	-- Run on every block
	local vm, emin, emax = minetest.get_voxel_manip()
	local e1, e2 = vm:read_from_map(minp, maxp)
	local area = VoxelArea:new{MinEdge = e1, MaxEdge = e2}
	local data = vm:get_data()
	local param2_data = vm:get_param2_data()
	local i = area:indexp(e1)
	local imax = area:indexp(e2)
	if progressive_edges then
		local edges = get_edges(minp, maxp)
		for _, edge in ipairs(edges) do
			naturalslopeslib.register_progressive_area_update(edge[1], edge[2], factor, skip, type, {x = edge[3][1], y = edge[3][2], z = edge[3][3]})
		end
	end
	while i <= imax do
		local x = (i-1) % area.ystride
		local y = (i-1) % area.zstride
		if x == 0 or x == area.ystride - 1
		or y == 0 or y == area.zstride - 1 then
			-- Skip edges
		else
			local replacement = naturalslopeslib.get_replacement_id(data[i])
			if replacement ~= nil then
				local chance_factor = 1
				if type == "mapgen" or type == "stomp" or type == "place" or type == "time" then
					chance_factor = replacement.chance_factors[type]
				end
				if math.random() * (replacement.chance * factor * chance_factor) < 1.0 then
					local new_data = naturalslopeslib.get_replacement_node(i, data[i], area, data, param2_data)
					if new_data then
						data[i] = new_data.id
						if new_data.param2_data then
							param2_data[i] = new_data.param2_data
						end
					end
				end
			end
		end
		i = i + 1 + math.random(skip / 2, skip)
	end
	vm:set_data(data)
	vm:set_param2_data(param2_data)
	vm:write_to_map()
end

naturalslopeslib.progressive_area_updates = {}

function naturalslopeslib.register_progressive_area_update(minp, maxp, factor, skip, type, edge_normal)
	if edge_normal ~= nil or minp.x == maxp.x or minp.y == maxp.y or minp.z == maxp.z then
		-- Explicit edge or ignored
		table.insert(naturalslopeslib.progressive_area_updates, {minp = minp, maxp = maxp,
				factor = factor, skip = skip, i = 1, edge_normal = edge_normal})
		return
	end
	-- else register the inner cube and all edges
	-- The inner cube
	table.insert(naturalslopeslib.progressive_area_updates, {
			minp = vector.add(minp, 1),
			maxp = vector.add(maxp, -1),
			factor = factor, skip = skip, i = 1, edge_normal = nil})
	local edges = get_edges(minp, maxp)
	-- Register
	for _, edge in ipairs(edges) do
		table.insert(naturalslopeslib.progressive_area_updates, {
				minp = edge[1], maxp = edge[2],
				factor = factor, type = type, skip = skip, i = 1,
				edge_normal = {x = edge[3][1], y = edge[3][2], z = edge[3][3]}
		})
	end
end


local function check_area_edges(area)
	if area.edge_normal == nil then
		return true
	end
	local edge = area.edge_normal
	local pos = area.minp
	local requirements = math.abs(edge.x) + math.abs(edge.y) + math.abs(edge.z)
	local found = 0
	if edge.x ~= 0 then
		if minetest.get_node_or_nil(vector.add(pos, {x = edge.x, y = 0, z = 0})) ~= nil then
			found = found + 1
		end
	end
	if edge.y ~= 0 then
		if minetest.get_node_or_nil(vector.add(pos, {x = 0, y = edge.y, z = 0})) ~= nil then
			found = found + 1
		end
	end
	if edge.z ~= 0 then
		if minetest.get_node_or_nil(vector.add(pos, {x = 0, y = 0, z = edge.z})) ~= nil then
			found = found + 1
		end
	end
	return found == requirements
end

local function progressive_area_update(start_time)
	if #naturalslopeslib.progressive_area_updates == 0 then
		return true
	end
	if start_time == nil then
		start_time = os.clock()
	end
	-- pick an area around a player at random and process it
	local players = minetest.get_connected_players()
	local processed_area_index = nil
	local alt_processed_area_index = nil
	for area_index, area in ipairs(naturalslopeslib.progressive_area_updates) do
		for _, p in ipairs(players) do
			local minp = area.minp
			local maxp = area.maxp
			local ppos = p:get_pos()
			if ppos.x >= minp.x and ppos.x <= maxp.x and ppos.y >= minp.y and ppos.y <= maxp.y and ppos.z >= minp.z and ppos.z <= maxp.z then
				-- Prefer an area in which a player is
				if (check_area_edges(area)) then
					processed_area_index = area_index
					break
				end
			elseif alt_processed_area_index == nil and ppos.x + 16 >= minp.x and ppos.x - 16 <= maxp.x and ppos.y + 16 >= minp.y and ppos.y - 16 <= maxp.y and ppos.z + 16 >= minp.z and ppos.z - 16 <= maxp.z then
				-- Else pick an area near a player
				if (check_area_edges(area)) then
					alt_processed_area_index = area_index
				end
			end
		end
		if processed_area_index ~= nil then
			local area = naturalslopeslib.progressive_area_updates[processed_area_index]
		end
	end
	if processed_area_index == nil then
		if alt_processed_area_index ~= nil then
			processed_area_index = alt_processed_area_index
		else
			processed_area_index = 1 -- try to reduce the queue as fast as possible
		end
	end
	local area = naturalslopeslib.progressive_area_updates[processed_area_index]
	local i = area.i
	local y_size = area.maxp.y - area.minp.y + 1
	local z_size = area.maxp.z - area.minp.z + 1
	local imax = y_size * z_size * (area.maxp.x - area.minp.x + 1)
	while i <= imax do
		local x = math.floor((i - 1) / (y_size * z_size))
		local y = math.floor((i - 1) / z_size) % y_size
		local z = (i - 1) % (z_size)
		local pos = {x = area.minp.x + x, y = area.minp.y + y, z = area.minp.z + z}
		local node = minetest.get_node(pos)
		naturalslopeslib.chance_update_shape(pos, node, area.factor, area.type)
		i = i + 1 + math.random(area.skip / 2, area.skip)
		if (os.clock() - start_time) > 0.1 and i <= imax then
			area.i = i
			return false
		end
	end
	table.remove(naturalslopeslib.progressive_area_updates, processed_area_index)
	if os.clock() - start_time < 0.1 then
		progressive_area_update(start_time)
	end
	return true
end

local generation_dtime = 0
local function generation_globalstep(dtime)
	generation_dtime = generation_dtime + dtime
	if generation_dtime > 0.1 then
		progressive_area_update()
		generation_dtime = 0
	end
end
minetest.register_globalstep(generation_globalstep)

minetest.register_on_shutdown(function()
	if #naturalslopeslib.progressive_area_updates > 0 then
		minetest.log("info", "Processing slope generation for queued areas")
		for i, area in ipairs(naturalslopeslib.progressive_area_updates) do
			minetest.log("info", (#naturalslopeslib.progressive_area_updates - i + 1) .. " remaining area(s)")
			naturalslopeslib.area_chance_update_shape(area.minp, area.maxp, area.factor, area.skip, false, area.type)
		end
	end
end)

--[[
Triggers registration
--]]

-- Stomp function to get the replacement node name
function naturalslopeslib.update_shape_on_walk(player, pos, node, desc, trigger_meta)
	return naturalslopeslib.get_replacement_node(pos, node)
end

-- Chat command
minetest.register_chatcommand('updshape', {
	func = function(name, param)
		local player = minetest.get_player_by_name(name)
		if not player then return false, 'Player not found' end
		if not minetest.check_player_privs(player, {server=true}) then return false, 'Update shape requires server privileges' end
		local pos = player:get_pos()
		local node_pos = {['x'] = pos.x, ['y'] = pos.y - 1, ['z'] = pos.z}
		local node = minetest.get_node(node_pos)
		if naturalslopeslib.update_shape(node_pos, node) then
			return true, 'Shape updated.'
		end
		return false, node.name .. " cannot have it's shape updated."
	end,
})

-- On generation big update
local function register_on_generation()
	if not naturalslopeslib._register_on_generated then
		return
	end
	if naturalslopeslib.setting_enable_shape_on_generation() then
		if naturalslopeslib.setting_generation_method() == "Progressive" then
			minetest.register_on_generated(function(minp, maxp, seed)
				naturalslopeslib.register_progressive_area_update(minp, maxp, naturalslopeslib.setting_generation_factor(), naturalslopeslib.setting_generation_skip(), "mapgen")
			end)
		else
			minetest.register_on_generated(function(minp, maxp, seed)
				naturalslopeslib.area_chance_update_shape(minp, maxp, naturalslopeslib.setting_generation_factor(), naturalslopeslib.setting_generation_skip(), true, "mapgen")
			end)
		end
	end
end
if not naturalslopeslib.setting_revert() then
	minetest.register_on_mods_loaded(register_on_generation)
end

--- On place neighbor update
local function on_place_or_dig(pos, force_below)
	local function update(pos, x, y, z, factor)
		local new_pos = vector.add(pos, vector.new(x, y, z))
		naturalslopeslib.chance_update_shape(new_pos, minetest.get_node(new_pos), factor, "place")
	end
	-- Update 8 neighbors plus above and below
	local place_factor = naturalslopeslib.setting_dig_place_factor()
	update(pos, 0, 0, 0, place_factor)
	update(pos, 1, 0, 0, place_factor)
	update(pos, 0, 0, 1, place_factor)
	update(pos, -1, 0, 0, place_factor)
	update(pos, 0, 0, -1, place_factor)
	update(pos, 1, 0, 1, place_factor)
	update(pos, 1, 0, -1, place_factor)
	update(pos, -1, 0, 1, place_factor)
	update(pos, -1, 0, -1, place_factor)
	if force_below then update(pos, 0, -1, 0, 0)
	else update(pos, 0, -1, 0, place_factor)
	end
	update(pos, 0, 1, 0, place_factor)
end

if naturalslopeslib.setting_enable_shape_on_dig_place() and not naturalslopeslib.setting_revert() then
	minetest.register_on_placenode(function(pos, new_node, placer, old_node, item_stack, pointed_thing)
		on_place_or_dig(pos, true)
	end)
	minetest.register_on_dignode(function(pos, old_node, digger)
		on_place_or_dig(pos)
	end)
end

