package.loaded.Constants = nil; require("Constants")

CrabBehaviors = {}

function CrabBehaviors.LookForTargets(self)
	local viewAng
	if self.Target then	-- widen the view angle to increase the chance of spotting new targets
		viewAng = 0.6
	elseif self.FirearmIsReady then
		if self.deviceState == ACrab.AIMING then
			viewAng = 0.2
		elseif self.deviceState == ACrab.POINTING then
			viewAng = 0.3
		else
			viewAng = RangeRand(0.4, 0.8)
		end
	else
		viewAng = RangeRand(0.5, 1.2)
	end
	
	local viewLen = SceneMan:ShortestDistance(self.EyePos, self.ViewPoint, false).Magnitude + FrameMan.PlayerScreenWidth*0.5
	local Trace = Vector(viewLen, 0):RadRotate(viewAng*NormalRand()+self:GetAimAngle(true))
	local ID = SceneMan:CastMORay(self.EyePos, Trace, self.ID, self.IgnoresWhichTeam, rte.grassID, false, 5)
	
	if ID < rte.NoMOID then
		local HitPoint = SceneMan:GetLastRayHitPos()
		if not SceneMan:IsUnseen(HitPoint.X, HitPoint.Y, self.Team) then
			local MO = MovableMan:GetMOFromID(ID)
			if ID ~= MO.RootID then
				MO = MovableMan:GetMOFromID(MO.RootID)
			end
			
			return MO, HitPoint
		end
	end
end

-- in sentry behavior the agent only looks for new enemies, it sometimes sharp aims to increse spotting range
function CrabBehaviors.Sentry(self)
	local sweepUp = true
	local sweepDone = false
	local maxAng = 0.49	-- TODO: get AimRange from the ini
	local minAng = -0.49
	local aim
	
	if self.OldTargetPos then	-- try to reaquire an old target
		local Dist = SceneMan:ShortestDistance(self.EyePos, self.OldTargetPos, false)
		self.OldTargetPos = nil
		if (Dist.X < 0 and self.HFlipped) or (Dist.X > 0 and not self.HFlipped) then	-- we are facing the target	
			self.deviceState = ACrab.AIMING
			self.Ctrl.AnalogAim = Dist.Normalized
			
			for i = 1, 30 do
				coroutine.yield()	-- aim here for ~0.5s
			end
		end
	elseif not self.isPlayerOwned then -- face the most likely enemy approach direction
		for i = 1, math.random(5) do	-- wait for a while
			coroutine.yield()
		end
		
		self:AddAISceneWaypoint(Vector(self.Pos.X, 0))
		self:UpdateMovePath()
		coroutine.yield()	-- wait until next frame
		
		-- face the direction of the first waypoint
		for WptPos in self.MovePath do
			local Dist = SceneMan:ShortestDistance(self.Pos, WptPos, false)
			if Dist.X > 5 then
				self.SentryFacing = false
				self.Ctrl.AnalogAim = Dist.Normalized
			elseif Dist.X < -5 then
				self.SentryFacing = true
				self.Ctrl.AnalogAim = Dist.Normalized
			end
			
			break
		end
	end
	
	while true do	-- start by looking forward
		aim = self:GetAimAngle(false)
		
		if sweepUp then
			if aim < maxAng/3 then
				self.Ctrl:SetState(Controller.AIM_UP, false)
				coroutine.yield()	-- wait until next frame
				self.Ctrl:SetState(Controller.AIM_UP, true)
			else
				sweepUp = false
			end
		else
			if aim > minAng/3 then
				self.Ctrl:SetState(Controller.AIM_DOWN, false)
				coroutine.yield()	-- wait until next frame
				self.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 self.HFlipped ~= self.SentryFacing then
		self.HFlipped = self.SentryFacing	-- turn to the direction we have been order to guard
		return true	-- restart this behavior
	end
	
	while true do	-- look down
		aim = self:GetAimAngle(false)
		if aim > minAng then
			self.Ctrl:SetState(Controller.AIM_DOWN, true)
		else
			break
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	local Hit = Vector()
	local NoObstacle = {}
	self.deviceState = ACrab.AIMING
	
	while true do	-- scan the area for obstacles
		aim = self:GetAimAngle(false)
		if aim < maxAng then
			self.Ctrl:SetState(Controller.AIM_UP, true)
		else
			break
		end
		
		-- save the angle to a table if there is no obstacle
		if not SceneMan:CastStrengthRay(self.EyePos, Vector(60, 0):RadRotate(self: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
	self.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 = self:GetAimAngle(false)
		
		if sweepUp then
			if aim < maxAng then
				if aim < maxAng/5 and aim > minAng/5 and PosRand() > 0.3 then
					self.Ctrl:SetState(Controller.AIM_UP, false)
				else
					self.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
					self.Ctrl:SetState(Controller.AIM_DOWN, false)
				else
					self.Ctrl:SetState(Controller.AIM_DOWN, true)
				end
			else
				sweepUp = true
			end
		end
		
		if SharpTimer:IsPastSimMS(aimTime) then
			SharpTimer:Reset()
			
			if self.deviceState == ACrab.AIMING then
				aimTime = RangeRand(1000, 3000)
				self.deviceState = ACrab.POINTING
			else
				aimTime = RangeRand(6000, 12000) * angDiff
				self.deviceState = ACrab.AIMING
			end
			
			if self.HFlipped ~= self.SentryFacing then
				self.HFlipped = self.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(self)
	if not self.MOMoveTarget then
		local Destination = self:GetLastAIWaypoint()
		if SceneMan:ShortestDistance(Destination, self.Pos, false).Magnitude < 20 then
			self:ClearAIWaypoints()
			if self.AIMode == Actor.AIMODE_GOTO then
				self.AIMode = Actor.AIMODE_SENTRY
			end
			return true
		end
	end
	
	local UpdatePathTimer = Timer()
	UpdatePathTimer:SetSimTimeLimitMS(5000)
	
	local StuckTimer = Timer()
	StuckTimer:SetSimTimeLimitMS(2000)
	
	local PrevWptPos = self.Pos
	local WptList, Waypoint, Dist, CurrDist
	
	while true do
		while self.Target and self.FirearmIsReady do	-- don't move around if we have something to shoot at
			coroutine.yield()	-- wait until next frame
		end
		
		if self.Vel.Magnitude > 2 then
			StuckTimer:Reset()
		end
		
		if self.MOMoveTarget then	-- make the last waypoint marker stick to the MO we are following
			if MovableMan:ValidMO(self.MOMoveTarget) then
				self:RemoveMovePathEnd()
				self:AddToMovePathEnd(self.MOMoveTarget.Pos)
			else
				self.MOMoveTarget = nil
			end
		end
		
		if UpdatePathTimer:IsPastSimTimeLimit() then
			UpdatePathTimer:Reset()
			self.deviceState = AHuman.STILL
			self.lateralMoveState = Actor.LAT_STILL
			Waypoint = nil
			WptList = nil
		elseif StuckTimer:IsPastSimTimeLimit() then	-- dislodge
			StuckTimer:Reset()
			if self.lateralMoveState == Actor.LAT_LEFT then
				self.lateralMoveState = Actor.LAT_RIGHT
			elseif self.lateralMoveState == Actor.LAT_LEFT then
				self.lateralMoveState = Actor.LAT_LEFT
			else
				self.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 self.MOMoveTarget and MovableMan:ValidMO(self.MOMoveTarget) and 
						 SceneMan:ShortestDistance(self:GetLastAIWaypoint(), self.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
							self:RemoveMovePathBeginning()
						elseif SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false).X < 10 then	-- the last waypoint
							self:ClearAIWaypoints()
							self.AIMode = Actor.AIMODE_SENTRY
							break
						end
					end
				end
				
				if Waypoint then
					if self.MOMoveTarget and MovableMan:ValidMO(self.MOMoveTarget) and
						SceneMan:ShortestDistance(self.Pos, self.MOMoveTarget.Pos, false).Magnitude < (self.Radius+self.MOMoveTarget.Radius)*3
					then
						self.lateralMoveState = Actor.LAT_STILL
					else
						CurrDist = SceneMan:ShortestDistance(self.Pos, Waypoint.Pos, false)
						if CurrDist.X < -3 then
							self.lateralMoveState = Actor.LAT_LEFT
						elseif CurrDist.X > 3 then
							self.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=self.Pos})
			self:UpdateMovePath()
			coroutine.yield()	-- wait until next frame
			
			for WptPos in self.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, self.Pos, false)
				end
				
				-- already at the target
				if not Dist or Dist.Magnitude < 25 then
					self:ClearMovePath()
					break
				end
			end
			
			coroutine.yield()	-- wait until next frame
			
			WptList = TmpList
			
			-- create the move path seen on the screen
			self:ClearMovePath()
			for _, Wpt in pairs(TmpList) do
				self:AddToMovePathEnd(Wpt.Pos)
			end
		end
		
		if self.BlockingActor then
			if not MovableMan:IsActor(self.BlockingActor) or
				SceneMan:ShortestDistance(self.Pos, self.BlockingActor.Pos, false).Magnitude > (self.Diameter + self.BlockingActor.Diameter)*1.2
			then
				self.BlockingActor = nil
				self.teamBlockState = Actor.NOTBLOCKED
				
				if self.AIMode == Actor.AIMODE_BRAINHUNT and self.FollowingActor then
					self.FollowingActor = nil
					break	-- end this behavior
				end
			elseif self.teamBlockState == Actor.NOTBLOCKED and Waypoint then
				if (Waypoint.Pos.X > self.Pos.X and self.BlockingActor.Pos.X > self.Pos.X) or
					 (Waypoint.Pos.X < self.Pos.X and self.BlockingActor.Pos.X < self.Pos.X)
				then
					self.teamBlockState = Actor.BLOCKED
					if self.AIMode == Actor.AIMODE_BRAINHUNT and self.AIMode == self.BlockingActor.AIMode and
						(not self.BlockingActor.MOMoveTarget or self.BlockingActor.MOMoveTarget.ID ~= self.ID)
					then	-- don't follow an actor that is following us
						self.FollowingActor = self.BlockingActor
						self:ClearAIWaypoints()
						self:AddAIMOWaypoint(self.FollowingActor)
						self:CreateGoToBehavior()
					end
				else
					self.BlockingActor = nil
				end
			end
		end
		
		coroutine.yield()	-- wait until next frame
	end
	
	return true
end


-- open fire on the selected target
function CrabBehaviors.ShootTarget(self)
	if not MovableMan:IsActor(self.Target) then
		return true
	end
	
	local TargetAvgVel = self.Target.Vel
	local ShootTimer = Timer()
	local aimTime = RangeRand(500, 800)
	local aimError = RangeRand(-0.2, 0.2)
	local aim, AimPoint
	
	local LOSTimer = Timer()
	LOSTimer:SetSimTimeLimitMS(650)
	self.TargetLostTimer:SetSimTimeLimitMS(2000)
	
	while true do		
		if self.FirearmIsReady then
			TargetAvgVel = TargetAvgVel * 0.4 + self.Target.Vel * 0.6	-- filter the target's velocity
			self.deviceState = ACrab.AIMING
			
			if SceneMan:ShortestDistance(self.Pos, AimPoint or self.Target.Pos, false).Magnitude < 40 then
				AimPoint = self.Target.Pos + self.TargetOffset * 0.9	-- move the aimpoint closer to the center of the target at close ranges
			else
				AimPoint = self.Target.Pos + self.TargetOffset
			end
			
			aim = SceneMan:ShortestDistance(self.EyePos, AimPoint, false).AbsRadAngle
			self.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim+aimError+RangeRand(-0.018, 0.018))
			
			if ShootTimer:IsPastSimMS(aimTime) then
				self.fire = true
				aimError = aimError * RangeRand(0.95, 0.99)
			else
				self.fire = false
			end
		else
			self.deviceState = ACrab.POINTING
			self.fire = false
			
			ShootTimer:Reset()
			if self.FirearmIsEmpty then
				self:ReloadFirearm()
				aimTime = RangeRand(300, 500)
				aimError = RangeRand(-0.14, 0.14)
			else
				break -- no firearm avaliable
			end
		end
		
		if LOSTimer:IsPastSimTimeLimit() then
			LOSTimer:Reset()
			
			if AimPoint and not SceneMan:IsUnseen(AimPoint.X, AimPoint.Y, self.Team) then
				-- periodically check that we can see the target
				local Dist = SceneMan:ShortestDistance(self.EyePos, AimPoint, false)
				if Dist.Magnitude < 130 + FrameMan.PlayerScreenWidth*0.5 then	-- TODO: get AimDistance and SharpLength from the ini
					local ID = SceneMan:CastMORay(self.EyePos, Dist, self.ID, self.IgnoresWhichTeam, rte.grassID, false, 9)
					if ID < rte.NoMOID and (ID == self.Target.ID or (MovableMan:GetMOFromID(ID)).RootID == self.Target.ID) then
						self.TargetLostTimer:Reset()	-- we can see the target
					end
				end
			end
		end
		
		coroutine.yield()	-- wait until next frame
		
		if not MovableMan:IsActor(self.Target) or self.Target:IsDead() then
			break
		end
	end
	
	return true
end

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

	-- aim at the target in case we can see it when sharp aiming
	self:SetAimAngle(SceneMan:ShortestDistance(self.EyePos, StartPos, false).AbsRadAngle)
	self.deviceState = ACrab.AIMING
	
	-- aim for ~250ms
	for i = 1, 15 do
		coroutine.yield()	-- wait until next frame
	end
	
	local AimPoint
	for i = 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(self.EyePos, AimPoint, false)
		local rayLenght = SceneMan:CastObstacleRay(self.EyePos, Trace, Vector(), Vector(), rte.NoMOID, self.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(self.EyePos, AimPoint, false).AbsRadAngle
	
	while true do
		if self.FirearmIsReady then
			self.deviceState = ACrab.AIMING
			self.Ctrl.AnalogAim = Vector(1,0):RadRotate(aim+aimError+RangeRand(-0.01, 0.01))
			
			if ShootTimer:IsPastSimMS(aimTime) then
				self.fire = true
				aimError = aimError * RangeRand(0.982, 0.995)
			else
				self.fire = false
			end
		else
			self.deviceState = ACrab.POINTING
			self.fire = false
			
			if self.FirearmIsEmpty then
				self:ReloadFirearm()
			end
			
			break -- stop this behavior when the mag is empty
		end
		
		coroutine.yield()	-- wait until next frame
		
		if self.UnseenTarget and CheckTargetTimer:IsPastSimMS(400) then
			if MovableMan:IsActor(self.UnseenTarget) and (self.UnseenTarget.ClassName == "AHuman" or self.UnseenTarget.ClassName == "ACrab") then
				CheckTargetTimer:Reset()
				if self.UnseenTarget:GetController():IsState(Controller.WEAPON_FIRE) then
					-- compare the enemy aim angle with the angle of the alarm vector
					local enemyAim = self.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(self.UnseenTarget.Pos, self.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 = self.UnseenTarget.Pos + SceneMan:ShortestDistance(self.UnseenTarget.Pos, AimPoint, false) / 2 + Vector(RangeRand(-30, 30), RangeRand(-30, 30))
						aimError = RangeRand(-0.2, 0.2)
						aim = SceneMan:ShortestDistance(self.EyePos, AimPoint, false).AbsRadAngle
					end
				end
			else
				self.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
