-- This file is part of Raid DPS
--
-- (C) 2016 Scott Yeskie (Sasky)
--
-- This program is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.

-- TODO: Fix bosses removed > 0hp
-- TODO: Clear on player activate?

---
-- Raid DPS
--
-- Calculates the raid DPS by examining health changes on the boss(es)
-- This doesn't account for adds (as there isn't an easy way to account for them)
-- but still provides an effective measure (provided the group kills the boss).
--
-- One thing to note is TOTAL HEALTH is used here as the {starting health} + {healing to boss}
--   * Starting health is used incase the addon in initialized mid-fight (or a boss isn't initially picked up)
--     It should be the same as max health 99%+ of the time.
--   * Boss healing is added in as it represents additional damage needed to take off that can be accounted for.
--     Note: because of this, on some bosses (eg: 3 flesh atronauchs) a higher dps might be a longer time
--
-- Design: There are two different update loops: the model/data update and the UI update. The model is updated on
-- the EVENT_POWER_UPDATE, but the UI is moderated to only update every second to lessen addon impact
--
local RaidDPS = ZO_Object:Subclass()
function RaidDPS:New(...)
    local ctl = ZO_Object.New(self)
    ctl:Init(...)
    return ctl
end

---
-- @desc Main initialization function
--
function RaidDPS:Init()
    self:ResetCounters()
    self.inFight = false
    local control = CreateTopLevelWindow("RaidDPS_UI")

    control:RegisterForEvent(EVENT_BOSSES_CHANGED, function(a,b,c,d) self:CheckAllBosses(a,b,c,d) end)

    control:RegisterForEvent(EVENT_POWER_UPDATE, function(_, unitTag, _, _, powerValue) self:OnPowerUpdate(unitTag, powerValue) end)
    control:AddFilterForEvent(EVENT_POWER_UPDATE, REGISTER_FILTER_POWER_TYPE, POWERTYPE_HEALTH)
    control:AddFilterForEvent(EVENT_POWER_UPDATE, REGISTER_FILTER_UNIT_TAG_PREFIX, "boss")

    control:RegisterForEvent(EVENT_PLAYER_ACTIVATED, function() self:CreateUI() end)

    self.ui = self.ui or {}
    self.ui.main = control

    --TODO Check bosses on combat state change for end?
end

--- Add boss to be tracked
-- @desc This is used to initiate tracking for a boss. For each boss, last value and effective max are stored
-- @param unitTag - boss to register health values for
--
function RaidDPS:AddBoss(unitTag)
    local bossEntry = {}
    local health, max = GetUnitPower(unitTag, POWERTYPE_HEALTH)

    bossEntry.current = health
    --Max is used for determining resets
    bossEntry.max = max
    --Entered mid-fight, so go from current health for total health
    bossEntry.total = health

    if not self.bossHealth[unitTag] or not self.inFight then
        --Update totals
        self.allBossesCurrent = self.allBossesCurrent + health
        self.allBossesMax = self.allBossesMax + max
        self.allBossesTotal = self.allBossesTotal + health
    end

    self.bossHealth[unitTag] = bossEntry
end

--TODO: Tie UI update to start/stop
function RaidDPS:StartFight()
    local now = GetGameTimeMilliseconds()
    if self.allBossesTotal > self.allBossesMax then
        self:CheckAllBosses()
    end

    self.inFight = true
    self.fightStart = now
    self.fightEnd = false
    EVENT_MANAGER:RegisterForUpdate("RaidDPS_UI_Update", 1000, function() self:UpdateUI() end)
end

function RaidDPS:EndFight()
    if self.inFight then
        self.fightEnd = GetGameTimeMilliseconds()
        self:UpdateUI()
        self.inFight = false
        EVENT_MANAGER:UnregisterForUpdate("RaidDPS_UI_Update")
    end
end

---
-- @desc Runs through all boss tags and adds those that exist
-- The update event doesn't have anything, so it needs to run
--
function RaidDPS:CheckAllBosses()
    self.ui.main:SetHidden(false)
    if not self.inFight then
        self:ResetCounters()
    end

    local nBosses = 0
    for i = 1, MAX_BOSSES do
        if DoesUnitExist("boss"..i) then
            nBosses = nBosses + 1
            self:AddBoss("boss"..i)
        end
    end

    if self.inFight then
        if nBosses == 0 then
            self:EndFight()
        end
    else
        if nBosses ~= 0 and self.allBossesMax ~= self.allBossesCurrent and self.allBossesCurrent ~= 0 then
            --d("Mid-fight start")
            self:StartFight()
        end
    end

    --d("Bosses changed: " .. nBosses .. " bosses")
end

function RaidDPS:ResetCounters()
    self.bossHealth = {}

    self.fightStart = 0
    self.fightEnd = 1
    self.allBossesCurrent = 0
    self.allBossesMax = 0
    self.allBossesTotal = 0

    -- Zero-indexing totals so we can use modulo
    self.totals = {}
    for i=0,9 do
        self.totals[i] = 0
    end
    self.nextTotal = 0
end

--- Utility function for
-- @return1 current health of all bosses
-- @return2 current effective total of all bosses
function RaidDPS:GetDamage()
    return self.allBossesTotal - self.allBossesCurrent
end

--- Main handler for the EVENT_POWER_UPDATE
-- @param unitTag - boss that has health value changing
-- @param powerValue - new health value for that boss
--
function RaidDPS:OnPowerUpdate(unitTag, powerValue)
    if(self.bossHealth[unitTag]) then
        --Start fight
        if not self.inFight then
            self:StartFight()
        end

        local bossEntry = self.bossHealth[unitTag]
        local oldHP = bossEntry.current
        local deltaPower = powerValue - oldHP

        --d("UPDATE("..unitTag.."): "..oldHP.." -> "..powerValue)

        --Update current
        bossEntry.current = powerValue
        self.allBossesCurrent = self.allBossesCurrent + deltaPower

        --Increase cap if healing done to boss
        if deltaPower > 0 then
            bossEntry.total = bossEntry.total + deltaPower
            self.allBossesTotal = self.allBossesTotal + deltaPower
            if self.allBossesCurrent == self.allBossesMax then
                self:EndFight() --Raid wipe and boss reset
            end
        end

        --End fight
        if self.allBossesCurrent == 0 then
            --Out of boss range
            self:EndFight()
        end
    end
end

function RaidDPS:CreateUI()
    --Load settings
    self.cfg = ZO_SavedVars:NewAccountWide("RaidDPS_SavedVars", 1.0, "config", {})
    local cfg = self.cfg

    local font = "EsoUI/Common/Fonts/univers67.otf|16|thin"

    local ui = self.ui
    ui.main = ui.main or WINDOW_MANAGER:CreateTopLevelWindow("RaidDPS_UI")
    ui.main:SetDimensions(280, 30)
    ui.main:SetMouseEnabled(true)
    ui.main:SetMovable(true)
    ui.main:SetClampedToScreen(true)
    ui.main:SetHandler("OnMoveStop", function(window) self:SaveUIPosition(window) end)
    ui.main:ClearAnchors()

    ui.bg = WINDOW_MANAGER:CreateControl("RaidDPS_UI_bg", RaidDPS_UI, CT_BACKDROP)
    ui.bg:SetDimensions(280, 30)
    ui.bg:SetCenterColor(0, 0, 0, 0.3)
    ui.bg:SetEdgeColor(0, 0, 0, 0)

    ui.txtFullDps = WINDOW_MANAGER:CreateControl("RaidDPS_UI_dpsFull", ui.bg, CT_LABEL)
    ui.txtDps10 = WINDOW_MANAGER:CreateControl("RaidDPS_UI_dps10", ui.bg, CT_LABEL)
    ui.txtTime = WINDOW_MANAGER:CreateControl("RaidDPS_UI_time", ui.bg, CT_LABEL)
    ui.txtTime10 = WINDOW_MANAGER:CreateControl("RaidDPS_UI_time10", ui.bg, CT_LABEL)
    ui.txtDps10:SetFont(font)
    ui.txtFullDps:SetFont(font)
    ui.txtTime:SetFont(font)
    ui.txtTime10:SetFont(font)
    --
    ui.imgTimer1 = WINDOW_MANAGER:CreateControl("RaidDPS_UI_clock1", ui.bg, CT_TEXTURE)
    ui.imgTimer1:SetTexture("/esoui/art/miscellaneous/timer_32.dds")
    ui.imgTimer1:SetDimensions(16, 16)
    ui.imgTimer2 = WINDOW_MANAGER:CreateControl("RaidDPS_UI_clock2", ui.bg, CT_TEXTURE)
    ui.imgTimer2:SetTexture("/esoui/art/miscellaneous/timer_32.dds")
    ui.imgTimer2:SetDimensions(16, 16)
    ui.imgFull = WINDOW_MANAGER:CreateControl("RaidDPS_UI_iconFull", ui.bg, CT_TEXTURE)
    ui.imgFull:SetTexture("/esoui/art/trials/trialpoints_high.dds")
    ui.imgFull:SetDimensions(20, 20)
    ui.img10 = WINDOW_MANAGER:CreateControl("RaidDPS_UI_icon10", ui.bg, CT_TEXTURE)
    ui.img10:SetTexture("/esoui/art/trials/trialpoints_low.dds")
    ui.img10:SetDimensions(20, 20)

    ui.main:SetAnchor(
        cfg.selfPoint or TOPLEFT, GuiRoot,
        cfg.anchPoint or TOPLEFT,
        cfg.xoff or 10, cfg.yoff or 50
    )
    ui.bg:SetAnchor(TOPLEFT, RaidDPS_UI, TOPLEFT, 0, yoff)
    ui.imgFull:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 5, 5)
    ui.txtFullDps:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 30, 6)
    ui.imgTimer1:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 80, 9)
    ui.txtTime:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 100, 6)

    ui.img10:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 155, 5)
    ui.txtDps10:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 180, 6)
    ui.imgTimer2:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 230, 9)
    ui.txtTime10:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 250, 6)

    ui.txtFullDps:SetText("0")
    ui.txtDps10:SetText("0")
    ui.txtTime:SetText("0:00")
    ui.txtTime10:SetText(":10")
    ui.main:SetHidden(true)
    ui.main:UnregisterForEvent(EVENT_PLAYER_ACTIVATED)
    ui.main:RegisterForEvent(EVENT_ACTION_LAYER_POPPED, function(_,_,indx) self:ActionLayerChange(indx) end)
    ui.main:RegisterForEvent(EVENT_ACTION_LAYER_PUSHED, function(_,_,indx) self:ActionLayerChange(indx) end)
end

function RaidDPS:ActionLayerChange(activeLayerIndex)
    self.ui.bg:SetHidden(activeLayerIndex > 2)
end

function RaidDPS:SaveUIPosition( window )
    local _, sP, _, aP, x, y = window:GetAnchor()
    local cfg = self.cfg
    cfg.anchPoint = aP
    cfg.selfPoint = sP
    cfg.xoff = x
    cfg.yoff = y
end

function RaidDPS:UpdateUI()
    if not self.inFight then
        return
    end

    local curDamage = self:GetDamage()
    local ui = self.ui

    local endTime = self.fightEnd
    if self.inFight then
        endTime = GetGameTimeMilliseconds()
    end
    local fullFightTime = (endTime - self.fightStart)
    ui.txtTime:SetText(FormatTimeMilliseconds(fullFightTime,
        TIME_FORMAT_STYLE_COLONS, TIME_FORMAT_PRECISION_SECONDS, TIME_FORMAT_DIRECTION_ASCENDING))

    local dps_total = curDamage * 1000 / fullFightTime
    ui.txtFullDps:SetText(FormatIntegerWithDigitGrouping(zo_round(dps_total), ",", 3))

    if self.inFight then
        self.nextTotal = (self.nextTotal + 1) % 10
        local dps_10sec = (curDamage - self.totals[self.nextTotal]) / 10
        ui.txtDps10:SetText(FormatIntegerWithDigitGrouping(dps_10sec, ",", 3))

        self.totals[self.nextTotal] = curDamage
    end
end

RAID_DPS = RaidDPS:New()