// HOW IT WORKS:

// PopExtPopulator.InitializeWave() is fired on wave init, this is the main function that parses the WaveSchedule table.

// InitializeWave does the following:
// - Spawns bot generators at the location provided in the "Where" keyvalue and sets the appropriate keyvalues
// - attaches a think function to the generators that handles most of the spawning logic
// - Adds icons to the wavebar
// - fills out the WaveArray

// Each element in WaveArray in another array that contains the WaveSpawn information
// Each element in this nested array is a table where the key is the bot_generator, and the value is the keyvalue table for the wavespawn.
// To access the bot_generator and the keyvalues from waves and individual wavespawns, use the PopExtPopulator.GetWavespawnInfo() function.

// Major syntax differences compared to standard popfiles:
// - Does not support unordered WaveSpawns, there is a fixed order of execution.  If you wanna rearrange your wavespawns you can't just change the names/waits around
// - CASE SENSITIVE! may change this in the future but it makes table look-ups easier for now.
// - Tags and Items are applied to bots in a single array value

// TODO:
// - Tank, Halloween NPC, and SentryGun spawners
// - RandomChoice and Squad spawners
// - reliable icon decrementing

const EFL_SPAWNER_PENDING = 1048576 // EFL_IS_BEING_LIFTED_BY_BARNACLE
const EFL_SPAWNER_ACTIVE = 1073741824 //EFL_NO_PHYSCANNON_INTERACTION
const EFL_SPAWNER_EXPENDED = 33554432 //EFL_DONTBLOCKLOS
const MAX_WAVESPAWNS_PER_WAVE = 128

POPEXT_CREATE_SCOPE( "__popext_populator", "PopExtPopulator", "PopulatorEnt" )

PopExtPopulator.WaveArray <- []

PopExtPopulator.PopulatorFunctions <- {

	function Mission() {
		printl( "mission spawner" )
	}
}

function PopExtPopulator::_OnDestroy() {

	if ( "WaveSchedule" in ROOT )
		delete ::WaveSchedule
}

function PopExtPopulator::DoEntityIO( type, WaveSchedule = ::WaveSchedule ) {

	if ( !( type in WaveSchedule[PopExtUtil.CurrentWaveNum] ) ) return

	local target =  WaveSchedule[PopExtUtil.CurrentWaveNum][type].Target

	local action = "", param = "", delay = -1, activator = null, caller = null

	if ( "Param" in WaveSchedule[PopExtUtil.CurrentWaveNum][type] ) param = WaveSchedule[PopExtUtil.CurrentWaveNum][type].Param
	if ( "Delay" in WaveSchedule[PopExtUtil.CurrentWaveNum][type] ) delay = WaveSchedule[PopExtUtil.CurrentWaveNum][type].Delay
	if ( "Activator" in WaveSchedule[PopExtUtil.CurrentWaveNum][type] ) activator = WaveSchedule[PopExtUtil.CurrentWaveNum][type].Activator
	if ( "Caller" in WaveSchedule[PopExtUtil.CurrentWaveNum][type] ) caller = WaveSchedule[PopExtUtil.CurrentWaveNum][type].Caller

	local entfirefunc = typeof target == "instance" ? EntFireByHandle : DoEntFire
	entfirefunc( target, action, param, delay, activator, caller )
}

function PopExtPopulator::InitializeWave( WaveSchedule = ::WaveSchedule ) {

	printl( "PopulatorEnt: " + PopulatorEnt )
	if ( !( "WaveSchedule" in getroottable() ) || !PopulatorEnt.IsValid() ) return

	PopExtWavebar.RemoveWaveIcon( "*", 0 )

	WaveArray = "CustomWaveOrder" in WaveSchedule ? WaveSchedule.CustomWaveOrder : array( WaveSchedule.len() )

	foreach( wave, wavespawns in WaveSchedule ) {

		// printl( "Wave: " + wave )

		if ( !( "CustomWaveOrder" in WaveSchedule ) && typeof wave == "integer" )
			WaveArray[wave] = array( wavespawns.len() )

		if ( wave == "MissionAttrs" ) {

			MissionAttrs( wavespawns )
			continue
		}

		//special support populators fire an additional function of the same name
		if ( wave in PopExtPopulator.PopulatorFunctions )
			PopExtPopulator.PopulatorFunctions.wave()

		foreach ( wavespawn, keyvalues in wavespawns ) {

			// printl( "\tWaveSpawn: " + wavespawn )

			foreach( k, v in keyvalues ) {
				// printl( "\t\t" + k + " : " + v )
				if ( k.tolower() == "tfbot" ) {

					// foreach( a, b in v ) printl( "\t\t\t" + a + " : " + b )
					local icon = ""
					local playerclass = ""

					"Class" in v ? playerclass = ( typeof v.Class != "integer" ? v.Class : PopExtUtil.Classes[v.Class] ) : playerclass = PopExtUtil.Classes[RandomInt( 1, 9 )] // don't use bot_generators "auto" here, grab a random player class string instead so we can use playerclass for icon fallback

					"ClassIcon" in v ? icon = v.ClassIcon : icon = playerclass

					PopExtWavebar.AddCustomIcon(
						icon,
						keyvalues.TotalCount,
						( "Attributes" in v && v.Attributes.find( ALWAYS_CRIT ) ) ? true : false,
						( "Attributes" in v && v.Attributes.find( MINIBOSS ) ) ? true : false,
						( "Support" in keyvalues ) ? true : false,
						( "Support" in keyvalues && ( keyvalues.Support > 1 || ( typeof keyvalues.Support == "string" && keyvalues.Support.tolower() == "limited" ) ) ) ? true : false
					)

					local generator = CreateByClassname( "bot_generator" )
					// foreach ( w in WaveArray[wave] ) printl( w )
					WaveArray[wave][wavespawn] = {}
					WaveArray[wave][wavespawn][generator] <- keyvalues

					PopExtUtil.SetTargetname( generator, "Name" in keyvalues ? keyvalues.Name : format( "__popext_generator%d", generator.entindex() ) )

					// generator.KeyValueFromInt( "spawnOnlyWhenTriggered", "WaitBetweenSpawnsAfterDeath" in keyvalues || ( "SpawnCount" in keyvalues && keyvalues.SpawnCount > 1 ) ? 1 : 0 ) //we need manual control always for the spawn interval
					generator.KeyValueFromInt( "spawnOnlyWhenTriggered", 1 )
					// generator.KeyValueFromFloat( "interval", "WaitBetweenSpawns" in keyvalues ? keyvalues.WaitBetweenSpawns.tofloat() : SINGLE_TICK )
					generator.KeyValueFromFloat( "interval", SINGLE_TICK ) //this keyvalue is weird, just control the interval in the think function manually
					generator.KeyValueFromInt( "useTeamSpawnPoint", 0 )
					generator.KeyValueFromInt( "maxActive", keyvalues.MaxActive.tointeger() )
					generator.KeyValueFromInt( "count", keyvalues.TotalCount.tointeger() )
					generator.KeyValueFromInt( "difficulty", "Skill" in keyvalues ? keyvalues.Skill.tointeger() : 1 )
					generator.KeyValueFromInt( "disableDodge", "disableDodge" in keyvalues ? keyvalues.disableDodge.tointeger() : 0 )
					generator.KeyValueFromString( "team", "TeamString" in keyvalues ? keyvalues.TeamString : "blue" )
					generator.KeyValueFromString( "class", playerclass )

					local org = keyvalues.Where

					if ( typeof org == "string" ) {

						//check targetname first
						local spawnpoints = []
						for ( local ent; ent = FindByName( ent, org ); ) spawnpoints.append( ent )

						if ( spawnpoints.len() ) {

							org = spawnpoints[RandomInt( 0, spawnpoints.len() - 1 )].GetOrigin()
							return
						}

						//no targetnames found, assume KVString
						local split = split( org, " " ).apply( @( val ) val.tofloat() )

						org = Vector( split[0], split[1], split[2] )
					}

					generator.SetAbsOrigin( org )

					AddOutput( generator, "OnExpended", "!self", "RunScriptCode", "self.AddEFlags( EFL_SPAWNER_EXPENDED )", -1, -1 )

					generator.AddEFlags( EFL_SPAWNER_PENDING )
					// generator.AddEFlags( EFL_SPAWNER_PENDING )

					DispatchSpawn( generator )

					generator.AcceptInput( "Disable", "", null, null )

					local genscope = PopExtUtil.GetEntScope( generator )

					if ( "WaveSpawn" in genscope && "WaitBetweenSpawns" in genscope.WaveSpawn )
						genscope.spawninterval <- genscope.WaveSpawn.WaitBetweenSpawns.tofloat()

					function GeneratorThink() {

						if ( self.IsEFlagSet( EFL_SPAWNER_ACTIVE ) ) {

							local scope = PopExtUtil.GetEntScope( self )

							if ( !( "spawninterval" in scope ) ) scope.spawninterval <- 0.0
							if ( !( "nextspawn" in scope ) ) scope.nextspawn <- 0.0

							if ( Time() < scope.nextspawn ) return

							//spawn 1 bot

							EntFireByHandle( self, "SpawnBot", "", -1, null, null )

							//spawncount is > 1, spawn the SpawnCount number of bots - 1 since we already did 1 above

							if ( "SpawnCount" in genscope.WaveSpawn && genscope.WaveSpawn.SpawnCount > 1 ) for ( local i = 0; i < genscope.WaveSpawn.SpawnCount - 1; i ++ ) EntFireByHandle( self, "SpawnBot", "", -1, null, null )

							scope.nextspawn <- Time() + scope.spawninterval

						}

						//we've finished spawning, look for other WaveSpawns that wait for this one and change their flag from PENDING to ACTIVE
						else if ( self.IsEFlagSet( EFL_SPAWNER_EXPENDED ) ) {

							for ( local g; g = FindByClassname( g, "bot_generator" ); ) {

								if (
									!g.IsEFlagSet( EFL_SPAWNER_ACTIVE )
									&& "WaveSpawn" in g.GetScriptScope()
									&& "WaitForAllSpawned" in g.GetScriptScope().WaveSpawn
									&& self.GetName() == g.GetScriptScope().WaveSpawn.WaitForAllSpawned
								) {

									self.AcceptInput( "Disable", "", null, null )
									g.AddEFlags( EFL_SPAWNER_ACTIVE )
									g.RemoveEFlags( EFL_SPAWNER_PENDING )
									g.AcceptInput( "Enable", "", null, null )

								}
							}
						}

						return -1
					}

					genscope.WaveSpawn <- keyvalues
					genscope.GeneratorThink <- GeneratorThink
					AddThinkToEnt( generator, "GeneratorThink" )
				}
			}
		}
	}
}

function PopExtPopulator::GetWavespawnInfo( wavenum = 0, wavespawn = 0 ) {

	// this function expects the actual wave number, not the array index ( wavenum 1 would get the array index 0 )
	wavespawn -= 1
	wavenum   -= 1

	//specific wave number passed, look at wavespawns at the wavenum index
	if ( wavenum > -1 ) {

		//valid wavespawn index passed, return only information about this wavespawn
		if ( wavespawn > -1 )	{

			return PopExtPopulator.WaveArray[wavenum][wavespawn]
		}
		//wave number passed, but no wavespawn, return every wavespawn in an array
		else {
			local allwavespawns = array( PopExtPopulator.WaveArray[wavenum].len() )

			foreach( i, wavespawn in allwavespawns ) wavespawn = PopExtPopulator.WaveArray[wavenum][i]

			return allwavespawns
		}
	}

	//no wave number passed, put every wave in an array and put every wavespawn in another array at each wave
	else {

		// Create an array with each element being another MAX_WAVESPAWNS_PER_WAVE-length array.
		// If your mission has >MAX_WAVESPAWNS_PER_WAVE wavespawns on a single wave may god help you

		local allwaves = array( GetPropInt( PopExtUtil.ObjectiveResource, "m_nMannVsMachineMaxWaveCount" ), array( MAX_WAVESPAWNS_PER_WAVE, 0 ) )

		foreach( i, wave in allwaves ) {

			//valid wavespawn index passed, just get this wavespawn index at every wave
			if ( wavespawn > -1 ) wave = PopExtPopulator.WaveArray[i]

			//no wavespawn index passed, get everything
			else {
				local allwavespawns = array( PopExtPopulator.WaveArray[wavenum].len(), [] )
				foreach( j, _ in PopExtPopulator.WaveArray[i] ) wave[i] = PopExtPopulator.WaveArray[i][j]
			}
		}
		return allwaves
	}
}

POP_EVENT_HOOK( "teamplay_round_start", "InitializeWave", function( params ) {

	PopExtPopulator.InitializeWave()

	PopExtPopulator.DoEntityIO( "InitWaveOutput" )

}, EVENT_WRAPPER_POPULATOR )

POP_EVENT_HOOK( "mvm_begin_wave", "StartWave", function( params ) {

	//grab the first spawner and enable it on wave start
	local firstspawner = PopExtPopulator.WaveArray[PopExtUtil.CurrentWaveNum][1].keys()[0]
	EntFireByHandle( firstspawner, "Enable", "", -1, null, null )

	PopExtPopulator.DoEntityIO( "StartWaveOutput" )

}, EVENT_WRAPPER_POPULATOR )

POP_EVENT_HOOK( "mvm_wave_complete", "DoneOutput", function( params ) {

	PopExtPopulator.DoEntityIO( "DoneOutput" )

}, EVENT_WRAPPER_POPULATOR )

POP_EVENT_HOOK( "player_death", "RemoveIcon", function( params ) {

	local player = GetPlayerFromUserID( params.userid )

	if ( !player.IsBotOfType( TF_BOT_TYPE ) ) return

	local scope = PopExtUtil.GetEntScope( player )
	PopExtUtil.PrintTable( scope )
	local spawner = FindByName( null, scope.spawner )

	PopExtWavebar.DecrementWaveIcon( spawner.GetScriptScope().WaveSpawn.TFBot.ClassIcon, 0, 1 )
}, EVENT_WRAPPER_POPULATOR )

POP_EVENT_HOOK( "player_death", "WaitBetweenSpawnsAfterDeath", function( params ) {

	local player = GetPlayerFromUserID( params.userid )

	if ( !player.IsBotOfType( TF_BOT_TYPE ) ) return

	local spawner = FindByName( null, player.GetScriptScope().spawner )

	if ( "WaitBetweenSpawnsAfterDeath" in spawner.GetScriptScope().WaveSpawn ) {

		EntFireByHandle( spawner, "Enable", "", -1, null, null )
		EntFireByHandle( spawner, "SpawnBot", "", -1, null, null )
	}
}, EVENT_WRAPPER_POPULATOR )

POP_EVENT_HOOK( "player_death", "DoneOutput", function( params ) {

	local player = GetPlayerFromUserID( params.userid )

	if ( !player.IsBotOfType( TF_BOT_TYPE ) ) return

	PopExtPopulator.DoEntityIO( "DoneOutput" )

}, EVENT_WRAPPER_POPULATOR)

POP_EVENT_HOOK( "player_spawn", "WaitBetweenSpawnsAfterDeath", function( params ) {

	local player = GetPlayerFromUserID( params.userid )

	if ( !player.IsBotOfType( TF_BOT_TYPE ) ) return

	//disable our spawner if we have WaitBetweenSpawnsAfterDeath
	PopExtUtil.RunWithDelay( SINGLE_TICK, function() {

		if ( "WaitBetweenSpawnsAfterDeath" in spawner.GetScriptScope().WaveSpawn )
			EntFireByHandle(spawner, "Disable", "", -1, null, null )

	}, player.GetScriptScope() )

}, EVENT_WRAPPER_POPULATOR )

POP_EVENT_HOOK( "player_spawn", "SetSpawner", function( params ) {

		local player = GetPlayerFromUserID( params.userid )

		if ( !player.IsBotOfType( TF_BOT_TYPE ) ) return

		local scope = player.GetScriptScope()

		local spawner = FindByClassnameNearest( "bot_generator", player.GetOrigin(), 256 )

		scope.spawner <- spawner.GetName()
		scope.bot_attributes <- BECOME_SPECTATOR_ON_DEATH

		local additional_attributes = 0

		//MVM hack: these keyvalues get removed by mvm logic from bot_generator spawned bots
		local spawner_keyvalues = {
			m_bDisableDodge = DISABLE_DODGE,
			m_bSuppressFire = SUPPRESS_FIRE,
			m_bRetainBuildings = RETAIN_BUILDINGS,
			m_iOnDeathAction = REMOVE_ON_DEATH //1 = remove
		}

		foreach ( key, value in spawner_keyvalues )
			if ( NetProps.GetPropInt( spawner, key ) ) {

				if ( key == "m_iOnDeathAction" && NetProps.GetPropInt( spawner, key ) == 1 )
					scope.bot_attributes = scope.bot_attributes |~ BECOME_SPECTATOR_ON_DEATH

				additional_attributes = additional_attributes | value
			}


		scope.bot_attributes = scope.bot_attributes | additional_attributes

		PopExtUtil.ScriptEntFireSafe( player, "self.AddBotAttribute( self.GetScriptScope().bot_attributes )" )
})

// ::WaveSchedule <- {

// 	// CustomWaveOrder = [1,5,2,6,3]

// 	MissionAttrs = {
// 		WaveStartCountdown = 1
// 	}

// 	Mission = {
// 		CooldownTime = 10
// 		WaitBetweenSpawns = 5
// 		TFBot = {

// 		}
// 	},

// 	[1] = { //wave number

// 		AutoRelay = true, //set to true to automatically call wave_start_relay and wave_finished_relay without needing to define them

// 		InitWaveOutput = {
// 			Target = "BigNet"
// 			Action = "RunScriptCode"
// 			Param = "Info( `Wave 1 loaded` )"
// 		},
// 		// StartWaveOutput = { //not necessary with AutoRelay
// 		// 	Target = "wave_start_relay"
// 		// 	Action = "Trigger"
// 		// },
// 		// DoneOutput = {
// 		// 	Target = "wave_finished_relay"
// 		// 	Action = "Trigger"
// 		// },
// 		[1] = { //wavespawn
// 			Name = "wave1a"
// 			Where = "-198.163635 4760.567871 291.313049" //accepts KVString
// 			TotalCount = 15
// 			MaxActive = 5
// 			WaitBetweenSpawns = 3
// 			Team = 3
// 			TFBot = {
// 				Class = TF_CLASS_SCOUT
// 				Health = 150
// 				ClassIcon = "scout_bat"
// 				Name = "Scout"
// 				Items = ["The Shortstop", "Bonk! Atomic Punch"]
// 				Tags = ["popext_addcond|11"]
// 			}
// 		},
// 		[2] = { //wavespawn

// 			Name = "wave1b"
// 			Where = Vector( -1071.587646, 4216.348633, 200.016907 ) //accepts vector
// 			TotalCount = 10
// 			MaxActive = 10
// 			SpawnCount = 2
// 			Team = 3
// 			WaitForAllSpawned = "wave1a"
// 			ActionPoint = "action_point_test"
// 			RetainBuildings = true

// 			TFBot = {
// 				Class = TF_CLASS_SCOUT
// 				Health = 150
// 				Name = "Scout"
// 				Items = ["The Shortstop", "Bonk! Atomic Punch"]
// 				Tags = ["popext_usehumananimations", "popext_addcond|11"]
// 			}
// 		},
// 		[3] = { //wavespawn

// 			Name = "wave1c"
// 			Where = "spawnbot" //accepts targetname
// 			TotalCount = 100
// 			MaxActive = 10
// 			SpawnCount = 5
// 			Team = 3
// 			WaitForAllDead = "wave1b"
// 			RetainBuildings = true

// 			TFBot = {
// 				Class = "Engineer"
// 				Health = 275
// 				Name = "Scout"
// 				Items = ["The Shortstop", "Bonk! Atomic Punch"]
// 				Tags = ["popext_usehumananimations", "popext_addcond|11"]
// 			}
// 		}
// 	}
// }
// PopExtPopulator.InitializeWave()
