//! Shared helper functions for TUI layout handling.
//!
//! This module extracts common utility functions that `TuiApp<O: TuiOutput>`
//! uses for layout handling and buffer management.
//!
//! # Functions
//!
//! - `collect_windows` - Recursively flatten window tree to list
//! - `apply_layout` - Apply layout response to core state
//! - `create_default_window` - Create a default window for empty layouts
//! - `fetch_buffer_contents` - Fetch buffer content for visible windows

use reovim_protocol::v2::{GetLayoutResponse, WindowInfo, WindowNode, WindowRect};

use crate::{
    TuiCoreState,
    grpc_client::{TuiGrpcClient, TuiGrpcError},
};

/// Recursively collect windows from a layout tree node.
///
/// Flattens the hierarchical window tree into a linear list, marking
/// the focused window based on `focused_id`.
///
/// # Arguments
///
/// * `node` - Current window tree node
/// * `focused_id` - ID of the focused window
/// * `out` - Output vector to collect windows into
pub fn collect_windows(node: &WindowNode, focused_id: u64, out: &mut Vec<WindowInfo>) {
    if let Some(n) = &node.node {
        match n {
            reovim_protocol::v2::window_node::Node::Leaf(leaf) => {
                // Skip windows with no buffer (they can't be displayed)
                if leaf.buffer_id.is_some() {
                    out.push(WindowInfo {
                        window_id: leaf.window_id,
                        buffer_id: leaf.buffer_id,
                        rect: leaf.rect,
                        focused: leaf.window_id == focused_id,
                        opacity: None,
                    });
                } else {
                    tracing::debug!(window_id = leaf.window_id, "Skipping window with no buffer");
                }
            }
            reovim_protocol::v2::window_node::Node::Split(split) => {
                for child in &split.children {
                    collect_windows(child, focused_id, out);
                }
            }
        }
    }
}

/// Apply a layout response to the core state.
///
/// Updates focused window ID and flattens the window tree. If the layout
/// is empty, marks that a default window needs to be created.
///
/// # Arguments
///
/// * `state` - Mutable reference to core state
/// * `layout` - Layout response from server
pub fn apply_layout(state: &mut TuiCoreState, layout: &GetLayoutResponse) {
    // focused_window_id is Option<u64> - None means "no windows" or "no focus"
    state.focused_window_id = layout.focused_window_id.unwrap_or(0);

    // Flatten window tree to list
    state.windows.clear();
    if let Some(root) = &layout.root {
        collect_windows(root, state.focused_window_id, &mut state.windows);
    }

    // #401 Phase 5: Store tab info from server
    state.active_tab_id = layout.active_tab_id;
    state.tabs.clone_from(&layout.tabs);

    // Mark if we need to create a default window
    state.set_needs_default_window(state.windows.is_empty());
}

/// Apply a layout changed notification to the core state.
///
/// Similar to `apply_layout` but uses the notification format which has
/// a flat window list instead of a tree.
///
/// # Arguments
///
/// * `state` - Mutable reference to core state
/// * `focused_window_id` - Optional focused window ID
/// * `windows` - Flat list of windows from notification
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn apply_layout_notification(
    state: &mut TuiCoreState,
    focused_window_id: Option<u64>,
    windows: Vec<WindowInfo>,
) {
    // Determine effective focused ID
    let effective_focused_id = match focused_window_id {
        Some(id) => id,
        None if !windows.is_empty() => {
            tracing::debug!(
                "Server sent no focused_window_id with {} windows, using first",
                windows.len()
            );
            windows.first().map_or(0, |w| w.window_id)
        }
        None => 0,
    };

    state.focused_window_id = effective_focused_id;

    // Set focused flag on the matching window
    state.windows = windows
        .into_iter()
        .map(|mut w| {
            w.focused = w.window_id == effective_focused_id;
            w
        })
        .collect();

    // Clean up stale cursor entries
    state.cleanup_stale_cursors();
}

/// Create a default window view for empty layout.
///
/// Called when server returns no windows. Creates a local window
/// viewing the specified buffer with full viewport dimensions.
///
/// # Arguments
///
/// * `state` - Mutable reference to core state
/// * `buffer_id` - Buffer ID to display in the window
/// * `width` - Viewport width
/// * `height` - Viewport height
pub fn create_default_window(state: &mut TuiCoreState, buffer_id: u64, width: u16, height: u16) {
    let content_height = height.saturating_sub(1); // Reserve statusline

    let window = WindowInfo {
        window_id: 1, // Local ID
        buffer_id: Some(buffer_id),
        rect: Some(WindowRect {
            x: 0,
            y: 0,
            width: u64::from(width),
            height: u64::from(content_height),
        }),
        focused: true,
        opacity: None,
    };

    state.windows.push(window);
    state.focused_window_id = 1;
    state.set_needs_default_window(false);

    tracing::debug!(buffer_id, "Created default window for empty layout");
}

/// Fetch buffer content for all visible windows.
///
/// Iterates through windows and fetches content for any buffers not
/// already in the cache.
///
/// # Arguments
///
/// * `client` - gRPC client for server communication
/// * `state` - Mutable reference to core state
///
/// # Errors
///
/// Returns an error if buffer content fetch fails.
#[cfg_attr(coverage_nightly, coverage(off))]
pub async fn fetch_buffer_contents(
    client: &mut TuiGrpcClient,
    state: &mut TuiCoreState,
) -> Result<(), TuiGrpcError> {
    // Collect buffer IDs to fetch (filter out None values)
    let buffer_ids: Vec<u64> = state.windows.iter().filter_map(|w| w.buffer_id).collect();

    for buffer_id in buffer_ids {
        if let std::collections::hash_map::Entry::Vacant(e) = state.buffer_cache.entry(buffer_id) {
            let content = client
                .get_buffer_content(Some(buffer_id), None, None)
                .await?;
            e.insert(content.lines);
        }
    }

    Ok(())
}

/// Invalidate and refetch a specific buffer's content.
///
/// Called when a buffer is modified to refresh the cached content.
///
/// # Arguments
///
/// * `client` - gRPC client for server communication
/// * `state` - Mutable reference to core state
/// * `buffer_id` - ID of the modified buffer
///
/// # Errors
///
/// Returns an error if buffer content fetch fails.
#[cfg_attr(coverage_nightly, coverage(off))]
pub async fn refetch_buffer(
    client: &mut TuiGrpcClient,
    state: &mut TuiCoreState,
    buffer_id: u64,
) -> Result<(), TuiGrpcError> {
    state.buffer_cache.remove(&buffer_id);
    let content = client
        .get_buffer_content(Some(buffer_id), None, None)
        .await?;
    state.buffer_cache.insert(buffer_id, content.lines);
    Ok(())
}

#[cfg(test)]
#[path = "core_helpers_tests.rs"]
mod tests;
