
package.loaded.Constants = nil; require("Constants")
require("Actors/AI/HumanBehaviors")
require("Actors/AI/CrabBehaviors")

function Create(self)
	---------------- AI variables start ----------------
	self.Ctrl = self:GetController()
	
	self.lateralMoveState = Actor.LAT_STILL
	self.deviceState = ACrab.STILL
	self.lastAIMode = Actor.AIMODE_NONE
	self.teamBlockState = Actor.NOTBLOCKED
	self.SentryFacing = self.HFlipped
	self.fire = false
	
	self.ReloadTimer = Timer()
	self.BlockedTimer = Timer()
	self.TargetLostTimer = Timer()
	
	self.PlayerInterferedTimer = Timer()
	self.PlayerInterferedTimer:SetSimTimeLimitMS(500)
	
	self.AlarmTimer = Timer()
	self.AlarmTimer:SetSimTimeLimitMS(500)
	
	-- check if this team is controlled by a human
	local Activ = ActivityMan:GetActivity()
	for player = Activity.PLAYER_1, Activity.MAXPLAYERCOUNT - 1 do
		if Activ:PlayerActive(player) and Activ:PlayerHuman(player) and
		   self.Team == Activ:GetTeamOfPlayer(player)
		then
			self.isPlayerOwned = true
			break
		end
	end
	
	function self:CreatePatrolBehavior()
		self.NextBehavior = coroutine.create(HumanBehaviors.Patrol)
		self.NextCleanup = nil
		self.NextBehaviorName = "Patrol"
	end
	
	function self:CreateSentryBehavior()
		if not self.FirearmIsReady then
			return
		end
		
		self.NextBehavior = coroutine.create(CrabBehaviors.Sentry)
		self.NextCleanup = nil
		self.NextBehaviorName = "Sentry"
	end
	
	function self:CreateAttackBehavior()
		if self.FirearmIsReady then
			self.NextBehavior = coroutine.create(CrabBehaviors.ShootTarget)
			self.NextBehaviorName = "ShootTarget"
		else
			if self.FirearmIsEmpty then
				self:ReloadFirearm()
			end
			
			return
		end
		
		self.NextCleanup = function(self)
			self.fire = false
			self.Target = nil
			self.deviceState = ACrab.STILL
		end
	end
	
	function self:CreateSuppressBehavior()
		if self.FirearmIsReady then
			self.NextBehavior = coroutine.create(CrabBehaviors.ShootArea)
			self.NextBehaviorName = "ShootArea"
		else
			if self.FirearmIsEmpty then
				self:ReloadFirearm()
			end
			
			return
		end
		
		self.NextCleanup = function(self)
			self.fire = false
			self.UnseenTarget = nil
			self.deviceState = ACrab.STILL
		end
	end
	
	function self:CreateGoToBehavior()
		self.NextBehavior = coroutine.create(CrabBehaviors.GoToWpt)
		self.NextCleanup = function(self)
			self.lateralMoveState = Actor.LAT_STILL
			self:UpdateMovePath()
		end
		self.NextBehaviorName = "GoToWpt"
	end
	
	function self:CreateBrainSearchBehavior()
		self.NextBehavior = coroutine.create(HumanBehaviors.BrainSearch)
		self.NextCleanup = nil
		self.NextBehaviorName = "BrainSearch"
	end
	
	---------------- AI variables end ----------------
	self.SearchTimer = Timer()
	self.radarRange = 1024
	self.ValidTargets = {}
	self.Frame = 1
	
	function self.UpdateInertSAM(self)
		if self.SearchTimer:IsPastSimMS(450) then
			local ArmedSAM = CreateAEmitter("Armed SAM", "Base.rte")
			if ArmedSAM then
				self.SAM.ToDelete = true
				self.SAM.HitsMOs = false
				self.SAM.GetsHitByMOs = false
				
				ArmedSAM.Pos = self.SAM.Pos
				ArmedSAM.Vel = self.SAM.Vel
				ArmedSAM.Team = self.SAM.Team
				ArmedSAM.IgnoresTeamHits = true
				ArmedSAM.RotAngle = self.SAM.RotAngle
				ArmedSAM.AngularVel = self.SAM.AngularVel
				
				self.proximityFuze = nil	-- Reset the proximity fuze
				
				-- Don't target the center of a craft
				if self.SAM_Target.ClassName == "ACDropShip" then
					if self.SAM_Target.Pos.X > self.SAM.Pos.X then
						self.SAM_Offset = Vector(-self.SAM_Target.Radius, 0) -- Left
					else
						self.SAM_Offset = Vector(self.SAM_Target.Radius, 0) -- Right
					end
				else
					self.SAM_Offset = Vector(0, self.SAM_Target.Radius*0.5) -- Below
				end
				
				-- Initialize missile velocity history and targeting data
				self.SAM_AimPos = self.SAM_Target.Pos + self.SAM_Target:RotateOffset(self.SAM_Offset)
				self.SAM_LastVel = Vector(self.SAM.Vel.X, self.SAM.Vel.Y)
				
				self.SAM = ArmedSAM
				MovableMan:AddMO(self.SAM)
				
				self.UpdateSAM = self.UpdateArmedSAM	-- Use the UpdateArmedSAM-function from now on
			else
				self.SAM = nil
			end
		else
			local angError = math.asin(Vector(0, -1):Cross(self.SAM:RotateOffset(Vector(1, 0))))	-- The angle between missile facing and straight up
			self.SAM.RotAngle = self.SAM.RotAngle + math.min(math.max(angError, -0.02), 0.02)
			self.SAM.AngularVel = self.SAM.AngularVel * 0.95
		end
	end
	
	function self.UpdateArmedSAM(self)
		-- Find the velocity vector that will take the missile to the target
		local FutureVel = self.SAM.Vel + (self.SAM.Vel-self.SAM_LastVel)*10
		local OptimalVel = SceneMan:ShortestDistance(self.SAM.Pos, self.SAM_AimPos, false)
		local angError = math.asin(OptimalVel.Normalized:Cross(FutureVel.Normalized))	-- The angle between FutureVel and OptimalVel
		
		-- Gradually turn towards the optimal velocity vector
		self.SAM.RotAngle = self.SAM.RotAngle + math.min(math.max(angError, -0.04), 0.04)
		
		-- Gradually return the thruster to the starting position if the missile is facing the target
		if math.abs(angError) < 0.15 then
			self.SAM.EmitAngle = self.SAM.EmitAngle * 0.8 + math.pi * 0.2
		else
			self.SAM.EmitAngle = math.max(math.min(self.SAM.EmitAngle+angError*0.1, 4.14), 2.14)	-- Vector thrust
		end
		
		-- Detonate the missile when appropriate
		local range = SceneMan:ShortestDistance(self.SAM.Pos, self.SAM_Target.Pos+self.SAM_Target:RotateOffset(self.SAM_Offset), false).Magnitude
		if self.proximityFuze then
			if range < 30 then
				self.SAM:GibThis()	-- The target is close enough; detonate
			elseif math.abs(angError) > 1.5 and range > self.proximityFuze then	-- The missile is moving away from the target: detonate
				self.SAM:GibThis()
			else
				self.proximityFuze = range
			end
		elseif range < 120 then	-- The target is close: arm the proximity fuze
			self.proximityFuze = range
		end
		
		self.SAM.AngularVel = self.SAM.AngularVel * 0.96 + (self.SAM.EmitAngle - math.pi) * 0.05	-- The vector thrust will cause the SAM to rotate
		self.SAM_LastVel = self.SAM_LastVel * 0.6 + self.SAM.Vel * 0.4	-- Used to calculate the acceleration of the missile
		self.SAM_AimPos = self.SAM_AimPos * 0.6 + (self.SAM_Target.Pos+self.SAM_Target:RotateOffset(self.SAM_Offset)+self.SAM_Target.Vel*math.min(range/50, 20)) * 0.4	-- Filter the AimPos to reduce noise
	end
end

function UpdateAI(self)
	if self.PlayerInterferedTimer:IsPastSimTimeLimit() then
		self.Behavior = nil	-- remove the current behavior
		if self.BehaviorCleanup then
			self.BehaviorCleanup(self)	-- clean up after the current behavior
			self.BehaviorCleanup = nil
		end
		
		self.Target = nil
		self.UnseenTarget = nil
		self.OldTargetPos = nil
		self.fire = false
		self.SentryFacing = self.HFlipped
		self.lastAIMode = Actor.AIMODE_NONE
	end
	
	self.PlayerInterferedTimer:Reset()
	
	if self.Target and not MovableMan:IsActor(self.Target) then
		self.Target = nil
	end
	
	if self.UnseenTarget and not MovableMan:IsActor(self.UnseenTarget) then
		self.UnseenTarget = nil
	end
	
	-- switch to the next behavior, if avaliable
	if self.NextBehavior then
		if self.BehaviorCleanup then
			self.BehaviorCleanup(self)
		end
		
		self.Behavior = self.NextBehavior
		self.BehaviorCleanup = self.NextCleanup
		self.BehaviorName = self.NextBehaviorName
		self.NextBehavior = nil
	end
	
	-- check if the AI mode has changed or if we need a new behavior
	if self.AIMode ~= self.lastAIMode or (not self.Behavior and not self.GoToBehavior) then
		self.Behavior = nil
		if self.BehaviorCleanup then
			self.BehaviorCleanup(self)	-- stop the current behavior
			self.BehaviorCleanup = nil
		end
		
		-- select a new behavior based on AI mode
		if self.AIMode == Actor.AIMODE_GOTO then
			self:CreateGoToBehavior()
		elseif self.AIMode == Actor.AIMODE_PATROL then
			self:CreatePatrolBehavior()
		elseif self.AIMode == Actor.AIMODE_BRAINHUNT then
			self:CreateBrainSearchBehavior()
		else
			if self.AIMode ~= self.lastAIMode and self.AIMode == Actor.AIMODE_SENTRY then
				self.SentryFacing = self.HFlipped	-- store the direction in which we should be looking
			end
			
			self:CreateSentryBehavior()
		end
		
		self.lastAIMode = self.AIMode
	end
	
	-- look for targets
	local FoundMO, HitPoint = CrabBehaviors.LookForTargets(self)
	if FoundMO then
		if self.Target and MovableMan:IsActor(self.Target) and FoundMO.ID == self.Target.ID then	-- found the same target
			self.TargetOffset = SceneMan:ShortestDistance(self.Target.Pos, HitPoint, false)
			self.TargetLostTimer:Reset()
			self.ReloadTimer:Reset()
		elseif MovableMan:IsActor(FoundMO) then
			if FoundMO.Team == self.Team then	-- found an ally
				if self.Target then
					if SceneMan:ShortestDistance(self.Pos, FoundMO.Pos, false).Magnitude < SceneMan:ShortestDistance(self.Pos, self.Target.Pos, false).Magnitude then
						self.Target = nil	-- stop shooting
					end
				elseif FoundMO.ClassName ~= "ADoor" and
					SceneMan:ShortestDistance(self.Pos, FoundMO.Pos, false).Magnitude < self.Diameter + FoundMO.Diameter
				then
					self.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
					self.ReloadTimer:Reset()
					self.TargetLostTimer:Reset()
					
					if self.Target then
						-- check if this MO sould be targeted instead
						if HumanBehaviors.CalculateThreatLevel(self, FoundMO) > HumanBehaviors.CalculateThreatLevel(self, self.Target) + 0.2 then
							self.OldTargetPos = self.Target.Pos
							self.Target = FoundMO
							self.TargetOffset = SceneMan:ShortestDistance(self.Target.Pos, HitPoint, false)	-- this is the distance vector from the target center to the point we hit with our ray
							self:CreateAttackBehavior()
						end
					else
						self.OldTargetPos = nil
						self.Target = FoundMO
						self.TargetOffset = SceneMan:ShortestDistance(self.Target.Pos, HitPoint, false)	-- this is the distance vector from the target center to the point we hit with our ray
						self:CreateAttackBehavior()
					end
				end
			end
		end
	else	-- no target found this frame
		if self.Target and self.TargetLostTimer:IsPastSimTimeLimit() then
			self.OldTargetPos = self.Target.Pos
			self.Target = nil	-- the target has been out of sight for too long, ignore it
		end
		
		if self.ReloadTimer:IsPastSimMS(8000) then	-- check if we need to reload
			self.ReloadTimer:Reset()
			if ToACrab(self).FirearmNeedsReload then
				self:ReloadFirearm()
			end
		end
	end
	
	-- run the selected behavior and delete it if it returns true
	if self.Behavior then
		local msg, done = coroutine.resume(self.Behavior, self)
		if not msg then
			ConsoleMan:PrintString(self.PresetName .. " behavior " .. self.BehaviorName .. " error:\n" .. done)	-- print the error message
			done = true
		end
		
		if done then
			self.Behavior = nil
			if self.BehaviorCleanup then
				self.BehaviorCleanup(self)
				self.BehaviorCleanup = nil
			end
		end
	end
	
	-- react to relevant AlarmEvents
	if not self.Target and not self.UnseenTarget then
		if self.AlarmTimer:IsPastSimTimeLimit() and HumanBehaviors.ProcessAlarmEvent(self) then
			self.AlarmTimer:Reset()
		elseif self.AlarmPos then	-- if alarmed, look at the alarming point until AlarmTimer has expired
			self.deviceState = ACrab.AIMING
			self.lateralMoveState = Actor.LAT_STILL
			self.Ctrl.AnalogAim = SceneMan:ShortestDistance(self.EyePos, self.AlarmPos, false)
		end
	end
	
	if self.teamBlockState == Actor.IGNORINGBLOCK then
		if self.BlockedTimer:IsPastSimMS(20000) then
			self.teamBlockState = Actor.NOTBLOCKED
		end
	elseif self.teamBlockState == Actor.BLOCKED then	-- we are blocked by a teammate, stop
		self.lateralMoveState = Actor.LAT_STILL
		if self.BlockedTimer:IsPastSimMS(10000) then
			self.BlockedTimer:Reset()
			self.teamBlockState = Actor.IGNORINGBLOCK
			self.BlockingActor = nil
		end
	else
		self.BlockedTimer:Reset()
	end
	
	-- controller states
	if self.fire then
		self.Ctrl:SetState(Controller.WEAPON_FIRE, true)
	end
	
	if self.deviceState == ACrab.AIMING then
		self.Ctrl:SetState(Controller.AIM_SHARP, true)
	end
	
	if self.lateralMoveState == Actor.LAT_LEFT then
		self.Ctrl:SetState(Controller.MOVE_LEFT, true)
	elseif self.lateralMoveState == Actor.LAT_RIGHT then
		self.Ctrl:SetState(Controller.MOVE_RIGHT, true)
	end
end

function Update(self)
	if self.SAM then	-- Check if any old missile is alive
		if MovableMan:ValidMO(self.SAM) then
			if self.SAM_Target and MovableMan:IsActor(self.SAM_Target) then
				self.UpdateSAM(self)
			else
				-- The target is not valid any more: replace the missile with an intert one
				if self.SAM.PresetName == "Armed SAM" then
					local InertSAM = CreateAEmitter("Inert SAM", "Base.rte")
					if InertSAM then
						self.SAM.ToDelete = true
						self.SAM.HitsMOs = false
						self.SAM.GetsHitByMOs = false
						
						InertSAM.Pos = self.SAM.Pos
						InertSAM.Vel = self.SAM.Vel
						InertSAM.RotAngle = self.SAM.RotAngle
						InertSAM.AngularVel = self.SAM.AngularVel
						InertSAM.Team = self.Team
						InertSAM.IgnoresTeamHits = true
						MovableMan:AddMO(InertSAM)
					end
				end
				
				self.SAM = nil
				self.SAM_Target = nil
			end
		else
			self.SAM = nil
		end
	elseif self.Frame > 0 then	-- we have SAMs left
		if #self.ValidTargets < 1 then	-- Find valid targets
			if self.SearchTimer:IsPastSimMS(250) then
				self.SearchTimer:Reset()	-- Only search a few times/sec to reduce calculations per update
				
				-- Only look for targets if there are no obstacles above us
				if SceneMan:CastObstacleRay(self.Pos, self:RotateOffset(Vector(0, -120)), Vector(), Vector(), self.ID, self.IgnoresWhichTeam, 0, 10) < 0 then
					local Dist, range, angle
					for Act in MovableMan.Actors do
						if Act.Team ~= self.Team and not Act:IsDead() and
							(Act.ClassName == "ACDropShip" or Act.ClassName == "ACRocket" or Act.ClassName == "ACraft")
						then
							Dist = SceneMan:ShortestDistance(self.Pos, Act.Pos+Act.Vel*3, false)
							range = Dist.Magnitude - Act.Radius
							if range < self.radarRange then	-- Go for enemy actors within x pixels
								angle = math.abs(math.asin(Dist.Normalized:Cross(self:RotateOffset(Vector(0, -1)))))
								if angle < 1.7 then -- Search in a ~200 degree arc above us
									table.insert(self.ValidTargets, {Actor=Act, priority=angle/3+range/300+(3-Act.Health/100)})	-- prioritize close, damaged targets that are straight above us
								end
							end
						end
					end
				end
			end
			
			-- Sort the targets in ascending order
			if #self.ValidTargets > 1 then
				table.sort(self.ValidTargets, function(A, B) return A.priority > B.priority end)
			end
			
			-- Store brain location
			local GmActiv = ActivityMan:GetActivity()
			for player = Activity.PLAYER_1, Activity.MAXPLAYERCOUNT - 1 do
				if GmActiv:PlayerActive(player) and GmActiv:GetTeamOfPlayer(player) == self.Team then
					local Brain = GmActiv:GetPlayerBrain(player)
					if Brain and MovableMan:IsActor(Brain) then
						self.MyBrainPos = Vector(Brain.Pos.X, Brain.Pos.Y)
					end
					
					break
				end
			end
		else	-- Check if the missile have a clear line of sight to any of the selected targets
			local NewTarget = table.remove(self.ValidTargets).Actor	-- Only check one target to reduce calculations per update
			if NewTarget and MovableMan:IsActor(NewTarget) and not NewTarget:IsDead() then
				local Trace = SceneMan:ShortestDistance(self.AboveHUDPos, NewTarget.Pos, false)
				-- Don't shoot at targets that are out of reach
				if Trace.Magnitude < self.radarRange then
					-- Don't shoot at targets that are very close to the brain
					if SceneMan:ShortestDistance(self.MyBrainPos or self.Pos, NewTarget.Pos, false).Largest - NewTarget.Radius > 120 then
						-- First do a very inexact scan of half the distance to the target for friendly dropships and terrain
						if SceneMan:CastObstacleRay(self.AboveHUDPos, Trace*0.5, Vector(), Vector(), self.ID, self.IgnoresWhichTeam, 0, 25) < 0 then
							-- If nothing was found, do a more exact scan for terrain all the way to the target
							if not SceneMan:CastStrengthRay(self.AboveHUDPos, Trace, 5, Vector(), 9, -1, true) then	-- Terrain str 5
								self.SearchTimer:Reset()
								
								-- Spawn the SAM
								self.SAM = CreateAEmitter("Inert SAM", "Base.rte")
								if self.SAM then
									local SpawnOffset = Vector(0, -15)
									if self.Frame < 2 then
										self.Frame = 2 -- Remove the left SAM
										SpawnOffset.X = -10
									else
										self.Frame = 0 -- Remove the right SAM
										SpawnOffset.X = 10
									end
									
									self.SAM.Team = self.Team
									self.SAM.RotAngle = self.RotAngle + 1.571
									self.SAM.AngularVel = self.AngularVel
									self.SAM.Pos = self.Pos + self:RotateOffset(SpawnOffset)
									self.SAM.Vel = self.Vel + self:RotateOffset(Vector(0, -15))
									self.SAM.IgnoresTeamHits = true
									self.SAM:TriggerBurst()
									MovableMan:AddMO(self.SAM)
									
									self.armedSAM = false
									self.SAM_Target = NewTarget
									
									-- Call this function to update the missile
									self.UpdateSAM = self.UpdateInertSAM
								end
							end
						end
					end
				end
			end
		end
	end
end
