--[[---------------------------------------------------------------------------
    Name: phRaid
    Author: pherl <pherl@live.com>
-----------------------------------------------------------------------------]]

local L = AceLibrary("AceLocale-2.2"):new("phRaid")
phRaid = AceLibrary("AceAddon-2.0"):new(
    "AceConsole-2.0",
    "AceDB-2.0",
    "AceEvent-2.0",
    "AceDebug-2.0",
    "AceModuleCore-2.0"
)

phRaid:RegisterDB("phRaidDB", "phRaidDBPC")

function phRaid:OnInitialize()
    self.frames = {}
    self.visible = {}
    self.rosterNames = {}

    self.main = self:CreateMainFrame("phRaidMainFrame")
    self.regions, self.regionFrames = self:CreateRegionFrames()
    self.mini = self:CreateMiniButton()

    local profile = {
        Sort = "CLASS",
        MiniPos = 155,
        ShowMini = true,
        ShowTooltip = true,
        HideWhenCombat = true,
        ClickHeal = true,
        Lock = true,
        AltDragAlways = true,
        FrameStyle = {
            BarTexture = "halcyone",
            BarWidth = 85,
            StatusWidth = 60,
            NameWidth = 85,
            HPBarHeight = 11,
            MPBarHeight = 2,
            Layout = L["Double Lines Mode"],
            RightToLeft = false,
            ShowGroup = true,
            GroupGap = 15,
            NameOnBar = false,
            ShowMana = true,
            EnableAllTypeMP = false,
            HPTextType = L["Show Deficit"],
            BuffSize = 13,
        },
        Regions = {}
    }
    for i = 1, self.MAXREGION do
        profile.Regions[i] = {
            Sort = tostring(math.mod(i - 1, 8) + 1),
            Scale = 1,
            Columns = 3,
            ColumnGap = 40,
            NoBreak = true,
        }
    end
    self:RegisterDefaults("profile", profile)
    self.dewdrop = AceLibrary("Dewdrop-2.0")
    self.dewdrop:Register(self.main, "children",
        function(level, value) self.dewdrop:FeedAceOptionsTable(self.options) end,
        "dontHook", true
    )
    phRaid:RegisterChatCommand({"/phraid"}, self.options)
end

function phRaid:OnEnable()
    self:Print(L["WelcomeMsg"])
    self:Print(L["AltDrag"])
    self:Print(L["AltConfig"])

    self:RegisterEvent("CHAT_MSG_SYSTEM")
    self:RegisterBucketEvent("RAID_ROSTER_UPDATE", 0.5)
    self:RegisterBucketEvent("UNIT_HEALTH", 0.2)
    self:RegisterBucketEvent("UNIT_MAXHEALTH", 0.2)
    self:RegisterBucketEvent("UNIT_MANA", 0.2)
    self:RegisterBucketEvent("UNIT_MAXMANA", 0.2)
    
    if self.db.profile.FrameStyle.EnableAllTypeMP then
        self:RegisterBucketEvent("UNIT_RAGE", 0.2, "UNIT_MANA")
        self:RegisterBucketEvent("UNIT_ENERGY", 0.2, "UNIT_MANA")
        self:RegisterBucketEvent("UNIT_MAXRAGE", 0.2, "UNIT_MAXMANA")
        self:RegisterBucketEvent("UNIT_MAXENERGY", 0.2, "UNIT_MAXMANA")
    end

    self:UpdateSortFunctions()
    self.main:Show()
    for i = 1, self.MAXREGION do
        self.regionFrames[i]:SetScale(self.db.profile.Regions[i].Scale)
    end
    self:RestorePosition()
    self:MiniUpdatePosition()
    self:RAID_ROSTER_UPDATE()
end

function phRaid:OnDisable()
    self.main:Hide()
end
--[[---------------------------------------------------------------------------
    Main event handle
-----------------------------------------------------------------------------]]
function phRaid:CHAT_MSG_SYSTEM()
    if string.find(arg1, ERR_RAID_YOU_LEFT) then
        self:RAID_ROSTER_UPDATE()
    end
end

function phRaid:RAID_ROSTER_UPDATE()
    self:UpdateUnitFrames()
    self:UpdateLayout()
    self:TriggerEvent("PHRAID_ROSTER_UPDATE")
end

local newvisible, addlist, workframes, backframes = {}, {}, {}, {}
function phRaid:UpdateUnitFrames()
    local player = UnitName("player")
    local frames = self.frames
    for k = 1, 40 do
        local unit = self.RAIDUNIT[k]
        --local name, rank, subgroup, level, class, fileName, zone, online, isDead, role, isML = GetRaidRosterInfo(raidIndex)
        local name, _, group, _, _, class = GetRaidRosterInfo(k)
        if not name then break end

        if player == name then
            self.mygroup = group
        end

        local f = frames[unit]
        local oldunit = self.rosterNames[name]
        if not oldunit or frames[oldunit].group ~= group then -- Units To Add Or Modify
            addlist[name] = {k, unit, class, group}
        elseif frames[oldunit] and frames[oldunit].name == name then -- Units To Keep
            workframes[unit] = frames[oldunit]
            workframes[unit].id = k
            workframes[unit].unit = unit
            self.rosterNames[name] = unit
            frames[oldunit] = nil
            newvisible[unit] = self.visible[oldunit]
        else -- Incase Roster Update during function call
            self:Debug("Roster Update during function call")
        end
    end
    for unit, frame in pairs(frames) do -- Units To Delete
        if self.visible[unit] then
            self.regions[self.visible[unit]].groups[frame.sortGroup][frame.name] = nil
            self.regions[self.visible[unit]].numToShow = self.regions[self.visible[unit]].numToShow - 1
        end
        frame:Hide()
        frame.fd = nil
        frame.redem = nil
        table.insert(backframes, frame)
        self.rosterNames[frame.name] = nil
        frames[unit] = nil
    end
    for name, info in pairs(addlist) do -- Units Add
        local id, unit, class, group = unpack(info)
        local frame = table.remove(backframes) -- Pop one frame from unused frame-pool
        if not frame then frame = self:CreateOneUnitFrame(self.main) end -- If the pool is empty, we create one
        frame.id = id
        frame.unit = unit
        self:LinkFrameToUnit(frame, unit, name, class, group)
        workframes[unit] = frame
        self.rosterNames[name] = unit
        addlist[name] = nil
        local i, j = self:GetUnitPosition(unit, class, group)
        if i then
            frame:Show()
            newvisible[unit] = i
            frame:SetParent(self.regionFrames[i])
            frame.sortGroup = j
            self.regions[i].numToShow = self.regions[i].numToShow + 1
            if not self.regions[i].groups[j] then self.regions[i].groups[j] = {} end
            self.regions[i].groups[j][name] = frame
        else
            frame:Hide()
        end
    end
    self:EraseTable(self.visible)
    self.frames, workframes = workframes, self.frames
    self.visible, newvisible = newvisible, self.visible
    self:UNIT_MAXHEALTH(self.visible)
    self:UNIT_MAXMANA(self.visible)
end

function phRaid:GetUnitPosition(unit, class, group)
    for i, reg in ipairs(self.regions) do
        for j, func in ipairs(reg.sortFunctions) do
            if func(unit, class, group) then return i, j end
        end
    end
end

function phRaid:UpdateVisibility()
    self:EraseTable(self.visible)
    for i = 1, self.MAXREGION do
        self.regions[i].numToShow = 0
        self:EraseTable(self.regions[i].groups)
    end
    for i = 1, 40 do
        local unit = self.RAIDUNIT[i]
        local f = self.frames[unit]
        if not f then break end
        local regIndex, groupIndex = self:GetUnitPosition(unit, f.class, f.group)
        if regIndex then
            self.visible[unit] = regIndex
            local reg = self.regions[regIndex]
            if not reg.groups[groupIndex] then reg.groups[groupIndex] = {} end
            reg.groups[groupIndex][f.name] = f
            reg.numToShow = reg.numToShow + 1
            f.sortGroup = groupIndex
            f:SetParent(self.regionFrames[regIndex])
            f:Show()
        else
            f:Hide()
        end
    end
    self:UNIT_MAXHEALTH(self.visible)
    self:UNIT_MAXMANA(self.visible)
end

function phRaid:UpdateLayout()
    for k = 1, self.MAXREGION do
        local reg = self.regions[k]
        if self.moving and self.moving[k] then
            self.regionFrames[k]:StopMovingOrSizing()
        end
        if reg.numToShow ~= 0 then 
            local numInColumn = math.ceil(reg.numToShow / self.db.profile.Regions[k].Columns)
            local IsNoBreak = self.db.profile.Regions[k].NoBreak
            local IsFit = true
            local i = 1
            local indexInColumn = 0
            local prev
            local anchorframe
            for _, group in pairs(reg.groups) do
                local IsNewGroup = true
                local groupSize = self:GetTableSize(group)
                for _, f in pairs(group) do
                    f:ClearAllPoints()
                    if i == 1 then
                        anchorframe = f
                        f:SetPoint("TOPLEFT", self.regionFrames[k], "TOPLEFT")
                    else
                        local IsFit = groupSize <= 2 *(numInColumn - indexInColumn)
                        if IsNoBreak and IsNewGroup and not IsFit or not IsNoBreak and indexInColumn == numInColumn then
                            -- New Column
                            f:SetPoint("LEFT", anchorframe, "RIGHT", self.db.profile.Regions[k].ColumnGap, 0)
                            indexInColumn = 0
                            anchorframe = f
                        elseif IsNewGroup then
                            -- Jump
                            f:SetPoint("TOP", prev, "BOTTOM", 0, - self.db.profile.FrameStyle.GroupGap)
                        else
                            -- Follow
                            f:SetPoint("TOP", prev, "BOTTOM", 0, 0)
                        end
                    end
                    prev = f
                    i = i + 1
                    indexInColumn = indexInColumn + 1
                    IsNewGroup = false
                end
            end
        end
    end
end

function phRaid:UNIT_HEALTH(units)
    for unit in pairs(units) do
        if self.frames[unit] and self.visible[unit] and UnitExists(unit) then
            local f = self.frames[unit]
            local hp = UnitIsConnected(unit) and UnitHealth(unit) or 0
            local hpmax = UnitHealthMax(unit)
            -- To hack API bug
            if hpmax < hp then hpmax = hp end
            local hpp = (hpmax ~= 0) and hp / hpmax or 0
            local deficit = hp - hpmax
            local status
            f.unavail = nil
            f.fd = nil
            if UnitIsDead(unit) then
                if f.class == "HUNTER" and UnitBuff(unit, 1) then f.fd = true
                else status = "|cffff0000" .. L["Dead"] .."|r" end
            elseif UnitIsGhost(unit) then status = "|cff9d9d9d" .. L["Ghost"] .. "|r"
            elseif not UnitIsConnected(unit) then status = "|cffff8000" .. L["Offline"] .. "|r" end
            if status then
                f.unavail = true
                f.HPBar:SetValue(hpmax)
                f.HPBar:SetStatusBarColor(0.6, 0.6, 0.6, 0.4)
                f.HPBG:SetVertexColor(0, 1, 0, 0.2)
                f.HP:SetText(status)
                f.Status:SetText("")
                f.Highlight:SetVertexColor(0, 0, 0)
                f.Highlight:Hide()
            elseif f.fd then
                self:SetFeignDeath(f)
            elseif f.impd then
                self:SetImpDying(f)
            else
                f.HPBar:SetValue(hp)
                local r, g, b = self:GetHPColor(hpp)
                f.HPBar:SetStatusBarColor(r, g, b, 1.0)
                f.HPBG:SetVertexColor(0.2, 0.6, 0.2, 0.6)
                local textType = self.db.profile.FrameStyle.HPTextType
                if textType == L["Do Not Show"] or deficit == 0 then
                    f.HP:SetText("")
                elseif textType == L["Show Percent"] then
                    f.HP:SetText(math.floor(hpp * 100) .. "%")
                else
                    f.HP:SetText(tostring(deficit))
                end
            end
        end
    end
end

function phRaid:UNIT_MANA(units)
    for unit in pairs(units) do
        if self.frames[unit] and self.visible[unit] then
            self.frames[unit].MPBar:SetValue(UnitMana(unit))
        end
    end
end

function phRaid:UNIT_MAXHEALTH(units)
    for unit in pairs(units) do
        if self.frames[unit] and self.visible[unit] then
            self.frames[unit].HPBar:SetMinMaxValues(0, UnitHealthMax(unit))
        end
    end
    self:UNIT_HEALTH(units)    
end

function phRaid:UNIT_MAXMANA(units)
    for unit in pairs(units) do
        if self.frames[unit] and self.visible[unit] then
            self.frames[unit].MPBar:SetMinMaxValues(0, UnitManaMax(unit))
        end
    end
    self:UNIT_MANA(units)
end

function phRaid:SetFeignDeath(f)
    f.HPBar:SetValue(UnitHealthMax(f.unit))
    f.HPBar:SetStatusBarColor(0, 0.9, 0.78)
    f.HP:SetText(L["Feign Death"])
end

function phRaid:SetImpDying(f)
    f.HPBar:SetValue(UnitHealthMax(f.unit))
    f.HPBar:SetStatusBarColor(0.6, 0, 1)
    f.HP:SetText(L["Imp. Dying"])
end

--[[---------------------------------------------------------------------------
    Show and Sort Functions
-----------------------------------------------------------------------------]]

function phRaid:ParseStringToFunctions(value)
    if not value then return {} end
    value = string.upper(value)
    local funcs = {}
    for token in string.gfind(value, "[^%s]+") do
        if self.CLASSNAME[token] then token = self.CLASSNAME[token] end
        if self.FUNCS[token] then
            table.insert(funcs, self.FUNCS[token])
        else
            self:Print(string.format("%s: '|cffff0000%s|r' %s", L["Token"], token, L["Incorrect"]))
            local s = {}
            local output = ""
            for k, v in pairs(self.CLASSNAME) do
                table.insert(s, k)
            end
            for k, v in pairs(self.FUNCS) do 
                table.insert(s, k)
            end
            table.sort(s)
            for k, v in ipairs(s) do
                output = string.format("%s%s | ", output, v)
            end
            self:Print(L["ValidToken"])
            self:Print(output)
            return nil
        end
    end  
    return funcs
end

function phRaid:UpdateSortFunctions()
    local sortType = self.db.profile.Sort
    if sortType == "CUSTOM" then
        for i = 1, self.MAXREGION do
            local funcs = self:ParseStringToFunctions(self.db.profile.Regions[i].Sort)
            if not funcs then funcs = {} end
            self.regions[i].sortFunctions = funcs
        end
    elseif sortType == "GROUP" then
        self.regions[1].sortFunctions = {}
        for i = 1, 8 do
            table.insert(self.regions[1].sortFunctions, self.FUNCS[tostring(i)])
        end
        for i = 2, self.MAXREGION do
            self.regions[i].sortFunctions = {}
        end
    else -- sortType == "CLASS"
        self.regions[1].sortFunctions = {
            self.FUNCS.WARRIOR, self.FUNCS.PRIEST, self.FUNCS.MAGE, self.FUNCS.ROGUE, self.FUNCS.DRUID,
            self.FUNCS.PALADIN, self.FUNCS.SHAMAN, self.FUNCS.WARLOCK, self.FUNCS.HUNTER,
        }
        for i = 2, self.MAXREGION do
            self.regions[i].sortFunctions = {}
        end
    end
end
--[[---------------------------------------------------------------------------
    Utility Functions
-----------------------------------------------------------------------------]]
function phRaid:EraseTable(t)
    if not t or type(t) ~= "table" then return end
    for i, v in pairs(t) do t[i] = nil end
    if table.setn then table.setn(t, 0) end
end
function phRaid:GetTableSize(t)
    local i = 0
    for _,_ in pairs(t) do
        i = i + 1
    end
    return i
end
function phRaid:CopyTable(from, to)
    phRaid:EraseTable(to)
    if from and type(from) == "table" then
        for key, value in pairs(from) do
            if type(value) == "table" then 
                to[key] = {}
                self:CopyTable(value, to[key])
            else
                to[key] = value
            end
        end
        table.setn(to, table.getn(from))
    end
end
