require("game_object_wrapper")
require("timed_actions")
require("utils")

-- wave end conditions:
-- - all enemies die (spawn points with limits)
-- - time runs out (spawn points deactivate, enemies stay)

-- wave limiters:
-- - time limit
-- - enemy spawn limit (end wave when certain number of enemies are spawned)
-- - spawn point count limit (activates only some of the spawn points)

-- wave activators:
-- - simple activate of spawn points
-- - chained activate with a delay, each group at a time

DEBUG_PROCESS_ACTOR = false
DEBUG_WAVE = false

-- default regex patterns for matching wave objects
local PATTERN_SPAWN_POINT = "WAVE_([%d]+)_GROUP_([%d]+)_SPAWN_([%d]+)"
local PATTERN_TRIGGER = "WAVE_([%d]+)_TRIGGER"
local PATTERN_ZONE = "WAVE_([%d]+)_ZONE_([%d]+)"

WaveManager = {}
WaveManager.waveCount = 0
WaveManager.spawnPointPattern = PATTERN_SPAWN_POINT
WaveManager.triggerPattern = PATTERN_TRIGGER
WaveManager.zonePattern = PATTERN_ZONE
WaveManager.waves = {}


-- compares two ordered actors
function compareActorOrder(actor1, actor2)
	if not (actor1 == nil) and not (actor2 == nil) and actor1.order and actor2.order then
		return  actor1.order < actor2.order
	end
	return false
end

Wave = {
	isActive = false,
	spawnActivationDelay = 0, -- delay between activating consecutive spawn points within the same group
	firstSpawnActivationDelay = 0, -- delay to activating first spawn point in a group
	spawnPointLimit = 0, -- limit number of active spawn points ger group
	enemySpawnLimit = 0, -- limit total number of spawned enemies (TODO)
	durationLimit = 0, -- limit duration of wave
	isEndOnInfect = false, -- indicates whether wave end after all zones are infected
	isTriggerVisible = true, -- if true, the sensor that triggerst the wave will be visible until triggered
	isTriggerNotificationSoundEnabled = true, -- if true, will play notification sound when hud indicator for a trigger pops up
	isZoneNotificationSoundEnabled = true, -- if true, will play notification sound when hud indicator for a zone pops up

}

function Wave.onGroupDisabled(objectIds, command)
	local activeCount = 0
	for _,id in pairs(objectIds) do
		if Object.isEnabled(id) then activeCount = activeCount + 1 end
	end
	
	local callback = function(obejctId, isEnabled)
		if isEnabled then 
			activeCount = activeCount + 1
		else
			activeCount = activeCount - 1
		end
		
		if activeCount == 0 then
			command()
		end
	end
	
	for i,id in pairs(objectIds) do
		Object.onEnable(id, callback)
	end
end

function Wave:new(waveNo)
	log("Create new wave", DEBUG_WAVE)
	local wave = { spawnPoints = {}, zones = {}, id = waveNo }
	setmetatable(wave, self)
	self.__index = self
	return wave
end

function Wave:onStart()
	log("Wave start", DEBUG_WAVE)
end

function Wave:onEnd()
	log("Wave end", DEBUG_WAVE)
end

function Wave:setup()
	log("Wave " .. self.id .. " setup", DEBUG_WAVE)
	if self.trigger then
		if not self.isTriggerVisible then
			self.trigger:setVisible(false)
		end
		self.trigger:onEnable(function(id, isEnabled)
			if not self.isTriggerVisible then return end -- don't track invisible triggers
			local isIndicatorShown = Director.setHudIndicator(self.trigger.id, isEnabled)
			if self.isTriggerNotificationSoundEnabled and isIndicatorShown then
				Audio.playSound("GAME_HUD_INDICATOR")
			end
		end)
		self.trigger:onEnterArea(function(areaId, objectId) self:start() end)
	end

	local spawnIds = {}
	for _,group in ipairs(self.spawnPoints) do
		for _,spawn in ipairs(group) do
			spawn:showOnEnable():hideOnDisable()
			table.insert(spawnIds, spawn.id)
		end
	end

	local zoneIds = {}
	for _,zone in ipairs(self.zones) do
		zone:showOnEnable():hideOnDisable()
		zone:onEnable(function(id, isEnabled)
			local isIndicatorShown = Director.setHudIndicator(zone.id, isEnabled)
			if self.isZoneNotificationSoundEnabled and isIndicatorShown then
				Audio.playSound("GAME_HUD_INDICATOR")
			end
		end)
		table.insert(zoneIds, zone.id)
	end

	if self.isEndOnInfect then
		-- finish when all zones are infected
		Wave.onGroupDisabled(zoneIds, 
			function()
				self:finish()
			end)
	elseif self.durationLimit == 0 then 
		-- finish when all enemies die
		local activeEnemyCount = 0
		local activeSpawnCount = 0
	
		local endCallbackSentry = function()
			log("endCallback: spawn points: " .. activeSpawnCount .. ", enemies: " .. activeEnemyCount, DEBUG_WAVE)
			if activeSpawnCount == 0 and activeEnemyCount == 0 then
				self:finish()
			end
		end
	
		local destroyListener = function(objectId)
			log("Object " .. objectId .. " destroyed, " .. (activeEnemyCount - 1) .. " left", DEBUG_WAVE)
			activeEnemyCount = activeEnemyCount - 1
			endCallbackSentry()
		end
	
		local spawnListener = function(spawnerId, spawnedId)
			log("Object " .. spawnedId .. " spawned, " .. (activeEnemyCount + 1) .. " in play", DEBUG_WAVE)
			activeEnemyCount = activeEnemyCount + 1
			Object.onDestroy(spawnedId, destroyListener)
		end
	
		for i,id in pairs(spawnIds) do
			log("registering spawn listener for " .. id, DEBUG_WAVE)
			activeSpawnCount = activeSpawnCount + 1
			Object.onSpawn(id, spawnListener)
		end
	
		Wave.onGroupDisabled(spawnIds,
			function() 
				log("Spawn points disabled", DEBUG_WAVE)
				activeSpawnCount = 0
				endCallbackSentry()
			end)
	end
end

function Wave:finish()
	if not self.isActive then
		log("Wave " .. self.id .. " cannot be finished, as it's not active", DEBUG_WAVE)
		return
	end
	self.isActive = false
	log("Wave " .. self.id .. " finish", DEBUG_WAVE)
	for _,group in ipairs(self.spawnPoints) do
		for _,spawn in ipairs(group) do
			spawn:setEnabled(false)
		end
	end
	for _,zone in pairs(self.zones) do
		zone:setEnabled(false)
	end
	self:onEnd()
end

function Wave:start()
	if self.isActive then
		log("Wave " .. self.id .. " cannot be started, as it's already active", DEBUG_WAVE)
		return
	end
	log("Wave " .. self.id .. " start", DEBUG_WAVE)
	self:reset()
	self.isActive = true
	for _,group in ipairs(self.spawnPoints) do
		local delay = self.firstSpawnActivationDelay
		local count = 0
		for _,spawn in ipairs(group) do
			if self.spawnPointLimit > 0 and count >= self.spawnPointLimit then
				break
			end
			TimedActionManager.delayAction(delay, function() spawn:setEnabled(true) end)
			delay = delay + self.spawnActivationDelay
			count = count + 1
		end
	end
	if self.trigger then
		TimedActionManager.delayAction(0, function() self.trigger:setEnabled(false) end)
	end

	if self.durationLimit > 0 then
		TimedActionManager.delayAction(self.durationLimit, function() self:finish() end)
	end
	
	for _,zone in pairs(self.zones) do
		zone:setEnabled(true)
		if (not self.isEndOnInfect) and zone.timeLimit then
			TimedActionManager.delayAction(zone.timeLimit, function() zone:setEnabled(false) end)
		end
	end
	self:onStart()
end

function Wave:reset()
	log("Wave " .. self.id .. " reset", DEBUG_WAVE)
	for _,group in ipairs(self.spawnPoints) do
		for _,spawn in ipairs(group) do
			spawn:reset()
		end
	end
	for _,zone in ipairs(self.zones) do
		zone:reset()
	end
end

function WaveManager:processTrigger(actor)
	local waveNo = nil
	
	waveNo = string.match(actor.name, self.triggerPattern)
	waveNo = tonumber(waveNo)
	if waveNo then
		log("Trigger for wave " .. waveNo, DEBUG_PROCESS_ACTOR)
		if not self.waves[waveNo] then
			self.waves[waveNo] = Wave:new(waveNo)
			self.waveCount = self.waveCount + 1;
		end
		local wave = self.waves[waveNo]
		wave.trigger = actor
	else
		log("Sensor " .. (actor.name and actor.name or "nil") .. " [" .. actor.id .. "] not part of any wave" , DEBUG_PROCESS_ACTOR)
	end
end

function WaveManager:processSpawnPoint(actor)
	local waveNo = nil
	local groupNo = nil
	local order = nil
	waveNo, groupNo, order = string.match(actor.name, self.spawnPointPattern)

	if waveNo and groupNo and order then
		waveNo = tonumber(waveNo)
		groupNo = tonumber(groupNo)
		order = tonumber(order)

		log("Spawn point wave/group/order " .. waveNo .. "/" .. groupNo .. "/" .. order, DEBUG_PROCESS_ACTOR)
		actor.order = order

		if not self.waves[waveNo] then
			self.waves[waveNo] = Wave:new(waveNo)
			self.waveCount = self.waveCount + 1;
		end
		local wave = self.waves[waveNo]

		if not wave.spawnPoints[groupNo] then
			log("Create new spawn points group", DEBUG_PROCESS_ACTOR)
			wave.spawnPoints[groupNo] = {}
		end
		local group = wave.spawnPoints[groupNo]
		table.insert(group, actor)
		table.sort(group, compareActorOrder)

		log("spawn points in group " .. groupNo, DEBUG_PROCESS_ACTOR)
		for i,spawnPoint in ipairs(group) do
			log(spawnPoint.name .. " [" .. spawnPoint.id .. "]", DEBUG_PROCESS_ACTOR)
		end
	else
		log("Spawn point " .. (actor.name and actor.name or "nil") .. " [" .. actor.id .. "] not part of any wave", DEBUG_PROCESS_ACTOR)
	end
end

function WaveManager:processZone(actor)
	local waveNo = nil
	local order = nil
	waveNo, order = string.match(actor.name, self.zonePattern)
	if waveNo and order then
		log("Zone wave/order " .. waveNo .. "/" .. order, DEBUG_PROCESS_ACTOR)
		waveNo = tonumber(waveNo)
		order = tonumber(order)

		actor.order = order
		if not self.waves[waveNo] then
			self.waves[waveNo] = Wave:new(waveNo)
			self.waveCount = self.waveCount + 1;
		end
		local wave = self.waves[waveNo]
		
		local zones = wave.zones
		table.insert(zones, actor)
		table.sort(zones, compareActorOrder)

		log("zones in wave " .. waveNo, DEBUG_PROCESS_ACTOR)
		for i,zone in ipairs(zones) do
			log(zone.name .. " [" .. zone.id .. "]", DEBUG_PROCESS_ACTOR)
		end
	else
		log("Zone " .. (actor.name and actor.name or "nil") .. " [" .. actor.id .. "] not part of any wave" , DEBUG_PROCESS_ACTOR)
	end
end

function WaveManager:processActor(id, name)
	if not name then
		log("Attempted to process unnamed actor, bail out.", DEBUG_PROCESS_ACTOR)
		return
	end
	
	log("Process actor " .. id, DEBUG_PROCESS_ACTOR)
	
	local actor = ObjectManager.get(id)
	if not actor.name then
		actor.name = name
	end
	
	if actor:getType() == ObjectType.SPAWN_POINT then
		log("Processing spawn point " .. name .. "[" .. id .. "]", DEBUG_PROCESS_ACTOR)
		self:processSpawnPoint(actor)
	elseif actor:getType() == ObjectType.SENSOR then
		log("Processing sensor " .. name .. "[" .. id .. "]", DEBUG_PROCESS_ACTOR)
		self:processTrigger(actor)
	elseif actor:getType() == ObjectType.SPECIAL_ZONE then
		log("Processing zone " .. name .. "[" .. id .. "]", DEBUG_PROCESS_ACTOR)
		self:processZone(actor)
	end
end
