//! The fuzzy matching algorithm used in clangd.
//! https://github.com/llvm-mirror/clang-tools-extra/blob/master/clangd/FuzzyMatch.cpp
//!
//! # Example:
//! ```
//! use skim::fuzzy_matcher::FuzzyMatcher;
//! use skim::fuzzy_matcher::clangd::ClangdMatcher;
//!
//! let matcher = ClangdMatcher::default();
//!
//! assert_eq!(None, matcher.fuzzy_match("abc", "abx"));
//! assert!(matcher.fuzzy_match("axbycz", "abc").is_some());
//! assert!(matcher.fuzzy_match("axbycz", "xyz").is_some());
//!
//! let (score, indices) = matcher.fuzzy_indices("axbycz", "abc").unwrap();
//! assert_eq!(indices, [0, 2, 4]);
//!
//! ```
//!
//! Algorithm modified from
//! https://github.com/llvm-mirror/clang-tools-extra/blob/master/clangd/FuzzyMatch.cpp
//! Also check: https://github.com/lewang/flx/issues/98
use crate::fuzzy_matcher::util::{CharRole, CharType, char_equal, char_role, char_type_of, cheap_matches};
use crate::fuzzy_matcher::{FuzzyMatcher, IndexType, MatchIndices, ScoreType};
use std::cell::RefCell;
use std::cmp::max;
use thread_local::ThreadLocal;

#[derive(Eq, PartialEq, Debug, Copy, Clone)]
enum CaseMatching {
    Respect,
    Ignore,
    Smart,
}

#[derive(Debug)]
/// Fuzzy matcher using the clangd algorithm
pub struct ClangdMatcher {
    case: CaseMatching,

    use_cache: bool,

    c_cache: ThreadLocal<RefCell<Vec<char>>>, // vector to store the characters of choice
    p_cache: ThreadLocal<RefCell<Vec<char>>>, // vector to store the characters of pattern
}

impl Default for ClangdMatcher {
    fn default() -> Self {
        Self {
            case: CaseMatching::Ignore,
            use_cache: true,
            c_cache: ThreadLocal::new(),
            p_cache: ThreadLocal::new(),
        }
    }
}

impl ClangdMatcher {
    /// Sets the matcher to ignore case when matching
    #[must_use]
    pub fn ignore_case(mut self) -> Self {
        self.case = CaseMatching::Ignore;
        self
    }

    /// Sets the matcher to use smart case (case insensitive unless pattern contains uppercase)
    #[must_use]
    pub fn smart_case(mut self) -> Self {
        self.case = CaseMatching::Smart;
        self
    }

    /// Sets the matcher to respect case when matching
    #[must_use]
    pub fn respect_case(mut self) -> Self {
        self.case = CaseMatching::Respect;
        self
    }

    /// Enables or disables caching for improved performance
    #[must_use]
    pub fn use_cache(mut self, use_cache: bool) -> Self {
        self.use_cache = use_cache;
        self
    }

    fn contains_upper(string: &str) -> bool {
        for ch in string.chars() {
            if ch.is_uppercase() {
                return true;
            }
        }

        false
    }

    fn is_case_sensitive(&self, pattern: &str) -> bool {
        match self.case {
            CaseMatching::Respect => true,
            CaseMatching::Ignore => false,
            CaseMatching::Smart => Self::contains_upper(pattern),
        }
    }
}

impl FuzzyMatcher for ClangdMatcher {
    fn fuzzy_indices(&self, choice: &str, pattern: &str) -> Option<(ScoreType, MatchIndices)> {
        let case_sensitive = self.is_case_sensitive(pattern);

        let mut choice_chars = self.c_cache.get_or(|| RefCell::new(Vec::new())).borrow_mut();
        let mut pattern_chars = self.p_cache.get_or(|| RefCell::new(Vec::new())).borrow_mut();

        choice_chars.clear();
        for char in choice.chars() {
            choice_chars.push(char);
        }

        pattern_chars.clear();
        for char in pattern.chars() {
            pattern_chars.push(char);
        }

        cheap_matches(&choice_chars, &pattern_chars, case_sensitive)?;

        let num_pattern_chars = pattern_chars.len();
        let num_choice_chars = choice_chars.len();

        let dp = build_graph(&choice_chars, &pattern_chars, false, case_sensitive);

        // search backwards for the matched indices
        let mut indices_reverse = Vec::with_capacity(num_pattern_chars);
        let cell = dp[num_pattern_chars][num_choice_chars];

        let (mut last_action, score) = if cell.hit > cell.missed {
            (Action::Match, cell.hit)
        } else {
            (Action::Miss, cell.missed)
        };

        let mut row = num_pattern_chars;
        let mut col = num_choice_chars;

        while col > 0 {
            let cell = &dp[row][col];
            if last_action == Action::Match {
                if row == 0 {
                    debug_assert!(false, "clangd backtracking hit a match with no pattern left");
                    break;
                }
                indices_reverse.push((col - 1) as IndexType);
                last_action = cell.last_action_match;
                row -= 1;
                col -= 1;
            } else {
                last_action = cell.last_action_miss;
                col -= 1;
            }
        }

        // Release the cache borrows before (optionally) freeing their backing
        // storage below; `replace` would otherwise re-borrow the same RefCells.
        drop(choice_chars);
        drop(pattern_chars);
        if !self.use_cache {
            // drop the allocated memory
            self.c_cache.get().map(|cell| cell.replace(vec![]));
            self.p_cache.get().map(|cell| cell.replace(vec![]));
        }

        indices_reverse.reverse();
        Some((
            adjust_score(score, num_choice_chars),
            MatchIndices::from(indices_reverse),
        ))
    }

    fn fuzzy_match(&self, choice: &str, pattern: &str) -> Option<ScoreType> {
        let case_sensitive = self.is_case_sensitive(pattern);

        let mut choice_chars = self.c_cache.get_or(|| RefCell::new(Vec::new())).borrow_mut();
        let mut pattern_chars = self.p_cache.get_or(|| RefCell::new(Vec::new())).borrow_mut();

        choice_chars.clear();
        for char in choice.chars() {
            choice_chars.push(char);
        }

        pattern_chars.clear();
        for char in pattern.chars() {
            pattern_chars.push(char);
        }

        cheap_matches(&choice_chars, &pattern_chars, case_sensitive)?;

        let num_pattern_chars = pattern_chars.len();
        let num_choice_chars = choice_chars.len();

        let dp = build_graph(&choice_chars, &pattern_chars, true, case_sensitive);

        let cell = dp[num_pattern_chars & 1][num_choice_chars];
        let score = max(cell.hit, cell.missed);

        // Release the cache borrows before (optionally) freeing their backing
        // storage below; `replace` would otherwise re-borrow the same RefCells.
        drop(choice_chars);
        drop(pattern_chars);
        if !self.use_cache {
            // drop the allocated memory
            self.c_cache.get().map(|cell| cell.replace(vec![]));
            self.p_cache.get().map(|cell| cell.replace(vec![]));
        }

        Some(adjust_score(score, num_choice_chars))
    }
}

/// fuzzy match `line` with `pattern`, returning the score and indices of matches
#[must_use]
pub fn fuzzy_indices(line: &str, pattern: &str) -> Option<(ScoreType, MatchIndices)> {
    ClangdMatcher::default().ignore_case().fuzzy_indices(line, pattern)
}

/// fuzzy match `line` with `pattern`, returning the score(the larger the better) on match
#[must_use]
pub fn fuzzy_match(line: &str, pattern: &str) -> Option<ScoreType> {
    ClangdMatcher::default().ignore_case().fuzzy_match(line, pattern)
}

// checkout https://github.com/llvm-mirror/clang-tools-extra/blob/master/clangd/FuzzyMatch.cpp
// for the description
fn build_graph(line: &[char], pattern: &[char], compressed: bool, case_sensitive: bool) -> Vec<Vec<Score>> {
    let num_line_chars = line.len();
    let num_pattern_chars = pattern.len();
    let max_rows = if compressed { 2 } else { num_pattern_chars + 1 };

    let mut dp: Vec<Vec<Score>> = Vec::with_capacity(max_rows);

    for _ in 0..max_rows {
        dp.push(vec![Score::default(); num_line_chars + 1]);
    }

    dp[0][0].missed = 0;

    // first line
    for (idx, &ch) in line.iter().enumerate() {
        dp[0][idx + 1] = Score {
            missed: dp[0][idx].missed - skip_penalty(idx, ch, Action::Miss),
            last_action_miss: Action::Miss,
            hit: AWFUL_SCORE,
            last_action_match: Action::Miss,
        };
    }

    // build the matrix
    let mut pat_prev_ch = '\0';
    for (pat_idx, &pat_ch) in pattern.iter().enumerate() {
        let current_row_idx = if compressed { (pat_idx + 1) & 1 } else { pat_idx + 1 };
        let prev_row_idx = if compressed { pat_idx & 1 } else { pat_idx };

        let mut line_prev_ch = '\0';
        for (line_idx, &line_ch) in line.iter().enumerate() {
            if line_idx < pat_idx {
                line_prev_ch = line_ch;
                continue;
            }

            // what if we skip current line character?
            // we need to calculate the cases where the pre line character is matched/missed
            let pre_miss = &dp[current_row_idx][line_idx];
            let mut match_missed = pre_miss.hit;
            let mut miss_missed = pre_miss.missed;
            if pat_idx < num_pattern_chars - 1 {
                match_missed -= skip_penalty(line_idx, line_ch, Action::Match);
                miss_missed -= skip_penalty(line_idx, line_ch, Action::Miss);
            }

            let (missed, last_action_miss) = if match_missed > miss_missed {
                (match_missed, Action::Match)
            } else {
                (miss_missed, Action::Miss)
            };

            // what if we want to match current line character?
            // so we need to calculate the cases where the pre pattern character is matched/missed
            let pre_match = &dp[prev_row_idx][line_idx];
            let match_hit = if allow_match(pat_ch, line_ch, case_sensitive) {
                pre_match.hit
                    + match_bonus(
                        pat_idx,
                        pat_ch,
                        pat_prev_ch,
                        line_idx,
                        line_ch,
                        line_prev_ch,
                        Action::Match,
                    )
            } else {
                AWFUL_SCORE
            };

            let miss_hit = if allow_match(pat_ch, line_ch, case_sensitive) {
                pre_match.missed
                    + match_bonus(
                        pat_idx,
                        pat_ch,
                        pat_prev_ch,
                        line_idx,
                        line_ch,
                        line_prev_ch,
                        Action::Match,
                    )
            } else {
                AWFUL_SCORE
            };

            let (hit, last_action_match) = if match_hit > miss_hit {
                (match_hit, Action::Match)
            } else {
                (miss_hit, Action::Miss)
            };

            dp[current_row_idx][line_idx + 1] = Score {
                last_action_miss,
                last_action_match,
                missed,
                hit,
            };

            line_prev_ch = line_ch;
        }

        pat_prev_ch = pat_ch;
    }

    dp
}

fn adjust_score(score: ScoreType, num_line_chars: usize) -> ScoreType {
    // line width will affect 10 scores
    // Cast through u32 to avoid precision-loss lint (character counts fit in u32).
    let width = f64::from(u32::try_from(num_line_chars + 1).unwrap_or(u32::MAX));
    let adjustment = width.ln().floor();
    // SAFETY: adjustment = floor(ln(usize + 1)) ≤ floor(ln(u32::MAX + 1)) ≈ 22, always fits in i64.
    #[allow(clippy::cast_possible_truncation)]
    let adjustment_i = adjustment as ScoreType;
    score - adjustment_i
}

const AWFUL_SCORE: ScoreType = -(1 << 30);

#[derive(Debug, PartialEq, Clone, Copy)]
enum Action {
    Miss,
    Match,
}

#[derive(Debug, Clone, Copy)]
struct Score {
    pub last_action_miss: Action,
    pub last_action_match: Action,
    pub missed: ScoreType,
    pub hit: ScoreType,
}

impl Default for Score {
    fn default() -> Self {
        Self {
            last_action_miss: Action::Miss,
            last_action_match: Action::Miss,
            missed: AWFUL_SCORE,
            hit: AWFUL_SCORE,
        }
    }
}

fn skip_penalty(_ch_idx: usize, ch: char, last_action: Action) -> ScoreType {
    let mut score = 1;
    if last_action == Action::Match {
        // Non-consecutive match.
        score += 3;
    }

    if char_type_of(ch) == CharType::NonWord {
        // skip separator
        score += 6;
    }

    score
}

fn allow_match(pat_ch: char, line_ch: char, case_sensitive: bool) -> bool {
    char_equal(pat_ch, line_ch, case_sensitive)
}

fn match_bonus(
    pat_idx: usize,
    pat_ch: char,
    pat_prev_ch: char,
    line_idx: usize,
    line_ch: char,
    line_prev_ch: char,
    last_action: Action,
) -> ScoreType {
    let mut score = 10;
    let pat_role = char_role(pat_prev_ch, pat_ch);
    let line_role = char_role(line_prev_ch, line_ch);

    // Bonus: pattern so far is a (case-insensitive) prefix of the word.
    if pat_idx == line_idx {
        score += 10;
    }

    // Bonus: case match
    if pat_ch == line_ch {
        score += 8;
    }

    // Bonus: match header
    if line_role == CharRole::Head {
        score += 9;
    }

    // Bonus: a Head in the pattern aligns with one in the word.
    if pat_role == CharRole::Head && line_role == CharRole::Head {
        score += 10;
    }

    // Penalty: matching inside a segment (and previous char wasn't matched).
    if line_role == CharRole::Tail && pat_idx > 0 && last_action == Action::Miss {
        score -= 30;
    }

    // Penalty: a Head in the pattern matches in the middle of a word segment.
    if pat_role == CharRole::Head && line_role == CharRole::Tail {
        score -= 10;
    }

    // Penalty: matching the first pattern character in the middle of a segment.
    if pat_idx == 0 && line_role == CharRole::Tail {
        score -= 40;
    }

    score
}

#[allow(dead_code)]
fn print_dp(line: &str, pattern: &str, dp: &[Vec<Score>]) {
    let num_line_chars = line.chars().count();
    let num_pattern_chars = pattern.chars().count();

    print!("\t");
    for (idx, ch) in line.chars().enumerate() {
        print!("\t\t{}/{}", idx + 1, ch);
    }

    for (row_num, row) in dp.iter().enumerate().take(num_pattern_chars + 1) {
        print!("\n{row_num}\t");
        for cell in row.iter().take(num_line_chars + 1) {
            print!(
                "({},{})/({},{})\t",
                cell.missed,
                if cell.last_action_miss == Action::Miss {
                    'X'
                } else {
                    'O'
                },
                cell.hit,
                if cell.last_action_match == Action::Miss {
                    'X'
                } else {
                    'O'
                }
            );
        }
    }
}

#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod tests {
    use super::*;
    use crate::fuzzy_matcher::util::{assert_order, wrap_matches};

    fn wrap_fuzzy_match(line: &str, pattern: &str) -> Option<String> {
        let (_score, indices) = fuzzy_indices(line, pattern)?;
        Some(wrap_matches(line, &indices))
    }

    #[test]
    fn test_match_or_not() {
        assert_eq!(None, fuzzy_match("abcdefaghi", "中"));
        assert_eq!(None, fuzzy_match("abc", "abx"));
        assert!(fuzzy_match("axbycz", "abc").is_some());
        assert!(fuzzy_match("axbycz", "xyz").is_some());

        assert_eq!("[a]x[b]y[c]z", &wrap_fuzzy_match("axbycz", "abc").unwrap());
        assert_eq!("a[x]b[y]c[z]", &wrap_fuzzy_match("axbycz", "xyz").unwrap());
        assert_eq!("[H]ello, [世]界", &wrap_fuzzy_match("Hello, 世界", "H世").unwrap());
    }

    #[test]
    fn test_match_quality() {
        let matcher = ClangdMatcher::default();
        // case
        assert_order(&matcher, "monad", &["monad", "Monad", "mONAD"]);

        // initials
        assert_order(&matcher, "ab", &["ab", "aoo_boo", "acb"]);
        assert_order(&matcher, "CC", &["CamelCase", "camelCase", "camelcase"]);
        assert_order(&matcher, "cC", &["camelCase", "CamelCase", "camelcase"]);
        assert_order(
            &matcher,
            "cc",
            &["camel case", "camelCase", "camelcase", "CamelCase", "camel ace"],
        );
        assert_order(
            &matcher,
            "Da.Te",
            &["Data.Text", "Data.Text.Lazy", "Data.Aeson.Encoding.text"],
        );
        assert_order(&matcher, "foobar.h", &["foobar.h", "foo/bar.h"]);
        // prefix
        assert_order(&matcher, "is", &["isIEEE", "inSuf"]);
        // shorter
        assert_order(&matcher, "ma", &["map", "many", "maximum"]);
        assert_order(&matcher, "print", &["printf", "sprintf"]);
        // score(PRINT) = kMinScore
        assert_order(&matcher, "ast", &["ast", "AST", "INT_FAST16_MAX"]);
        // score(PRINT) > kMinScore
        assert_order(&matcher, "Int", &["int", "INT", "PRINT"]);
    }

    #[test]
    fn respect_case_is_sensitive() {
        let matcher = ClangdMatcher::default().respect_case();
        assert!(matcher.fuzzy_match("Foo", "Fo").is_some());
        assert!(matcher.fuzzy_match("foo", "Fo").is_none());
    }

    #[test]
    fn ignore_case_is_insensitive() {
        let matcher = ClangdMatcher::default().ignore_case();
        assert!(matcher.fuzzy_match("FOO", "foo").is_some());
        assert!(matcher.fuzzy_match("foo", "FOO").is_some());
    }

    #[test]
    fn smart_case_uppercase_pattern_is_sensitive() {
        let matcher = ClangdMatcher::default().smart_case();
        // Uppercase in the pattern makes matching case-sensitive.
        assert!(matcher.fuzzy_match("Foo", "Fo").is_some());
        assert!(matcher.fuzzy_match("foo", "Fo").is_none());
        // All-lowercase pattern matches case-insensitively.
        assert!(matcher.fuzzy_match("FOO", "fo").is_some());
    }

    #[test]
    fn use_cache_toggle_is_chainable() {
        // Enabling the cache explicitly still produces matches.
        let matcher = ClangdMatcher::default().use_cache(true);
        assert!(matcher.fuzzy_indices("foobar", "fb").is_some());
    }

    #[test]
    fn use_cache_disabled_drops_buffers() {
        // With the cache disabled, both fuzzy_indices and fuzzy_match free their
        // scratch buffers (the `!use_cache` arms) yet still return correct
        // results, including on a repeated call after the drop.
        let matcher = ClangdMatcher::default().ignore_case().use_cache(false);
        let (_score, indices) = matcher.fuzzy_indices("axbycz", "abc").unwrap();
        assert_eq!(indices, vec![0, 2, 4]);
        assert!(matcher.fuzzy_match("axbycz", "abc").is_some());
        // A second call still works after the first dropped the buffers.
        assert!(matcher.fuzzy_indices("axbycz", "xyz").is_some());
    }

    #[test]
    fn fuzzy_match_range_spans_match() {
        let matcher = ClangdMatcher::default().ignore_case();
        let (_score, begin, end) = matcher.fuzzy_match_range("foobar", "fb").unwrap();
        assert!(begin <= end);
        assert!(matcher.fuzzy_match_range("foobar", "zzz").is_none());
    }

    #[test]
    fn match_bonus_in_segment_after_miss_penalty() {
        // The DP only ever calls match_bonus with `Action::Match`, so the
        // mid-segment-after-a-miss penalty (line_role == Tail, pat_idx > 0,
        // last_action == Miss) is unreachable through the public API. Exercise
        // it directly: matching 'b' in the middle of a segment ('a'→'b' is a
        // Tail) right after a skipped character costs 30 points.
        let prev = 'a'; // lowercase → 'b' is a Tail, not a Head
        let after_miss = match_bonus(1, 'b', prev, 1, 'b', prev, Action::Miss);
        let after_match = match_bonus(1, 'b', prev, 1, 'b', prev, Action::Match);
        assert_eq!(
            after_match - after_miss,
            30,
            "an in-segment match preceded by a miss must cost 30 points"
        );
    }
}
