--[[
    Commander 3.0 - Drag Controller
    Centralizes drag state and hit-testing for tabs and windows.
]]
local ADDON_NAME, C3 = ...

local DragController = {
    active = false,
    kind = nil,
    state = nil,
    ticker = nil,
}
C3.DragController = DragController

local UPDATE_INTERVAL = 0.016
local TAB_BAR_FALLBACK_HEIGHT = 24
local DETACH_BUFFER_PERCENT = 0.05  -- 5% of window size to trigger detach

local function hasVisibleTabBar(window)
    return window and window.tabBar and window.tabBar:IsShown()
end

local function getCursorPosition()
    local scale = UIParent:GetEffectiveScale()
    local cursorX, cursorY = GetCursorPosition()
    return cursorX / scale, cursorY / scale
end

function DragController:IsActive()
    return self.active
end

function DragController:IsDraggingTab()
    return self.active and self.kind == "tab"
end

function DragController:IsDraggingWindow()
    return self.active and self.kind == "window"
end

---Begin dragging a tab
---@param tabStrip table
---@param tab table
function DragController:BeginTabDrag(tabStrip, tab)
    if not tabStrip or not tab then return end

    if self.active then
        self:EndDrag(true)
    end

    self.active = true
    self.kind = "tab"
    self.state = {
        sourceStrip = tabStrip,
        sourceWindow = tabStrip.window,
        tab = tab,
        action = "neutral",
        label = "Dragging tab",
        kind = "tab",
    }

    if tab.SetDragging then
        tab:SetDragging(true)
    end

    if C3.DragFeedback then
        C3.DragFeedback:Start("tab", "Dragging tab")
    end

    self:_startTicker()
    self:Update()
end

---Begin dragging a window
---@param window table
function DragController:BeginWindowDrag(window)
    if not window then return end

    if self.active then
        if self.kind == "window" and self.state and self.state.sourceWindow == window then
            return
        end
        self:EndDrag(true)
    end

    self.active = true
    self.kind = "window"
    self.state = {
        sourceWindow = window,
        action = "neutral",
        label = "Dragging window",
        kind = "window",
    }

    if C3.DragFeedback then
        C3.DragFeedback:Start("window", "Dragging window")
    end

    self:_startTicker()
    self:Update()
end

---End the current drag and apply the drop action
---@param cancelOnly boolean?
function DragController:EndDrag(cancelOnly)
    if not self.active then return end

    self:_stopTicker()

    if C3.DragFeedback then
        C3.DragFeedback:Stop()
    end

    local state = self.state
    if state and state.tab and state.tab.SetDragging then
        state.tab:SetDragging(false)
    end

    if not cancelOnly then
        self:_applyDrop(state)
    end

    self.active = false
    self.kind = nil
    self.state = nil
    C3.DragState = nil
end

function DragController:_startTicker()
    if self.ticker then
        self.ticker:Cancel()
        self.ticker = nil
    end
    self.ticker = C_Timer.NewTicker(UPDATE_INTERVAL, function()
        self:Update()
    end)
end

function DragController:_stopTicker()
    if self.ticker then
        self.ticker:Cancel()
        self.ticker = nil
    end
end

function DragController:Update()
    if not self.active or not self.state then return end

    local cursorX, cursorY = getCursorPosition()
    local action, label, targetWindow, insertPosition, targetTab, indicatorPos
    if self.kind == "window" then
        action, label, targetWindow, insertPosition, targetTab, indicatorPos =
            self:_determineWindowAction(self.state, cursorX, cursorY)
    else
        action, label, targetWindow, insertPosition, targetTab, indicatorPos =
            self:_determineAction(self.state, cursorX, cursorY)
    end

    local state = self.state
    state.action = action
    state.label = label
    state.targetWindow = targetWindow
    state.insertPosition = insertPosition
    state.targetTab = targetTab

    C3.DragState = C3.DragState or {}
    C3.DragState.action = action
    C3.DragState.targetWindow = targetWindow
    C3.DragState.insertPosition = insertPosition
    C3.DragState.targetTab = targetTab

    if C3.DragFeedback then
        C3.DragFeedback:Update({
            cursorX = cursorX,
            cursorY = cursorY,
            action = action,
            label = label,
            targetWindow = targetWindow,
            insertPosition = indicatorPos,
        })
    end
end

function DragController:_applyDrop(state)
    if not state then return end

    if state.kind == "window" then
        if state.action == "merge" and state.targetWindow and C3.WindowRegistry and C3.WindowRegistry.MergeWindows then
            C3.WindowRegistry:MergeWindows(state.sourceWindow, state.targetWindow, state.insertPosition)
        end
        return
    end

    if not state.sourceStrip or not state.tab then return end

    local action = state.action or "neutral"
    local targetWindow = state.targetWindow
    local insertPosition = state.insertPosition
    local targetTab = state.targetTab
    local sourceStrip = state.sourceStrip

    if action == "detach" then
        sourceStrip:_detachToNewWindow(state.tab)
        return
    end

    if action == "swap" and targetWindow and targetTab then
        sourceStrip:_swapWithTarget(state.tab, targetWindow, targetTab)
        return
    end

    if action == "insert" then
        if targetWindow and targetWindow ~= state.sourceWindow then
            sourceStrip:_transferTab(state.tab, targetWindow, insertPosition)
        else
            sourceStrip:_reorderTab(state.tab, insertPosition)
        end
        return
    end

    if action == "move" and targetWindow then
        sourceStrip:_transferTab(state.tab, targetWindow, insertPosition)
        return
    end

    sourceStrip:_layoutTabs()
end

function DragController:_determineAction(state, cursorX, cursorY)
    local sourceWindow = state.sourceWindow
    local sourceStrip = state.sourceStrip
    local targetWindow
    if C3.WindowRegistry and C3.WindowRegistry.FindWindowTabBarAt then
        targetWindow = C3.WindowRegistry:FindWindowTabBarAt(cursorX, cursorY)
    end
    if not targetWindow and C3.WindowRegistry then
        targetWindow = C3.WindowRegistry:FindWindowAt(cursorX, cursorY)
    end

    local targetUsesTabBar = hasVisibleTabBar(targetWindow)
    local overTabBar = targetWindow and self:_isCursorOverTabBar(targetWindow, cursorX, cursorY)

    if targetWindow and targetUsesTabBar and not overTabBar then
        targetWindow = nil
        targetUsesTabBar = false
        overTabBar = false
    end

    if targetWindow and overTabBar then
        local targetTab, insertPosition = self:_getTabOrInsertAt(targetWindow, cursorX)

        if targetWindow == sourceWindow then
            if targetTab and targetTab ~= state.tab then
                return "swap", "Swap", targetWindow, nil, targetTab, nil
            elseif insertPosition then
                return "insert", "Reorder", targetWindow, insertPosition, nil, insertPosition
            end
            return "neutral", "Dragging", nil, nil, nil, nil
        else
            if targetTab then
                return "swap", "Swap", targetWindow, nil, targetTab, nil
            elseif insertPosition then
                return "insert", "Insert here", targetWindow, insertPosition, nil, insertPosition
            end
            local fallback = self:_getInsertPositionAtEnd(targetWindow)
            if fallback then
                return "move", "Move here", targetWindow, fallback, nil, nil
            end
            return "neutral", "Dragging", nil, nil, nil, nil
        end
    end

    if targetWindow and targetWindow ~= sourceWindow and not targetUsesTabBar then
        local fallback = self:_getInsertPositionAtEnd(targetWindow)
        if fallback then
            return "move", "Move here", targetWindow, fallback, nil, nil
        end
    end

    if sourceStrip and sourceStrip:GetTabCount() > 1 then
        if sourceWindow and self:_isCursorOutsideWindow(sourceWindow, cursorX, cursorY) then
            return "detach", "New window", nil, nil, nil, nil
        end
    end

    return "neutral", "Dragging", nil, nil, nil, nil
end

function DragController:_determineWindowAction(state, cursorX, cursorY)
    local sourceWindow = state.sourceWindow

    -- Use cursor position for targeting: find if cursor is over any target window's tab bar
    local targetWindow
    if C3.WindowRegistry then
        -- First check visible tab bars
        if C3.WindowRegistry.FindWindowTabBarAt then
            targetWindow = C3.WindowRegistry:FindWindowTabBarAt(cursorX, cursorY, sourceWindow)
        end
        -- Fallback: check top edge of windows with hidden tab bars (single-tab windows)
        if not targetWindow and C3.WindowRegistry.FindWindowTopEdgeAt then
            targetWindow = C3.WindowRegistry:FindWindowTopEdgeAt(cursorX, cursorY, sourceWindow)
        end
    end

    if targetWindow then
        -- Use cursor X for insert position (where user is pointing)
        local insertPosition = self:_getMergeInsertPosition(targetWindow, cursorX)
        return "merge", "Merge into window", targetWindow, insertPosition, nil, insertPosition
    end

    return "neutral", "Dragging window", nil, nil, nil, nil
end

function DragController:_getInsertPositionAtEnd(window)
    if not window or not window.tabs then return nil end
    local tabs = window.tabs.tabs or {}
    if #tabs == 0 then
        return 1
    end
    return #tabs + 1
end

function DragController:_getMergeInsertPosition(window, cursorX)
    if not window or not window.tabs then return nil end

    local targetTab, insertPosition = self:_getTabOrInsertAt(window, cursorX)
    if targetTab and window.tabs and window.tabs._getTabIndex then
        local idx = window.tabs:_getTabIndex(targetTab)
        if idx then
            return idx + 1
        end
    end

    if insertPosition then
        return insertPosition
    end

    return self:_getInsertPositionAtEnd(window)
end

function DragController:_getTabBarBounds(window)
    if not window then return nil end

    -- Try actual tabBar bounds first (most accurate when visible)
    if window.tabBar and not window.tabBar:IsShown() then
        return nil
    end

    if window.tabBar then
        local left = window.tabBar:GetLeft()
        local right = window.tabBar:GetRight()
        local top = window.tabBar:GetTop()
        local bottom = window.tabBar:GetBottom()
        if left and right and top and bottom then
            return left, right, top, bottom
        end
    end

    -- Fallback: calculate from frame position
    -- Tab bar is ABOVE frame: SetPoint("BOTTOMLEFT", frame, "TOPLEFT", 0, -1)
    if window.frame then
        local left = window.frame:GetLeft()
        local right = window.frame:GetRight()
        local frameTop = window.frame:GetTop()
        if left and right and frameTop then
            local height = TAB_BAR_FALLBACK_HEIGHT
            if window.tabBar then
                local h = window.tabBar:GetHeight()
                if h and h > 0 then
                    height = h
                end
            end
            -- Tab bar bottom is at frameTop - 1 (1px overlap), top is frameTop + height - 1
            local tabBarBottom = frameTop - 1
            local tabBarTop = frameTop + height - 1
            return left, right, tabBarTop, tabBarBottom
        end
    end

    return nil
end

function DragController:_isCursorOverTabBar(window, cursorX, cursorY)
    local left, right, top, bottom = self:_getTabBarBounds(window)
    if not left or not right or not top or not bottom then return false end
    return cursorX >= left and cursorX <= right and cursorY <= top and cursorY >= bottom
end

function DragController:_getTabOrInsertAt(window, cursorX)
    if not window or not window.tabs then return nil, nil end

    local tabStrip = window.tabs
    local tabs = tabStrip.tabs

    if #tabs == 0 then return nil, 1 end

    for i, tab in ipairs(tabs) do
        local btn = tab.button
        if btn and btn:IsShown() then
            local left = btn:GetLeft()
            local right = btn:GetRight()

            if left and right and cursorX >= left and cursorX <= right then
                local width = right - left
                local centerStart = left + width * 0.2
                local centerEnd = right - width * 0.2

                if cursorX >= centerStart and cursorX <= centerEnd then
                    return tab, nil
                elseif cursorX < centerStart then
                    return nil, i
                else
                    return nil, i + 1
                end
            end

            if i > 1 then
                local prevBtn = tabs[i - 1].button
                if prevBtn then
                    local prevRight = prevBtn:GetRight()
                    if prevRight and cursorX > prevRight and cursorX < left then
                        return nil, i
                    end
                end
            end
        end
    end

    local lastTab = tabs[#tabs]
    if lastTab and lastTab.button then
        local lastRight = lastTab.button:GetRight()
        if lastRight and cursorX > lastRight then
            return nil, #tabs + 1
        end
    end

    local firstTab = tabs[1]
    if firstTab and firstTab.button then
        local firstLeft = firstTab.button:GetLeft()
        if firstLeft and cursorX < firstLeft then
            return nil, 1
        end
    end

    return nil, nil
end

function DragController:_isCursorOutsideWindow(window, cursorX, cursorY)
    if not window or not window.frame then return false end

    local frame = window.frame
    local left = frame:GetLeft()
    local right = frame:GetRight()
    local top = frame:GetTop()
    local bottom = frame:GetBottom()

    if not (left and right and top and bottom) then return false end

    -- Calculate buffer as percentage of window's smaller dimension
    local width = right - left
    local height = top - bottom
    local buffer = math.min(width, height) * DETACH_BUFFER_PERCENT

    return cursorX < left - buffer or
        cursorX > right + buffer or
        cursorY > top + buffer or
        cursorY < bottom - buffer
end

return DragController
