from __future__ import unicode_literals

from pepper.language.pos import POS
from pepper.language.analyzer import Analyzer
from pepper.language.utils.atoms import UtteranceType
from pepper.brain.utils.helper_functions import casefold_text
from pepper.brain.utils.rdf_builder import RdfBuilder
from pepper.brain.utils.response import Triple, Perspective

from pepper import logger, config
from nltk import pos_tag

from nltk import CFG, RecursiveDescentParser, edit_distance

from collections import Counter

from random import getrandbits
from datetime import datetime
import enum
import json
import os

from typing import List, Optional


class Time(enum.Enum):
    PAST = -1
    PRESENT = 0
    FUTURE = 1


class Emotion(enum.Enum):  # Not used yet
    ANGER = 0
    DISGUST = 1
    FEAR = 2
    HAPPINESS = 3
    SADNESS = 4
    SURPRISE = 5


class Chat(object):
    def __init__(self, speaker, context):
        """
        Create Chat

        Parameters
        ----------
        speaker: str
            Name of speaker (a.k.a. the person Pepper has a chat with)
        context: Context
            Context Chat is part of
        """

        self._id = getrandbits(128)
        self._context = context
        self._speaker = speaker
        self._utterances = []

        self._log = self._update_logger()
        self._log.info("<< Start of Chat with {} >>".format(speaker))

    @property
    def context(self):
        """
        Returns
        -------
        context: Context
            Context
        """
        return self._context

    @property
    def speaker(self):
        # type: () -> str
        """
        Returns
        -------
        speaker: str
            Name of speaker (a.k.a. the person Pepper has a chat with)
        """
        return self._speaker

    @speaker.setter
    def speaker(self, value):
        self._speaker = value

    @property
    def id(self):
        # type: () -> int
        """
        Returns
        -------
        id: int
            Unique (random) identifier of this chat
        """
        return self._id

    @property
    def utterances(self):
        # type: () -> List[Utterance]
        """
        Returns
        -------
        utterances: list of Utterance
            List of utterances that occurred in this chat
        """
        return self._utterances

    @property
    def last_utterance(self):
        # type: () -> Utterance
        """
        Returns
        -------
        last_utterance: Utterance
            Most recent Utterance
        """
        return self._utterances[-1]

    def add_utterance(self, hypotheses, me):
        # type: (List[UtteranceHypothesis], bool) -> Utterance
        """
        Add Utterance to Conversation

        Parameters
        ----------
        hypotheses: list of UtteranceHypothesis

        Returns
        -------
        utterance: Utterance
        """
        utterance = Utterance(self, hypotheses, me, len(self._utterances))
        self._utterances.append(utterance)

        self._log = self._update_logger()
        self._log.info(utterance)

        return utterance

    def _update_logger(self):
        return logger.getChild("Chat {:19s} {:03d}".format("({})".format(self.speaker), len(self._utterances)))

    def __repr__(self):
        return "\n".join([str(utterance) for utterance in self._utterances])


class Utterance(object):

    def __init__(self, chat, hypotheses, me, turn):
        # type: (Chat, List[UtteranceHypothesis], bool, int) -> Utterance
        """
        Construct Utterance Object

        Parameters
        ----------
        chat: Chat
            Reference to Chat Utterance is part of
        hypotheses: List[UtteranceHypothesis]
            Hypotheses on uttered text (transcript, confidence)
        me: bool
            True if Robot spoke, False if Person Spoke
        turn: int
            Utterance Turn
        """

        self._log = logger.getChild(self.__class__.__name__)

        self._datetime = datetime.now()
        self._chat = chat
        self._context = self._chat.context
        self._chat_speaker = self._chat.speaker
        self._turn = turn
        self._me = me

        self._hypothesis = self._choose_hypothesis(hypotheses)

        self._tokens = self._clean(self._tokenize(self.transcript))

        # TODO: Optimize: takes 2.6 seconds now! Should be < 1 second!?
        self._parser = None if self.me else Parser(self)
        # TODO analyze sets triple, perspective and type, but currently is not called on constructor
        self._type = None
        self._triple = None
        self._perspective = None

    @property
    def chat(self):
        # type: () -> Chat
        """
        Returns
        -------
        chat: Chat
            Utterance Chat
        """
        return self._chat

    @property
    def context(self):
        # type: () -> Context
        """
        Returns
        -------
        context: Context
            Context (a.k.a. people, objects and other detections )
        """
        return self._context

    @property
    def chat_speaker(self):
        # type: () -> str
        """
        Returns
        -------
        speaker: str
            Name of speaker (a.k.a. the person Pepper has a chat with)
        """
        return self._chat_speaker

    @property
    def type(self):
        # type: () -> UtteranceType
        """
        Returns
        -------
        type: UtteranceType
            Whether the utterance was a statement, a question or an experience
        """
        return self._type

    @property
    def transcript(self):
        # type: () -> str
        """
        Returns
        -------
        transcript: str
            Utterance Transcript
        """
        return self._hypothesis.transcript

    @property
    def confidence(self):
        # type: () -> float
        """
        Returns
        -------
        confidence: float
            Utterance Confidence
        """
        return self._hypothesis.confidence

    @property
    def me(self):
        # type: () -> bool
        """
        Returns
        -------
        me: bool
            True if Robot spoke, False if Person Spoke
        """
        return self._me

    @property
    def turn(self):
        # type: () -> int
        """
        Returns
        -------
        turn: int
            Utterance Turn
        """
        return self._turn

    @property
    def triple(self):
        # type: () -> Triple
        """
        Returns
        -------
        triple: Triple
            Structured representation of the utterance
        """
        return self._triple

    @property
    def perspective(self):
        # type: () -> Perspective
        """
        Returns
        -------
        perspective: Perspective
            NLP features related to the utterance
        """
        return self._perspective

    @property
    def datetime(self):
        return self._datetime

    @property
    def language(self):
        """
        Returns
        -------
        language: str
            Original language of the Transcript
        """
        raise NotImplementedError()

    @property
    def tokens(self):
        """
        Returns
        -------
        tokens: list of str
            Tokenized transcript
        """
        return self._tokens

    @property
    def parser(self):
        # type: () -> Optional[Parser]
        """
        Returns
        -------
        parsed_tree: ntlk Tree generated by the CFG parser
        """
        return self._parser

    def analyze(self):
        """
        Determines the type of utterance, extracts the RDF triple and perspective attaching them to the last utterance
        Parameters
        ----------
        chat

        Returns
        -------

        """
        analyzer = Analyzer.analyze(self._chat)

        if not analyzer:
            return "I cannot parse your input"

        for el in ["subject", "predicate", "object"]:
            Analyzer.LOG.info(
                "RDF {:>10}: {}".format(el, json.dumps(analyzer.rdf[el], sort_keys=True, separators=(', ', ': '))))

        self.pack_triple(analyzer.rdf, analyzer.utterance_type)

        if analyzer.utterance_type == UtteranceType.STATEMENT:
            self.pack_perspective(analyzer.perspective)

    def pack_triple(self, rdf, utterance_type):
        """
        Sets utterance type, the extracted triple and (in future) the perspective
        Parameters
        ----------
        rdf
        utterance_type

        Returns
        -------

        """
        self._type = utterance_type
        if type(rdf) == str:
            return rdf

        if not rdf:
            return 'error in the rdf'

        builder = RdfBuilder()

        # Build each element
        subject = builder.fill_entity(casefold_text(rdf['subject']['text'], format='triple'),
                                      rdf['subject']['type'])
        predicate = builder.fill_predicate(casefold_text(rdf['predicate']['text'], format='triple'))
        object = builder.fill_entity(casefold_text(rdf['object']['text'], format='triple'),
                                                rdf['object']['type'])

        self.set_triple(Triple(subject, predicate, object))

    def pack_perspective(self, persp):
        self.set_perspective(Perspective(persp['certainty'], persp['polarity'], persp['sentiment']))

    def set_triple(self, triple):
        # type: (Triple) -> ()
        self._triple = triple

    def set_perspective(self, perspective):
        # type: (Perspective) -> ()
        self._perspective = perspective

    def casefold(self, format='triple'):
        # type (str) -> ()
        """
        Format the labels to match triples or natural language
        Parameters
        ----------
        format

        Returns
        -------

        """
        self._triple.casefold(format)
        self._chat_speaker = casefold_text(self.chat_speaker, format)

    def _choose_hypothesis(self, hypotheses):
        return sorted(self._patch_names(hypotheses), key=lambda hypothesis: hypothesis.confidence, reverse=True)[0]

    def _patch_names(self, hypotheses):
        if not self.me:

            names = []

            # Patch Transcripts with Names
            for hypothesis in hypotheses:

                transcript = []

                for word in hypothesis.transcript.split():
                    name = Utterance._get_closest_name(word)

                    if name:
                        names.append(name)
                        transcript.append(name)
                    else:
                        transcript.append(word)

                hypothesis.transcript = " ".join(transcript)

            if names:
                # Count Name Frequency and Adjust Hypothesis Confidence
                names = Counter(names)
                max_freq = max(names.values())

                for hypothesis in hypotheses:
                    for name in names.keys():
                        if name in hypothesis.transcript:
                            hypothesis.confidence *= float(names[name]) / float(max_freq)

        return hypotheses

    @staticmethod
    def _get_closest_name(word, names=config.PEOPLE_FRIENDS_NAMES, max_name_distance=2):
        # type: (str, List[str], int) -> str
        if word[0].isupper():
            name, distance = sorted([(name, edit_distance(name, word)) for name in names], key=lambda key: key[1])[0]

            if distance <= max_name_distance:
                return name

    def _tokenize(self, transcript):
        """
        Parameters
        ----------
        transcript: str
            Uttered text (Natural Language)

        Returns
        -------
        tokens: list of str
            Tokenized transcript: list of cleaned tokens
                - remove contractions
        """

        openers = ['leolani', 'sorry', 'excuse me', 'hey']
        introductions = ['can you tell me', 'do you know', 'please tell me', 'do you maybe know']

        for o in openers:
            if transcript.startswith(o):
                transcript = transcript.replace(o, '')

        for i in introductions:
            if transcript.startswith(i):
                tmp = transcript.replace(i, '')
                first_word = tmp.split()[0]
                if first_word in ['what', 'that', 'who', 'when', 'where', 'which']:
                    transcript = transcript.replace(i, '')

        tokens_raw = transcript.replace("'", " ").split()
        dict = {'m': 'am', 're': 'are', 'll': 'will'}
        dict_not = {'won': 'will', 'don': 'do', 'doesn': 'does', 'didn': 'did', 'haven': 'have', 'wouldn': 'would',
                    'aren': 'are'}

        for key in dict:
            tokens_raw = self.replace_token(tokens_raw, key, dict[key])

        if 't' in tokens_raw:
            tokens_raw = self.replace_token(tokens_raw, 't', 'not')
            for key in dict_not:
                tokens_raw = self.replace_token(tokens_raw, key, dict_not[key])

        if 's' in tokens_raw:
            index = tokens_raw.index('s')
            tag = pos_tag([tokens_raw[index + 1]])
            if tag[0][1] in ['DT', 'JJ', 'IN'] or tag[0][1].startswith('V'):  # determiner, adjective, verb
                tokens_raw.remove('s')
                tokens_raw.insert(index, 'is')
            else:
                tokens_raw.remove('s')

        return tokens_raw

    def replace_token(self, tokens_raw, old, new):
        if old in tokens_raw:
            index = tokens_raw.index(old)
            tokens_raw.remove(old)
            tokens_raw.insert(index, new)
        return tokens_raw

    def _clean(self, tokens):
        """
        Parameters
        ----------
        tokens: list of str
            Tokenized transcript

        Returns
        -------
        cleaned_tokens: list of str
            Tokenized & Cleaned transcript
        """
        return tokens

    def __repr__(self):
        author = config.NAME if self.me else self.chat.speaker
        return '{:>10s}: "{}"'.format(author, self.transcript)


class Parser(object):
    POS_TAGGER = None  # Type: POS
    CFG_GRAMMAR_FILE = os.path.join(os.path.dirname(__file__), 'data', 'cfg_new.txt')

    def __init__(self, utterance):

        if not Parser.POS_TAGGER:
            Parser.POS_TAGGER = POS()

        with open(Parser.CFG_GRAMMAR_FILE) as cfg_file:
            self._cfg = cfg_file.read()

        self._log = logger.getChild(self.__class__.__name__)

        self._forest, self._constituents = self._parse(utterance)

    @property
    def forest(self):
        return self._forest

    @property
    def constituents(self):
        return self._constituents

    def _parse(self, utterance):
        tokenized_sentence = utterance.tokens
        pos = self.POS_TAGGER.tag(tokenized_sentence)

        # # Fixing POS matching
        # import spacy
        # nlp = spacy.load('en_core_web_sm')
        #
        # doc = nlp(utterance.transcript)
        # for token in doc:
        #     #print(token.text, token.lemma_, token.pos_)
        #     ind = 0
        #     for w in pos:
        #         if w[0]==token.text and w[1]!=token.tag_:
        #             if (w[1]=='TO' and token.tag_=='IN') or w[1][:-1]==token.tag_ or w[1]==token.tag_[:-1]:
        #                 continue
        #             else:
        #                 #print('pos_mismatch ',w[1],token.tag_)
        #                 pos[ind] = (w[0],token.tag_)
        #         ind += 1

        ind = 0
        for w in tokenized_sentence:
            if w == 'like':
                pos[ind] = (w, 'VB')
            ind += 1

        if pos[0][0] == 'Does':
            pos[0] = ('Does', 'VBD')

        ind = 0
        for word, tag in pos:
            if '?' in word:
                word = word[:-1]
            if tag.endswith('$'):
                new_rule = tag[:-1] + 'POS -> \'' + word + '\'\n'
                pos[ind] = (pos[ind][0], 'PRPPOS')

            else:
                new_rule = tag + ' -> \'' + word + '\'\n'
            if new_rule not in self._cfg:
                self._cfg += new_rule
            ind += 1

        try:
            cfg_parser = CFG.fromstring(self._cfg)
            RD = RecursiveDescentParser(cfg_parser)

            last_token = tokenized_sentence[len(tokenized_sentence) - 1]

            if '?' in last_token:
                tokenized_sentence[len(tokenized_sentence) - 1] = last_token[:-1]

            parsed = RD.parse(tokenized_sentence)

            s_r = {}  # syntactic_realizations
            index = 0

            forest = [tree for tree in parsed]

            if len(forest):
                if (len(forest)) > 1:
                    self._log.debug('* Ambiguity in grammar *')
                for tree in forest[0]:  # alternative trees? f
                    for branch in tree:
                        s_r[index] = {'label': branch.label(), 'structure': branch}
                        raw = ''
                        for node in branch:
                            for leaf in node.leaves():
                                raw += leaf + '-'

                        s_r[index]['raw'] = raw[:-1]
                        index += 1

            else:
                self._log.debug("no forest")

            for el in s_r:
                if type(s_r[el]['raw']) == list:
                    string = ''
                    for e in s_r[el]['raw']:
                        string += e + ' '
                    s_r[el]['raw'] = string
                s_r[el]['raw'] = s_r[el]['raw'].strip()

            return forest, s_r

        except:
            return [], {}
