--========================================================--
--             Scorpio Secure Element Panel               --
--                                                        --
-- Author      :  kurapica125@outlook.com                 --
-- Create Date :  2020/11/12                              --
--========================================================--

--========================================================--
Scorpio           "Scorpio.Secure.SecurePanel"       "1.0.0"
--========================================================--

-----------------------------------------------------------
--              Secure Element Panel Widget              --
-----------------------------------------------------------
__Sealed__()
class "SecurePanel" (function(_ENV)
    inherit "SecureFrame" extend "ICountable"

    -------------------------------------
    -- Secure Manager
    -------------------------------------
    -- Manager Frame
    _ManagerFrame               = SecureFrame("Scorpio_SecurePanel_LayoutMananger", UIParent, "SecureHandlerStateTemplate")
    _ManagerFrame:Hide()

    _ManagerFrame:Execute[[
        Manager = self

        _Panels = newtable()
        _Cache  = newtable()
        _Map    = newtable()

        _Queue  = newtable()

        QueueUpdatePanel = [=[
            local panel     = _Map[self] or self
            local noForce= ...

            if not noForce then
                _Queue[panel] = false
            elseif _Queue[panel] == nil then
                _Queue[panel] = true
            end

            -- Reset the timer
            Manager:SetAttribute("state-timer", "reset")
        ]=]

        UnQueueUpdatePanel = [=[
            local panel     = _Map[self] or self
            _Queue[panel]   = nil
        ]=]

        UpdatePanelSize = [=[
            local noForce   = ...
            local panel     = _Map[self] or self
            local elements  = _Panels[panel]
            local count     = 0

            local row
            local column
            local columnCount   = panel:GetAttribute("IFSecurePanel_ColumnCount") or 5
            local rowCount      = panel:GetAttribute("IFSecurePanel_RowCount") or 5
            local elementWidth  = panel:GetAttribute("IFSecurePanel_ElementWidth") or 16
            local elementHeight = panel:GetAttribute("IFSecurePanel_ElementHeight") or 16
            local hSpacing      = panel:GetAttribute("IFSecurePanel_HSpacing") or 0
            local vSpacing      = panel:GetAttribute("IFSecurePanel_VSpacing") or 0
            local marginTop     = panel:GetAttribute("IFSecurePanel_MarginTop") or 0
            local marginBottom  = panel:GetAttribute("IFSecurePanel_MarginBottom") or 0
            local marginLeft    = panel:GetAttribute("IFSecurePanel_MarginLeft") or 0
            local marginRight   = panel:GetAttribute("IFSecurePanel_MarginRight") or 0
            local orientation   = panel:GetAttribute("IFSecurePanel_Orientation") or "HORIZONTAL"
            local leftToRight   = panel:GetAttribute("IFSecurePanel_LeftToRight")
            local topToBottom   = panel:GetAttribute("IFSecurePanel_TopToBottom")
            local autoPos       = panel:GetAttribute("IFSecurePanel_AutoPosition")
            local autoSize      = panel:GetAttribute("IFSecurePanel_AutoSize")

            if leftToRight == nil then leftToRight = true end
            if topToBottom == nil then topToBottom = true end
            if     autoPos == nil then autoPos     = true end
            if     autoSize== nil then autoSize    = true end

            if elements then
                for i = 1, #elements do
                    local frm = elements[i]

                    frm:SetWidth(elementWidth)
                    frm:SetHeight(elementHeight)

                    if not autoPos or frm:IsShown() then
                        local posX = (orientation == "HORIZONTAL" and count % columnCount or floor(count / rowCount)) * (elementWidth + hSpacing)
                        local posY = (orientation == "HORIZONTAL" and floor(count / columnCount) or count % rowCount) * (elementHeight + vSpacing)

                        frm:ClearAllPoints()

                        if topToBottom then
                            frm:SetPoint("TOP", panel, "TOP", 0, - posY - marginTop)
                        else
                            frm:SetPoint("BOTTOM", panel, "BOTTOM", 0, posY + marginBottom)
                        end

                        if leftToRight then
                            frm:SetPoint("LEFT", panel, "LEFT", posX + marginLeft, 0)
                        else
                            frm:SetPoint("RIGHT", panel, "RIGHT", - posX - marginRight, 0)
                        end

                        count = count + 1
                    end
                end
            end

            if not panel:GetAttribute("IFSecurePanel_KeepMaxSize") then
                if not noForce or autoSize then
                    if count ~= _Cache[panel] then
                        _Cache[panel] = count

                        if orientation == "HORIZONTAL" then
                            row = ceil(count / columnCount)
                            column = row == 1 and count or columnCount
                        else
                            column = ceil(count / rowCount)
                            row = column == 1 and count or rowCount
                        end

                        if panel:GetAttribute("IFSecurePanel_KeepColumnSize") then
                            column = columnCount
                            if row == 0 then row = 1 end
                        end
                        if panel:GetAttribute("IFSecurePanel_KeepRowSize") then
                            row = rowCount
                            if column == 0 then column = 1 end
                        end

                        if row > 0 and column > 0 then
                            panel:SetWidth(column * elementWidth + (column - 1) * hSpacing + marginLeft + marginRight)
                            panel:SetHeight(row * elementHeight + (row - 1) * vSpacing + marginTop + marginBottom)
                        else
                            panel:SetWidth(1)
                            panel:SetHeight(1)
                        end
                    end
                end
            else
                panel:SetWidth(columnCount * elementWidth + (columnCount - 1) * hSpacing + marginLeft + marginRight)
                panel:SetHeight(rowCount * elementHeight + (rowCount - 1) * vSpacing + marginTop + marginBottom)
            end
        ]=]
    ]]

    -- The condition has no real use, just a timer ticker
    _ManagerFrame:SetAttribute("_onstate-timer", [=[
        if newstate ~= "reset" then
            for obj, noForce in pairs(_Queue) do
                Manager:RunFor(obj, UpdatePanelSize, noForce)
            end

            wipe(_Queue)
        end
    ]=])
    _ManagerFrame:RegisterStateDriver("timer", "[pet]pet;nopet;")

    _RegisterPanel              = [=[
        local panel = Manager:GetFrameRef("SecurePanel")

        if panel and not _Panels[panel] then
            _Panels[panel] = newtable()
        end
    ]=]

    _UnregisterPanel            = [=[
        local panel = Manager:GetFrameRef("SecurePanel")

        if panel then
            _Panels[panel] = nil
            _Cache[panel] = nil
        end
    ]=]

    _RegisterFrame              = [=[
        local panel = Manager:GetFrameRef("SecurePanel")
        local frame = Manager:GetFrameRef("SecureElement")

        if panel and frame then
            _Panels[panel] = _Panels[panel] or newtable()
            tinsert(_Panels[panel], frame)

            _Map[frame] = panel
        end
    ]=]

    _UnregisterFrame            = [=[
        local panel = Manager:GetFrameRef("SecurePanel")
        local frame = Manager:GetFrameRef("SecureElement")

        _Map[frame] = nil

        if panel and frame and _Panels[panel] then
            for k, v in ipairs(_Panels[panel]) do
                if v == frame then
                    return tremove(_Panels[panel], k)
                end
            end
        end
    ]=]

    _WrapShow                   = [[
        Manager:RunFor(self, QueueUpdatePanel, true)
    ]]

    _WrapHide                   = [[
        Manager:RunFor(self, QueueUpdatePanel, true)
    ]]

    _UpdatePanelSize            = [=[
        local panel = Manager:GetFrameRef("SecurePanel")

        _Cache[panel] = nil
        Manager:RunFor(panel, QueueUpdatePanel)
    ]=]

    _ForceUpdatePanel           = [=[
        local panel = Manager:GetFrameRef("SecurePanel")

        _Cache[panel] = nil
        Manager:RunFor(panel, UnQueueUpdatePanel)
        Manager:RunFor(panel, UpdatePanelSize)
    ]=]

    local function registerPanel(self)
        _ManagerFrame:SetFrameRef("SecurePanel", self)
        _ManagerFrame:Execute(_RegisterPanel)
    end

    local function unregisterPanel(self)
        _ManagerFrame:SetFrameRef("SecurePanel", self)
        _ManagerFrame:Execute(_UnregisterPanel)
    end

    local function registerFrame(self, frame)
        _ManagerFrame:SetFrameRef("SecurePanel", self)
        _ManagerFrame:SetFrameRef("SecureElement", frame)
        _ManagerFrame:Execute(_RegisterFrame)

        _ManagerFrame:WrapScript(frame, "OnShow", _WrapShow)
        _ManagerFrame:WrapScript(frame, "OnHide", _WrapHide)
    end

    local function unregisterFrame(self, frame)
        _ManagerFrame:UnwrapScript(frame, "OnShow")
        _ManagerFrame:UnwrapScript(frame, "OnHide")

        _ManagerFrame:SetFrameRef("SecurePanel", self)
        _ManagerFrame:SetFrameRef("SecureElement", frame)
        _ManagerFrame:Execute(_UnregisterFrame)
    end

    local function secureUpdatePanelSize(self)
        _ManagerFrame:SetFrameRef("SecurePanel", self)
        _ManagerFrame:Execute(_UpdatePanelSize)
    end

    local function nextItem(self, index)
        index                   = index + 1
        local ele               = self[index]
        if ele then return index, ele end
    end

    local function reduce(self, index)
        index                   = index or self.RowCount * self.ColumnCount

        if index < self.Count then
            for i = self.Count, index + 1, -1 do
                local ele       = self[i]
                self[i]         = nil

                ele:Hide()
                OnElementRemove(self, ele)

                self.ElementPool(ele)
                self:SetAttribute("IFSecurePanel_Count", i - 1)
            end

            return secureUpdatePanelSize(self)
        end
    end

    local function generate(self, index)
        if self.ElementPool and index > self.Count then
            for i = self.Count + 1, index do
                local ele       = self.ElementPool()
                self[i]         = ele
                ele:SetParent(self)
                ele:SetID(i)

                ele:Show()
                OnElementAdd(self, ele)

                self:SetAttribute("IFSecurePanel_Count", i)
            end

            return secureUpdatePanelSize(self)
        end
    end

    local function handlePropertyChange(self, prop, value)
        self:SetAttribute("IFSecurePanel_" .. prop, value)

        if prop == "RowCount" or prop == "ColumnCount" then reduce(self) end
        return secureUpdatePanelSize(self)
    end

    local function onPropertyChanged(self, value, old, prop)
        return NoCombat(handlePropertyChange, self, prop, value)
    end


    ------------------------------------------------------
    -- Event
    ------------------------------------------------------
    -- Fired when an element is added
    event "OnElementAdd"

    -- Fired when an element is removed
    event "OnElementRemove"

    ------------------------------------------------------
    -- Method
    ------------------------------------------------------
    function GetIterator(self, key)
        return nextItem, self, tonumber(key) or 0
    end

    __NoCombat__()
    RefreshLayout               = secureUpdatePanelSize

    function ForceRefreshLayout(self)
        if not InCombatLockdown() then
            _ManagerFrame:SetFrameRef("SecurePanel", self)
            _ManagerFrame:Execute(_ForceUpdatePanel)
        end
    end

    ------------------------------------------------------
    -- Property
    ------------------------------------------------------
    --- The Element Pool
    property "ElementPool"      { type = Recycle, default = function(self) return self.ElementType and Recycle(self.ElementType, self.ElementPrefix .. "%d", self) end }

    -- The element's type
    property "ElementType"      { type = ClassType }

    -- The prefix for the element's name
    property "ElementPrefix"    { type = String, default = "Element" }

    -- The Element accessor, used like obj.Elements[i].
    __Indexer__(NaturalNumber)
    property "Elements"         {
        get                     = function(self, index)
            if index >= 1 and index <= self.ColumnCount * self.RowCount then
                if self[index] then return self[index] end

                if self.ElementPool and not InCombatLockdown() then
                    generate(self, index)

                    return self[index]
                else
                    return nil
                end
            end
        end,
    }

    -- The columns's count
    property "ColumnCount"      { type = PositiveNumber, default = 5, handler = onPropertyChanged }

    -- The row's count
    property "RowCount"         { type = PositiveNumber, default = 5, handler = onPropertyChanged }

    -- The elements's max count
    property "MaxCount"         { Get = function(self) return self.ColumnCount * self.RowCount end }

    -- The element's width
    property "ElementWidth"     { type = PositiveNumber, default = 16, handler = onPropertyChanged }

    -- The element's height
    property "ElementHeight"    { type = PositiveNumber, default = 16, handler = onPropertyChanged }

    -- The element's count
    property "Count"            {
        get                     = function(self)
            return self:GetAttribute("IFSecurePanel_Count") or 0
        end,
        set                     = function(self, cnt)
            if cnt > self.RowCount * self.ColumnCount then
                error("Count can't be more than "..self.RowCount * self.ColumnCount, 2)
            end

            if cnt > self.Count then
                if self.ElementPool then
                    NoCombat(generate, self, cnt)
                else
                    error("ElementType not set.", 2)
                end
            elseif cnt < self.Count then
                NoCombat(reduce, self, cnt)
            end
        end,
        type = NaturalNumber,
    }

    -- The orientation for elements
    property "Orientation"      { type = Orientation, default = Orientation.HORIZONTAL, handler = onPropertyChanged }

    -- Whether the elements start from left to right
    property "LeftToRight"      { type = Boolean, default = true, handler = onPropertyChanged }

    -- Whether the elements start from top to bottom
    property "TopToBottom"      { type = Boolean, default = true, handler = onPropertyChanged }

    -- The horizontal spacing
    property "HSpacing"         { type = Number, handler = onPropertyChanged }

    -- The vertical spacing
    property "VSpacing"         { type = Number, handler = onPropertyChanged }

    -- Whether the elementPanel is autosize
    property "AutoSize"         { type = Boolean, default = true, handler = onPropertyChanged }

    -- The top margin
    property "MarginTop"        { type = Number, handler = onPropertyChanged }

    -- The bottom margin
    property "MarginBottom"     { type = Number, handler = onPropertyChanged }

    -- The left margin
    property "MarginLeft"       { type = Number, handler = onPropertyChanged }

    -- The right margin
    property "MarginRight"      { type = Number, handler = onPropertyChanged }

    -- Whether the elementPanel should keep it's max size
    property "KeepMaxSize"      { type = Boolean, handler = onPropertyChanged }

    -- Whether adjust the elements position automatically
    property "AutoPosition"     { type = Boolean, default = true, handler = onPropertyChanged }

    -- Whether keep the max size for columns
    property "KeepColumnSize"   { type = Boolean, handler = onPropertyChanged }

    -- Whether keep the max size for rows
    property "KeepRowSize"      { type = Boolean, handler = onPropertyChanged }


    ------------------------------------------------------
    -- Constructor
    ------------------------------------------------------
    function __ctor(self)
        self.OnElementAdd       = self.OnElementAdd + registerFrame
        self.OnElementRemove    = self.OnElementRemove + unregisterFrame

        registerPanel(self)
    end
end)