-- powered by schizophrenia

local TIMECONSTRAINT_WAVE = 7
local realcontraint

local classIndices_Internal = {
	[1] = "Scout",
	[3] = "Soldier",
	[7] = "Pyro",
	[4] = "Demoman",
	[6] = "Heavyweapons",
	[9] = "Engineer",
	[5] = "Medic",
	[2] = "Sniper",
	[8] = "Spy",
}

local DEATH_INSULTS = {
	Scout = {
		"Jumping won't save you, %s.",
		"Try not running straight to the robots, %s.",
		"Try upgrading your primary weapon, %s.",
	},
	Soldier = {
		"Try not missing your rockets, %s.",
		"Can't rocket jump out of that one, %s?",
	},
	Pyro = {
		--"Hey %s! Did you know that the airblast force upgrade makes you take 50%% more damage?",
		--"Hey %s! Did you know that equipping the scorch shot makes you take 150%% more damage?",
		"%s, have you considered not walking straight towards me?",
		"Burn in your own failure, %s.",
	},
	Demoman = {
		"Try using your movement keys next time, %s.",
		"Are you actually drunk, %s? You're playing like you are.",
		"Your incompetency is truly explosive, %s.",
	},
	Heavyweapons = {
		"Consider playing a more interesting class, %s.",
		"I hope that got you to leave a negative review on the end-of-operation survey, %s.",
		"Keep standing directly in front of me, %s, see how well that goes next time.",
	},
	Engineer = {
		"Build yourself better gamesense, %s.",
		"Sentry blocking going well, aye %s?",
		-- "Unequip the wrangler, %s.",
	},
	Medic = {
		"Nice healing, %s.",
		"Try idling more %s, maybe that will work.",
		"Too bad you can't reanimate yourself, %s.",
	},
	Sniper = {
		"Stop aiming for my body and start aiming to be better at the game, %s.",
		"You're supposed to shoot the head, %s.",
		"Thanks for standing still, %s!",
	},
	Spy = {
		"I hate the french.",
		"Try running in circles more, %s.",
		"Couldn't dead ring that one, %s? Unfortunate.",
	},
}

-- flags
-- 1: main wave
-- 2: support
-- 4: mission support
-- 8: giant
-- 16: crit

local WAVE_ICONS = {
    [1] = {
        [1] = {
            name = "timer_lite",
            flag = 8,
            count = 1,
        },
    },
    [2] = {
        [1] = {
            name = "timer_lite",
            flag = 8,
            count = 1,
        },
        [2] = {
            name = "heavy_chief",
            flag = 8,
            count = 1,
        },
        [3] = {
            name = "soldier_sergeant_crits",
            flag = 24,
            count = 1,
        },
    },
}

local wavebarLogic

local randomIcons = {
	"scout",
	"soldier",
	"heavy",
	"demo",
	"pyro",
	"soldier_spammer",
	"spy",
	"pyro_dragon_fury",
	"soldier_blackbox",
	"demoknight_charge",
	"soldier_bison",
	"demo_bow",
	"medic_giant",
	"timer_lite",
	"helicopter_blue_nys",
	"soldier_barrage",
	"heavy_chief",
	"demoknight_giant",
	"soldier_sergeant_crits",
	"demo_bomber",
	"heavy_shotgun",
}

local iconFlags = {
	soldier_sergeant_crits = 16,
}

local inWave = false
local curWave = nil

local timeconstraint_alive = false
local cur_constraint = false -- current time constraint bot

local pvpActive = false
local gamestateEnded = false
local specialLinePlaying = false

local function chatMessage(message)
	local outputMessage = "{blue}" .. "Time-Constraint Prime" .. "{reset} : " .. message

	local allPlayers = ents.GetAllPlayers()

	for _, player in pairs(allPlayers) do
		player:AcceptInput("$DisplayTextChat", outputMessage)
	end
end

local function hasTag(tags, tagToFind)
	for _, tag in pairs(tags) do
		if tag == tagToFind then
			return true
		end
	end
end

local function removeCallbacks(bot, callbacks)
	if not IsValid(bot) then
		return
	end

	for _, callbackId in pairs(callbacks) do
		bot:RemoveCallback(callbackId)
	end
end
local function removeTimers(timers)
	for _, timerId in pairs(timers) do
		print(timerId)
		timer.Stop(timerId)
	end
end

local function resetWavebar()
	if wavebarLogic then
		timer.Stop(wavebarLogic)
		wavebarLogic = nil
	end

	-- local objResource = ents.FindByClass("tf_objective_resource")

	-- objResource.m_nMannVsMachineWaveEnemyCount = 1

	-- objResource.m_nMannVsMachineWaveClassFlags[1] = 9
	-- objResource.m_iszMannVsMachineWaveClassNames[1] = "timer_lite"

	-- for i = 1, #objResource.m_nMannVsMachineWaveClassCounts do
	--     if i > 1 then
	--         objResource.m_nMannVsMachineWaveClassCounts[i] = 0
	--     end
	-- end
end

local function setWaveBar(subwave)
	if wavebarLogic then
		timer.Stop(wavebarLogic)
		wavebarLogic = nil
	end

	local objResource = ents.FindByClass("tf_objective_resource")

	local subwaveIcons = WAVE_ICONS[subwave]

	local totalCount = 0
	for _, iconData in pairs(subwaveIcons) do
		totalCount = totalCount + iconData.count
	end

	objResource.m_nMannVsMachineWaveEnemyCount = totalCount

	objResource.m_nMannVsMachineWaveClassFlags[1] = 9
	objResource.m_iszMannVsMachineWaveClassNames[1] = "timer_lite"

	for i = 1, #objResource.m_iszMannVsMachineWaveClassNames do
	    if subwaveIcons[i] then
	        objResource.m_iszMannVsMachineWaveClassNames[i] = subwaveIcons[i].name
	    end
	end
	for i = 1, #objResource.m_nMannVsMachineWaveClassFlags do
	    if subwaveIcons[i] then
	        objResource.m_nMannVsMachineWaveClassFlags[i] = subwaveIcons[i].flag
	    end
	end
	for i = 1, #objResource.m_nMannVsMachineWaveClassCounts do
	    if subwaveIcons[i] then
	        objResource.m_nMannVsMachineWaveClassCounts[i] = subwaveIcons[i].count
		else
	        objResource.m_nMannVsMachineWaveClassCounts[i] = 0
	    end
	end
end

local playersCallback = {}
local timers = {}

function _TimeConstraintOnWaveInit(wave)
	inWave = false
	curWave = wave

	gamestateEnded = false
	timeconstraint_alive = false
	pvpActive = false
    specialLinePlaying = false

	removeTimers(timers)
	for player, plrCallbacks in pairs(playersCallback) do
		removeCallbacks(player, plrCallbacks)
	end

	resetWavebar()

	if wave ~= TIMECONSTRAINT_WAVE then
		return
	end

	-- resetWavebar()

	local objResource = ents.FindByClass("tf_objective_resource")

	timer.Simple(1, function()
		if inWave then
			return
		end

		if curWave ~= TIMECONSTRAINT_WAVE then
			return
		end

		chatMessage("Back so soon?")

		wavebarLogic = timer.Create(0.5, function()
			for i = 1, #objResource.m_iszMannVsMachineWaveClassNames do
				objResource.m_iszMannVsMachineWaveClassNames[i] = randomIcons[math.random(#randomIcons)]
			end
			for i = 1, #objResource.m_nMannVsMachineWaveClassFlags do
				local random = math.random(1, 4)

				local flag = 1
				if random == 2 then
					flag = 9
				end

				local iconAtIndex = objResource.m_iszMannVsMachineWaveClassNames[i]

				if iconFlags[iconAtIndex] then
					flag = flag + iconFlags[iconAtIndex]
				end

				objResource.m_nMannVsMachineWaveClassFlags[i] = flag
			end

			local totalCount = 0

			for i = 1, #objResource.m_nMannVsMachineWaveClassCounts do
				local count = math.random(0, 999)
				objResource.m_nMannVsMachineWaveClassCounts[i] = count

				totalCount = totalCount + count
			end

			objResource.m_nMannVsMachineWaveEnemyCount = math.floor(totalCount * (math.random(1, 50) / 10))
		end, 0)
	end)
end

function OnWaveStart(wave)
	inWave = true
	resetWavebar()

	if wave ~= TIMECONSTRAINT_WAVE then
		return
	end

	setWaveBar(1)

	-- resetWavebar()
end

local rollbacks = {}

local CLASSES = { "player", "obj_*" }

local function addEntToRollback(ent, class)
	if not ent:IsCombatCharacter() then
		return
	end

	if ent.m_iTeamNum ~= 2 then
		return
	end

	rollbacks[ent] = {
		Origin = ent:GetAbsOrigin(), --+ Vector(0, 0, 0),
		Angles = ent:GetAbsAngles(),
		IsPlayer = class == "player" and true,
	}
end

local function storeRollback()
	rollbacks = {}

	for _, class in pairs(CLASSES) do
		for _, ent in pairs(ents.FindAllByClass(class)) do
			addEntToRollback(ent, class)
		end
	end
end

local function fade()
	local fadeEnt = ents.CreateWithKeys("env_fade", {
		holdtime = 1,
		duration = 0.5,
		rendercolor = "255, 255, 255",
		renderamt = 255,
	}, true, true)

	fadeEnt.Fade(fadeEnt)

	timer.Simple(1, function()
		fadeEnt:Remove()
	end)
end

local function revertRollback()
	fade()

	timer.Simple(0.6, function()
		for _, door in pairs(ents.FindAllByClass("func_door")) do
			door:Remove()
		end

		for _, player in pairs(ents.GetAllPlayers()) do
			if player:IsRealPlayer() then
				player:ForceRespawn()
			elseif player ~= realcontraint then
				player:Suicide()
			end
		end

		for ent, data in pairs(rollbacks) do
			if IsValid(ent) and ent:IsAlive() then
				-- ent:SetAbsOrigin(data.Origin)
				-- ent:SetAbsAngles(data.Angles)

				if data.IsPlayer then
					ent:SetAbsOrigin(data.Origin)
					ent:SnapEyeAngles(data.Angles)

					-- local pitch = ent["m_angEyeAngles[0]"]
					-- local yaw = ent["m_angEyeAngles[1]"]
					-- ent:SnapEyeAngles(Vector(-pitch, yaw, 0))
				else
					ent:Teleport(data.Origin, data.Angles)
				end
			end
		end
	end)
end

-- reanimators are disabled during pvp
-- when outside of pvp (during timeconstraint wave), reanimator flies away after 5 seconds
ents.AddCreateCallback("entity_revive_marker", function(entity)
	if not timeconstraint_alive then
		return
	end

	if not pvpActive then
		timer.Simple(1, function()
			for i = 1, 100 do
				timer.Simple(0.03 * i, function()
					if not IsValid(entity) then
						return
					end

					entity:SetAbsOrigin(entity:GetAbsOrigin() + Vector(0, 0, 5))

					if i == 100 then
						entity:Remove()
					end
				end)
			end
		end)

		return
	end

	timer.Simple(0, function()
		entity:Remove()
	end)
end)

local function handlePlayerDeath(player)
	playersCallback[player] = {}

	playersCallback[player].died = player:AddCallback(ON_DEATH, function()
		if pvpActive then
			return
		end

		if specialLinePlaying then
			return
		end

		if player.m_iTeamNum ~= 2 then
			return
		end

		local allInsults = DEATH_INSULTS[classIndices_Internal[player.m_iClass]]
		local chosenInsult = allInsults[math.random(#allInsults)]

		local name = player:GetPlayerName()

		chatMessage(string.format(chosenInsult, name))
	end)
end

local function Holder(bot)
	timeconstraint_alive = true
	realcontraint = bot

	local allPlayers = ents.GetAllPlayers()

	for _, player in pairs(allPlayers) do
		if player:IsRealPlayer() then
			handlePlayerDeath(player)
		end
	end

	local callbacks = {}

	storeRollback()

	local milkers = {}

	timers.milkCheck = timer.Create(0.1, function()
		if not timeconstraint_alive then
			removeCallbacks(bot, callbacks)
			removeTimers(timers)
		end

		if not cur_constraint or not cur_constraint:IsAlive() then
			return
		end

		local milker = cur_constraint:GetConditionProvider(TF_COND_MAD_MILK)

		if not milker then
			return
		end

		local secondary = milker:GetPlayerItemBySlot(LOADOUT_POSITION_SECONDARY)

		-- prevents mistaking mad milk syringes from mad milk
		if secondary.m_iClassname ~= "tf_weapon_jar_milk" then
			return
		end

		local handle = milker:GetHandleIndex()

		if milkers[handle] then
			return
		end

		milkers[handle] = true

		timer.Simple(1.5, function()
			-- chatMessage("Here, have a better weapon. You're welcome")
			milker:GiveItem("The Winger")
			milker:WeaponSwitchSlot(LOADOUT_POSITION_SECONDARY)
		end)
	end, 0)

	callbacks.died = bot:AddCallback(ON_DEATH, function()
		timeconstraint_alive = false

		removeCallbacks(bot, callbacks)
		removeTimers(timers)

		for player, plrCallbacks in pairs(playersCallback) do
			removeCallbacks(player, plrCallbacks)
			-- removeTimers(timers)
		end
	end)
	callbacks.spawned = bot:AddCallback(ON_SPAWN, function()
		removeCallbacks(bot, callbacks)
		removeTimers(timers)

		for player, plrCallbacks in pairs(playersCallback) do
			removeCallbacks(player, plrCallbacks)
			-- removeTimers(timers)
		end
	end)
end

local function Handle1(bot)
	local callbacks = {}

	storeRollback()

	-- specialLinePlaying = true

	-- timer.Simple(1, function()
	-- 	chatMessage("I've been thinking...")
	-- end)

	-- timer.Simple(4, function()
	-- 	chatMessage("If I simply do not leave spawn, I am invincible.")
	-- end)

	-- timer.Simple(8, function()
	-- 	specialLinePlaying = false
	-- 	chatMessage("You cannot defeat me. Give up while you can.")
	-- end)

	callbacks.died = bot:AddCallback(ON_DEATH, function()
		specialLinePlaying = true

		timer.Simple(0.5, function()
			chatMessage("This has never happened and will not occur again.")
		end)

		timer.Simple(1.7, function()
			chatMessage("Let's do that again.")
		end)

		timer.Simple(3.5, function()
			setWaveBar(1)
			specialLinePlaying = false
			revertRollback()
		end)

		removeCallbacks(bot, callbacks)
	end)
	callbacks.spawned = bot:AddCallback(ON_SPAWN, function()
		removeCallbacks(bot, callbacks)
	end)
end

local function Handle2(bot)
	local callbacks = {}

	storeRollback()

	specialLinePlaying = true

	timer.Simple(1, function()
		chatMessage("That Engineer bot had a couple of his drones rotting on the back.")
	end)

	timer.Simple(5, function()
		specialLinePlaying = false
		chatMessage("What a waste. I have granted them a better purpose.")
	end)

	-- timer.Simple(10, function()
	-- 	chatMessage("Remember this? Say hello to it again.")
	-- end)

	callbacks.died = bot:AddCallback(ON_DEATH, function()
		specialLinePlaying = true

		timer.Simple(0.5, function()
			chatMessage("I knew learning from bosses that just died was not the right choice.")
		end)

		timer.Simple(1.7, function()
			chatMessage("Let's do that again.")
		end)

		timer.Simple(3.5, function()
			setWaveBar(1)
			specialLinePlaying = false
			revertRollback()
		end)

		removeCallbacks(bot, callbacks)
	end)
	callbacks.spawned = bot:AddCallback(ON_SPAWN, function()
		removeCallbacks(bot, callbacks)
	end)
end

local function HandleFinal(bot)
	local callbacks = {}

	storeRollback()

	timer.Simple(2, function()
		chatMessage("What I couldn't do myself,  I could with five of me.")
	end)

	callbacks.died = bot:AddCallback(ON_DEATH, function()
		specialLinePlaying = true

		timer.Simple(0.5, function()
			chatMessage("This is getting boring. I'm leaving.")
		end)

		timer.Simple(3.5, function()
			realcontraint:Suicide()
		end)

		removeCallbacks(bot, callbacks)
	end)
	callbacks.spawned = bot:AddCallback(ON_SPAWN, function()
		removeCallbacks(bot, callbacks)
	end)
end

local function checkBot(bot, tags)
	if hasTag(tags, "realcontraint") then
		Holder(bot)
		return
	end
	if hasTag(tags, "timeconstraint1") then
		Handle1(bot)
		return true
	end
	if hasTag(tags, "timeconstraint2") then
		Handle2(bot)
		return true
	end
	-- if hasTag(tags, "timeconstraint3") then
	-- 	Handle3(bot)
	-- 	return true
	-- end
	if hasTag(tags, "timeconstraintFinal") then
		HandleFinal(bot)
		return true
	end
end

function _OnWaveSpawnBot_TimeConstraint(bot, _, tags)
	local result = checkBot(bot, tags)

	if result then
		cur_constraint = bot
	end
end