///|
/// VNode-based TUI Application renderer
/// Provides efficient rendering pipeline: TuiNode -> crater -> CharBuffer -> ANSI

///|
/// VNodeApp manages the vnode render loop with diff-based updates
pub struct VNodeApp {
  mut width : Int
  mut height : Int
  mut prev_buffer : @render.CharBuffer?
  mut last_layout : @ccore.Layout?
  mut overlay_layouts : Array[@ccore.Layout]
  mut render_ctx : RenderContext
}

///|
pub fn VNodeApp::new(width : Int, height : Int) -> VNodeApp {
  {
    width,
    height,
    prev_buffer: None,
    last_layout: None,
    overlay_layouts: [],
    render_ctx: RenderContext::new(),
  }
}

///|
/// Reset the render context for a new frame
fn VNodeApp::reset_context(self : VNodeApp) -> Unit {
  self.render_ctx = RenderContext::new()
}

///|
/// Render a vnode tree to ANSI string
pub fn VNodeApp::render(self : VNodeApp, root : TuiNode) -> String {
  // Reset context for new render
  self.reset_context()

  // Convert vnode to crater node
  let crater_node = to_crater_node(self.render_ctx, root)

  // Compute layout
  let layout = @render.compute_layout(crater_node, self.width, self.height)

  // Save layout for hit testing
  self.last_layout = Some(layout)

  // Create style map from render context
  let styles = @core.RenderStyleMap::new()
  for entry in self.render_ctx.styles {
    styles.set(entry.0, entry.1)
  }

  // Create text map from render context
  let texts = @core.TextContentMap::new()
  for entry in self.render_ctx.texts {
    texts.set(entry.0, entry.1)
  }

  // Create new buffer
  let buf = @render.CharBuffer::new(self.width, self.height)
  @render.render_layout(buf, layout, styles, texts, 0, 0)

  // Generate output with diff if possible
  let output = match self.prev_buffer {
    Some(prev) => buf.diff_to_ansi(prev)
    None =>
      // First render: clear screen and render full
      @render.ansi_clear_screen() + buf.to_ansi()
  }

  // Save current buffer for next diff
  self.prev_buffer = Some(buf)
  output
}

///|
/// Render main content with overlay layers on top
pub fn VNodeApp::render_with_overlays(
  self : VNodeApp,
  main : TuiNode,
  overlays : Array[OverlayEntry],
) -> String {
  // Reset context for new render
  self.reset_context()
  self.overlay_layouts = []

  // Convert main vnode to crater node
  let crater_node = to_crater_node(self.render_ctx, main)

  // Compute layout
  let layout = @render.compute_layout(crater_node, self.width, self.height)

  // Save layout for hit testing
  self.last_layout = Some(layout)

  // Create style map from render context
  let styles = @core.RenderStyleMap::new()
  for entry in self.render_ctx.styles {
    styles.set(entry.0, entry.1)
  }

  // Create text map from render context
  let texts = @core.TextContentMap::new()
  for entry in self.render_ctx.texts {
    texts.set(entry.0, entry.1)
  }

  // Create buffer and render main content (1st pass)
  let buf = @render.CharBuffer::new(self.width, self.height)
  @render.render_layout(buf, layout, styles, texts, 0, 0)

  // Render each overlay (2nd+ pass)
  for overlay in overlays {
    // Draw backdrop if requested
    if overlay.backdrop {
      buf.fill_bg(0, 0, self.width, self.height, @core.Color::rgb(0, 0, 0))
    }

    // Create a new render context for the overlay
    let overlay_ctx = RenderContext::new()
    let overlay_crater = to_crater_node(overlay_ctx, overlay.node)
    let overlay_layout = if overlay.shrink {
      @render.compute_layout_max_content(
        overlay_crater,
        self.width,
        self.height,
      )
    } else {
      @render.compute_layout(overlay_crater, self.width, self.height)
    }

    // Calculate offset based on position
    let overlay_rect = @core.normalize_grid_rect(
      0.0,
      0.0,
      overlay_layout.width,
      overlay_layout.height,
    )
    let overlay_w = overlay_rect.width
    let overlay_h = overlay_rect.height
    let (offset_x, offset_y) = match overlay.position {
      PopupPosition::Center =>
        ((self.width - overlay_w) / 2, (self.height - overlay_h) / 2)
      PopupPosition::Top => ((self.width - overlay_w) / 2, 0)
      PopupPosition::Bottom =>
        ((self.width - overlay_w) / 2, self.height - overlay_h)
      PopupPosition::At(x, y) => (x, y)
    }

    // Build style/text maps for overlay
    let overlay_styles = @core.RenderStyleMap::new()
    for entry in overlay_ctx.styles {
      overlay_styles.set(entry.0, entry.1)
    }
    let overlay_texts = @core.TextContentMap::new()
    for entry in overlay_ctx.texts {
      overlay_texts.set(entry.0, entry.1)
    }

    // Render overlay onto the same buffer
    @render.render_layout(
      buf, overlay_layout, overlay_styles, overlay_texts, offset_x, offset_y,
    )

    // Merge handlers into main render context
    for entry in overlay_ctx.handlers {
      self.render_ctx.handlers[entry.0] = entry.1
    }
    for entry in overlay_ctx.roles {
      self.render_ctx.roles[entry.0] = entry.1
    }

    // Save overlay layout with offset applied
    self.overlay_layouts.push({
      ..overlay_layout,
      x: overlay_layout.x + offset_x.to_double(),
      y: overlay_layout.y + offset_y.to_double(),
    })
  }

  // Generate output with diff if possible
  let output = match self.prev_buffer {
    Some(prev) => buf.diff_to_ansi(prev)
    None => @render.ansi_clear_screen() + buf.to_ansi()
  }

  // Save current buffer for next diff
  self.prev_buffer = Some(buf)
  output
}

///|
/// Render with overlays and cursor hidden
pub fn VNodeApp::render_frame_with_overlays(
  self : VNodeApp,
  main : TuiNode,
  overlays : Array[OverlayEntry],
) -> String {
  @render.ansi_hide_cursor() + self.render_with_overlays(main, overlays)
}

///|
/// Render with cursor hidden and positioned at origin
pub fn VNodeApp::render_frame(self : VNodeApp, root : TuiNode) -> String {
  @render.ansi_hide_cursor() + self.render(root)
}

///|
/// Create a reactive effect that re-renders when signals change
/// Returns a dispose function
pub fn VNodeApp::create_render_effect(
  self : VNodeApp,
  render_fn : () -> TuiNode,
  output_fn : (String) -> Unit,
) -> () -> Unit {
  @signals.render_effect(fn() {
    let node = render_fn()
    let output = self.render_frame(node)
    output_fn(output)
  })
}

///|
/// Initialize terminal (enter alt screen, hide cursor)
pub fn VNodeApp::init_terminal() -> String {
  @render.ansi_enter_alt_screen() +
  @render.ansi_hide_cursor() +
  @render.ansi_clear_screen()
}

///|
/// Restore terminal (show cursor, leave alt screen)
pub fn VNodeApp::restore_terminal() -> String {
  @render.ansi_show_cursor() + @render.ansi_leave_alt_screen()
}

///|
/// Perform hit test at coordinates
/// Checks overlays first (top-most layer) before main layout
pub fn VNodeApp::hit_test(
  self : VNodeApp,
  x : Int,
  y : Int,
) -> @events.HitTestResult? {
  // Check overlays in reverse order (top-most first)
  let len = self.overlay_layouts.length()
  for i = len - 1; i >= 0; i = i - 1 {
    let result = @events.hit_test_root(self.overlay_layouts[i], x, y)
    if result is Some(_) {
      return result
    }
  }
  match self.last_layout {
    Some(layout) => @events.hit_test_root(layout, x, y)
    None => None
  }
}

///|
/// Find component by ID
pub fn VNodeApp::find_by_id(
  self : VNodeApp,
  id : String,
) -> @events.HitTestResult? {
  match self.last_layout {
    Some(layout) => @events.find_layout_by_id_root(layout, id)
    None => None
  }
}

///|
/// Get all layout rects from the last render (absolute coordinates).
pub fn VNodeApp::layout_rects(self : VNodeApp) -> Array[@events.HitTestResult] {
  match self.last_layout {
    Some(layout) => @events.layout_rects_root(layout)
    None => []
  }
}

///|
/// Get accessibility tree from the last render.
pub fn VNodeApp::a11y_tree(self : VNodeApp) -> Array[A11yNode] {
  match self.last_layout {
    Some(layout) => build_a11y_tree(layout, self.render_ctx)
    None => []
  }
}

///|
/// Debug: dump accessibility tree structure.
pub fn VNodeApp::dump_a11y_tree(self : VNodeApp) -> String {
  dump_a11y_tree(self.a11y_tree())
}

///|
/// Handle a mouse event using the render context's handlers
/// Returns the clicked component ID if found and handler was invoked
pub fn VNodeApp::handle_click(
  self : VNodeApp,
  event : @events.MouseEvent,
) -> String? {
  match (event.event_type, event.button) {
    (@events.MouseEventType::Press, @events.MouseButton::Left) =>
      match self.hit_test(event.x, event.y) {
        Some(hit) => {
          match self.render_ctx.handlers.get(hit.id) {
            Some(handler) =>
              handler(
                TuiEvent::Click(x=event.x, y=event.y, button=MouseButton::Left),
              )
            None => ()
          }
          Some(hit.id)
        }
        None => None
      }
    _ => None
  }
}

///|
/// Dispatch hover event - returns hovered component ID on Move/Drag events
/// Also dispatches TuiEvent::Hover to handlers for overlay-aware hit testing
pub fn VNodeApp::dispatch_hover(
  self : VNodeApp,
  event : @events.MouseEvent,
) -> String? {
  match (event.event_type, event.button) {
    (@events.MouseEventType::Drag, @events.MouseButton::None)
    | (@events.MouseEventType::Move, _) =>
      match self.hit_test(event.x, event.y) {
        Some(hit) => {
          match self.render_ctx.handlers.get(hit.id) {
            Some(handler) =>
              handler(TuiEvent::Hover(x=event.x, y=event.y, id=hit.id))
            None => ()
          }
          Some(hit.id)
        }
        None => Some("")
      }
    _ => None
  }
}

///|
/// Clear the previous buffer to force a full re-render on next render call
pub fn VNodeApp::clear_prev_buffer(self : VNodeApp) -> Unit {
  self.prev_buffer = None
}

///|
/// Resize the render target and force a full redraw on next render.
pub fn VNodeApp::resize(self : VNodeApp, width : Int, height : Int) -> Unit {
  self.width = width
  self.height = height
  self.clear_prev_buffer()
}

///|
/// Full-screen vnode app helper: sets up alt screen and handles cleanup
pub fn run_vnode_app(
  width : Int,
  height : Int,
  render_fn : () -> TuiNode,
  output_fn : (String) -> Unit,
) -> () -> Unit {
  let app = VNodeApp::new(width, height)

  // Initialize terminal
  output_fn(VNodeApp::init_terminal())

  // Create render effect
  let dispose = app.create_render_effect(render_fn, output_fn)

  // Return cleanup function
  fn() {
    dispose()
    output_fn(VNodeApp::restore_terminal())
  }
}

///|
/// Initialize a VNodeApp with terminal setup and return a cleanup function.
pub fn init_vnode_terminal(
  width : Int,
  height : Int,
  output_fn : (String) -> Unit,
  mouse? : Bool = false,
  raw_mode? : Bool = true,
) -> (VNodeApp, () -> Unit) {
  let app = VNodeApp::new(width, height)

  // Initialize terminal
  output_fn(VNodeApp::init_terminal())
  if mouse {
    output_fn(@render.enable_mouse_all())
  }
  if raw_mode {
    @io.enable_raw_mode()
  }
  fn cleanup() {
    @io.stop_keypress_listener()
    @io.stop_resize_listener()
    if mouse {
      output_fn(@render.disable_mouse_all())
    }
    @io.cleanup_stdin()
    output_fn(VNodeApp::restore_terminal())
  }

  (app, cleanup)
}

///|
/// Simple render function for one-shot rendering (no reactivity)
pub fn render_vnode_once(width : Int, height : Int, root : TuiNode) -> String {
  let app = VNodeApp::new(width, height)
  app.render(root)
}