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

--========================================================--
Scorpio           "Scorpio.UI.ElementPanel"          "1.0.0"
--========================================================--

__Sealed__() class "ElementPanel" (function(_ENV)
    inherit "Frame" extend "ICountable"

    export{ max = math.max }

    local function onSizeChanged(self)
        return self:Refresh()
    end

    local function adjustElement(element, self)
        local id                = element:GetID()
        if not id then return end

        element:SetSize(self.ElementWidth, self.ElementHeight)

        local posX              = (self.Orientation == Orientation.HORIZONTAL and (id - 1) % self.ColumnCount or floor((id - 1) / self.RowCount)) * (self.ElementWidth + self.HSpacing)
        local posY              = (self.Orientation == Orientation.HORIZONTAL and floor((id - 1) / self.ColumnCount) or (id - 1) % self.RowCount) * (self.ElementHeight + self.VSpacing)

        element:ClearAllPoints()

        if self.TopToBottom then
            element:SetPoint("TOP", 0, - posY - self.MarginTop)
        else
            element:SetPoint("BOTTOM", 0, posY + self.MarginBottom)
        end

        if self.LeftToRight then
            element:SetPoint("LEFT", posX + self.MarginLeft, 0)
        else
            element:SetPoint("RIGHT", - posX - self.MarginRight, 0)
        end
    end

    local function adjustPanel(self)
        if self.KeepMaxSize then
            if not self.FixedWidth then
                self:SetWidth(self.ColumnCount * self.ElementWidth  + (self.ColumnCount - 1) * self.HSpacing + self.MarginLeft + self.MarginRight)
            end

            if not self.FixedHeight then
                self:SetHeight(self.RowCount * self.ElementHeight + (self.RowCount    - 1) * self.VSpacing + self.MarginTop  + self.MarginBottom)
            end
        else
            local i             = self.Count

            while i > 0 do
                if self[i]:IsShown() then break end
                i               = i - 1
            end

            local row
            local column

            if self.Orientation == Orientation.HORIZONTAL then
                row             = ceil(i / self.ColumnCount)
                column          = row == 1 and i or self.ColumnCount
            else
                column          = ceil(i / self.RowCount)
                row             = column == 1 and i or self.RowCount
            end

            if self.KeepColumnSize then
                column          = self.ColumnCount
                if row == 0 then row = 1 end
            end
            if self.KeepRowSize then
                row             = self.RowCount
                if column == 0 then column = 1 end
            end

            if row > 0 and column > 0 then
                if not self.FixedWidth then
                    self:SetWidth(column * self.ElementWidth + (column - 1) * self.HSpacing + self.MarginLeft + self.MarginRight)
                end

                if not self.FixedHeight then
                    self:SetHeight(row * self.ElementHeight + (row - 1) * self.VSpacing + self.MarginTop + self.MarginBottom)
                end
            else
                if not self.FixedWidth then
                    self:SetWidth(1)
                end

                if not self.FixedHeight then
                    self:SetHeight(1)
                end
            end
        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]

                -- still keep them to be re-use
                ele:ClearAllPoints()
                ele:Hide()

                OnElementRemove(self, ele)

                self.__ElementPanel_Count = i - 1
            end

            if self.FixedHeight or self.FixedWidth then
                return self:Refresh()
            else
                return adjustPanel(self)
            end
        end
    end

    local function generate(self, index)
        if self.ElementType and index > self.Count then
            local ele

            for i = self.Count + 1, index do
                local ele       = self[i]

                if not ele then
                    ele         = self.ElementType(self.ElementPrefix .. i, self)
                    self[i]     = ele
                    ele:SetID(i)

                    OnElementCreated(self, ele)
                end

                ele:Show()

                adjustElement(ele, self)
                OnElementAdd(self, ele)

                self.__ElementPanel_Count = i
            end

            if self.FixedHeight or self.FixedWidth then
                return self:Refresh()
            else
                return adjustPanel(self)
            end
        end
    end

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

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

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

    -- Fired when an element is created
    event "OnElementCreated"

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

    __AsyncSingle__()
    function Refresh(self)
        Next()

        reduce(self)

        if self.FixedHeight or self.FixedWidth then
            self:SetAttribute("__ElementPanel_FixedSize", true)
            self.OnSizeChanged  = self.OnSizeChanged + onSizeChanged

            local row
            local column

            if self.Orientation == Orientation.HORIZONTAL then
                row             = ceil(self.Count / self.ColumnCount)
                column          = row == 1 and self.Count or self.ColumnCount
            else
                column          = ceil(self.Count / self.RowCount)
                row             = column == 1 and self.Count or self.RowCount
            end

            if self.FixedWidth then
                local total     = self:GetWidth()
                self.ElementWidth = max(1, (total - self.MarginLeft - self.MarginRight - (column - 1) * self.HSpacing) / column)
            end

            if self.FixedHeight then
                local total     = self:GetHeight()
                self.ElementHeight = max(1, (total - self.MarginTop - self.MarginBottom - (row - 1) * self.VSpacing) / row)
            end
        elseif self:GetAttribute("__ElementPanel_FixedSize") then
            self:SetAttribute("__ElementPanel_FixedSize", nil)
            self.OnSizeChanged  = self.OnSizeChanged - onSizeChanged
        end

        self:Each(adjustElement, self)

        adjustPanel(self)
    end

    ------------------------------------------------------
    -- Property
    ------------------------------------------------------
    -- The Element accessor, used like obj.Element[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.ElementType then
                    generate(self, index)

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

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

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

    -- 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 = Refresh }

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

    -- The element's count
    property "Count"            {
        type                    = NaturalNumber,
        field                   = "__ElementPanel_Count",
        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.ElementType then
                    generate(self, cnt)
                else
                    error("ElementType not set.", 2)
                end
            elseif cnt < self.Count then
                reduce(self, cnt)
            end
        end,
        default                 = 0,
    }

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

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

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

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

    -- The horizontal spacing
    property "HSpacing"         { type = Number, default = 0, handler = Refresh }

    -- The vertical spacing
    property "VSpacing"         { type = Number, default = 0, handler = Refresh }

    -- The top margin
    property "MarginTop"        { type = Number, default = 0, handler = Refresh }

    -- The bottom margin
    property "MarginBottom"     { type = Number, default = 0, handler = Refresh }

    -- The left margin
    property "MarginLeft"       { type = Number, default = 0, handler = Refresh }

    -- The right margin
    property "MarginRight"      { type = Number, default = 0, handler = Refresh }

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

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

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

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

    -- Whether the panel's height is fixed, so the element height will be modified
    property "FixedHeight"      { type = Boolean, handler = Refresh }

    -- Whether the panel's width is fixed, so the element width will be modified
    property "FixedWidth"       { type = Boolean, handler = Refresh }
end)