//! Loading spinner indicators (full-screen and corner).

use ftui::core::geometry::Rect;
use ftui::render::cell::{Cell, PackedRgba};
use ftui::render::drawing::Draw;
use ftui::render::frame::Frame;

use crate::state::LoadState;

/// Braille spinner frames for loading animation.
const SPINNER_FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];

/// Select spinner frame from tick count.
#[must_use]
pub(crate) fn spinner_char(tick: u64) -> char {
    SPINNER_FRAMES[(tick as usize) % SPINNER_FRAMES.len()]
}

/// Render a loading indicator.
///
/// - `LoadingInitial`: centered full-screen spinner with "Loading..."
/// - `Refreshing`: subtle spinner in top-right corner
/// - Other states: no-op
pub fn render_loading(
    frame: &mut Frame<'_>,
    area: Rect,
    load_state: &LoadState,
    text_color: PackedRgba,
    muted_color: PackedRgba,
    tick: u64,
) {
    match load_state {
        LoadState::LoadingInitial => {
            render_centered_spinner(frame, area, "Loading...", text_color, tick);
        }
        LoadState::Refreshing => {
            render_corner_spinner(frame, area, muted_color, tick);
        }
        _ => {}
    }
}

/// Render a centered spinner with message.
fn render_centered_spinner(
    frame: &mut Frame<'_>,
    area: Rect,
    message: &str,
    color: PackedRgba,
    tick: u64,
) {
    if area.height == 0 || area.width < 5 {
        return;
    }

    let spinner = spinner_char(tick);
    let text = format!("{spinner} {message}");
    let text_len = text.len() as u16;

    // Center horizontally and vertically.
    let x = area
        .x
        .saturating_add(area.width.saturating_sub(text_len) / 2);
    let y = area.y.saturating_add(area.height / 2);

    let cell = Cell {
        fg: color,
        ..Cell::default()
    };
    frame.print_text_clipped(x, y, &text, cell, area.right());
}

/// Render a subtle spinner in the top-right corner.
fn render_corner_spinner(frame: &mut Frame<'_>, area: Rect, color: PackedRgba, tick: u64) {
    if area.width < 2 || area.height == 0 {
        return;
    }

    let spinner = spinner_char(tick);
    let x = area.right().saturating_sub(2);
    let y = area.y;

    let cell = Cell {
        fg: color,
        ..Cell::default()
    };
    frame.print_text_clipped(x, y, &spinner.to_string(), cell, area.right());
}

#[cfg(test)]
mod tests {
    use super::*;
    use ftui::render::grapheme_pool::GraphemePool;

    macro_rules! with_frame {
        ($width:expr, $height:expr, |$frame:ident| $body:block) => {{
            let mut pool = GraphemePool::new();
            let mut $frame = Frame::new($width, $height, &mut pool);
            $body
        }};
    }

    fn white() -> PackedRgba {
        PackedRgba::rgb(0xFF, 0xFF, 0xFF)
    }

    fn gray() -> PackedRgba {
        PackedRgba::rgb(0x80, 0x80, 0x80)
    }

    #[test]
    fn test_loading_initial_renders_spinner() {
        with_frame!(80, 24, |frame| {
            render_loading(
                &mut frame,
                Rect::new(0, 0, 80, 24),
                &LoadState::LoadingInitial,
                white(),
                gray(),
                0,
            );

            let center_y = 12u16;
            let has_content = (0..80u16).any(|x| {
                let cell = frame.buffer.get(x, center_y).unwrap();
                !cell.is_empty()
            });
            assert!(has_content, "Expected loading spinner at center row");
        });
    }

    #[test]
    fn test_loading_refreshing_renders_corner() {
        with_frame!(80, 24, |frame| {
            render_loading(
                &mut frame,
                Rect::new(0, 0, 80, 24),
                &LoadState::Refreshing,
                white(),
                gray(),
                0,
            );

            let cell = frame.buffer.get(78, 0).unwrap();
            assert!(!cell.is_empty(), "Expected corner spinner");
        });
    }

    #[test]
    fn test_loading_idle_noop() {
        with_frame!(80, 24, |frame| {
            render_loading(
                &mut frame,
                Rect::new(0, 0, 80, 24),
                &LoadState::Idle,
                white(),
                gray(),
                0,
            );

            let has_content = (0..80u16).any(|x| {
                (0..24u16).any(|y| {
                    let cell = frame.buffer.get(x, y).unwrap();
                    !cell.is_empty()
                })
            });
            assert!(!has_content, "Idle state should render nothing");
        });
    }

    #[test]
    fn test_spinner_animation_cycles() {
        let frame0 = spinner_char(0);
        let frame1 = spinner_char(1);
        let frame_wrap = spinner_char(SPINNER_FRAMES.len() as u64);

        assert_ne!(frame0, frame1, "Adjacent frames should differ");
        assert_eq!(frame0, frame_wrap, "Should wrap around");
    }
}
