--========================================================--
--                Scorpio UnitFrame API                   --
--                                                        --
-- Author      :  kurapica125@outlook.com                 --
-- Create Date :  2020/11/25                              --
--========================================================--

--========================================================--
Scorpio           "Scorpio.Secure.UnitFrame.API"     "1.0.0"
--========================================================--

namespace "Scorpio.Secure.UnitFrame"

import "System.Reactive"
import "System.Toolset"

------------------------------------------------------------
--                    Simple Unit API                     --
------------------------------------------------------------
local _UnitNameSubject          = Subject()
local _UnitTimerSubject         = Subject()
local GetUnitOwner              = function(unit)
    return unit and (unit:match("^[pP][eE][tT]$") and "player" or unit:gsub("[pP][eE][tT]", "")) or nil
end

Wow.GetUnitOwner                = GetUnitOwner

-- Use service so this can be recover from errors
__Service__(true)
function UnitTimerService()
    while true do
        _UnitTimerSubject:OnNext("any")
        Delay(0.5)
    end
end

__SystemEvent__()
function UNIT_NAME_UPDATE(unit)
    _UnitNameSubject:OnNext(unit)
end

__SystemEvent__()
function GROUP_ROSTER_UPDATE()
    _UnitNameSubject:OnNext("any")
end

__Static__()
function Wow.Unit()
    return Wow.FromUnitEvent()
end

__Static__()
function Wow.UnitOwner()
    return Wow.FromUnitEvent():Map(GetUnitOwner)
end

__Static__() __AutoCache__()
function Wow.UnitTimer()
    return Wow.FromUnitEvent(_UnitTimerSubject)
end

__Static__() __AutoCache__()
function Wow.UnitName(withServer)
    return Wow.FromUnitEvent(_UnitNameSubject):Next():Map(withServer and function(unit) return GetUnitName(unit, true) end or GetUnitName)
end

__Static__() __AutoCache__()
function Wow.UnitOwnerName(withServer)
    return Wow.FromUnitEvent(_UnitNameSubject):Next():Map(function(unit) return GetUnitName(GetUnitOwner(unit), withServer) end)
end

__Static__() __AutoCache__()
function Wow.UnitColor()
    return Wow.FromUnitEvent(_UnitNameSubject):Next():Map(function(unit)
        local _, cls            = UnitClass(unit)
        return Color[cls or "PALADIN"]
    end)
end

__Static__() __AutoCache__()
function Wow.UnitOwnerColor()
    return Wow.FromUnitEvent(_UnitNameSubject):Next():Map(function(unit)
        local _, cls            = UnitClass(GetUnitOwner(unit))
        return Color[cls or "PALADIN"]
    end)
end

__Static__() __AutoCache__()
function Wow.UnitExtendColor(withThreat)
    local scolor                = Color(1, 1, 1)
    return Wow.FromUnitEvent("UNIT_FACTION", "UNIT_THREAT_SITUATION_UPDATE"):Map(function(unit)
        if not UnitIsPlayer(unit) then
            if UnitIsTapDenied(unit) then return Color.RUNES end

            if withThreat and UnitCanAttack("player", unit) then
                local threat    = UnitThreatSituation("player", unit)
                if threat and threat > 0 then
                    scolor.r, scolor.g, scolor.b = GetThreatStatusColor(threat)
                    return scolor
                end
            end

            scolor.r, scolor.g, scolor.b = UnitSelectionColor(unit, true)
            return scolor
        end
        local _, cls            = UnitClass(unit)
        return Color[cls or "PALADIN"]
    end)
end

-- Unit Level API
__Static__()
function Wow.UnitLevel(format)
    format                      = format or "%s"
    local unknownFormat         = format:gsub("%%%w+", "%%s")
    return Wow.FromUnitEvent(Wow.FromEvent("PLAYER_LEVEL_UP"):Map(function(level) return "player", level end))
        :Map(Scorpio.IsRetail and function(unit, level)
            level               = level or UnitLevel(unit)
            if level and level > 0 then
                if UnitIsWildBattlePet(unit) or UnitIsBattlePetCompanion(unit) then
                    level       = UnitBattlePetLevel(unit)
                end
                return strformat(format, level)
            else
                return strformat(unknownFormat, "???")
            end
        end or function(unit, level)
            level               = level or UnitLevel(unit)
            if level and level > 0 then
                return strformat(format, level)
            else
                return strformat(unknownFormat, "???")
            end
        end)
end

__Static__() __AutoCache__()
function Wow.UnitClassification()
    return Wow.FromUnitEvent("UNIT_CLASSIFICATION_CHANGED"):Map(UnitClassification)
end

__Static__() __AutoCache__()
function Wow.UnitClassificationColor()
    return Wow.UnitClassification():Map(function(class)
        if class == "elite" then
            return Color.YELLOW
        elseif class == "rare" then
            return Color.WHITE
        elseif class == "rareelite" then
            return Color.CYAN
        elseif class == "worldboss" then
            return Color.RAGE
        else
            return Color.NORMAL
        end
    end)
end

__Static__() __Arguments__{ ColorType/nil }
function Wow.UnitLevelColor(default)
    return Wow.FromUnitEvent(Wow.FromEvent("PLAYER_LEVEL_UP"):Map(function(level) return "player", level end))
        :Map(function(unit, level)
            if not level and UnitCanAttack("player", unit) then
                return GetQuestDifficultyColor(UnitLevel(unit) or 99)
            end
            return default or Color.NORMAL
        end)
end

__Static__() __AutoCache__()
function Wow.UnitTotem()
    return Wow.FromUnitEvent(Wow.FromEvent("PLAYER_TOTEM_UPDATE"):Map("=>'player'"))
end

-- Unit State API
__Static__() __AutoCache__()
function Wow.UnitIsDisconnected()
    return Wow.FromUnitEvent("UNIT_HEALTH", "UNIT_CONNECTION"):Next():Map(function(unit) return not UnitIsConnected(unit) end)
end

__Static__() __AutoCache__()
function Wow.UnitIsTarget()
    return Wow.FromUnitEvent(Wow.FromEvent("PLAYER_TARGET_CHANGED"):Map("=>'any'")):Map(function(unit) return UnitIsUnit(unit, "target") end)
end

__Static__() __AutoCache__()
function Wow.UnitIsPlayer()
    return Wow.FromUnitEvent(_UnitNameSubject):Map(function(unit) return UnitIsUnit(unit, "player") end)
end

__Static__() __AutoCache__()
function Wow.UnitNotPlayer()
    return Wow.FromUnitEvent(_UnitNameSubject):Map(function(unit) return not UnitIsUnit(unit, "player") end)
end

__Static__() __AutoCache__()
function Wow.PlayerInCombat()
    local subject               = BehaviorSubject()
    Continue(function(subject)
        while true do
            local status        = UnitAffectingCombat("player") or false
            subject:OnNext(status)

            NextEvent(status and "PLAYER_REGEN_ENABLED" or "PLAYER_REGEN_DISABLED")
        end
    end, subject)
    return subject
end

__Static__() __AutoCache__()
function Wow.UnitIsResurrect()
    local subject               = Subject()

    Wow.FromEvent("INCOMING_RESURRECT_CHANGED"):Next():Subscribe(function(unit)
        subject:OnNext(unit)

        if UnitHasIncomingResurrection(unit) then
            Next(function()
                -- Avoid the event not fired when the unit is already resurrected
                while UnitHasIncomingResurrection(unit) do Delay(1) end
                subject:OnNext(unit)
            end)
        end
    end)

    return Wow.FromUnitEvent(subject):Map(function(unit) return UnitHasIncomingResurrection(unit) end)
end

__Static__() __AutoCache__()
function Wow.UnitRaidTargetIndex()
    return Wow.FromUnitEvent(Wow.FromEvent("RAID_TARGET_UPDATE"):Map("=>'any'")):Map(GetRaidTargetIndex)
end

__Static__() __AutoCache__()
function Wow.UnitThreatLevel()
    return Wow.FromUnitEvent("UNIT_THREAT_SITUATION_UPDATE"):Map(function(unit)
        return UnitIsPlayer(unit) and UnitThreatSituation(unit) or 0
    end)
end

__Static__() __AutoCache__()
function Wow.UnitGroupRoster()
    return Wow.FromUnitEvent(_UnitNameSubject):Map(function(unit)
        if IsInRaid() and not UnitHasVehicleUI(unit) then
            if GetPartyAssignment('MAINTANK', unit) then
                return "MAINTANK"
            elseif GetPartyAssignment('MAINASSIST', unit) then
                return "MAINASSIST"
            else
                return "NONE"
            end
        else
            return "NONE"
        end
    end)
end

__Static__() __AutoCache__()
function Wow.UnitGroupRosterVisible()
    return Wow.UnitGroupRoster():Map(function(assign) return assign and assign ~= "NONE" or false end)
end

__Static__() __AutoCache__()
function Wow.UnitRole()
    return Wow.FromUnitEvent(Wow.FromEvent("GROUP_ROSTER_UPDATE", "PLAYER_ROLES_ASSIGNED"):Map("=>'any'")):Map(UnitGroupRolesAssigned or Toolset.fakefunc)
end

__Static__() __AutoCache__()
function Wow.UnitOwnerRole()
    return Wow.FromUnitEvent(Wow.FromEvent("GROUP_ROSTER_UPDATE", "PLAYER_ROLES_ASSIGNED"):Map("=>'any'")):Map(GetUnitOwner):Map(UnitGroupRolesAssigned or Toolset.fakefunc)
end

__Static__() __AutoCache__()
function Wow.UnitRoleVisible()
    return Wow.UnitRole():Map(function(role) return role and role ~= "NONE" or false end)
end

__Static__() __AutoCache__()
function Wow.UnitIsLeader()
    return Wow.FromUnitEvent(Wow.FromEvent("GROUP_ROSTER_UPDATE", "PARTY_LEADER_CHANGED"):Map("=>'any'")):Map(function(unit)
        return (UnitInParty(unit) or UnitInRaid(unit)) and UnitIsGroupLeader(unit) or false
    end)
end

__Static__() __AutoCache__()
function Wow.UnitInRange()
    return Wow.UnitTimer():Map(function(unit)
        return UnitExists(unit) and (UnitIsUnit(unit, "player") or not (UnitInParty(unit) or UnitInRaid(unit)) or UnitInRange(unit))
    end)
end

__Static__() __AutoCache__()
function Wow.UnitOwnerInRange()
    return Wow.UnitTimer():Map(function(unit)
        unit                    = GetUnitOwner(unit)
        return UnitExists(unit) and (UnitIsUnit(unit, "player") or not (UnitInParty(unit) or UnitInRaid(unit)) or UnitInRange(unit))
    end)
end

------------------------------------------------------------
--                      READY CHECK                       --
------------------------------------------------------------
local _ReadyChecking            = 0
local _ReadyCheckSubject        = Subject()
local _ReadyCheckConfirmSubject = Subject()
local _ReadyCheckingCache       = {}

__SystemEvent__() __Async__()
function READY_CHECK()
    wipe(_ReadyCheckingCache)

    _ReadyChecking              = 1
    _ReadyCheckSubject:OnNext("any")
    _ReadyCheckConfirmSubject:OnNext("any")

    if "READY_CHECK_FINISHED" == Wait("READY_CHECK_FINISHED", "PLAYER_REGEN_DISABLED") then
        _ReadyChecking          = 2
        _ReadyCheckConfirmSubject:OnNext("any")

        Wait(11, "PLAYER_REGEN_DISABLED")
    end

    _ReadyChecking              = 0
    _ReadyCheckSubject:OnNext("any")
end

__SystemEvent__()
function READY_CHECK_CONFIRM()
    _ReadyCheckConfirmSubject:OnNext("any")
end

__Static__() __AutoCache__()
function Wow.UnitReadyCheckVisible()
    return Wow.FromUnitEvent(_ReadyCheckSubject):Map(function(unit) return _ReadyChecking > 0 and UnitGUID(unit) ~= nil end)
end

__Static__() __AutoCache__()
function Wow.UnitReadyCheck()
    return Wow.FromUnitEvent(_ReadyCheckConfirmSubject):Map(function(unit)
        if _ReadyChecking == 0 then return end

        local guid              = UnitGUID(unit)
        if not guid then return "notready" end

        local state             = GetReadyCheckStatus(unit) or _ReadyCheckingCache[guid]
        _ReadyCheckingCache[guid]= state
        return _ReadyChecking == 2 and state == "waiting" and "notready" or state
    end)
end

------------------------------------------------------------
--                      Wow Classic                       --
------------------------------------------------------------
if Scorpio.IsRetail then return end

_Parent.UnitHasVehicleUI        = _G.UnitHasVehicleUI     or Toolset.fakefunc
_Parent.GetThreatStatusColor    = _G.GetThreatStatusColor or function (index) if index == 3 then return 1, 0, 0 elseif index == 2 then return 1, 0.6, 0 elseif index == 1 then return 1, 1, 0.47 else return 0.69, 0.69, 0.69 end end