speedtest-tui 0.1.1

A terminal-based network speed test tool with real-time gauges and graphs
use ratatui::{
    layout::{Constraint, Direction, Layout, Rect},
    prelude::*,
    widgets::{Block, Borders, Paragraph, Wrap},
    Frame,
};

use super::{gauge::GaugeWidget, graph::GraphWidget, Theme};
use crate::app::{App, TestPhase};

pub fn create_main_layout(area: Rect) -> Vec<Rect> {
    Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(3),
            Constraint::Min(10),
            Constraint::Length(3),
        ])
        .split(area)
        .to_vec()
}

pub fn render_header(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
    let title = format!(" SPEEDTEST TUI v{} ", env!("CARGO_PKG_VERSION"));
    let title_len = title.len();

    let provider_status = if app.is_connected {
        format!("[{}] ● Connected", app.current_provider)
    } else {
        format!("[{}] ○ Disconnected", app.current_provider)
    };
    let provider_len = provider_status.len();

    let padding = area
        .width
        .saturating_sub(title_len as u16 + provider_len as u16 + 2) as usize;

    let header_text = Line::from(vec![
        Span::styled(title, Style::default().fg(theme.primary).bold()),
        Span::raw(" ".repeat(padding)),
        Span::styled(
            provider_status,
            Style::default().fg(if app.is_connected {
                theme.success
            } else {
                theme.muted
            }),
        ),
    ]);

    let header = Paragraph::new(header_text)
        .block(
            Block::default()
                .borders(Borders::BOTTOM)
                .border_style(Style::default().fg(theme.border)),
        )
        .style(Style::default().bg(theme.background));

    frame.render_widget(header, area);
}

pub fn render_main_view(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Min(12),
            Constraint::Length(3),
            Constraint::Length(4),
        ])
        .split(area);

    let top_chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
        .split(chunks[0]);

    render_gauge_area(frame, top_chunks[0], app, theme);
    render_graph_area(frame, top_chunks[1], app, theme);
    render_stats_bar(frame, chunks[1], app, theme);
    render_results(frame, chunks[2], app, theme);
}

fn render_gauge_area(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::default().fg(theme.border))
        .style(Style::default().bg(theme.background));

    let inner = block.inner(area);
    frame.render_widget(block, area);

    // Calculate current speed and phase label
    let (current_speed, phase_label) = match app.test_phase {
        TestPhase::Idle => (0.0, "IDLE"),
        TestPhase::Connecting => (0.0, "CONNECTING"),
        TestPhase::Ping => (0.0, "PING"),
        TestPhase::Download => (app.current_download_speed, "DOWNLOAD"),
        TestPhase::Upload => (app.current_upload_speed, "UPLOAD"),
        TestPhase::Complete => {
            let speed = app
                .results
                .download
                .as_ref()
                .map(|d| d.speed_mbps)
                .unwrap_or(0.0);
            (speed, "COMPLETE")
        }
    };

    let gauge = GaugeWidget::new(current_speed, app.gauge_max_scale)
        .with_label(phase_label)
        .with_theme(theme);

    gauge.render(frame, inner);
}

fn render_graph_area(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
    let title = match app.test_phase {
        TestPhase::Download => " Download Speed ",
        TestPhase::Upload => " Upload Speed ",
        _ => " Speed Over Time ",
    };

    let block = Block::default()
        .title(title)
        .title_style(Style::default().fg(theme.primary))
        .borders(Borders::ALL)
        .border_style(Style::default().fg(theme.border))
        .style(Style::default().bg(theme.background));

    let inner = block.inner(area);
    frame.render_widget(block, area);

    let graph = GraphWidget::new(&app.speed_samples, app.gauge_max_scale).with_theme(theme);

    graph.render(frame, inner);
}

fn render_stats_bar(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
    let ping = app.results.ping.as_ref();
    let ping_text = ping
        .map(|p| format!("{:.0} ms", p.latency_ms))
        .unwrap_or_else(|| "-".to_string());
    let jitter_text = ping
        .map(|p| format!("{:.1} ms", p.jitter_ms))
        .unwrap_or_else(|| "-".to_string());
    let loss_text = ping
        .map(|p| format!("{:.0}%", p.packet_loss_percent))
        .unwrap_or_else(|| "-".to_string());

    let stats = Line::from(vec![
        Span::styled("  ● Ping: ", Style::default().fg(theme.muted)),
        Span::styled(ping_text, Style::default().fg(theme.foreground)),
        Span::styled("    ● Jitter: ", Style::default().fg(theme.muted)),
        Span::styled(jitter_text, Style::default().fg(theme.foreground)),
        Span::styled("    ● Packet Loss: ", Style::default().fg(theme.muted)),
        Span::styled(loss_text, Style::default().fg(theme.foreground)),
    ]);

    let stats_widget = Paragraph::new(stats)
        .block(
            Block::default()
                .borders(Borders::ALL)
                .border_style(Style::default().fg(theme.border)),
        )
        .style(Style::default().bg(theme.highlight));

    frame.render_widget(stats_widget, area);
}

fn render_results(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
    let download = app.results.download.as_ref();
    let upload = app.results.upload.as_ref();

    let download_speed = download
        .map(|d| format!("{:.1} Mbps", d.speed_mbps))
        .unwrap_or_else(|| "-".to_string());
    let download_peak = download
        .map(|d| format!("(peak: {:.1})", d.peak_speed_mbps))
        .unwrap_or_default();

    let upload_speed = upload
        .map(|u| format!("{:.1} Mbps", u.speed_mbps))
        .unwrap_or_else(|| "-".to_string());
    let upload_peak = upload
        .map(|u| format!("(peak: {:.1})", u.peak_speed_mbps))
        .unwrap_or_default();

    let results_text = vec![
        Line::from(vec![
            Span::styled("  ↓ Download: ", Style::default().fg(theme.success)),
            Span::styled(download_speed, Style::default().fg(theme.foreground).bold()),
            Span::styled(
                format!(" {}", download_peak),
                Style::default().fg(theme.muted),
            ),
        ]),
        Line::from(vec![
            Span::styled("  ↑ Upload:   ", Style::default().fg(theme.secondary)),
            Span::styled(upload_speed, Style::default().fg(theme.foreground).bold()),
            Span::styled(
                format!(" {}", upload_peak),
                Style::default().fg(theme.muted),
            ),
        ]),
    ];

    let results_widget = Paragraph::new(results_text)
        .block(
            Block::default()
                .borders(Borders::ALL)
                .border_style(Style::default().fg(theme.border)),
        )
        .style(Style::default().bg(theme.background));

    frame.render_widget(results_widget, area);
}

pub fn render_footer(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
    let keybindings = if app.is_testing {
        "[Esc] Cancel"
    } else {
        "[S]tart  [P]rovider  [H]istory  [D]iagnostics  [E]xport  [T]heme  [?]Help  [Q]uit"
    };

    let footer = Paragraph::new(keybindings)
        .style(Style::default().fg(theme.muted))
        .alignment(Alignment::Center)
        .block(
            Block::default()
                .borders(Borders::TOP)
                .border_style(Style::default().fg(theme.border)),
        );

    frame.render_widget(footer, area);
}

pub fn render_settings_view(frame: &mut Frame, area: Rect, app: &App, theme: &Theme) {
    let block = Block::default()
        .title(" Settings ")
        .title_style(Style::default().fg(theme.primary))
        .borders(Borders::ALL)
        .border_style(Style::default().fg(theme.border))
        .style(Style::default().bg(theme.background));

    let inner = block.inner(area);
    frame.render_widget(block, area);

    let settings_text = vec![
        Line::from(vec![
            Span::styled("Theme: ", Style::default().fg(theme.muted)),
            Span::styled(
                app.theme_name.as_str().to_string(),
                Style::default().fg(theme.foreground),
            ),
        ]),
        Line::from(""),
        Line::from(vec![
            Span::styled("Provider: ", Style::default().fg(theme.muted)),
            Span::styled(&app.current_provider, Style::default().fg(theme.foreground)),
        ]),
        Line::from(""),
        Line::from(vec![
            Span::styled("Test Duration: ", Style::default().fg(theme.muted)),
            Span::styled(
                format!("{} seconds", app.test_duration),
                Style::default().fg(theme.foreground),
            ),
        ]),
    ];

    let settings = Paragraph::new(settings_text).wrap(Wrap { trim: true });

    frame.render_widget(settings, inner);
}