from collections import deque

import pkg_resources
from Corpus.Sentence cimport Sentence
from Dictionary.Word cimport Word
from MorphologicalAnalysis.FsmMorphologicalAnalyzer cimport FsmMorphologicalAnalyzer
from NGram.NGram cimport NGram

from SpellChecker.Candidate cimport Candidate
from SpellChecker.NGramSpellChecker cimport NGramSpellChecker
from SpellChecker.SpellCheckerParameter cimport SpellCheckerParameter
from SpellChecker.Trie cimport Trie
from SpellChecker.TrieCandidate cimport TrieCandidate
from SpellChecker.TrieNode cimport TrieNode

cdef class TrieBasedSpellChecker(NGramSpellChecker):

    def __init__(self,
                 fsm: FsmMorphologicalAnalyzer,
                 nGram: NGram,
                 parameter: SpellCheckerParameter):
        """
        A constructor of {@link TrieBasedSpellChecker} class which takes a {@link FsmMorphologicalAnalyzer}, an {@link NGram}
        and a {@link SpellCheckerParameter} as inputs. Then, calls its super class {@link NGramSpellChecker} with given inputs.
        :param fsm: {@link FsmMorphologicalAnalyzer} type input.
        :param nGram: {@link NGram} type input.
        :param parameter: {@link SpellCheckerParameter} type input.
        """
        super().__init__(fsm,
                         nGram,
                         parameter)
        self.loadTrieDictionaries()
        self.prepareTrie()

    cpdef loadTrieDictionaries(self):
        """
        This method also loads generated words from a file.
        """
        cdef list lines
        cdef str line
        self.__generated_words = []
        input_file = open(pkg_resources.resource_filename(__name__, 'data/generated_words.txt'), "r", encoding="utf8")
        lines = input_file.readlines()
        for line in lines:
            self.__generated_words.append(line.strip())
        input_file.close()

    cpdef prepareTrie(self):
        """
        Populates a Trie data structure with a set of strings generated by the morphological analyzer.
        """
        cdef str word
        self.__trie = Trie()
        for word in self.__generated_words:
            self.__trie.insert(word)

    cpdef list candidateList(self, Word word, Sentence sentence):
        """
        Checks if the trie object contains the candidate.
        If it does, it compares the penalty of the candidate in the results with the penalty of the candidate variable.
        If the current penalty of the candidate in the results is less than or equal to the current penalty
        of the candidate variable, it removes the first element of the candidates ArrayDeque.
        If not, it adds the candidate variable to the results ArrayList and removes the first element.
        If the trie object does not contain the candidate, it checks if the current penalty and the current
        index of the candidate variable and if they are not at the limit,
        it moves the index one character to the right and generates new candidates.
        :param word: the word to search for candidates for
        :param sentence: the sentence containing the word
        :return: an ArrayList of Candidate objects representing the potential candidates
        """
        cdef list results
        cdef float penalty_limit
        cdef TrieCandidate candidate, new_candidate
        cdef str candidate_name
        cdef int item_index
        candidates = deque()
        results = []
        candidates.append(TrieCandidate(word.getName(), -1, 0))
        penalty_limit = min(word.charCount() / 2.0, 3.0)
        while len(candidates) != 0:
            candidate = candidates[0]
            candidate_name = candidate.getName()
            if self.__trie.search(candidate_name):
                item_index = self.searchCandidates(results, candidate)
                if item_index != -1 and results[item_index].getCurrentPenalty() <= candidate.getCurrentPenalty():
                    candidates.popleft()
                else:
                    results.append(candidates.popleft())
            else:
                if candidate.getCurrentPenalty() > penalty_limit - 1 or candidate.getCurrentIndex() >= len(
                        candidate_name) - 1:
                    candidates.popleft()
                else:
                    candidate = candidates.popleft()
                    candidate.nextIndex()
                    candidates.append(
                        TrieCandidate(candidate.getName(), candidate.getCurrentIndex(), candidate.getCurrentPenalty()))
                    new_candidates = self.generateTrieCandidates(candidate)
                    for new_candidate in new_candidates:
                        candidates.append(new_candidate)
        return results

    cpdef int searchCandidates(self, list result, TrieCandidate candidate):
        cdef int i
        for i in range(len(result)):
            if result[i].getName() == candidate.getName():
                return i
        return -1

    cpdef list generateTrieCandidates(self, TrieCandidate candidate):
        """
        Generates a set of candidates based on a given TrieCandidate.
        The generated candidates are created by applying a set of operations to
        the input TrieCandidate. The possible operations are:
        De-asciification: replacing certain ASCII characters with their non-ASCII counterparts.
        Substitution: replacing a single character with another character.
        Insertion: adding a single character to the string.
        Deletion: removing a single character from the string.
        Transposition: swapping the positions of two adjacent characters in the string.
        :param candidate: the input TrieCandidate
        :return: a set of candidate strings, each contained in a TrieCandidate object
        """
        cdef list candidates
        cdef str current_name, ch2, letters, deasciified_word, replaced, added, added_last, deleted, swapped
        cdef int current_index, j
        cdef float current_penalty
        cdef TrieNode current_node
        candidates = []
        current_name = candidate.getName()
        current_index = candidate.getCurrentIndex()
        current_penalty = candidate.getCurrentPenalty()
        current_node = self.__trie.getTrieNode(current_name[:current_index])
        if current_node is None:
            return candidates
        letters = current_node.childrenToString()
        if current_name[current_index] == 'c':
            ch2 = 'ç'
        elif current_name[current_index] == 'g':
            ch2 = 'ğ'
        elif current_name[current_index] == 'i':
            ch2 = 'ı'
        elif current_name[current_index] == 's':
            ch2 = 'ş'
        elif current_name[current_index] == 'o':
            ch2 = 'ö'
        elif current_name[current_index] == 'u':
            ch2 = 'ü'
        else:
            ch2 = ''
        if ch2 != '':
            deasciified_word = current_name[:current_index] + ch2 + current_name[current_index + 1:]
        else:
            deasciified_word = current_name
        if deasciified_word != current_name and self.__trie.startsWith(deasciified_word[:current_index + 1]):
            candidates.append(TrieCandidate(deasciified_word, current_index, current_penalty + 0.2))
        for j in range(len(letters)):
            replaced = current_name[:current_index] + letters[j] + current_name[current_index + 1:]
            candidates.append(TrieCandidate(replaced, current_index, current_penalty + 1))
            added = current_name[:current_index] + letters[j] + current_name[current_index:]
            candidates.append(TrieCandidate(added, current_index, current_penalty + 1))
            added_last = current_name + letters[j]
            if self.__trie.startsWith(added_last):
                candidates.append(TrieCandidate(added_last, current_index, current_penalty + 1))
        deleted = current_name[:current_index] + current_name[current_index + 1:]
        if len(deleted) > 1:
            candidates.append(TrieCandidate(deleted, current_index - 1, current_penalty + 1))
        if current_index < len(current_name) - 1 and current_name[current_index] != current_name[current_index + 1] \
                and self.__trie.startsWith(current_name[:current_index + 2]):
            swapped = current_name[:current_index] + current_name[current_index + 1] + current_name[current_index] + \
                      current_name[current_index + 2:]
            candidates.append(TrieCandidate(swapped, current_index, current_penalty + 1))
        return candidates
