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

from SpellChecker.Candidate cimport Candidate
from SpellChecker.NGramSpellChecker cimport NGramSpellChecker
from SpellChecker.Operator import Operator
from SpellChecker.SpellCheckerParameter cimport SpellCheckerParameter


cdef class ContextBasedSpellChecker(NGramSpellChecker):

    __context_list: dict

    def __init__(self,
                 fsm: FsmMorphologicalAnalyzer,
                 nGram: NGram,
                 parameter: SpellCheckerParameter):
        """
        A constructor of {@link ContextBasedSpellChecker} 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.loadContextDictionaries()

    cpdef loadContextDictionaries(self):
        """
        This method also loads context information from a file.
        """
        cdef list lines, items, otherWords
        cdef str line, word
        self.__context_list = {}
        input_file = open(pkg_resources.resource_filename(__name__, 'data/context_list.txt'), "r", encoding="utf8")
        lines = input_file.readlines()
        for line in lines:
            items = line.strip().split("\t")
            if len(items) == 2:
                word = items[0]
                otherWords = items[1].split(" ")
                self.__context_list[word] = otherWords
        input_file.close()

    cpdef list candidateList(self, Word word, Sentence sentence):
        """
        Uses context information to generate candidates for a misspelled word.
        The candidates are the words that are in the context of the neighbouring words of the misspelled word.
        Uses the {@Link damerauLevenshteinDistance(String, String) method to calculate the distance between the misspelled word and
        the candidates and to determine whether the candidates are valid.
        :param word: the misspelled word
        :param sentence: the sentence containing the misspelled word
        :return: an ArrayList of valid candidates for the misspelled word
        """
        cdef list words, valid_candidates
        cdef set candidates
        cdef Word w
        cdef FsmParseList parses
        cdef str root, s
        cdef Candidate candidate
        cdef int distance
        words = []
        candidates = set()
        valid_candidates = []
        for w in sentence.getWords():
            if w != word:
                words.append(w)
        for w in words:
            parses = self.fsm.morphologicalAnalysis(Word.toCapital(w.getName()))
            if parses.size() > 0:
                root = parses.getParseWithLongestRootWord().getWord().getName()
                if self.__context_list.get(root) is not None:
                    for s in self.__context_list[root]:
                        candidates.add(Candidate(s, Operator.CONTEXT_BASED))
        for candidate in candidates:
            if len(candidate.getName()) < 5:
                distance = 1
            elif len(candidate.getName()) < 7:
                distance = 2
            else:
                distance = 3
            if self.damerauLevenshteinDistance(word.getName(), candidate.getName()) <= distance:
                valid_candidates.append(candidate)
        return valid_candidates

    cpdef int damerauLevenshteinDistance(self, str first, str second):
        """
        Calculates the Damerau-Levenshtein distance between two strings.
        This method also allows for the transposition of adjacent characters,
        which is not possible in a standard Levenshtein distance calculation.
        :param first: the first string
        :param second: the second string
        :return: the Damerau-Levenshtein distance between the two strings
        """
        cdef int first_length, second_length, first_index, second_index, cost
        cdef list distance_matrix
        if first is None or first == "":
            if second is None or second == "":
                return 0
            else:
                return len(second)
        if second is None or second == "":
            return len(first)
        first_length = len(first)
        second_length = len(second)
        distance_matrix = []
        for first_index in range(first_length + 1):
            distance_matrix.append([])
            for second_index in range(second_length + 1):
                distance_matrix[first_index].append(0)
        for first_index in range(first_length + 1):
            distance_matrix[first_index][0] = first_index
        for second_index in range(second_length + 1):
            distance_matrix[0][second_index] = second_index
        for first_index in range(1, first_length + 1):
            for second_index in range(1, second_length + 1):
                if first[first_index - 1] == second[second_index - 1]:
                    cost = 0
                else:
                    cost = 1
                distance_matrix[first_index][second_index] = min(min(distance_matrix[first_index - 1][second_index] + 1,
                                                                  distance_matrix[first_index][second_index - 1] + 1),
                                                              distance_matrix[first_index - 1][second_index - 1] + cost)
                if first_index == 1 or second_index == 1:
                    continue
                if first[first_index - 1] == second[second_index - 2] and first[first_index - 2] == second[
                    second_index - 1]:
                    distance_matrix[first_index][second_index] = min(distance_matrix[first_index][second_index],
                                                                  distance_matrix[first_index - 2][
                                                                      second_index - 2] + cost)
        return distance_matrix[first_length][second_length]
