use crate::component::{Component, EventCx, MeasureCx};
use crate::event::Event;
use crate::geom::{Rect, Size};
use crate::layout::Constraint;
use crate::render::RenderCx;
use crate::style::Style;
use crate::text::Text;

const FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];

/// An animated spinner for loading states.
///
/// Renders a braille animation frame followed by a message. Advances one frame
/// on each [`Event::Tick`]. Use [`App::tick_rate`] to control animation speed.
pub struct Spinner {
    message: Text,
    frame: usize,
    style: Style,
}

impl Spinner {
    /// Creates a spinner with the given message.
    pub fn new(message: impl Into<Text>) -> Self {
        Self { message: message.into(), frame: 0, style: Style::default() }
    }

    /// Builder: sets the spinner style.
    pub fn style(mut self, style: Style) -> Self { self.style = style; self }
}

impl Component for Spinner {
    fn render(&self, cx: &mut RenderCx) {
        let ch = FRAMES[self.frame % FRAMES.len()];
        cx.set_style(self.style.clone());
        cx.line(&format!("{} {}", ch, self.message.first_text()));
    }

    fn measure(&self, _c: Constraint, _cx: &mut MeasureCx) -> Size {
        Size { width: self.message.max_width() + 2, height: 1 }
    }

    fn event(&mut self, event: &Event, cx: &mut EventCx) {
        if matches!(event, Event::Tick) {
            self.frame += 1;
            cx.invalidate_paint();
        }
    }

    fn layout(&mut self, _rect: Rect, _cx: &mut crate::component::LayoutCx) {}
    fn focusable(&self) -> bool { false }
    fn style(&self) -> Style { self.style.clone() }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testbuffer::TestBuffer;

    #[test]
    fn test_spinner_renders() {
        let mut tb = TestBuffer::new(20, 1);
        tb.render(&Spinner::new("loading"));
        assert!(tb.buffer.cells[0].symbol.chars().next().unwrap() != ' '); // frame char
    }
}
