tngl 0.1.0

Repo-native TUI graph tool for code relationships
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::Frame;
use ratatui::layout::{Constraint, Flex, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph};

use crate::parser::config::{Config, OnDelete};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingsEvent {
    None,
    Changed,
    Close,
}

#[derive(Debug, Clone, Copy, Default)]
pub struct SettingsPanelState {
    pub selected_row: usize,
}

const SETTINGS_ROW_COUNT: usize = 3;

pub fn handle_key(
    key: KeyEvent,
    state: &mut SettingsPanelState,
    config: &mut Config,
) -> SettingsEvent {
    match key.code {
        KeyCode::Esc | KeyCode::Backspace | KeyCode::Char('q') => SettingsEvent::Close,
        KeyCode::Up | KeyCode::Char('k') => {
            state.selected_row = state.selected_row.saturating_sub(1);
            SettingsEvent::None
        }
        KeyCode::Down | KeyCode::Char('j') => {
            state.selected_row = (state.selected_row + 1).min(SETTINGS_ROW_COUNT - 1);
            SettingsEvent::None
        }
        KeyCode::Left | KeyCode::Char('h') => adjust(config, state.selected_row),
        KeyCode::Right | KeyCode::Char('l') | KeyCode::Enter | KeyCode::Char(' ') => {
            adjust(config, state.selected_row)
        }
        _ => SettingsEvent::None,
    }
}

pub fn draw(frame: &mut Frame, state: &SettingsPanelState, config: &Config) {
    let area = centered_rect(frame.area(), 56, 44);
    frame.render_widget(Clear, area);

    let title = Line::from(vec![
        Span::styled(
            "Setup",
            Style::default()
                .fg(Color::Cyan)
                .add_modifier(Modifier::BOLD),
        ),
        Span::raw("  "),
        Span::styled("[Esc] close", Style::default().fg(Color::Gray)),
    ]);

    let selected_row = state.selected_row.min(SETTINGS_ROW_COUNT - 1);
    let mut lines = vec![
        settings_row(
            selected_row == 0,
            "reveal linked paths",
            config.auto_reveal_links,
        ),
        settings_row(selected_row == 1, "git hook updates", config.git_hooks),
        settings_row(
            selected_row == 2,
            "warn blank comments",
            config.warn_uncommented_edges,
        ),
        Line::from(""),
        Line::from(Span::styled(
            "About this option",
            Style::default()
                .fg(Color::Gray)
                .add_modifier(Modifier::BOLD),
        )),
    ];
    for text in selected_row_description(selected_row) {
        lines.push(Line::from(Span::styled(
            text,
            Style::default().fg(Color::DarkGray),
        )));
    }
    lines.extend([
        Line::from(""),
        Line::from(Span::styled(
            "Use arrows/hjkl or Enter/Space to toggle.",
            Style::default().fg(Color::DarkGray),
        )),
        Line::from(Span::styled(
            "Changes write to config immediately.",
            Style::default().fg(Color::DarkGray),
        )),
    ]);

    let panel = Paragraph::new(lines).block(
        Block::default()
            .title(title)
            .borders(Borders::ALL)
            .border_type(BorderType::Rounded)
            .border_style(Style::default().fg(Color::Cyan))
            .padding(Padding::new(1, 1, 1, 0)),
    );
    frame.render_widget(panel, area);
}

pub fn serialize_config(config: &Config) -> String {
    let on_delete = match config.on_delete {
        OnDelete::Prompt => "prompt",
        OnDelete::Delete => "delete",
        OnDelete::Preserve => "preserve",
    };
    let editor = config.editor.clone().unwrap_or_default();
    format!(
        "\
# tngl configuration
# Edit manually or run: tngl setup

# What to do with edges when a file is deleted
# Options: prompt | delete | preserve
on_delete: {}

# Whether to show orphan nodes in the default TUI view
show_orphans: {}

# In view mode, reveal linked nodes inside collapsed folders while focused
auto_reveal_links: {}

# Auto-update graph.tngl on git operations (requires hook install)
git_hooks: {}

# Preferred editor for `tngl edit` (falls back to $VISUAL, then $EDITOR)
editor: {}

# Warn in `tngl status` when an edge has an empty label/comment
warn_uncommented_edges: {}
",
        on_delete,
        config.show_orphans,
        config.auto_reveal_links,
        config.git_hooks,
        editor,
        config.warn_uncommented_edges
    )
}

fn settings_row(selected: bool, key: &str, enabled: bool) -> Line<'static> {
    let indicator = if selected { ">" } else { " " };
    let base_style = if selected {
        Style::default()
            .fg(Color::White)
            .bg(Color::DarkGray)
            .add_modifier(Modifier::BOLD)
    } else {
        Style::default().fg(Color::Gray)
    };
    let value_text = if enabled { "[ON]" } else { "[OFF]" };
    let mut value_style = if enabled {
        Style::default().fg(Color::Green)
    } else {
        Style::default().fg(Color::LightRed)
    };
    if selected {
        value_style = value_style.bg(Color::DarkGray).add_modifier(Modifier::BOLD);
    } else {
        value_style = value_style.add_modifier(Modifier::BOLD);
    }

    Line::from(vec![
        Span::styled(format!("{indicator} {key:<24}"), base_style),
        Span::styled(value_text, value_style),
    ])
}

fn selected_row_description(selected_row: usize) -> [&'static str; 2] {
    match selected_row {
        0 => [
            "Temporarily opens collapsed folders that contain links",
            "to or from the currently focused node.",
        ],
        1 => [
            "If hooks are installed, Git operations can run",
            "`tngl update --silent` automatically.",
        ],
        2 => [
            "Shows status warnings when an edge has an empty",
            "label/comment so tangles stay documented.",
        ],
        _ => ["", ""],
    }
}

fn adjust(config: &mut Config, selected_row: usize) -> SettingsEvent {
    match selected_row {
        0 => {
            config.auto_reveal_links = !config.auto_reveal_links;
            SettingsEvent::Changed
        }
        1 => {
            config.git_hooks = !config.git_hooks;
            SettingsEvent::Changed
        }
        2 => {
            config.warn_uncommented_edges = !config.warn_uncommented_edges;
            SettingsEvent::Changed
        }
        _ => SettingsEvent::None,
    }
}

fn centered_rect(area: Rect, width_percent: u16, height_percent: u16) -> Rect {
    let vertical = Layout::vertical([
        Constraint::Percentage((100 - height_percent) / 2),
        Constraint::Percentage(height_percent),
        Constraint::Percentage((100 - height_percent) / 2),
    ])
    .flex(Flex::Center)
    .split(area);
    Layout::horizontal([
        Constraint::Percentage((100 - width_percent) / 2),
        Constraint::Percentage(width_percent),
        Constraint::Percentage((100 - width_percent) / 2),
    ])
    .flex(Flex::Center)
    .split(vertical[1])[1]
}