extern crate sealed;
use self::sealed::sealed;
use std::collections::BinaryHeap;

use super::keychecker::KeyChecker;
use super::search_result::SearchResult;
use super::trie::Trie;
use super::trie::VisitOutcome;
use super::trienode::TrieKey;
use super::trienode::TrieNode;

pub type LevTrie<KEY, KEYCHECKER> = Trie<KEY, KEYCHECKER>;
pub type LevTrieNode<KEY> = TrieNode<KEY>;

#[sealed]
pub trait DistanceCalculator<KEY, KEYCHECKER>
where
    KEY: TrieKey,
    KEYCHECKER: KeyChecker<KEY>,
{
    fn compute(
        workspace: &mut Vec<Vec<usize>>,
        nodes: &[LevTrieNode<KEY>],
        stack: &[usize],
        wordkey: &[KEY],
        worst_case: Option<usize>,
    ) -> VisitOutcome;
}

impl<KEY, KEYCHECKER> LevTrie<KEY, KEYCHECKER>
where
    KEY: TrieKey,
    KEYCHECKER: KeyChecker<KEY>,
{
    pub fn new() -> LevTrie<KEY, KEYCHECKER> {
        Trie::default()
    }

    pub fn from_words<T, U>(wordlist: U) -> LevTrie<KEY, KEYCHECKER>
    where
        T: IntoIterator<Item = KEY>,
        U: IntoIterator<Item = T>,
    {
        let mut result = LevTrie::new();
        for word in wordlist {
            result.add(word);
        }
        result
    }

    pub fn fuzzy_search<DC>(&mut self, word: &[KEY], max_result: usize) -> Vec<SearchResult>
    where
        DC: DistanceCalculator<KEY, KEYCHECKER>,
    {
        let word_len = word.len();
        let required_size = word_len + 1;
        let workspace: &mut Vec<Vec<usize>> = &mut vec![Vec::new(); self.nodes()];
        let mut result_heap = BinaryHeap::<SearchResult>::with_capacity(max_result + 1);
        let visit_pre = |stack: &Vec<usize>| -> VisitOutcome {
            let stack_size = stack.len();
            let current_node_id = *stack.last().unwrap();
            let payload = &mut workspace[current_node_id];
            payload.resize(required_size, usize::default());
            if stack_size == 1 {
                for (i, item) in payload.iter_mut().enumerate() {
                    *item = i;
                }
            } else {
                for (i, item) in payload.iter_mut().enumerate() {
                    *item = if i == 0 { stack_size - 1 } else { 0 }
                }
            }
            if stack_size > 1 {
                let current_node = &mut self.get_node(current_node_id);
                if current_node.key.is_none() {
                    let distance = workspace[stack[stack_size - 2]][word_len];
                    let search_result = SearchResult {
                        distance,
                        word: current_node_id,
                    };
                    result_heap.push(search_result);
                    if result_heap.len() > max_result {
                        result_heap.pop();
                    }
                    VisitOutcome::Skip
                } else {
                    let worst_case = result_heap
                        .peek()
                        .filter(|_| result_heap.len() == max_result)
                        .map(|it| it.distance);
                    DC::compute(workspace, &self.nodes, stack, word, worst_case)
                }
            } else {
                VisitOutcome::Continue
            }
        };
        let visit_post = |_: &Vec<usize>| {};
        self.walk(visit_pre, visit_post);

        result_heap.into_sorted_vec()
    }
}

pub struct LevenshteinDistanceCalculator {}

#[sealed]
impl<KEY, KEYCHECKER> DistanceCalculator<KEY, KEYCHECKER> for LevenshteinDistanceCalculator
where
    KEY: TrieKey,
    KEYCHECKER: KeyChecker<KEY>,
{
    fn compute(
        workspace: &mut Vec<Vec<usize>>,
        nodes: &[LevTrieNode<KEY>],
        stack: &[usize],
        wordkey: &[KEY],
        worst_case: Option<usize>,
    ) -> VisitOutcome {
        let sz = stack.len();
        let key_size = wordkey.len();
        for i in 1..=key_size {
            if KEYCHECKER::check(Some(wordkey[i - 1]), nodes[stack[sz - 1]].key) {
                workspace[stack[sz - 1]][i] = workspace[stack[sz - 2]][i - 1];
            } else {
                workspace[stack[sz - 1]][i] = std::cmp::min(
                    std::cmp::min(
                        workspace[stack[sz - 1]][i - 1],
                        workspace[stack[sz - 2]][i - 1],
                    ),
                    workspace[stack[sz - 2]][i],
                ) + 1;
            }
        }
        let condition = worst_case
            .map(|wv| wv <= *workspace[stack[sz - 1]][..].iter().min().unwrap())
            .unwrap_or(false);
        if condition {
            VisitOutcome::Skip
        } else {
            VisitOutcome::Continue
        }
    }
}

pub struct DamerauLevenshteinDistanceCalculator {}

#[sealed]
impl<KEY, KEYCHECKER> DistanceCalculator<KEY, KEYCHECKER> for DamerauLevenshteinDistanceCalculator
where
    KEY: TrieKey,
    KEYCHECKER: KeyChecker<KEY>,
{
    fn compute(
        workspace: &mut Vec<Vec<usize>>,
        nodes: &[LevTrieNode<KEY>],
        stack: &[usize],
        wordkey: &[KEY],
        worst_case: Option<usize>,
    ) -> VisitOutcome {
        let sz = stack.len();
        let key_size = wordkey.len();
        for i in 1..=key_size {
            if KEYCHECKER::check(
                Some(wordkey[i - 1]),
                stack.last().and_then(|it| nodes[*it].key),
            ) {
                workspace[stack[sz - 1]][i] = workspace[stack[sz - 2]][i - 1];
            } else {
                workspace[stack[sz - 1]][i] = std::cmp::min(
                    std::cmp::min(
                        workspace[stack[sz - 1]][i - 1],
                        workspace[stack[sz - 2]][i - 1],
                    ),
                    workspace[stack[sz - 2]][i],
                ) + 1;
            }
            if sz > 2
                && i > 1
                && KEYCHECKER::check(Some(wordkey[i - 2]), nodes[stack[sz - 1]].key)
                && KEYCHECKER::check(Some(wordkey[i - 1]), nodes[stack[sz - 2]].key)
            {
                workspace[stack[sz - 1]][i] = std::cmp::min(
                    workspace[stack[sz - 1]][i],
                    workspace[stack[sz - 3]][i - 2] + 1,
                );
            }
        }
        let condition = worst_case
            .map(|wv| wv <= *workspace[stack[sz - 2]][..].iter().min().unwrap())
            .unwrap_or(false);
        if condition {
            VisitOutcome::Skip
        } else {
            VisitOutcome::Continue
        }
    }
}

pub struct LevenshteinNoSubDistanceCalculator {}

#[sealed]
impl<KEY, KEYCHECKER> DistanceCalculator<KEY, KEYCHECKER> for LevenshteinNoSubDistanceCalculator
where
    KEY: TrieKey,
    KEYCHECKER: KeyChecker<KEY>,
{
    fn compute(
        workspace: &mut Vec<Vec<usize>>,
        nodes: &[LevTrieNode<KEY>],
        stack: &[usize],
        wordkey: &[KEY],
        worst_case: Option<usize>,
    ) -> VisitOutcome {
        let sz = stack.len();
        let key_size = wordkey.len();
        for i in 1..=key_size {
            if KEYCHECKER::check(Some(wordkey[i - 1]), nodes[stack[sz - 1]].key) {
                workspace[stack[sz - 1]][i] = workspace[stack[sz - 2]][i - 1];
            } else {
                workspace[stack[sz - 1]][i] = std::cmp::min(
                    std::cmp::min(
                        workspace[stack[sz - 1]][i - 1] + 1,
                        workspace[stack[sz - 2]][i - 1] + 2,
                    ),
                    workspace[stack[sz - 2]][i] + 1,
                );
            }
        }
        let condition = worst_case
            .map(|wv| wv <= *workspace[stack[sz - 1]][..].iter().min().unwrap())
            .unwrap_or(false);
        if condition {
            VisitOutcome::Skip
        } else {
            VisitOutcome::Continue
        }
    }
}

pub struct LcsDistanceCalculator {}

#[sealed]
impl<KEY, KEYCHECKER> DistanceCalculator<KEY, KEYCHECKER> for LcsDistanceCalculator
where
    KEY: TrieKey,
    KEYCHECKER: KeyChecker<KEY>,
{
    fn compute(
        workspace: &mut Vec<Vec<usize>>,
        nodes: &[LevTrieNode<KEY>],
        stack: &[usize],
        wordkey: &[KEY],
        _: Option<usize>,
    ) -> VisitOutcome {
        let max_distance = wordkey.len();
        let sz = stack.len();
        let key_size = wordkey.len();
        for i in 1..=key_size {
            if KEYCHECKER::check(Some(wordkey[i - 1]), nodes[stack[sz - 1]].key) {
                workspace[stack[sz - 1]][i] = if sz == 2 || i == 1 {
                    max_distance - 1
                } else {
                    workspace[stack[sz - 2]][i - 1] - 1
                };
            } else {
                let up = if sz == 2 {
                    max_distance
                } else {
                    workspace[stack[sz - 2]][i]
                };
                let left = if i == 1 {
                    max_distance
                } else {
                    workspace[stack[sz - 1]][i - 1]
                };
                workspace[stack[sz - 1]][i] = std::cmp::min(up, left);
            }
        }
        let condition = workspace[stack[sz - 1]][key_size] == 0;
        if condition {
            VisitOutcome::Skip
        } else {
            VisitOutcome::Continue
        }
    }
}
