-- Path of Building
--
-- Module: Calc Mirages
-- Handles mirages that use player skills
--

local calcs = ...
local pairs = pairs
local ipairs = ipairs
local t_insert = table.insert
local t_remove = table.remove
local m_min = math.min
local m_max = math.max
local m_ceil = math.ceil
local m_floor = math.floor
local m_modf = math.modf
local s_format = string.format
local m_huge = math.huge
local bor = OR64 -- bit.bor
local band = AND64 -- bit.band

local function calculateMirage(env, config)
	if not config then
		return
	end

	local mirageSkill = nil

	if config.compareFunc then
		for _, skill in ipairs(env.player.activeSkillList) do
			if not skill.skillCfg.skillCond["usedByMirage"] then
				mirageSkill = config.compareFunc(skill, env, config, mirageSkill)
			end
		end
	end

	if mirageSkill then
		local newSkill, newEnv = calcs.copyActiveSkill(env, env.mode, mirageSkill)
		newSkill.skillCfg.skillCond["usedByMirage"] = true
		newEnv.limitedSkills = newEnv.limitedSkills or {}
		newEnv.limitedSkills[cacheSkillUUID(newSkill, newEnv)] = true
		newSkill.skillData.mirageUses = env.player.mainSkill.skillData.storedUses
		newSkill.skillTypes[SkillType.OtherThingUsesSkill] = true

		config.preCalcFunc(env, newSkill, newEnv)

		newEnv.player.mainSkill = newSkill
		calcs.perform(newEnv)
		config.postCalcFunc(env, newSkill, newEnv)
	else
		config.mirageSkillNotFoundFunc(env, config)
	end
	return not config.calcMainSkillOffence
end

function calcs.mirages(env)
	local config

	if env.player.mainSkill.skillCfg.skillCond["usedByMirage"] or env.player.mainSkill.skillFlags.disable then
		return
	end

	if env.player.mainSkill.skillData.triggeredByMirageArcher then
		config = {
			calcMainSkillOffence = true,
			compareFunc = function(skill, env, config, mirageSkill)
				if not env.player.mainSkill.skillCfg.skillCond["usedByMirage"] and env.player.weaponData1.type == "Bow" then
					return env.player.mainSkill
				end
			end,
			preCalcFunc = function(env, newSkill, newEnv)
				local moreDamage =  newSkill.skillModList:Sum("BASE", newSkill.skillCfg, "MirageArcherLessDamage")
				local moreAttackSpeed = newSkill.skillModList:Sum("BASE", newSkill.skillCfg, "MirageArcherLessAttackSpeed")
				local mirageCount = newSkill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "MirageArcherMaxCount")

				env.player.mainSkill.mirage = { }
				env.player.mainSkill.mirage.name = newSkill.activeEffect.grantedEffect.name
				env.player.mainSkill.mirage.count = mirageCount

				if not env.player.mainSkill.infoMessage then
					env.player.mainSkill.infoMessage = tostring(mirageCount) .. " Mirage Archers using " .. newSkill.activeEffect.grantedEffect.name
				end

				-- Add new modifiers to new skill (which already has all the old skill's modifiers)
				newSkill.skillModList:NewMod("Damage", "MORE", moreDamage, "Mirage Archer", env.player.mainSkill.ModFlags, env.player.mainSkill.KeywordFlags)
				newSkill.skillModList:NewMod("Speed", "MORE", moreAttackSpeed, "Mirage Archer", env.player.mainSkill.ModFlags, env.player.mainSkill.KeywordFlags)

				-- Does not use player resources
				newSkill.skillModList:NewMod("HasNoCost", "FLAG", true, "Used by mirage")

				if newSkill.skillPartName then
					env.player.mainSkill.mirage.skillPart = newSkill.skillPart
					env.player.mainSkill.mirage.skillPartName = newSkill.skillPartName
					env.player.mainSkill.mirage.infoMessage2 = newSkill.activeEffect.grantedEffect.name
				else
					env.player.mainSkill.mirage.skillPartName = nil
				end
			end,
			postCalcFunc = function(env, newSkill, newEnv)
				env.player.mainSkill.mirage.output = newEnv.player.output
				env.player.mainSkill.skillFlags.mirageArcher = true
				if newSkill.minion then
					env.player.mainSkill.mirage.minion = {}
					env.player.mainSkill.mirage.minion.output = newEnv.minion.output
				end

				if newEnv.player.breakdown then
					env.player.mainSkill.mirage.breakdown = newEnv.player.breakdown
					if newSkill.minion then
						env.player.mainSkill.mirage.minion.breakdown = newEnv.minion.breakdown
					end
				end
			end,
			mirageSkillNotFoundFunc = function(env, config)
				if not env.player.mainSkill.infoMessage2 then
					env.player.mainSkill.infoMessage2 = "No Mirage Archer active skill found"
				end
			end
		}
	elseif env.player.mainSkill.activeEffect.grantedEffect.name == "Reflection" then
		local usedSkillBestDps
		local maxMirageWarriors = env.player.mainSkill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "SaviourMirageWarriorMaxCount")
		config = {
			compareFunc = function(skill, env, config, mirageSkill)
				if skill ~= env.player.mainSkill and skill.skillTypes[SkillType.Attack] and not skill.skillTypes[SkillType.Totem] and not skill.skillTypes[SkillType.SummonsTotem] and band(skill.skillCfg.flags, bor(ModFlag.Sword, ModFlag.Weapon1H)) == bor(ModFlag.Sword, ModFlag.Weapon1H) and not skill.skillCfg.skillCond["usedByMirage"] then
					local uuid = cacheSkillUUID(skill, env)
					if not GlobalCache.cachedData[env.mode][uuid] then
						calcs.buildActiveSkill(env, env.mode, skill, uuid)
					end

					if GlobalCache.cachedData[env.mode][uuid] and GlobalCache.cachedData[env.mode][uuid].CritChance and GlobalCache.cachedData[env.mode][uuid].CritChance > 0 then
						if not mirageSkill then
							usedSkillBestDps = GlobalCache.cachedData[env.mode][uuid].TotalDPS
							return GlobalCache.cachedData[env.mode][uuid].ActiveSkill
						elseif GlobalCache.cachedData[env.mode][uuid].TotalDPS > usedSkillBestDps then
							usedSkillBestDps = GlobalCache.cachedData[env.mode][uuid].TotalDPS
							return GlobalCache.cachedData[env.mode][uuid].ActiveSkill
						end
					end
				end
				return mirageSkill
			end,
			preCalcFunc = function(env, newSkill, newEnv)
				local moreDamage = env.player.mainSkill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "SaviourMirageWarriorLessDamage")
				-- Add new modifiers to new skill (which already has all the old skill's modifiers)
				newSkill.skillModList:NewMod("Damage", "MORE", moreDamage, "The Saviour", env.player.mainSkill.ModFlags, env.player.mainSkill.KeywordFlags)
				if env.player.itemList["Weapon 1"] and env.player.itemList["Weapon 2"] and env.player.itemList["Weapon 1"].name == env.player.itemList["Weapon 2"].name then
					maxMirageWarriors = maxMirageWarriors / 2
				end
				newSkill.skillModList:NewMod("QuantityMultiplier", "BASE", maxMirageWarriors, "The Saviour Mirage Warriors", env.player.mainSkill.ModFlags, env.player.mainSkill.KeywordFlags)
				-- Does not use player resources
				newSkill.skillModList:NewMod("HasNoCost", "FLAG", true, "Used by mirage")
			end,
			postCalcFunc = function(env, newSkill, newEnv)
				env.player.mainSkill = newSkill
				env.player.mainSkill.infoMessage = tostring(maxMirageWarriors) .. " Mirage Warriors using " .. newSkill.activeEffect.grantedEffect.name

				-- Re-link over the output
				env.player.output = newEnv.player.output
				if newSkill.minion then
					env.minion = newEnv.player.mainSkill.minion
					env.minion.output = newEnv.minion.output
				end

				-- Re-link over the breakdown (if present)
				if newEnv.player.breakdown then
					env.player.breakdown = newEnv.player.breakdown
					if newSkill.minion then
						env.minion.breakdown = newEnv.minion.breakdown
					end
				end
			end,
			mirageSkillNotFoundFunc = function(env, config)
				env.player.mainSkill.disableReason = "No Saviour active skill found"
				env.player.mainSkill.skillFlags.disable = true
			end
		}
	elseif env.player.mainSkill.activeEffect.grantedEffect.name == "Tawhoa's Chosen" then
		local usedSkillBestDps
		local EffectiveSourceRate

		local triggerCD
		local triggerCDAdjusted
		local triggerCDTickRounded
		local triggeredCD
		local triggeredCDAdjusted
		local triggeredCDTickRounded
		local icdrSkill
		local TriggerRateCap
		local actionCooldown
		local SkillTriggerRate

		config = {
			compareFunc = function(skill, env, config, mirageSkill)
				local isDisabled = skill.skillFlags and skill.skillFlags.disable
				local skillTypeMatch = (skill.skillTypes[SkillType.Slam] or skill.skillTypes[SkillType.Melee]) and skill.skillTypes[SkillType.Attack] 
				local skillTypeExcludes = skill.skillTypes[SkillType.Vaal] or skill.skillTypes[SkillType.Totem] or skill.skillTypes[SkillType.SummonsTotem]
				if skill ~= env.player.mainSkill and not isTriggered(skill) and not isDisabled and skillTypeMatch and not skillTypeExcludes and not skill.skillCfg.skillCond["usedByMirage"] then
					local uuid = cacheSkillUUID(skill, env)
					if not GlobalCache.cachedData[env.mode][uuid] or env.mode == "CALCULATOR" then
						calcs.buildActiveSkill(env, env.mode, skill, uuid)
					end

					if not mirageSkill or (GlobalCache.cachedData[env.mode][uuid] and GlobalCache.cachedData[env.mode][uuid].TotalDPS > usedSkillBestDps) then
						usedSkillBestDps = GlobalCache.cachedData[env.mode][uuid].TotalDPS
						EffectiveSourceRate = GlobalCache.cachedData[env.mode][uuid].Speed
						return GlobalCache.cachedData[env.mode][uuid].ActiveSkill
					end
				end
				return mirageSkill
			end,
			preCalcFunc = function(env, newSkill, newEnv)
				icdrSkill = calcLib.mod(newSkill.skillModList, newSkill.skillCfg, "CooldownRecovery")
				
				triggeredCD = newSkill.skillData.cooldown or 0
				triggeredCDAdjusted = triggeredCD / icdrSkill
				triggeredCDTickRounded = m_ceil(triggeredCDAdjusted * data.misc.ServerTickRate) / data.misc.ServerTickRate

				triggerCD = env.player.mainSkill.skillData.cooldown or  0
				triggerCDAdjusted = triggerCD / icdrSkill
				triggerCDTickRounded = m_ceil(triggerCDAdjusted * data.misc.ServerTickRate) / data.misc.ServerTickRate
				
				actionCooldown = m_max( triggeredCDTickRounded or 0, triggerCDTickRounded or 0 )
				
				TriggerRateCap = m_huge
				if actionCooldown ~= 0 then
					TriggerRateCap = 1 / actionCooldown
				end

				SkillTriggerRate = EffectiveSourceRate ~= 0 and calcMultiSpellRotationImpact(env, {{ uuid = cacheSkillUUID(env.player.mainSkill, env), cd = triggeredCD, icdr = icdrSkill }}, EffectiveSourceRate, triggerCD) or 0

				-- Override attack speed with trigger rate
				newSkill.skillData.triggerRate = SkillTriggerRate
				newSkill.skillData.triggered = true
				newSkill.skillFlags.triggered = true

				-- Does not use player resources
				newSkill.skillModList:NewMod("HasNoCost", "FLAG", true, "Used by Tawhoa's Chosen")

				local moreDamage = env.player.mainSkill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "ChieftainMirageChieftainMoreDamage")
				-- Add new modifiers to new skill (which already has all the old skill's modifiers)
				newSkill.skillModList:NewMod("Damage", "MORE", moreDamage, "Tawhoa's Chosen", env.player.mainSkill.ModFlags, env.player.mainSkill.KeywordFlags)
			end,
			postCalcFunc = function(env, newSkill, newEnv)
				env.player.mainSkill = newSkill
				env.player.mainSkill.infoMessage = "Tawhoa's Chosen using " .. newSkill.activeEffect.grantedEffect.name

				env.player.output = newEnv.player.output
				env.player.output.Speed = SkillTriggerRate
				env.player.output.TriggerRateCap = TriggerRateCap
				env.player.output.EffectiveSourceRate = EffectiveSourceRate
				env.player.output.SkillTriggerRate = SkillTriggerRate

				if newEnv.player.breakdown then
					if triggeredCD ~= 0 then
						newEnv.player.breakdown.TriggerRateCap = {
							s_format("%.2f ^8(base cooldown of triggered skill)", triggeredCD),
							s_format("/ %.2f ^8(increased/reduced cooldown recovery)", icdrSkill),
							s_format("= %.2f ^8(final cooldown of triggered skill)", triggeredCDAdjusted),
							"",
							s_format("%.2f ^8(Tawhoa's Chosen base cooldown)", triggerCD),
							s_format("/ %.2f ^8(increased/reduced cooldown recovery)", icdrSkill),
							s_format("= %.2f ^8(effective trigger cooldown)", triggerCDAdjusted),
							"",
							s_format("%.2f ^8(biggest of trigger cooldown and triggered skill cooldown)", m_max(triggerCDAdjusted, triggeredCDAdjusted)),
							"",
							s_format("%.2f ^8(adjusted for server tick rate)", actionCooldown),
							"",
							"Trigger rate:",
							s_format("1 / %.3f", actionCooldown),
							s_format("= %.2f ^8per second", TriggerRateCap),
						}
					else
						newEnv.player.breakdown.TriggerRateCap = {
							"Triggered skill has no base cooldown",
							"",
							s_format("%.2f ^8(Tawhoa's Chosen base cooldown)", triggerCD),
							s_format("/ %.2f ^8(increased/reduced cooldown recovery)", icdrSkill),
							s_format("= %.2f ^8(effective trigger cooldown)", triggerCDAdjusted),
							"",
							s_format("%.2f ^8(adjusted for server tick rate)", actionCooldown),
							"",
							"Trigger rate:",
							s_format("1 / %.3f", actionCooldown),
							s_format("= %.2f ^8per second", TriggerRateCap),
						}
					end

					env.player.breakdown = newEnv.player.breakdown
				end
			end,
			mirageSkillNotFoundFunc = function(env, config)
				env.player.mainSkill.disableReason = "No Tawhoa's Chosen active skill found"
				env.player.mainSkill.skillFlags.disable = true
			end
		}
	elseif env.player.mainSkill.skillData.triggeredBySacredWisps then
		config = {
			calcMainSkillOffence = true,
			compareFunc = function(skill, env, config, mirageSkill)
				if not env.player.mainSkill.skillCfg.skillCond["usedByMirage"] and env.player.weaponData1.type == "Wand" then
					return env.player.mainSkill
				end
			end,
			preCalcFunc = function(env, newSkill, newEnv)
				local lessDamage =  newSkill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "SacredWispsLessDamage")
				local wispsMaxCount
				local wispsCastChance
				
				-- Find Wisps summoning skill for cast chance and wisp count
				for _, skill in ipairs(env.player.activeSkillList) do
					if skill.activeEffect.grantedEffect.name == "Summon Sacred Wisps" then
							wispsCastChance = skill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "SacredWispsChance")
							wispsMaxCount = skill.skillModList:Sum("BASE", env.player.mainSkill.skillCfg, "SacredWispsMaxCount")
						break
					end
				end

				env.player.mainSkill.mirage = { }
				env.player.mainSkill.mirage.name = newSkill.activeEffect.grantedEffect.name
				env.player.mainSkill.mirage.count = wispsMaxCount

				if not env.player.mainSkill.infoMessage then
					env.player.mainSkill.infoMessage = tostring(wispsMaxCount) .. " Sacred Wisps using " .. newSkill.activeEffect.grantedEffect.name
				end

				-- Add new modifiers to new skill (which already has all the old skill's modifiers)
				newSkill.skillModList:NewMod("Damage", "MORE", lessDamage, "Used by Sacred Wisps", env.player.mainSkill.ModFlags, env.player.mainSkill.KeywordFlags)
				newSkill.skillModList:NewMod("Speed", "MORE", wispsCastChance - 100, "Sacred Wisps cast chance", env.player.mainSkill.ModFlags, env.player.mainSkill.KeywordFlags)

				-- Does not use player resources
				newSkill.skillModList:NewMod("HasNoCost", "FLAG", true, "Used by Sacred Wisps")

				if newSkill.skillPartName then
					env.player.mainSkill.mirage.skillPart = newSkill.skillPart
					env.player.mainSkill.mirage.skillPartName = newSkill.skillPartName
					env.player.mainSkill.mirage.infoMessage2 = newSkill.activeEffect.grantedEffect.name
				else
					env.player.mainSkill.mirage.skillPartName = nil
				end
			end,
			postCalcFunc = function(env, newSkill, newEnv)
				env.player.mainSkill.mirage.output = newEnv.player.output
				env.player.mainSkill.skillFlags.wisp = true
				if newSkill.minion then
					env.player.mainSkill.mirage.minion = {}
					env.player.mainSkill.mirage.minion.output = newEnv.minion.output
				end

				if newEnv.player.breakdown then
					env.player.mainSkill.mirage.breakdown = newEnv.player.breakdown
					if newSkill.minion then
						env.player.mainSkill.mirage.minion.breakdown = newEnv.minion.breakdown
					end
				end
			end,
			mirageSkillNotFoundFunc = function(env, config)
				if not env.player.mainSkill.infoMessage2 then
					env.player.mainSkill.infoMessage2 = "No active skill for Sacred Wisps found"
				end
			end
		}
	elseif env.player.mainSkill.skillData.triggeredByGeneralsCry then
		env.player.mainSkill[SkillType.Triggered] = true
		local maxMirageWarriors = 0
		local cooldown = 1
		local generalsCryActiveSkill

		-- Find the active General's Cry gem to get active properties
		for _, skill in ipairs(env.player.activeSkillList) do
			if skill.activeEffect.grantedEffect.name == "General's Cry" and env.player.mainSkill.socketGroup.slot == env.player.mainSkill.socketGroup.slot then
				cooldown = calcSkillCooldown(skill.skillModList, skill.skillCfg, skill.skillData)
				generalsCryActiveSkill = skill
				break
			end
		end

		-- Scale dps with GC's cooldown
		env.player.mainSkill.skillData.dpsMultiplier = (env.player.mainSkill.skillData.dpsMultiplier or 1) * (1 / cooldown)

		-- Does not use player resources
		env.player.mainSkill.skillModList:NewMod("HasNoCost", "FLAG", true, "Used by mirage")

		-- Non-channelled skills only attack once, disregard attack rate
		if not env.player.mainSkill.skillTypes[SkillType.Channel] then
			env.player.mainSkill.skillData.timeOverride = 1
		end

		-- Supported Attacks Count as Exerted
		for _, value in ipairs(env.player.mainSkill.skillModList:Tabulate("INC", env.player.mainSkill.skillCfg, "ExertIncrease")) do
			local mod = value.mod
			env.player.mainSkill.skillModList:NewMod("Damage", mod.type, mod.value, mod.source, mod.flags, mod.keywordFlags)
		end
		for _, value in ipairs(env.player.mainSkill.skillModList:Tabulate("MORE", env.player.mainSkill.skillCfg, "ExertIncrease")) do
			local mod = value.mod
			env.player.mainSkill. skillModList:NewMod("Damage", mod.type, mod.value, mod.source, mod.flags, mod.keywordFlags)
		end
		for _, value in ipairs(env.player.mainSkill.skillModList:Tabulate("MORE", env.player.mainSkill.skillCfg, "ExertAttackIncrease")) do
			local mod = value.mod
			env.player.mainSkill.skillModList:NewMod("Damage", mod.type, mod.value, mod.source, mod.flags, mod.keywordFlags)
		end
		for _, value in ipairs(env.player.mainSkill.skillModList:Tabulate("MORE", env.player.mainSkill.skillCfg, "OverexertionExertAverageIncrease")) do
			local mod = value.mod
			env.player.mainSkill.skillModList:NewMod("Damage", mod.type, mod.value, mod.source, mod.flags, mod.keywordFlags)
		end
		for _, value in ipairs(env.player.mainSkill.skillModList:Tabulate("BASE", env.player.mainSkill.skillCfg, "ExertDoubleDamageChance")) do
			local mod = value.mod
			env.player.mainSkill.skillModList:NewMod("DoubleDamageChance", mod.type, mod.value, mod.source, mod.flags, mod.keywordFlags)
		end

		-- Scale dps with mirage quantity
		for _, value in ipairs(generalsCryActiveSkill.skillModList:Tabulate("BASE", generalsCryActiveSkill.skillCfg, "GeneralsCryDoubleMaxCount")) do
			local mod = value.mod
			env.player.mainSkill.skillModList:NewMod("QuantityMultiplier", mod.type, mod.value, mod.source, mod.flags, mod.keywordFlags)
			maxMirageWarriors = maxMirageWarriors + mod.value
		end
		env.player.mainSkill.infoMessage = tostring(maxMirageWarriors) .. " GC Mirage Warriors using " .. env.player.mainSkill.activeEffect.grantedEffect.name
	end

	return calculateMirage(env, config)
end