local PSBT_Module           = PSBT_Module
local PSBT_Combat           = PSBT_Module:Subclass()
PSBT_Combat._iconRegistry   = setmetatable( {}, { __mode = 'kv' } )
PSBT_Combat._stackingIn     = {}
PSBT_Combat._stackingOut    = {}
local CBM                   = CALLBACK_MANAGER

local MAX_EVENTS            = 15
local STACK_TIME            = 0.85
local PlayerName            = GetUnitName( 'player' )
local PlayerNameRaw         = GetRawUnitName( 'player' )

local COMBAT_UNIT_TYPE_PLAYER       = COMBAT_UNIT_TYPE_PLAYER
local COMBAT_UNIT_TYPE_PLAYER_PET   = COMBAT_UNIT_TYPE_PLAYER_PET
local COMBAT_UNIT_TYPE_NONE         = COMBAT_UNIT_TYPE_NONE

local PSBT_AREAS            = PSBT_AREAS
local PSBT_EVENTS           = PSBT_EVENTS

local zo_strformat          = zo_strformat
local GetString             = GetString
local select                = select
local kVerison              = 1.0
local NonContiguousCount    = NonContiguousCount

local function IsPlayerType( targetType )
    return targetType == COMBAT_UNIT_TYPE_PLAYER or
           targetType == COMBAT_UNIT_TYPE_PLAYER_PET
end

local function IsPlayer( targetType, targetName )
    if ( IsPlayerType( targetType ) ) then
        return true
    end

    if ( targetType == COMBAT_UNIT_TYPE_NONE ) then
        return targetName == PlayerName or targetName == PlayerNameRaw
    end

    return false
end

local static_events =
{
    [ ACTION_RESULT_CANT_SEE_TARGET ] = true,
    [ ACTION_RESULT_KILLING_BLOW ] = true,
    [ ACTION_RESULT_CANNOT_USE ] = true,
    [ ACTION_RESULT_BUSY ] = true,
    [ ACTION_RESULT_FALLING ] = true,
}

local critical_events =
{
    [ ACTION_RESULT_DOT_TICK_CRITICAL ] = true,
    [ ACTION_RESULT_CRITICAL_DAMAGE ] = true,
    [ ACTION_RESULT_CRITICAL_HEAL ] = true,
    [ ACTION_RESULT_HOT_TICK_CRITICAL ] = true,
    [ ACTION_RESULT_INTERRUPT ] = true,
    [ ACTION_RESULT_BLOCKED ] = true,
    [ ACTION_RESULT_BLOCKED_DAMAGE ] = true,
    [ ACTION_RESULT_ABSORBED ] = true,
    [ ACTION_RESULT_RESIST ] = true,
}

local damage_events =
{
    [ ACTION_RESULT_CRITICAL_DAMAGE ] = true,
    [ ACTION_RESULT_DAMAGE ] = true,
    [ ACTION_RESULT_DAMAGE_SHIELDED ] = true,
    [ ACTION_RESULT_DOT_TICK ] = true,
    [ ACTION_RESULT_DOT_TICK_CRITICAL ] = true,
    [ ACTION_RESULT_FALL_DAMAGE ] = true,
}

local healing_events =
{
    [ ACTION_RESULT_CRITICAL_HEAL ] = true,
    [ ACTION_RESULT_HEAL ] = true,
    [ ACTION_RESULT_HOT_TICK ] = true,
    [ ACTION_RESULT_HOT_TICK_CRITICAL ] = true,
}


function PSBT_Combat:Initialize( ... )
    PSBT_Module.Initialize( self, ... )

    self._buffer    = ZO_CircularBuffer:New( DEFAULT_MAX_BUFFERED_EVENTS )
    self._index     = 1
    self._free      = nil

    self:RefreshAbilityIcons()
    self:Initialize_Text()

    self:RegisterForEvent( EVENT_COMBAT_EVENT,          function( ... ) self:OnCombatEvent( ... )    end )
    self:RegisterForEvent( EVENT_SKILLS_FULL_UPDATE,    function() self:RefreshAbilityIcons()        end )
    self:RegisterForEvent( EVENT_SKILL_POINTS_CHANGED,  function() self:RefreshAbilityIcons()        end )
end

function PSBT_Combat:Initialize_Text()
    self._text =
    {
        [ ACTION_RESULT_ABSORBED ]          = GetString( _G[ PSBT_STRINGS.ABSORED ] ),
        [ ACTION_RESULT_BLADETURN ]         = GetString( _G[ PSBT_STRINGS.BLADE_TURN ] ),
        [ ACTION_RESULT_BLOCKED ]           = GetString( _G[ PSBT_STRINGS.BLOCK ] ),
        [ ACTION_RESULT_BLOCKED_DAMAGE ]    = GetString( _G[ PSBT_STRINGS.BLOCK_DAMAGE ] ),
        [ ACTION_RESULT_DAMAGE_SHIELDED ]   = GetString( _G[ PSBT_STRINGS.SHIELDED ] ),
        [ ACTION_RESULT_CANT_SEE_TARGET ]   = GetString( _G[ PSBT_STRINGS.CANNOT_SEE ] ),
        [ ACTION_RESULT_CRITICAL_DAMAGE ]   = GetString( _G[ PSBT_STRINGS.DAMAGE_CRIT ] ),
        [ ACTION_RESULT_CRITICAL_HEAL ]     = GetString( _G[ PSBT_STRINGS.HEALING_CRIT ] ),
        [ ACTION_RESULT_DAMAGE ]            = GetString( _G[ PSBT_STRINGS.DAMAGE ] ),
        [ ACTION_RESULT_DEFENDED ]          = GetString( _G[ PSBT_STRINGS.DEFENDED ] ),
        [ ACTION_RESULT_DOT_TICK ]          = GetString( _G[ PSBT_STRINGS.DAMAGE ] ),
        [ ACTION_RESULT_DOT_TICK_CRITICAL ] = GetString( _G[ PSBT_STRINGS.DAMAGE_CRIT ] ),
        [ ACTION_RESULT_HEAL ]              = GetString( _G[ PSBT_STRINGS.HEALING ] ),
        [ ACTION_RESULT_HOT_TICK ]          = GetString( _G[ PSBT_STRINGS.HEALING ] ),
        [ ACTION_RESULT_HOT_TICK_CRITICAL ] = GetString( _G[ PSBT_STRINGS.HEALING_CRIT ] ),
        [ ACTION_RESULT_DODGED ]            = GetString( _G[ PSBT_STRINGS.DODGE ] ),
        [ ACTION_RESULT_MISS ]              = GetString( _G[ PSBT_STRINGS.MISS ] ),
        [ ACTION_RESULT_PARRIED ]           = GetString( _G[ PSBT_STRINGS.PARRY ] ),
        [ ACTION_RESULT_RESIST ]            = GetString( _G[ PSBT_STRINGS.RESIST ] ),
        [ ACTION_RESULT_PARTIAL_RESIST ]    = GetString( _G[ PSBT_STRINGS.RESIST_PARTIAL ] ),
        [ ACTION_RESULT_FALL_DAMAGE ]       = GetString( _G[ PSBT_STRINGS.FALL_DAMAGE ] ),
        [ ACTION_RESULT_KILLING_BLOW ]      = GetString( _G[ PSBT_STRINGS.KILLING_BLOW ] ),
        [ ACTION_RESULT_BUSY ]              = GetString( _G[ PSBT_STRINGS.BUSY ] ),
        [ ACTION_RESULT_FALLING ]           = GetString( _G[ PSBT_STRINGS.FALLING ] ),
        [ ACTION_RESULT_DISORIENTED ]       = GetString( _G[ PSBT_STRINGS.DISORIENTED ] ),
        [ ACTION_RESULT_DISARMED ]          = GetString( _G[ PSBT_STRINGS.DISARMED ] ),
        [ ACTION_RESULT_FEARED ]            = GetString( _G[ PSBT_STRINGS.FEARED ] ),
        [ ACTION_RESULT_IMMUNE ]            = GetString( _G[ PSBT_STRINGS.IMMUNE ] ),
        [ ACTION_RESULT_INTERRUPT ]         = GetString( _G[ PSBT_STRINGS.INTERRUPT ] ),
        [ ACTION_RESULT_INTERCEPTED ]       = GetString( _G[ PSBT_STRINGS.INTERCEPTED ] ),
        [ ACTION_RESULT_POWER_ENERGIZE ]    = GetString( _G[ PSBT_STRINGS.ENERGIZE ] ),
    }
end

function PSBT_Combat:RefreshAbilityIcons()
    for i=1,GetNumAbilities() do
        local name, icon = GetAbilityInfoByIndex( i )
        self._iconRegistry[ name ] = icon
    end
end

function PSBT_Combat:OnCombatEvent( ... )
    local result = select( 1, ... )
    if ( not self._text[ result ] ) then
        return
    end

    local args = { ... }

    -- did we get hit or do something?
    if ( IsPlayer( args[ 7 ], args[ 6 ] )
      or IsPlayer( args[ 9 ], args[ 8 ] ) ) then

        if ( self._free ) then
            local argCount = #args
            for i = 1, argCount do
                self._free[ i ] = args[ i ]
            end

            for i = #self._free, argCount + 1, -1 do
                self._free[ i ] = nil
            end

            self._free = self._buffer:Add( self._free )
        else
            self._free = self._buffer:Add( args )
        end

        self._index = self._index - 1
    end
end

function PSBT_Combat:StackEvent( result, _, abilityName, _, _, sourceName, sourceType, targetName, targetType, hitValue, powerType, damageType )
    local stack = nil

    if ( IsPlayer( targetType, targetName ) ) then
        stack = self._stackingIn
    elseif ( IsPlayer( sourceType, sourceName ) ) then
        stack = self._stackingOut
    end

    if ( not stack[ result ] ) then
        stack[ result ] = {}
    end

    if ( not stack[ result ][ abilityName ] ) then
        stack[result][ abilityName ] =
        {
            lastTick    = 0,
            result      = result,
            abilityName = abilityName,
            sourceName  = sourceName,
            sourceType  = sourceType,
            targetName  = targetName,
            targetType  = targetType,
            hitValue    = 0,
            powerType   = powerType,
            damageType  = damageType
        }
    end

    local entry     = stack[result][ abilityName ]
    entry.lastTick  = GetFrameTimeMilliseconds()
    entry.hitValue  = entry.hitValue + hitValue
end

function PSBT_Combat:OnUpdate()
    if ( self._index <= 0 ) then
        self._index = 1
    end

    local bufferSize = self._buffer:Size()
    local endPoint = zo_min( self._index + MAX_EVENTS, bufferSize )
    for i = self._index, endPoint do
        local entry = self._buffer:At( i )
        if ( entry ) then
            self:StackEvent( unpack( entry ) )
        end
    end

    if ( endPoint >= bufferSize ) then
        self._buffer:Clear()
    else
        self._index = endPoint + 1
    end

    -- Dispatch
    if ( NonContiguousCount( self._stackingOut ) ) then
        self:ProcessStackedEvents( self._stackingOut, GetFrameTimeMilliseconds() )
    end

    if ( NonContiguousCount( self._stackingIn ) ) then
        self:ProcessStackedEvents( self._stackingIn, GetFrameTimeMilliseconds() )
    end
end

function PSBT_Combat:ProcessStackedEvents( stacking, frameTime )
    for result,entries in pairs( stacking ) do
        for k,v in pairs( entries ) do
            if ( frameTime - v.lastTick > STACK_TIME ) then
                self:DispatchEvent( result, v )
                stacking[ result ][ k ] = nil
            end
        end
    end
end

--integer result, bool isError, string abilityName, integer abilityGraphic, integer abilityActionSlotType, string sourceName, integer sourceType, string targetName, integer targetType, integer hitValue, integer powerType, integer damageType, bool log
function PSBT_Combat:DispatchEvent( result, combatEvent )
    local textFormat = self._text[ result ]
    if ( not textFormat ) then
        return
    end

    local area = PSBT_EVENTS.STATIC
    local crit = critical_events[ result ]
    local icon = self._iconRegistry[ combatEvent.abilityName ]
    local color = PSBT_SETTINGS.normal_color
    local text = ''

    if ( not static_events[ result ] ) then
        if ( IsPlayer( combatEvent.targetType, combatEvent.targetName ) ) then
            area = PSBT_AREAS.INCOMING
        elseif ( IsPlayer( combatEvent.sourceType, combatEvent.sourceName ) ) then
            area = PSBT_AREAS.OUTGOING
        end
    end

    if ( healing_events[ result ] ) then
        color = PSBT_SETTINGS.healing_color
    elseif( damage_events[ result ] ) then
        color = PSBT_SETTINGS.damage_color
    end

    if ( result == ACTION_RESULT_POWER_ENERGIZE or result == ACTION_RESULT_POWER_DRAIN ) then
        local mechanicName = GetString( 'SI_COMBATMECHANICTYPE', combatEvent.powerType )
        text = zo_strformat( textFormat, combatEvent.hitValue, mechanicName )
    else
        text = zo_strformat( textFormat, combatEvent.hitValue )
    end

    self:NewEvent( area, crit, icon, text, self._root:GetSetting( color ) )
end

CBM:RegisterCallback( PSBT_EVENTS.LOADED,
    function( psbt )
        psbt:RegisterModule( PSBT_MODULES.COMBAT, PSBT_Combat:New( psbt ), kVerison )
    end)