spikes 0.2.0

Drop-in feedback collection for static HTML mockups
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

use crate::spike::{Rating, Spike};

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum InputMode {
    Normal,
    Filter,
}

pub struct App {
    pub spikes: Vec<Spike>,
    pub filtered: Vec<usize>,
    pub selected: usize,
    pub filter_text: String,
    pub filter_rating: Option<Rating>,
    pub show_detail: bool,
    pub should_quit: bool,
    pub input_mode: InputMode,
    pub table_offset: usize,
}

impl App {
    pub fn new(spikes: Vec<Spike>) -> Self {
        let filtered: Vec<usize> = (0..spikes.len()).collect();
        Self {
            spikes,
            filtered,
            selected: 0,
            filter_text: String::new(),
            filter_rating: None,
            show_detail: false,
            should_quit: false,
            input_mode: InputMode::Normal,
            table_offset: 0,
        }
    }

    pub fn handle_key(&mut self, key: KeyEvent) {
        match self.input_mode {
            InputMode::Normal => self.handle_normal_key(key),
            InputMode::Filter => self.handle_filter_key(key),
        }
    }

    fn handle_normal_key(&mut self, key: KeyEvent) {
        match key.code {
            KeyCode::Char('q') | KeyCode::Esc => {
                self.should_quit = true;
            }
            KeyCode::Char('j') | KeyCode::Down => {
                self.move_down();
            }
            KeyCode::Char('k') | KeyCode::Up => {
                self.move_up();
            }
            KeyCode::Enter => {
                self.show_detail = !self.show_detail;
            }
            KeyCode::Char('/') => {
                self.input_mode = InputMode::Filter;
            }
            KeyCode::Char('1') => {
                self.set_rating_filter(Some(Rating::Love));
            }
            KeyCode::Char('2') => {
                self.set_rating_filter(Some(Rating::Like));
            }
            KeyCode::Char('3') => {
                self.set_rating_filter(Some(Rating::Meh));
            }
            KeyCode::Char('4') => {
                self.set_rating_filter(Some(Rating::No));
            }
            KeyCode::Char('0') => {
                self.set_rating_filter(None);
            }
            KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                self.should_quit = true;
            }
            KeyCode::Tab => {
                self.input_mode = InputMode::Filter;
            }
            KeyCode::Home | KeyCode::Char('g') => {
                self.selected = 0;
                self.table_offset = 0;
            }
            KeyCode::End | KeyCode::Char('G') => {
                if !self.filtered.is_empty() {
                    self.selected = self.filtered.len() - 1;
                }
            }
            KeyCode::PageDown => {
                self.page_down(10);
            }
            KeyCode::PageUp => {
                self.page_up(10);
            }
            _ => {}
        }
    }

    fn handle_filter_key(&mut self, key: KeyEvent) {
        match key.code {
            KeyCode::Esc | KeyCode::Tab => {
                self.input_mode = InputMode::Normal;
            }
            KeyCode::Enter => {
                self.input_mode = InputMode::Normal;
            }
            KeyCode::Backspace => {
                self.filter_text.pop();
                self.apply_filters();
            }
            KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
                self.should_quit = true;
            }
            KeyCode::Char(c) => {
                self.filter_text.push(c);
                self.apply_filters();
            }
            _ => {}
        }
    }

    fn move_down(&mut self) {
        if !self.filtered.is_empty() && self.selected < self.filtered.len() - 1 {
            self.selected += 1;
        }
    }

    fn move_up(&mut self) {
        if self.selected > 0 {
            self.selected -= 1;
        }
    }

    fn page_down(&mut self, amount: usize) {
        if !self.filtered.is_empty() {
            self.selected = (self.selected + amount).min(self.filtered.len() - 1);
        }
    }

    fn page_up(&mut self, amount: usize) {
        self.selected = self.selected.saturating_sub(amount);
    }

    fn set_rating_filter(&mut self, rating: Option<Rating>) {
        self.filter_rating = rating;
        self.apply_filters();
    }

    fn apply_filters(&mut self) {
        let filter_lower = self.filter_text.to_lowercase();

        self.filtered = self
            .spikes
            .iter()
            .enumerate()
            .filter(|(_, spike)| {
                if let Some(ref rating) = self.filter_rating {
                    if spike.rating.as_ref() != Some(rating) {
                        return false;
                    }
                }

                if !filter_lower.is_empty() {
                    let matches = spike.page.to_lowercase().contains(&filter_lower)
                        || spike.reviewer.name.to_lowercase().contains(&filter_lower)
                        || spike.comments.to_lowercase().contains(&filter_lower)
                        || spike.id.to_lowercase().contains(&filter_lower);
                    if !matches {
                        return false;
                    }
                }

                true
            })
            .map(|(i, _)| i)
            .collect();

        if self.selected >= self.filtered.len() {
            self.selected = self.filtered.len().saturating_sub(1);
        }
    }

    pub fn selected_spike(&self) -> Option<&Spike> {
        self.filtered
            .get(self.selected)
            .and_then(|&idx| self.spikes.get(idx))
    }

}