//! Application runner
//!
//! This module provides the main application runner with support for:
//! - Inline mode (default, like Ink/Bubbletea)
//! - Fullscreen mode (alternate screen, like vim)
//! - Runtime mode switching
//! - Println for persistent output
//! - Cross-thread render requests

use crossterm::event::Event;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};

use crate::core::Element;
use crate::hooks::context::{HookContext, with_hooks};
use crate::hooks::use_app::{AppContext, set_app_context};
use crate::hooks::use_input::{clear_input_handlers, dispatch_key_event};
use crate::hooks::use_mouse::{clear_mouse_handlers, dispatch_mouse_event, is_mouse_enabled};
use crate::layout::LayoutEngine;
use crate::renderer::{Output, Terminal};

// === App runtime + registry ===

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct AppId(u64);

impl AppId {
    fn new() -> Self {
        Self(APP_ID_COUNTER.fetch_add(1, Ordering::SeqCst))
    }

    fn from_raw(raw: u64) -> Option<Self> {
        if raw == 0 { None } else { Some(Self(raw)) }
    }

    fn raw(self) -> u64 {
        self.0
    }
}

static APP_ID_COUNTER: AtomicU64 = AtomicU64::new(1);

/// Printable content that can be sent to println
#[derive(Clone)]
pub enum Printable {
    /// Plain text message
    Text(String),
    /// Rendered element (boxed to reduce enum size)
    Element(Box<Element>),
}

/// Trait for types that can be printed via println
pub trait IntoPrintable {
    /// Convert into a printable value
    fn into_printable(self) -> Printable;
}

impl IntoPrintable for String {
    fn into_printable(self) -> Printable {
        Printable::Text(self)
    }
}

impl IntoPrintable for &str {
    fn into_printable(self) -> Printable {
        Printable::Text(self.to_string())
    }
}

impl IntoPrintable for Element {
    fn into_printable(self) -> Printable {
        Printable::Element(Box::new(self))
    }
}

pub trait AppSink: Send + Sync {
    fn request_render(&self);
    fn println(&self, message: Printable);
    fn enter_alt_screen(&self);
    fn exit_alt_screen(&self);
    fn is_alt_screen(&self) -> bool;
}

/// Mode switch direction
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModeSwitch {
    /// Switch to alternate screen (fullscreen)
    EnterAltScreen,
    /// Switch to inline mode
    ExitAltScreen,
}

struct AppRuntime {
    id: AppId,
    render_flag: Arc<AtomicBool>,
    println_queue: Mutex<Vec<Printable>>,
    mode_switch_request: Mutex<Option<ModeSwitch>>,
    alt_screen_state: Arc<AtomicBool>,
}

impl AppRuntime {
    fn new(alternate_screen: bool) -> Arc<Self> {
        Arc::new(Self {
            id: AppId::new(),
            render_flag: Arc::new(AtomicBool::new(true)),
            println_queue: Mutex::new(Vec::new()),
            mode_switch_request: Mutex::new(None),
            alt_screen_state: Arc::new(AtomicBool::new(alternate_screen)),
        })
    }

    fn id(&self) -> AppId {
        self.id
    }

    fn set_alt_screen_state(&self, value: bool) {
        self.alt_screen_state.store(value, Ordering::SeqCst);
    }

    fn render_requested(&self) -> bool {
        self.render_flag.load(Ordering::SeqCst)
    }

    fn clear_render_request(&self) {
        self.render_flag.store(false, Ordering::SeqCst);
    }

    fn take_mode_switch_request(&self) -> Option<ModeSwitch> {
        if let Ok(mut request) = self.mode_switch_request.lock() {
            request.take()
        } else {
            None
        }
    }

    fn take_println_messages(&self) -> Vec<Printable> {
        if let Ok(mut queue) = self.println_queue.lock() {
            std::mem::take(&mut *queue)
        } else {
            Vec::new()
        }
    }
}

impl AppSink for AppRuntime {
    fn request_render(&self) {
        self.render_flag.store(true, Ordering::SeqCst);
    }

    fn println(&self, message: Printable) {
        if let Ok(mut queue) = self.println_queue.lock() {
            queue.push(message);
        }
        self.request_render();
    }

    fn enter_alt_screen(&self) {
        if let Ok(mut request) = self.mode_switch_request.lock() {
            *request = Some(ModeSwitch::EnterAltScreen);
        }
        self.request_render();
    }

    fn exit_alt_screen(&self) {
        if let Ok(mut request) = self.mode_switch_request.lock() {
            *request = Some(ModeSwitch::ExitAltScreen);
        }
        self.request_render();
    }

    fn is_alt_screen(&self) -> bool {
        self.alt_screen_state.load(Ordering::SeqCst)
    }
}

type AppRegistry = HashMap<AppId, Arc<dyn AppSink>>;

static APP_REGISTRY: OnceLock<Mutex<AppRegistry>> = OnceLock::new();
static CURRENT_APP: AtomicU64 = AtomicU64::new(0);

fn registry() -> &'static Mutex<AppRegistry> {
    APP_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
}

fn set_current_app(id: Option<AppId>) {
    let raw = id.map(|value| value.raw()).unwrap_or(0);
    CURRENT_APP.store(raw, Ordering::SeqCst);
}

fn current_app_sink() -> Option<Arc<dyn AppSink>> {
    let id = AppId::from_raw(CURRENT_APP.load(Ordering::SeqCst))?;
    let registry = registry().lock().ok()?;
    registry.get(&id).cloned()
}

struct AppRegistrationGuard {
    id: AppId,
}

impl Drop for AppRegistrationGuard {
    fn drop(&mut self) {
        unregister_app(self.id);
    }
}

fn register_app(runtime: Arc<AppRuntime>) -> AppRegistrationGuard {
    let id = runtime.id();
    if let Ok(mut registry) = registry().lock() {
        let sink: Arc<dyn AppSink> = runtime;
        registry.insert(id, sink);
        set_current_app(Some(id));
    }
    AppRegistrationGuard { id }
}

fn unregister_app(id: AppId) {
    if let Ok(mut registry) = registry().lock() {
        registry.remove(&id);
    }
    if AppId::from_raw(CURRENT_APP.load(Ordering::SeqCst)) == Some(id) {
        set_current_app(None);
    }
}

/// Request a re-render from any thread.
///
/// This is useful when state is updated from a background thread
/// and you need to notify the UI to refresh.
///
/// # Example
///
/// ```ignore
/// use std::thread;
/// use rnk::request_render;
///
/// // In a background thread
/// thread::spawn(|| {
///     // ... update some shared state ...
///     request_render(); // Notify rnk to re-render
/// });
/// ```
pub fn request_render() {
    if let Some(sink) = current_app_sink() {
        sink.request_render();
    }
}

/// Print a message that persists above the UI (like Bubbletea's Println).
///
/// In inline mode, this clears the current UI, writes the message,
/// and the UI is re-rendered below it. The message stays in terminal history.
///
/// In fullscreen mode, this is a no-op (messages are ignored, like Bubbletea).
///
/// **Fallback behavior**: If no rnk app is running, the message is printed
/// directly to stdout (using `render_to_string_auto` for Elements).
///
/// Supports both plain text and rendered elements:
///
/// # Examples
///
/// ```ignore
/// use rnk::println;
///
/// // Simple text
/// rnk::println("Task completed successfully!");
/// rnk::println(format!("Downloaded {} files", count));
///
/// // Complex components
/// let banner = Box::new()
///     .border_style(BorderStyle::Round)
///     .padding(1)
///     .child(Text::new("Welcome!").bold().into_element())
///     .into_element();
/// rnk::println(banner);
/// ```
pub fn println(message: impl IntoPrintable) {
    if let Some(sink) = current_app_sink() {
        sink.println(message.into_printable());
        return;
    }

    // No app running, print directly as fallback
    let printable = message.into_printable();
    let output = match printable {
        Printable::Text(text) => text,
        Printable::Element(element) => render_to_string_auto(&element),
    };
    std::println!("{}", output);
}

/// Request to enter alternate screen mode (fullscreen).
///
/// This can be called from any thread. The mode switch happens
/// on the next render cycle.
///
/// # Example
///
/// ```ignore
/// use rnk::enter_alt_screen;
///
/// // Switch to fullscreen mode
/// enter_alt_screen();
/// ```
pub fn enter_alt_screen() {
    if let Some(sink) = current_app_sink() {
        sink.enter_alt_screen();
    }
}

/// Request to exit alternate screen mode (return to inline).
///
/// This can be called from any thread. The mode switch happens
/// on the next render cycle.
///
/// # Example
///
/// ```ignore
/// use rnk::exit_alt_screen;
///
/// // Return to inline mode
/// exit_alt_screen();
/// ```
pub fn exit_alt_screen() {
    if let Some(sink) = current_app_sink() {
        sink.exit_alt_screen();
    }
}

/// Check if currently in alternate screen mode.
///
/// Returns `None` if no app is running.
pub fn is_alt_screen() -> Option<bool> {
    current_app_sink().map(|sink| sink.is_alt_screen())
}

/// A handle for requesting renders from any thread.
///
/// This is a cloneable, thread-safe handle that can be used to trigger
/// re-renders from background threads or async tasks.
///
/// # Example
///
/// ```ignore
/// use std::thread;
/// use rnk::render_handle;
///
/// let handle = render_handle().expect("App must be running");
///
/// thread::spawn(move || {
///     // ... do some work ...
///     handle.request_render();
/// });
/// ```
#[derive(Clone)]
pub struct RenderHandle {
    sink: Arc<dyn AppSink>,
}

impl RenderHandle {
    pub(crate) fn new(sink: Arc<dyn AppSink>) -> Self {
        Self { sink }
    }

    /// Request a re-render
    pub fn request_render(&self) {
        self.sink.request_render();
    }

    /// Print a message that persists above the UI
    pub fn println(&self, message: impl IntoPrintable) {
        self.sink.println(message.into_printable());
    }

    /// Request to enter fullscreen mode
    pub fn enter_alt_screen(&self) {
        self.sink.enter_alt_screen();
    }

    /// Request to exit fullscreen mode
    pub fn exit_alt_screen(&self) {
        self.sink.exit_alt_screen();
    }

    /// Check if currently in fullscreen mode
    pub fn is_alt_screen(&self) -> bool {
        self.sink.is_alt_screen()
    }
}

/// Get a render handle for cross-thread render requests.
///
/// Returns `None` if no app is currently running.
pub fn render_handle() -> Option<RenderHandle> {
    current_app_sink().map(RenderHandle::new)
}

// === Application Options ===

/// Application options for configuring the renderer
#[derive(Debug, Clone)]
pub struct AppOptions {
    /// Target frames per second (default: 60)
    pub fps: u32,
    /// Exit on Ctrl+C (default: true)
    pub exit_on_ctrl_c: bool,
    /// Use alternate screen / fullscreen mode (default: false = inline mode)
    ///
    /// - `false` (default): Inline mode, like Ink and Bubbletea's default.
    ///   Output appears at current cursor position and persists in terminal history.
    ///
    /// - `true`: Fullscreen mode, like vim or Bubbletea's `WithAltScreen()`.
    ///   Uses alternate screen buffer, content is cleared on exit.
    pub alternate_screen: bool,
}

impl Default for AppOptions {
    fn default() -> Self {
        Self {
            fps: 60, // Bubbletea default
            exit_on_ctrl_c: true,
            alternate_screen: false, // Inline mode by default (like Ink/Bubbletea)
        }
    }
}

// === App Builder ===

/// Builder for configuring and running an application.
///
/// This provides a fluent API similar to Bubbletea's `WithXxx()` options.
///
/// # Example
///
/// ```ignore
/// use rnk::prelude::*;
///
/// // Inline mode (default)
/// render(my_app).run()?;
///
/// // Fullscreen mode
/// render(my_app).fullscreen().run()?;
///
/// // Custom configuration
/// render(my_app)
///     .fullscreen()
///     .fps(30)
///     .exit_on_ctrl_c(false)
///     .run()?;
/// ```
pub struct AppBuilder<F>
where
    F: Fn() -> Element,
{
    component: F,
    options: AppOptions,
}

impl<F> AppBuilder<F>
where
    F: Fn() -> Element,
{
    /// Create a new app builder with default options (inline mode)
    pub fn new(component: F) -> Self {
        Self {
            component,
            options: AppOptions::default(),
        }
    }

    /// Use fullscreen mode (alternate screen buffer).
    ///
    /// Like Bubbletea's `WithAltScreen()`.
    pub fn fullscreen(mut self) -> Self {
        self.options.alternate_screen = true;
        self
    }

    /// Use inline mode (default).
    ///
    /// Output appears at current cursor position and persists in terminal history.
    pub fn inline(mut self) -> Self {
        self.options.alternate_screen = false;
        self
    }

    /// Set the target frames per second.
    ///
    /// Default is 60 FPS.
    pub fn fps(mut self, fps: u32) -> Self {
        self.options.fps = fps;
        self
    }

    /// Set whether to exit on Ctrl+C.
    ///
    /// Default is `true`.
    pub fn exit_on_ctrl_c(mut self, exit: bool) -> Self {
        self.options.exit_on_ctrl_c = exit;
        self
    }

    /// Get the current options
    pub fn options(&self) -> &AppOptions {
        &self.options
    }

    /// Run the application
    pub fn run(self) -> std::io::Result<()> {
        App::with_options(self.component, self.options).run()
    }
}

// === Application State ===

/// Application state
pub struct App<F>
where
    F: Fn() -> Element,
{
    component: F,
    terminal: Terminal,
    layout_engine: LayoutEngine,
    hook_context: Rc<RefCell<HookContext>>,
    options: AppOptions,
    should_exit: Arc<AtomicBool>,
    runtime: Arc<AppRuntime>,
    render_handle: RenderHandle,
    /// Lines of static content that have been committed
    static_lines: Vec<String>,
    /// Last known terminal width (for detecting width decreases)
    last_width: u16,
    /// Last known terminal height
    last_height: u16,
}

impl<F> App<F>
where
    F: Fn() -> Element,
{
    /// Create a new app with default options (inline mode)
    pub fn new(component: F) -> Self {
        Self::with_options(component, AppOptions::default())
    }

    /// Create a new app with custom options
    pub fn with_options(component: F, options: AppOptions) -> Self {
        let runtime = AppRuntime::new(options.alternate_screen);
        let render_handle = RenderHandle::new(runtime.clone());
        let hook_context = Rc::new(RefCell::new(HookContext::new()));

        // Set up render callback
        let runtime_clone = runtime.clone();
        hook_context
            .borrow_mut()
            .set_render_callback(Rc::new(move || {
                runtime_clone.request_render();
            }));

        // Get initial terminal size
        let (initial_width, initial_height) = Terminal::size().unwrap_or((80, 24));

        Self {
            component,
            terminal: Terminal::new(),
            layout_engine: LayoutEngine::new(),
            hook_context,
            options,
            should_exit: Arc::new(AtomicBool::new(false)),
            runtime,
            render_handle,
            static_lines: Vec::new(),
            last_width: initial_width,
            last_height: initial_height,
        }
    }

    /// Run the application
    pub fn run(&mut self) -> std::io::Result<()> {
        use std::time::{Duration, Instant};

        let _app_guard = register_app(self.runtime.clone());

        // Enter terminal mode based on options
        if self.options.alternate_screen {
            self.terminal.enter()?;
            self.runtime.set_alt_screen_state(true);
        } else {
            self.terminal.enter_inline()?;
            self.runtime.set_alt_screen_state(false);
        }

        let frame_duration = Duration::from_millis(1000 / self.options.fps as u64);
        let mut last_render = Instant::now();

        // Initial render
        self.render_frame()?;

        loop {
            // Handle input
            if let Some(event) = Terminal::poll_event(Duration::from_millis(10))? {
                self.handle_event(event);
            }

            if self.should_exit.load(Ordering::SeqCst) {
                break;
            }

            // Handle mode switch requests
            if let Some(mode_switch) = self.runtime.take_mode_switch_request() {
                self.handle_mode_switch(mode_switch)?;
            }

            // Handle println messages
            let messages = self.runtime.take_println_messages();
            if !messages.is_empty() {
                self.handle_println_messages(&messages)?;
            }

            // Throttle rendering - only render if needed and time elapsed
            let now = Instant::now();
            let time_elapsed = now.duration_since(last_render) >= frame_duration;
            let render_requested = self.runtime.render_requested();

            if render_requested && time_elapsed {
                self.runtime.clear_render_request();
                self.render_frame()?;
                last_render = now;
            }
        }

        // Clean up input handlers
        clear_input_handlers();

        // Exit terminal mode
        if self.terminal.is_alt_screen() {
            self.terminal.exit()?;
        } else {
            self.terminal.exit_inline()?;
        }

        Ok(())
    }

    /// Handle mode switch request
    fn handle_mode_switch(&mut self, mode_switch: ModeSwitch) -> std::io::Result<()> {
        match mode_switch {
            ModeSwitch::EnterAltScreen => {
                if !self.terminal.is_alt_screen() {
                    self.terminal.switch_to_alt_screen()?;
                    self.runtime.set_alt_screen_state(true);
                    self.terminal.repaint();
                }
            }
            ModeSwitch::ExitAltScreen => {
                if self.terminal.is_alt_screen() {
                    self.terminal.switch_to_inline()?;
                    self.runtime.set_alt_screen_state(false);
                    self.terminal.repaint();
                }
            }
        }
        Ok(())
    }

    /// Handle println messages (like Bubbletea's Println)
    fn handle_println_messages(&mut self, messages: &[Printable]) -> std::io::Result<()> {
        // Println only works in inline mode
        if self.terminal.is_alt_screen() {
            return Ok(());
        }

        // Get terminal width for rendering elements
        let (width, _) = Terminal::size().unwrap_or((80, 24));

        for message in messages {
            match message {
                Printable::Text(text) => {
                    // Simple text - print directly
                    self.terminal.println(text)?;
                }
                Printable::Element(element) => {
                    // Render element to string first
                    let rendered = self.render_element_to_string(element, width);
                    self.terminal.println(&rendered)?;
                }
            }
        }

        // Force repaint after println
        self.terminal.repaint();

        Ok(())
    }

    /// Render an element to a string (for println)
    fn render_element_to_string(&self, element: &Element, width: u16) -> String {
        render_to_string(element, width)
    }

    fn handle_event(&mut self, event: Event) {
        match event {
            Event::Key(key_event) => {
                // Handle Ctrl+C
                if self.options.exit_on_ctrl_c && Terminal::is_ctrl_c(&Event::Key(key_event)) {
                    self.should_exit.store(true, Ordering::SeqCst);
                    return;
                }

                // Dispatch to input handlers
                dispatch_key_event(&key_event);

                // Request re-render after input
                self.runtime.request_render();
            }
            Event::Mouse(mouse_event) => {
                // Dispatch to mouse handlers
                dispatch_mouse_event(&mouse_event);

                // Request re-render after mouse event
                self.runtime.request_render();
            }
            Event::Resize(new_width, new_height) => {
                // Handle resize - clear screen if width decreased to prevent artifacts
                self.handle_resize(new_width, new_height);
                // Re-render on resize
                self.runtime.request_render();
            }
            _ => {}
        }
    }

    /// Handle terminal resize events
    ///
    /// Clears the screen on any resize to prevent rendering artifacts.
    /// This is necessary because:
    /// - Width decrease: old content from wider lines remains visible
    /// - Height decrease: old content from taller layouts remains visible
    /// - Height increase: old separator lines at fixed positions remain visible
    fn handle_resize(&mut self, new_width: u16, new_height: u16) {
        use crossterm::cursor::MoveTo;
        use crossterm::execute;
        use crossterm::terminal::{Clear, ClearType};
        use std::io::stdout;

        // Always clear on resize to prevent artifacts
        // This is the safest approach and matches Ink/Bubbletea behavior
        if new_width != self.last_width || new_height != self.last_height {
            // Move to top and clear entire screen
            let _ = execute!(stdout(), MoveTo(0, 0), Clear(ClearType::All));
            self.terminal.repaint();
        }

        // Update tracked size
        self.last_width = new_width;
        self.last_height = new_height;
    }

    fn render_frame(&mut self) -> std::io::Result<()> {
        // Clear input and mouse handlers before render (they'll be re-registered)
        clear_input_handlers();
        clear_mouse_handlers();

        // Get terminal size
        let (width, height) = Terminal::size()?;

        // Set up app context for use_app hook
        set_app_context(Some(AppContext::new(
            self.should_exit.clone(),
            self.render_handle.clone(),
        )));

        // Build element tree with hooks context
        let root = with_hooks(self.hook_context.clone(), || (self.component)());

        // Clear app context after render
        set_app_context(None);

        // Enable/disable mouse mode based on whether any component uses it
        if is_mouse_enabled() {
            self.terminal.enable_mouse()?;
        } else {
            self.terminal.disable_mouse()?;
        }

        // Extract and commit static content
        let new_static_lines = self.extract_static_content(&root, width);
        if !new_static_lines.is_empty() {
            self.commit_static_content(&new_static_lines)?;
        }

        // Filter out static elements from the tree for dynamic rendering
        let dynamic_root = self.filter_static_elements(&root);

        // Compute layout for dynamic content
        self.layout_engine.compute(&dynamic_root, width, height);

        // Get the actual content size from layout
        let root_layout = self
            .layout_engine
            .get_layout(dynamic_root.id)
            .unwrap_or_default();
        let content_width = (root_layout.width as u16).max(1).min(width);

        // Use actual content height in both modes
        // In inline mode, using full terminal height causes scroll issues - the empty lines
        // push the cursor down and scroll the terminal to show empty white space.
        // In fullscreen mode, content naturally fills the screen.
        let render_height = (root_layout.height as u16).max(1).min(height);

        // Render to output buffer
        let mut output = Output::new(content_width, render_height);
        self.render_element(&dynamic_root, &mut output, 0.0, 0.0);

        // Write to terminal
        // Use regular render() which trims trailing empty lines
        let rendered = output.render();
        self.terminal.render(&rendered)
    }

    /// Extract static content from the element tree
    ///
    /// Only extracts content from Static elements that have actual children
    /// (new items to render). Empty Static elements are skipped.
    fn extract_static_content(&self, element: &Element, width: u16) -> Vec<String> {
        let mut lines = Vec::new();

        if element.style.is_static {
            // Only render if the static element has children (new items)
            // Empty Static elements mean all items have already been rendered
            if !element.children.is_empty() {
                // Render static element to get its content
                let mut engine = LayoutEngine::new();
                engine.compute(element, width, 100); // Use large height for static content

                let layout = engine.get_layout(element.id).unwrap_or_default();
                // Ensure we have valid dimensions
                let render_width = (layout.width as u16).max(1);
                let render_height = (layout.height as u16).max(1);
                let mut output = Output::new(render_width, render_height);
                self.render_element_to_output(element, &engine, &mut output, 0.0, 0.0);

                let rendered = output.render();
                for line in rendered.lines() {
                    // Skip empty lines to avoid clutter
                    let trimmed = line.trim();
                    if !trimmed.is_empty() {
                        lines.push(line.to_string());
                    }
                }
            }
        }

        // Check children for static content (non-static elements might contain static children)
        for child in &element.children {
            lines.extend(self.extract_static_content(child, width));
        }

        lines
    }

    /// Commit static content to the terminal (write permanently)
    ///
    /// This follows the Ink/Bubbletea pattern:
    /// 1. Clear the current dynamic UI
    /// 2. Write the static content (which will persist)
    /// 3. The dynamic UI will be re-rendered below
    fn commit_static_content(&mut self, new_lines: &[String]) -> std::io::Result<()> {
        use std::io::{Write, stdout};

        // Skip if no lines to commit
        if new_lines.is_empty() {
            return Ok(());
        }

        // Clear current dynamic UI first (like Ink's log.clear())
        self.terminal.clear()?;

        let mut stdout = stdout();
        for line in new_lines {
            // Write the line with erase-to-end-of-line to ensure clean output
            writeln!(stdout, "{}\x1b[K", line)?;
            self.static_lines.push(line.clone());
        }
        stdout.flush()?;

        // Force a full repaint of the dynamic UI
        self.terminal.repaint();

        Ok(())
    }

    /// Filter out static elements from the tree
    fn filter_static_elements(&self, element: &Element) -> Element {
        let mut new_element = element.clone();

        // Remove static children
        new_element.children = element
            .children
            .iter()
            .filter(|child| !child.style.is_static)
            .map(|child| self.filter_static_elements(child))
            .collect();

        new_element
    }

    /// Render element to output buffer (helper for static content)
    fn render_element_to_output(
        &self,
        element: &Element,
        engine: &LayoutEngine,
        output: &mut Output,
        offset_x: f32,
        offset_y: f32,
    ) {
        // Skip elements with display: none
        if element.style.display == crate::core::Display::None {
            return;
        }

        let layout = engine.get_layout(element.id).unwrap_or_default();

        let x = (offset_x + layout.x) as u16;
        let y = (offset_y + layout.y) as u16;
        let width = layout.width as u16;
        let height = layout.height as u16;

        if element.style.background_color.is_some() {
            output.fill_rect(x, y, width, height, ' ', &element.style);
        }

        if element.style.has_border() {
            self.render_border(element, output, x, y, width, height);
        }

        if let Some(text) = &element.text_content {
            let text_x = x
                + if element.style.has_border() { 1 } else { 0 }
                + element.style.padding.left as u16;
            let text_y = y
                + if element.style.has_border() { 1 } else { 0 }
                + element.style.padding.top as u16;
            output.write(text_x, text_y, text, &element.style);
        }

        let child_offset_x = offset_x + layout.x;
        let child_offset_y = offset_y + layout.y;

        for child in &element.children {
            self.render_element_to_output(child, engine, output, child_offset_x, child_offset_y);
        }
    }

    fn render_element(&self, element: &Element, output: &mut Output, offset_x: f32, offset_y: f32) {
        // Skip elements with display: none
        if element.style.display == crate::core::Display::None {
            return;
        }

        // Get layout for this element
        let layout = self
            .layout_engine
            .get_layout(element.id)
            .unwrap_or_default();

        let x = (offset_x + layout.x) as u16;
        let y = (offset_y + layout.y) as u16;
        let width = layout.width as u16;
        let height = layout.height as u16;

        // Render background if set
        if element.style.background_color.is_some() {
            output.fill_rect(x, y, width, height, ' ', &element.style);
        }

        // Render border if set
        if element.style.has_border() {
            self.render_border(element, output, x, y, width, height);
        }

        // Render text content (simple or rich text with spans)
        let text_x =
            x + if element.style.has_border() { 1 } else { 0 } + element.style.padding.left as u16;
        let text_y =
            y + if element.style.has_border() { 1 } else { 0 } + element.style.padding.top as u16;

        if let Some(spans) = &element.spans {
            // Rich text with multiple spans
            self.render_spans(spans, output, text_x, text_y);
        } else if let Some(text) = &element.text_content {
            // Simple text
            output.write(text_x, text_y, text, &element.style);
        }

        // Check if overflow clipping is needed for children
        let needs_clip = element.style.overflow_x == crate::core::Overflow::Hidden
            || element.style.overflow_x == crate::core::Overflow::Scroll
            || element.style.overflow_y == crate::core::Overflow::Hidden
            || element.style.overflow_y == crate::core::Overflow::Scroll;

        // Calculate content area for clipping (inside border and padding)
        let clip_x = x + if element.style.has_border() { 1 } else { 0 };
        let clip_y = y + if element.style.has_border() { 1 } else { 0 };
        let clip_width = width.saturating_sub(if element.style.has_border() { 2 } else { 0 });
        let clip_height = height.saturating_sub(if element.style.has_border() { 2 } else { 0 });

        // Apply clip region if overflow is hidden or scroll
        if needs_clip && clip_width > 0 && clip_height > 0 {
            output.clip(crate::renderer::output::ClipRegion {
                x1: clip_x,
                y1: clip_y,
                x2: clip_x + clip_width,
                y2: clip_y + clip_height,
            });
        }

        // Render children - Taffy already includes border/padding in child positions
        // Apply scroll offset if element has scroll_offset set
        let scroll_offset_x = element.scroll_offset_x.unwrap_or(0) as f32;
        let scroll_offset_y = element.scroll_offset_y.unwrap_or(0) as f32;

        let child_offset_x = offset_x + layout.x - scroll_offset_x;
        let child_offset_y = offset_y + layout.y - scroll_offset_y;

        for child in &element.children {
            self.render_element(child, output, child_offset_x, child_offset_y);
        }

        // Remove clip region
        if needs_clip && clip_width > 0 && clip_height > 0 {
            output.unclip();
        }
    }

    fn render_border(
        &self,
        element: &Element,
        output: &mut Output,
        x: u16,
        y: u16,
        width: u16,
        height: u16,
    ) {
        let (tl, tr, bl, br, h, v) = element.style.border_style.chars();

        // Create base style for borders
        let mut base_style = element.style.clone();
        base_style.dim = element.style.border_dim;

        // Create per-side styles with their respective colors
        let mut top_style = base_style.clone();
        top_style.color = element.style.get_border_top_color();

        let mut right_style = base_style.clone();
        right_style.color = element.style.get_border_right_color();

        let mut bottom_style = base_style.clone();
        bottom_style.color = element.style.get_border_bottom_color();

        let mut left_style = base_style.clone();
        left_style.color = element.style.get_border_left_color();

        // Top border
        if element.style.border_top && height > 0 {
            // Top-left corner uses left color if no top color, or top color
            output.write_char(x, y, tl.chars().next().unwrap(), &top_style);
            for col in (x + 1)..(x + width - 1) {
                output.write_char(col, y, h.chars().next().unwrap(), &top_style);
            }
            if width > 1 {
                // Top-right corner uses top color
                output.write_char(x + width - 1, y, tr.chars().next().unwrap(), &top_style);
            }
        }

        // Bottom border
        if element.style.border_bottom && height > 1 {
            let bottom_y = y + height - 1;
            output.write_char(x, bottom_y, bl.chars().next().unwrap(), &bottom_style);
            for col in (x + 1)..(x + width - 1) {
                output.write_char(col, bottom_y, h.chars().next().unwrap(), &bottom_style);
            }
            if width > 1 {
                output.write_char(
                    x + width - 1,
                    bottom_y,
                    br.chars().next().unwrap(),
                    &bottom_style,
                );
            }
        }

        // Left border
        if element.style.border_left {
            for row in (y + 1)..(y + height - 1) {
                output.write_char(x, row, v.chars().next().unwrap(), &left_style);
            }
        }

        // Right border
        if element.style.border_right && width > 1 {
            for row in (y + 1)..(y + height - 1) {
                output.write_char(x + width - 1, row, v.chars().next().unwrap(), &right_style);
            }
        }
    }

    /// Render rich text spans
    fn render_spans(
        &self,
        lines: &[crate::components::text::Line],
        output: &mut Output,
        start_x: u16,
        start_y: u16,
    ) {
        for (line_idx, line) in lines.iter().enumerate() {
            let y = start_y + line_idx as u16;
            let mut x = start_x;

            for span in &line.spans {
                output.write(x, y, &span.content, &span.style);
                x += span.width() as u16;
            }
        }
    }

    /// Request exit
    pub fn exit(&self) {
        self.should_exit.store(true, Ordering::SeqCst);
    }
}

// === Public API Functions ===

/// Create an app builder for configuring and running a component.
///
/// This is the main entry point for running an rnk application.
/// Returns an `AppBuilder` that allows fluent configuration.
///
/// # Default Behavior
///
/// By default, the app runs in **inline mode** (like Ink and Bubbletea):
/// - Output appears at the current cursor position
/// - Content persists in terminal history after exit
/// - Supports `println()` for persistent messages
///
/// # Examples
///
/// ```ignore
/// use rnk::prelude::*;
///
/// // Inline mode (default)
/// render(my_app).run()?;
///
/// // Fullscreen mode
/// render(my_app).fullscreen().run()?;
///
/// // Custom configuration
/// render(my_app)
///     .fullscreen()
///     .fps(30)
///     .exit_on_ctrl_c(false)
///     .run()?;
/// ```
pub fn render<F>(component: F) -> AppBuilder<F>
where
    F: Fn() -> Element,
{
    AppBuilder::new(component)
}

/// Run a component in inline mode (convenience function).
///
/// This is equivalent to `render(component).run()`.
pub fn render_inline<F>(component: F) -> std::io::Result<()>
where
    F: Fn() -> Element,
{
    render(component).inline().run()
}

/// Run a component in fullscreen mode (convenience function).
///
/// This is equivalent to `render(component).fullscreen().run()`.
pub fn render_fullscreen<F>(component: F) -> std::io::Result<()>
where
    F: Fn() -> Element,
{
    render(component).fullscreen().run()
}

// === Public Render API ===

/// Render an element to a string with specified width.
///
/// This is useful for rendering elements outside the runtime,
/// such as in CLI tools or for testing.
///
/// # Arguments
///
/// * `element` - The element to render
/// * `width` - The maximum width for rendering
///
/// # Example
///
/// ```ignore
/// use rnk::prelude::*;
///
/// let element = Box::new()
///     .border_style(BorderStyle::Round)
///     .child(Text::new("Hello!").into_element())
///     .into_element();
///
/// let output = rnk::render_to_string(&element, 80);
/// println!("{}", output);
/// ```
pub fn render_to_string(element: &Element, width: u16) -> String {
    render_to_string_impl(element, width, true)
}

/// Render an element to a string without trimming trailing spaces.
///
/// This is useful when you need to preserve exact spacing.
///
/// # Arguments
///
/// * `element` - The element to render
/// * `width` - The maximum width for rendering
pub fn render_to_string_no_trim(element: &Element, width: u16) -> String {
    render_to_string_impl(element, width, false)
}

/// Render an element to a string with automatic width detection.
///
/// This detects the terminal width and uses it for rendering.
///
/// # Example
///
/// ```ignore
/// use rnk::prelude::*;
///
/// let element = Text::new("Hello, world!").into_element();
/// let output = rnk::render_to_string_auto(&element);
/// println!("{}", output);
/// ```
pub fn render_to_string_auto(element: &Element) -> String {
    let (width, _) = Terminal::size().unwrap_or((80, 24));
    render_to_string(element, width)
}

/// Internal implementation for rendering element to string
fn render_to_string_impl(element: &Element, width: u16, trim: bool) -> String {
    // Create temporary app components
    let app_helper = RenderHelper;

    app_helper.render_element_to_string_impl(element, width, trim)
}

/// Helper struct for rendering elements outside the app runtime
struct RenderHelper;

impl RenderHelper {
    fn render_element_to_string_impl(&self, element: &Element, width: u16, trim: bool) -> String {
        let mut engine = LayoutEngine::new();

        // Use the passed width directly for layout computation
        // This respects the caller's intended width (e.g., terminal width)
        let layout_width = width;

        // Calculate actual height considering text wrapping
        let height = self.calculate_element_height(element, layout_width, &mut engine);

        // Compute layout with the specified width
        engine.compute(element, layout_width, height.max(1000));

        // Get layout dimensions (layout is used to confirm computation completed)
        let _layout = engine.get_layout(element.id).unwrap_or_default();
        // IMPORTANT: Use the full layout_width for the output buffer, not the computed root width.
        // Taffy computes child positions relative to the container width (layout_width),
        // so we need the output buffer to match this width for correct positioning.
        let render_width = layout_width;
        let content_height = height.max(1);

        // Render to output buffer
        let mut output = Output::new(render_width, content_height);
        self.render_element_to_output(element, &engine, &mut output, 0.0, 0.0);

        let rendered = output.render();

        // Trim trailing spaces from each line if requested
        if trim {
            rendered
                .lines()
                .map(|line| line.trim_end())
                .collect::<Vec<_>>()
                .join("\n")
        } else {
            rendered
        }
    }

    fn calculate_content_width(&self, element: &Element) -> u16 {
        use unicode_width::UnicodeWidthStr;

        // If element has explicit width set, use it
        if let crate::core::Dimension::Points(w) = element.style.width {
            return w as u16;
        }

        let mut width = 0u16;

        // Calculate text content width
        if let Some(text) = &element.text_content {
            width = width.max(text.width() as u16);
        }

        // Calculate spans width
        if let Some(lines) = &element.spans {
            for line in lines {
                let line_width: usize = line.spans.iter().map(|span| span.width()).sum();
                width = width.max(line_width as u16);
            }
        }

        // Recursively check children for row layout
        if element.style.flex_direction == crate::core::FlexDirection::Row {
            let mut child_width_sum = 0u16;
            for child in &element.children {
                let child_width = self.calculate_content_width(child);
                child_width_sum = child_width_sum.saturating_add(child_width);
            }
            width = width.max(child_width_sum);
        } else {
            // For column layout, take the maximum child width
            for child in &element.children {
                let child_width = self.calculate_content_width(child);
                width = width.max(child_width);
            }
        }

        // Add border width
        if element.style.has_border() {
            width = width.saturating_add(2);
        }

        // Add padding width
        let padding_h = (element.style.padding.left + element.style.padding.right) as u16;
        width = width.saturating_add(padding_h);

        width.max(1)
    }

    fn calculate_element_height(
        &self,
        element: &Element,
        max_width: u16,
        _engine: &mut LayoutEngine,
    ) -> u16 {
        use crate::layout::measure::wrap_text;

        let mut height = 1u16;

        // Calculate available width for text
        let available_width = if element.style.has_border() {
            max_width.saturating_sub(2)
        } else {
            max_width
        };
        let padding_h = (element.style.padding.left + element.style.padding.right) as u16;
        let available_width = available_width.saturating_sub(padding_h).max(1);

        // Check for multiline spans with wrapping
        if let Some(lines) = &element.spans {
            let mut total_lines = 0usize;
            for line in lines {
                let line_text: String = line.spans.iter().map(|s| s.content.as_str()).collect();
                let wrapped = wrap_text(&line_text, available_width as usize);
                total_lines += wrapped.len();
            }
            height = height.max(total_lines as u16);
        }

        // Check text_content with wrapping
        if let Some(text) = &element.text_content {
            let wrapped = wrap_text(text, available_width as usize);
            height = height.max(wrapped.len() as u16);
        }

        // Add border height
        if element.style.has_border() {
            height = height.saturating_add(2);
        }

        // Add padding height
        let padding_v = (element.style.padding.top + element.style.padding.bottom) as u16;
        height = height.saturating_add(padding_v);

        // Recursively check children and accumulate height for column layout
        if !element.children.is_empty() {
            let mut child_height_sum = 0u16;
            for child in &element.children {
                let child_height = self.calculate_element_height(child, max_width, _engine);
                child_height_sum = child_height_sum.saturating_add(child_height);
            }
            height = height.max(child_height_sum);
        }

        height
    }

    fn render_element_to_output(
        &self,
        element: &Element,
        engine: &LayoutEngine,
        output: &mut Output,
        offset_x: f32,
        offset_y: f32,
    ) {
        // Skip elements with display: none
        if element.style.display == crate::core::Display::None {
            return;
        }

        let layout = engine.get_layout(element.id).unwrap_or_default();

        let x = (offset_x + layout.x) as u16;
        let y = (offset_y + layout.y) as u16;
        let width = layout.width as u16;
        let height = layout.height as u16;

        if element.style.background_color.is_some() {
            output.fill_rect(x, y, width, height, ' ', &element.style);
        }

        if element.style.has_border() {
            self.render_border(element, output, x, y, width, height);
        }

        if let Some(text) = &element.text_content {
            let text_x = x
                + if element.style.has_border() { 1 } else { 0 }
                + element.style.padding.left as u16;
            let text_y = y
                + if element.style.has_border() { 1 } else { 0 }
                + element.style.padding.top as u16;
            output.write(text_x, text_y, text, &element.style);
        }

        let child_offset_x = offset_x + layout.x;
        let child_offset_y = offset_y + layout.y;

        for child in &element.children {
            self.render_element_to_output(child, engine, output, child_offset_x, child_offset_y);
        }
    }

    fn render_border(
        &self,
        element: &Element,
        output: &mut Output,
        x: u16,
        y: u16,
        width: u16,
        height: u16,
    ) {
        let (tl, tr, bl, br, h, v) = element.style.border_style.chars();

        // Create base style for borders
        let mut base_style = element.style.clone();
        base_style.dim = element.style.border_dim;

        // Create per-side styles with their respective colors
        let mut top_style = base_style.clone();
        top_style.color = element.style.get_border_top_color();

        let mut right_style = base_style.clone();
        right_style.color = element.style.get_border_right_color();

        let mut bottom_style = base_style.clone();
        bottom_style.color = element.style.get_border_bottom_color();

        let mut left_style = base_style.clone();
        left_style.color = element.style.get_border_left_color();

        // Top border
        if element.style.border_top && height > 0 {
            output.write_char(x, y, tl.chars().next().unwrap(), &top_style);
            for col in (x + 1)..(x + width - 1) {
                output.write_char(col, y, h.chars().next().unwrap(), &top_style);
            }
            if width > 1 {
                output.write_char(x + width - 1, y, tr.chars().next().unwrap(), &top_style);
            }
        }

        // Bottom border
        if element.style.border_bottom && height > 1 {
            let bottom_y = y + height - 1;
            output.write_char(x, bottom_y, bl.chars().next().unwrap(), &bottom_style);
            for col in (x + 1)..(x + width - 1) {
                output.write_char(col, bottom_y, h.chars().next().unwrap(), &bottom_style);
            }
            if width > 1 {
                output.write_char(
                    x + width - 1,
                    bottom_y,
                    br.chars().next().unwrap(),
                    &bottom_style,
                );
            }
        }

        // Left border
        if element.style.border_left {
            for row in (y + 1)..(y + height - 1) {
                output.write_char(x, row, v.chars().next().unwrap(), &left_style);
            }
        }

        // Right border
        if element.style.border_right && width > 1 {
            for row in (y + 1)..(y + height - 1) {
                output.write_char(x + width - 1, row, v.chars().next().unwrap(), &right_style);
            }
        }
    }
}

/// Print a message with trimmed trailing spaces (explicit API).
///
/// This is the same as `println()` but makes the trimming behavior explicit.
///
/// # Example
///
/// ```ignore
/// use rnk::prelude::*;
///
/// rnk::println_trimmed("Hello!");
/// rnk::println_trimmed(Text::new("World").into_element());
/// ```
pub fn println_trimmed(message: impl IntoPrintable) {
    println(message)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_app_options_default() {
        let options = AppOptions::default();
        assert_eq!(options.fps, 60); // Changed from 30 to 60 (Bubbletea default)
        assert!(options.exit_on_ctrl_c);
        assert!(!options.alternate_screen); // Changed: inline mode by default
    }

    #[test]
    fn test_app_builder_defaults() {
        fn dummy() -> Element {
            crate::components::Text::new("test").into_element()
        }
        let builder = AppBuilder::new(dummy);
        assert!(!builder.options().alternate_screen);
        assert_eq!(builder.options().fps, 60);
    }

    #[test]
    fn test_app_builder_fullscreen() {
        fn dummy() -> Element {
            crate::components::Text::new("test").into_element()
        }
        let builder = AppBuilder::new(dummy).fullscreen();
        assert!(builder.options().alternate_screen);
    }

    #[test]
    fn test_app_builder_inline() {
        fn dummy() -> Element {
            crate::components::Text::new("test").into_element()
        }
        let builder = AppBuilder::new(dummy).fullscreen().inline();
        assert!(!builder.options().alternate_screen);
    }

    #[test]
    fn test_app_builder_fps() {
        fn dummy() -> Element {
            crate::components::Text::new("test").into_element()
        }
        let builder = AppBuilder::new(dummy).fps(30);
        assert_eq!(builder.options().fps, 30);
    }

    #[test]
    fn test_mode_switch_enum() {
        assert_eq!(ModeSwitch::EnterAltScreen, ModeSwitch::EnterAltScreen);
        assert_ne!(ModeSwitch::EnterAltScreen, ModeSwitch::ExitAltScreen);
    }

    #[test]
    fn test_registry_cleanup_on_drop() {
        let runtime = AppRuntime::new(false);

        {
            let _guard = register_app(runtime);
            assert!(render_handle().is_some());
            assert_eq!(is_alt_screen(), Some(false));
        }

        assert!(render_handle().is_none());
        assert_eq!(is_alt_screen(), None);
    }
}
