PTUnitFrameGroup = {}
PTUtil.SetEnvironment(PTUnitFrameGroup)
PTUnitFrameGroup.__index = PTUnitFrameGroup
local _G = getfenv(0)
local PT = Puppeteer
local util = PTUtil

PTUnitFrameGroup.name = "???"

PTUnitFrameGroup.profile = nil

PTUnitFrameGroup.container = nil
PTUnitFrameGroup.borderFrame = nil
PTUnitFrameGroup.header = nil
PTUnitFrameGroup.label = nil
PTUnitFrameGroup.uis = nil
PTUnitFrameGroup.units = nil

PTUnitFrameGroup.petGroup = false
PTUnitFrameGroup.environment = "all" -- party, raid, or all
PTUnitFrameGroup.sortByRole = true

PTUnitFrameGroup.moveContainer = CreateFrame("Frame", "PTUnitFrameGroupBulkMoveContainer", UIParent)
PTUnitFrameGroup.moveContainer:EnableMouse(true)
PTUnitFrameGroup.moveContainer:SetMovable(true)

ContextMenu = PTGuiLib.Get("dropdown", UIParent)
ContextMenu:SetDynamicOptions(function(addOption, level, args)
    addOption("text", "Lock Position",
        "func", function(self, gui)
            PuppeteerSettings.SetFrameLocked(gui.FrameGroup.name, not self.checked)
        end,
        "initFunc", function(self, gui)
            self.checked = PuppeteerSettings.IsFrameLocked(gui.FrameGroup.name)
        end)

    addOption("notCheckable", true,
        "disabled", true)

    addOption("text", "Open Settings",
        "notCheckable", true,
        "func", function(self, gui)
            PTSettingsGui.TabFrame:Show()
        end)
end)

function PTUnitFrameGroup:New(name, environment, units, petGroup, profile, sortByRole)
    local obj = setmetatable({name = name, environment = environment, uis = {}, units = units, petGroup = petGroup, 
        profile = profile, sortByRole = sortByRole}, self)
    obj:Initialize()
    return obj
end

function PTUnitFrameGroup:EvaluateShown()
    if self:CanShowInEnvironment(Puppeteer.CurrentlyInRaid and "raid" or "party") and self:ShowCondition() then
        self:Show()
        self:UpdateUIPositions()
    else
        self:Hide()
    end
end

function PTUnitFrameGroup:ShowCondition()
    if PTOptions.Hidden or PuppeteerSettings.IsFrameHidden(self.name) then
        return false
    end

    if PTOptions.HideWhileSolo and (GetNumPartyMembers() == 0 and GetNumRaidMembers() == 0) then
        return false
    end

    for _, ui in pairs(self.uis) do
        if ui:IsShown() then
            return true
        end
    end
    return false
 end

function PTUnitFrameGroup:AddUI(ui, noUpdate)
    self.uis[ui:GetUnit()] = ui
    ui:SetOwningGroup(self)
    if not noUpdate then
        self:UpdateUIPositions()
    end
end

function PTUnitFrameGroup:GetContainer()
    return self.container
end

function PTUnitFrameGroup:ResetFrameLevel()
    self.container:SetFrameLevel(0)
    self.borderFrame:SetFrameLevel(1)
end

function PTUnitFrameGroup:GetEnvironment()
    return self.environment
end

function PTUnitFrameGroup:CanShowInEnvironment(environment)
    return self.environment == "all" or self.environment == environment
end

function PTUnitFrameGroup:Show()
    self.container:Show()
    for _, ui in pairs(self.uis) do
        ui:UpdateAll()
    end
end

function PTUnitFrameGroup:Hide()
    self.container:Hide()
end

-- Used while moving frames to avoid the lag while moving over other toplevel frames
function PTUnitFrameGroup:RemoveToplevel()
    self.container:SetToplevel(false)
    self.container:SetFrameStrata("HIGH")
end

function PTUnitFrameGroup:ApplyToplevel()
    self.container:SetToplevel(true)
    self.container:SetFrameStrata("MEDIUM")
end

function PTUnitFrameGroup:UpdateHeaderColor()
    local opacity = PuppeteerSettings.IsFrameLocked(self.name) and 0.1 or 0.5
    self.header:SetBackdropColor(0, 0, 0, opacity)
end

function PTUnitFrameGroup:Initialize()
    local container = CreateFrame("Frame", "PTUnitFrameGroupContainer_"..self.name, UIParent)
    self.container = container
    container:EnableMouse(true)
    container:SetMovable(true)
    container:SetUserPlaced(false)
    self:ApplyToplevel()
    container:ClearAllPoints()
    local anchor, x, y = PuppeteerSettings.GetFramePosition(self.name)
    container:SetPoint(anchor or "TOPLEFT", UIParent, "TOPLEFT", x or 100, y or -100)
    container:SetBackdrop({bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background"})
    container:SetBackdropColor(0, 0, 0, 0.5)

    container:SetScript("OnMouseDown", function()
        local button = arg1

        if button ~= "LeftButton" or container.isMoving then
            return
        end

        if PuppeteerSettings.IsFrameLocked(self.name) then
            return
        end

        container.isMoving = true

        if (util.GetKeyModifier() == PTOptions.FrameDrag.AltMoveKey) == PTOptions.FrameDrag.MoveAll then
            container:StartMoving()
            container:SetUserPlaced(false) -- StartMoving sets this and needs to be reverted
            self:RemoveToplevel()
            return
        end

        container.bulkMovement = true

        local moveContainer = PTUnitFrameGroup.moveContainer
        moveContainer:ClearAllPoints()
        moveContainer:SetPoint("TOPLEFT", 0, 0)
        -- If the container doesn't have a size, it doesn't move
        moveContainer:SetWidth(1)
        moveContainer:SetHeight(1)
        local movingGroups = {}
        for _, group in pairs(Puppeteer.UnitFrameGroups) do
            if not PuppeteerSettings.IsFrameLocked(group.name) and group:GetContainer():IsVisible() then
                group:RemoveToplevel()
                local gc = group:GetContainer()
                local xOffset = gc:GetLeft()
                local yOffset = gc:GetTop() - GetScreenHeight()
                gc:ClearAllPoints()
                gc:SetPoint("TOPLEFT", moveContainer, "TOPLEFT", xOffset, yOffset)
                table.insert(movingGroups, group)
            end
        end
        moveContainer.groups = movingGroups
        moveContainer:StartMoving()
    end)

    container:SetScript("OnMouseUp", function()
        local button = arg1

        if button == "RightButton" and MouseIsOver(self.header) and self.header:IsVisible() then
            ContextMenu.FrameGroup = self
            ContextMenu:SetToggleState(false)
            ContextMenu:SetToggleState(true, container, container:GetWidth(), container:GetHeight())
            PlaySound("igMainMenuOpen")
            return
        end

        if (button ~= "LeftButton" or not container.isMoving) then
            return
        end

        container.isMoving = false

        if not container.bulkMovement then
            container:StopMovingOrSizing()
            self:ApplyToplevel()
            util.ConvertAnchor(container, PuppeteerSettings.GetFramePosition(self.name))
            return
        end

        container.bulkMovement = false

        local moveContainer = PTUnitFrameGroup.moveContainer
        moveContainer:StopMovingOrSizing()
        for _, group in pairs(moveContainer.groups) do
            group:ApplyToplevel()
            local gc = group:GetContainer()
            util.ConvertAnchor(gc, PuppeteerSettings.GetFramePosition(group.name))
        end
        -- Prevent container from potentially blocking mouse by setting it back to 0 size
        moveContainer:SetWidth(0)
        moveContainer:SetHeight(0)
    end)

    container:SetScript("OnHide", function()
        if not container.isMoving then
            return
        end
        local prevArg = arg1
        arg1 = "LeftButton"
        container:GetScript("OnMouseUp")()
        arg1 = prevArg
    end)

    local header = CreateFrame("Frame", "$parentHeader", container)
    self.header = header
    header:SetPoint("TOPLEFT", container, 0, 0)
    header:SetBackdrop({bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background"})
    self:UpdateHeaderColor()

    local label = header:CreateFontString(header, "OVERLAY", "GameFontNormal")
    self.label = label
    label:SetPoint("CENTER", header, "CENTER", 0, 0)
    label:SetText(self.name)

    local borderFrame = CreateFrame("Frame", "$parentBorder", container)
    self.borderFrame = borderFrame
    borderFrame:SetPoint("CENTER", container, 0, 0)

    self:ApplyProfile()

    self:UpdateUIPositions()
end

function PTUnitFrameGroup:ApplyProfile()
    local profile = self:GetProfile()
    
    local borderFrame = self.borderFrame
    if profile.BorderStyle == "Tooltip" then
        borderFrame:SetBackdrop({edgeFile="Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 17, 
            tile = true, tileSize = 17})
    elseif profile.BorderStyle == "Dialog Box" then
        borderFrame:SetBackdrop({edgeFile="Interface\\DialogFrame\\UI-DialogBox-Border", edgeSize = 24, 
            tile = true, tileSize = 24})
    else
        borderFrame:SetBackdrop({})
    end
end

function PTUnitFrameGroup:UpdateUIPositions()
    local profile = self:GetProfile()
    local profileWidth = profile.Width
    local profileHeight = profile:GetHeight()
    local maxUnitsInAxis = profile.MaxUnitsInAxis
    local orientation = profile.Orientation
    local headerEnabled = not PuppeteerSettings.IsTitleHidden(self.name)
    local headerHeight = headerEnabled and 20 or 0

    local sortedUIs = self:GetSortedUIs()
    local splitSortedUIs = {}
    for _, group in ipairs(sortedUIs) do
        local currentTable = {}
        local unitsUntilShift = maxUnitsInAxis
        for _, ui in ipairs(group) do
            if unitsUntilShift == 0 then
                table.insert(splitSortedUIs, currentTable)
                currentTable = {}
                unitsUntilShift = maxUnitsInAxis
            end
            table.insert(currentTable, ui)
            unitsUntilShift = unitsUntilShift - 1
        end
        if table.getn(currentTable) > 0 then
            table.insert(splitSortedUIs, currentTable)
        end
    end

    -- IMPORTANT: "Column" does not necessarily mean vertical!
    local largestColumn = orientation == "Vertical" and profile.MinUnitsY or profile.MinUnitsX
    for _, column in ipairs(splitSortedUIs) do
        largestColumn = math.max(largestColumn, table.getn(column))
    end

    local xSpacing = profile.HorizontalSpacing
    local ySpacing = profile.VerticalSpacing
    for columnIndex, column in ipairs(splitSortedUIs) do
        for i, ui in ipairs(column) do -- Column is guaranteed to be less than max units
            local container = ui:GetRootContainer()
            local x = orientation == "Vertical" and ((profileWidth + xSpacing) * (columnIndex - 1)) or ((profileWidth + xSpacing) * (i - 1))
            local y = orientation == "Vertical" and ((profileHeight + ySpacing) * (i - 1)) or ((profileHeight + ySpacing) * (columnIndex - 1))
            container:SetPoint("TOPLEFT", self.container, "TOPLEFT", x, -y - headerHeight)
        end
    end

    local largestRow = math.max(table.getn(splitSortedUIs), orientation == "Vertical" and profile.MinUnitsX or profile.MinUnitsY)

    
    --largestRow = math.max(largestRow, 1)
    --largestColumn = math.max(largestColumn, 1)

    local width = orientation == "Vertical" and (profileWidth * largestRow + (xSpacing * (largestRow - 1))) or (profileWidth * largestColumn + (xSpacing * (largestColumn - 1)))
    width = math.max(width, profileWidth) -- Prevent width from being 0
    local height = orientation == "Vertical" and (profileHeight * largestColumn + (ySpacing * (largestColumn - 1))) or (profileHeight * largestRow + (ySpacing * (largestRow - 1)))
    height = height + headerHeight
    self.container:SetWidth(width)
    self.container:SetHeight(height)

    local header = self.header
    if headerEnabled then
        header:Show()
        header:SetWidth(width)
        header:SetHeight(headerHeight)
    else
        header:Hide()
    end

    local borderPadding = 0
    if profile.BorderStyle == "Tooltip" then
        borderPadding = 10
    elseif profile.BorderStyle == "Dialog Box" then
        borderPadding = 18
    end
    self.borderFrame:SetWidth(width + borderPadding)
    self.borderFrame:SetHeight(height + borderPadding)

    local label = self.label
    label:SetPoint("CENTER", header, "CENTER", 0, 0)
end

-- Returns an array with the index being the group number, and the value being an array of units
function PTUnitFrameGroup:GetSortedUIs()
    local profile = self:GetProfile()
    local uis = self.uis
    local groups = {}
    
    if self.environment == "raid" and profile.SplitRaidIntoGroups and not self.petGroup then
        local foundRaidNumbers = {} -- Used for testing UI
        for i = 1, 8 do
            groups[i] = {}
            local group = groups[i]
            if RAID_SUBGROUP_LISTS and RAID_SUBGROUP_LISTS[i] then
                for frameNumber, raidNumber in pairs(RAID_SUBGROUP_LISTS[i]) do
                    table.insert(group, uis["raid"..raidNumber]) -- Effectively sorts raid members by ID at this point
                    foundRaidNumbers[raidNumber] = 1
                end
            end
        end
        -- If testing, fill empty slots with fake players
        if Puppeteer.TestUI and RAID_SUBGROUP_LISTS then
            local unoccupied = {}
            for i = 1, 40 do
                if not foundRaidNumbers[i] then
                    table.insert(unoccupied, i)
                end
            end
            for i = 1, 8 do
                local group = groups[i]
                for frameNumber = 1, 5 do
                    if not RAID_SUBGROUP_LISTS[i] or not RAID_SUBGROUP_LISTS[i][frameNumber] then
                        table.insert(group, uis["raid"..table.remove(unoccupied, table.getn(unoccupied))])
                    end
                end
            end
        end
    else
        groups[1] = {}
        local group = groups[1]
        for _, ui in pairs(uis) do
            if ui:IsShown() then
                table.insert(group, ui)
            end
        end
    end

    local sortedGroups = {}
    if profile.SortUnitsBy == "ID" then
        for groupNumber, group in ipairs(groups) do
            if table.getn(group) > 0 then
                local groupSet = {} -- Convert group UI array to a set with the key as the Unit ID
                for _, ui in ipairs(group) do
                    groupSet[ui:GetUnit()] = ui
                end
                if self.environment == "raid" then
                    if not self.petGroup then -- Should already be sorted if we're not dealing with the pets
                        table.insert(sortedGroups, group)
                    else -- Pets need to be sorted manually
                        local sortedGroup = {}
                        for _, unit in ipairs(self.units) do -- Iterate through all unit IDs this UI group can handle, in order
                            if groupSet[unit] then
                                table.insert(sortedGroup, groupSet[unit])
                            end
                        end
                        table.insert(sortedGroups, sortedGroup)
                    end
                else
                    local sortedGroup = {}
                    for _, unit in ipairs(self.units) do -- Iterate through all unit IDs this UI group can handle, in order
                        if groupSet[unit] then
                            table.insert(sortedGroup, groupSet[unit])
                        end
                    end
                    table.insert(sortedGroups, sortedGroup)
                end
            end
        end
    elseif profile.SortUnitsBy == "Name" then
        for groupNumber, group in ipairs(groups) do
            if table.getn(group) > 0 then
                table.sort(group, function(a, b)
                    local aName = UnitName(a:GetUnit()) or a.fakeStats.name or a:GetUnit()
                    local bName = UnitName(b:GetUnit()) or b.fakeStats.name or b:GetUnit()
                    return aName < bName
                end)
                table.insert(sortedGroups, group)
            end
        end
    elseif profile.SortUnitsBy == "Class Name" then
        for groupNumber, group in ipairs(groups) do
            if table.getn(group) > 0 then
                table.sort(group, function(a, b)
                    local aName = util.GetClass(a:GetUnit()) or a.fakeStats.class
                    local bName = util.GetClass(b:GetUnit()) or b.fakeStats.class
                    return aName < bName
                end)
                table.insert(sortedGroups, group)
            end
        end
    end
    local sortByRole = self.sortByRole
    for _, group in ipairs(sortedGroups) do
        if sortByRole then
            local rolePriority = {
                ["Tank"] = 1,
                ["Healer"] = 2,
                ["Damage"] = 3
            }
            local groupCopy = util.CloneTable(group)
            local roleSorter = function(a, b)
                if not a or not b then
                    return false
                end
                local aRank = ((rolePriority[a:GetRole()] or 4) * 100) + util.IndexOf(groupCopy, a)
                local bRank = ((rolePriority[b:GetRole()] or 4) * 100) + util.IndexOf(groupCopy, b)
                return aRank < bRank
            end
            table.sort(group, roleSorter)
        end
        for _, ui in ipairs(group) do
            ui:UpdateRole()
        end
    end
    return sortedGroups
end

function PTUnitFrameGroup:GetProfile()
    return self.profile
end