--[[
    This file is part of Decursive.

    Decursive (v 2.7.8.3) add-on for World of Warcraft UI
    Copyright (C) 2006-2019 John Wellesz (Decursive AT 2072productions.com) ( http://www.2072productions.com/to/decursive.php )

    Decursive 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 3 of the License, or
    (at your option) any later version.

    Decursive 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 Decursive.  If not, see <https://www.gnu.org/licenses/>


    Decursive is inspired from the original "Decursive v1.9.4" by Patrick Bohnet (Quu).
    The original "Decursive 1.9.4" is in public domain ( www.quutar.com )

    Decursive is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY.

    This file was last updated on 2020-03-08T20:44:24Z

--]]
-------------------------------------------------------------------------------

local addonName, T = ...;
-- big ugly scary fatal error message display function {{{
if not T._FatalError then
-- the beautiful error popup : {{{ -
StaticPopupDialogs["DECURSIVE_ERROR_FRAME"] = {
    text = "|cFFFF0000Decursive Error:|r\n%s",
    button1 = "OK",
    OnAccept = function()
        return false;
    end,
    timeout = 0,
    whileDead = 1,
    hideOnEscape = 1,
    showAlert = 1,
    preferredIndex = 3,
    }; -- }}}
T._FatalError = function (TheError) StaticPopup_Show ("DECURSIVE_ERROR_FRAME", TheError); end
end
-- }}}
if not T._LoadedFiles or not T._LoadedFiles["Dcr_DebuffsFrame.xml"] or not T._LoadedFiles["Dcr_DebuffsFrame.lua"] then
    if not DecursiveInstallCorrupted then T._FatalError("Decursive installation is corrupted! (Dcr_DebuffsFrame.xml or Dcr_DebuffsFrame.lua not loaded)"); end;
    DecursiveInstallCorrupted = true;
    return;
end
T._LoadedFiles["Dcr_LiveList.lua"] = false;

local D   = T.Dcr;

local L     = D.L;
local LC    = D.LC;
local DC    = T._C;

--D.LiveList = OOP.Class();

-- http://www.lua.org/pil/13.4.1.html
-- define the namespace
D.LiveList = {};
local LiveList = D.LiveList;
-- a prototype for LiveList objects, empty, defaults are defined in the :New for better performances
LiveList.prototype = {};
LiveList.metatable ={ __index = LiveList.prototype };

function LiveList:new(...)
    local instance = setmetatable({}, self.metatable);
    instance:init(...);
    return instance;
end

local MicroUnitF = D.MicroUnitF;

LiveList.ExistingPerID      = {};
LiveList.Number             = 0;
LiveList.NumberShown        = 0;
D.ForLLDebuffedUnitsNum     = 0; -- this counts the number of displayed debuffed units. Used to know if the alert sound should/has been played

-- temporary variables often used in function
local Debuff, Debuffs, IsCharmed, MF, i, Index, RangeStatus;

-- local shortcuts to often called global functions
local pairs             = _G.pairs;
local ipairs            = _G.ipairs;
local next              = _G.next;
local select            = _G.select;
local unpack            = _G.unpack;
local table             = _G.table;
local UnitExists        = _G.UnitExists;
local IsSpellInRange    = D.IsSpellInRange;
local UnitClass         = _G.UnitClass;
local UnitIsFriend      = _G.UnitIsFriend;
local UnitGUID          = _G.UnitGUID;
local floor             = _G.math.floor;
local str_upper         = _G.string.upper;
local GetRaidTargetIndex= _G.GetRaidTargetIndex;
local t_wipe            = _G.table.wipe;


-- defines what is printed when the object is read as a string
function LiveList:ToString() -- {{{
    return "Decursive Live-List object";
end -- }}}

-- The Factory for LiveList objects
function LiveList:Create() -- {{{

    if self.Number >= D.profile.Amount_Of_Afflicted then
        return false;
    end

    self.ExistingPerID[self.Number + 1] = self:new(DcrLiveList, self.Number + 1);

    self.Number = self.Number + 1;

    return self.ExistingPerID[self.Number];

end -- }}}

function LiveList:PreCreate()
    if D.Status.Combat or self.Number >= D.profile.Amount_Of_Afflicted then
        return;
    end

    D:Debug("(LiveList) Precreating LL item");

    self.ExistingPerID[self.Number + 1] = self:new(DcrLiveList, self.Number + 1);

    self.Number = self.Number + 1;
    D:Debug("done");

    LiveList:PreCreate();

end

function LiveList:DisplayItem (ID, UnitID, Debuff) -- {{{

    --D:Debug("(LiveList) Displaying LVItem %d for UnitID %s", ID, UnitID);
    local LVItem = false;

    if ID > self.Number + 1 then -- sanity check
        return error(("LiveList:DisplayItem: bad argument #1 'ID (= %d)' must be < LiveList.Number + 1 (LiveList.Number = %d) UnitID was %s, Amount_Of_Afflicted 2disp: %d"):format(ID, self.Number, UnitID, D.profile.Amount_Of_Afflicted), 2);
    end

    if not self.ExistingPerID[ID] then
        LVItem = self:Create();
    else
        LVItem = self.ExistingPerID[ID];
    end

    if not LVItem then
        return false;
    end

    if not Debuff then
        Debuff = D.ManagedDebuffUnitCache[UnitID][1];
    end

    LVItem:SetDebuff(UnitID, Debuff, nil);
    --D:Debug("XXXX => Updating ll item %d for %s", ID, UnitID);

    if not LVItem.IsShown then
        --[==[@debug@--
        D:Debug("(LiveList) Showing LVItem %d", ID);
        --@end-debug@]==]

        LVItem.Frame:Show();

        --[==[@debug@--
        D:Debug("(LiveList) done", ID);
        --@end-debug@]==]

        self.NumberShown = self.NumberShown + 1;
        LVItem.IsShown = true;
    end

end -- }}}

function LiveList:RestAllPosition () -- {{{
    for _, LVitem in ipairs(self.ExistingPerID) do
        LVitem.Frame:ClearAllPoints();
        LVitem.Frame:SetPoint(LVitem:GiveAnchor());
    end
end -- }}}

function LiveList.prototype:GiveAnchor() -- {{{

    local ItemHeight = self.Frame:GetHeight();

    if self.ID == 1 then
        if D.profile.ReverseLiveDisplay then
            return "BOTTOMLEFT", DecursiveMainBar, "BOTTOMLEFT", 5, -1 * (ItemHeight + 1) * D.profile.Amount_Of_Afflicted;
        else
            return "TOPLEFT", DecursiveMainBar, "BOTTOMLEFT", 5, 0;
        end
    else
        if D.profile.ReverseLiveDisplay then
            return "BOTTOMLEFT", LiveList.ExistingPerID[self.ID - 1].Frame, "TOPLEFT", 0, 1;
        else
            return "TOPLEFT", LiveList.ExistingPerID[self.ID - 1].Frame, "BOTTOMLEFT", 0, -1;
        end
    end

end -- }}}


function LiveList.prototype:init(Container,ID) -- {{{

    --LiveList.super.prototype.init(self); -- needed
    D:Debug("(LiveList) Initializing LiveList object '%s'", ID);

    --ObjectRelated
    self.ID                 = ID;
    self.IsShown            = false;
    self.Parent             = Container;

    --Debuff info
    self.UnitID             = false;
    self.UnitName           = false;
    self.RaidTargetIndex    = false;
    self.PrevUnitName       = false;
    self.PrevUnitID         = false;
    self.PrevRaidTargetIndex= false;
    self.UnitClass          = false;

    self.Debuff             = {};

    self.PrevDebuffIndex    = false;
    self.PrevDebuffName     = false;
    self.PrevDebuffTypeName    = false;
    self.PrevDebuffApplicaton  = false;
    self.PrevDebuffTexture  = false;

    self.IsCharmed          = false;
    self.PrevIsCharmed      = false;

    self.Alpha              = false;

    -- Create the frame
    self.Frame = CreateFrame ("Button", "DcrLiveListItem"..ID, self.Parent, "DcrLVItemTemplate");

    -- Set some basic properties
    self.Frame:SetFrameStrata("LOW");
    self.Frame:RegisterForClicks("AnyDown");

    -- Set the anchor of this item
    self.Frame:SetPoint(self:GiveAnchor());

    -- create the background
    self.BackGroundTexture = self.Frame:CreateTexture("DcrLiveListItem"..ID.."BackTexture", "BACKGROUND", "DcrLVBackgroundTemplate");

    -- Create the Icon Texture
    self.IconTexture = self.Frame:CreateTexture("DcrLiveListItem"..ID.."Icon", "ARTWORK", "DcrLVIconTemplate");

    -- Create the Debuff application count font string
    self.DebuffAppsFontString = self.Frame:CreateFontString("DcrLiveListItem"..ID.."Count", "OVERLAY", "DcrLLAfflictionCountFont");

    -- Create the character name Fontstring
    self.UnitNameFontString = self.Frame:CreateFontString("DcrLiveListItem"..ID.."UnitName", "OVERLAY", "DcrLLUnitNameFont");

    -- Create the unitID Fontstring
    self.UnitIDFontString = self.Frame:CreateFontString("DcrLiveListItem"..ID.."UnitID", "OVERLAY", "DcrLLUnitIDFont");
    --self.UnitIDFontString:SetHeight(3);

    -- Create the debuff type fontstring
    self.DebuffTypeFontString = self.Frame:CreateFontString("DcrLiveListItem"..ID.."Type", "OVERLAY", "DcrLLDebuffTypeFont");

    -- Create the Raid Target Icon Texture
    self.RaidIconTexture = self.Frame:CreateTexture("DcrLiveListItem"..ID.."RaidIcon", "ARTWORK", "DcrLVRaidIconTemplate");

    -- Create the debuff name fontstring
    self.DebuffNameFontString = self.Frame:CreateFontString("DcrLiveListItem"..ID.."Name", "OVERLAY", "DcrLLDebuffNameFont");


    -- a reference to this object
    self.Frame.Object = self;

end -- }}}

function LiveList.prototype:SetDebuff(UnitID, Debuff, IsCharmed) -- {{{
    self.UnitID             = UnitID;
    self.UnitName           = D:PetUnitName(UnitID, true);
    self.Debuff             = Debuff;
    self.IsCharmed          = IsCharmed;
    self.RaidTargetIndex    = GetRaidTargetIndex(UnitID);

    if D.profile.LiveListAlpha ~= self.Alpha then
        self.Frame:SetAlpha(D.profile.LiveListAlpha);
        self.Alpha = D.profile.LiveListAlpha;
    end

    -- Set the graphical elements to the right values
    -- Icon
    if self.PrevDebuffTexture ~= Debuff.Texture then
        self.IconTexture:SetTexture(Debuff.Texture);
        self.PrevDebuffTexture =  Debuff.Texture;
    end

    -- Raid Icon
    if self.PrevRaidTargetIndex ~= self.RaidTargetIndex then
        self.RaidIconTexture:SetTexture(self.RaidTargetIndex and DC.RAID_ICON_TEXTURE_LIST[self.RaidTargetIndex] or nil);
        self.PrevRaidTargetIndex = self.RaidTargetIndex;
    end

    -- Applications count
    if self.PrevDebuffApplicaton ~= Debuff.Applications then
        if (Debuff.Applications > 1) then
            self.DebuffAppsFontString:SetText(Debuff.Applications);
            self.PrevDebuffApplicaton = Debuff.Applications;
        else
            self.DebuffAppsFontString:SetText(" ");
            self.PrevDebuffApplicaton = " ";
        end
    end

    -- Unit Name
    if self.PrevUnitName ~= self.UnitName then
        self.UnitClass = (select(2, UnitClass(UnitID)));
        self.UnitNameFontString:SetText(self.UnitName);
        if self.UnitClass then
            self.UnitNameFontString:SetTextColor(unpack(DC.ClassesColors[self.UnitClass]));
        end
        self.PrevUnitName =  self.UnitName;
        --D:Debug("(LiveList) Updating %d with %s", self.ID, UnitID);
    end

    -- Unit ID
    if self.PrevUnitID ~= UnitID then
        self.UnitIDFontString:SetText("( "..UnitID.." )");
        self.PrevUnitID = UnitID;
    end

    -- Debuff Type Name
    --if self.PrevDebuffTypeName ~= Debuff.TypeName then
        if Debuff.Type then
            self.DebuffTypeFontString:SetText(D:ColorText(L[str_upper(Debuff.TypeName)], D.profile.TypeColors[Debuff.Type] ));
            --self.DebuffTypeFontString:SetTextColor(D.profile.TypeColors[Debuff.Type]);
        else
            self.DebuffTypeFontString:SetText("Unknown");
        end
        self.PrevDebuffTypeName = Debuff.TypeName;
    --end

    -- Debuff Name
    if self.PrevDebuffName ~= Debuff.Name then
        self.DebuffNameFontString:SetText(Debuff.Name);
        self.PrevDebuffName = Debuff.Name;
    end

end -- }}}


function LiveList:GetDebuff(UnitID) -- {{{
    --  (note that this function is only called for the mouseover and target if the MUFs are active)

    if (UnitID == "target" or UnitID == "mouseover") and (not UnitIsFriend(UnitID, "player") or not UnitExists(UnitID)) then
        if D.ManagedDebuffUnitCache[UnitID] and D.ManagedDebuffUnitCache[UnitID][1] then
            t_wipe(D.ManagedDebuffUnitCache[UnitID]); -- clear target/mouseover debufs, else it would stay on
            if D.UnitDebuffed[UnitID] then
                D.ForLLDebuffedUnitsNum = D.ForLLDebuffedUnitsNum - 1;
                D.UnitDebuffed[UnitID] = false;
            end
        end
        --D:Debug("(LiveList) GetDebuff() |cFF00DDDDcanceled|r, unit %s is hostile or gone.", UnitID);
        return false;
    end

    -- Get the unit Debuffs
    if not D.profile.ShowDebuffsFrame or not MicroUnitF.UnitToMUF[UnitID] or UnitID == "mouseover" or UnitID == "target" then
        --D:Debug("(|cff00ff00LiveList|r) Getting Debuff for ", UnitID, Debuffs, IsCharmed);
        Debuffs, IsCharmed = D:UnitCurableDebuffs(UnitID);
        --Debuffs, IsCharmed = D:UnitCurableDebuffs(UnitID, true);
    else -- The MUFs are active and Unit is not mouseover and is not target
        MF = MicroUnitF.UnitToMUF[UnitID];
        if MF then
            Debuffs = MF.Debuffs;
        end
    end

    return D.UnitDebuffed[UnitID];
end -- }}}

function LiveList:DelayedGetDebuff(UnitID) -- {{{
    if not D:DelayedCallExixts("Dcr_GetDebuff"..UnitID) then
        D.DebuffUpdateRequest = D.DebuffUpdateRequest + 1;
        D:Debug("LiveList: GetDebuff scheduled for, ", UnitID);
        D:ScheduleDelayedCall("Dcr_GetDebuff"..UnitID, self.GetDebuff, (D.profile.ScanTime / 3) * (1 + D.DebuffUpdateRequest / 30), self, UnitID);
    end
end -- }}}

local IndexOffset = 0; -- used when target and/or mouseover are found
local DebuffedUnitsNumber = 0;
local _;
function LiveList:Update_Display() -- {{{

    if not D.DcrFullyInitialized  then
        return;
    end

    --
    self:PreCreate();

    Index = 0;

    if D.profile.ShowDebuffsFrame and D.profile.LV_OnlyInRange then -- The MUFs are here and we test for range
        DebuffedUnitsNumber = MicroUnitF.UnitsDebuffedInRange;
    else -- the MUFs are not here or we don't test for range
        DebuffedUnitsNumber = D.ForLLDebuffedUnitsNum;
    end

    -- Check the units in order of importance:

    -- First the Target
    if D.Status.TargetExists and not D.Status.Unit_Array_GUIDToUnit[UnitGUID("target")] and self:GetDebuff("target") then -- TargetExists implies that the unit is a friend
        Index = Index + 1;
        self:DisplayItem(Index, "target");
        --D:Debug("frenetic target update");

        if D.profile.ShowDebuffsFrame and D.profile.LV_OnlyInRange then
            DebuffedUnitsNumber = DebuffedUnitsNumber + 1;
        end

        if not D.Status.SoundPlayed then
            D:PlaySound ("target", "LV target" );
        end
    end

    -- Then the MouseOver
    if not D.Status.MouseOveringMUF and D.UnitDebuffed["mouseover"] and not D.Status.Unit_Array_GUIDToUnit[UnitGUID("mouseover")] and self:GetDebuff("mouseover") then -- this won't catch new debuff if all debuffs disappeard while overing the unit...
        Index = Index + 1;
        self:DisplayItem(Index, "mouseover");
        --D:Debug("frenetic mouseover update");

        if D.profile.ShowDebuffsFrame and D.profile.LV_OnlyInRange then
            DebuffedUnitsNumber = DebuffedUnitsNumber + 1;
        end

        if not D.Status.SoundPlayed then
            D:PlaySound ("mouseover", "LV mouseover" );
        end
    end

    -- the sound played status is reset here because the live list is able to display target and mouseover units and far away ones...
    if DebuffedUnitsNumber == 0 and D.Status.SoundPlayed then
        D.Status.SoundPlayed = false;
        D:Debug("LiveList:Update_Display(): sound re-enabled");
    end

    IndexOffset = Index;

    -- Then continue with all the remaining units if at least one of them is debuffed
    -- We need this loop because:
    --      1, we have to show an ordered list (always true)
    --      2, we want to test if the unit is in spell range (only if the option is active and the MUFs hidden)
    --      There is no event to do the last and a not simple table.sort() would be needed for the first...
    if DebuffedUnitsNumber > 0 and Index < D.profile.Amount_Of_Afflicted then
        for _, UnitID in ipairs(D.Status.Unit_Array) do
            -- if the unit is debuffed and still exists and is not stealthed check this only if the MUFs engine is not there, redudent tests otherwise...
            if D.UnitDebuffed[UnitID] and UnitExists(UnitID) then

                -- we don't care about range
                if not D.profile.LV_OnlyInRange then
                    Index = Index + 1;
                    self:DisplayItem(Index, UnitID);

                    -- play the sound if not already done
                    if not D.Status.SoundPlayed then
                        D:PlaySound (UnitID, "LV scan NR" );
                    end

                else -- we care about range

                    if D.profile.ShowDebuffsFrame and MicroUnitF.UnitToMUF[UnitID] then
                        RangeStatus = MicroUnitF.UnitToMUF[UnitID].UnitStatus; -- MicroUnitF.UnitToMUF[UnitID] is nil sometimes XXX
                        RangeStatus = (RangeStatus == DC.AFFLICTED or RangeStatus == DC.AFFLICTED_AND_CHARMED) and true or false;
                    else
                        -- Test if the spell we are going to use is in range
                        -- Some time can elaps between the instant the debuff is detected and the instant it is shown.
                        -- Between those instants, a reconfiguration can happen (pet dies or some spells become unavailable)
                        -- So we test before calling this api that we can still cure this debuff type
                        if D.Status.CuringSpells[D.ManagedDebuffUnitCache[UnitID][1].Type] then
                            RangeStatus = IsSpellInRange(D.Status.CuringSpells[D.ManagedDebuffUnitCache[UnitID][1].Type], UnitID);
                        else
                            --[[
                            D:AddDebugText(
                                "LiveList:Update_Display(): couldn't get range, DType:", D.ManagedDebuffUnitCache[UnitID][1].Type,
                                "DTypeName:", D.ManagedDebuffUnitCache[UnitID][1].TypeName,
                                "DName:", D.ManagedDebuffUnitCache[UnitID][1].Name,
                                "MUFs are:", D.profile.ShowDebuffsFrame,
                                "InCombatLockdown():", InCombatLockdown(),
                                "UnitID:", UnitID,
                                "HasSpell", D.Status.HasSpell,

                                "MAGIC:", D.Status.CuringSpells[DC.MAGIC],
                                "ENEMYMAGIC:", D.Status.CuringSpells[DC.ENEMYMAGIC],
                                "CURSE:", D.Status.CuringSpells[DC.CURSE],
                                "POISON:", D.Status.CuringSpells[DC.POISON],
                                "DISEASE:", D.Status.CuringSpells[DC.DISEASE],
                                "CHARMED:", D.Status.CuringSpells[DC.CHARMED]
                            );
                            --]]
                            RangeStatus = 0;
                        end
                        RangeStatus = (RangeStatus and RangeStatus ~= 0) and true or false;
                    end

                    if (RangeStatus) then
                        Index = Index + 1;
                        self:DisplayItem(Index, UnitID);
                        -- play the sound if not already done
                        if not D.Status.SoundPlayed then
                            D:PlaySound (UnitID, "LV R" );
                        end
                    end
                end
            end

            -- don't loop if we reach the max displayed unit num or if all debuffed units have been displayed
            if Index == D.profile.Amount_Of_Afflicted or Index == DebuffedUnitsNumber + IndexOffset then
                break;
            end
        end
    end

    -- reset the sound if no units were displayed
    if not D.profile.ShowDebuffsFrame and Index == 0 and D.Status.SoundPlayed then
        D:Debug("LV: No more unit displayed, sound re-enabled");
        D.Status.SoundPlayed = false; -- re-enable the sound if no more debuff
    end

    -- Hide unneeded Items
    if self.NumberShown > Index then -- if there are more units shown than the actual number of debuffed units
        for i = Index + 1, self.NumberShown do
            if self.ExistingPerID[i] and self.ExistingPerID[i].IsShown then
                --D:Debug("(LiveList) Hidding LVItem %d", i);
                self.ExistingPerID[i].Frame:Hide();
                self.ExistingPerID[i].IsShown = false;
                self.NumberShown = self.NumberShown - 1;
            else
                break;
            end
        end
    end


end -- }}}



function LiveList:DisplayTestItem() -- {{{
    if not self.TestItemDisplayed and D.Status.Unit_Array[1] then
        self.TestItemDisplayed = GetTime();
        for i,unit in ipairs(D.Status.Unit_Array) do
            D:DummyDebuff(unit, "Test item");
        end
    end
end -- }}}

function LiveList:HideTestItem() -- {{{
     self.TestItemDisplayed = false;
     local i = 1;

     for UnitID, Debuffed in pairs(D.UnitDebuffed) do
         if Debuffed then
             D:ScheduleDelayedCall("Dcr_rmt"..i, D.DummyDebuff, i * (D.profile.ScanTime / 3), D, UnitID);
             i = i + 1;
         end
     end

end -- }}}


-- this displays the tooltips of the live-list
function LiveList:DebuffTemplate_OnEnter(frame) --{{{
    if D.profile.AfflictionTooltips and frame.Object.UnitID then
        DcrDisplay_Tooltip:SetOwner(frame, "ANCHOR_CURSOR");
        DcrDisplay_Tooltip:SetUnitDebuff(frame.Object.UnitID,frame.Object.Debuff.index); -- Reported to trigger a "script ran too long" error on 2016-09-13...
        DcrDisplay_Tooltip:Show();
    else
        D:Debug(D.profile.AfflictionTooltips, frame.Object.UnitID);
    end
end --}}}

function LiveList:Onclick() -- {{{
    D:Println(L["HLP_LL_ONCLICK_TEXT"]);
end -- }}}

T._LoadedFiles["Dcr_LiveList.lua"] = "2.7.8.3";
