
-- Compare function for prioritizing healees
function AI_CompareHealee(First, Second)

	-- Compare health
	if First[2] == Second[2] then
		return First[3] < Second[3]
	end

	-- Prefer players over summons
	return First[2] < Second[2]
end

-- Compare function for prioritizing dead allies
function AI_CompareDead(First, Second)

	-- Prefer ally with highest max health
	return First[2] > Second[2]
end

-- Check for taunted debuff and attack target only
function AI_HandleTaunt(Source, ActionBarSlot)

	-- Check for taunt and use first action
	for i = 1, #Source.StatusEffects do
		local Effect = Source.StatusEffects[i]
		if Effect.Buff == Buff_Taunted and Effect.Source ~= nil then
			Source.SetTarget(Effect.Source)
			Source.SetAction(ActionBarSlot)
			return true
		end
	end

	return false
end

-- Return the first enemy index that has an attractant debuff
function AI_FindAttractant(Buff, Enemies)

	-- Find enemy with attractant with lowest priority value
	local LowestPriority = math.huge
	local EnemyIndex = 0
	for i = 1, #Enemies do
		for j = 1, #Enemies[i].StatusEffects do
			local StatusEffect = Enemies[i].StatusEffects[j]
			if StatusEffect.Buff == Buff and StatusEffect.Priority < LowestPriority then
				EnemyIndex = i
				LowestPriority = StatusEffect.Priority
			end
		end
	end

	return EnemyIndex
end

-- Base AI class

AI_Base = Base_Object:New({

	TrackTurns = false,

	SetAction = function(self, Object, Enemies, Allies, Storage)

		-- Use basic attack
		if Object.SetAction(0) then
			return true
		end

		return false
	end,

	SetTarget = function(self, Object, Enemies, Allies, Storage)

		-- Set random target
		local TargetIndex = Random.GetInt(1, #Enemies)
		Object.SetTarget(Enemies[TargetIndex].Pointer)
	end,

	Update = function(self, Object, Enemies, Allies, Time)

		-- Keep track of turns
		local Storage
		if self.TrackTurns then
			local BattleStorage = Battles[Object.BattleID]
			BattleStorage[Object.ID] = BattleStorage[Object.ID] or { Turns = 0 }
			Storage = BattleStorage[Object.ID]
		end

		-- Handle taunt debuff
		if AI_HandleTaunt(Object, 0) then
			return
		end

		-- Check for enemies
		if #Enemies == 0 then
			return
		end

		-- Set random target
		self:SetTarget(Object, Enemies, Allies, Storage)

		-- Set action
		if self:SetAction(Object, Enemies, Allies, Storage) then

			-- Update turn counter
			if Storage then
				Storage.Turns = Storage.Turns + 1
			end
		end
	end,
})

-- Attack random target

AI_Dumb = AI_Base:New()

-- Attack first enemy found with attractant debuff, otherwise attack random target

AI_Attract = AI_Base:New({

	SetTarget = function(self, Object, Enemies, Allies, Storage)

		-- Find enemy with debuff
		local AttractIndex = AI_FindAttractant(Buff_Fractured, Enemies)
		if AttractIndex == 0 then
			AttractIndex = AI_FindAttractant(Buff_Flayed, Enemies)
		end

		-- Get target
		local TargetIndex = AttractIndex > 0 and AttractIndex or Random.GetInt(1, #Enemies)

		-- Set target
		Object.SetTarget(Enemies[TargetIndex].Pointer)
	end,
})

-- Attack enemy with lowest health

AI_Smart = AI_Base:New({

	SetTarget = function(self, Object, Enemies, Allies, Storage)

		-- Get lowest health target
		local LowestHealth = math.huge
		local TargetIndex = 1
		for i = 1, #Enemies do
			if Enemies[i].Health < LowestHealth then
				TargetIndex = i
				LowestHealth = Enemies[i].Health
			end
		end

		-- Set target
		Object.SetTarget(Enemies[TargetIndex].Pointer)
	end,
})

-- Generic boss AI that randomly uses special attack against all enemies

AI_Boss = AI_Base:New({

	TrackTurns = true,
	SpecialMinTurns = 2,
	SpecialChance = 100,
	SpecialActionIndex = 1,
	SpecialTargetCount = BATTLE_LIMIT,

	ProcSpecial = function(self, Storage)
		return Storage.Turns >= self.SpecialMinTurns and Random.GetInt(1, 100) <= self.SpecialChance
	end,

	SetAction = function(self, Object, Enemies, Allies, Storage)

		-- Chance to do special attack
		if self:ProcSpecial(Storage) and Object.SetAction(self.SpecialActionIndex, self.SpecialTargetCount) then
			return true
		end

		-- Use basic attack
		if Object.SetAction(0) then
			return true
		end

		return false
	end,
})

-- Dead Queen

AI_DeadQueen = AI_Boss:New({

	SetAction = function(self, Object, Enemies, Allies, Storage)

		-- Chance to do special attack
		if self:ProcSpecial(Storage) and Object.SetAction(self.SpecialActionIndex, self.SpecialTargetCount) then
			return true
		end

		-- Set basic attack
		local ActionSet
		if Random.GetInt(1, 100) <= 65 then
			ActionSet = Object.SetAction(0)
		else
			ActionSet = Object.SetAction(2)
		end

		return ActionSet
	end,
})

-- Slime Prince

AI_SlimePrince = AI_Boss:New({

	ManaThreshold = 0.5,

	SetAction = function(self, Object, Enemies, Allies, Storage)

		-- Use potion when mana is low
		local CanUse = Object.SetAction(10)
		if CanUse and Object.Mana < Object.MaxMana * self.ManaThreshold then
			Object.SetTarget(Object.Pointer)
			return true
		end

		-- Build list of dead allies
		local DeadAllies = {}
		for i = 1, #Allies do
			if Allies[i].Health == 0 and Object.CanTarget(1, Allies[i].Pointer) then
				table.insert(DeadAllies, {
					i,
					Allies[i].MaxHealth
				})
			end
		end

		-- Attempt to resurrect
		if #DeadAllies > 0 then

			-- Sort
			table.sort(DeadAllies, AI_CompareDead)

			-- Resurrect target
			if Object.SetAction(1) then
				Object.SetTarget(Allies[DeadAllies[1][1]].Pointer)
				return true
			end
		end

		-- Use basic attack
		Object.SetAction(0)

		return true
	end,
})

-- Skeleton Priest

AI_SkeletonPriest = AI_Base:New({

	HealThreshold = 2.0 / 3.0,

	Update = function(self, Object, Enemies, Allies, Time)

		-- Handle taunt debuff
		if AI_HandleTaunt(Object, 0) then
			return
		end

		-- Get threshold for healing
		local HealThreshold = #Enemies > 0 and self.HealThreshold or 1

		-- Build list of healees
		local Healees = {}
		for i = 1, #Allies do
			if Allies[i].Health > 0 and Allies[i].Health < Allies[i].MaxHealth * HealThreshold then
				table.insert(Healees, {
					i,
					Allies[i].MonsterID ~= 0 and 1 or 0,
					Allies[i].Health
				})
			end
		end

		-- Attempt to heal
		if #Healees > 0 then

			-- Sort by priority
			table.sort(Healees, AI_CompareHealee)

			-- Set target
			if Object.SetAction(1) then
				Object.SetTarget(Allies[Healees[1][1]].Pointer)
				return
			end
		end

		-- Check for enemies
		if #Enemies == 0 then
			return
		end

		-- Find enemy with debuff
		local AttractIndex = AI_FindAttractant(Buff_Fractured, Enemies)
		if AttractIndex == 0 then
			AttractIndex = AI_FindAttractant(Buff_Flayed, Enemies)
		end

		-- Get target
		local TargetIndex = AttractIndex > 0 and AttractIndex or Random.GetInt(1, #Enemies)

		-- Set target
		Object.SetTarget(Enemies[TargetIndex].Pointer)

		-- Set skill
		Object.SetAction(0)
	end,
})

-- Goblin Thief

AI_GoblinThief = AI_Base:New({

	Update = function(self, Object, Enemies, Allies, Time)

		-- Get object storage
		local BattleStorage = Battles[Object.BattleID]
		BattleStorage[Object.ID] = BattleStorage[Object.ID] or { Steals = 0, Turns = 0 }
		local Storage = BattleStorage[Object.ID]

		-- Handle taunt debuff
		if AI_HandleTaunt(Object, 0) then
			return
		end

		-- Check for enemies
		if #Enemies == 0 then
			return
		end

		-- Check number of steals
		if Storage.Steals >= 2 then

			-- Flee
			Object.SetTarget(Object.Pointer)
			Object.SetAction(2)
		else

			-- Use elusive potion first turn
			if Storage.Turns == 0 then
				Object.SetTarget(Object.Pointer)
				Object.SetAction(10)
			else

				-- Pick target
				local TargetIndex = Random.GetInt(1, #Enemies)
				Object.SetTarget(Enemies[TargetIndex].Pointer)

				-- Steal
				if Random.GetInt(1, 100) <= 75 and Object.SetAction(1) then
					Storage.Steals = Storage.Steals + 1
				else
					-- Attack
					Object.SetAction(0)
				end
			end
		end

		-- Update turn counter
		if Storage then
			Storage.Turns = Storage and Storage.Turns + 1
		end
	end,
})

-- Lava Man

AI_LavaMan = AI_Boss:New({
	SpecialMinTurns = 1,
	SpecialActionIndex = 10,
	SpecialTargetCount = 1,
})

-- Snow Man

AI_Snowman = AI_Boss:New({
	SpecialMinTurns = 0,
	SpecialChance = 30,
	SpecialTargetCount = 1,
})

-- Lava Spitter

AI_LavaSpitter = AI_Boss:New({
	SpecialMinTurns = 1,
	SpecialChance = 50,
	SpecialActionIndex = 10,
	SpecialTargetCount = 1,
})

-- Goblin

AI_Goblin = AI_Boss:New({
	SpecialMinTurns = 0,
	SpecialChance = 33,
	SpecialTargetCount = 1,
})

-- Skeleton Mage

AI_SkeletonMage = AI_Base:New({

	Update = function(self, Object, Enemies, Allies, Time)

		-- Handle taunt debuff
		if AI_HandleTaunt(Object, 0) then
			return
		end

		-- Check for enemies
		if #Enemies == 0 then
			return
		end

		-- Look for existing demon
		local FoundDemon = false
		local AllyCount = 0
		for i = 1, #Allies do
			if (Allies[i].MonsterID == 23 or Allies[i].MonsterID == 39) and Allies[i].Owner == Object.Pointer then
				FoundDemon = true
			end

			AllyCount = AllyCount + 1
		end

		-- Don't summon if one exists
		local SummonDemon = false
		if FoundDemon == false then
			SummonDemon = true
		end

		-- Can't summon due to limit
		if AllyCount == BATTLE_LIMIT then
			SummonDemon = false
		end

		-- Summon demon
		if SummonDemon then
			Object.SetTarget(Object.Pointer)
			if Object.SetAction(1) then
				return
			end
		end

		-- Random target
		local TargetIndex = Random.GetInt(1, #Enemies)
		Object.SetTarget(Enemies[TargetIndex].Pointer)

		-- Cast spell
		local Roll = Random.GetInt(1, 100)
		if Roll <= 40 then

			-- Ice nova
			if Object.SetAction(2, BATTLE_LIMIT) then
				return
			end
		else

			-- Enfeeble or Flay
			if Object.SetAction(Random.GetInt(3, 4)) then
				return
			end
		end

		-- Basic attack
		Object.SetAction(0)
	end,
})

-- Raj

AI_Raj = AI_Boss:New()

-- Arach

AI_Arach = AI_Boss:New({
	ProcSpecial = function(self, Storage)
		return Storage.Turns % 4 == 0
	end,
})

-- Jem

AI_Jem = AI_Boss:New({

	SpecialMinTurns = 1,
	SpecialChance = 40,
	SpecialActionIndex = 11,

	SetAction = function(self, Object, Enemies, Allies, Storage)

		-- Heal
		if Object.Health <= Object.MaxHealth * 0.75 then
			if Object.SetAction(10) then
				Object.SetTarget(Object.Pointer)
				return true
			end
		end

		-- Chance to do special attack
		if self:ProcSpecial(Storage) and Object.SetAction(self.SpecialActionIndex, self.SpecialTargetCount) then
			return true
		end

		-- Use basic attack
		if Object.SetAction(0) then
			return true
		end

		return false
	end,
})

-- Zog

AI_Zog = AI_Boss:New({

	SpecialMinTurns = 0,
	SpecialChance = 40,

	SetAction = function(self, Object, Enemies, Allies, Storage)

		-- Chance to do special attack
		if self:ProcSpecial(Storage) and Object.SetAction(Random.GetInt(1, 6), self.SpecialTargetCount) then
			return true
		end

		-- Use basic attack
		if Object.SetAction(0) then
			return true
		end

		return false
	end,
})

-- Sol

AI_Sol = AI_Boss:New({

	ProcSpecial = function(self, Storage)
		return Storage.Turns % 5 == 0
	end,

	SetTarget = function(self, Object, Enemies, Allies, Storage)

		-- Get lowest health target
		local LowestHealth = math.huge
		local TargetIndex = 1
		for i = 1, #Enemies do
			if Enemies[i].Health < LowestHealth then
				TargetIndex = i
				LowestHealth = Enemies[i].Health
			end
		end

		-- Set target
		Object.SetTarget(Enemies[TargetIndex].Pointer)
	end,
})

-- Ultimate Chores

AI_UltimateChores = AI_Boss:New({

	SpecialMinTurns = 0,
	SpecialChance = 75,

	UpdateMessage = function(self, Object, Storage)

		-- Update count
		Storage.ItemsUsed = Storage.ItemsUsed and Storage.ItemsUsed + 1 or 0

		-- Get message
		local Message = nil
		if Storage.ItemsUsed == 10 then
			Message = "Muahahaha!!!"
		elseif Storage.ItemsUsed == 25 then
			Message = "You will never out chore me!"
		elseif Storage.ItemsUsed == 100 then
			Message = "I can do chores all day!"
		elseif Storage.ItemsUsed == 200 then
			Message = "Come on! Put your back into it!"
		elseif Storage.ItemsUsed == 300 then
			Message = "Is that all you got!?!"
		elseif Storage.ItemsUsed == 400 then
			Message = "That's it! Give in to the chores!"
		end

		-- Message user
		if Message then
			Object.BattleChat(Message)
		end
	end,

	SetAction = function(self, Object, Enemies, Allies, Storage)

		-- Purge debuffs
		for i = 1, #Object.StatusEffects do
			local Effect = Object.StatusEffects[i]
			if Effect.Buff == Buff_Poisoned then
				if Object.SetAction(10) then
					Object.SetTarget(Object.Pointer)
					self:UpdateMessage(Object, Storage)

					return true
				end
			elseif Effect.Buff == Buff_Fractured then
				if Object.SetAction(11) then
					Object.SetTarget(Object.Pointer)
					self:UpdateMessage(Object, Storage)

					return true
				end
			elseif Effect.Buff == Buff_Flayed then
				if Object.SetAction(12) then
					self:UpdateMessage(Object, Storage)

					return true
				end
			elseif Effect.Buff == Buff_Weak then
				if Object.SetAction(13) then
					Object.SetTarget(Object.Pointer)
					self:UpdateMessage(Object, Storage)

					return true
				end
			elseif Effect.Buff == Buff_Burning then
				if Object.SetAction(14) then
					self:UpdateMessage(Object, Storage)

					return true
				end
			end
		end

		-- Chance to do special attack
		if self:ProcSpecial(Storage) and Object.SetAction(Random.GetInt(1, 6), self.SpecialTargetCount) then
			return true
		end

		-- Use attack
		if Object.SetAction(0) then
			return true
		end

		return false
	end,
})

-- Dummy

AI_Dummy = AI_Base:New({

	FleeTime = 30.0,
	MessagePeriod = 5.0,

	Update = function(self, Object, Enemies, Allies, Time)

		-- Set up storage
		local BattleStorage = Battles[Object.BattleID]
		if BattleStorage[Object.ID] == nil then
			BattleStorage[Object.ID] = { Sent = 0, MessageTime = 5.0 }
		end
		local Storage = BattleStorage[Object.ID]

		-- Show running DPS
		if Time > Storage.MessageTime and Time < self.FleeTime then
			local DPS = (Object.MaxHealth - Object.Health) / Time
			Object.BattleChat(FormatSI(DPS, true) .. " DPS")
			Storage.MessageTime = Storage.MessageTime + self.MessagePeriod
		end

		-- Flee
		if Time < self.FleeTime then
			return
		end

		-- Set flee
		Object.SetTarget(Object.Pointer)
		Object.SetAction(1)

		-- Message clients
		if Storage.Sent == 0 then
			Storage.Sent = 1
			local Damage = Object.MaxHealth - Object.Health
			local DPS = FormatSI(Damage / self.FleeTime, true)
			Object.BroadcastMessage(FormatSI(Damage) .. " Damage (" .. DPS .. " DPS)", "cyan")
		end
	end,
})
