local utf8 = utf8 or require("utf8")

---@class TextEditor
---@field editable boolean
---@field multiline boolean
---@field passwordMode boolean
---@field textWrap boolean|"word"|"char"
---@field maxLines number?
---@field maxLength number?
---@field placeholder string?
---@field inputType "text"|"number"|"email"|"url"
---@field textOverflow "clip"|"ellipsis"|"scroll"
---@field scrollable boolean
---@field autoGrow boolean
---@field selectOnFocus boolean
---@field sanitize boolean
---@field allowNewlines boolean
---@field allowTabs boolean
---@field customSanitizer function?
---@field cursorColor Color?
---@field selectionColor Color?
---@field cursorBlinkRate number
---@field _textBuffer string
---@field _lines table?
---@field _wrappedLines table?
---@field _textDirty boolean
---@field _cursorPosition number
---@field _cursorLine number
---@field _cursorColumn number
---@field _cursorBlinkTimer number
---@field _cursorVisible boolean
---@field _cursorBlinkPaused boolean
---@field _cursorBlinkPauseTimer number
---@field _selectionStart number?
---@field _selectionEnd number?
---@field _selectionAnchor number?
---@field _focused boolean
---@field _textScrollX number
---@field onFocus fun(element:Element)?
---@field onBlur fun(element:Element)?
---@field onTextInput fun(element:Element, text:string)?
---@field onTextChange fun(element:Element, text:string)?
---@field onEnter fun(element:Element)?
---@field onSanitize fun(element:Element, original:string, sanitized:string)?
---@field _Context table
---@field _StateManager table
---@field _Color table
---@field _FONT_CACHE table
---@field _getModifiers function
---@field _utils table
---@field _textDragOccurred boolean?
local TextEditor = {}
TextEditor.__index = TextEditor

---@class TextEditorConfig
---@field editable boolean -- Whether text is editable
---@field multiline boolean -- Whether multi-line is supported
---@field passwordMode boolean -- Whether to mask text
---@field textWrap boolean|"word"|"char" -- Text wrapping mode
---@field maxLines number? -- Maximum number of lines
---@field maxLength number? -- Maximum text length in characters
---@field placeholder string? -- Placeholder text when empty
---@field inputType "text"|"number"|"email"|"url" -- Input validation type
---@field textOverflow "clip"|"ellipsis"|"scroll" -- Text overflow behavior
---@field scrollable boolean -- Whether text is scrollable
---@field autoGrow boolean -- Whether element auto-grows with text
---@field selectOnFocus boolean -- Whether to select all text on focus
---@field sanitize boolean? -- Whether to sanitize text input (default: true)
---@field allowNewlines boolean? -- Whether to allow newline characters (default: true in multiline)
---@field allowTabs boolean? -- Whether to allow tab characters (default: true)
---@field customSanitizer function? -- Custom sanitization function
---@field cursorColor Color? -- Cursor color
---@field selectionColor Color? -- Selection background color
---@field cursorBlinkRate number -- Cursor blink rate in seconds

---Create a new TextEditor instance
---@param config TextEditorConfig
---@param deps table Dependencies {Context, StateManager, Color, utils}
---@return table TextEditor instance
function TextEditor.new(config, deps)
  local self = setmetatable({}, TextEditor)

  -- Store dependencies
  self._Context = deps.Context
  self._StateManager = deps.StateManager
  self._Color = deps.Color
  self._FONT_CACHE = deps.utils.FONT_CACHE
  self._getModifiers = deps.utils.getModifiers
  self._utils = deps.utils

  -- Store configuration
  self.editable = config.editable or false
  self.multiline = config.multiline or false
  self.passwordMode = config.passwordMode or false
  self.textWrap = config.textWrap
  self.maxLines = config.maxLines
  self.maxLength = config.maxLength
  self.placeholder = config.placeholder
  self.inputType = config.inputType or "text"
  self.textOverflow = config.textOverflow or "clip"
  self.scrollable = config.scrollable
  self.autoGrow = config.autoGrow
  self.selectOnFocus = config.selectOnFocus or false
  self.cursorColor = config.cursorColor
  self.selectionColor = config.selectionColor
  self.cursorBlinkRate = config.cursorBlinkRate or 0.5

  -- Sanitization configuration
  self.sanitize = config.sanitize ~= false -- Default to true
  -- If allowNewlines is explicitly set, use that value; otherwise follow multiline setting
  if config.allowNewlines ~= nil then
    self.allowNewlines = config.allowNewlines
  else
    self.allowNewlines = self.multiline
  end
  self.allowTabs = config.allowTabs ~= false -- Default to true
  self.customSanitizer = config.customSanitizer

  -- Initialize text buffer state (with sanitization)
  local initialText = config.text or ""
  self._textBuffer = self:_sanitizeText(initialText)
  self._lines = nil
  self._wrappedLines = nil
  self._textDirty = true

  -- Initialize cursor state
  self._cursorPosition = 0
  self._cursorLine = 1
  self._cursorColumn = 0
  self._cursorBlinkTimer = 0
  self._cursorVisible = true
  self._cursorBlinkPaused = false
  self._cursorBlinkPauseTimer = 0

  -- Initialize selection state
  self._selectionStart = nil
  self._selectionEnd = nil
  self._selectionAnchor = nil

  -- Initialize focus state
  self._focused = false

  -- Initialize scroll state
  self._textScrollX = 0

  -- Store callbacks
  self.onFocus = config.onFocus
  self.onBlur = config.onBlur
  self.onTextInput = config.onTextInput
  self.onTextChange = config.onTextChange
  self.onEnter = config.onEnter
  self.onSanitize = config.onSanitize

  return self
end

---Internal: Sanitize text input
---@param text string -- Text to sanitize
---@return string -- Sanitized text
function TextEditor:_sanitizeText(text)
  if not self.sanitize then
    return text
  end

  -- Use custom sanitizer if provided
  if self.customSanitizer then
    return self.customSanitizer(text) or text
  end

  local options = {
    maxLength = self.maxLength,
    allowNewlines = self.allowNewlines,
    allowTabs = self.allowTabs,
    trimWhitespace = false, -- Preserve whitespace in text editors
  }

  local sanitized = self._utils.sanitizeText(text, options)

  return sanitized
end

---Restore state from StateManager (for immediate mode)
---@param element table The parent Element instance
function TextEditor:restoreState(element)
  -- Restore state from StateManager if in immediate mode
  if element._stateId and self._Context._immediateMode then
    local state = self._StateManager.getState(element._stateId)
    if state then
      if state._focused then
        self._focused = true
        self._Context._focusedElement = element
      end
      if state._textBuffer and state._textBuffer ~= "" then
        self._textBuffer = state._textBuffer
      end
      if state._cursorPosition then
        self._cursorPosition = state._cursorPosition
      end
      if state._selectionStart then
        self._selectionStart = state._selectionStart
      end
      if state._selectionEnd then
        self._selectionEnd = state._selectionEnd
      end
      if state._cursorBlinkTimer then
        self._cursorBlinkTimer = state._cursorBlinkTimer
      end
      if state._cursorVisible ~= nil then
        self._cursorVisible = state._cursorVisible
      end
      if state._cursorBlinkPaused ~= nil then
        self._cursorBlinkPaused = state._cursorBlinkPaused
      end
      if state._cursorBlinkPauseTimer then
        self._cursorBlinkPauseTimer = state._cursorBlinkPauseTimer
      end
    end
  end
end

-- ====================
-- Text Buffer Management
-- ====================

---Get current text buffer
---@return string
function TextEditor:getText()
  return self._textBuffer or ""
end

---Set text buffer and mark dirty
---@param element Element? The parent element (for state saving)
---@param text string
---@param skipSanitization boolean? -- Skip sanitization (for trusted input)
function TextEditor:setText(element, text, skipSanitization)
  text = text or ""

  -- Sanitize text unless explicitly skipped
  if not skipSanitization then
    local originalText = text
    text = self:_sanitizeText(text)

    -- Trigger onSanitize callback if text was sanitized
    if text ~= originalText and self.onSanitize and element then
      self.onSanitize(element, originalText, text)
    end
  end

  self._textBuffer = text
  self:_markTextDirty()
  self:_updateTextIfDirty(element)
  self:_validateCursorPosition()
  self:_saveState(element)
end

---Insert text at position
---@param element Element The parent element (for state saving)
---@param text string -- Text to insert
---@param position number? -- Position to insert at (default: cursor position)
---@param skipSanitization boolean? -- Skip sanitization (for internal use)
function TextEditor:insertText(element, text, position, skipSanitization)
  position = position or self._cursorPosition
  local buffer = self._textBuffer or ""

  -- Sanitize text unless explicitly skipped
  if not skipSanitization then
    text = self:_sanitizeText(text)
  end

  -- Check if text is empty after sanitization
  if not text or text == "" then
    return
  end

  -- Check maxLength constraint before inserting
  if self.maxLength then
    local currentLength = utf8.len(buffer) or 0
    local textLength = utf8.len(text) or 0
    local newLength = currentLength + textLength

    if newLength > self.maxLength then
      -- Truncate text to fit
      local remaining = self.maxLength - currentLength
      if remaining <= 0 then
        return
      end
      -- Truncate to remaining characters
      local truncated = ""
      local count = 0
      for _, code in utf8.codes(text) do
        if count >= remaining then
          break
        end
        truncated = truncated .. utf8.char(code)
        count = count + 1
      end
      text = truncated
    end
  end

  -- Convert character position to byte offset
  local byteOffset = utf8.offset(buffer, position + 1) or (#buffer + 1)

  -- Insert text
  local before = buffer:sub(1, byteOffset - 1)
  local after = buffer:sub(byteOffset)
  self._textBuffer = before .. text .. after

  self._cursorPosition = position + utf8.len(text)

  self:_markTextDirty()
  self:_updateTextIfDirty(element)
  self:_validateCursorPosition()
  self:_resetCursorBlink(element, true)
  self:_saveState(element)
end

---Delete text in range
---@param element Element The parent element (for state saving)
---@param startPos number -- Start position (inclusive)
---@param endPos number -- End position (inclusive)
function TextEditor:deleteText(element, startPos, endPos)
  local buffer = self._textBuffer or ""

  -- Ensure valid range
  local textLength = utf8.len(buffer)
  startPos = math.max(0, math.min(startPos, textLength))
  endPos = math.max(0, math.min(endPos, textLength))

  if startPos > endPos then
    startPos, endPos = endPos, startPos
  end

  -- Convert character positions to byte offsets
  local startByte = utf8.offset(buffer, startPos + 1) or 1
  local endByte = utf8.offset(buffer, endPos + 1) or (#buffer + 1)

  -- Delete text
  local before = buffer:sub(1, startByte - 1)
  local after = buffer:sub(endByte)
  self._textBuffer = before .. after

  self:_markTextDirty()
  self:_updateTextIfDirty(element)
  self:_resetCursorBlink(element, true)
  self:_saveState(element)
end

---Replace text in range
---@param element Element The parent element (for state saving)
---@param startPos number -- Start position (inclusive)
---@param endPos number -- End position (inclusive)
---@param newText string -- Replacement text
function TextEditor:replaceText(element, startPos, endPos, newText)
  self:deleteText(element, startPos, endPos)
  self:insertText(element, newText, startPos)
end

---Mark text as dirty (needs recalculation)
function TextEditor:_markTextDirty()
  self._textDirty = true
end

---Update text if dirty (recalculate lines and wrapping)
---@param element Element? The parent element (for wrapping calculations)
function TextEditor:_updateTextIfDirty(element)
  if not self._textDirty then
    return
  end

  self:_splitLines()
  self:_calculateWrapping(element)
  self:_validateCursorPosition()
  self._textDirty = false
end

-- ====================
-- Line Splitting and Wrapping
-- ====================

---Split text into lines (for multi-line text)
function TextEditor:_splitLines()
  if not self.multiline then
    self._lines = { self._textBuffer or "" }
    return
  end

  self._lines = {}
  local text = self._textBuffer or ""

  -- Split on newlines
  for line in (text .. "\n"):gmatch("([^\n]*)\n") do
    table.insert(self._lines, line)
  end

  -- Ensure at least one line
  if #self._lines == 0 then
    self._lines = { "" }
  end
end

---Calculate text wrapping
---@param element Element? The parent element
function TextEditor:_calculateWrapping(element)
  if not self.textWrap or not element then
    self._wrappedLines = nil
    return
  end

  self._wrappedLines = {}
  local availableWidth = element.width - element.padding.left - element.padding.right

  for lineNum, line in ipairs(self._lines or {}) do
    if line == "" then
      table.insert(self._wrappedLines, {
        text = "",
        startIdx = 0,
        endIdx = 0,
        lineNum = lineNum,
      })
    else
      local wrappedParts = self:_wrapLine(element, line, availableWidth)
      for _, part in ipairs(wrappedParts) do
        part.lineNum = lineNum
        table.insert(self._wrappedLines, part)
      end
    end
  end
end

---Wrap a single line of text
---@param element Element The parent element
---@param line string -- Line to wrap
---@param maxWidth number -- Maximum width in pixels
---@return table -- Array of wrapped line parts
function TextEditor:_wrapLine(element, line, maxWidth)
  if not element then
    return { { text = line, startIdx = 0, endIdx = utf8.len(line) } }
  end

  -- Delegate to Renderer
  return element._renderer:wrapLine(element, line, maxWidth)
end

-- ====================
-- Cursor Management
-- ====================

---Set cursor position
---@param element Element? The parent element (for scroll updates)
---@param position number -- Character index (0-based)
function TextEditor:setCursorPosition(element, position)
  self._cursorPosition = position
  self:_validateCursorPosition()
  self:_resetCursorBlink(element)
end

---Get cursor position
---@return number -- Character index (0-based)
function TextEditor:getCursorPosition()
  return self._cursorPosition
end

---Move cursor by delta characters
---@param element Element? The parent element (for scroll updates)
---@param delta number -- Number of characters to move (positive or negative)
function TextEditor:moveCursorBy(element, delta)
  self._cursorPosition = self._cursorPosition + delta
  self:_validateCursorPosition()
  self:_resetCursorBlink(element)
end

---Move cursor to start of text
---@param element Element? The parent element (for scroll updates)
function TextEditor:moveCursorToStart(element)
  self._cursorPosition = 0
  self:_resetCursorBlink(element)
end

---Move cursor to end of text
---@param element Element? The parent element (for scroll updates)
function TextEditor:moveCursorToEnd(element)
  local textLength = utf8.len(self._textBuffer or "")
  self._cursorPosition = textLength
  self:_resetCursorBlink(element)
end

---Move cursor to start of current line
---@param element Element? The parent element (for scroll updates)
function TextEditor:moveCursorToLineStart(element)
  -- For now, just move to start (will be enhanced for multi-line)
  self:moveCursorToStart(element)
end

---Move cursor to end of current line
---@param element Element? The parent element (for scroll updates)
function TextEditor:moveCursorToLineEnd(element)
  -- For now, just move to end (will be enhanced for multi-line)
  self:moveCursorToEnd(element)
end

---Move cursor to start of previous word
function TextEditor:moveCursorToPreviousWord()
  if not self._textBuffer then
    return
  end

  local text = self._textBuffer
  local pos = self._cursorPosition

  if pos <= 0 then
    return
  end

  -- Helper function to get character at position
  local function getCharAt(p)
    if p < 0 or p >= utf8.len(text) then
      return nil
    end
    local offset1 = utf8.offset(text, p + 1)
    local offset2 = utf8.offset(text, p + 2)
    if not offset1 then
      return nil
    end
    if not offset2 then
      return text:sub(offset1)
    end
    return text:sub(offset1, offset2 - 1)
  end

  -- Skip any whitespace/punctuation before current position
  while pos > 0 do
    local char = getCharAt(pos - 1)
    if char and char:match("[%w]") then
      break
    end
    pos = pos - 1
  end

  -- Move to start of current word
  while pos > 0 do
    local char = getCharAt(pos - 1)
    if not char or not char:match("[%w]") then
      break
    end
    pos = pos - 1
  end

  self._cursorPosition = pos
  self:_validateCursorPosition()
end

---Move cursor to start of next word
function TextEditor:moveCursorToNextWord()
  if not self._textBuffer then
    return
  end

  local text = self._textBuffer
  local textLength = utf8.len(text) or 0
  local pos = self._cursorPosition

  if pos >= textLength then
    return
  end

  -- Helper function to get character at position
  local function getCharAt(p)
    if p < 0 or p >= textLength then
      return nil
    end
    local offset1 = utf8.offset(text, p + 1)
    local offset2 = utf8.offset(text, p + 2)
    if not offset1 then
      return nil
    end
    if not offset2 then
      return text:sub(offset1)
    end
    return text:sub(offset1, offset2 - 1)
  end

  -- Skip current word
  while pos < textLength do
    local char = getCharAt(pos)
    if not char or not char:match("[%w]") then
      break
    end
    pos = pos + 1
  end

  -- Skip any whitespace/punctuation
  while pos < textLength do
    local char = getCharAt(pos)
    if char and char:match("[%w]") then
      break
    end
    pos = pos + 1
  end

  self._cursorPosition = pos
  self:_validateCursorPosition()
end

---Validate cursor position (ensure it's within text bounds)
function TextEditor:_validateCursorPosition()
  local textLength = utf8.len(self._textBuffer or "") or 0
  local cursorPos = tonumber(self._cursorPosition) or 0
  self._cursorPosition = math.max(0, math.min(cursorPos, textLength))
end

---Reset cursor blink (show cursor immediately)
---@param element Element? The parent element (for scroll updates)
---@param pauseBlink boolean|nil -- Whether to pause blinking (for typing)
function TextEditor:_resetCursorBlink(element, pauseBlink)
  self._cursorBlinkTimer = 0
  self._cursorVisible = true

  if pauseBlink then
    self._cursorBlinkPaused = true
    self._cursorBlinkPauseTimer = 0
  end

  self:_updateTextScroll(element)
end

---Update text scroll offset to keep cursor visible
---@param element Element? The parent element
function TextEditor:_updateTextScroll(element)
  if not element or self.multiline then
    return
  end

  local font = self:_getFont(element)
  if not font then
    return
  end

  -- Calculate cursor X position in text coordinates
  local cursorText = ""
  if self._textBuffer and self._textBuffer ~= "" and self._cursorPosition > 0 then
    local byteOffset = utf8.offset(self._textBuffer, self._cursorPosition + 1)
    if byteOffset then
      cursorText = self._textBuffer:sub(1, byteOffset - 1)
    end
  end
  local cursorX = font:getWidth(cursorText)

  -- Get available text area width
  local textAreaWidth = element.width
  local scaledContentPadding = element:getScaledContentPadding()
  if scaledContentPadding then
    local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
    textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
  end

  -- Add some padding on the right for the cursor
  local cursorPadding = 4
  local visibleWidth = textAreaWidth - cursorPadding

  -- Adjust scroll to keep cursor visible
  if cursorX - self._textScrollX < 0 then
    self._textScrollX = cursorX
  elseif cursorX - self._textScrollX > visibleWidth then
    self._textScrollX = cursorX - visibleWidth
  end

  -- Ensure we don't scroll past the beginning
  self._textScrollX = math.max(0, self._textScrollX)
end

---Get cursor screen position for rendering (handles multiline text)
---@param element Element? The parent element
---@return number, number -- Cursor X and Y position relative to content area
function TextEditor:_getCursorScreenPosition(element)
  local font = self:_getFont(element)
  if not font then
    return 0, 0
  end

  local text = self._textBuffer or ""
  local cursorPos = self._cursorPosition or 0

  -- Apply password masking for cursor position calculation
  local textForMeasurement = text
  if self.passwordMode and text ~= "" then
    textForMeasurement = string.rep("•", utf8.len(text))
  end

  -- For single-line text, calculate simple X position
  if not self.multiline then
    local cursorText = ""
    if textForMeasurement ~= "" and cursorPos > 0 then
      local byteOffset = utf8.offset(textForMeasurement, cursorPos + 1)
      if byteOffset then
        cursorText = textForMeasurement:sub(1, byteOffset - 1)
      end
    end
    return font:getWidth(cursorText), 0
  end

  -- For multiline text, we need to find which wrapped line the cursor is on
  self:_updateTextIfDirty(element)

  if not element then
    return 0, 0
  end

  -- Get text area width for wrapping
  local textAreaWidth = element.width
  local scaledContentPadding = element:getScaledContentPadding()
  if scaledContentPadding then
    local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
    textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
  end

  -- Split text by actual newlines first
  local lines = {}
  for line in (text .. "\n"):gmatch("([^\n]*)\n") do
    table.insert(lines, line)
  end
  if #lines == 0 then
    lines = { "" }
  end

  -- Track character position as we iterate through lines
  local charCount = 0
  local cursorX = 0
  local cursorY = 0
  local lineHeight = font:getHeight()

  for lineNum, line in ipairs(lines) do
    local lineLength = utf8.len(line) or 0

    -- Check if cursor is on this line
    if cursorPos <= charCount + lineLength then
      local posInLine = cursorPos - charCount

      -- If text wrapping is enabled, find which wrapped segment
      if self.textWrap and textAreaWidth > 0 then
        local wrappedSegments = self:_wrapLine(element, line, textAreaWidth)

        for segmentIdx, segment in ipairs(wrappedSegments) do
          if posInLine >= segment.startIdx and posInLine <= segment.endIdx then
            local posInSegment = posInLine - segment.startIdx
            local segmentText = ""
            if posInSegment > 0 and segment.text ~= "" then
              local endByte = utf8.offset(segment.text, posInSegment + 1)
              if endByte then
                segmentText = segment.text:sub(1, endByte - 1)
              else
                segmentText = segment.text
              end
            end
            cursorX = font:getWidth(segmentText)
            cursorY = (lineNum - 1) * lineHeight + (segmentIdx - 1) * lineHeight

            return cursorX, cursorY
          end
        end
      else
        -- No wrapping, simple calculation
        local lineText = ""
        if posInLine > 0 then
          local endByte = utf8.offset(line, posInLine + 1)
          if endByte then
            lineText = line:sub(1, endByte - 1)
          else
            lineText = line
          end
        end
        cursorX = font:getWidth(lineText)
        cursorY = (lineNum - 1) * lineHeight
        return cursorX, cursorY
      end
    end

    charCount = charCount + lineLength + 1
  end

  -- Cursor is at the very end
  return 0, #lines * lineHeight
end

-- ====================
-- Selection Management
-- ====================

---Set selection range
---@param element Element? The parent element (for scroll updates)
---@param startPos number -- Start position (inclusive)
---@param endPos number -- End position (inclusive)
function TextEditor:setSelection(element, startPos, endPos)
  local textLength = utf8.len(self._textBuffer or "")
  self._selectionStart = math.max(0, math.min(startPos, textLength))
  self._selectionEnd = math.max(0, math.min(endPos, textLength))

  -- Ensure start <= end
  if self._selectionStart > self._selectionEnd then
    self._selectionStart, self._selectionEnd = self._selectionEnd, self._selectionStart
  end

  self:_resetCursorBlink(element)
end

---Get selection range
---@return number?, number? -- Start and end positions, or nil if no selection
function TextEditor:getSelection()
  if not self:hasSelection() then
    return nil, nil
  end
  return self._selectionStart, self._selectionEnd
end

---Check if there is an active selection
---@return boolean
function TextEditor:hasSelection()
  return self._selectionStart ~= nil and self._selectionEnd ~= nil and self._selectionStart ~= self._selectionEnd
end

---Clear selection
function TextEditor:clearSelection()
  self._selectionStart = nil
  self._selectionEnd = nil
  self._selectionAnchor = nil
end

---Select all text
---@param element Element? The parent element (for scroll updates)
function TextEditor:selectAll(element)
  local textLength = utf8.len(self._textBuffer or "")
  self._selectionStart = 0
  self._selectionEnd = textLength
  self:_resetCursorBlink(element)
end

---Get selected text
---@return string? -- Selected text or nil if no selection
function TextEditor:getSelectedText()
  if not self:hasSelection() then
    return nil
  end

  local startPos, endPos = self:getSelection()
  if not startPos or not endPos then
    return nil
  end

  -- Convert character indices to byte offsets
  local text = self._textBuffer or ""
  local startByte = utf8.offset(text, startPos + 1)
  local endByte = utf8.offset(text, endPos + 1)

  if not startByte then
    return ""
  end

  if endByte then
    endByte = endByte - 1
  end

  return string.sub(text, startByte, endByte)
end

---Delete selected text
---@param element Element The parent element (for state saving)
---@return boolean -- True if text was deleted
function TextEditor:deleteSelection(element)
  if not self:hasSelection() then
    return false
  end

  local startPos, endPos = self:getSelection()
  if not startPos or not endPos then
    return false
  end

  self:deleteText(element, startPos, endPos)
  self:clearSelection()
  self._cursorPosition = startPos
  self:_validateCursorPosition()
  self:_saveState(element)

  return true
end

---Get selection rectangles for rendering
---@param element Element The parent element
---@param selStart number -- Selection start position
---@param selEnd number -- Selection end position
---@return table -- Array of rectangles {x, y, width, height}
function TextEditor:_getSelectionRects(element, selStart, selEnd)
  local font = self:_getFont(element)
  if not font or not element then
    return {}
  end

  local text = self._textBuffer or ""
  local rects = {}

  -- Apply password masking
  local textForMeasurement = text
  if self.passwordMode and text ~= "" then
    textForMeasurement = string.rep("•", utf8.len(text))
  end

  -- For single-line text, calculate simple rectangle
  if not self.multiline then
    local startByte = utf8.offset(textForMeasurement, selStart + 1)
    local endByte = utf8.offset(textForMeasurement, selEnd + 1)

    if startByte and endByte then
      local beforeSelection = textForMeasurement:sub(1, startByte - 1)
      local selectedText = textForMeasurement:sub(startByte, endByte - 1)
      local selX = font:getWidth(beforeSelection)
      local selWidth = font:getWidth(selectedText)
      local selY = 0
      local selHeight = font:getHeight()

      table.insert(rects, { x = selX, y = selY, width = selWidth, height = selHeight })
    end

    return rects
  end

  -- For multiline text, handle line wrapping
  self:_updateTextIfDirty(element)

  -- Get text area width for wrapping
  local textAreaWidth = element.width
  local scaledContentPadding = element:getScaledContentPadding()
  if scaledContentPadding then
    local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
    textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
  end

  -- Split text by actual newlines
  local lines = {}
  for line in (text .. "\n"):gmatch("([^\n]*)\n") do
    table.insert(lines, line)
  end
  if #lines == 0 then
    lines = { "" }
  end

  local lineHeight = font:getHeight()
  local charCount = 0
  local visualLineNum = 0

  for lineNum, line in ipairs(lines) do
    local lineLength = utf8.len(line) or 0
    local lineStartChar = charCount
    local lineEndChar = charCount + lineLength

    if selEnd > lineStartChar and selStart <= lineEndChar then
      local selStartInLine = math.max(0, selStart - charCount)
      local selEndInLine = math.min(lineLength, selEnd - charCount)

      if self.textWrap and textAreaWidth > 0 then
        local wrappedSegments = self:_wrapLine(element, line, textAreaWidth)

        for segmentIdx, segment in ipairs(wrappedSegments) do
          if selEndInLine > segment.startIdx and selStartInLine <= segment.endIdx then
            local segSelStart = math.max(segment.startIdx, selStartInLine)
            local segSelEnd = math.min(segment.endIdx, selEndInLine)

            local beforeText = ""
            local selectedText = ""

            if segSelStart > segment.startIdx then
              local startByte = utf8.offset(segment.text, segSelStart - segment.startIdx + 1)
              if startByte then
                beforeText = segment.text:sub(1, startByte - 1)
              end
            end

            local selStartByte = utf8.offset(segment.text, segSelStart - segment.startIdx + 1)
            local selEndByte = utf8.offset(segment.text, segSelEnd - segment.startIdx + 1)
            if selStartByte and selEndByte then
              selectedText = segment.text:sub(selStartByte, selEndByte - 1)
            end

            local selX = font:getWidth(beforeText)
            local selWidth = font:getWidth(selectedText)
            local selY = visualLineNum * lineHeight
            local selHeight = lineHeight

            table.insert(rects, { x = selX, y = selY, width = selWidth, height = selHeight })
          end

          visualLineNum = visualLineNum + 1
        end
      else
        -- No wrapping
        local beforeText = ""
        local selectedText = ""

        if selStartInLine > 0 then
          local startByte = utf8.offset(line, selStartInLine + 1)
          if startByte then
            beforeText = line:sub(1, startByte - 1)
          end
        end

        local selStartByte = utf8.offset(line, selStartInLine + 1)
        local selEndByte = utf8.offset(line, selEndInLine + 1)
        if selStartByte and selEndByte then
          selectedText = line:sub(selStartByte, selEndByte - 1)
        end

        local selX = font:getWidth(beforeText)
        local selWidth = font:getWidth(selectedText)
        local selY = visualLineNum * lineHeight
        local selHeight = lineHeight

        table.insert(rects, { x = selX, y = selY, width = selWidth, height = selHeight })
        visualLineNum = visualLineNum + 1
      end
    else
      -- Selection doesn't intersect, but count visual lines
      if self.textWrap and textAreaWidth > 0 then
        local wrappedSegments = self:_wrapLine(element, line, textAreaWidth)
        visualLineNum = visualLineNum + #wrappedSegments
      else
        visualLineNum = visualLineNum + 1
      end
    end

    charCount = charCount + lineLength + 1
  end

  return rects
end

-- ====================
-- Focus Management
-- ====================

---Focus this element for keyboard input
---@param element Element The parent element
function TextEditor:focus(element)
  if not element then
    return
  end

  if self._Context._focusedElement and self._Context._focusedElement ~= element then
    -- Blur the previously focused element's text editor if it has one
    if self._Context._focusedElement._textEditor then
      self._Context._focusedElement._textEditor:blur(self._Context._focusedElement)
    end
  end

  self._focused = true
  self._Context._focusedElement = element

  self:_resetCursorBlink(element)

  if self.selectOnFocus then
    self:selectAll(element)
  else
    self:moveCursorToEnd(element)
  end

  if self.onFocus then
    self.onFocus(element)
  end

  self:_saveState(element)
end

---Remove focus from this element
---@param element Element The parent element
function TextEditor:blur(element)
  if not element then
    return
  end

  self._focused = false

  if self._Context._focusedElement == element then
    self._Context._focusedElement = nil
  end

  if self.onBlur then
    self.onBlur(element)
  end

  self:_saveState(element)
end

---Check if this element is focused
---@return boolean
function TextEditor:isFocused()
  return self._focused == true
end

-- ====================
-- Input Handling
-- ====================

---Handle text input (character insertion)
---@param element Element The parent element
---@param text string
function TextEditor:handleTextInput(element, text)
  if not self._focused then
    return
  end

  -- Trigger onTextInput callback if defined
  if self.onTextInput then
    local result = self.onTextInput(element, text)
    if result == false then
      return
    end
  end

  local oldText = self._textBuffer

  -- Delete selection if exists
  if self:hasSelection() then
    self:deleteSelection(element)
  end

  -- Insert text at cursor position
  self:insertText(element, text)
  -- Trigger onTextChange callback
  if self.onTextChange and self._textBuffer ~= oldText then
    self.onTextChange(element, self._textBuffer, oldText)
  end

  self:_saveState(element)
end

---Handle key press (special keys)
---@param element Element The parent element
---@param key string -- Key name
---@param scancode string -- Scancode
---@param isrepeat boolean -- Whether this is a key repeat
function TextEditor:handleKeyPress(element, key, scancode, isrepeat)
  if not self._focused then
    return
  end

  local modifiers = self._getModifiers()
  local ctrl = modifiers.ctrl or modifiers.super

  -- Handle cursor movement with selection
  if key == "left" or key == "right" or key == "home" or key == "end" or key == "up" or key == "down" then
    if modifiers.shift and not self._selectionAnchor then
      self._selectionAnchor = self._cursorPosition
    end

    if key == "left" then
      if modifiers.super then
        self:moveCursorToStart(element)
        if not modifiers.shift then
          self:clearSelection()
        end
      elseif modifiers.alt then
        self:moveCursorToPreviousWord()
      elseif self:hasSelection() and not modifiers.shift then
        local startPos, _ = self:getSelection()
        self._cursorPosition = startPos
        self:clearSelection()
      else
        self:moveCursorBy(element, -1)
      end
    elseif key == "right" then
      if modifiers.super then
        self:moveCursorToEnd(element)
        if not modifiers.shift then
          self:clearSelection()
        end
      elseif modifiers.alt then
        self:moveCursorToNextWord()
      elseif self:hasSelection() and not modifiers.shift then
        local _, endPos = self:getSelection()
        self._cursorPosition = endPos
        self:clearSelection()
      else
        self:moveCursorBy(element, 1)
      end
    elseif key == "home" then
      if not self.multiline then
        self:moveCursorToStart(element)
      else
        self:moveCursorToLineStart(element)
      end
      if not modifiers.shift then
        self:clearSelection()
      end
    elseif key == "end" then
      if not self.multiline then
        self:moveCursorToEnd(element)
      else
        self:moveCursorToLineEnd(element)
      end
      if not modifiers.shift then
        self:clearSelection()
      end
    elseif key == "up" or key == "down" then
      if not modifiers.shift then
        self:clearSelection()
      end
    end

    -- Update selection if Shift is pressed
    if modifiers.shift and self._selectionAnchor then
      self:setSelection(element, self._selectionAnchor, self._cursorPosition)
    elseif not modifiers.shift then
      self._selectionAnchor = nil
    end

    self:_resetCursorBlink(element)

  -- Handle backspace and delete
  elseif key == "backspace" then
    local oldText = self._textBuffer
    if self:hasSelection() then
      self:deleteSelection(element)
    elseif ctrl then
      if self._cursorPosition > 0 then
        self:deleteText(element, 0, self._cursorPosition)
        self._cursorPosition = 0
        self:_validateCursorPosition()
      end
    elseif self._cursorPosition > 0 then
      local deleteStart = self._cursorPosition - 1
      local deleteEnd = self._cursorPosition
      self._cursorPosition = deleteStart
      self:deleteText(element, deleteStart, deleteEnd)
      self:_validateCursorPosition()
    end

    if self.onTextChange and self._textBuffer ~= oldText then
      self.onTextChange(element, self._textBuffer, oldText)
    end
    self:_resetCursorBlink(element, true)
  elseif key == "delete" then
    local oldText = self._textBuffer
    if self:hasSelection() then
      self:deleteSelection(element)
    else
      local textLength = utf8.len(self._textBuffer or "")
      if self._cursorPosition < textLength then
        self:deleteText(element, self._cursorPosition, self._cursorPosition + 1)
      end
    end

    if self.onTextChange and self._textBuffer ~= oldText then
      self.onTextChange(element, self._textBuffer, oldText)
    end
    self:_resetCursorBlink(element, true)

  -- Handle return/enter
  elseif key == "return" or key == "kpenter" then
    if self.multiline then
      local oldText = self._textBuffer
      if self:hasSelection() then
        self:deleteSelection(element)
      end
      self:insertText(element, "\n")

      if self.onTextChange and self._textBuffer ~= oldText then
        self.onTextChange(element, self._textBuffer, oldText)
      end
    else
      if self.onEnter then
        self.onEnter(element)
      end
    end
    self:_resetCursorBlink(element, true)

  -- Handle Ctrl/Cmd+A (select all)
  elseif ctrl and key == "a" then
    self:selectAll(element)
    self:_resetCursorBlink(element)

  -- Handle Ctrl/Cmd+C (copy)
  elseif ctrl and key == "c" then
    if self:hasSelection() then
      local selectedText = self:getSelectedText()
      if selectedText then
        love.system.setClipboardText(selectedText)
      end
    end
    self:_resetCursorBlink(element)

  -- Handle Ctrl/Cmd+X (cut)
  elseif ctrl and key == "x" then
    if self:hasSelection() then
      local selectedText = self:getSelectedText()
      if selectedText then
        love.system.setClipboardText(selectedText)

        local oldText = self._textBuffer
        self:deleteSelection(element)

        if self.onTextChange and self._textBuffer ~= oldText then
          self.onTextChange(element, self._textBuffer, oldText)
        end
      end
    end
    self:_resetCursorBlink(element, true)

  -- Handle Ctrl/Cmd+V (paste)
  elseif ctrl and key == "v" then
    local clipboardText = love.system.getClipboardText()
    if clipboardText and clipboardText ~= "" then
      local oldText = self._textBuffer

      if self:hasSelection() then
        self:deleteSelection(element)
      end

      self:insertText(element, clipboardText)

      if self.onTextChange and self._textBuffer ~= oldText then
        self.onTextChange(element, self._textBuffer, oldText)
      end
    end
    self:_resetCursorBlink(element, true)

  -- Handle Escape
  elseif key == "escape" then
    if self:hasSelection() then
      self:clearSelection()
    else
      self:blur(element)
    end
    self:_resetCursorBlink(element)
  end

  self:_saveState(element)
end

-- ====================
-- Mouse Input
-- ====================

---Convert mouse coordinates to cursor position in text
---@param element Element The parent element
---@param mouseX number -- Mouse X coordinate (absolute)
---@param mouseY number -- Mouse Y coordinate (absolute)
---@return number -- Cursor position (character index)
function TextEditor:mouseToTextPosition(element, mouseX, mouseY)
  if not element or not self._textBuffer then
    return 0
  end

  local font = self:_getFont(element)
  if not font then
    return 0
  end

  -- Get content area bounds
  local contentX = (element._absoluteX or element.x) + element.padding.left
  local contentY = (element._absoluteY or element.y) + element.padding.top

  -- Calculate relative position
  local relativeX = mouseX - contentX
  local relativeY = mouseY - contentY

  local text = self._textBuffer
  local textLength = utf8.len(text) or 0

  -- Single-line handling
  if not self.multiline then
    if self._textScrollX then
      relativeX = relativeX + self._textScrollX
    end

    local closestPos = 0
    local closestDist = math.huge

    for i = 0, textLength do
      local offset = utf8.offset(text, i + 1)
      local beforeText = offset and text:sub(1, offset - 1) or text
      local textWidth = font:getWidth(beforeText)
      local dist = math.abs(relativeX - textWidth)

      if dist < closestDist then
        closestDist = dist
        closestPos = i
      end
    end

    return closestPos
  end

  -- Multiline handling
  self:_updateTextIfDirty(element)

  -- Split text into lines
  local lines = {}
  for line in (text .. "\n"):gmatch("([^\n]*)\n") do
    table.insert(lines, line)
  end
  if #lines == 0 then
    lines = { "" }
  end

  local lineHeight = font:getHeight()

  -- Get text area width
  local textAreaWidth = element.width
  local scaledContentPadding = element:getScaledContentPadding()
  if scaledContentPadding then
    local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
    textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
  end

  -- Determine which line was clicked
  local clickedLineNum = math.floor(relativeY / lineHeight) + 1
  clickedLineNum = math.max(1, math.min(clickedLineNum, #lines))

  -- Calculate character offset for lines before clicked line
  local charOffset = 0
  for i = 1, clickedLineNum - 1 do
    local lineLen = utf8.len(lines[i]) or 0
    charOffset = charOffset + lineLen + 1
  end

  local clickedLine = lines[clickedLineNum]
  local lineLen = utf8.len(clickedLine) or 0

  -- Handle wrapped segments
  if self.textWrap and textAreaWidth > 0 then
    local wrappedSegments = self:_wrapLine(element, clickedLine, textAreaWidth)
    local lineYOffset = (clickedLineNum - 1) * lineHeight
    local segmentNum = math.floor((relativeY - lineYOffset) / lineHeight) + 1
    segmentNum = math.max(1, math.min(segmentNum, #wrappedSegments))

    local segment = wrappedSegments[segmentNum]
    local segmentText = segment.text
    local segmentLen = utf8.len(segmentText) or 0
    local closestPos = segment.startIdx
    local closestDist = math.huge

    for i = 0, segmentLen do
      local offset = utf8.offset(segmentText, i + 1)
      local beforeText = offset and segmentText:sub(1, offset - 1) or segmentText
      local textWidth = font:getWidth(beforeText)
      local dist = math.abs(relativeX - textWidth)

      if dist < closestDist then
        closestDist = dist
        closestPos = segment.startIdx + i
      end
    end

    return charOffset + closestPos
  end

  -- No wrapping
  local closestPos = 0
  local closestDist = math.huge

  for i = 0, lineLen do
    local offset = utf8.offset(clickedLine, i + 1)
    local beforeText = offset and clickedLine:sub(1, offset - 1) or clickedLine
    local textWidth = font:getWidth(beforeText)
    local dist = math.abs(relativeX - textWidth)

    if dist < closestDist then
      closestDist = dist
      closestPos = i
    end
  end

  return charOffset + closestPos
end

---Handle mouse click on text
---@param element Element The parent element
---@param mouseX number
---@param mouseY number
---@param clickCount number -- 1=single, 2=double, 3=triple
function TextEditor:handleTextClick(element, mouseX, mouseY, clickCount)
  if not self._focused then
    return
  end

  if clickCount == 1 then
    local pos = self:mouseToTextPosition(element, mouseX, mouseY)
    self:setCursorPosition(element, pos)
    self:clearSelection()
    self._mouseDownPosition = pos
  elseif clickCount == 2 then
    self:_selectWordAtPosition(element, self:mouseToTextPosition(element, mouseX, mouseY))
  elseif clickCount >= 3 then
    self:selectAll(element)
  end

  self:_resetCursorBlink(element)
end

---Handle mouse drag for text selection
---@param element Element The parent element
---@param mouseX number
---@param mouseY number
function TextEditor:handleTextDrag(element, mouseX, mouseY)
  if not self._focused or not self._mouseDownPosition then
    return
  end

  local currentPos = self:mouseToTextPosition(element, mouseX, mouseY)

  if currentPos ~= self._mouseDownPosition then
    self:setSelection(element, self._mouseDownPosition, currentPos)
    self._cursorPosition = currentPos
    self._textDragOccurred = true
  else
    self:clearSelection()
  end

  self:_resetCursorBlink(element)
end

---Select word at given position
---@param element Element? The parent element (for scroll updates)
---@param position number
function TextEditor:_selectWordAtPosition(element, position)
  if not self._textBuffer then
    return
  end

  local text = self._textBuffer
  local textLength = utf8.len(text) or 0

  if textLength == 0 then
    return
  end

  -- Helper to get character at position
  local function getCharAt(p)
    if p < 0 or p >= textLength then
      return nil
    end
    local offset1 = utf8.offset(text, p + 1)
    local offset2 = utf8.offset(text, p + 2)
    if not offset1 then
      return nil
    end
    if not offset2 then
      return text:sub(offset1)
    end
    return text:sub(offset1, offset2 - 1)
  end

  -- Find word boundaries
  local startPos = position
  local endPos = position

  -- Expand left to start of word
  while startPos > 0 do
    local char = getCharAt(startPos - 1)
    if not char or not char:match("[%w]") then
      break
    end
    startPos = startPos - 1
  end

  -- Expand right to end of word
  while endPos < textLength do
    local char = getCharAt(endPos)
    if not char or not char:match("[%w]") then
      break
    end
    endPos = endPos + 1
  end

  self:setSelection(element, startPos, endPos)
  self._cursorPosition = endPos
end

-- ====================
-- Update and Rendering
-- ====================

---Update cursor blink animation
---@param element Element The parent element
---@param dt number -- Delta time
function TextEditor:update(element, dt)
  if not self._focused then
    return
  end

  -- Update cursor blink
  if self._cursorBlinkPaused then
    self._cursorBlinkPauseTimer = (self._cursorBlinkPauseTimer or 0) + dt
    if self._cursorBlinkPauseTimer >= 0.5 then
      self._cursorBlinkPaused = false
      self._cursorBlinkPauseTimer = 0
    end
  else
    self._cursorBlinkTimer = self._cursorBlinkTimer + dt
    if self._cursorBlinkTimer >= self.cursorBlinkRate then
      self._cursorBlinkTimer = 0
      self._cursorVisible = not self._cursorVisible
    end
  end

  -- Save state for immediate mode (cursor blink timer changes need to persist)
  self:_saveState(element)
end

---Update element height based on text content (for autoGrow)
---@param element Element The parent element
function TextEditor:updateAutoGrowHeight(element)
  if not self.multiline or not self.autoGrow or not element then
    return
  end

  local font = self:_getFont(element)
  if not font then
    return
  end

  local text = self._textBuffer or ""
  local lineHeight = font:getHeight()

  -- Get text area width
  local textAreaWidth = element.width
  local scaledContentPadding = element:getScaledContentPadding()
  if scaledContentPadding then
    local borderBoxWidth = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
    textAreaWidth = borderBoxWidth - scaledContentPadding.left - scaledContentPadding.right
  end

  -- Split text by newlines
  local lines = {}
  for line in (text .. "\n"):gmatch("([^\n]*)\n") do
    table.insert(lines, line)
  end
  if #lines == 0 then
    lines = { "" }
  end

  -- Count total wrapped lines
  local totalWrappedLines = 0
  if self.textWrap and textAreaWidth > 0 then
    for _, line in ipairs(lines) do
      if line == "" then
        totalWrappedLines = totalWrappedLines + 1
      else
        local wrappedSegments = self:_wrapLine(element, line, textAreaWidth)
        totalWrappedLines = totalWrappedLines + #wrappedSegments
      end
    end
  else
    totalWrappedLines = #lines
  end

  totalWrappedLines = math.max(1, totalWrappedLines)
  local newContentHeight = totalWrappedLines * lineHeight

  if element.height ~= newContentHeight then
    element.height = newContentHeight
    element._borderBoxHeight = element.height + element.padding.top + element.padding.bottom
    if element.parent and not element._explicitlyAbsolute then
      element.parent:layoutChildren()
    end
  end
end

-- ====================
-- Helper Methods
-- ====================

---Get font for text rendering
---@param element Element? The parent element
---@return love.Font?
function TextEditor:_getFont(element)
  if not element then
    return nil
  end

  -- Delegate to Renderer
  return element._renderer:getFont(element)
end

--- Get current state for persistence
---@return table state TextEditor state snapshot
function TextEditor:getState()
  return {
    _cursorPosition = self._cursorPosition,
    _selectionStart = self._selectionStart,
    _selectionEnd = self._selectionEnd,
    _textBuffer = self._textBuffer,
    _cursorBlinkTimer = self._cursorBlinkTimer,
    _cursorVisible = self._cursorVisible,
    _cursorBlinkPaused = self._cursorBlinkPaused,
    _cursorBlinkPauseTimer = self._cursorBlinkPauseTimer,
    _focused = self._focused,
  }
end

--- Restore state from persistence
---@param state table State to restore
---@param element Element? The parent element (needed for focus restoration)
function TextEditor:setState(state, element)
  if not state then
    return
  end

  if state._cursorPosition ~= nil then
    self._cursorPosition = state._cursorPosition
  end

  if state._selectionStart ~= nil then
    self._selectionStart = state._selectionStart
  end

  if state._selectionEnd ~= nil then
    self._selectionEnd = state._selectionEnd
  end

  if state._textBuffer ~= nil then
    self._textBuffer = state._textBuffer
  end

  if state._cursorBlinkTimer ~= nil then
    self._cursorBlinkTimer = state._cursorBlinkTimer
  end

  if state._cursorVisible ~= nil then
    self._cursorVisible = state._cursorVisible
  end

  if state._cursorBlinkPaused ~= nil then
    self._cursorBlinkPaused = state._cursorBlinkPaused
  end

  if state._cursorBlinkPauseTimer ~= nil then
    self._cursorBlinkPauseTimer = state._cursorBlinkPauseTimer
  end

  if state._focused ~= nil then
    self._focused = state._focused
    -- Restore focused element in Context if this element was focused
    if self._focused and element then
      self._Context._focusedElement = element
    end
  end
end

---Save state to StateManager (for immediate mode)
---@param element Element? The parent element
function TextEditor:_saveState(element)
  if not element or not element._stateId or not self._Context._immediateMode then
    return
  end

  -- Get current state (may have other sub-modules like eventHandler, scrollManager)
  local currentState = self._StateManager.getState(element._stateId) or {}

  -- Update only the textEditor sub-table to match the nested structure
  -- used by element:saveState() at endFrame
  currentState.textEditor = {
    _focused = self._focused,
    _textBuffer = self._textBuffer,
    _cursorPosition = self._cursorPosition,
    _selectionStart = self._selectionStart,
    _selectionEnd = self._selectionEnd,
    _cursorBlinkTimer = self._cursorBlinkTimer,
    _cursorVisible = self._cursorVisible,
    _cursorBlinkPaused = self._cursorBlinkPaused,
    _cursorBlinkPauseTimer = self._cursorBlinkPauseTimer,
  }

  self._StateManager.updateState(element._stateId, currentState)
end

return TextEditor
