-- AddOn Namespace Definition
RotationHero = {}
RotationHero.name = "RotationHero"
RotationHero.debugAbilities = false
RotationHero.debugCombatState = false

function RotationHero:Initialize()
    self.inCombat = IsUnitInCombat("player")
    self.playerName = GetRawUnitName("player")

    self.combatStartTime = 0
    self.combatEventsLog = {}

    self.abilityBarSelected = ""

    self.statCombatTime = 0
    self.statCombatLACount = 0
    self.statCombatLAHit = 0
    self.statCombatLACancelCount = 0
    self.statCombatLACancelTime = 0
    self.statCombatABCount = 0
    self.statCombatABCancelCount = 0
    self.statCombatABCancelTime = 0
    self.statCombatBSCount = 0
    self.statCombatULCount = 0

    self.settingEfficencyOn = false

    -- call the handler to initialize the action slot values
    self.OnActionSlotsFullUpdate(nil, true)

    EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_COMBAT_STATE, self.OnPlayerCombatState)
    EVENT_MANAGER:RegisterForEvent(self.name, EVENT_COMBAT_EVENT, self.OnCombatEvent)
    EVENT_MANAGER:RegisterForEvent(self.name, EVENT_ACTION_SLOTS_FULL_UPDATE, self.OnActionSlotsFullUpdate)
    EVENT_MANAGER:RegisterForEvent(self.name, EVENT_ACTION_SLOT_ABILITY_USED, self.OnActionSlotAbilityUsed)

    SLASH_COMMANDS["/rh"] = RotationHero.SlashCommand
end

function RotationHero.OnAddOnLoaded(eventCode, addonName)
    if addonName == RotationHero.name then
        EVENT_MANAGER:UnregisterForEvent(addonName, eventCode)
        RotationHero:Initialize()
    end
end
EVENT_MANAGER:RegisterForEvent(RotationHero.name, EVENT_ADD_ON_LOADED, RotationHero.OnAddOnLoaded)

function RotationHero.SlashCommand(extra)
    local options = {}
    local searchResult = { string.match(extra,"^(%S*)%s*(.-)$") }
    for i,v in pairs(searchResult) do
        if (v ~= nil and v ~= "") then
            options[i] = string.lower(v)
        end
    end

    if #options == 0 or options[1] == "help" then
        d("Usage of /rh:")
        d("  help - display this help")
        d("  eff - show combat efficiency")
        d("  eff on/off - toggle automatic display after combat")
        d("  print - show animation cancelling stats")
    elseif options[1] == "eff" then
        if options[2] == "on" then
            RotationHero.settingEfficencyOn = true
            d("Combat Efficiency will be displayed after each combat")
        elseif options[2] == "off" then
            RotationHero.settingEfficencyOn = false
            d("Combat Efficiency will not be displayed after combat")
        else
            RotationHero.PrintEfficency()
        end
    elseif options[1] == "print" then
        RotationHero.PrintStats()
    end
end

-- handle change of combat state (in combat, or out of combat)
function RotationHero.OnPlayerCombatState(event, inCombat)
    if inCombat ~= RotationHero.inCombat then

        local nowTime = GetGameTimeMilliseconds()

        if inCombat then
            if RotationHero.debugCombatState then
                d(RotationHero.LogCombatTime(nowTime) .. "Entering combat.")
            end

            -- we set the inCombat state and record the timestamp when combat started
            RotationHero.inCombat = true
            RotationHero.combatStartTime = nowTime

            RotationHero.AddCombatEvent(nowTime, { type = "start" })

        else
            if RotationHero.debugCombatState then
                d(RotationHero.LogCombatTime(nowTime) .. "Exiting combat.")
            end

            RotationHero.AddCombatEvent(nowTime, { type = "end" })

            -- set the inCombat to false
            RotationHero.inCombat = false

            RotationHero.CalculateStats()

            if RotationHero.settingEfficencyOn then
                RotationHero.PrintEfficency()
            end

            -- reset the combat dependant vars
            RotationHero.combatStartTime = 0
            RotationHero.combatEventsLog = {}
        end
    end
end

function RotationHero.OnCombatEvent(eventID, result, isError, abilityName, abilityGraphic, abilityActionSlotType,
    sourceName, sourceType, targetName, targetType, hitValue, powerType, damageType, log, sourceUnitId, targetUnitId, abilityId)

    local nowTime = GetGameTimeMilliseconds()

    --[[
    if RotationHero.debug then
        d("result=" .. tostring(result)
        .. ", isError=" .. tostring(isError)
        .. ", abilityName=" .. tostring(abilityName)
        .. ", abilityGraphic=" .. tostring(abilityGraphic)
        .. ", abilityActionSlotType=" .. tostring(abilityActionSlotType)
        .. ", sourceName=" .. tostring(sourceName)
        .. ", sourceType=" .. tostring(sourceType)
        .. ", targetName=" .. tostring(targetName)
        .. ", targetType=" .. tostring(targetType)
        .. ", hitValue=" .. tostring(hitValue)
        .. ", powerType=" .. tostring(powerType)
        .. ", damageType=" .. tostring(damageType)
        .. ", log=" .. tostring(log)
        .. ", sourceUnitId=" .. tostring(sourceUnitId)
        .. ", targetUnitId=" .. tostring(targetUnitId)
        .. ", abilityId=" .. tostring(abilityId))
    end]]

    if sourceName == RotationHero.playerName then
        if abilityActionSlotType == ACTION_SLOT_TYPE_LIGHT_ATTACK then
            if result == 1 or result == 2 then
                RotationHero.AddCombatEvent(nowTime, { type = "damage", name = abilityName, ability = ACTION_SLOT_TYPE_LIGHT_ATTACK, critical = (result == 2 and true or false), damage = hitValue })
            end
        end
    end
end

-- called anytime action slots change and on bar swap
function RotationHero.OnActionSlotsFullUpdate(eventID, isBarSwap)
    if isBarSwap then
        local activeWeaponPair, locked = GetActiveWeaponPairInfo()
        -- should be one of: ACTIVE_WEAPON_PAIR_MAIN, ACTIVE_WEAPON_PAIR_BACKUP, ACTIVE_WEAPON_PAIR_NONE

        local abilityBar = activeWeaponPair == ACTIVE_WEAPON_PAIR_MAIN and "front" or "back"
        if abilityBar ~= RotationHero.abilityBarSelected then

            local nowTime = GetGameTimeMilliseconds()

            if RotationHero.abilityBarSelected ~= "" then
                if RotationHero.debugAbilities then
                    d(RotationHero.LogCombatTime(nowTime) .. "Bar swap: " .. RotationHero.abilityBarSelected .. " -> " .. abilityBar)
                end
            end

            RotationHero.AddCombatEvent(nowTime, {type = "barswap", bar = abilityBar})

            RotationHero.abilityBarSelected = abilityBar
        end
    end
end

-- player has activated an ability
function RotationHero.OnActionSlotAbilityUsed(eventId, abilitySlot)

    -- get the name of activated ability
    local abilityName = GetSlotName(abilitySlot)

    -- get the current time, so we can calculate offsets
    local nowTime = GetGameTimeMilliseconds()

    if RotationHero.debugAbilities then
        d(RotationHero.LogCombatTime(nowTime) .. abilityName)
    end

    RotationHero.AddCombatEvent(nowTime, { type = "ability", slot = abilitySlot, name = abilityName })
end

function RotationHero.LogCombatTime(time)
    if RotationHero.combatStartTime == 0 or RotationHero.combatStartTime > time then
        return "[0] "
    end
    return "[" .. tostring(time - RotationHero.combatStartTime) .. "] "
end

function RotationHero.AddCombatEvent(time, event)
    RotationHero.combatEventsLog[time] = event
end

function RotationHero.CalculateStats()
    local timestamps = {}
    for index, value in pairs(RotationHero.combatEventsLog) do
        table.insert(timestamps, index)
    end
    table.sort(timestamps)

    -- find the combat start; we can count anything cast up to 1s before the start of combat to belong aswell
    local startParsingFromIdx = 0
    local timestampIdx = 1
    while timestamps[timestampIdx] ~= nil and timestamps[timestampIdx] < (RotationHero.combatStartTime - 1000) do
        timestampIdx = timestampIdx + 1
    end

    -- search until the actual start of combat event, and see if there is an end of combat event, then adjust
    startParsingFromIndex = timestampIdx
    while timestamps[timestampIdx] ~= nil and timestamps[timestampIdx] < RotationHero.combatStartTime do
        if RotationHero.combatEventsLog[timestamps[timestampIdx]].type == "end" then
            startParsingFromIndex = timestampIdx + 1
        end
        timestampIdx = timestampIdx + 1
    end

    -- initialize stats to zero
    RotationHero.statCombatTime = 0
    RotationHero.statCombatLACount = 0
    RotationHero.statCombatLAHit = 0
    RotationHero.statCombatLACancelCount = 0
    RotationHero.statCombatLACancelTime = 0
    RotationHero.statCombatABCount = 0
    RotationHero.statCombatABCancelCount = 0
    RotationHero.statCombatABCancelTime = 0
    RotationHero.statCombatBSCount = 0
    RotationHero.statCombatULCount = 0

    -- some state handling variables
    local sLAFired = 0
    local sABFired = 0
    local sLastTime = 0

    -- ok now we parse until the end of combat
    timestampIdx = startParsingFromIndex
    while timestamps[timestampIdx] do
        local timestamp = timestamps[timestampIdx]
        local event = RotationHero.combatEventsLog[timestamp]

        if event.type == "ability" then

            sLastTime = timestamp

            if event.slot == 1 then

                RotationHero.statCombatLACount = RotationHero.statCombatLACount + 1

                sLAFired = timestamp
                sABFired = 0

            elseif event.slot > 2 and event.slot < 8 then

                if sLAFired > 0 and timestamp < sLAFired + 1000 then
                    RotationHero.statCombatLACancelCount = RotationHero.statCombatLACancelCount + 1
                    RotationHero.statCombatLACancelTime = RotationHero.statCombatLACancelTime + (timestamp - sLAFired)
                end

                RotationHero.statCombatABCount = RotationHero.statCombatABCount + 1
                sABFired = timestamp

            elseif event.slot == 8 then
                RotationHero.statCombatULCount = RotationHero.statCombatULCount + 1
            end

        elseif event.type == "damage" then

            if event.ability == ACTION_SLOT_TYPE_LIGHT_ATTACK then
                RotationHero.statCombatLAHit = RotationHero.statCombatLAHit + 1
                sLAFired = 0
            end

        elseif event.type == "barswap" then

            RotationHero.statCombatBSCount = RotationHero.statCombatBSCount + 1

            if sABFired > 0 and timestamp < sABFired + 1000 then
                RotationHero.statCombatABCancelCount = RotationHero.statCombatABCancelCount + 1
                RotationHero.statCombatABCancelTime = RotationHero.statCombatABCancelTime + (timestamp - sABFired)
            end

        end

        timestampIdx = timestampIdx + 1
    end

    RotationHero.statCombatTime = math.ceil((sLastTime - RotationHero.combatStartTime)/1000)
end

function RotationHero.PrintEfficency()
    if (RotationHero.statCombatLACount > 0 and RotationHero.statCombatTime > 0) then
        d("Combat Efficiency:"
        .. " LA hits " .. tostring(RotationHero.statCombatLAHit) .. "/" .. tostring(RotationHero.statCombatLACount)
        .. "(" .. tostring(math.floor(RotationHero.statCombatLAHit / RotationHero.statCombatLACount * 100.0)) .. "%)"
        .. " Abilities " .. tostring(RotationHero.statCombatABCount) .. "/" .. tostring(RotationHero.statCombatTime)
        .. "(" .. tostring(math.floor(RotationHero.statCombatABCount / RotationHero.statCombatTime * 100.0)) .. "%)")
    end
end

function RotationHero.PrintStats()
    if RotationHero.statCombatLACount > 0 then
        d("Light Attack Canceling: " .. tostring(RotationHero.statCombatLACancelCount) .. "/" .. tostring(RotationHero.statCombatLACount)
        .. "(" .. tostring(math.floor(RotationHero.statCombatLACancelCount / RotationHero.statCombatLACount * 100.0)) .. "%)"
        .. " [" .. tostring(RotationHero.statCombatLACancelCount > 0 and math.floor(RotationHero.statCombatLACancelTime / RotationHero.statCombatLACancelCount) or 0) .. "ms]")
    end

    if RotationHero.statCombatBSCount > 0 then
        d("Ability Canceling: " .. tostring(RotationHero.statCombatABCancelCount) .. "/" .. tostring(RotationHero.statCombatBSCount)
        .. "(" .. tostring(math.floor(RotationHero.statCombatABCancelCount / RotationHero.statCombatBSCount * 100.0)) .. "%)"
        .. " [" .. tostring(RotationHero.statCombatABCancelCount > 0 and math.floor(RotationHero.statCombatABCancelTime / RotationHero.statCombatABCancelCount) or 0) .. "ms]")
    end
end

function RotationHero.DumpTable(o)
    if type(o) == 'table' then
       local s = '{ '
       for k,v in pairs(o) do
          if type(k) ~= 'number' then k = '"'..k..'"' end
          s = s .. k .. ' : ' .. RotationHero.DumpTable(v) .. ','
       end
       return s .. '} '
    else
       if type(o) ~= 'number' then return '"' .. tostring(o) .. '"'
       else return tostring(o) end
    end
 end