local _, Addon = ...

local Gathering = {}
Addon.Gathering = Gathering
local L = Addon.L

local mapFrame
local viewport
local canvas
local helpText
local nativeContainer
local blipContainer
local nativeState
local eventFrame
local active = false
local blipsActive = false
local applying = false
local errorShown = false
local blipErrorShown = false
local openMiyamapAfterWarning = false

local GATHERING_WARNING_DIALOG = "MIYAMAP_GATHERING_NATIVE_WARNING"

local function SafeCall(func, ...)
    if type(func) ~= "function" then
        return nil
    end

    local ok, value = pcall(func, ...)
    if ok then
        return value
    end
    return nil
end

local function SavePoints(frame)
    local points = {}
    for index = 1, frame:GetNumPoints() do
        points[index] = { frame:GetPoint(index) }
    end
    return points
end

local function IsInstancedContent()
    local inInstance, instanceType = IsInInstance()
    return inInstance and instanceType and instanceType ~= "none"
end

local function SaveMapFramePosition()
    if not mapFrame or type(Addon.db.gatheringMapPosition) == "table" then
        return
    end

    local point, relativeFrame, relativePoint, x, y = mapFrame:GetPoint(1)
    if not point then
        return
    end

    local relativeFrameName = relativeFrame and relativeFrame:GetName()
    Addon.db.gatheringMapPosition = {
        point = point,
        relativeFrame = relativeFrameName or "UIParent",
        relativePoint = relativePoint or point,
        x = x or 0,
        y = y or 0,
    }
end

local function RestoreMapFramePosition()
    local saved = Addon.db.gatheringMapPosition
    if not mapFrame or type(saved) ~= "table" or type(saved.point) ~= "string" then
        return
    end

    local relativeFrame = _G[saved.relativeFrame or "UIParent"] or UIParent
    local relativePoint = saved.relativePoint or saved.point
    local x = tonumber(saved.x) or 0
    local y = tonumber(saved.y) or 0

    mapFrame:ClearAllPoints()
    mapFrame:SetPoint(saved.point, relativeFrame, relativePoint, x, y)
    Addon.db.point = saved.point
    Addon.db.relativeFrame = relativeFrame:GetName() or "UIParent"
    Addon.db.relativePoint = relativePoint
    Addon.db.x = x
    Addon.db.y = y
end

local function DetachMapFrameFromMinimap()
    if not mapFrame or not Minimap then
        return
    end

    local anchoredToMinimap = false
    for index = 1, mapFrame:GetNumPoints() do
        local _, relativeFrame = mapFrame:GetPoint(index)
        if relativeFrame == Minimap then
            anchoredToMinimap = true
            break
        end
    end
    if not anchoredToMinimap then
        return
    end

    local centerX, centerY = mapFrame:GetCenter()
    if not centerX or not centerY then
        return
    end

    centerX = math.floor(centerX + 0.5)
    centerY = math.floor(centerY + 0.5)
    mapFrame:ClearAllPoints()
    mapFrame:SetPoint("CENTER", UIParent, "BOTTOMLEFT", centerX, centerY)
    Addon.db.point = "CENTER"
    Addon.db.relativeFrame = "UIParent"
    Addon.db.relativePoint = "BOTTOMLEFT"
    Addon.db.x = centerX
    Addon.db.y = centerY
end

local function SaveNativeState()
    if nativeState or not Minimap then
        return
    end

    nativeState = {
        parent = Minimap:GetParent(),
        points = SavePoints(Minimap),
        width = Minimap:GetWidth(),
        height = Minimap:GetHeight(),
        scale = Minimap:GetScale(),
        alpha = Minimap:GetAlpha(),
        strata = Minimap:GetFrameStrata(),
        level = Minimap:GetFrameLevel(),
        mouseEnabled = Minimap.IsMouseEnabled and Minimap:IsMouseEnabled(),
        mouseWheelEnabled = Minimap.IsMouseWheelEnabled and Minimap:IsMouseWheelEnabled(),
        zoom = Minimap.GetZoom and Minimap:GetZoom(),
        drawGroundTextures = SafeCall(C_Minimap.GetDrawGroundTextures),
        ignoreRotate = SafeCall(C_Minimap.IsRotateMinimapIgnored),
        children = {},
    }

    for _, child in ipairs({ Minimap:GetChildren() }) do
        if not (child.IsForbidden and child:IsForbidden()) then
            nativeState.children[#nativeState.children + 1] = {
                frame = child,
                shown = child:IsShown(),
            }
        end
    end
end

local function ShowCustomMap()
    if nativeContainer then
        nativeContainer:Hide()
    end
    if canvas then
        canvas:Show()
    end
end

local function RestoreNativeMinimap(showAfterRestore)
    if not Minimap or not nativeState then
        active = false
        blipsActive = false
        RestoreMapFramePosition()
        ShowCustomMap()
        return true
    end

    local saved = nativeState
    local ok = pcall(function()
        Minimap:Hide()
        Minimap:SetParent(saved.parent or MinimapCluster or UIParent)
        Minimap:ClearAllPoints()
        for _, point in ipairs(saved.points or {}) do
            Minimap:SetPoint(unpack(point))
        end
        Minimap:SetScale(saved.scale or 1)
        Minimap:SetSize(
            math.max(saved.width or 140, 1),
            math.max(saved.height or saved.width or 140, 1)
        )
        Minimap:SetAlpha(saved.alpha or 1)
        Minimap:SetFrameStrata(saved.strata or "LOW")
        Minimap:SetFrameLevel(saved.level or 1)
        if saved.mouseEnabled ~= nil and Minimap.EnableMouse then
            Minimap:EnableMouse(saved.mouseEnabled)
        end
        if saved.mouseWheelEnabled ~= nil and Minimap.EnableMouseWheel then
            Minimap:EnableMouseWheel(saved.mouseWheelEnabled)
        end
        if saved.zoom ~= nil and Minimap.SetZoom then
            pcall(Minimap.SetZoom, Minimap, saved.zoom)
        end
        if saved.drawGroundTextures ~= nil and C_Minimap.SetDrawGroundTextures then
            pcall(C_Minimap.SetDrawGroundTextures, saved.drawGroundTextures)
        end
        if saved.ignoreRotate ~= nil and C_Minimap.SetIgnoreRotateMinimap then
            pcall(C_Minimap.SetIgnoreRotateMinimap, saved.ignoreRotate)
        end
        for _, childState in ipairs(saved.children or {}) do
            if childState.frame then
                pcall(childState.frame.SetShown, childState.frame, childState.shown)
            end
        end
        Minimap:SetShown(showAfterRestore)
    end)

    active = false
    blipsActive = false
    if blipContainer then
        blipContainer:Hide()
    end
    ShowCustomMap()
    if ok then
        nativeState = nil
        RestoreMapFramePosition()
    end
    return ok
end

local function ActivateNativeView()
    if active then
        nativeContainer:Show()
        Minimap:Show()
        return true
    end
    if blipsActive and not RestoreNativeMinimap(false) then
        return false
    end
    if not Minimap or (Minimap.IsForbidden and Minimap:IsForbidden()) then
        return false
    end

    SaveMapFramePosition()
    SaveNativeState()
    DetachMapFrameFromMinimap()
    active = true
    blipsActive = false
    blipContainer:Hide()
    canvas:Hide()
    if helpText then
        helpText:Hide()
    end
    nativeContainer:Show()

    local ok = pcall(function()
        Minimap:Hide()
        Minimap:SetParent(nativeContainer)
        Minimap:ClearAllPoints()
        Minimap:SetPoint("TOPLEFT", nativeContainer, "TOPLEFT")
        Minimap:SetPoint("BOTTOMRIGHT", nativeContainer, "BOTTOMRIGHT")
        Minimap:SetScale(1)
        Minimap:SetAlpha(1)
        Minimap:SetFrameStrata(mapFrame:GetFrameStrata())
        Minimap:SetFrameLevel(nativeContainer:GetFrameLevel() + 1)
        if Minimap.EnableMouse then
            Minimap:EnableMouse(false)
        end
        if Minimap.EnableMouseWheel then
            Minimap:EnableMouseWheel(false)
        end
        if nativeState.zoom ~= nil and Minimap.SetZoom then
            pcall(Minimap.SetZoom, Minimap, nativeState.zoom)
        end
        if C_Minimap.SetDrawGroundTextures then
            pcall(C_Minimap.SetDrawGroundTextures, true)
        end
        if nativeState.ignoreRotate ~= nil and C_Minimap.SetIgnoreRotateMinimap then
            pcall(C_Minimap.SetIgnoreRotateMinimap, nativeState.ignoreRotate)
        end
        for _, childState in ipairs(nativeState.children or {}) do
            if childState.frame then
                pcall(childState.frame.Hide, childState.frame)
            end
        end
        Minimap:Show()
    end)

    if not ok then
        RestoreNativeMinimap(false)
    end
    return ok
end

local function UpdateNativeBlipGeometry(playerX, playerY)
    if not blipsActive or not Minimap or not Addon.state.mapID or not Addon.state.layer then
        return false
    end

    if not playerX or not playerY then
        playerX, playerY = Addon.Coordinates:GetUnitMapPosition(Addon.state.mapID, "player")
    end
    if not playerX or not playerY then
        Minimap:Hide()
        return false
    end

    local worldOK, worldWidth = pcall(C_Map.GetMapWorldSize, Addon.state.mapID)
    local radiusOK, viewRadius = pcall(C_Minimap.GetViewRadius)
    if not worldOK
        or not radiusOK
        or Addon:IsSecret(worldWidth)
        or Addon:IsSecret(viewRadius)
        or type(worldWidth) ~= "number"
        or type(viewRadius) ~= "number"
        or worldWidth <= 0
        or viewRadius <= 0
    then
        Minimap:Hide()
        return false
    end

    local pixelsPerYard = Addon.state.layer.layerWidth / worldWidth
    local diameter = viewRadius * 2 * pixelsPerYard * Addon.state.scale
    local maximum = math.max(viewport:GetWidth(), viewport:GetHeight(), 1) * 4
    diameter = Addon:Clamp(diameter, 8, maximum)

    Minimap:ClearAllPoints()
    Minimap:SetPoint(
        "CENTER",
        canvas,
        "TOPLEFT",
        playerX * canvas:GetWidth(),
        -playerY * canvas:GetHeight()
    )
    Minimap:SetSize(diameter, diameter)
    Minimap:Show()
    return true
end

local function ActivateBlipOverlay()
    if active then
        if not RestoreNativeMinimap(false) then
            return false
        end
        Addon.db.gatheringMapPosition = nil
    end
    if blipsActive then
        blipContainer:Show()
        UpdateNativeBlipGeometry()
        return true
    end
    if not Minimap or (Minimap.IsForbidden and Minimap:IsForbidden()) then
        return false
    end

    SaveNativeState()
    DetachMapFrameFromMinimap()
    active = false
    blipsActive = true
    ShowCustomMap()
    blipContainer:Show()

    local ok = pcall(function()
        Minimap:Hide()
        Minimap:SetParent(blipContainer)
        Minimap:ClearAllPoints()
        Minimap:SetScale(1)
        Minimap:SetAlpha(1)
        Minimap:SetFrameStrata(mapFrame:GetFrameStrata())
        Minimap:SetFrameLevel(blipContainer:GetFrameLevel() + 1)
        if Minimap.EnableMouse then
            Minimap:EnableMouse(false)
        end
        if Minimap.EnableMouseWheel then
            Minimap:EnableMouseWheel(false)
        end
        for _, childState in ipairs(nativeState.children or {}) do
            if childState.frame then
                pcall(childState.frame.Hide, childState.frame)
            end
        end
        if C_Minimap.SetDrawGroundTextures then
            pcall(C_Minimap.SetDrawGroundTextures, false)
        end
        if C_Minimap.SetIgnoreRotateMinimap then
            pcall(C_Minimap.SetIgnoreRotateMinimap, true)
        end
    end)

    if not ok then
        RestoreNativeMinimap(false)
        return false
    end
    UpdateNativeBlipGeometry()
    return true
end

local function RefreshControls()
    if Addon.AddonButtons and Addon.AddonButtons.UpdateGatheringButton then
        Addon.AddonButtons:UpdateGatheringButton()
    end
    if Addon.Options and Addon.Options.Refresh then
        Addon.Options:Refresh()
    end
    if Addon.AddonButtons and Addon.AddonButtons.UpdateServicesButton then
        Addon.AddonButtons:UpdateServicesButton()
    end
end

local function RebuildCustomMap()
    C_Timer.After(0, function()
        if mapFrame and mapFrame:IsShown() and not active then
            Addon.MapView:Rebuild(true)
        end
    end)
end

function Gathering:RegisterWarningDialog()
    StaticPopupDialogs[GATHERING_WARNING_DIALOG] = {
        text = L.GATHERING_WARNING,
        button1 = L.GATHERING_ACCEPT,
        button2 = L.CANCEL,
        OnAccept = function()
            local shouldOpenMiyamap = openMiyamapAfterWarning
            openMiyamapAfterWarning = false
            Addon.db.gatheringWarningAccepted = true
            if shouldOpenMiyamap and not Addon.state.replacementApplied then
                Addon.UI:SetReplacementMode(true, false)
            end
            Addon.Gathering:SetShown(true, true)
        end,
        OnCancel = function()
            openMiyamapAfterWarning = false
            Addon.db.gatheringNativeView = false
            Addon.Gathering:Apply()
            RefreshControls()
        end,
        timeout = 0,
        whileDead = true,
        hideOnEscape = true,
        preferredIndex = 3,
    }
end

function Gathering:Initialize(ui)
    mapFrame = ui.mapFrame
    viewport = ui.viewport
    canvas = ui.canvas
    helpText = ui.helpText

    nativeContainer = CreateFrame("Frame", "MapaEnMiniaturaGatheringView", viewport)
    nativeContainer:SetAllPoints(viewport)
    nativeContainer:SetFrameLevel(viewport:GetFrameLevel() + 40)
    nativeContainer:SetClipsChildren(true)
    nativeContainer:Hide()

    blipContainer = CreateFrame("Frame", "MapaEnMiniaturaNativeBlips", viewport)
    blipContainer:SetAllPoints(viewport)
    blipContainer:SetFrameLevel(viewport:GetFrameLevel() + 5)
    blipContainer:SetClipsChildren(true)
    blipContainer:Hide()

    self:RegisterWarningDialog()

    eventFrame = CreateFrame("Frame")
    eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
    eventFrame:RegisterEvent("MINIMAP_UPDATE_ZOOM")
    eventFrame:SetScript("OnEvent", function(_, event)
        if event == "MINIMAP_UPDATE_ZOOM" and blipsActive then
            C_Timer.After(0, function()
                UpdateNativeBlipGeometry()
            end)
            return
        end
        C_Timer.After(0.10, function()
            self:Apply()
        end)
    end)
end

function Gathering:Apply()
    if applying or not nativeContainer then
        return
    end
    applying = true

    local shouldBeActive = Addon.state.replacementApplied
        and Addon.db.gatheringNativeView
        and Addon.db.gatheringWarningAccepted
        and mapFrame:IsShown()
        and not IsInstancedContent()
    local shouldShowBlips = Addon.state.replacementApplied
        and Addon.db.showNativeServices
        and mapFrame:IsShown()
        and not IsInstancedContent()

    local wasActive = active
    local ok
    if shouldBeActive then
        ok = ActivateNativeView()
    elseif shouldShowBlips then
        ok = ActivateBlipOverlay()
    else
        ok = RestoreNativeMinimap(not Addon.state.replacementApplied)
        if Addon.state.replacementApplied and Minimap then
            Minimap:Hide()
        end
        if wasActive then
            RebuildCustomMap()
        end
    end

    applying = false
    if not ok then
        if shouldBeActive then
            if not errorShown then
                errorShown = true
                Addon:Print(L.GATHERING_ERROR)
            end
        elseif not blipErrorShown then
            blipErrorShown = true
            Addon:Print(L.NATIVE_BLIP_ERROR)
        end
    end
end

function Gathering:SetShown(shown, announce)
    shown = not not shown
    if shown and not Addon.db.gatheringWarningAccepted then
        Addon.db.gatheringNativeView = false
        RefreshControls()
        StaticPopup_Show(GATHERING_WARNING_DIALOG)
        return false
    end

    Addon.db.gatheringNativeView = shown
    self:Apply()
    if not shown and not active then
        Addon.db.gatheringMapPosition = nil
    end
    RefreshControls()
    if announce then
        Addon:Print(shown and L.GATHERING_ENABLED_MESSAGE or L.GATHERING_DISABLED_MESSAGE)
    end
    return true
end

function Gathering:Toggle(announce, openMiyamap)
    local shown = not self:IsEnabled()
    local shouldOpenMiyamap = shown
        and openMiyamap
        and not Addon.state.replacementApplied
        and not IsInstancedContent()

    if shouldOpenMiyamap and not Addon.db.gatheringWarningAccepted then
        openMiyamapAfterWarning = true
    elseif shouldOpenMiyamap then
        Addon.UI:SetReplacementMode(true, false)
    end
    return self:SetShown(shown, announce)
end

function Gathering:IsActive()
    return active
end

function Gathering:IsBlipOverlayActive()
    return blipsActive
end

function Gathering:UpdateNativeBlips(playerX, playerY)
    if blipsActive then
        UpdateNativeBlipGeometry(playerX, playerY)
    end
end

function Gathering:SetNativeServicesShown(shown, announce)
    Addon.db.showNativeServices = not not shown
    self:Apply()
    RefreshControls()
    if announce then
        Addon:Print(shown and L.NEARBY_SERVICES_ENABLED or L.NEARBY_SERVICES_DISABLED)
    end
end

function Gathering:AreNativeServicesShown()
    return Addon.db.showNativeServices == true
end

function Gathering:IsEnabled()
    return Addon.db.gatheringNativeView == true
end

function Gathering:IsPaused()
    return self:IsEnabled() and IsInstancedContent()
end

function Gathering:IsAvailable()
    return Minimap ~= nil and not (Minimap.IsForbidden and Minimap:IsForbidden())
end

BINDING_NAME_MIYAMAP_TOGGLE_GATHERING = L.GATHERING_BINDING

function Miyamap_ToggleGathering()
    if Addon.db and Addon.Gathering then
        Addon.Gathering:Toggle(true, true)
    end
end
