import os
import sys
import math
import numpy as np
from germanetpy.filterconfig import Filterconfig
from germanetpy.semrel_measures import SemRelMeasure


class ICBasedSimilarity:
    """
    The IC-based measures are computed based on relative frequencies of words in a large corpus. Synset frequencies
    are computed
    by adding up the frequencies of all words that belong to a Synset. These measures can not be computed between
    synsets with different word categories
    """

    def __init__(self, germanet, wordcategory, path: str, separator: str = "\t"):
        """
        The constructor of the ICBasedSimilarity class. The frequency dictionary is initialized for a given word
        category. The *jcnmaxdist* is twice the information content of two leaf nodes with the highest information
        content (MAX_IC), whose LCS is GNROOT and is needed for the jiang and conraths measure. The total_freq is the
        frequency of ROOT, so the sum of all Synset
        frequencies of the GermaNet Graph. It is needed to compute the information content.

        :type wordcategory: WordCategory
        :type germanet: Germanet
        :param germanet: The GermaNet Graph
        :param wordcategory: The word category to compute the IC-based similarities for
        :param path: The path to a frequency list containing words and their frequencies in a corpus
        :param separator: The char that separates a word and its frequency in the given frequency list,
        default is a tabulator
        """
        self._germanet = germanet
        self._synset2simplefreq = self._init_simple_freq_dic(wordcategory)
        self.create_simple_freq_dic(wordcategory, path, separator)
        self._synset2cumfreq = self.synset2simple_freq.copy()
        self._compute_cum_freq_dic()

        self._root_freq = self.synset2cumfreq[self.germanet.root.id]
        self._jcnmaxdist = 2 * -math.log10(1 / self.root_freq)

        self._synset2ic, self._most_informative_synset = self.init_ic_map()
        synset_pair = (self.most_informative_synset, self.germanet.root)
        self._normalization_dic = self.init_min_max_normalization_values(synset_pair)

    def create_simple_freq_dic(self, word_category, path: str, separator: str):
        """
        Reads in the frequency list files and stores the frequency information for each Synset in a dictionary. The keys
        are the Synset IDs. This method also adds all available Synset frequencies for the given category.

        :type word_category: WordCategory
        :param word_category: The word category
        :param path: The path to a frequency list containing words and their frequencies in a corpus
        :param separator: The char that separates a word and its frequency in the given frequency list
        """
        if os.path.exists(path):
            with open(path, "r") as f:
                try:
                    for line in f:
                        parts = line.strip().split(separator)
                        # frequency of word form
                        freq = int(parts[1])
                        # word form
                        word = parts[0]
                        config = Filterconfig(word, ignore_case=False)
                        config.word_categories = [word_category]
                        synsets = config.filter_synsets(self.germanet)
                        for syn in synsets:
                            self.synset2simple_freq[syn.id] += freq
                    f.close()
                except OSError:
                    print("Could not open/read file:", path)
                    sys.exit()
        else:
            print("file %s does not exist" % path)

    def _init_simple_freq_dic(self, word_category) -> dict:
        """
        creates a dictionary of synsets of a specific word category. each synset has a default frequency of 1 (as a
        smoothing technique to avoid division by 0 when computing the information content). the root is added as a
        Synset as well

        :type word_category: WordCategory
        :param word_category: the word category
        :return: the initialized dictionary with a default frequency of 1
        """
        allsynsets_wordcat = self.germanet.get_synsets_by_wordcategory(word_category)
        allsynsets_wordcat = set([synset.id for synset in allsynsets_wordcat])
        allsynsets_wordcat.add(self.germanet.root.id)
        synset2cumfreq = dict(zip(list(allsynsets_wordcat), len(allsynsets_wordcat) * [1]))
        return synset2cumfreq

    def _compute_cum_freq_dic(self):
        """
        For each Synset in the Graph of a certain word category, add its frequency to all of its hypernyms
        """
        for synset_id, simple_freq in self.synset2simple_freq.items():
            self._cumulate(self.germanet.get_synset_by_id(synset_id), simple_freq)

    def _cumulate(self, synset, freq):
        """
        recursive method to add a simple frequency to all hypernyms of a synset

        :type freq: int
        :type synset: Synset
        :param synset: the synset to whose direct hypernyms the frequency will be added
        :param freq: the frequency
        :return: call the method until root is reached
        """
        hypernyms = synset.direct_hypernyms
        if hypernyms:
            for hypernym in hypernyms:
                self.synset2cumfreq[hypernym.id] += freq
                self._cumulate(hypernym, freq)
        else:
            return

    def init_min_max_normalization_values(self, synset_pair) -> dict:
        """
        This methods computes the minimal values (two Synsets are equal) and the maximum values (two Synsets are
        maximally apart in the graph) for normalization

        :type synset_pair: tuple(Synset, Synset)
        :param synset_pair: The Tuple of synsets that have the maximum distance in the graph
        :return: a dictionary containing the (minimum value, maximum value) for each semantic similarity measure.
        """
        min_res = self.resnik(synset_pair[0], synset_pair[1])
        max_res = self.resnik(synset_pair[0], synset_pair[0])
        min_lin = self.lin(synset_pair[0], synset_pair[1])
        max_lin = self.lin(synset_pair[0], synset_pair[0])
        max_jcn = self.jiang_and_conrath(synset_pair[0], synset_pair[0])
        norm_values = {
            SemRelMeasure.Resnik: (min_res, max_res),
            SemRelMeasure.Lin: (min_lin, max_lin),
            SemRelMeasure.JiangAndConrath: (0.0, max_jcn)
        }
        return norm_values

    def init_ic_map(self):
        """
        Computes the information content for each synset in GermaNet (of a given word category).

        :rtype: dict, Synset
        :return: A dictionary with a Synset and the corresponding IC, a Synset with the highest IC
        """
        synset2ic = {}
        max_ic = 0
        most_informative_synset = self.germanet.root
        for synset_id in self.synset2cumfreq.keys():
            synset = self.germanet.get_synset_by_id(synset_id)
            ic = self.get_information_content(synset)
            synset2ic[synset_id] = ic
            if ic > max_ic:
                max_ic = ic
                most_informative_synset = synset
        return synset2ic, most_informative_synset

    def get_information_content(self, synset) -> float:
        """
        The information content graduates semantic concepts from general to specific. The more specific a concept, the
        smaller the probability and thus the higher its informativeness. The information content of a semantic con-
        cept is estimated by the relative frequency of the concept in a large corpus (cumulated synset frequency)

        :type synset: Synset
        :param synset: the information content should be computed for
        :return: the information content for the given synset
        """
        assert synset.id in self.synset2cumfreq.keys(), "no frequency information for this synset available"
        synset_freq = self.synset2cumfreq[synset.id]
        return -math.log10(synset_freq / self.root_freq)

    def resnik(self, synset1, synset2, normalize: bool = False, normalized_max: float = 1.0) -> float:
        """
        Two concepts are more related the more information they share. The shared information of two
        concepts can be quantified by the information content of two concepts' lowest common subsumer.
        When several LCS are available the highest IC is returned.

        :type synset2: Synset
        :type synset1: Synset
        :param synset1: The source synset
        :param synset2: The target synset
        :param normalize: The relatedness value can be normalized to a number between the possible minimum of that measure and a given upper bound.
        :param normalized_max: The upper bound of the range the measure is normalized to.
        :return: The information content of the LCS of the two given synsets.
        """
        if synset1 == synset2:
            # if the two synsets are the same, resnik is the IC of the synset
            resnik = self.get_information_content(synset1)
        else:
            lcs_list = synset1.lowest_common_subsumer(synset2)
            resnik = 0
            for lcs in lcs_list:
                ic = self.get_information_content(lcs)
                if ic > resnik:
                    resnik = ic
        if normalize:
            resnik = self.normalize(raw_value=resnik, normalized_max=normalized_max,
                                    semrel_measure=SemRelMeasure.Resnik)
        return resnik

    def jiang_and_conrath(self, synset1, synset2, normalize: float = False, normalized_max: float = 1.0) -> float:
        """
        The Jiang and Conraths measure includes knowledge about the individual information contents of each synset.
        The smaller the difference of the information content of the two synsets, the more related they are.

        :type synset1: Synset
        :type synset2: Synset
        :param synset1: The source synset
        :param synset2: The target synset
        :param normalize: The relatedness value can be normalized to a number between the possible minimum of that measure and a given upper bound.
        :param normalized_max: The upper bound of the range the measure is normalized to.
        :return: The jiang and conrath relatedness measure
        """
        distance = self.get_information_content(synset1) + self.get_information_content(synset2) - 2 * self.resnik(
            synset1, synset2)
        jcn = self.jcnmaxdist - distance
        if normalize:
            jcn = self.normalize(raw_value=jcn, normalized_max=normalized_max,
                                 semrel_measure=SemRelMeasure.JiangAndConrath)
        return jcn

    def lin(self, synset1, synset2, normalize: bool = False, normalized_max: float = 1.0) -> float:
        """
        The lin measure takes the individual information contents of each synset and the information content of the
        LCS into account. The LCS with the highest information content is used for the computation.

        :type synset2: Synset
        :type synset1: Synset
        :param synset1: The source synset
        :param synset2: The target synset
        :param normalize: The relatedness value can be normalized to a number between the possible minimum of that measure and a given upper bound.
        :param normalized_max: The upper bound of the range the measure is normalized to.
        :return: The Lin relatedness measure
        """
        ic_lcs = self.resnik(synset1, synset2)
        lin = (2 * ic_lcs) / (self.get_information_content(synset1) + self.get_information_content(synset2))
        if normalize:
            lin = self.normalize(raw_value=lin, normalized_max=normalized_max, semrel_measure=SemRelMeasure.Lin)
        return lin

    def normalize(self, raw_value: float, normalized_max: float, semrel_measure: SemRelMeasure) -> float:
        """
        Normalizes a raw value of semantic relatedness to a value between a lower bound and the given upper bound.

        :param raw_value: The raw value
        :param normalized_max: The upper bound
        :param semrel_measure: The semantic relatedness measure, the value corresponds to.
        :return: The normalized semantic relatedness value
        """
        lower_bound, upper_bound = self.normalization_dic[semrel_measure]
        return np.round(((raw_value - lower_bound) / (upper_bound - lower_bound)) * normalized_max, decimals=5)

    @property
    def germanet(self):
        return self._germanet

    @property
    def root_freq(self):
        return self._root_freq

    @property
    def synset2cumfreq(self):
        return self._synset2cumfreq

    @property
    def jcnmaxdist(self):
        return self._jcnmaxdist

    @property
    def normalization_dic(self):
        return self._normalization_dic

    @property
    def synset2ic(self):
        return self._synset2ic

    @property
    def most_informative_synset(self):
        return self._most_informative_synset

    @property
    def synset2simple_freq(self):
        return self._synset2simplefreq
