local S = core.get_translator("eb_eyeballs")

local EYEBALL_UPDATE_TIME = 0.1
local EYEBALL_FAIL_TIME = 1.0
local SEE_RAYCAST_STEPS = 7
local MAX_EYE_ANGLE = math.pi/4
local EYE_FRONT_OFFSET = 0.501
local MIN_SEE_LIGHT = 3
local EYEBALL_FAIL_TIMER_RECOVER_FACTOR = 0.5

-- Set to true to render particles for the
-- raycasts made by seeing eyes
local DEBUG_RENDER_RAYS = false

local active_static_eyeballs = {}
local active_mobile_eyeballs = {}
local eyeball_fail_active = false
local eyeball_fail_timer = 0

eb_eyeballs.get_static_pupil_pos_and_dir = function(pos)
	local node = core.get_node(pos)
	if core.get_item_group(node.name, "eyeball") == 0 then
		return nil
	end
	local dir = core.facedir_to_dir(node.param2) or vector.zero()
	local offset = vector.multiply(dir, -EYE_FRONT_OFFSET)
	local pupil_pos = vector.add(pos, offset)
	dir = vector.multiply(dir, -1)
	return pupil_pos, dir
end

eb_eyeballs.get_mobile_pupil_pos_and_dir = function(ent)
	local rot = ent.object:get_rotation()
	local dir = vector.rotate(vector.new(0, 0, -1), rot)
	local offset = vector.multiply(dir, EYE_FRONT_OFFSET)
	local pos = ent.object:get_pos()
	local pupil_pos = vector.add(pos, offset)
	return pupil_pos, dir
end



local particle_line = function(pos1, pos2)
	local change = vector.new(
		pos2.x - pos1.x,
		pos2.y - pos1.y,
		pos2.z - pos1.z
	)
	local PSTEPS = 4
	for i=0, PSTEPS do
		local pos = vector.add(pos1, vector.multiply(change, i/PSTEPS))
		core.add_particle({
			pos = pos,
			texture = "eb_eyeballs_see_particle.png",
			expirationtime = 1.0,
			size = 0.25,
		})
	end
end

local ray_sees_player = function(pos1, pos2)
	local rc = core.raycast(pos1, pos2, true, true, { nodes = { ["group:eye_sees_through"] = false }})
	if DEBUG_RENDER_RAYS then
		particle_line(pos1, pos2)
	end
	for pointed_thing in rc do
		if pointed_thing.type == "object" then
			local obj = pointed_thing.ref
			if obj:is_player() then
				return obj
			end
		elseif pointed_thing.type == "node" then
			if not vector.equals(pointed_thing.under, pos1) then
				return nil
			end
		end
	end
	return nil
end


local is_player_in_front_of_eye = function(player, eye_pos, pupil_dir)
	if not player then
		return false
	end
	local player_pos = player:get_pos()
	local axes = { "x", "y", "z" }
	for a=1, #axes do
		local axis = axes[a]
		if pupil_dir[axis] > 0 then
			return player_pos[axis] >= eye_pos[axis]
		elseif pupil_dir[axis] < 0 then
			return player_pos[axis] <= eye_pos[axis]
		end
	end
	return false
end

eb_eyeballs.eye_sees_player = function(pupil_pos, pupil_dir_centered, eye_rotation)
	local step = MAX_EYE_ANGLE/SEE_RAYCAST_STEPS
	for i = -SEE_RAYCAST_STEPS, SEE_RAYCAST_STEPS do
		local yaw = i * step
		for j = -SEE_RAYCAST_STEPS, SEE_RAYCAST_STEPS do
			local pitch = j * step
			local ray_dir = vector.rotate(vector.new(0, 0, 1), { x = pitch, y = yaw, z = 0 })
			local pdc_rot = vector.dir_to_rotation(pupil_dir_centered)
			ray_dir = vector.rotate(ray_dir, pdc_rot)
			local offset = vector.multiply(ray_dir, eb_eyeballs.MAX_SEE_DISTANCE)
			local max_see_pos = vector.add(pupil_pos, offset)
			local seen_player = ray_sees_player(pupil_pos, max_see_pos)
			if seen_player then
				if eye_rotation then
					if eye_rotation.x >= math.pi/2 and eye_rotation.x <= 3*math.pi/2 then
						return seen_player, -i, j
					else
						return seen_player, i, -j
					end
				else
					if pupil_dir_centered.y > 0 then
						return seen_player, i, -j
					elseif pupil_dir_centered.y < 0 then
						return seen_player, -i, j
					elseif pupil_dir_centered.x > 0 then
						return seen_player, i, -j
					elseif pupil_dir_centered.x < 0 then
						return seen_player, i, j
					elseif pupil_dir_centered.z < 0 then
						return seen_player, i, -j
					elseif pupil_dir_centered.z > 0 then
						return seen_player, i, j
					end
				end
			end
		end
	end
end

eb_eyeballs.static_eye_sees_player = function(pos)
	local pupil_pos, pupil_dir_centered = eb_eyeballs.get_static_pupil_pos_and_dir(pos)
	-- Shortcut: Skip the expensive raycasts if the player isn't in front
	-- of the eyeball anyway
	if core.is_singleplayer() then
		local player = core.get_player_by_name("singleplayer")
		if not is_player_in_front_of_eye(player, pos, pupil_dir_centered) then
			return false
		end
	end
	return eb_eyeballs.eye_sees_player(pupil_pos, pupil_dir_centered)
end

eb_eyeballs.activate_eyeball = function(pos_or_obj)
	if type(pos_or_obj) == "table" and pos_or_obj.x then
		local hash = core.hash_node_position(pos_or_obj)
		active_static_eyeballs[hash] = true
	else
		active_mobile_eyeballs[pos_or_obj] = true
	end
end
eb_eyeballs.deactivate_eyeball = function(pos_or_obj)
	if type(pos_or_obj) == "table" and pos_or_obj.x then
		local hash = core.hash_node_position(pos_or_obj)
		active_static_eyeballs[hash] = nil
	else
		active_mobile_eyeballs[pos_or_obj] = nil
	end
end
eb_eyeballs.deactivate_eyeballs = function()
	active_static_eyeballs = {}
	active_mobile_eyeballs = {}
end
eb_eyeballs.activate_static_eyeballs_in_area = function(pos1, pos2)
	local eyeballs = core.find_nodes_in_area(pos1, pos2, "group:eyeball")
	for e=1, #eyeballs do
		local pos = eyeballs[e]
		local node = core.get_node(pos)
		local g = core.get_item_group(node.name, "eyeball")
		if g == 2 or g == 3 then
			eb_eyeballs.activate_eyeball(pos)
		end
	end
end
eb_eyeballs.activate_eyeball_fail = function()
	eyeball_fail_timer = 0
	eyeball_fail_active = true
	core.log("info", "[eb_eyeballs] Eyeball fail activated!")
end
eb_eyeballs.deactivate_eyeball_fail = function()
	eyeball_fail_timer = 0
	eyeball_fail_active = false
	core.log("info", "[eb_eyeballs] Eyeball fail deactivated!")
end

local registered_on_eyeball_fails = {}
eb_eyeballs.register_on_eyeball_fail = function(func)
	table.insert(registered_on_eyeball_fails, func)
end

local eyeball_fail = function(reason)
	local player = core.get_player_by_name("singleplayer")
	if not player then
		return
	end
	local pmeta = player:get_meta()
	if pmeta:get_int("eb_immortal:immortal") == 1 then
		return
	end
	if reason == "unseen" then
		core.sound_play({name="eb_eyeballs_fail_unseen", gain=1.0}, {to_player=player:get_player_name()}, true)
		core.log("action", "[eb_eyeballs] Player lost the level for not being seen by good eyes.")
		eb_transition.start_transition("death_unseen", 300)
	elseif reason == "seen_by_evil" then
		core.sound_play({name="eb_eyeballs_fail_seen_by_evil", gain=1.0}, {to_player=player:get_player_name()}, true)
		core.log("action", "[eb_eyeballs] Player lost the level for being seen by an evil eye.")
		eb_transition.start_transition("death_seen_by_evil", 300)
	else
		core.log("warning", "[eb_eyeballs] Player lost the level for an unknown reason.")
		eb_transition.start_transition("death", 300)
	end
	eyeball_fail_timer = 0
	eb_hud.update_eyeball_counters(nil, nil)
	for i=1, #registered_on_eyeball_fails do
		registered_on_eyeball_fails[i]()
	end
end

local last_good_seen_counter, last_evil_seen_counter

eb_eyeballs.get_seen_counters = function()
	return last_good_seen_counter, last_evil_seen_counter
end

local eyeball_timer = 0
core.register_globalstep(function(dtime)
	eyeball_timer = eyeball_timer + dtime
	if eyeball_timer < EYEBALL_UPDATE_TIME then
		return
	end

	local player = core.get_player_by_name("singleplayer")
	if not player then
		return
	end
	if eb_editor.player_in_editor(player) then
		return
	end
	if eb_teleport.is_teleporting() then
		return
	end

	local timediff = dtime + EYEBALL_UPDATE_TIME
	eyeball_timer = 0
	local seen_counter_good = 0
	local seen_counter_evil = 0

	-- check light level
	local ppos1 = player:get_pos()
	ppos1.y = ppos1.y + 0.5
	local ppos2 = table.copy(ppos1)
	ppos2.y = ppos2.y + 0.5
	-- NOTE: At midnight, there is still a small
	-- amount of light level to simulate the "moonlight"
	local light1 = core.get_node_light(ppos1)
	local light2 = core.get_node_light(ppos2)
	if not light1 or not light2 then
		return
	end
	local light = math.max(light1, light2)
	-- eyes can only see player if not too dark
	local too_dark = light < MIN_SEE_LIGHT

	for hash, _ in pairs(active_static_eyeballs) do
		local pos = core.get_position_from_hash(hash)
		local node = core.get_node(pos)
		local seen_player, sx, sy
		if too_dark then
			seen_player = false
		else
			seen_player, sx, sy = eb_eyeballs.static_eye_sees_player(pos)
		end
		if seen_player then
			local p2 = node.param2 % 32
			local newnodename
			local eyedef = core.registered_nodes[node.name]
			if eyedef and eyedef._eb_eyeball_open then
				local rev = false
				if p2 == 1 or p2 == 2 then
					rev = true
				end
				newnodename = eyedef._eb_eyeball_open.."_seeing"
				if eyedef._eb_sound_see then
					core.sound_play({name=eyedef._eb_sound_see, gain=1.0}, {pos=pos}, true)
				end
			end
			if core.get_item_group(node.name, "eyeball_good") == 1 then
				seen_counter_good = seen_counter_good + 1
			elseif core.get_item_group(node.name, "eyeball_evil") == 1 then
				seen_counter_evil = seen_counter_evil + 1
			end
			if newnodename then
				local g = core.get_item_group(node.name, "eyeball")
				if g == 2 or (g == 3 and node.name ~= newnodename) then
					node.name = newnodename
					minetest.swap_node(pos, node)
				end
			end
		else
			if core.get_item_group(node.name, "eyeball") == 3 then
				local eyedef = core.registered_nodes[node.name]
				if eyedef and eyedef._eb_eyeball_open then
					node.name = eyedef._eb_eyeball_open
					if eyedef._eb_sound_unsee then
						core.sound_play({name=eyedef._eb_sound_unsee, gain=1.0}, {pos=pos}, true)
					end
					minetest.swap_node(pos, node)
				end
			end
		end
	end

	-- Only check mobile eyes near player for performance
	local near_active_mobile_eyeballs = {}
	local far_active_mobile_eyeballs = {}
	local objs = core.get_objects_inside_radius(player:get_pos(), eb_eyeballs.MAX_SEE_DISTANCE+1)
	for o=1, #objs do
		local obj = objs[o]
		local lua = obj:get_luaentity()
		if lua and active_mobile_eyeballs[lua] then
			near_active_mobile_eyeballs[lua] = true
		end
	end
	for lua,_ in pairs(active_mobile_eyeballs) do
		if not near_active_mobile_eyeballs[lua] then
			far_active_mobile_eyeballs[lua] = true
		end
	end

	for ent in pairs(near_active_mobile_eyeballs) do
		local pos = ent.object:get_pos()
		if pos then
		if ent._state == 2 or ent._state == 3 then
			local ppos, pdir = eb_eyeballs.get_mobile_pupil_pos_and_dir(ent)
			local prot = ent.object:get_rotation()
			local seen_player, sx, sy
			if too_dark then
				seen_player = false
			else
				seen_player, sx, sy = eb_eyeballs.eye_sees_player(ppos, pdir, prot)
			end
			if seen_player then
				if ent.name == "eb_eyeballs:mobile_good" then
					seen_counter_good = seen_counter_good + 1
				elseif ent.name == "eb_eyeballs:mobile_evil" then
					seen_counter_evil = seen_counter_evil + 1
				end
				ent:_set_state(3)
			else
				if ent._state == 3 then
					ent:_set_state(2)
				end
			end
		end
		end
	end
	for ent in pairs(far_active_mobile_eyeballs) do
		if ent._state == 3 then
			ent:_set_state(2)
		end
	end

	if eyeball_fail_active then
		eb_hud.update_eyeball_counters(seen_counter_good, seen_counter_evil)
		last_good_seen_counter = seen_counter_good
		last_evil_seen_counter = seen_counter_evil
		local failing_good, failing_evil = false, false
		if seen_counter_good == 0 then
			failing_good = true
		end
		if seen_counter_evil > 0 then
			failing_evil = true
		end
		if failing_good or failing_evil then
			eyeball_fail_timer = eyeball_fail_timer + timediff
			if eyeball_fail_timer >= EYEBALL_FAIL_TIME then
				if failing_evil then
					eyeball_fail("seen_by_evil")
				elseif failing_good then
					eyeball_fail("unseen")
				end
			end
		else
			eyeball_fail_timer = math.max(0, eyeball_fail_timer - timediff * EYEBALL_FAIL_TIMER_RECOVER_FACTOR)
		end
		local danger_level = 0
		if eyeball_fail_timer > 0.05 then
			danger_level = math.min(eb_hud.MAX_DANGER_LEVEL, math.floor((eyeball_fail_timer / EYEBALL_FAIL_TIME) * (eb_hud.MAX_DANGER_LEVEL+1)))
		end
		eyeball_fail_timer = math.min(eyeball_fail_timer, EYEBALL_FAIL_TIME + 0.1)
		local updated = eb_hud.update_danger_meter(danger_level)
		if updated and danger_level > 0 and (failing_good or failing_evil) then
			core.sound_play({name="eb_eyeballs_danger_level", gain=0.4}, {to_player="singleplayer"}, true)
		end
	else
		eb_hud.update_eyeball_counters(nil, nil)
		eb_hud.update_danger_meter(nil)
		eyeball_fail_timer = 0
		last_good_seen_counter = nil
		last_evil_seen_counter = nil
	end
end)

function eb_eyeballs.object_is_eyeball(obj)
	local lua = obj:get_luaentity()
	if lua and eb_eyeballs.eyeball_entities[lua.name] then
		return true
	end
	return false
end
