local addonName, SinsToolkit = ...

local FrameMover = {}
SinsToolkit.FrameMover = FrameMover

local sessionPositions = {}
local sessionScales = {}
local registered = {}
local HOOK_VERSION = 2
local SaveScale

local DEFAULT_FRAMES = {
    "CharacterFrame",
    "PlayerSpellsFrame",
    "SpellBookFrame",
    "ProfessionsFrame",
    "ProfessionsBookFrame",
    "TradeSkillFrame",
    "CraftFrame",
    "ProfessionsCustomerOrdersFrame",
    "EncounterJournal",
    "CollectionsJournal",
    "WorldMapFrame",
    "QuestLogPopupDetailFrame",
    "QuestLogFrame",
    "QuestLogDetailFrame",
    "PVEFrame",
    "FriendsFrame",
    "CalendarFrame",
    "AchievementFrame",
    "MacroFrame",
    "MacroPopupFrame",
    "CommunitiesFrame",
    "GuildFrame",
    "MailFrame",
    "MerchantFrame",
    "GossipFrame",
    "QuestFrame",
    "BankFrame",
    "AuctionHouseFrame",
    "ItemInteractionFrame",
    "ItemTextFrame",
    "DressUpFrame",
    "InspectFrame",
    "TradeFrame",
    "GuildBankFrame",
    "SettingsPanel",
    "DelvesCompanionConfigurationFrame",
    "DelvesCompanionFrame",
    "DelvesDashboardFrame",
    "DelvesDifficultyPickerFrame",
    "DelvesScenarioOverviewFrame",
    "HousingModelPreviewFrame",
    "HouseEditorFrame.StoragePanel",
    "NamePlatesTutorial",
}

local DEFAULT_FRAME_LOOKUP = {}
for _, name in ipairs(DEFAULT_FRAMES) do
    DEFAULT_FRAME_LOOKUP[name] = true
end

local DELAYED_ADDON_FRAMES = {
    Blizzard_MacroUI = { "MacroFrame", "MacroPopupFrame" },
    Blizzard_Communities = { "CommunitiesFrame" },
    Blizzard_GuildUI = { "GuildFrame" },
    Blizzard_Calendar = { "CalendarFrame" },
    Blizzard_AchievementUI = { "AchievementFrame" },
    Blizzard_ItemInteractionUI = { "ItemInteractionFrame" },
    Blizzard_HouseEditor = { "HouseEditorFrame.StoragePanel" },
    Blizzard_HousingModelPreview = { "HousingModelPreviewFrame" },
    Blizzard_SettingsDefinitions_Frame = { "NamePlatesTutorial" },
}

local function Print(message)
    print("|cff9b59b6[Sin's Toolkit: QOL]|r " .. message)
end

local function IsSafe(frame)
    return frame
        and not (frame.IsForbidden and frame:IsForbidden())
        and not ((frame.IsProtected and frame:IsProtected()) and InCombatLockdown())
end

local function IsEnabled()
    return SinsToolkit.IsFeatureActive and SinsToolkit.IsFeatureActive("frameMoverEnabled")
end

local function IsFrameDisabled(name)
    return SinsToolkitDB
        and SinsToolkitDB.frameMoverDisabledFrames
        and SinsToolkitDB.frameMoverDisabledFrames[name] == true
end

local function IsLikelyTopLevelPanel(frame, name)
    if not frame or not name then return false end
    if name == "UIParent" or name == "WorldFrame" then return false end
    if frame.GetParent and frame:GetParent() ~= UIParent then return false end
    if frame.GetObjectType and frame:GetObjectType() ~= "Frame" then return false end
    if frame.IsForbidden and frame:IsForbidden() then return false end
    return true
end

local function ModifierSatisfied()
    if not SinsToolkitDB or not SinsToolkitDB.frameMoverRequireModifier then return true end
    return IsShiftKeyDown and IsShiftKeyDown()
end

local function SavePosition(frame, name)
    if not frame or not name or not SinsToolkitDB then return end
    local mode = SinsToolkitDB.frameMoverPositionMode or "session"
    if mode == "off" then return end

    local point, relativeTo, relativePoint, x, y = frame:GetPoint()
    local data = {
        point = point or "CENTER",
        relativePoint = relativePoint or point or "CENTER",
        x = x or 0,
        y = y or 0,
    }
    if mode == "permanent" then
        SinsToolkitDB.frameMoverPositions[name] = data
    else
        sessionPositions[name] = data
    end
end

local function CaptureOriginalPlacement(frame)
    if not frame or frame.__SinsToolkitOriginalPlacement then return end
    local point, relativeTo, relativePoint, x, y = frame:GetPoint()
    frame.__SinsToolkitOriginalPlacement = {
        point = point or "CENTER",
        relativePoint = relativePoint or point or "CENTER",
        x = x or 0,
        y = y or 0,
    }
end

local function RestorePosition(frame, name)
    if not frame or not name or not SinsToolkitDB then return end
    local mode = SinsToolkitDB.frameMoverPositionMode or "session"
    local data = mode == "permanent" and SinsToolkitDB.frameMoverPositions[name] or sessionPositions[name]
    if not data then return end
    if not IsSafe(frame) then return end
    frame:ClearAllPoints()
    frame:SetPoint(data.point or "CENTER", UIParent, data.relativePoint or data.point or "CENTER", data.x or 0, data.y or 0)
end

local function ApplyScaleDelta(frame, name, delta)
    if not IsEnabled() or IsFrameDisabled(name) or not IsSafe(frame) then return end
    if not (SinsToolkitDB and SinsToolkitDB.frameMoverCtrlScrollScale) then return end
    if not (IsControlKeyDown and IsControlKeyDown()) then return end

    local current = frame:GetScale() or 1
    local nextScale = math.max(0.5, math.min(2.0, current + (delta > 0 and 0.05 or -0.05)))
    if math.abs(nextScale - current) < 0.001 then return end

    frame:SetScale(nextScale)
    SaveScale(frame, name)
end

SaveScale = function(frame, name)
    if not frame or not name or not SinsToolkitDB then return end
    local mode = SinsToolkitDB.frameMoverScaleMode or "session"
    if mode == "off" then return end
    if mode == "permanent" then
        SinsToolkitDB.frameScales[name] = frame:GetScale()
        SinsToolkitDB.frameMoverScales = SinsToolkitDB.frameScales
    else
        sessionScales[name] = frame:GetScale()
    end
end

local function RestoreScale(frame, name)
    if not frame or not name or not SinsToolkitDB then return end
    local mode = SinsToolkitDB.frameMoverScaleMode or "session"
    local scale = mode == "permanent" and SinsToolkitDB.frameScales[name] or sessionScales[name]
    if not scale then return end
    if not IsSafe(frame) then return end
    frame:SetScale(math.max(0.5, math.min(2.0, tonumber(scale) or 1)))
end

local function ResetScale(frame, name)
    if not frame or not name or not SinsToolkitDB then return end
    SinsToolkitDB.frameScales[name] = nil
    SinsToolkitDB.frameMoverScales = SinsToolkitDB.frameScales
    sessionScales[name] = nil
    if IsSafe(frame) then
        frame:SetScale(1)
        Print(name .. " scale reset.")
    end
end

local function ResetFrame(frame, name, resetScale)
    if not frame or not name or not SinsToolkitDB then return end
    SinsToolkitDB.frameMoverPositions[name] = nil
    SinsToolkitDB.frameScales[name] = nil
    SinsToolkitDB.frameMoverScales = SinsToolkitDB.frameScales
    sessionPositions[name] = nil
    sessionScales[name] = nil
    if IsSafe(frame) then
        if resetScale then frame:SetScale(1) end
        frame:ClearAllPoints()
        local original = frame.__SinsToolkitOriginalPlacement
        if original then
            frame:SetPoint(original.point or "CENTER", UIParent, original.relativePoint or original.point or "CENTER", original.x or 0, original.y or 0)
        else
            frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
        end
        frame:SetUserPlaced(false)
    end
end

local function ShowFrameMoverMenu(frame, name)
    if MenuUtil and MenuUtil.CreateContextMenu then
        MenuUtil.CreateContextMenu(frame, function(_, rootDescription)
            rootDescription:CreateTitle(name)
            rootDescription:CreateButton("Reset Position", function()
                ResetFrame(frame, name, false)
                Print(name .. " position reset.")
            end)
            rootDescription:CreateButton("Reset Scale", function()
                ResetScale(frame, name)
            end)
            rootDescription:CreateButton("Reset Position and Scale", function()
                ResetFrame(frame, name, true)
                Print(name .. " position and scale reset.")
            end)
        end)
        return
    end
    ResetScale(frame, name)
end

local function StartMovingFrame(frame, name, button, fromDragStrip)
    if not IsEnabled() or IsFrameDisabled(name) or not IsSafe(frame) then return end
    if button == "LeftButton" and ModifierSatisfied() and not (IsControlKeyDown and IsControlKeyDown()) then
        frame.__SinsToolkitMoving = true
        frame:StartMoving()
    elseif button == "RightButton"
        and SinsToolkitDB
        and SinsToolkitDB.frameMoverRightClickReset
        and (fromDragStrip or (IsShiftKeyDown and IsShiftKeyDown()))
    then
        ShowFrameMoverMenu(frame, name)
    end
end

local function StopMovingFrame(frame, name)
    if frame.__SinsToolkitMoving then
        frame.__SinsToolkitMoving = nil
        frame:StopMovingOrSizing()
        frame:SetUserPlaced(false)
        SavePosition(frame, name)
    end
end

local function EnsureDragStrip(frame, name)
    if frame.__SinsToolkitFrameMoverDragStrip then
        frame.__SinsToolkitFrameMoverDragStrip:Show()
        frame.__SinsToolkitFrameMoverDragStrip:EnableMouse(true)
        if frame.__SinsToolkitFrameMoverDragStrip.EnableMouseWheel then
            frame.__SinsToolkitFrameMoverDragStrip:EnableMouseWheel(true)
        end
        return
    end
    if not frame.CreateTexture then return end

    local dragStrip = CreateFrame("Frame", nil, frame)
    dragStrip:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -6)
    dragStrip:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -44, -6)
    dragStrip:SetHeight(28)
    dragStrip:EnableMouse(true)
    if dragStrip.EnableMouseWheel then dragStrip:EnableMouseWheel(true) end
    dragStrip:SetFrameLevel(math.min((frame:GetFrameLevel() or 0) + 20, 10000))
    dragStrip:SetScript("OnMouseDown", function(_, button)
        StartMovingFrame(frame, name, button, true)
    end)
    dragStrip:SetScript("OnMouseUp", function()
        StopMovingFrame(frame, name)
    end)
    dragStrip:SetScript("OnLeave", function()
        StopMovingFrame(frame, name)
    end)
    dragStrip:SetScript("OnMouseWheel", function(_, delta)
        ApplyScaleDelta(frame, name, delta)
    end)

    frame.__SinsToolkitFrameMoverDragStrip = dragStrip
end

local function ResolveChildFrame(parentName, childPath)
    if not parentName or not childPath then return nil end
    if childPath:find("%.", 1, true) then
        local current = _G[parentName]
        for segment in childPath:gmatch("[^%.]+") do
            if segment ~= parentName then
                current = current and current[segment]
            end
        end
        return current
    end
    return _G[childPath]
end

local function ResolveFrame(name)
    if not name then return nil end
    if _G[name] then return _G[name] end
    if not name:find("%.", 1, true) then return nil end

    local current
    for segment in name:gmatch("[^%.]+") do
        current = current and current[segment] or _G[segment]
    end
    return current
end

local function EnsureChildDragStrip(parentFrame, parentName, childFrame)
    if not parentFrame or not parentName or not childFrame then return end
    if childFrame.__SinsToolkitFrameMoverChildDragStrip then
        childFrame.__SinsToolkitFrameMoverChildDragStrip:Show()
        childFrame.__SinsToolkitFrameMoverChildDragStrip:EnableMouse(true)
        return
    end
    if childFrame.IsForbidden and childFrame:IsForbidden() then return end
    if not childFrame.CreateTexture then return end

    local dragStrip = CreateFrame("Frame", nil, childFrame)
    dragStrip:SetPoint("TOPLEFT", childFrame, "TOPLEFT", 0, 0)
    dragStrip:SetPoint("BOTTOMRIGHT", childFrame, "BOTTOMRIGHT", -44, 0)
    dragStrip:EnableMouse(true)
    if dragStrip.EnableMouseWheel then dragStrip:EnableMouseWheel(true) end
    dragStrip:SetFrameLevel(math.min((childFrame:GetFrameLevel() or 0) + 20, 10000))
    dragStrip:SetScript("OnMouseDown", function(_, button)
        StartMovingFrame(parentFrame, parentName, button, true)
    end)
    dragStrip:SetScript("OnMouseUp", function()
        StopMovingFrame(parentFrame, parentName)
    end)
    dragStrip:SetScript("OnLeave", function()
        StopMovingFrame(parentFrame, parentName)
    end)
    dragStrip:SetScript("OnMouseWheel", function(_, delta)
        ApplyScaleDelta(parentFrame, parentName, delta)
    end)
    childFrame.__SinsToolkitFrameMoverChildDragStrip = dragStrip
end

local function EnsureHeaderDragStrips(frame, name)
    local candidates = {
        name .. ".Header",
        name .. "Header",
        name .. ".TitleContainer",
        name .. "TitleContainer",
    }
    for _, childName in ipairs(candidates) do
        local childFrame = ResolveChildFrame(name, childName)
        if childFrame and childFrame ~= frame then
            EnsureChildDragStrip(frame, name, childFrame)
        end
    end
end

function FrameMover:RegisterFrame(name)
    if registered[name] then return end
    registered[name] = true
end

function FrameMover:GetFrameNames()
    local names = {}
    for name in pairs(registered) do
        names[#names + 1] = name
    end
    table.sort(names)
    return names
end

function FrameMover:ApplyFrame(name)
    if not IsEnabled() then
        self:DisableFrame(name)
        return
    end
    local frame = ResolveFrame(name)
    if not IsSafe(frame) then return end
    if IsFrameDisabled(name) then return end

    CaptureOriginalPlacement(frame)
    RestorePosition(frame, name)
    RestoreScale(frame, name)

    frame.__SinsToolkitFrameMover = true

    frame:SetMovable(true)
    frame:EnableMouse(true)
    if frame.SetResizable then frame:SetResizable(true) end
    if frame.EnableMouseWheel then frame:EnableMouseWheel(true) end
    EnsureDragStrip(frame, name)
    EnsureHeaderDragStrips(frame, name)

    if frame.__SinsToolkitFrameMoverHookVersion == HOOK_VERSION then return end
    frame.__SinsToolkitFrameMoverHookVersion = HOOK_VERSION

    frame:HookScript("OnMouseDown", function(self, button)
        StartMovingFrame(self, name, button)
    end)

    frame:HookScript("OnMouseUp", function(self)
        StopMovingFrame(self, name)
    end)

    frame:HookScript("OnMouseWheel", function(self, delta)
        ApplyScaleDelta(self, name, delta)
    end)
end

function FrameMover:Apply()
    if not SinsToolkit.EnsureDB then return end
    SinsToolkit.EnsureDB()
    if not IsEnabled() then
        self:DisableApplied()
        return
    end

    for _, name in ipairs(DEFAULT_FRAMES) do
        self:RegisterFrame(name)
    end
    for name in pairs(registered) do
        self:ApplyFrame(name)
    end
end

function FrameMover:ApplyAddOnFrames(addonName)
    local names = DELAYED_ADDON_FRAMES[addonName]
    if not names then return end
    for _, name in ipairs(names) do
        self:RegisterFrame(name)
        self:ApplyFrame(name)
    end
end

function FrameMover:ApplyVisibleFrame(frame)
    if not IsEnabled() then
        if frame and frame.GetName then self:DisableFrame(frame:GetName()) end
        return
    end
    if not frame or not frame.GetName then return end
    local name = frame:GetName()
    if not name then return end
    if not DEFAULT_FRAME_LOOKUP[name] and not IsLikelyTopLevelPanel(frame, name) then return end
    self:RegisterFrame(name)
    self:ApplyFrame(name)
end

function FrameMover:DisableFrame(name)
    if not name then return end
    local frame = ResolveFrame(name)
    if not frame then return end
    if frame.__SinsToolkitMoving then
        StopMovingFrame(frame, name)
    end
    if frame.__SinsToolkitFrameMoverDragStrip then
        frame.__SinsToolkitFrameMoverDragStrip:EnableMouse(false)
        frame.__SinsToolkitFrameMoverDragStrip:Hide()
    end
end

function FrameMover:DisableApplied()
    for name in pairs(registered) do
        self:DisableFrame(name)
    end
end

function FrameMover:ResetAll()
    if not SinsToolkitDB then return end
    wipe(SinsToolkitDB.frameMoverPositions)
    wipe(SinsToolkitDB.frameScales)
    SinsToolkitDB.frameMoverScales = SinsToolkitDB.frameScales
    wipe(sessionPositions)
    wipe(sessionScales)
    Print("Frame Mover saved positions and scales cleared. Reload UI if a frame is already open.")
end

function FrameMover:GetStatusText()
    return ("Frame Mover: %s\nPosition saving: %s\nScale saving: %s\nMove modifier: %s\nNote: Another addon may also manage frame movement or scaling. Use one frame mover at a time for the most predictable results."):format(
        IsEnabled() and "|cff00ff00Enabled|r" or "|cffff4040Disabled|r",
        SinsToolkitDB and SinsToolkitDB.frameMoverPositionMode or "session",
        SinsToolkitDB and SinsToolkitDB.frameMoverScaleMode or "session",
        SinsToolkitDB and SinsToolkitDB.frameMoverRequireModifier and "Shift required" or "None"
    )
end

function SinsToolkit.ApplyFrameMover()
    if FrameMover.Apply then FrameMover:Apply() end
end

local eventFrame = CreateFrame("Frame")
local applyQueued = false
local loginApplyQueued = false

local function SafeRegisterEvent(frame, event)
    if not frame or not event then return false end
    local ok = pcall(frame.RegisterEvent, frame, event)
    return ok == true
end

local function QueueApply()
    if applyQueued then return end
    if not IsEnabled() then return end
    applyQueued = true
    C_Timer.After(0, function()
        applyQueued = false
        FrameMover:Apply()
    end)
end

local function QueueLoginApply()
    if loginApplyQueued then return end
    loginApplyQueued = true
    QueueApply()
    C_Timer.After(0.75, function()
        loginApplyQueued = false
        QueueApply()
    end)
end

SafeRegisterEvent(eventFrame, "PLAYER_LOGIN")
SafeRegisterEvent(eventFrame, "PLAYER_ENTERING_WORLD")
SafeRegisterEvent(eventFrame, "ADDON_LOADED")
SafeRegisterEvent(eventFrame, "PLAYER_REGEN_ENABLED")
SafeRegisterEvent(eventFrame, "VARIABLES_LOADED")
SafeRegisterEvent(eventFrame, "PLAYER_INTERACTION_MANAGER_FRAME_SHOW")
SafeRegisterEvent(eventFrame, "TRADE_SKILL_SHOW")
eventFrame:SetScript("OnEvent", function(_, event, ...)
    if event == "ADDON_LOADED" then
        local addonName = select(1, ...)
        if addonName and DELAYED_ADDON_FRAMES[addonName] then
            FrameMover:ApplyAddOnFrames(addonName)
            C_Timer.After(0, function()
                FrameMover:ApplyAddOnFrames(addonName)
            end)
        end
        QueueApply()
    elseif event == "PLAYER_LOGIN" or event == "PLAYER_ENTERING_WORLD" or event == "VARIABLES_LOADED" then
        QueueLoginApply()
    else
        QueueApply()
    end
end)

hooksecurefunc("ShowUIPanel", function(frame)
    FrameMover:ApplyVisibleFrame(frame)
    QueueApply()
end)
