use core::{mem::swap, ops::Deref};

pub struct Spell(Vec<usize>, Vec<usize>);

impl Spell {
    pub const fn new() -> Self {
        Self(Vec::new(), Vec::new())
    }

    pub fn suggest<T: Deref<Target = str>>(
        &mut self,
        word: &str,
        dictionary: impl IntoIterator<Item = T>,
        maximum: usize,
    ) -> Vec<(T, usize)> {
        let dictionary = dictionary.into_iter();
        let mut results = Vec::with_capacity(dictionary.size_hint().0);
        for candidate in dictionary {
            let distance = self.distance(word.as_bytes(), candidate.as_bytes());
            if distance <= maximum {
                results.push((candidate, distance));
            }
        }
        results.sort_by_key(|&(_, distance)| distance);
        results
    }

    fn distance(&mut self, left: &[u8], right: &[u8]) -> usize {
        let left_count = left.len();
        let right_count = right.len();
        if left_count > right_count {
            return self.distance(right, left);
        }

        let Self(previous, current) = self;
        previous.clear();
        previous.resize(left_count + 1, 0);
        current.resize(left_count + 1, 0);

        for i in 1..=right_count {
            current[0] = i;
            for j in 1..=left_count {
                let left = char::from(left[j - 1]).to_ascii_lowercase();
                let right = char::from(right[i - 1]).to_ascii_lowercase();
                let insert = current[j - 1] + 1;
                let delete = previous[j] + 1;
                let replace = previous[j - 1] + if left == right { 0 } else { 1 };
                current[j] = insert.min(delete).min(replace);
            }
            swap(previous, current);
        }

        previous[left_count]
    }
}

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

    #[test]
    fn distance() {
        let mut spell = Spell::new();
        assert_eq!(spell.distance(b"boba", b"boba"), 0);
        assert_eq!(spell.distance(b"boba", b"bobo"), 1);
        assert_eq!(spell.distance(b"boba", b"bobba"), 1);
        assert_eq!(spell.distance(b"boba", b"boa"), 1);
        assert_eq!(spell.distance(b"boba", b"fett"), 4);
    }

    #[test]
    fn suggest() {
        let results = Spell::new().suggest(
            "poulaye",
            [
                "poullayye",
                "poupou",
                "pilaye",
                "poulah",
                "piulaye",
                "paliyoo",
                "vladimarre",
                "poulaye",
                "p",
                "poulay",
            ],
            2,
        );
        assert_eq!(
            results,
            vec![
                ("poulaye", 0),
                ("piulaye", 1),
                ("poulay", 1),
                ("poullayye", 2),
                ("pilaye", 2),
                ("poulah", 2)
            ]
        );
    }
}
