class_name DialogueTextbox extends PanelContainer

const SCENE:PackedScene = preload("res://ui/component/DialogueTextbox.tscn")

enum AdvancementMode { PLAYER, TIMED }

const LINES_PER_PAGE:int = 4
const CHARS_PER_SECOND:float = 24.0
const PAUSE_COMMA:float = 0.15
const PAUSE_SENTENCE:float = 0.4
const PAUSE_ELLIPSIS_DOT:float = 0.3
const READING_CHARS_PER_SECOND:float = 16.0
const MAX_WIDTH:float = 120.0

signal dismissed

var _entity:Entity = null
var _parsedText:String = ""
var _startLine:int = 0
var _linesPerPage:int = 0
var _revealTimer:float = 0.0
var _pauseTimer:float = 0.0
var _autoAdvanceTimer:float = 0.0
var _isRevealing:bool = false
var _isWaitingForInput:bool = false
var _advancementMode:AdvancementMode = AdvancementMode.PLAYER

@onready var _speakerLabel:Label = $VBoxContainer/SpeakerLabel
@onready var _bodyLabel:RichTextLabel = $VBoxContainer/BodyLabel
@onready var _advanceIndicator:Label = $VBoxContainer/AdvanceIndicator

func _ready() -> void:
  size.x = MAX_WIDTH
  visible = false
  var scrollbar = _bodyLabel.get_v_scroll_bar()
  if scrollbar:
    scrollbar.modulate.a = 0.0
    scrollbar.mouse_filter = Control.MOUSE_FILTER_IGNORE

func setup(line:DialogueLine, entity:Entity, mode:AdvancementMode = AdvancementMode.PLAYER) -> void:
  _entity = entity
  _advancementMode = mode
  _speakerLabel.text = entity.displayName if entity else ""
  _speakerLabel.visible = _speakerLabel.text != ""

  # Set width explicitly before text so get_character_line() uses correct metrics.
  # The Container layout pass hasn't run yet, so we derive inner width from the
  # panel StyleBox margins rather than waiting for a deferred layout frame.
  size.x = MAX_WIDTH
  _bodyLabel.size.x = _bodyLabelWidth()
  _bodyLabel.text = line.text
  _bodyLabel.visible_characters = -1
  _bodyLabel.scroll_to_line(0)

  # get_character_line() forces synchronous text shaping via _validate_line_caches(),
  # so we can compute pre-wrapped text right now without any pre-pass frame.
  _parsedText = _buildPreWrappedText(_bodyLabel.get_parsed_text())
  _bodyLabel.text = _parsedText
  _bodyLabel.autowrap_mode = TextServer.AUTOWRAP_OFF
  _bodyLabel.visible_characters = 0

  _startLine = 0
  _linesPerPage = LINES_PER_PAGE
  _revealTimer = 0.0
  _pauseTimer = 0.0
  _autoAdvanceTimer = 0.0
  _isRevealing = true
  _isWaitingForInput = false

  _updateWorldPosition()
  visible = true
  _revealNextChar()

func _bodyLabelWidth() -> float:
  var style:StyleBox = get_theme_stylebox("panel")
  if style == null:
    return MAX_WIDTH
  return MAX_WIDTH - style.get_margin(SIDE_LEFT) - style.get_margin(SIDE_RIGHT)

func _process(delta:float) -> void:
  if not visible:
    return

  _updateWorldPosition()
  if _isWaitingForInput:
    _advanceIndicator.modulate.a = 0.5 + 0.5 * sin(Time.get_ticks_msec() / 300.0)
  else:
    _advanceIndicator.modulate.a = 0.0

  if _isRevealing:
    _processReveal(delta)
    return

  if _isWaitingForInput:
    _processAdvanceInput()
    return

  if _advancementMode == AdvancementMode.TIMED:
    _processAutoAdvance(delta)

func _buildPreWrappedText(parsed:String) -> String:
  if parsed.is_empty():
    return parsed
  var result:String = ""
  var prevLine:int = 0
  for i in range(len(parsed)):
    var ch:String = parsed[i]
    if ch == "\n":
      result += "\n"
      prevLine += 1
      continue
    var charLine:int = _bodyLabel.get_character_line(i)
    if charLine > prevLine:
      result += "\n"
      prevLine = charLine
    result += ch
  return result

func _processReveal(delta:float) -> void:
  var scaledDelta:float = delta * SETTINGS.textSpeed

  if _pauseTimer > 0.0:
    _pauseTimer -= scaledDelta
    return

  _revealTimer += scaledDelta

  while _revealTimer >= 1.0 / CHARS_PER_SECOND:
    _revealTimer -= 1.0 / CHARS_PER_SECOND
    if not _revealNextChar():
      return

func _revealNextChar() -> bool:
  var totalChars:int = len(_parsedText)
  if _bodyLabel.visible_characters >= totalChars:
    _onRevealComplete()
    return false

  _bodyLabel.visible_characters += 1
  var idx:int = _bodyLabel.visible_characters - 1

  var charLine:int = _parsedText.left(idx + 1).count("\n")
  if charLine >= _startLine + _linesPerPage:
    _bodyLabel.visible_characters -= 1
    _onPageFull()
    return false

  if idx < len(_parsedText):
    var isLastVisible:bool = idx >= len(_parsedText) - 1
    if not isLastVisible:
      var nextCharLine:int = _parsedText.left(idx + 2).count("\n")
      isLastVisible = nextCharLine >= _startLine + _linesPerPage
    if not isLastVisible:
      _pauseTimer = _getPauseForChar(idx)

  return true

func _getPauseForChar(idx:int) -> float:
  var ch:String = _parsedText[idx]
  if ch == "," or ch == ";":
    return PAUSE_COMMA
  if ch == "." or ch == "!" or ch == "?":
    if ch == ".":
      var prevDot:bool = idx > 0 and _parsedText[idx - 1] == "."
      var nextDot:bool = idx < len(_parsedText) - 1 and _parsedText[idx + 1] == "."
      if prevDot or nextDot:
        return PAUSE_ELLIPSIS_DOT
    return PAUSE_SENTENCE
  return 0.0

func _onPageFull() -> void:
  _isRevealing = false
  _isWaitingForInput = true

func _onRevealComplete() -> void:
  _isRevealing = false
  if _advancementMode == AdvancementMode.TIMED:
    _autoAdvanceTimer = len(_parsedText) / READING_CHARS_PER_SECOND
  else:
    _isWaitingForInput = true

func _processAdvanceInput() -> void:
  if Input.is_action_just_pressed("interact"):
    _advance()

func _processAutoAdvance(delta:float) -> void:
  _autoAdvanceTimer -= delta
  if _autoAdvanceTimer <= 0.0:
    _advance()

func _advance() -> void:
  _advanceIndicator.modulate.a = 0.0
  var totalLines:int = _parsedText.count("\n") + 1
  var hasMorePages:bool = _startLine + _linesPerPage < totalLines
  if hasMorePages:
    _startLine += _linesPerPage
    _bodyLabel.scroll_to_line(_startLine)
    _isWaitingForInput = false
    _revealTimer = 0.0
    _pauseTimer = 0.0
    _isRevealing = true
    _revealNextChar()
  else:
    dismissed.emit()
    visible = false
    queue_free()

func _updateWorldPosition() -> void:
  if _entity == null:
    return
  var camera:Camera3D = get_viewport().get_camera_3d()
  if camera == null:
    return
  var worldPos:Vector3 = _entity.global_position + Vector3(0, 2.5, 0)
  var screenPos:Vector2 = camera.unproject_position(worldPos)
  var viewportSize:Vector2 = get_viewport().get_visible_rect().size
  position = screenPos - size * 0.5
  position.x = clamp(position.x, 0.0, viewportSize.x - size.x)
  position.y = clamp(position.y, 0.0, viewportSize.y - size.y)
