-- When trying to find a safe spot, the mob makes multiple raycasts
-- from the mob all around the mob horizontally. This number is
-- the angle difference in degrees between each ray.
local FIND_LAND_ANGLE_STEP = 15

vg_mobs.tasks = {}
vg_mobs.task_queues = {}

-- Returns true if the given node (by node name) is a liquid
function vg_mobs.is_liquid(nodename)
	local ndef = core.registered_nodes[nodename]
	return ndef and (
			ndef.liquid_move_physics == true or
			(ndef.liquid_move_physics == nil and ndef.liquidtype ~= "none")
		)
end

-- Returns true if node deals damage
function vg_mobs.is_damaging(nodename)
	local ndef = core.registered_nodes[nodename]
	return ndef and ndef.damage_per_second > 0
end

-- Returns true if node is walkable
function vg_mobs.is_walkable(nodename)
	local ndef = core.registered_nodes[nodename]
	return ndef and ndef.walkable
end

-- Returns true if node is climbable
function vg_mobs.is_climbable(nodename)
	local ndef = core.registered_nodes[nodename]
	return ndef and ndef.climbable
end

-- Returns true if the node(s) in front of the mob are safe.
-- This is considered unsafe:
-- * damage_per_second > 0
-- * drowning > 0
-- * a drop, if greater than cliff_depth
-- * a drop on a node with high fall_damage_add_percent
--
-- Parameters:
-- * mob: Mob object to check
-- * cliff_depth: How deep the mob is allowed to fall
-- * max_fall_damage_add_percent_drop_on: (optional): If set, mob can
--   not fall on a node with a fall_damage_add_percent group that is higher or equal than this value
function vg_mobs.is_front_safe(mob, cliff_depth, max_fall_damage_add_percent_drop_on)
	local vel = mob.object:get_velocity()
	vel.y = 0

	local yaw = mob.object:get_yaw()
	local dir = vector.normalize(vel)

	if vector.length(dir) > 0.5 then
		yaw = core.dir_to_yaw(dir)
	else
		dir = core.yaw_to_dir(yaw)
	end

	local pos = mob.object:get_pos()
	if mob._front_body_point then
		local fbp = table.copy(mob._front_body_point)
		fbp = vector.rotate_around_axis(fbp, vector.new(0, 1, 0), yaw)
		pos = vector.add(pos, fbp)
	end

	local pos_front = vector.add(pos, dir)
	local node_front = core.get_node(pos_front)
	local def_front = core.registered_nodes[node_front.name]
	if not def_front then
		return true
	end

	if (def_front.drowning > 0 or def_front.damage_per_second > 0) then
		return false
	end

	if def_front.walkable then
		return true
	end

	local safe_drop = false
	for c = 1, cliff_depth do
		local cpos = vector.add(pos_front, vector.new(0, -c, 0))
		local cnode = core.get_node(cpos)
		local cdef = core.registered_nodes[cnode.name]

		if not cdef then
			-- Unknown node
			return false
		elseif cdef.drowning > 0 then
			return false
		elseif cdef.damage_per_second > 0 then
			return false
		elseif cdef.walkable then
			safe_drop = true
			break
		end
	end
	return safe_drop
end

-- This function helps the mob find safe land from a lake or ocean.
--
-- Assuming that pos is a position above a large body of
-- liquid (like a lake or ocean), this function can return
-- the (approximately) closest position of walkable land
-- from that position, up to a hardcoded maximum range.
--
--
-- Argument:
-- * pos: Start position
-- * find_land_length: How far the mob looks away for safe land (raycast length)
--
-- returns: <position>, <angle from position>
-- or nil, nil if no position found
function vg_mobs.find_land_from_liquid(pos, find_land_length)
	local startpos = vector.copy(pos)
	startpos.y = startpos.y - 1

	local startnode = core.get_node(startpos)
	if not vg_mobs.is_liquid(startnode.name) then
		startpos.y = startpos.y - 1
	end

	local vec_y = vector.new(0, 1, 0)
	local best_pos, best_dist, best_angle

	for angle = 0, 359, FIND_LAND_ANGLE_STEP do
		local angle_rad = (angle/360) * (math.pi*2)
		local vec = vector.new(0, 0, 1)
		vec = vector.rotate_around_axis(vec, vec_y, angle_rad)
		vec = vector.multiply(vec, find_land_length)

		local rc = core.raycast(startpos, vector.add(startpos, vec), false, false)
		for pt in rc do
			if pt.type == "node" then
				local dist = vector.distance(startpos, pt.under)
				local up = vector.add(pt.under, vector.new(0, 1, 0))
				local upnode = core.get_node(up)
				if not best_dist or dist < best_dist then
					-- Ignore if ray collided with overhigh selection boxes (kelp, seagrass, etc.)
					if pt.intersection_point.y - 0.5 < pt.under.y and
							-- Node above must be non-walkable
							not vg_mobs.is_walkable(upnode.name) then
						best_pos = up
						best_dist = dist
						local pos1 = vector.copy(startpos)
						local pos2 = vector.copy(up)
						pos1.y = 0
						pos2.y = 0
						best_angle = core.dir_to_yaw(vector.direction(pos1, pos2))
						break
					end
				end
				if vg_mobs.is_walkable(upnode.name) then
					break
				end
			end
		end
	end
	return best_pos, best_angle
end

-- Arguments:
-- * pos: Start position
-- * find_land_length: How far the mob looks away for safe land (raycast length)
--
-- returns: <position>, <angle from position>
-- or nil, nil if no position found
function vg_mobs.find_safe_node_from_pos(pos, find_land_length)
	local startpos = vector.copy(pos)
	startpos.y = math.floor(startpos.y)
	startpos.y = startpos.y - 1

	local vec_y = vector.new(0, 1, 0)
	local best_pos, best_dist, best_angle

	for angle = 0, 359, FIND_LAND_ANGLE_STEP do
		local angle_rad = (angle/360) * (math.pi*2)
		local vec = vector.new(0, 0, 1)
		vec = vector.rotate_around_axis(vec, vec_y, angle_rad)
		vec = vector.multiply(vec, find_land_length)

		local rc = core.raycast(startpos, vector.add(startpos, vec), false, false)
		for pt in rc do
			if pt.type == "node" then
				local floor = pt.under
				local floornode = core.get_node(floor)
				local up = vector.add(floor, vector.new(0, 1, 0))
				local upnode = core.get_node(up)
				if vg_mobs.is_walkable(floornode.name) then
					if vg_mobs.is_walkable(upnode.name) then
						break
					elseif not vg_mobs.is_walkable(upnode.name) and
							-- Node must not hurt
							not vg_mobs.is_damaging(upnode.name) then
						local dist = vector.distance(startpos, floor)
						if not best_dist or dist < best_dist then
							best_pos = up
							best_dist = dist
							local pos1 = vector.copy(startpos)
							local pos2 = vector.copy(up)
							pos1.y = 0
							pos2.y = 0
							best_angle = core.dir_to_yaw(vector.direction(pos1, pos2))
						end
						break
					end
				end
			end
		end
	end
	return best_pos, best_angle
end

-- Add a "stand still" task to the mob's task queue with
-- an optional yaw
function vg_mobs.add_halt_to_task_queue(task_queue, mob, set_yaw, idle_min, idle_max)
	local mt_sleep = vg_mobs.microtasks.sleep(math.random(idle_min, idle_max)/1000)
	mt_sleep.start_animation = "idle"
	local task = vg_mobs.create_task({label="stand still"})

	local vel = mob.object:get_velocity()
	vel.x = 0
	vel.z = 0

	local yaw
	if not set_yaw then
		yaw = mob.object:get_yaw()
	else
		yaw = set_yaw
	end

	local mt_yaw = vg_mobs.microtasks.set_yaw(yaw)
	local mt_acceleration = vg_mobs.microtasks.set_acceleration(vg_mobs.GRAVITY_VECTOR)

	vg_mobs.add_microtask_to_task(mob, mt_acceleration, task)
	if set_yaw then
		vg_mobs.add_microtask_to_task(mob, mt_yaw, task)
	end
	vg_mobs.add_microtask_to_task(mob, vg_mobs.microtasks.move_straight(vel, yaw, vector.new(0.5,0,0.5), 1), task)
	vg_mobs.add_microtask_to_task(mob, mt_sleep, task)
	vg_mobs.add_task_to_task_queue(task_queue, task)
end
