Input actor

use "collections"
use "signals"

class _WinchNotify is SignalNotify
  """
  Signal handler that notifies the InputActor on SIGWINCH.
  """
  let _input: InputActor tag

  new create(input: InputActor tag) =>
    _input = input

  fun apply(times: U32): Bool =>
    _input._winch()
    true

actor InputActor is (InputListener & _HitTestRequester)
  """
  Reads terminal input, parses events, routes to focused widget.
  Listens for SIGWINCH to detect terminal resize.

  Focus is scope-aware: widgets are registered with an optional scope token.
  Disabling a scope removes its widgets from the active focus list.
  Widgets with no scope (None) are always active.
  """
  var _parser: InputParser ref
  let _compositor: Compositor tag
  let _focus_scopes: Array[(Widget tag, (Any tag | None))]
  let _focus_list: Array[Widget tag]
  let _disabled_scopes: Array[Any tag]
  let _resize_list: Array[Widget tag]
  var _focus_index: USize = 0
  var _pending_mouse: (MouseEvent | None) = None

  new create(input: TerminalInput tag, compositor: Compositor tag) =>
    _parser = InputParser
    _compositor = compositor
    _focus_scopes = Array[(Widget tag, (Any tag | None))]
    _focus_list = Array[Widget tag]
    _disabled_scopes = Array[Any tag]
    _resize_list = Array[Widget tag]
    input.subscribe(this)
    ifdef not windows then
      SignalHandler(recover _WinchNotify(this) end, Sig.winch())
    end

  be _winch() =>
    """
    Called when SIGWINCH is received. Queries the new terminal size and
    routes it as a resize event.
    """
    (let w, let h) = TermSize()
    _route_resize(ResizeEvent(w, h))

  be register_focusable(widget: Widget tag, scope: (Any tag | None) = None) =>
    """
    Add a widget to the focus list with an optional scope. The first
    registered widget in an enabled scope receives focus automatically.
    """
    _focus_scopes.push((widget, scope))
    if _is_scope_enabled(scope) then
      _focus_list.push(widget)
      if _focus_list.size() == 1 then
        widget.receive_focus()
      end
    end

  be register_widget(widget: Widget tag) =>
    """
    Register a widget for resize notifications without adding to focus list.
    """
    _resize_list.push(widget)

  be unregister_focusable(widget: Widget tag) =>
    """
    Remove a widget from both the scope registry and the focus list,
    and adjust the focus index.
    """
    // Remove from _focus_scopes
    for i in Range[USize](0, _focus_scopes.size()) do
      try
        (let w, _) = _focus_scopes(i)?
        if w is widget then
          _focus_scopes.delete(i)?
          break
        end
      end
    end
    // Remove from _focus_list
    try
      let idx = _find_in_focus_list(widget)?
      _focus_list.delete(idx)?
      if _focus_list.size() == 0 then
        _focus_index = 0
      elseif idx < _focus_index then
        _focus_index = _focus_index - 1
      elseif _focus_index >= _focus_list.size() then
        _focus_index = _focus_list.size() - 1
      end
    end

  be disable_scope(scope: Any tag) =>
    """
    Disable a scope, removing its widgets from the active focus list.
    If the currently focused widget is in the disabled scope, focus
    moves to the first remaining widget.
    """
    // Don't double-add
    for s in _disabled_scopes.values() do
      if s is scope then return end
    end
    _disabled_scopes.push(scope)
    _rebuild_focus_list()

  be enable_scope(scope: Any tag) =>
    """
    Re-enable a previously disabled scope, restoring its widgets to
    the active focus list. No-op if the scope is not currently disabled.
    """
    var found: Bool = false
    for i in Range[USize](0, _disabled_scopes.size()) do
      try
        if _disabled_scopes(i)? is scope then
          _disabled_scopes.delete(i)?
          found = true
          break
        end
      end
    end
    if found then
      _rebuild_focus_list()
    end

  be _query_focus_state(cb: {(USize, USize)} val) =>
    """
    Query the current focus state (focus index and focus list size).
    Package-private — used by tests to verify focus after async operations.
    Messages to this behavior are ordered with other behaviors on this actor,
    so querying after disable_scope/enable_scope guarantees the state reflects
    those operations.
    """
    cb(_focus_index, _focus_list.size())

  be receive(data: Array[U8] val) =>
    """
    Called by TerminalInput when raw bytes arrive. Parses and routes events.
    """
    let events = _parser.parse(data)
    for event in events.values() do
      match event
      | let ke: KeyEvent => _route_key(ke)
      | let me: MouseEvent => _route_mouse(me)
      | let re: ResizeEvent => _route_resize(re)
      end
    end

  be hit_test_result(widget: (Any tag | None)) =>
    """
    Callback from compositor hit_test. Mouse routing is not yet implemented.
    """
    _pending_mouse = None

  fun ref _route_key(ke: KeyEvent) =>
    if (ke.key is Tab) and ((ke.modifiers and Modifiers.shift()) != 0) then
      _focus_prev()
    elseif ke.key is Tab then
      _focus_next()
    else
      try _focus_list(_focus_index)?.receive_key(ke) end
    end

  fun ref _route_mouse(me: MouseEvent) =>
    _pending_mouse = me
    _compositor.hit_test(me.col, me.row, this)

  fun ref _route_resize(re: ResizeEvent) =>
    for widget in _resize_list.values() do
      widget.resize(re.width, re.height)
    end
    _compositor.screen_resize(re.width, re.height)

  fun ref _focus_next() =>
    if _focus_list.size() == 0 then return end
    try _focus_list(_focus_index)?.receive_blur() end
    _focus_index = (_focus_index + 1) % _focus_list.size()
    try _focus_list(_focus_index)?.receive_focus() end

  fun ref _focus_prev() =>
    if _focus_list.size() == 0 then return end
    try _focus_list(_focus_index)?.receive_blur() end
    if _focus_index == 0 then
      _focus_index = _focus_list.size() - 1
    else
      _focus_index = _focus_index - 1
    end
    try _focus_list(_focus_index)?.receive_focus() end

  fun _find_in_focus_list(widget: Widget tag): USize ? =>
    for i in Range[USize](0, _focus_list.size()) do
      try
        if _focus_list(i)? is widget then return i end
      end
    end
    error

  fun ref _rebuild_focus_list() =>
    """
    Rebuild _focus_list from _focus_scopes, excluding disabled scopes.
    Preserves focus on the current widget if it's still in the list,
    otherwise focuses the first available widget.
    """
    // Remember the currently focused widget before rebuilding
    let prev_focused: (Widget tag | None) =
      try _focus_list(_focus_index)? else None end

    // Blur the current widget — it may or may not retain focus
    match prev_focused
    | let w: Widget tag => w.receive_blur()
    end

    _focus_list.clear()
    for entry in _focus_scopes.values() do
      (let w, let s) = entry
      if _is_scope_enabled(s) then
        _focus_list.push(w)
      end
    end

    // Try to keep focus on the same widget
    _focus_index = 0
    match prev_focused
    | let pw: Widget tag =>
      try
        _focus_index = _find_in_focus_list(pw)?
      end
    end

    // Focus the widget at the new index
    try _focus_list(_focus_index)?.receive_focus() end

  fun _is_scope_enabled(scope: (Any tag | None)): Bool =>
    """
    Check if a scope is enabled. None (no scope) is always enabled.
    """
    // Can't match (Any tag | None) because None is a subtype of Any tag.
    // Use identity comparison instead.
    if scope is None then return true end
    for disabled in _disabled_scopes.values() do
      if disabled is scope then return false end
    end
    true