#!/usr/bin/env python
from __future__ import print_function
from builtins import range

from argparse import ArgumentParser, RawTextHelpFormatter as RawText, FileType
from collections import namedtuple
import random
import math

from networkx_tools import io as nxio
from networkx_tools import tree_distance

class WbstQuestion(namedtuple("WbstQuestionTuple", "question answer detractors")):

    def __new__(cls, question, answer, detractors):
        return super(WbstQuestion, cls).__new__(cls, question, answer, tuple(detractors))

    def to_csv(self):
        answer_with_detractors = [self.answer] + list(self.detractors)
        random.Random(self).shuffle(answer_with_detractors)
        return "{};{};{}".format(self.question, self.answer, ";".join(answer_with_detractors))

class WbstQuestionGenerator(object):

    def __init__(self, graph, detractors=None, detractor_count=3, graph_depth=None):
        self.graph = self.load_graph(graph)
        self.detractors = detractors
        self.detractor_count = detractor_count
        self.graph_depth = graph_depth or self.calculate_graph_depth(graph)
        self.lemma_to_node = {}

    @staticmethod
    def load_graph(graph, format_=None):
        if isinstance(graph, basestring):
            graph = nxio.read(graph, format_)
        return graph

    @staticmethod
    def calculate_graph_depth(graph):
        distance_from_root = (len(min(graph.nodes[node]["path"], key=len))
                              for node in graph.nodes())
        return max(distance_from_root)

    def get_words(self, node):
        return {lemma[1] for lemma in self.get_lemmas(node)}

    def get_lemmas(self, node):
        if node < 0:
            return set()
        return {(unit.pos, unit.lemma, unit.variant)
                for unit in self.graph.nodes[node]["synset"].lu_set}

    def random_word(self, node):
        return self.random_lemma(node)[1]

    def random_lemma(self, node):
        return random.choice(tuple(self.get_lemmas(node)))

    def hypernyms(self, node):
        return (target for target in self.graph[node])

    def find_word(self, word):
        if not self.lemma_to_node:
            self.generate_lemma_to_node()
        return {value
                for (key, value) in self.lemma_to_node.items()
                if key[1] == word}

    def find_lemma(self, lemma):
        if not self.lemma_to_node:
            self.generate_lemma_to_node()
        return self.lemma_to_node[lemma]

    def generate_lemma_to_node(self):
        self.lemma_to_node = {}
        for node in self.graph.nodes():
            for lemma in self.get_lemmas(node):
                self.lemma_to_node[lemma] = node

    def generate_questions_for_word(self, word):
        questions = []
        for word_node in self.find_word(word):
            questions.extend(self.generate_questions_for_node(word, word_node))
        return questions

    def generate_questions_for_node(self, word, word_node):
        neighbors = self.get_neighbors(word_node, word)
        detractors = self.get_detractors(word_node, neighbors)
        detractors_measure = self.get_wordnet_similarity_measure(detractors)
        for neighbor in neighbors:
            detractors_sample = [self.random_detractor(detractors_measure)
                                 for _ in range(self.detractor_count)]
            yield WbstQuestion(word, neighbor, detractors_sample)

    def get_neighbors(self, word_node, word):
        node_with_hypernyms = [word_node]
        node_with_hypernyms.extend(self.hypernyms(word_node))
        return {word
                for node in node_with_hypernyms
                for word in self.get_words(node)}.difference({word})

    def get_detractors(self, word_node, neighbors):
        detractors = {}
        for other_node in self.graph.nodes():
            if other_node == word_node:
                continue
            if set(self.get_words(other_node)).intersection(neighbors):
                continue
            if not self.is_detractor(other_node):
                continue
            detractors[other_node] = self.distance(word_node, other_node)
        return detractors

    def distance(self, first, second):
        return tree_distance.distance(self.graph, first, second)

    def is_detractor(self, node):
        if not self.detractors:
            return True
        words = self.get_words(node)
        return any(word in self.detractors for word in words)

    def get_wordnet_similarity_measure(self, detractors):
        dict_ = {node: self.wordnet_similarity_measure(distance)
                 for (node, distance) in detractors.items()}
        dict_ = {key: value
                 for (key, value) in dict_.items()
                 if value > 0}
        return dict_

    def wordnet_similarity_measure(self, distance):
        measure = -math.log(1.0 * distance / (2 * self.graph_depth))
        if measure < 0:
            measure = 0
        return measure

    def random_detractor(self, detractors):
        random_node = WbstQuestionGenerator.random_weighted_choice(detractors)
        return self.random_word(random_node)

    @staticmethod
    def random_weighted_choice(dict_):
        bisection = random.random() * sum(dict_.values())
        for (item, value) in dict_.items():
            bisection -= value
            if bisection <= 0:
                return item


