
CrabBehaviors = {}

function CrabBehaviors.LookForTargets(AI, Owner)
	local viewAng
	if AI.Target then	-- widen the view angle to increase the chance of spotting new targets
		viewAng = 0.6
	elseif AI.deviceState == ACrab.AIMING then
		viewAng = 0.2
	elseif AI.deviceState == ACrab.POINTING then
		viewAng = 0.3
	else
		viewAng = RangeRand(0.4, 0.8)
	end
	
	local viewLen = SceneMan:ShortestDistance(Owner.EyePos, Owner.ViewPoint, false).Magnitude + FrameMan.PlayerScreenWidth * 0.55
	local Trace = Vector(viewLen, 0):RadRotate(viewAng*NormalRand()+Owner:GetAimAngle(true))
	local ID = SceneMan:CastMORay(Owner.EyePos, Trace, Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, false, 5)
	
	local FoundMO, HitPoint
	if ID < rte.NoMOID then
		HitPoint = SceneMan:GetLastRayHitPos()
		if not AI.isPlayerOwned or not SceneMan:IsUnseen(HitPoint.X, HitPoint.Y, Owner.Team) then	-- AI-teams ignore the fog
			FoundMO = MovableMan:GetMOFromID(ID)
			if ID ~= FoundMO.RootID then
				FoundMO = MovableMan:GetMOFromID(FoundMO.RootID)
			end
		end
	end
	
	if FoundMO then
		if AI.Target and MovableMan:IsActor(AI.Target) and FoundMO.ID == AI.Target.ID then	-- found the same target
			AI.TargetOffset = SceneMan:ShortestDistance(AI.Target.Pos, HitPoint, false)
			AI.TargetLostTimer:Reset()
			AI.ReloadTimer:Reset()
		elseif MovableMan:IsActor(FoundMO) then
			if FoundMO.Team == Owner.Team then	-- found an ally
				if AI.Target then
					if SceneMan:ShortestDistance(Owner.Pos, FoundMO.Pos, false).Magnitude < SceneMan:ShortestDistance(Owner.Pos, AI.Target.Pos, false).Magnitude then
						AI.Target = nil	-- stop shooting
					end
				elseif FoundMO.ClassName ~= "ADoor" and
					SceneMan:ShortestDistance(Owner.Pos, FoundMO.Pos, false).Magnitude < Owner.Diameter + FoundMO.Diameter
				then
					AI.BlockingActor = ToActor(FoundMO)	-- this actor is blocking our path
				end
			else
				if FoundMO.ClassName == "AHuman" then
					FoundMO = ToAHuman(FoundMO)
				elseif FoundMO.ClassName == "ACrab" then
					FoundMO = ToACrab(FoundMO)
				elseif FoundMO.ClassName == "ACRocket" then
					FoundMO = ToACRocket(FoundMO)
				elseif FoundMO.ClassName == "ACDropShip" then
					FoundMO = ToACDropShip(FoundMO)
				elseif FoundMO.ClassName == "ADoor" then
					FoundMO = ToADoor(FoundMO)
				elseif FoundMO.ClassName == "Actor" then
					FoundMO = ToActor(FoundMO)
				else
					FoundMO = nil
				end
				
				if FoundMO then
					if AI.Target then
						-- check if this MO sould be targeted instead
						if HumanBehaviors.CalculateThreatLevel(FoundMO, Owner) > HumanBehaviors.CalculateThreatLevel(AI.Target, Owner) + 0.2 then
							AI.OldTargetPos = Vector(AI.Target.Pos.X, AI.Target.Pos.Y)
							AI.Target = FoundMO
							AI.TargetOffset = SceneMan:ShortestDistance(AI.Target.Pos, HitPoint, false)	-- this is the distance vector from the target center to the point we hit with our ray
							AI:CreateAttackBehavior(Owner)
						end
					else
						AI.OldTargetPos = nil
						AI.Target = FoundMO
						AI.TargetOffset = SceneMan:ShortestDistance(AI.Target.Pos, HitPoint, false)	-- this is the distance vector from the target center to the point we hit with our ray
						AI:CreateAttackBehavior(Owner)
					end
				end
			end
		end
	else	-- no target found this frame
		if AI.Target and AI.TargetLostTimer:IsPastSimTimeLimit() then
			AI.OldTargetPos = Vector(AI.Target.Pos.X, AI.Target.Pos.Y)
			AI.Target = nil	-- the target has been out of sight for too long, ignore it
		end
		
		if AI.ReloadTimer:IsPastSimMS(8000) then	-- check if we need to reload
			AI.ReloadTimer:Reset()
			if Owner.FirearmNeedsReload then
				Owner:ReloadFirearm()
			end
		end
	end
end

-- in sentry behavior the agent only looks for new enemies, it sometimes sharp aims to increse spotting range
function CrabBehaviors.Sentry(AI, Owner)
	local sweepUp = true
	local sweepDone = false
	local maxAng = 0.49	-- TODO: get AimRange from the ini
	local minAng = -0.49
	local aim
	
	if AI.OldTargetPos then	-- try to reaquire an old target
		local Dist = SceneMan:ShortestDistance(Owner.EyePos, AI.OldTargetPos, false)
		AI.OldTargetPos = nil
		if (Dist.X < 0 and Owner.HFlipped) or (Dist.X > 0 and not Owner.HFlipped) then	-- we are facing the target	
			AI.deviceState = ACrab.AIMING
			AI.Ctrl.AnalogAim = Dist.Normalized
			
			for _ = 1, 30 do
				coroutine.yield()	-- aim here for ~0.5s
			end
		end
	elseif not AI.isPlayerOwned then -- face the most likely enemy approach direction
		for _ = 1, math.random(5) do	-- wait for a while
			coroutine.yield()
		end
		
		Owner:AddAISceneWaypoint(Vector(Owner.Pos.X, 0))
		Owner:UpdateMovePath()
		coroutine.yield()	-- wait until next frame
		
		-- face the direction of the first waypoint
		for WptPos in Owner.MovePath do
			local Dist = SceneMan:ShortestDistance(Owner.Pos, WptPos, false)
			if Dist.X > 5 then
				AI.SentryFacing = false
				AI.Ctrl.AnalogAim = Dist.Normalized
			elseif Dist.X < -5 then
				AI.SentryFacing = true
				AI.Ctrl.AnalogAim = Dist.Normalized
			end
			
			break
		end
	end
	
	while true do	-- start by looking forward
		aim = Owner:GetAimAngle(false)
		
		if sweepUp then
			if aim < maxAng/3 then
				AI.Ctrl:SetState(Controller.AIM_UP, false)
				coroutine.yield()	-- wait until next frame
				AI.Ctrl:SetState(Controller.AIM_UP, true)
			else
				sweepUp = false
			end
		else
			if aim > minAng/3 then
				AI.Ctrl:SetState(Controller.AIM_DOWN, false)
				coroutine.yield()	-- wait until next frame
				AI.Ctrl:SetState(Controller.AIM_DOWN, true)
			else
				sweepUp = true
				if sweepDone then
					break
				else
					sweepDone = true
				end
			end
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	if Owner.HFlipped ~= AI.SentryFacing then
		Owner.HFlipped = AI.SentryFacing	-- turn to the direction we have been order to guard
		return true	-- restart this behavior
	end
	
	while true do	-- look down
		aim = Owner:GetAimAngle(false)
		if aim > minAng then
			AI.Ctrl:SetState(Controller.AIM_DOWN, true)
		else
			break
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	local Hit = Vector()
	local NoObstacle = {}
	AI.deviceState = ACrab.AIMING
	
	while true do	-- scan the area for obstacles
		aim = Owner:GetAimAngle(false)
		if aim < maxAng then
			AI.Ctrl:SetState(Controller.AIM_UP, true)
		else
			break
		end
		
		-- save the angle to a table if there is no obstacle
		if not SceneMan:CastStrengthRay(Owner.EyePos, Vector(60, 0):RadRotate(Owner:GetAimAngle(true)), 5, Hit, 2, 0, true) then
			table.insert(NoObstacle, aim)	-- TODO: don't use a table for this
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	local SharpTimer = Timer()
	local aimTime = 2000
	local angDiff = 1
	AI.deviceState = ACrab.POINTING
	
	if #NoObstacle > 1 then	-- only aim where we know there are no obstacles, e.g. out of a gun port
		minAng = NoObstacle[1] * 0.95
		maxAng = NoObstacle[#NoObstacle] * 0.95
		angDiff = 1 / math.max(math.abs(maxAng - minAng), 0.1)	-- sharp aim longer from a small aiming window
	end
	
	while true do
		aim = Owner:GetAimAngle(false)
		
		if sweepUp then
			if aim < maxAng then
				if aim < maxAng/5 and aim > minAng/5 and PosRand() > 0.3 then
					AI.Ctrl:SetState(Controller.AIM_UP, false)
				else
					AI.Ctrl:SetState(Controller.AIM_UP, true)
				end
			else
				sweepUp = false
			end
		else
			if aim > minAng then
				if aim < maxAng/5 and aim > minAng/5 and PosRand() > 0.3 then
					AI.Ctrl:SetState(Controller.AIM_DOWN, false)
				else
					AI.Ctrl:SetState(Controller.AIM_DOWN, true)
				end
			else
				sweepUp = true
			end
		end
		
		if SharpTimer:IsPastSimMS(aimTime) then
			SharpTimer:Reset()
			
			if AI.deviceState == ACrab.AIMING then
				aimTime = RangeRand(1000, 3000)
				AI.deviceState = ACrab.POINTING
			else
				aimTime = RangeRand(6000, 12000) * angDiff
				AI.deviceState = ACrab.AIMING
			end
			
			if Owner.HFlipped ~= AI.SentryFacing then
				Owner.HFlipped = AI.SentryFacing	-- turn to the direction we have been order to guard
				break	-- restart this behavior
			end
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	return true
end

-- move to the next waypoint
function CrabBehaviors.GoToWpt(AI, Owner)
	if not Owner.MOMoveTarget then
		local Destination = Owner:GetLastAIWaypoint()
		if SceneMan:ShortestDistance(Destination, Owner.Pos, false).Magnitude < 20 then
			Owner:ClearAIWaypoints()
			if Owner.AIMode == Actor.AIMODE_GOTO then
				Owner.AIMode = Actor.AIMODE_SENTRY
			end
			return true
		end
	end
	
	local UpdatePathTimer = Timer()
	UpdatePathTimer:SetSimTimeLimitMS(5000)
	
	local StuckTimer = Timer()
	StuckTimer:SetSimTimeLimitMS(2000)
	
	local WptList, Waypoint, Dist, CurrDist
	
	while true do
		while AI.Target and Owner.FirearmIsReady do	-- don't move around if we have something to shoot at
			coroutine.yield()	-- wait until next frame
		end
		
		if Owner.Vel.Magnitude > 2 then
			StuckTimer:Reset()
		end
		
		if Owner.MOMoveTarget then	-- make the last waypoint marker stick to the MO we are following
			if MovableMan:ValidMO(Owner.MOMoveTarget) then
				Owner:RemoveMovePathEnd()
				Owner:AddToMovePathEnd(Owner.MOMoveTarget.Pos)
			else
				Owner.MOMoveTarget = nil
			end
		end
		
		if UpdatePathTimer:IsPastSimTimeLimit() then
			UpdatePathTimer:Reset()
			AI.deviceState = AHuman.STILL
			AI.lateralMoveState = Actor.LAT_STILL
			Waypoint = nil
			WptList = nil
		elseif StuckTimer:IsPastSimTimeLimit() then	-- dislodge
			StuckTimer:Reset()
			if AI.lateralMoveState == Actor.LAT_LEFT then
				AI.lateralMoveState = Actor.LAT_RIGHT
			elseif AI.lateralMoveState == Actor.LAT_LEFT then
				AI.lateralMoveState = Actor.LAT_LEFT
			else
				AI.lateralMoveState = math.random(Actor.LAT_LEFT, Actor.LAT_RIGHT)
			end
		elseif WptList then	-- we have a list of waypoints, folow it
			if #WptList < 1 and not Waypoint then	-- arrived
				break
			else
				if not Waypoint then	-- get the next waypoint in the list
					-- if we are following someone we must update the path more often
					if Owner.MOMoveTarget and MovableMan:ValidMO(Owner.MOMoveTarget) and 
						 SceneMan:ShortestDistance(Owner:GetLastAIWaypoint(), Owner.MOMoveTarget.Pos, false).Magnitude > 40 
					then	-- the target has moved
						WptList = nil
						Waypoint = nil
					else
						UpdatePathTimer:Reset()
						Waypoint = table.remove(WptList, 1)
						if #WptList > 0 then
							Owner:RemoveMovePathBeginning()
						elseif SceneMan:ShortestDistance(Owner.Pos, Waypoint.Pos, false).X < 10 then	-- the last waypoint
							Owner:ClearAIWaypoints()
							Owner.AIMode = Actor.AIMODE_SENTRY
							break
						end
					end
				end
				
				if Waypoint then
					if Owner.MOMoveTarget and MovableMan:ValidMO(Owner.MOMoveTarget) and
						SceneMan:ShortestDistance(Owner.Pos, Owner.MOMoveTarget.Pos, false).Magnitude < (Owner.Radius+Owner.MOMoveTarget.Radius)*3
					then
						AI.lateralMoveState = Actor.LAT_STILL
					else
						CurrDist = SceneMan:ShortestDistance(Owner.Pos, Waypoint.Pos, false)
						if CurrDist.X < -3 then
							AI.lateralMoveState = Actor.LAT_LEFT
						elseif CurrDist.X > 3 then
							AI.lateralMoveState = Actor.LAT_RIGHT
						else
							Waypoint = nil
						end
					end
				end
			end
		else	-- no waypoint list, create one in several small steps to reduce lag
			local TmpList = {}
			table.insert(TmpList, {Pos=Owner.Pos})
			Owner:UpdateMovePath()
			coroutine.yield()	-- wait until next frame
			
			for WptPos in Owner.MovePath do	-- skip any waypoint too close to the previous one
				if SceneMan:ShortestDistance(TmpList[#TmpList].Pos, WptPos, false).Magnitude > 10 then
					table.insert(TmpList, {Pos=WptPos})
				end
			end
			
			if #TmpList < 3 then
				Dist = nil
				if TmpList[2] then
					Dist = SceneMan:ShortestDistance(TmpList[2].Pos, Owner.Pos, false)
				end
				
				-- already at the target
				if not Dist or Dist.Magnitude < 25 then
					Owner:ClearMovePath()
					break
				end
			end
			
			coroutine.yield()	-- wait until next frame
			
			WptList = TmpList
			
			-- create the move path seen on the screen
			Owner:ClearMovePath()
			for _, Wpt in pairs(TmpList) do
				Owner:AddToMovePathEnd(Wpt.Pos)
			end
		end
		
		if AI.BlockingActor then
			if not MovableMan:IsActor(AI.BlockingActor) or
				SceneMan:ShortestDistance(Owner.Pos, AI.BlockingActor.Pos, false).Magnitude > (Owner.Diameter + AI.BlockingActor.Diameter)*1.2
			then
				AI.BlockingActor = nil
				AI.teamBlockState = Actor.NOTBLOCKED
				
				if Owner.AIMode == Actor.AIMODE_BRAINHUNT and AI.FollowingActor then
					AI.FollowingActor = nil
					break	-- end this behavior
				end
			elseif AI.teamBlockState == Actor.NOTBLOCKED and Waypoint then
				if (Waypoint.Pos.X > Owner.Pos.X and AI.BlockingActor.Pos.X > Owner.Pos.X) or
					 (Waypoint.Pos.X < Owner.Pos.X and AI.BlockingActor.Pos.X < Owner.Pos.X)
				then
					AI.teamBlockState = Actor.BLOCKED
					if Owner.AIMode == Actor.AIMODE_BRAINHUNT and Owner.AIMode == AI.BlockingActor.AIMode and
						(not AI.BlockingActor.MOMoveTarget or AI.BlockingActor.MOMoveTarget.ID ~= Owner.ID)
					then	-- don't follow an actor that is following us
						AI.FollowingActor = AI.BlockingActor
						Owner:ClearAIWaypoints()
						Owner:AddAIMOWaypoint(AI.FollowingActor)
						AI:CreateGoToBehavior(Owner)
						break	-- end this behavior
					end
				else
					AI.BlockingActor = nil
				end
			end
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	return true
end


-- open fire on the selected target
function CrabBehaviors.ShootTarget(AI, Owner)
	if not MovableMan:IsActor(AI.Target) then
		return true
	end
	
	AI.ReloadTimer:Reset()
	AI.TargetLostTimer:Reset()
	AI.TargetLostTimer:SetSimTimeLimitMS(1500)
	
	local LOSTimer = Timer()
	LOSTimer:SetSimTimeLimitMS(450)
	
	local TargetAvgVel = AI.Target.Vel
	local ShootTimer = Timer()
	local aimTime = RangeRand(400, 500)
	local aimError = RangeRand(-0.2, 0.2)
	local aim, AimPoint
	
	while true do
		if Owner.FirearmIsReady then
			TargetAvgVel = TargetAvgVel * 0.4 + AI.Target.Vel * 0.6	-- filter the target's velocity
			AI.deviceState = ACrab.AIMING
			AimPoint = AI.Target.Pos + AI.TargetOffset
			AI.TargetOffset = AI.TargetOffset * 0.967	-- move the aim point towards the target center
			
			aim = SceneMan:ShortestDistance(Owner.EyePos, AimPoint, false).AbsRadAngle
			AI.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim+aimError+RangeRand(-0.015, 0.015))
			
			if ShootTimer:IsPastSimMS(aimTime) then
				AI.fire = true
				aimError = aimError * RangeRand(0.96, 0.99)
			else
				AI.fire = false
			end
		else
			AI.deviceState = ACrab.POINTING
			AI.fire = false
			
			ShootTimer:Reset()
			if Owner.FirearmIsEmpty then
				Owner:ReloadFirearm()
				aimTime = RangeRand(300, 400)
				aimError = RangeRand(-0.14, 0.14)
			else
				break -- no firearm avaliable
			end
		end
		
		if LOSTimer:IsPastSimTimeLimit() then
			LOSTimer:Reset()
			
			if AimPoint and (not AI.isPlayerOwned or not SceneMan:IsUnseen(AimPoint.X, AimPoint.Y, Owner.Team)) then
				-- periodically check that we can see the target
				local Dist = SceneMan:ShortestDistance(Owner.EyePos, AimPoint, false)
				local viewLen = SceneMan:ShortestDistance(Owner.EyePos, Owner.ViewPoint, false).Magnitude + FrameMan.PlayerScreenWidth * 0.55	-- TODO: get AimDistance and SharpLength from the ini
				
				if Dist.Magnitude < viewLen then
					local ID = SceneMan:CastMORay(Owner.EyePos, Dist, Owner.ID, Owner.IgnoresWhichTeam, rte.grassID, false, 9)
					if ID < rte.NoMOID and (ID == AI.Target.ID or (MovableMan:GetMOFromID(ID)).RootID == AI.Target.ID) then
						AI.TargetLostTimer:Reset()	-- we can see the target
					end
				end
			end
		end
		
		coroutine.yield()	-- wait until next frame
		
		if not MovableMan:IsActor(AI.Target) or AI.Target:IsDead() then
			break
		end
	end
	
	return true
end

-- open fire on the area around the selected target
function CrabBehaviors.ShootArea(AI, Owner)
	if not MovableMan:IsActor(AI.UnseenTarget) then
		return true
	end
	
	local StartPos = Vector(AI.UnseenTarget.Pos.X, AI.UnseenTarget.Pos.Y)

	-- aim at the target in case we can see it when sharp aiming
	Owner:SetAimAngle(SceneMan:ShortestDistance(Owner.EyePos, StartPos, false).AbsRadAngle)
	AI.deviceState = ACrab.AIMING
	
	-- aim for ~250ms
	for _ = 1, 15 do
		coroutine.yield()	-- wait until next frame
	end
	
	local AimPoint
	for _ = 1, 5 do	-- try up to five times to find a target area that is resonably close to the target
		AimPoint = StartPos + Vector(RangeRand(-100, 100), RangeRand(-100, 50))
		if AimPoint.X > SceneMan.SceneWidth then
			AimPoint.X = SceneMan.SceneWidth - AimPoint.X
		elseif AimPoint.X < 0 then
			AimPoint.X = AimPoint.X + SceneMan.SceneWidth
		end
		
		-- check if we can fire at the AimPoint
		local Trace = SceneMan:ShortestDistance(Owner.EyePos, AimPoint, false)
		local rayLenght = SceneMan:CastObstacleRay(Owner.EyePos, Trace, Vector(), Vector(), rte.NoMOID, Owner.IgnoresWhichTeam, rte.grassID, 11)
		if Trace.Magnitude * 0.67 < rayLenght then
			break	-- the AimPoint is close enough to the target, start shooting
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	local CheckTargetTimer = Timer()
	local ShootTimer = Timer()
	local aimTime = RangeRand(200, 500)
	local aimError = RangeRand(-0.3, 0.3)
	local aim = SceneMan:ShortestDistance(Owner.EyePos, AimPoint, false).AbsRadAngle
	
	while true do
		if Owner.FirearmIsReady then
			AI.deviceState = ACrab.AIMING
			AI.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim+aimError+RangeRand(-0.01, 0.01))
			
			if ShootTimer:IsPastSimMS(aimTime) then
				AI.fire = true
				aimError = aimError * RangeRand(0.982, 0.995)
			else
				AI.fire = false
			end
		else
			AI.deviceState = ACrab.POINTING
			AI.fire = false
			
			if Owner.FirearmIsEmpty then
				Owner:ReloadFirearm()
			end
			
			break -- stop this behavior when the mag is empty
		end
		
		coroutine.yield()	-- wait until next frame
		
		if AI.UnseenTarget and CheckTargetTimer:IsPastSimMS(400) then
			if MovableMan:IsActor(AI.UnseenTarget) and (AI.UnseenTarget.ClassName == "AHuman" or AI.UnseenTarget.ClassName == "ACrab") then
				CheckTargetTimer:Reset()
				if AI.UnseenTarget:GetController():IsState(Controller.WEAPON_FIRE) then
					-- compare the enemy aim angle with the angle of the alarm vector
					local enemyAim = AI.UnseenTarget:GetAimAngle(true)
					if enemyAim > math.pi*2 then	-- make sure the angle is in the [0..2*pi] range
						enemyAim = enemyAim - math.pi*2
					elseif enemyAim < 0 then
						enemyAim = enemyAim + math.pi*2
					end
					
					local angDiff = SceneMan:ShortestDistance(AI.UnseenTarget.Pos, Owner.Pos, false).AbsRadAngle - enemyAim
					if angDiff > math.pi then	-- the difference between two angles can never be larger than pi
						angDiff = angDiff - math.pi*2
					elseif angDiff < -math.pi then
						angDiff = angDiff + math.pi*2
					end
					
					if math.abs(angDiff) < 0.5 then
						-- this actor is shooting in our direction
						AimPoint = AI.UnseenTarget.Pos + SceneMan:ShortestDistance(AI.UnseenTarget.Pos, AimPoint, false) / 2 + Vector(RangeRand(-30, 30), RangeRand(-30, 30))
						aimError = RangeRand(-0.2, 0.2)
						aim = SceneMan:ShortestDistance(Owner.EyePos, AimPoint, false).AbsRadAngle
					end
				end
			else
				AI.UnseenTarget = nil
			end
		end
	end
	
	return true
end

-- stop the user fom inadvertently modifying the storage table
local Proxy = {}
local Mt = {
	__index = CrabBehaviors,
	__newindex = function(Table, k, v)
		error("The CrabBehaviors table is read-only.", 2)
	end
}
setmetatable(Proxy, Mt)
CrabBehaviors = Proxy
