clin-rs 0.5.2

Encrypted terminal note-taking app
use ratatui::layout::Rect;
use ratatui::Frame;

use crate::graf::app::GrafAppState;
use crate::graf::config::GrafConfig;

pub fn draw_ui(frame: &mut Frame, state: &GrafAppState, config: &GrafConfig) {
    let area = frame.area();

    if state.show_help {
        draw_help(frame, area, config);
        return;
    }

    if !state.config_errors.is_empty() {
        draw_config_errors(frame, area, &state.config_errors, config);
        return;
    }

    // Compute theme colors once for all drawing functions
    let colors = config.theme_colors();

    if let Some(graph_state) = &state.graph_state {
        let guard = graph_state.read().unwrap_or_else(|e| e.into_inner());
        let flags = crate::graph::render::FeatureFlags {
            show_legend: state.show_legend,
            show_grid: state.show_grid,
            show_minimap: state.show_minimap,
            show_status_bar: state.show_status_bar,
        };
        crate::graph::render::draw_graph_view(frame, &guard, config, &flags);
    }

    if state.search_active {
        draw_search(frame, area, state, config, &colors);
    }

    if let Some(ref msg) = state.config_reload_msg {
        draw_reload_notification(frame, area, msg, &colors);
    }
}

fn draw_config_errors(frame: &mut Frame, area: Rect, errors: &[String], config: &GrafConfig) {
    let config_path = crate::graf::config::GrafConfig::config_path()
        .unwrap_or_default()
        .display()
        .to_string();
    let mut lines = vec!["Config Errors".to_string(), "".to_string()];
    for err in errors {
        lines.push(format!("  - {}", err));
        if let Some(suggestion) = suggest_fix(err) {
            lines.push(format!("    -> {}", suggestion));
        }
    }
    lines.push("".to_string());
    lines.push(format!("Fix: {}", config_path));
    lines.push("Press any key to close".to_string());

    let text = lines.join("\n");
    let paragraph = ratatui::widgets::Paragraph::new(text)
        .block(
            ratatui::widgets::Block::default()
                .borders(ratatui::widgets::Borders::ALL)
                .title("Config Error")
                .border_type(config.display.border_style.to_border_type()),
        )
        .alignment(ratatui::layout::Alignment::Left);

    let max_width = lines.iter().map(|l| l.len()).max().unwrap_or(0) + 4;
    let height = lines.len() as u16 + 2;
    let popup_area = ratatui::layout::Rect {
        x: (area.width.saturating_sub(max_width as u16)) / 2,
        y: (area.height.saturating_sub(height)) / 2,
        width: max_width.min(area.width as usize) as u16,
        height: height.min(area.height),
    };

    frame.render_widget(paragraph, popup_area);
}

fn suggest_fix(err: &str) -> Option<String> {
    let err_lower = err.to_lowercase();
    if err_lower.contains("theme") {
        return Some("Valid themes: default, tokyonight, catppuccinmocha, onedark, gruvbox, dracula, nord, rosepine, everforest, kanagawa, solarized".to_string());
    }
    if err_lower.contains("background") {
        return Some("Valid backgrounds: transparent, solid".to_string());
    }
    if err_lower.contains("node_color_mode") {
        return Some("Valid modes: tag, folder, linkcount, uniform".to_string());
    }
    if err_lower.contains("edge_color_mode") {
        return Some("Valid modes: source, target, uniform".to_string());
    }
    if err_lower.contains("label_mode") {
        return Some("Valid modes: selected, neighbors, all, none".to_string());
    }
    if err_lower.contains("node_size_mode") {
        return Some("Valid modes: fixed, linkcount".to_string());
    }
    if err_lower.contains("border_style") {
        return Some("Valid styles: plain, rounded, double, none".to_string());
    }
    if err_lower.contains("legend_position") {
        return Some("Valid positions: topright, topleft, bottomright, bottomleft".to_string());
    }
    None
}

fn draw_help(frame: &mut Frame, area: Rect, config: &GrafConfig) {
    let help_text = vec![
        "Keyboard",
        "  Arrows      Navigate nodes",
        "  +/-         Zoom in/out",
        "  Enter       Open selected file",
        "  a           Auto-fit view",
        "  f           Search nodes",
        "  Shift+m     Toggle minimap",
        "  Shift+l     Toggle legend",
        "  Shift+g     Toggle grid",
        "  Shift+s     Toggle status bar",
        "  r           Refresh simulation",
        "  Ctrl+r      Reload config",
        "  ?           Toggle help",
        "  q/Esc       Quit",
        "",
        "Mouse",
        "  Scroll    Zoom in/out",
        "  Drag bg   Pan view",
        "  Drag node Move node",
        "  Click     Select node",
        "  Dbl-click Open file",
    ];

    let text: String = help_text.join("\n");
    let paragraph = ratatui::widgets::Paragraph::new(text)
        .block(
            ratatui::widgets::Block::default()
                .borders(ratatui::widgets::Borders::ALL)
                .title("Help")
                .border_type(config.display.border_style.to_border_type()),
        )
        .alignment(ratatui::layout::Alignment::Left);

    let max_width = help_text.iter().map(|l| l.len()).max().unwrap_or(0) + 4;
    let help_height = help_text.len() as u16 + 2;
    let help_area = ratatui::layout::Rect {
        x: (area.width.saturating_sub(max_width as u16)) / 2,
        y: (area.height.saturating_sub(help_height)) / 2,
        width: max_width.min(area.width as usize) as u16,
        height: help_height.min(area.height),
    };

    frame.render_widget(paragraph, help_area);
}

fn draw_search(frame: &mut Frame, area: Rect, state: &GrafAppState, config: &GrafConfig, colors: &crate::graf::config::ThemeColors) {
    let max_visible = config.search.max_visible;
    let result_count = state.search_results.len();
    let visible_count = result_count.min(max_visible);
    let popup_width = config.search.popup_width.min(area.width.saturating_sub(4));
    let popup_height = (visible_count + 3).min(area.height.saturating_sub(4) as usize) as u16;

    let popup_x = (area.width.saturating_sub(popup_width)) / 2;
    let popup_y = config.search.popup_y;

    let popup_area = ratatui::layout::Rect::new(popup_x, popup_y, popup_width, popup_height);

    let before = &state.search_query[..state.search_cursor];
    let after = &state.search_query[state.search_cursor..];
    let label_style = ratatui::style::Style::default().fg(colors.label_color);
    let cursor_style = ratatui::style::Style::default()
        .fg(colors.border_color)
        .add_modifier(ratatui::style::Modifier::REVERSED);
    let input_line = ratatui::text::Line::from(vec![
        ratatui::text::Span::styled(before.to_string(), label_style),
        ratatui::text::Span::styled(
            after
                .chars()
                .next()
                .map(|c| c.to_string())
                .unwrap_or_else(|| " ".to_string()),
            cursor_style,
        ),
        ratatui::text::Span::styled(
            after
                .chars()
                .next()
                .map(|_| {
                    after[after
                        .char_indices()
                        .nth(1)
                        .map(|(i, _)| i)
                        .unwrap_or(after.len())..]
                        .to_string()
                })
                .unwrap_or_default(),
            label_style,
        ),
    ]);

    let mut lines: Vec<ratatui::text::Line> = vec![input_line];

    if result_count == 0 && !state.search_query.is_empty() {
        lines.push(ratatui::text::Line::styled(
            "  No matches",
            ratatui::style::Style::default().fg(colors.status_bar_color),
        ));
    } else {
        let scroll_offset = state
            .search_selected
            .saturating_sub(max_visible.saturating_sub(1));
        for (i, (_, title)) in state
            .search_results
            .iter()
            .enumerate()
            .skip(scroll_offset)
            .take(max_visible)
        {
            let is_selected = i == state.search_selected;
            let style = if is_selected {
                ratatui::style::Style::default()
                    .fg(ratatui::style::Color::Black)
                    .bg(colors.label_color)
            } else {
                ratatui::style::Style::default().fg(colors.label_color)
            };
            let prefix = "  ";
            let display = crate::graf::util::truncate(title, (popup_width as usize).saturating_sub(6));
            lines.push(ratatui::text::Line::styled(
                format!("{}{}", prefix, display),
                style,
            ));
        }
    }

    let block = ratatui::widgets::Block::default()
        .borders(ratatui::widgets::Borders::ALL)
        .title("Search")
        .border_type(config.display.border_style.to_border_type())
        .border_style(ratatui::style::Style::default().fg(colors.border_color));

    let paragraph = ratatui::widgets::Paragraph::new(lines).block(block);
    frame.render_widget(paragraph, popup_area);
}

fn draw_reload_notification(frame: &mut Frame, area: Rect, msg: &str, colors: &crate::graf::config::ThemeColors) {
    let width = (msg.len() as u16 + 4).min(area.width);
    let height = 3u16;
    let x = (area.width.saturating_sub(width)) / 2;
    let y = area.height.saturating_sub(height) / 2;

    let popup_area = ratatui::layout::Rect::new(x, y, width, height);

    let is_error = msg.starts_with("Config error");
    let fg = if is_error {
        ratatui::style::Color::Red
    } else {
        colors.label_color
    };

    let paragraph = ratatui::widgets::Paragraph::new(msg)
        .style(ratatui::style::Style::default().fg(fg))
        .alignment(ratatui::layout::Alignment::Center)
        .block(
            ratatui::widgets::Block::default()
                .borders(ratatui::widgets::Borders::ALL)
                .border_style(ratatui::style::Style::default().fg(colors.border_color)),
        );

    frame.render_widget(paragraph, popup_area);
}