--[[
    Commander 3.0 - Window Registry

    Central manager for all Commander3 terminal windows.
    Handles multi-window tracking, focus management, drag targeting, and persistence.
]]

local ADDON_NAME, C3 = ...

-------------------------------------------------------------------------------
-- Constants & Helpers
-------------------------------------------------------------------------------

local MAX_WINDOWS = 16
local LAYOUT_PATH = "/sys/config/window_layout.lua"

-- Get frame bounds, returns nil if any coordinate is missing
local function GetFrameBounds(frame)
    if not frame then return nil end
    local left, right, top, bottom = frame:GetLeft(), frame:GetRight(), frame:GetTop(), frame:GetBottom()
    if not (left and right and top and bottom) then return nil end
    return left, right, top, bottom
end

-- Check if point is within rectangle
local function PointInRect(x, y, left, right, top, bottom)
    return x >= left and x <= right and y >= bottom and y <= top
end

-------------------------------------------------------------------------------
-- Toast Notification
-------------------------------------------------------------------------------

local function ShowRejectedWindowToast(focusedWindow)
    local width = (focusedWindow and focusedWindow.frame) and focusedWindow.frame:GetWidth() or 700
    local height = (focusedWindow and focusedWindow.frame) and focusedWindow.frame:GetHeight() or 450

    local scale = UIParent:GetEffectiveScale()
    local cursorX, cursorY = GetCursorPosition()

    local f = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    f:SetSize(width, height)
    f:SetFrameStrata("TOOLTIP")
    f:SetPoint("CENTER", UIParent, "BOTTOMLEFT", cursorX / scale, cursorY / scale)
    f:SetBackdrop({
        bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
        insets = { left = 1, right = 1, top = 1, bottom = 1 },
    })
    f:SetBackdropColor(0.4, 0.1, 0.1, 0.8)
    f:SetBackdropBorderColor(0.8, 0.2, 0.2, 1)

    local text = f:CreateFontString(nil, "OVERLAY")
    text:SetFont(C3.UI.Theme and C3.UI.Theme.Font or "Fonts\\FRIZQT__.TTF", 14, "")
    text:SetPoint("CENTER")
    local errColor = C3.UI.Theme and C3.UI.Theme:GetColor("accent.error") or "BF616A"
    text:SetText("|cff" .. errColor .. "Maximum windows reached|r")
    f:Show()

    -- Fade out
    local elapsed = 0
    f:SetScript("OnUpdate", function(self, dt)
        elapsed = elapsed + dt
        if elapsed >= 0.6 then
            self:Hide()
            self:SetParent(nil)
            return
        end
        self:SetAlpha(1 - elapsed / 0.6)
    end)
end

-------------------------------------------------------------------------------
-- Window Registry
-------------------------------------------------------------------------------

local WindowRegistry = {
    _windows = {},        -- id -> window
    _windowOrder = {},    -- Array of window IDs (z-order, last = topmost)
    _focusedWindow = nil,
    _primaryWindow = nil,
    _nextFrameLevel = 100,
    _lastFocusedBeforeHide = nil,  -- Remember focus when hiding all windows
    _visibleBeforeHide = {},       -- Set of window IDs that were visible before HideAll
}

C3.WindowRegistry = WindowRegistry

-- Remove window ID from order array
local function RemoveFromOrder(self, windowId)
    for i, id in ipairs(self._windowOrder) do
        if id == windowId then
            table.remove(self._windowOrder, i)
            return i
        end
    end
    return nil
end

-------------------------------------------------------------------------------
-- Registration
-------------------------------------------------------------------------------

function WindowRegistry:Register(window)
    if not window or not window.id then
        C3:Debug("WindowRegistry: Cannot register window without id")
        return false
    end

    if #self._windowOrder >= MAX_WINDOWS then
        C3:Debug("WindowRegistry: Maximum windows reached (" .. MAX_WINDOWS .. ")")
        return false
    end

    self._windows[window.id] = window
    table.insert(self._windowOrder, window.id)

    if not self._primaryWindow then
        self._primaryWindow = window
        window._isPrimary = true
    end

    window.frame:SetFrameLevel(self._nextFrameLevel)
    self._nextFrameLevel = self._nextFrameLevel + 10

    C3:Debug("WindowRegistry: Registered window " .. window.id)
    return true
end

function WindowRegistry:Unregister(window)
    if not window or not window.id then return end

    self._windows[window.id] = nil
    RemoveFromOrder(self, window.id)

    if self._focusedWindow == window then
        self._focusedWindow = nil
    end

    if self._primaryWindow == window then
        self._primaryWindow = nil
        local nextId = self._windowOrder[1]
        if nextId and self._windows[nextId] then
            self._primaryWindow = self._windows[nextId]
            self._primaryWindow._isPrimary = true
        end
    end

    C3:Debug("WindowRegistry: Unregistered window " .. window.id)
end

-------------------------------------------------------------------------------
-- Window Lifecycle
-------------------------------------------------------------------------------

function WindowRegistry:Close(window)
    if not window then return false end

    if window._isPrimary then
        window:Hide()
        self:_focusNextWindow(window)
        return false
    end

    self:Unregister(window)
    window.frame:Hide()
    window.frame:SetParent(nil)
    self:_focusNextWindow(window)
    return true
end

function WindowRegistry:_focusNextWindow(excludeWindow)
    for i = #self._windowOrder, 1, -1 do
        local win = self._windows[self._windowOrder[i]]
        if win and win ~= excludeWindow and win.frame:IsShown() then
            self:Focus(win)
            return
        end
    end
end

-------------------------------------------------------------------------------
-- Focus Management
-------------------------------------------------------------------------------

function WindowRegistry:Focus(window)
    if not window then return end

    local prev = self._focusedWindow
    if prev and prev ~= window then
        prev.hasFocus = false
        prev:UpdateBorderColor()
    end

    self._focusedWindow = window
    window.hasFocus = true
    window:UpdateBorderColor()

    self._nextFrameLevel = self._nextFrameLevel + 10
    window.frame:SetFrameLevel(self._nextFrameLevel)

    RemoveFromOrder(self, window.id)
    table.insert(self._windowOrder, window.id)

    window:Show()
end

function WindowRegistry:GetFocused()
    return self._focusedWindow
end

-------------------------------------------------------------------------------
-- Window Queries
-------------------------------------------------------------------------------

function WindowRegistry:GetPrimary()
    return self._primaryWindow
end

function WindowRegistry:SetPrimary(window)
    if self._primaryWindow == window then return end

    if self._primaryWindow then
        self._primaryWindow._isPrimary = false
    end

    self._primaryWindow = window
    if window then
        window._isPrimary = true
        C3.Terminal = window
    end
end

function WindowRegistry:GetAll()
    local windows = {}
    for _, id in ipairs(self._windowOrder) do
        if self._windows[id] then
            windows[#windows + 1] = self._windows[id]
        end
    end
    return windows
end

function WindowRegistry:GetCount()
    return #self._windowOrder
end

function WindowRegistry:CanCreate()
    return #self._windowOrder < MAX_WINDOWS
end

-------------------------------------------------------------------------------
-- Bulk Visibility (Toggle All Windows)
-------------------------------------------------------------------------------

function WindowRegistry:AnyVisible()
    for _, id in ipairs(self._windowOrder) do
        local window = self._windows[id]
        if window and window.frame:IsShown() then
            return true
        end
    end
    return false
end

function WindowRegistry:ShowAll()
    -- Ensure primary exists
    if #self._windowOrder == 0 then
        C3:GetTerminal()
    end

    -- Only show windows that were visible before HideAll
    -- _visibleBeforeHide stores actual window references, not IDs
    local hasRestoreSet = next(self._visibleBeforeHide) ~= nil

    if hasRestoreSet then
        -- Restore only previously visible windows
        for window in pairs(self._visibleBeforeHide) do
            if window and window.frame then
                window:Show()
            end
        end
    else
        -- First toggle ever - show all registered windows
        for _, id in ipairs(self._windowOrder) do
            local window = self._windows[id]
            if window then
                window:Show()
            end
        end
    end

    -- Restore focus
    local toFocus = self._lastFocusedBeforeHide or self._primaryWindow
    if toFocus and toFocus.frame and toFocus.frame:IsShown() then
        self:Focus(toFocus)
    end

    -- Clear restore state
    self._lastFocusedBeforeHide = nil
    self._visibleBeforeHide = {}
end

function WindowRegistry:HideAll()
    self._lastFocusedBeforeHide = self._focusedWindow

    -- First pass: collect all visible windows (before any modifications)
    local visibleWindows = {}
    for _, id in ipairs(self._windowOrder) do
        local window = self._windows[id]
        if window and window.frame:IsShown() then
            visibleWindows[#visibleWindows + 1] = window
        end
    end

    -- Store as set keyed by window reference (not ID, which could change)
    self._visibleBeforeHide = {}
    for _, window in ipairs(visibleWindows) do
        self._visibleBeforeHide[window] = true
    end

    -- Second pass: hide them (safe, not iterating _windowOrder anymore)
    for _, window in ipairs(visibleWindows) do
        window:Hide()
    end

    self._focusedWindow = nil
end

function WindowRegistry:ToggleAll()
    if self:AnyVisible() then
        self:HideAll()
    else
        self:ShowAll()
    end
end

function WindowRegistry:ShowMaxWindowsToast()
    local focusedWindow = self._focusedWindow or self._primaryWindow
    ShowRejectedWindowToast(focusedWindow)
end

-------------------------------------------------------------------------------
-- Window Lookup (for drag targeting)
-------------------------------------------------------------------------------

function WindowRegistry:FindWindowAt(x, y)
    for i = #self._windowOrder, 1, -1 do
        local window = self._windows[self._windowOrder[i]]
        if not window or not window.frame:IsShown() then
            -- continue
        else
            local left, right, top, bottom = GetFrameBounds(window.frame)
            if left and PointInRect(x, y, left, right, top, bottom) then
                return window
            end
        end
    end
    return nil
end

function WindowRegistry:FindWindowTopEdgeAt(x, y, excludeWindow)
    local TOP_EDGE_PERCENT = 0.08

    for i = #self._windowOrder, 1, -1 do
        local window = self._windows[self._windowOrder[i]]
        if not window or window == excludeWindow or not window.frame:IsShown() then
            -- continue
        elseif window.tabBar and window.tabBar:IsShown() then
            -- skip windows with visible tab bars
        else
            local left, right, top, bottom = GetFrameBounds(window.frame)
            if left then
                local zoneBottom = top - (top - bottom) * TOP_EDGE_PERCENT
                if PointInRect(x, y, left, right, top, zoneBottom) then
                    return window
                end
            end
        end
    end
    return nil
end

function WindowRegistry:FindWindowTabBarAt(x, y, excludeWindow)
    local DEFAULT_TAB_HEIGHT = 24

    for i = #self._windowOrder, 1, -1 do
        local window = self._windows[self._windowOrder[i]]
        if not window or window == excludeWindow or not window.frame:IsShown() or not window.tabs then
            -- continue
        elseif not window.tabBar or not window.tabBar:IsShown() then
            -- tab bar hidden
        else
            -- Try actual tabBar bounds
            local left, right, top, bottom = GetFrameBounds(window.tabBar)
            if left then
                if PointInRect(x, y, left, right, top, bottom) then
                    return window
                end
            else
                -- Fallback: calculate from frame position
                local fLeft, fRight, fTop = window.frame:GetLeft(), window.frame:GetRight(), window.frame:GetTop()
                if fLeft and fRight and fTop then
                    local height = window.tabBar:GetHeight()
                    if not height or height <= 0 then height = DEFAULT_TAB_HEIGHT end
                    local tabBottom, tabTop = fTop - 1, fTop + height - 1
                    if PointInRect(x, y, fLeft, fRight, tabTop, tabBottom) then
                        return window
                    end
                end
            end
        end
    end
    return nil
end

-------------------------------------------------------------------------------
-- Window Merging
-------------------------------------------------------------------------------

function WindowRegistry:MergeWindows(sourceWindow, targetWindow, insertPosition)
    if not sourceWindow or not targetWindow then return false end
    if sourceWindow == targetWindow then return false end
    if not sourceWindow.tabs or not targetWindow.tabs then return false end

    -- Copy tabs array since DetachTab modifies the original
    local tabsToMove = {}
    for _, tab in ipairs(sourceWindow.tabs:GetTabs()) do
        tabsToMove[#tabsToMove + 1] = tab
    end
    if #tabsToMove == 0 then return false end

    local sourceActive = sourceWindow.tabs:GetActiveTab()

    if sourceWindow.BeginTabTransfer then sourceWindow:BeginTabTransfer() end
    for _, tab in ipairs(tabsToMove) do
        sourceWindow.tabs:DetachTab(tab)
    end
    if sourceWindow.EndTabTransfer then sourceWindow:EndTabTransfer() end

    local position = insertPosition or (targetWindow.tabs:GetTabCount() + 1)
    position = math.max(1, math.min(position, targetWindow.tabs:GetTabCount() + 1))

    for _, tab in ipairs(tabsToMove) do
        targetWindow.tabs:AddTab(tab, position)
        position = position + 1
    end

    if sourceActive then
        targetWindow.tabs:SelectTab(sourceActive)
    end

    self:Focus(targetWindow)

    if sourceWindow._isPrimary then
        self:SetPrimary(targetWindow)
    end

    if sourceWindow.tabs:GetTabCount() == 0 then
        sourceWindow:Close()
    end

    return true
end

-------------------------------------------------------------------------------
-- Persistence
-------------------------------------------------------------------------------

function WindowRegistry:SaveLayout()
    local vfs = C3:GetVFS()
    if not vfs then return end

    local state = { _version = 1, _savedAt = time(), windows = {} }

    for _, window in ipairs(self:GetAll()) do
        local frame = window.frame
        local ws = {
            id = window.id,
            isPrimary = window._isPrimary,
            x = frame:GetLeft(),
            y = frame:GetTop(),
            width = frame:GetWidth(),
            height = frame:GetHeight(),
            shown = frame:IsShown(),
            tabs = {},
        }

        if window.tabs then
            for _, tab in ipairs(window.tabs:GetTabs()) do
                ws.tabs[#ws.tabs + 1] = {
                    sessionId = tab.session and tab.session.id,
                    name = tab.name,
                    isActive = tab.isActive,
                }
            end
        end

        state.windows[#state.windows + 1] = ws
    end

    vfs:MakeDirP("/sys/config")
    vfs:Write(LAYOUT_PATH, C3.Serialize and C3.Serialize(state) or "")
    C3:Debug("WindowRegistry: Saved layout (" .. #state.windows .. " windows)")
end

function WindowRegistry:LoadLayout()
    local vfs = C3:GetVFS()
    if not vfs then return nil end

    local content = vfs:Read(LAYOUT_PATH)
    if not content or content == "" then return nil end

    local state = C3.Deserialize and C3.Deserialize(content)
    if not state or not state.windows then return nil end

    C3:Debug("WindowRegistry: Loaded layout (" .. #state.windows .. " windows)")
    return state
end

function WindowRegistry:RestoreLayout(state)
    if not state or not state.windows then return end

    for _, ws in ipairs(state.windows) do
        local window = self._windows[ws.id]
        if window then
            window.frame:ClearAllPoints()
            window.frame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", ws.x, ws.y)
            window.frame:SetSize(ws.width, ws.height)
            if ws.shown then window:Show() end
        end
    end
end

-- Auto-save on logout
local saveFrame = CreateFrame("Frame")
saveFrame:RegisterEvent("PLAYER_LOGOUT")
saveFrame:SetScript("OnEvent", function()
    WindowRegistry:SaveLayout()
end)

return WindowRegistry
