//! Deterministic fuzzy matching over the shared desktop action registry.

use crate::action::{self, UiAction};

#[derive(Debug, Default)]
pub struct CommandPalette {
    open: bool,
    query: String,
    selected: usize,
}

impl CommandPalette {
    pub fn is_open(&self) -> bool {
        self.open
    }

    pub fn query(&self) -> &str {
        &self.query
    }

    pub fn selected(&self) -> usize {
        self.selected
    }

    pub fn open(&mut self) {
        self.open = true;
        self.query.clear();
        self.selected = 0;
    }

    pub fn close(&mut self) {
        self.open = false;
        self.query.clear();
        self.selected = 0;
    }

    pub fn update_query(&mut self, query: String) {
        self.open = true;
        self.query = query;
        self.selected = 0;
    }

    pub fn move_selection(&mut self, forward: bool, result_count: usize) {
        if result_count == 0 {
            self.selected = 0;
        } else if forward {
            self.selected = (self.selected + 1) % result_count;
        } else {
            self.selected = self.selected.checked_sub(1).unwrap_or(result_count - 1);
        }
    }

    pub fn select_first(&mut self) {
        self.selected = 0;
    }

    pub fn select_last(&mut self, result_count: usize) {
        self.selected = result_count.saturating_sub(1);
    }

    pub fn results(&self) -> Vec<UiAction> {
        matches(&self.query)
    }

    pub fn selected_action(&self) -> Option<UiAction> {
        self.results().get(self.selected).copied()
    }
}

pub fn matches(query: &str) -> Vec<UiAction> {
    let query = normalize(query);
    let mut matches = action::ACTIONS
        .iter()
        .enumerate()
        .filter(|(_, spec)| spec.action != UiAction::CommandPalette)
        .filter_map(|(order, spec)| {
            std::iter::once(spec.label)
                .chain(std::iter::once(spec.action.id()))
                .chain(action::aliases(spec.action).iter().copied())
                .filter_map(|candidate| fuzzy_score(&query, &normalize(candidate)))
                .min()
                .map(|score| (score, order, spec.action))
        })
        .collect::<Vec<_>>();
    matches.sort_by_key(|(score, order, _)| (*score, *order));
    matches.into_iter().map(|(_, _, action)| action).collect()
}

fn normalize(value: &str) -> String {
    value
        .chars()
        .flat_map(char::to_lowercase)
        .filter(|character| character.is_alphanumeric())
        .collect()
}

fn fuzzy_score(query: &str, candidate: &str) -> Option<usize> {
    if query.is_empty() {
        return Some(0);
    }
    if query == candidate {
        return Some(0);
    }
    if candidate.starts_with(query) {
        return Some(10 + candidate.len().saturating_sub(query.len()));
    }
    if let Some(index) = candidate.find(query) {
        return Some(30 + index + candidate.len().saturating_sub(query.len()));
    }
    if let Some(gaps) = subsequence_gaps(query, candidate) {
        return Some(60 + gaps);
    }
    let distance = edit_distance(query, candidate);
    let tolerance = query.chars().count().div_ceil(3).clamp(1, 3);
    (distance <= tolerance).then_some(100 + distance * 10 + candidate.len())
}

fn subsequence_gaps(query: &str, candidate: &str) -> Option<usize> {
    let mut positions = candidate.char_indices();
    let mut previous = None;
    let mut gaps = 0;
    for expected in query.chars() {
        let (position, _) = positions.find(|(_, actual)| *actual == expected)?;
        if let Some(previous) = previous {
            gaps += position.saturating_sub(previous + 1);
        }
        previous = Some(position);
    }
    Some(gaps + candidate.len().saturating_sub(query.len()))
}

fn edit_distance(left: &str, right: &str) -> usize {
    let right = right.chars().collect::<Vec<_>>();
    let mut previous = (0..=right.len()).collect::<Vec<_>>();
    for (left_index, left_character) in left.chars().enumerate() {
        let mut current = Vec::with_capacity(right.len() + 1);
        current.push(left_index + 1);
        for (right_index, right_character) in right.iter().enumerate() {
            current.push(
                (previous[right_index + 1] + 1)
                    .min(current[right_index] + 1)
                    .min(previous[right_index] + usize::from(left_character != *right_character)),
            );
        }
        previous = current;
    }
    previous[right.len()]
}

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

    #[test]
    fn matching_handles_misspellings_subsequences_aliases_and_stable_order() {
        assert_eq!(matches("setings").first(), Some(&UiAction::Settings));
        assert_eq!(matches("rfsh").first(), Some(&UiAction::Refresh));
        assert_eq!(matches("open vault").first(), Some(&UiAction::OpenFolder));
        assert_eq!(
            matches("pass init").first(),
            Some(&UiAction::InitializeStore)
        );
        assert_eq!(matches("create folder").first(), Some(&UiAction::NewFolder));
        assert_eq!(matches("insert").first(), Some(&UiAction::NewEntry));
        assert_eq!(
            matches("generate").first(),
            Some(&UiAction::GeneratePassword)
        );
        assert_eq!(matches("grep").first(), Some(&UiAction::SearchContents));
        assert_eq!(matches("rename").first(), Some(&UiAction::MoveEntry));
        assert_eq!(
            matches("duplicate entry").first(),
            Some(&UiAction::CopyEntry)
        );
        assert_eq!(matches("rm").first(), Some(&UiAction::DeleteEntry));
        assert_eq!(
            matches(""),
            action::ACTIONS
                .iter()
                .filter_map(|spec| {
                    (spec.action != UiAction::CommandPalette).then_some(spec.action)
                })
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn selection_wraps_and_query_changes_reset_it() {
        let mut palette = CommandPalette::default();
        palette.open();
        palette.move_selection(false, 3);
        assert_eq!(palette.selected(), 2);
        palette.move_selection(true, 3);
        assert_eq!(palette.selected(), 0);
        palette.select_last(3);
        assert_eq!(palette.selected(), 2);
        palette.select_first();
        assert_eq!(palette.selected(), 0);
        palette.update_query("lock".to_owned());
        assert_eq!(palette.selected(), 0);
        assert_eq!(palette.selected_action(), Some(UiAction::Lock));
    }
}
