from collections import deque

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

from SpellChecker.Candidate import Candidate
from SpellChecker.NGramSpellChecker import NGramSpellChecker
from SpellChecker.SpellCheckerParameter import SpellCheckerParameter
from SpellChecker.Trie import Trie
from SpellChecker.TrieCandidate import TrieCandidate


class TrieBasedSpellChecker(NGramSpellChecker):
    __generated_words: []
    __trie: Trie

    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()

    def loadTrieDictionaries(self):
        """
        This method also loads generated words from a file.
        """
        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()

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

    def candidateList(self, word: Word, sentence: Sentence) -> list:
        """
        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
        """
        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

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

    def generateTrieCandidates(self, candidate: TrieCandidate) -> [TrieCandidate]:
        """
        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
        """
        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
