// std imports
use std::{cmp::Ordering, collections::HashSet, sync::Arc};

const MIN_RELEVANCE: f64 = 0.75;

#[derive(Debug, Clone, Default)]
pub struct Suggestions {
    candidates: Vec<(f64, Arc<str>)>,
    reg: HashSet<Arc<str>>,
}

impl Suggestions {
    pub fn new<T, I>(wanted: &str, variants: I) -> Self
    where
        T: Into<Arc<str>>,
        I: IntoIterator<Item = T>,
    {
        let mut candidates = Vec::<(f64, _)>::new();
        let mut reg = HashSet::new();

        for variant in variants {
            let variant = variant.into();
            if reg.contains(&*variant) {
                continue;
            }

            let relevance = strsim::jaro(wanted, &variant);

            if relevance > MIN_RELEVANCE {
                let item = variant;
                let candidate = (relevance, item.clone());
                let pos = candidates
                    .binary_search_by(|candidate| {
                        if candidate.0 < relevance {
                            Ordering::Greater
                        } else {
                            Ordering::Less
                        }
                    })
                    .unwrap_or_else(|e| e);
                candidates.insert(pos, candidate);
                reg.insert(item);
            }
        }

        Self { candidates, reg }
    }

    pub fn none() -> Self {
        Self::default()
    }

    pub fn is_empty(&self) -> bool {
        self.candidates.is_empty()
    }

    pub fn iter(&self) -> SuggestionsIter<'_> {
        SuggestionsIter {
            iter: self.candidates.iter(),
        }
    }

    pub fn merge(self, other: Self) -> Self {
        let mut a = self.candidates.into_iter();
        let mut reg = self.reg;

        let mut b = other.candidates.into_iter();
        let mut br = other.reg;
        let mut merged = Vec::with_capacity(a.len() + b.len());

        let mut i = a.next();
        let mut j = b.next();
        loop {
            let (val, left) = match (i.take(), j.take()) {
                (None, None) => break,
                (Some(x), None) => (x, true),
                (None, Some(y)) => (y, false),
                (Some(x), Some(y)) => {
                    if x >= y {
                        j = Some(y);
                        (x, true)
                    } else {
                        i = Some(x);
                        (y, false)
                    }
                }
            };

            if left {
                merged.push(val);
                i = a.next();
            } else {
                if !reg.contains(&val.1) {
                    reg.insert(br.take(&val.1).unwrap_or_else(|| val.1.clone()));
                    merged.push(val);
                }
                j = b.next();
            }
        }

        Self {
            candidates: merged,
            reg,
        }
    }
}

impl<'a> IntoIterator for &'a Suggestions {
    type Item = &'a str;
    type IntoIter = SuggestionsIter<'a>;

    fn into_iter(self) -> Self::IntoIter {
        SuggestionsIter {
            iter: self.candidates.iter(),
        }
    }
}

pub struct SuggestionsIter<'a> {
    iter: std::slice::Iter<'a, (f64, Arc<str>)>,
}

impl<'a> Iterator for SuggestionsIter<'a> {
    type Item = &'a str;

    fn next(&mut self) -> Option<Self::Item> {
        self.iter.next().map(|(_, candidate)| candidate.as_ref())
    }
}

#[cfg(test)]
mod tests;
