-- This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License.
-- To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/
-- or send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.

local MereHealingFrames, privateVars = ...

MereHealingFrames.RaidManagement = {}

function MereHealingFrames.RaidManagement.PopulateLayout(layout)

end

MereHealingFrames.RaidManagement.RaidChangedInCombat = false
MereHealingFrames.RaidManagement.InCombat = false

MereHealingFrames.RaidManagement.Units = {}
MereHealingFrames.RaidManagement.Specifiers = {}

-- in many ways this caches the info from Inspect.Unit.Detail, but provides some defaults

MereHealingFrames.RaidManagement.playerInfo = {
	-- these 3 must always be provided
	unitId = nil,
	specifier = nil,

	-- mhf tracking info
	FromSave = false,

	name = "unknown",
	afk = false,
	aggro = false,
	blocked = false,
	offline = false,
	calling = "unknown",
	combat = false,
	coordX = 0,
	coordY = 0,
	coordZ = 0,
	radius = 0.5,
	distanceFromPlayer = 0,
	inRange = nil,
	energy = 100,
	energyMax = 100,
	health = 1000,
	healthMax = 1000,
	healthCap = 1000,  -- what does healthcap mean?
	mana = 100,
	manaMax = 100,
	power = 100,
    absorb = 0,
	role = "unknown",
	grouping = 1,
	buffMgmt = nil,
	isFake = false,
}

local playerInfo = MereHealingFrames.RaidManagement.playerInfo

local function calculateGrouping(spec)
	if (spec == "player") then
		return 1
	end

	local groupId = spec:match("%d%d")
	if not groupId then
		return 1
	end

	local group = math.floor((groupId - 1) / 5) + 1
	return group
end



function playerInfo:new(settings, unitId, spec)
	if (unitId == nil) or (spec == nil) then
		error("must provide basic player info")
	end

	local this = settings or {}
	setmetatable(this, self)
	self.__index = self

	this.unitId = unitId
	this.specifier = spec

	if (MereHealingFrames.RaidManagement.InCombat) then
		MereHealingFrames.RaidManagement.RaidChangedInCombat = true
	end

	local existingPlayerInfo = MereHealingFrames.RaidManagement.Units[this.unitId]
	if existingPlayerInfo then
		existingPlayerInfo.FromSave = false
		existingPlayerInfo:changeSpecifier(spec)

		return existingPlayerInfo
	end

	local currentSpecifier = MereHealingFrames.RaidManagement.Specifiers[this.specifier]
	if currentSpecifier and currentSpecifier.isFake then
		currentSpecifier:delete()
	end

	MereHealingFrames.RaidManagement.Units[this.unitId] = this
	MereHealingFrames.RaidManagement.Specifiers[this.specifier] = this

	this.grouping = calculateGrouping(spec)

	this.buffMgmt = MereHealingFrames.BuffManager.PlayerBuffs:new(this)

    this.PlayerEvents = MereHealingFrames.PlayerEventManager.NewEvents()

	this:UpdateDetails()
	return this
end

function playerInfo:changeSpecifier(spec)
    playerInfo:Active()

    if (self.specifier == spec) then
        -- happens when loading, the saved frames are present, then the scanned ones are primed
        return
    end

    if MereHealingFrames.RaidManagement.Specifiers[self.specifier] == self then
        MereHealingFrames.RaidManagement.Specifiers[self.specifier] = nil
    end
	self.specifier = spec
	MereHealingFrames.RaidManagement.Specifiers[self.specifier] = self
	self.grouping = calculateGrouping(spec)
end

function playerInfo:Active()
    self.FromSave = false
end

function playerInfo:delete()
	MereHealingFrames.RaidManagement.Units[self.unitId] = nil
	MereHealingFrames.RaidManagement.Specifiers[self.specifier] = nil
    MereHealingFrames.PlayerEventManager.OldEvents(self.PlayerEvents)
    self.PlayerEvents = nil
end

local function PanelUpdate(unitId, targetFunction, value)
    if not targetFunction then return end
	local panels = MereHealingFrames.UnitIdPanels[unitId] or {}
	for i, panel in ipairs(panels) do
		targetFunction(panel, value)
	end
end

local function RelayLayouts()
	for layoutName, layout in pairs(MereHealingFrames.Layouts) do
		layout:RelayPanels()
	end
end

local eventLookups =
	{	health 		= false,
		healthMax	= false,
		mana		= false,
		manaMax 	= false,
		energy 		= false,
		energyMax	= false,
		power 		= false, -- change to power...
		role 		= MereHealingFrames.HealingPanel.SetRole,
		ready 		= MereHealingFrames.HealingPanel.Ready,
		aggro 		= MereHealingFrames.HealingPanel.StatusChange,
		blocked 	= MereHealingFrames.HealingPanel.StatusChange,
		offline 	= MereHealingFrames.HealingPanel.StatusChange,
		dead		= MereHealingFrames.HealingPanel.StatusChange,
		afk 		= MereHealingFrames.HealingPanel.Afk,
		name		= MereHealingFrames.HealingPanel.Name,
		calling		= MereHealingFrames.HealingPanel.Calling,	-- not currently triggered by an event
        absorb      = MereHealingFrames.HealingPanel.UpdateAbsorb,
	}

function playerInfo:UpdateValue(field, value)
	self[field] = value
    self.PlayerEvents.Functions[field](value)
	return PanelUpdate(self.unitId, eventLookups[field], value)
end

local RMUnits = MereHealingFrames.RaidManagement.Units
function playerInfo.UpdateUnitValues(UnitIds, field)
    local PanelFunction = eventLookups[field]
    local localRMUnits = RMUnits -- crazy as this seems this saves one lookup every loop

    for unitId, value in pairs(UnitIds) do
        local playerInfo = localRMUnits[unitId]
        if playerInfo then
            playerInfo[field] = value
            playerInfo.PlayerEvents.Functions[field](value)
            PanelUpdate(unitId, PanelFunction, value)
        end
    end
end

function playerInfo:UpdateValueAndRelay(field, value)
	self:UpdateDetails()
	return RelayLayouts()
end

function playerInfo:UpdateDetails()
	-- need to loop through looking for values we'd want to update
	if (self.isFake) then
		return
	end
	local unitDetails = Inspect.Unit.Detail(self.unitId)
	if unitDetails then
		for field, func in pairs(eventLookups) do
			if unitDetails[field] then
				self:UpdateValue(field, unitDetails[field])
			end
		end
		if unitDetails.coordX then
			self:UpdateCoords(unitDetails.coordX, unitDetails.coordY, unitDetails.coordZ)
		end
		if unitDetails.radius then
			self:UpdateRadius(unitDetails.radius)
		end

		self.buffMgmt:ResetBuffs()
	end
end

local PlayerPos = { x = 0, y = 0, z = 0, radius = 0.5}
function playerInfo:UpdateCoords(x, y, z)
	self.coordX = x
	self.coordY = y
	self.coordZ = z

	if self.unitId == MereHealingFrames.PlayerId then
        local playerPos = PlayerPos
        playerPos.x = x
        playerPos.y = y
        playerPos.z = z
		MereHealingFrames.RaidManagement.ReCalcAllRanges()
	else
		self:RecalcRange()
	end
end

function playerInfo:UpdateRadius(radius)
	self.raidus = radius
	if self.unitId == MereHealingFrames.PlayerId then
		PlayerPos.raidus = radius
		MereHealingFrames.RaidManagement.ReCalcAllRanges()
	else
		self:RecalcRange()
	end
end

local localsqrt = math.sqrt
function playerInfo:RecalcRange()
    local playerPos = PlayerPos
	local xDiff, yDiff, zDiff = playerPos.x - self.coordX, playerPos.y - self.coordY, playerPos.z - self.coordZ

	local distanceFromPlayer = localsqrt ((xDiff * xDiff) + (yDiff * yDiff) + (zDiff * zDiff))

    local castDistanceFromPlayer = distanceFromPlayer - self.radius - playerPos.radius

    local currentCastDistance = self.castDistanceFromPlayer

    if (currentCastDistance == castDistanceFromPlayer) then
        return
    end

    self.distanceFromPlayer = distanceFromPlayer
    self.castDistanceFromPlayer = castDistanceFromPlayer

    self.PlayerEvents.Functions.range(distanceFromPlayer)
    self.PlayerEvents.Functions.castRange(castDistanceFromPlayer)

	local currentRange = self.inRange
	local newRange = (castDistanceFromPlayer < MereHealingFrames.SpellRange)

	if newRange == currentRange then
		return
    else
        self.inRange = newRange
        self.PlayerEvents.Functions.inRange(newRange)
		return PanelUpdate(self.unitId, MereHealingFrames.HealingPanel.StatusChange,
			newRange)
	end
end

function MereHealingFrames.RaidManagement.ReCalcAllRanges()
	for unitId, playerInfo  in pairs(MereHealingFrames.RaidManagement.Units) do
		playerInfo:RecalcRange()
	end
end

function MereHealingFrames.RaidManagement.CreateUnitFilterList()
	local emptyList = {}

	for unitId, _  in pairs(MereHealingFrames.RaidManagement.Units) do
		emptyList[unitId] = true
	end
	return emptyList
end

function MereHealingFrames.RaidManagement.FilterUnitListByField(unitList, filter, field)
	local newList = {}
	if not filter then
		return unitList
	end

	for unitId, filtered in pairs(unitList) do
		if not filtered then
			newList[unitId] = false
		else
			local playerInfo = MereHealingFrames.RaidManagement.Units[unitId]
			if playerInfo then
				newList[unitId] = filter[playerInfo[field]] or false
			end
		end
	end

	return newList
end

function MereHealingFrames.RaidManagement.FilterUnitListByFunc(unitList, filterFunc)
	local newList = {}
	if not filterFunc then
		return unitList
	end

	local units = MereHealingFrames.RaidManagement.Units
	for unitId, filtered in pairs(unitList) do
		if not filtered then
			newList[unitId] = false
		else
			local playerInfo = units[unitId]
			if playerInfo then
				newList[unitId] = filterFunc(playerInfo) or false
			end
		end
	end

	return newList
end


function MereHealingFrames.RaidManagement.SaveRaid()
	local savedRaidData = {}
	for unitId, playerData in pairs(MereHealingFrames.RaidManagement.Units) do
		if not playerData.isFake then
			savedRaidData[unitId] = { name = playerData.name, spec = playerData.specifier }
		end
	end

	return savedRaidData
end

function MereHealingFrames.RaidManagement.CheckForStrays()
	local binnedPlayers = false
	for unitId, playerData in pairs(MereHealingFrames.RaidManagement.Units) do
		if playerData.FromSave then
			-- it's a stray so bin it
			playerData:delete()
			binnedPlayers = true
			for layoutName, layout in pairs(MereHealingFrames.Layouts) do
				layout:GroupLeave(unitId, playerData.specifier, true)
			end
		end
    end
    for layoutName, layout in pairs(MereHealingFrames.Layouts) do
        layout:ProcessPendingLayoutChanges()
    end
end

function MereHealingFrames.RaidManagement.LoadRaid(savedVarsData)
	local savedRaidData = {}
	for unitId, playerData in pairs(savedVarsData) do
		MereHealingFrames.Debug(1, "loading player from save: %s, %s, %s", unitId, tostring(playerData.name), tostring(playerData.spec))
		if not unitId:find("fake") then
			if playerData.spec then
				local playerSettings = {
					FromSave = true,
					name = playerData.name
					}
				local newPlayerInfo = playerInfo:new(playerSettings, unitId, playerData.spec)
			end
		end
	end
end


local fakeNames =
{
	"Orphiel",
	"Aedraxis",
	"Raj",
	"Kip",
	"Rusty",
	"Mere",
	"Vuhdo",
	"Kat",
	"Mupet",
	"Caaaaarl",
	"EatMe",
	"HandsHungry",
	"Putter",
	"Alpha",
	"FaceCake",
	"Jason",
	"Andy",
	"James",
	"Bill",
	"Claire",
	"Mel",
	"Ingrid",
	"Paul",
	"Emma",
	"MissPiggy",
	"Kermit",
	"Paul",
	"Mahna",
}

local fakeRealms =
{
	"Alpha",
	"Beta",
	"Gamma",
	"Delta",
}

local fakeCallings =
{
	"rogue",
	"warrior",
	"mage",
	"cleric",
}

local fakeRoles =
{
	"tank",
	"heal",
	"support",
	"dps",
}

function MereHealingFrames.RaidManagement.AddFakes(howManyFakes, realms, tanks, heal, support, dps)
    -- Fake frames can burn a fair amount of CPU, so don't have the watchdog get annoyed.
    Command.System.Watchdog.Quiet()

    local limitFakes = math.min(howManyFakes, 20)

	math.randomseed( os.time() )

	local roles = {
		tanks,
		heal,
		support,
		dps
	}
	local totalRoles = tanks + dps + support + heal

	if (totalRoles ~= howManyFakes) then
		error("roles are mis-calculated")
	end

	local actualPlayer = MereHealingFrames.RaidManagement.Specifiers["player"]
	if actualPlayer then
		MereHealingFrames.Events.GroupChange({}, actualPlayer.unitId,  "player", "group01")
	end

	MereHealingFrames.Debug(2,"units to find %d, %d, %d, %d", roles[1], roles[2], roles[3], roles[4])

	-- walk current members to remove role counts
	for unitid, unitInfo in pairs(MereHealingFrames.RaidManagement.Specifiers) do
		if unitInfo.role == "tank" then
			roles[1] = roles[1] - 1
		elseif unitInfo.role == "heal" then
			roles[2] = roles[2] - 1
		elseif unitInfo.role == "support" then
			roles[3] = roles[3] - 1
		else
			roles[4] = roles[4] - 1
		end
	end
	MereHealingFrames.Debug(2,"units after scan: %d, %d, %d, %d", roles[1], roles[2], roles[3], roles[4])
	for i = 1, 3 do
		if roles[i] < 0 then
			roles[4] = roles[4] - roles[i]
		end
	end

	MereHealingFrames.Debug(2,"final units: %d, %d, %d, %d", roles[1], roles[2], roles[3], roles[4])

    MereHealingFrames.Debug(2,"Faking up %d units", limitFakes)
	for i=1,limitFakes do
		local groupName = string.format("group%02d", i)
		MereHealingFrames.Debug(2,"Checking for %s", groupName)
		if MereHealingFrames.RaidManagement.Specifiers[groupName] == nil then
			local name = fakeNames[math.random(1, #fakeNames)]
			if (realms) then
				local realm = fakeRealms[math.random(1, #fakeRealms)]
				name = name .. "@" .. realm
			end

			local calling = fakeCallings[math.random(1,#fakeCallings)]

			local fakeUnitId = "fake" .. i

			MereHealingFrames.Debug(2,"Creating fake unit %s, as spec %s", fakeUnitId, groupName)
			local fakePlayerInfo = {
				isFake = true
				}
			local fakePlayer = playerInfo:new(fakePlayerInfo, fakeUnitId, groupName)

			local HealthMax = math.random(10000,15000)
			local Health = math.random(8000, HealthMax)
            local Absorb = math.random(0, HealthMax)

			fakePlayer.isFake = true
			fakePlayer:UpdateValue("name", name)
			local id = 4
			repeat
				id = math.random(1,4)
			until roles[id] > 0
			roles[id] = roles[id] - 1
			fakePlayer:UpdateValue("role", fakeRoles[id])
			if (id == 1) then
				fakePlayer:UpdateValue("aggro", true)
			end

			if math.random(1,10) == 1 then
				fakePlayer:UpdateValue("offline", true)
			elseif math.random(1,10) == 1 then
				fakePlayer:UpdateValue("blocked", true)
			end

			fakePlayer:UpdateValue("calling", calling)
			fakePlayer:UpdateValue("healthMax", HealthMax)
			fakePlayer:UpdateValue("health", Health)
            fakePlayer:UpdateValue("absorb", Absorb)

			if calling == "rogue" then
				fakePlayer:UpdateValue("energy", math.random(1,100))
			elseif calling == "warrior" then
				fakePlayer:UpdateValue("power", math.random(1,100))
			else
				local ManaMax = math.random(10000, 11000)
				fakePlayer:UpdateValue("manaMax", ManaMax)
				fakePlayer:UpdateValue("mana", math.random(1,ManaMax))
			end
		end
	end

	for layoutName, layout in pairs(MereHealingFrames.Layouts) do
		layout:RelayPanels()
	end
end

function MereHealingFrames.RaidManagement.RemoveFakes()
    -- Fake frames can burn a fair amount of CPU, so don't have the watchdog get annoyed.
    Command.System.Watchdog.Quiet()
	local binnedPlayers = false
	for unitId, playerData in pairs(MereHealingFrames.RaidManagement.Units) do
		if playerData.isFake then
			-- it's a stray so bin it
			binnedPlayers = true
			for layoutName, layout in pairs(MereHealingFrames.Layouts) do
				layout:GroupLeave(unitId, playerData.specifier)
            end
            playerData:delete()
		end
    end
    if (binnedPlayers) then
        for layoutName, layout in pairs(MereHealingFrames.Layouts) do
            layout:ProcessPendingLayoutChanges()
        end
    end
end

function MereHealingFrames.RaidManagement.EnterCombat()
	MereHealingFrames.RaidManagement.InCombat = true
	MereHealingFrames.RaidManagement.RemoveFakes()
end

function MereHealingFrames.RaidManagement.LeaveCombat()
	MereHealingFrames.RaidManagement.CheckForStrays()

	MereHealingFrames.RaidManagement.InCombat = false
	if MereHealingFrames.RaidManagement.RaidChangedInCombat then
		for layoutName, layout in pairs(MereHealingFrames.Layouts) do
			layout:RelayPanels()
		end
		MereHealingFrames.RaidManagement.RaidChangedInCombat = false
	end
end