/// Token filters transform, remove, or add tokens after tokenization.
///
/// Filters operate on the full token list rather than streaming, which
/// simplifies filters that need to remove tokens (like `stop`) or inspect
/// context (like `shingle`).
///
/// See [[analyzers#Token Filters]].

use std::collections::HashSet;

use crate::token::Token;

/// Transforms tokens in the analysis pipeline.
///
/// Implementations must be thread-safe (`Send + Sync`) so that analyzers
/// can be shared across indexing threads.
pub trait TokenFilter: Send + Sync {
    /// Apply the filter to `tokens`, modifying in place.
    fn apply(&self, tokens: &mut Vec<Token>);
}

/// Lowercases all token text.
///
/// Uses Unicode-aware lowercasing (`str::to_lowercase`). This is the most
/// common token filter — used by `standard`, `simple`, `stop`, and `pattern`
/// analyzers.
///
/// See [[analyzers#Token Filters]].
pub struct LowercaseFilter;

impl TokenFilter for LowercaseFilter {
    fn apply(&self, tokens: &mut Vec<Token>) {
        for token in tokens.iter_mut() {
            // Only allocate if the text actually changes.
            let lowered = token.text.to_lowercase();
            if lowered != token.text {
                token.text = lowered;
            }
        }
    }
}

/// Removes stop words from the token stream.
///
/// Stop words are common words (like "the", "is", "at") that carry little
/// meaning for search. Removing them reduces index size and can improve
/// precision. Positions are preserved so phrase queries still work correctly.
///
/// See [[analyzers#Token Filters]].
pub struct StopFilter {
    stop_words: HashSet<String>,
}

impl StopFilter {
    /// Create a stop filter with a custom set of stop words.
    pub fn new(words: impl IntoIterator<Item = impl Into<String>>) -> Self {
        Self {
            stop_words: words.into_iter().map(Into::into).collect(),
        }
    }

    /// Create a stop filter with the default English stop words.
    ///
    /// Uses the same list as Lucene's `EnglishAnalyzer`.
    pub fn english() -> Self {
        Self::new(ENGLISH_STOP_WORDS.iter().copied())
    }
}

impl TokenFilter for StopFilter {
    fn apply(&self, tokens: &mut Vec<Token>) {
        tokens.retain(|token| !self.stop_words.contains(&token.text));
    }
}

/// Default English stop words, matching Lucene's `EnglishAnalyzer`.
const ENGLISH_STOP_WORDS: &[&str] = &[
    "a", "an", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in",
    "into", "is", "it", "no", "not", "of", "on", "or", "such", "that", "the",
    "their", "then", "there", "these", "they", "this", "to", "was", "will",
    "with",
];

/// Reduces tokens to their word stems using the Snowball algorithm.
///
/// Stemming maps morphological variants to a common root (e.g., "running" →
/// "run", "cats" → "cat"). This improves recall at the cost of some precision.
///
/// Supports 17+ languages via the Snowball stemming algorithms.
///
/// See [[analyzers#Token Filters]] and
/// [Snowball](https://snowballstem.org/algorithms/).
pub struct StemmerFilter {
    algorithm: rust_stemmers::Algorithm,
}

impl StemmerFilter {
    /// Create a stemmer filter for the given language.
    pub fn new(algorithm: rust_stemmers::Algorithm) -> Self {
        Self { algorithm }
    }

    /// Create an English stemmer filter.
    pub fn english() -> Self {
        Self::new(rust_stemmers::Algorithm::English)
    }
}

impl TokenFilter for StemmerFilter {
    fn apply(&self, tokens: &mut Vec<Token>) {
        let stemmer = rust_stemmers::Stemmer::create(self.algorithm);
        for token in tokens.iter_mut() {
            let stemmed = stemmer.stem(&token.text);
            if stemmed != token.text {
                token.text = stemmed.into_owned();
            }
        }
    }
}

/// Re-export the Algorithm enum so callers don't need to depend on
/// `rust-stemmers` directly.
pub use rust_stemmers::Algorithm as StemmerAlgorithm;

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

    fn make_tokens(words: &[&str]) -> Vec<Token> {
        words
            .iter()
            .enumerate()
            .map(|(i, w)| Token::new(*w, 0, w.len(), i as u32))
            .collect()
    }

    // --- LowercaseFilter ---

    #[test]
    fn lowercase_basic() {
        let mut tokens = make_tokens(&["Hello", "WORLD", "TeSt"]);
        LowercaseFilter.apply(&mut tokens);
        assert_eq!(tokens[0].text, "hello");
        assert_eq!(tokens[1].text, "world");
        assert_eq!(tokens[2].text, "test");
    }

    #[test]
    fn lowercase_already_lower() {
        let mut tokens = make_tokens(&["hello", "world"]);
        LowercaseFilter.apply(&mut tokens);
        assert_eq!(tokens[0].text, "hello");
        assert_eq!(tokens[1].text, "world");
    }

    #[test]
    fn lowercase_unicode() {
        let mut tokens = make_tokens(&["CAFÉ", "Ñoño"]);
        LowercaseFilter.apply(&mut tokens);
        assert_eq!(tokens[0].text, "café");
        assert_eq!(tokens[1].text, "ñoño");
    }

    #[test]
    fn lowercase_preserves_positions() {
        let mut tokens = make_tokens(&["A", "B", "C"]);
        LowercaseFilter.apply(&mut tokens);
        assert_eq!(tokens[0].position, 0);
        assert_eq!(tokens[1].position, 1);
        assert_eq!(tokens[2].position, 2);
    }

    #[test]
    fn lowercase_empty() {
        let mut tokens: Vec<Token> = Vec::new();
        LowercaseFilter.apply(&mut tokens);
        assert!(tokens.is_empty());
    }

    // --- StopFilter ---

    #[test]
    fn stop_removes_stop_words() {
        let mut tokens = make_tokens(&["the", "quick", "brown", "fox"]);
        StopFilter::english().apply(&mut tokens);
        let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
        assert_eq!(texts, vec!["quick", "brown", "fox"]);
    }

    #[test]
    fn stop_preserves_positions() {
        let mut tokens = make_tokens(&["the", "quick", "brown", "fox"]);
        StopFilter::english().apply(&mut tokens);
        // Positions from the original tokenization are preserved.
        assert_eq!(tokens[0].position, 1); // "quick" was at position 1
        assert_eq!(tokens[1].position, 2); // "brown" was at position 2
    }

    #[test]
    fn stop_all_removed() {
        let mut tokens = make_tokens(&["the", "a", "is", "it"]);
        StopFilter::english().apply(&mut tokens);
        assert!(tokens.is_empty());
    }

    #[test]
    fn stop_none_removed() {
        let mut tokens = make_tokens(&["quick", "brown", "fox"]);
        StopFilter::english().apply(&mut tokens);
        assert_eq!(tokens.len(), 3);
    }

    #[test]
    fn stop_custom_words() {
        let mut tokens = make_tokens(&["hello", "world", "goodbye"]);
        let filter = StopFilter::new(["hello", "goodbye"]);
        filter.apply(&mut tokens);
        assert_eq!(tokens.len(), 1);
        assert_eq!(tokens[0].text, "world");
    }

    #[test]
    fn stop_case_sensitive() {
        let mut tokens = make_tokens(&["The", "quick"]);
        StopFilter::english().apply(&mut tokens);
        // "The" (uppercase) is NOT in the stop word set — stop filter
        // should be applied after lowercase filter.
        assert_eq!(tokens.len(), 2);
    }

    // --- StemmerFilter ---

    #[test]
    fn stemmer_english_basic() {
        let mut tokens = make_tokens(&["running", "cats", "easily"]);
        StemmerFilter::english().apply(&mut tokens);
        assert_eq!(tokens[0].text, "run");
        assert_eq!(tokens[1].text, "cat");
        assert_eq!(tokens[2].text, "easili");
    }

    #[test]
    fn stemmer_already_stemmed() {
        let mut tokens = make_tokens(&["run", "cat"]);
        StemmerFilter::english().apply(&mut tokens);
        assert_eq!(tokens[0].text, "run");
        assert_eq!(tokens[1].text, "cat");
    }

    #[test]
    fn stemmer_preserves_positions() {
        let mut tokens = make_tokens(&["running", "jumping"]);
        StemmerFilter::english().apply(&mut tokens);
        assert_eq!(tokens[0].position, 0);
        assert_eq!(tokens[1].position, 1);
    }

    #[test]
    fn stemmer_empty() {
        let mut tokens: Vec<Token> = Vec::new();
        StemmerFilter::english().apply(&mut tokens);
        assert!(tokens.is_empty());
    }
}
