local modulePath = (...):match("(.-)[^%.]+$") -- Get the module path prefix (e.g., "libs." or "")
local function req(name)
  return require(modulePath .. "modules." .. name)
end

---@type ErrorHandler
local ErrorHandler = req("ErrorHandler")
local ModuleLoader = req("ModuleLoader")
ModuleLoader.init({ ErrorHandler = ErrorHandler })

local function safeReq(name, isOptional)
  return ModuleLoader.safeRequire(modulePath .. "modules." .. name, isOptional)
end

-- Required core modules
local utils = req("utils")
local Units = req("Units")
local Context = req("Context")
---@type StateManager
local StateManager = req("StateManager")
local RoundedRect = req("RoundedRect")
local Grid = req("Grid")
local InputEvent = req("InputEvent")
local TextEditor = req("TextEditor")
---@type LayoutEngine
local LayoutEngine = req("LayoutEngine")
local Renderer = req("Renderer")
---@type EventHandler
local EventHandler = req("EventHandler")
local ScrollManager = req("ScrollManager")
---@type Element
local Element = req("Element")
---@type Color
local Color = req("Color")
---@type FFI
local FFI = req("FFI")

-- Optional modules (can be excluded in minimal builds)
local Blur = safeReq("Blur", true)
---@type Performance
local Performance = safeReq("Performance", true)
local ImageRenderer = safeReq("ImageRenderer", true)
local ImageScaler = safeReq("ImageScaler", true)
local NinePatch = safeReq("NinePatch", true)
local ImageCache = safeReq("ImageCache", true)
local GestureRecognizer = safeReq("GestureRecognizer", true)
---@type Animation
local Animation = safeReq("Animation", true)
---@type Theme
local Theme = safeReq("Theme", true)

-- Handle Animation.Transform safely
local Transform = Animation.Transform or nil

local enums = utils.enums

---@class FlexLove
local flexlove = Context
flexlove._VERSION = "0.5.1"
flexlove._DESCRIPTION = "UI Library for LÖVE Framework based on flexbox"
flexlove._URL = "https://github.com/mikefreno/FlexLove"
flexlove._LICENSE = [[
  MIT License

  Copyright (c) 2025 Mike Freno

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.
]]

-- GC (Garbage Collection) configuration
flexlove._gcConfig = {
  strategy = "auto", -- "auto", "periodic", "manual", "disabled"
  memoryThreshold = 100, -- MB before forcing GC
  interval = 60, -- Frames between GC steps (for periodic mode)
  stepSize = 200, -- Work units per GC step (higher = more aggressive)
}
flexlove._gcState = {
  framesSinceLastGC = 0,
  lastMemory = 0,
  gcCount = 0,
}

-- Deferred callback queue for operations that cannot run while Canvas is active
flexlove._deferredCallbacks = {}

-- Track accumulated delta time for immediate mode updates
flexlove._accumulatedDt = 0

--- Set up FlexLove for your application's specific needs - configure responsive scaling, theming, rendering mode, and debugging tools
--- Use this to establish a consistent UI foundation that adapts to different screen sizes and provides performance insights
---@param config FlexLoveConfig?
function flexlove.init(config)
  config = config or {}

  flexlove._ErrorHandler = ErrorHandler.init({
    includeStackTrace = config.includeStackTrace,
    logLevel = config.reportingLogLevel,
    logTarget = config.errorLogTarget,
    logFile = config.errorLogFile,
    maxLogSize = config.errorLogMaxSize,
    maxLogFiles = config.maxErrorLogFiles,
    enableRotation = config.errorLogRotateEnabled,
  })

  -- Initialize FFI module (LuaJIT optimizations)
  flexlove._FFI = FFI.init({ ErrorHandler = flexlove._ErrorHandler })

  -- Initialize Performance if available
  if ModuleLoader.isModuleLoaded(modulePath .. "modules.Performance") then
    flexlove._Performance = Performance.init({
      enabled = config.performanceMonitoring or true,
      hudEnabled = false, -- Start with HUD disabled
      hudToggleKey = config.performanceHudKey or "f3",
      hudPosition = config.performanceHudPosition or { x = 10, y = 10 },
      warningThresholdMs = config.performanceWarningThreshold or 13.0,
      criticalThresholdMs = config.performanceCriticalThreshold or 16.67,
      logToConsole = config.performanceLogToConsole or false,
      logWarnings = config.performanceWarnings or false,
      warningsEnabled = config.performanceWarnings or false,
      memoryProfiling = config.memoryProfiling or config.immediateMode and true or false,
    }, { ErrorHandler = flexlove._ErrorHandler, FFI = flexlove._FFI })

    if config.immediateMode then
      flexlove._Performance:registerTableForMonitoring("StateManager.stateStore", StateManager._getInternalState().stateStore)
      flexlove._Performance:registerTableForMonitoring("StateManager.stateMetadata", StateManager._getInternalState().stateMetadata)
    end
  else
    flexlove._Performance = Performance
  end

  -- Initialize optional modules if available
  if ModuleLoader.isModuleLoaded(modulePath .. "modules.ImageRenderer") then
    ImageRenderer.init({ ErrorHandler = flexlove._ErrorHandler, utils = utils })
  end

  if ModuleLoader.isModuleLoaded(modulePath .. "modules.ImageScaler") then
    ImageScaler.init({ ErrorHandler = flexlove._ErrorHandler })
  end

  if ModuleLoader.isModuleLoaded(modulePath .. "modules.NinePatch") then
    NinePatch.init({ ErrorHandler = flexlove._ErrorHandler })
  end

  -- Initialize Blur module with immediate mode optimization config
  if ModuleLoader.isModuleLoaded(modulePath .. "modules.Blur") then
    local blurOptimizations = config.immediateModeBlurOptimizations
    if blurOptimizations == nil then
      blurOptimizations = true -- Default to enabled
    end
    Blur.init({
      ErrorHandler = flexlove._ErrorHandler,
      immediateModeOptimizations = blurOptimizations and config.immediateMode or false,
    })
  end

  -- Initialize required modules
  Units.init({ Context = Context, ErrorHandler = flexlove._ErrorHandler })
  Color.init({ ErrorHandler = flexlove._ErrorHandler, FFI = flexlove._FFI })
  utils.init({ ErrorHandler = flexlove._ErrorHandler })

  -- Initialize optional Animation module
  if ModuleLoader.isModuleLoaded(modulePath .. "modules.Animation") then
    Animation.init({ ErrorHandler = flexlove._ErrorHandler, Color = Color })
  end

  -- Initialize optional Theme module
  if ModuleLoader.isModuleLoaded(modulePath .. "modules.Theme") then
    Theme.init({ ErrorHandler = flexlove._ErrorHandler, Color = Color, utils = utils })
  end

  LayoutEngine.init({ ErrorHandler = flexlove._ErrorHandler, Performance = flexlove._Performance, FFI = flexlove._FFI })
  EventHandler.init({ ErrorHandler = flexlove._ErrorHandler, Performance = flexlove._Performance, InputEvent = InputEvent, utils = utils })

  flexlove._defaultDependencies = {
    Context = Context,
    Theme = Theme,
    Color = Color,
    Units = Units,
    Blur = Blur,
    ImageRenderer = ImageRenderer,
    ImageScaler = ImageScaler,
    NinePatch = NinePatch,
    RoundedRect = RoundedRect,
    ImageCache = ImageCache,
    utils = utils,
    Grid = Grid,
    InputEvent = InputEvent,
    GestureRecognizer = GestureRecognizer,
    StateManager = StateManager,
    TextEditor = TextEditor,
    LayoutEngine = LayoutEngine,
    Renderer = Renderer,
    EventHandler = EventHandler,
    ScrollManager = ScrollManager,
    ErrorHandler = flexlove._ErrorHandler,
    Performance = flexlove._Performance,
    Transform = Transform,
  }

  -- Initialize Element module with dependencies
  Element.init(flexlove._defaultDependencies)

  if config.baseScale then
    flexlove.baseScale = {
      width = config.baseScale.width or 1920,
      height = config.baseScale.height or 1080,
    }

    local currentWidth, currentHeight = Units.getViewport()
    flexlove.scaleFactors.x = currentWidth / flexlove.baseScale.width
    flexlove.scaleFactors.y = currentHeight / flexlove.baseScale.height
  end

  if config.theme and ModuleLoader.isModuleLoaded(modulePath .. "modules.Theme") then
    local success, err = pcall(function()
      if type(config.theme) == "string" then
        Theme.load(config.theme)
        Theme.setActive(config.theme)
        flexlove.defaultTheme = config.theme
      elseif type(config.theme) == "table" then
        local theme = Theme.new(config.theme)
        Theme.setActive(theme)
        flexlove.defaultTheme = theme.name
      end
    end)

    if not success then
      print("[FlexLove] Failed to load theme: " .. tostring(err))
    end
  end

  local immediateMode = config.immediateMode or false
  flexlove.setMode(immediateMode and "immediate" or "retained")

  flexlove._autoFrameManagement = config.autoFrameManagement or false

  -- Configure GC strategy
  if config.gcStrategy then
    flexlove._gcConfig.strategy = config.gcStrategy
  end
  if config.gcMemoryThreshold then
    flexlove._gcConfig.memoryThreshold = config.gcMemoryThreshold
  end
  if config.gcInterval then
    flexlove._gcConfig.interval = config.gcInterval
  end
  if config.gcStepSize then
    flexlove._gcConfig.stepSize = config.gcStepSize
  end

  if config.stateRetentionFrames or config.maxStateEntries then
    StateManager.configure({
      stateRetentionFrames = config.stateRetentionFrames,
      maxStateEntries = config.maxStateEntries,
    })
  end
end

--- Safely schedule operations that modify LÖVE's rendering state (like window mode changes) to execute after all canvas operations complete
--- Prevents crashes from attempting canvas-incompatible operations during rendering
---@param callback function The callback to execute
function flexlove.deferCallback(callback)
  if type(callback) ~= "function" then
    flexlove._ErrorHandler:warn("FlexLove", "CORE_001")
    return
  end
  table.insert(flexlove._deferredCallbacks, callback)
end

--- Execute deferred operations at the safest point in the render cycle - after all canvas operations are complete
--- Call this at the end of love.draw() to enable window resizing and other state-modifying operations without crashes
--- @usage
--- function love.draw()
---   love.graphics.setCanvas(myCanvas)
---   FlexLove.draw()
---   love.graphics.setCanvas() -- Release ALL canvases
---   FlexLove.executeDeferredCallbacks() -- Now safe to execute
--- end
function flexlove.executeDeferredCallbacks()
  if #flexlove._deferredCallbacks == 0 then
    return
  end

  -- Copy callbacks and clear queue before execution
  -- This prevents infinite loops if callbacks defer more callbacks
  local callbacks = flexlove._deferredCallbacks
  flexlove._deferredCallbacks = {}

  for _, callback in ipairs(callbacks) do
    local success, err = xpcall(callback, debug.traceback)
    if not success then
      flexlove._ErrorHandler:warn("FlexLove", "CORE_002", {
        error = tostring(err),
      })
    end
  end
end

--- Recalculate all UI layouts when the window size changes - ensures your interface adapts seamlessly to new dimensions
--- Hook this to love.resize() to maintain proper scaling and positioning across window size changes
function flexlove.resize()
  local newWidth, newHeight = love.window.getMode()

  if flexlove.baseScale then
    flexlove.scaleFactors.x = newWidth / flexlove.baseScale.width
    flexlove.scaleFactors.y = newHeight / flexlove.baseScale.height
  end

  if ModuleLoader.isModuleLoaded(modulePath .. "modules.Blur") then
    Blur.clearCache()
  end

  -- Release old canvases explicitly
  if flexlove._gameCanvas then
    flexlove._gameCanvas:release()
  end
  if flexlove._backdropCanvas then
    flexlove._backdropCanvas:release()
  end

  flexlove._gameCanvas = nil
  flexlove._backdropCanvas = nil
  flexlove._canvasDimensions = { width = 0, height = 0 }

  for _, win in ipairs(flexlove.topElements) do
    win:resize(newWidth, newHeight)
  end
end

--- Switch between immediate mode (React-like, recreates UI each frame) and retained mode (persistent elements) to match your architectural needs
--- Use immediate for simpler state management and declarative UIs, retained for performance-critical applications with complex state
---@param mode "immediate"|"retained"
function flexlove.setMode(mode)
  if mode == "immediate" then
    flexlove._immediateMode = true
    flexlove._immediateModeState = StateManager
    flexlove._frameStarted = false
    flexlove._autoBeganFrame = false
  elseif mode == "retained" then
    flexlove._immediateMode = false
    flexlove._immediateModeState = nil
    flexlove._frameStarted = false
    flexlove._autoBeganFrame = false
    flexlove._currentFrameElements = {}
    flexlove._frameNumber = 0
  else
    error("[FlexLove] Invalid mode: " .. tostring(mode) .. ". Expected 'immediate' or 'retained'")
  end
end

--- Check which rendering mode is active to conditionally handle state management logic
--- Useful for libraries and reusable components that need to adapt to different rendering strategies
---@return "immediate"|"retained"
function flexlove.getMode()
  return flexlove._immediateMode and "immediate" or "retained"
end

--- Manually start a new frame in immediate mode for precise control over the UI lifecycle
--- Only needed when you want explicit frame boundaries; otherwise FlexLove auto-manages frames
function flexlove.beginFrame()
  if not flexlove._immediateMode then
    return
  end

  -- Reset accumulated delta time for new frame
  flexlove._accumulatedDt = 0

  -- Start performance frame timing
  flexlove._Performance:startFrame()

  -- Cleanup elements from PREVIOUS frame (after they've been drawn)
  -- This breaks circular references and allows GC to collect memory
  -- Note: Cleanup is minimal to preserve functionality
  if flexlove._currentFrameElements then
    local function cleanupChildren(elem)
      for _, child in ipairs(elem.children) do
        cleanupChildren(child)
      end
      elem:_cleanup()
    end

    for _, element in ipairs(flexlove._currentFrameElements) do
      if not element.parent then
        cleanupChildren(element)
      end
    end
  end

  flexlove._frameNumber = flexlove._frameNumber + 1
  StateManager.incrementFrame()
  flexlove._currentFrameElements = {}
  flexlove._frameStarted = true
  flexlove.topElements = {}

  Context.clearFrameElements()
end

--- Finalize the frame in immediate mode, triggering layout calculations and state persistence
--- Only needed when manually controlling frames with beginFrame(); otherwise handled automatically
function flexlove.endFrame()
  if not flexlove._immediateMode then
    return
  end

  Context.sortElementsByZIndex()

  -- Layout all top-level elements now that all children have been added
  -- This ensures overflow detection happens with complete child lists
  for _, element in ipairs(flexlove._currentFrameElements) do
    if not element.parent then
      element:layoutChildren() -- Layout with all children present
    end
  end

  -- Auto-update all top-level elements created this frame
  -- This happens AFTER layout so positions are correct
  -- Use accumulated dt from FlexLove.update() calls to properly update animations and cursor blink
  for _, element in ipairs(flexlove._currentFrameElements) do
    if not element.parent then
      element:update(flexlove._accumulatedDt)
    end
  end

  -- Save state for all elements created this frame
  -- State is collected from element and all sub-modules via element:saveState()
  -- This is the ONLY place state is saved in immediate mode
  for _, element in ipairs(flexlove._currentFrameElements) do
    if element.id and element.id ~= "" then
      -- Collect state from element and all sub-modules
      local stateUpdate = element:saveState()

      -- Use optimized update that only changes modified values
      -- Returns true if state was changed (meaning blur cache needs invalidation)
      local stateChanged = StateManager.updateStateIfChanged(element.id, stateUpdate)

      -- Invalidate blur cache if blur-related properties changed
      if stateChanged and (element.backdropBlur or element.contentBlur) and Blur then
        Blur.clearElementCache(element.id)
      end
    end
  end

  StateManager.cleanup()
  StateManager.forceCleanupIfNeeded()
  flexlove._frameStarted = false

  -- End performance frame timing
  flexlove._Performance:endFrame()
  flexlove._Performance:resetFrameCounters()
end

flexlove._gameCanvas = nil
flexlove._backdropCanvas = nil
flexlove._canvasDimensions = { width = 0, height = 0 }

--- Render all UI elements with optional backdrop blur support for glassmorphic effects
--- Place your game scene in gameDrawFunc to enable backdrop blur on UI elements; use postDrawFunc for overlays
---@param gameDrawFunc function|nil pass component draws that should be affected by a backdrop blur
---@param postDrawFunc function|nil pass component draws that should NOT be affected by a backdrop blur
function flexlove.draw(gameDrawFunc, postDrawFunc)
  if flexlove._immediateMode and flexlove._autoBeganFrame then
    flexlove.endFrame()
    flexlove._autoBeganFrame = false
  end

  local outerCanvas = love.graphics.getCanvas()
  local gameCanvas = nil

  if type(gameDrawFunc) == "function" then
    local width, height = love.graphics.getDimensions()

    if not flexlove._gameCanvas or flexlove._canvasDimensions.width ~= width or flexlove._canvasDimensions.height ~= height then
      -- Release old canvases before creating new ones
      if flexlove._gameCanvas then
        flexlove._gameCanvas:release()
      end
      if flexlove._backdropCanvas then
        flexlove._backdropCanvas:release()
      end

      flexlove._gameCanvas = love.graphics.newCanvas(width, height)
      flexlove._backdropCanvas = love.graphics.newCanvas(width, height)
      flexlove._canvasDimensions.width = width
      flexlove._canvasDimensions.height = height
    end

    gameCanvas = flexlove._gameCanvas

    love.graphics.setCanvas(gameCanvas)
    love.graphics.clear()
    gameDrawFunc()
    love.graphics.setCanvas(outerCanvas)

    love.graphics.setColor(1, 1, 1, 1)
    love.graphics.draw(gameCanvas, 0, 0)
  end

  table.sort(flexlove.topElements, function(a, b)
    return a.z < b.z
  end)

  local function hasBackdropBlur(element)
    if element.backdropBlur and element.backdropBlur.radius > 0 then
      return true
    end
    for _, child in ipairs(element.children) do
      if hasBackdropBlur(child) then
        return true
      end
    end
    return false
  end

  local needsBackdropCanvas = false
  for _, win in ipairs(flexlove.topElements) do
    if hasBackdropBlur(win) then
      needsBackdropCanvas = true
      break
    end
  end

  if needsBackdropCanvas and gameCanvas then
    local backdropCanvas = flexlove._backdropCanvas
    local prevColor = { love.graphics.getColor() }

    love.graphics.setCanvas(backdropCanvas)
    love.graphics.clear()
    love.graphics.setColor(1, 1, 1, 1)
    love.graphics.draw(gameCanvas, 0, 0)

    love.graphics.setCanvas(outerCanvas)
    love.graphics.setColor(unpack(prevColor))

    for _, win in ipairs(flexlove.topElements) do
      -- Check if this element tree has backdrop blur
      local needsBackdrop = hasBackdropBlur(win)

      -- Draw element with backdrop blur applied if needed
      if needsBackdrop then
        win:draw(backdropCanvas)
      else
        win:draw(nil)
      end

      -- IMPORTANT: Update backdrop canvas for EVERY element (respecting z-index order)
      -- This ensures that lower z-index elements are visible in the backdrop blur
      -- of higher z-index elements
      love.graphics.setCanvas(backdropCanvas)
      love.graphics.setColor(1, 1, 1, 1)
      win:draw(nil)
      love.graphics.setCanvas(outerCanvas)
    end
  else
    for _, win in ipairs(flexlove.topElements) do
      win:draw(nil)
    end
  end

  if type(postDrawFunc) == "function" then
    postDrawFunc()
  end

  -- Render performance HUD if enabled
  flexlove._Performance:renderHUD()

  love.graphics.setCanvas(outerCanvas)

  -- NOTE: Deferred callbacks are NOT executed here because the calling code
  -- (e.g., main.lua) might still have a canvas active. Callbacks must be
  -- executed by calling FlexLove.executeDeferredCallbacks() at the very end
  -- of love.draw() after ALL canvases have been released.
end

---@param element Element
---@param target Element
---@return boolean
local function isAncestor(element, target)
  local current = target.parent
  while current do
    if current == element then
      return true
    end
    current = current.parent
  end
  return false
end

--- Determine which UI element the user is interacting with at a specific screen position
--- Essential for custom input handling, tooltips, or debugging click targets in complex layouts
---@param x number
---@param y number
---@return Element?
function flexlove.getElementAtPosition(x, y)
  local candidates = {}
  local blockingElements = {}

  local function collectHits(element, scrollOffsetX, scrollOffsetY)
    scrollOffsetX = scrollOffsetX or 0
    scrollOffsetY = scrollOffsetY or 0

    local bx = element.x
    local by = element.y
    local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
    local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)

    -- Adjust mouse position by accumulated scroll offset for hit testing
    local adjustedX = x + scrollOffsetX
    local adjustedY = y + scrollOffsetY

    if adjustedX >= bx and adjustedX <= bx + bw and adjustedY >= by and adjustedY <= by + bh then
      -- Collect interactive elements (those with onEvent handlers)
      if element.onEvent and not element.disabled then
        table.insert(candidates, element)
      end

      -- Collect all visible elements for input blocking
      -- Elements with opacity > 0 block input to elements below them
      if element.opacity > 0 then
        table.insert(blockingElements, element)
      end

      -- Check if this element has scrollable overflow
      local overflowX = element.overflowX or element.overflow
      local overflowY = element.overflowY or element.overflow
      local hasScrollableOverflow = (
        overflowX == "scroll"
        or overflowX == "auto"
        or overflowY == "scroll"
        or overflowY == "auto"
        or overflowX == "hidden"
        or overflowY == "hidden"
      )

      -- Accumulate scroll offset for children if this element has overflow clipping
      local childScrollOffsetX = scrollOffsetX
      local childScrollOffsetY = scrollOffsetY
      if hasScrollableOverflow then
        childScrollOffsetX = childScrollOffsetX + (element._scrollX or 0)
        childScrollOffsetY = childScrollOffsetY + (element._scrollY or 0)
      end

      for _, child in ipairs(element.children) do
        collectHits(child, childScrollOffsetX, childScrollOffsetY)
      end
    end
  end

  for _, element in ipairs(flexlove.topElements) do
    collectHits(element)
  end

  -- Sort both lists by z-index (highest first)
  table.sort(candidates, function(a, b)
    return a.z > b.z
  end)

  table.sort(blockingElements, function(a, b)
    return a.z > b.z
  end)

  -- If we have interactive elements, return the topmost one
  -- But only if there's no blocking element with higher z-index (that isn't an ancestor)
  if #candidates > 0 then
    local topCandidate = candidates[1]

    -- Check if any blocking element would prevent this interaction
    if #blockingElements > 0 then
      local topBlocker = blockingElements[1]
      -- If the top blocker has higher z-index than the top candidate,
      -- and the blocker is NOT an ancestor of the candidate,
      -- return the blocker (even though it has no onEvent, it blocks input)
      if topBlocker.z > topCandidate.z and not isAncestor(topBlocker, topCandidate) then
        return topBlocker
      end
    end

    return topCandidate
  end

  -- No interactive elements, but return topmost blocking element if any
  -- This prevents clicks from passing through non-interactive overlays
  return blockingElements[1]
end

--- Update all UI animations, interactions, and state changes each frame
--- Hook this to love.update() to enable hover effects, animations, text cursors, and scrolling
---@param dt number
function flexlove.update(dt)
  -- Update Performance module with actual delta time for accurate FPS
  flexlove._Performance:updateDeltaTime(dt)

  -- Garbage collection management
  flexlove._manageGC()

  local mx, my = love.mouse.getPosition()
  local topElement = flexlove.getElementAtPosition(mx, my)

  flexlove._activeEventElement = topElement

  -- In immediate mode, accumulate dt and skip updating here - elements will be updated in endFrame after layout
  if flexlove._immediateMode then
    flexlove._accumulatedDt = flexlove._accumulatedDt + dt
  else
    for _, win in ipairs(flexlove.topElements) do
      win:update(dt)
    end
  end

  flexlove._activeEventElement = nil

  -- Note: State saving happens in endFrame() after element:update() is called
  -- This ensures all state changes (including cursor blink) are captured once per frame
end

--- Internal GC management function (called from update)
function flexlove._manageGC()
  local strategy = flexlove._gcConfig.strategy

  if strategy == "disabled" then
    return
  end

  local currentMemory = collectgarbage("count") / 1024 -- Convert to MB
  flexlove._gcState.lastMemory = currentMemory
  flexlove._gcState.framesSinceLastGC = flexlove._gcState.framesSinceLastGC + 1

  -- Check memory threshold (applies to all strategies except disabled)
  if currentMemory > flexlove._gcConfig.memoryThreshold then
    -- Force full GC when exceeding threshold
    collectgarbage("collect")
    flexlove._gcState.gcCount = flexlove._gcState.gcCount + 1
    flexlove._gcState.framesSinceLastGC = 0
    return
  end

  -- Strategy-specific GC
  if strategy == "periodic" then
    -- Run incremental GC step every N frames
    if flexlove._gcState.framesSinceLastGC >= flexlove._gcConfig.interval then
      collectgarbage("step", flexlove._gcConfig.stepSize)
      flexlove._gcState.gcCount = flexlove._gcState.gcCount + 1
      flexlove._gcState.framesSinceLastGC = 0
    end
  elseif strategy == "auto" then
    -- Let Lua's automatic GC handle it, but help with incremental steps
    -- Run a small step every frame to keep memory under control
    if flexlove._gcState.framesSinceLastGC >= 5 then
      collectgarbage("step", 50) -- Small steps to avoid frame drops
      flexlove._gcState.framesSinceLastGC = 0
    end
  end
  -- "manual" strategy: no automatic GC, user must call flexlove.collectGarbage()
end

--- Manually trigger garbage collection to prevent frame drops during critical gameplay moments
--- Use this to control when memory cleanup happens rather than letting it occur unpredictably
---@param mode? string "collect" for full GC, "step" for incremental (default: "collect")
---@param stepSize? number Work units for step mode (default: 200)
function flexlove.collectGarbage(mode, stepSize)
  mode = mode or "collect"
  stepSize = stepSize or 200

  if mode == "collect" then
    collectgarbage("collect")
    flexlove._gcState.gcCount = flexlove._gcState.gcCount + 1
    flexlove._gcState.framesSinceLastGC = 0
  elseif mode == "step" then
    collectgarbage("step", stepSize)
  elseif mode == "count" then
    return collectgarbage("count") / 1024 -- Return memory in MB
  end
end

--- Choose how FlexLove manages memory cleanup to balance performance and memory usage for your app's needs
--- Use "manual" for tight control in performance-critical sections, "auto" for hands-off operation
---@param strategy string "auto", "periodic", "manual", or "disabled"
function flexlove.setGCStrategy(strategy)
  if strategy == "auto" or strategy == "periodic" or strategy == "manual" or strategy == "disabled" then
    flexlove._gcConfig.strategy = strategy
  else
    flexlove._ErrorHandler:warn("FlexLove", "CORE_003", {
      strategy = tostring(strategy),
    })
  end
end

--- Monitor memory management behavior to diagnose performance issues and tune GC settings
--- Use this to identify memory leaks or optimize garbage collection timing
---@return table stats {gcCount, framesSinceLastGC, currentMemoryMB, strategy}
function flexlove.getGCStats()
  return {
    gcCount = flexlove._gcState.gcCount,
    framesSinceLastGC = flexlove._gcState.framesSinceLastGC,
    currentMemoryMB = flexlove._gcState.lastMemory,
    strategy = flexlove._gcConfig.strategy,
    threshold = flexlove._gcConfig.memoryThreshold,
  }
end

--- Forward text input to focused editable elements like text fields and text areas
--- Hook this to love.textinput() to enable text entry in your UI
---@param text string
function flexlove.textinput(text)
  if flexlove._focusedElement then
    flexlove._focusedElement:textinput(text)
  end
end

--- Handle keyboard input for text editing, navigation, and performance overlay toggling
--- Hook this to love.keypressed() to enable text selection, cursor movement, and the performance HUD
---@param key string
---@param scancode string
---@param isrepeat boolean
function flexlove.keypressed(key, scancode, isrepeat)
  -- Handle performance HUD toggle
  flexlove._Performance:keypressed(key)
  if flexlove._focusedElement then
    flexlove._focusedElement:keypressed(key, scancode, isrepeat)
  end
end

--- Enable mouse wheel scrolling in scrollable containers and lists
--- Hook this to love.wheelmoved() to allow users to scroll through content naturally
---@param dx number
---@param dy number
function flexlove.wheelmoved(dx, dy)
  local mx, my = love.mouse.getPosition()

  local function findScrollableAtPosition(elements, x, y)
    for i = #elements, 1, -1 do
      local element = elements[i]

      local bx = element.x
      local by = element.y
      local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
      local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)

      if x >= bx and x <= bx + bw and y >= by and y <= by + bh then
        if #element.children > 0 then
          local childResult = findScrollableAtPosition(element.children, x, y)
          if childResult then
            return childResult
          end
        end

        local overflowX = element.overflowX or element.overflow
        local overflowY = element.overflowY or element.overflow
        if (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") and (element._overflowX or element._overflowY) then
          return element
        end
      end
    end

    return nil
  end

  if flexlove._immediateMode then
    -- Find topmost scrollable element at mouse position using z-index ordering
    for i = #Context._zIndexOrderedElements, 1, -1 do
      local element = Context._zIndexOrderedElements[i]

      local bx = element.x
      local by = element.y
      local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
      local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)

      -- Calculate scroll offset from parent chain
      local scrollOffsetX = 0
      local scrollOffsetY = 0
      local current = element.parent
      while current do
        local overflowX = current.overflowX or current.overflow
        local overflowY = current.overflowY or current.overflow
        local hasScrollableOverflow = (
          overflowX == "scroll"
          or overflowX == "auto"
          or overflowY == "scroll"
          or overflowY == "auto"
          or overflowX == "hidden"
          or overflowY == "hidden"
        )
        if hasScrollableOverflow then
          scrollOffsetX = scrollOffsetX + (current._scrollX or 0)
          scrollOffsetY = scrollOffsetY + (current._scrollY or 0)
        end
        current = current.parent
      end

      -- Adjust mouse position by scroll offset
      local adjustedMx = mx + scrollOffsetX
      local adjustedMy = my + scrollOffsetY

      -- Check if mouse is within element bounds
      if adjustedMx >= bx and adjustedMx <= bx + bw and adjustedMy >= by and adjustedMy <= by + bh then
        -- Check if mouse position is clipped by any parent
        local isClipped = false
        local parentCheck = element.parent
        while parentCheck do
          local parentOverflowX = parentCheck.overflowX or parentCheck.overflow
          local parentOverflowY = parentCheck.overflowY or parentCheck.overflow

          if
            parentOverflowX == "hidden"
            or parentOverflowX == "scroll"
            or parentOverflowX == "auto"
            or parentOverflowY == "hidden"
            or parentOverflowY == "scroll"
            or parentOverflowY == "auto"
          then
            local parentX = parentCheck.x + parentCheck.padding.left
            local parentY = parentCheck.y + parentCheck.padding.top
            local parentW = parentCheck.width
            local parentH = parentCheck.height

            if mx < parentX or mx > parentX + parentW or my < parentY or my > parentY + parentH then
              isClipped = true
              break
            end
          end
          parentCheck = parentCheck.parent
        end

        if not isClipped then
          local overflowX = element.overflowX or element.overflow
          local overflowY = element.overflowY or element.overflow
          if (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") and (element._overflowX or element._overflowY) then
            element:_handleWheelScroll(dx, dy)

            -- Save scroll position to StateManager immediately in immediate mode
            if element._stateId then
              StateManager.updateState(element._stateId, {
                _scrollX = element._scrollX,
                _scrollY = element._scrollY,
              })
            end
            return
          end
        end
      end
    end
  else
    -- In retained mode, use the old tree traversal method
    local scrollableElement = findScrollableAtPosition(flexlove.topElements, mx, my)
    if scrollableElement then
      scrollableElement:_handleWheelScroll(dx, dy)
    end
  end
end

--- Clean up all UI elements and reset FlexLove to initial state when changing scenes or shutting down
--- Use this to prevent memory leaks when transitioning between game states or menus
function flexlove.destroy()
  for _, win in ipairs(flexlove.topElements) do
    win:destroy()
  end
  flexlove.topElements = {}
  flexlove.baseScale = nil
  flexlove.scaleFactors = { x = 1.0, y = 1.0 }
  flexlove._cachedViewport = { width = 0, height = 0 }

  -- Release canvases explicitly before destroying
  if flexlove._gameCanvas then
    flexlove._gameCanvas:release()
  end
  if flexlove._backdropCanvas then
    flexlove._backdropCanvas:release()
  end

  flexlove._gameCanvas = nil
  flexlove._backdropCanvas = nil
  flexlove._canvasDimensions = { width = 0, height = 0 }
  flexlove._focusedElement = nil
  StateManager:reset()
end

--- Create a new UI element with flexbox layout, styling, and interaction capabilities
--- This is your primary API for building interfaces - buttons, panels, text, images, and containers
---@param props ElementProps
---@return Element
function flexlove.new(props)
  props = props or {}

  -- If not in immediate mode, use standard Element.new
  if not flexlove._immediateMode then
    return Element.new(props)
  end

  -- Auto-begin frame if not manually started (convenience feature)
  if not flexlove._frameStarted then
    flexlove.beginFrame()
    flexlove._autoBeganFrame = true
  end

  -- Immediate mode: generate ID if not provided
  if not props.id then
    props.id = StateManager.generateID(props, props.parent)
  end

  -- Get or create state for this element
  local state = StateManager.getState(props.id, {})

  -- Mark state as used this frame
  StateManager.markStateUsed(props.id)

  -- Inject scroll state into props BEFORE creating element
  -- This ensures scroll position is set before layoutChildren/detectOverflow is called
  -- ScrollManager state uses _scrollX/_scrollY with underscore prefix
  if state.scrollManager then
    props._scrollX = state.scrollManager._scrollX or 0
    props._scrollY = state.scrollManager._scrollY or 0
  else
    -- Fallback to old state structure for backward compatibility
    props._scrollX = state._scrollX or 0
    props._scrollY = state._scrollY or 0
  end

  local element = Element.new(props)

  -- Restore all state from StateManager (delegates to sub-modules)
  element:restoreState(state)

  -- Bind element to StateManager for interactive states
  element._stateId = props.id

  -- Set initial theme state based on StateManager state
  -- This will be updated in Element:update() but we need an initial value
  if element.themeComponent then
    local eventState = state.eventHandler or {}
    if element.disabled or eventState.disabled then
      element._themeState = "disabled"
    elseif element.active or eventState.active then
      element._themeState = "active"
    elseif eventState._pressed and next(eventState._pressed) then
      element._themeState = "pressed"
    elseif eventState._hovered then
      element._themeState = "hover"
    else
      element._themeState = "normal"
    end
  end

  table.insert(flexlove._currentFrameElements, element)

  return element
end

--- Check how many UI element states are being tracked in immediate mode to detect memory leaks
--- Use this during development to ensure states are properly cleaned up
---@return number
function flexlove.getStateCount()
  if not flexlove._immediateMode then
    return 0
  end
  return StateManager.getStateCount()
end

--- Remove stored state for a specific element when you know it won't be rendered again
--- Use this to immediately free memory for elements you've removed from your UI
---@param id string
function flexlove.clearState(id)
  if not flexlove._immediateMode then
    return
  end
  StateManager.clearState(id)
end

--- Wipe all element state when transitioning between completely different UI screens
--- Use this for scene transitions to start with a clean slate and prevent state pollution
function flexlove.clearAllStates()
  if not flexlove._immediateMode then
    return
  end
  StateManager.clearAllStates()
end

--- Inspect state management metrics to diagnose performance issues and optimize immediate mode usage
--- Use this to understand state lifecycle and identify unexpected state accumulation
---@return { stateCount: number, frameNumber: number, oldestState: number|nil, newestState: number|nil }
function flexlove.getStateStats()
  if not flexlove._immediateMode then
    return { stateCount = 0, frameNumber = 0 }
  end
  return StateManager.getStats()
end

flexlove.Animation = Animation
flexlove.Color = Color
flexlove.Theme = Theme
flexlove.enums = enums

return flexlove
