# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from abc import ABCMeta, abstractmethod
from logging import getLogger
from pathlib import Path
from typing import List, Union, Iterator

import numpy as np
from overrides import overrides

from deeppavlov.core.data.utils import zero_pad
from deeppavlov.core.models.component import Component
from deeppavlov.core.models.serializable import Serializable

log = getLogger(__name__)

class Embedder(Component, Serializable, metaclass=ABCMeta):
    """
    Class implements fastText embedding model

    Args:
        load_path: path where to load pre-trained embedding model from
        pad_zero: whether to pad samples or not

    Attributes:
        model: model instance
        tok2emb: dictionary with already embedded tokens
        dim: dimension of embeddings
        pad_zero: whether to pad sequence of tokens with zeros or not
        mean: whether to return one mean embedding vector per sample
        load_path: path with pre-trained fastText binary model
    """
    def __init__(self, load_path: Union[str, Path], pad_zero: bool = False, mean: bool = False, **kwargs) -> None:
        """
        Initialize embedder with given parameters
        """
        super().__init__(save_path=None, load_path=load_path)
        self.tok2emb = {}
        self.pad_zero = pad_zero
        self.mean = mean
        self.dim = None
        self.model = None
        self.load()

    @overrides
    def save(self) -> None:
        """
        Class does not save loaded model again as it is not trained during usage
        """
        raise NotImplementedError

    @overrides
    def __call__(self, batch: List[List[str]], mean: bool = None) -> List[Union[list, np.ndarray]]:
        """
        Embed sentences from batch

        Args:
            batch: list of tokenized text samples
            mean: whether to return mean embedding of tokens per sample

        Returns:
            embedded batch
        """
        batch = [self._encode(sample, mean) for sample in batch]
        if self.pad_zero:
            batch = zero_pad(batch)
        return batch

    @abstractmethod
    def __iter__(self) -> Iterator[str]:
        """
        Iterate over all words from the model vocabulary

        Returns:
            iterator
        """

    @abstractmethod
    def _get_word_vector(self, w: str) -> np.ndarray:
        """
        Embed a word using ``self.model``

        Args:
            w: a word

        Returns:
            embedding vector
        """

    def _encode(self, tokens: List[str], mean: bool) -> Union[List[np.ndarray], np.ndarray]:
        """
        Embed one text sample

        Args:
            tokens: tokenized text sample
            mean: whether to return mean embedding of tokens per sample

        Returns:
            list of embedded tokens or array of mean values
        """
        embedded_tokens = []
        for t in tokens:
            try:
                emb = self.tok2emb[t]
            except KeyError:
                try:
                    emb = self._get_word_vector(t)
                except KeyError:
                    emb = np.zeros(self.dim, dtype=np.float32)
                self.tok2emb[t] = emb
            embedded_tokens.append(emb)

        if mean is None:
            mean = self.mean

        if mean:
            filtered = [et for et in embedded_tokens if np.any(et)]
            if filtered:
                return np.mean(filtered, axis=0)
            return np.zeros(self.dim, dtype=np.float32)

        return embedded_tokens


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#     http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import re

from setuptools import setup, find_packages

import deeppavlov

__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))

def read_requirements():
    """parses requirements from requirements.txt"""
    reqs_path = os.path.join(__location__, 'requirements.txt')
    with open(reqs_path, encoding='utf8') as f:
        reqs = [line.strip() for line in f if not line.strip().startswith('#')]

    names = []
    links = []
    for req in reqs:
        if '://' in req:
            links.append(req)
        else:
            names.append(req)
    return {'install_requires': names, 'dependency_links': links}

def readme():
    with open(os.path.join(__location__, 'README.md'), encoding='utf8') as f:
        text = f.read()
    text = re.sub(r']\((?!https?://)', r'](https://github.com/deepmipt/DeepPavlov/blob/master/', text)
    text = re.sub(r'\ssrc="(?!https?://)', r' src="https://raw.githubusercontent.com/deepmipt/DeepPavlov/master/', text)
    return text

setup(
    name='deeppavlov',
    packages=find_packages(exclude=('tests', 'docs', 'utils')),
    version=deeppavlov.__version__,
    description=deeppavlov.__description__,
    long_description=readme(),
    long_description_content_type='text/markdown',
    author=deeppavlov.__author__,
    author_email=deeppavlov.__email__,
    license=deeppavlov.__license__,
    url='https://github.com/deepmipt/DeepPavlov',
    download_url='https://github.com/deepmipt/DeepPavlov/archive/' + deeppavlov.__version__ + '.tar.gz',
    keywords=deeppavlov.__keywords__,
    include_package_data=True,
    extras_require={
            'tests': [
                'pytest',
                'pexpect'],
            'docs': [
                'sphinx>=1.7.9',
                'sphinx_rtd_theme>=0.4.0',
                'nbsphinx>=0.3.4',
                'ipykernel>=4.8.0'
            ]},
    **read_requirements()
)


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import re
from typing import List, Generator, Any

def detokenize(tokens):
    """
    Detokenizing a text undoes the tokenizing operation, restores
    punctuation and spaces to the places that people expect them to be.
    Ideally, `detokenize(tokenize(text))` should be identical to `text`,
    except for line breaks.
    """
    text = ' '.join(tokens)
    step0 = text.replace('. . .', '...')
    step1 = step0.replace("`` ", '"').replace(" ''", '"')
    step2 = step1.replace(" ( ", " (").replace(" ) ", ") ")
    step3 = re.sub(r' ([.,:;?!%]+)([ \'"`])', r"\1\2", step2)
    step4 = re.sub(r' ([.,:;?!%]+)$', r"\1", step3)
    step5 = step4.replace(" '", "'").replace(" n't", "n't") \
        .replace(" nt", "nt").replace("can not", "cannot")
    step6 = step5.replace(" ` ", " '")
    return step6.strip()

def ngramize(items: List[str], ngram_range=(1, 1)) -> Generator[List[str], Any, None]:
    """
    Make ngrams from a list of tokens/lemmas
    :param items: list of tokens, lemmas or other strings to form ngrams
    :param ngram_range: range for producing ngrams, ex. for unigrams + bigrams should be set to
    (1, 2), for bigrams only should be set to (2, 2)
    :return: ngrams (as strings) generator
    """

    ngrams = []
    ranges = [(0, i) for i in range(ngram_range[0], ngram_range[1] + 1)]
    for r in ranges:
        ngrams += list(zip(*[items[j:] for j in range(*r)]))

    formatted_ngrams = [' '.join(item) for item in ngrams]

    yield formatted_ngrams


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import List, Union

import numpy as np
from sklearn.metrics import log_loss

from deeppavlov.core.common.metrics_registry import register_metric

@register_metric('log_loss')
def sk_log_loss(y_true: Union[List[List[float]], List[List[int]], np.ndarray],
                y_predicted: Union[List[List[float]], List[List[int]], np.ndarray]) -> float:
    """
    Calculates log loss.

    Args:
        y_true: list or array of true values
        y_predicted: list or array of predicted values

    Returns:
        Log loss
    """
    return log_loss(y_true, y_predicted)


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import numpy as np
import tensorflow as tf
import tensorflow_hub as hub
import shutil

from deeppavlov.models.elmo.elmo_model import BidirectionalLanguageModel, weight_layers

def make_module_spec(options, weight_file):
    """Makes a module spec.

    Args:
      options: LM hyperparameters.
      weight_file: location of the hdf5 file with LM weights.

    Returns:
      A module spec object used for constructing a TF-Hub module.
    """
    def module_fn():
        """Spec function for a token embedding module."""
        # init
        _bos_id = 256
        _eos_id = 257
        _bow_id = 258
        _eow_id = 259
        _pad_id = 260

        _max_word_length = 50
        _parallel_iterations = 10
        _max_batch_size = 1024

        id_dtype = tf.int32
        id_nptype = np.int32
        max_word_length = tf.constant(_max_word_length, dtype=id_dtype, name='max_word_length')

        version = tf.constant('from_dp_1', dtype=tf.string, name='version')

        # the charcter representation of the begin/end of sentence characters
        def _make_bos_eos(c):
            r = np.zeros([_max_word_length], dtype=id_nptype)
            r[:] = _pad_id
            r[0] = _bow_id
            r[1] = c
            r[2] = _eow_id
            return tf.constant(r, dtype=id_dtype)

        bos_ids = _make_bos_eos(_bos_id)
        eos_ids = _make_bos_eos(_eos_id)

        def token2ids(token):
            with tf.name_scope("token2ids_preprocessor"):
                char_ids = tf.decode_raw(token, tf.uint8, name='decode_raw2get_char_ids')
                char_ids = tf.cast(char_ids, tf.int32, name='cast2int_token')
                char_ids = tf.strided_slice(char_ids, [0], [max_word_length - 2],
                                            [1], name='slice2resized_token')
                ids_num = tf.shape(char_ids)[0]
                fill_ids_num = (_max_word_length - 2) - ids_num
                pads = tf.fill([fill_ids_num], _pad_id)
                bow_token_eow_pads = tf.concat([[_bow_id], char_ids, [_eow_id], pads],
                                               0, name='concat2bow_token_eow_pads')
                return bow_token_eow_pads

        def sentence_tagging_and_padding(sen_dim):
            with tf.name_scope("sentence_tagging_and_padding_preprocessor"):
                sen = sen_dim[0]
                dim = sen_dim[1]
                extra_dim = tf.shape(sen)[0] - dim
                sen = tf.slice(sen, [0, 0], [dim, max_word_length], name='slice2sen')

                bos_sen_eos = tf.concat([[bos_ids], sen, [eos_ids]], 0, name='concat2bos_sen_eos')
                bos_sen_eos_plus_one = bos_sen_eos + 1
                bos_sen_eos_pads = tf.pad(bos_sen_eos_plus_one, [[0, extra_dim], [0, 0]],
                                          "CONSTANT", name='pad2bos_sen_eos_pads')
                return bos_sen_eos_pads

        # Input placeholders to the biLM.
        tokens = tf.placeholder(shape=(None, None), dtype=tf.string, name='ph2tokens')
        sequence_len = tf.placeholder(shape=(None, ), dtype=tf.int32, name='ph2sequence_len')

        tok_shape = tf.shape(tokens)
        line_tokens = tf.reshape(tokens, shape=[-1], name='reshape2line_tokens')

        with tf.device('/cpu:0'):
            tok_ids = tf.map_fn(
                token2ids,
                line_tokens,
                dtype=tf.int32, back_prop=False, parallel_iterations=_parallel_iterations,
                name='map_fn2get_tok_ids')

        tok_ids = tf.reshape(tok_ids, [tok_shape[0], tok_shape[1], -1], name='reshape2tok_ids')
        with tf.device('/cpu:0'):
            sen_ids = tf.map_fn(
                sentence_tagging_and_padding,
                (tok_ids, sequence_len),
                dtype=tf.int32, back_prop=False, parallel_iterations=_parallel_iterations,
                name='map_fn2get_sen_ids')

        # Build the biLM graph.
        bilm = BidirectionalLanguageModel(options, str(weight_file),
                                          max_batch_size=_max_batch_size)

        embeddings_op = bilm(sen_ids)

        # Get an op to compute ELMo (weighted average of the internal biLM layers)
        elmo_output = weight_layers('elmo_output', embeddings_op, l2_coef=0.0)

        weighted_op = elmo_output['weighted_op']
        mean_op = elmo_output['mean_op']
        word_emb = elmo_output['word_emb']
        lstm_outputs1 = elmo_output['lstm_outputs1']
        lstm_outputs2 = elmo_output['lstm_outputs2']

        hub.add_signature("tokens", {"tokens": tokens, "sequence_len": sequence_len},
                          {"elmo": weighted_op,
                           "default": mean_op,
                           "word_emb": word_emb,
                           "lstm_outputs1": lstm_outputs1,
                           "lstm_outputs2": lstm_outputs2,
                           "version": version})

        # #########################Next signature############################# #

        # Input placeholders to the biLM.
        def_strings = tf.placeholder(shape=(None), dtype=tf.string)
        def_tokens_sparse = tf.string_split(def_strings)
        def_tokens_dense = tf.sparse_to_dense(sparse_indices=def_tokens_sparse.indices,
                                              output_shape=def_tokens_sparse.dense_shape,
                                              sparse_values=def_tokens_sparse.values,
                                              default_value=''
                                              )
        def_mask = tf.not_equal(def_tokens_dense, '')
        def_int_mask = tf.cast(def_mask, dtype=tf.int32)
        def_sequence_len = tf.reduce_sum(def_int_mask, axis=-1)

        def_tok_shape = tf.shape(def_tokens_dense)
        def_line_tokens = tf.reshape(def_tokens_dense, shape=[-1], name='reshape2line_tokens')

        with tf.device('/cpu:0'):
            def_tok_ids = tf.map_fn(
                token2ids,
                def_line_tokens,
                dtype=tf.int32, back_prop=False, parallel_iterations=_parallel_iterations,
                name='map_fn2get_tok_ids')

        def_tok_ids = tf.reshape(def_tok_ids, [def_tok_shape[0], def_tok_shape[1], -1], name='reshape2tok_ids')
        with tf.device('/cpu:0'):
            def_sen_ids = tf.map_fn(
                sentence_tagging_and_padding,
                (def_tok_ids, def_sequence_len),
                dtype=tf.int32, back_prop=False, parallel_iterations=_parallel_iterations,
                name='map_fn2get_sen_ids')

        # Get ops to compute the LM embeddings.
        def_embeddings_op = bilm(def_sen_ids)

        # Get an op to compute ELMo (weighted average of the internal biLM layers)
        def_elmo_output = weight_layers('elmo_output', def_embeddings_op, l2_coef=0.0, reuse=True)

        def_weighted_op = def_elmo_output['weighted_op']
        def_mean_op = def_elmo_output['mean_op']
        def_word_emb = def_elmo_output['word_emb']
        def_lstm_outputs1 = def_elmo_output['lstm_outputs1']
        def_lstm_outputs2 = def_elmo_output['lstm_outputs2']

        hub.add_signature("default", {"strings": def_strings},
                          {"elmo": def_weighted_op,
                           "default": def_mean_op,
                           "word_emb": def_word_emb,
                           "lstm_outputs1": def_lstm_outputs1,
                           "lstm_outputs2": def_lstm_outputs2,
                           "version": version})

    return hub.create_module_spec(module_fn)

def export2hub(weight_file, hub_dir, options):
    """Exports a TF-Hub module
    """

    spec = make_module_spec(options, str(weight_file))

    try:
        with tf.Graph().as_default():
            module = hub.Module(spec)

            with tf.Session() as sess:
                sess.run(tf.global_variables_initializer())
                if hub_dir.exists():
                    shutil.rmtree(hub_dir)
                module.export(str(hub_dir), sess)
    finally:
        pass


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
from collections import defaultdict
from logging import getLogger
from typing import List, Dict, Any

from deeppavlov.agents.rich_content.default_rich_content import PlainText, ButtonsFrame, Button
from deeppavlov.core.agent.agent import Agent
from deeppavlov.core.agent.rich_content import RichMessage
from deeppavlov.core.commands.infer import build_model
from deeppavlov.core.skill.skill import Skill
from deeppavlov.deep import find_config
from deeppavlov.utils.ms_bot_framework.server import run_ms_bot_framework_server

parser = argparse.ArgumentParser()
parser.add_argument("-i", "--ms-id", help="microsoft bot framework app id", type=str)
parser.add_argument("-s", "--ms-secret", help="microsoft bot framework app secret", type=str)

log = getLogger(__name__)

class EcommerceAgent(Agent):
    """DeepPavlov Ecommerce agent.

    Args:
        skill: List of initiated agent skills instances.

    Attributes:
        skill: List of initiated agent skills instances.
        history: Histories for each each dialog with agent indexed
            by dialog ID. Each history is represented by list of incoming
            and outcoming replicas of the dialog.
        states: States for each each dialog with agent indexed by dialog ID.
    """

    def __init__(self, skills: List[Skill], *args, **kwargs) -> None:
        super(EcommerceAgent, self).__init__(skills=skills)
        self.states: dict = defaultdict(lambda: [{"start": 0, "stop": 5} for _ in self.skills])

    def _call(self, utterances_batch: List[str], utterances_ids: List[int] = None) -> List[RichMessage]:
        """Processes batch of utterances and returns corresponding responses batch.

        Args:
            utterances_batch: Batch of incoming utterances.
            utterances_ids: Batch of dialog IDs corresponding to incoming utterances.

        Returns:
            responses: A batch of responses corresponding to the
                utterance batch received by agent.
        """

        rich_message = RichMessage()
        for utt_id, utt in enumerate(utterances_batch):

            if utterances_ids:
                id_ = utterances_ids[utt_id]

            log.debug(f'Utterance: {utt}')

            if utt == "/start":
                welcome = "I am a new e-commerce bot. I will help you to find products that you are looking for. Please type your request in plain text."
                rich_message.add_control(PlainText(welcome))
                continue

            if utt[0] == "@":
                command, *parts = utt.split(":")
                log.debug(f'Actions: {parts}')

                if command == "@details":
                    batch_index = int(parts[0])  # batch index in history list
                    item_index = int(parts[1])  # index in batch
                    rich_message.add_control(PlainText(show_details(
                        self.history[id_][batch_index][item_index])))
                    continue

                if command == "@entropy":
                    state = self.history[id_][int(parts[0])]
                    state[parts[1]] = parts[2]
                    state["start"] = 0
                    state["stop"] = 5
                    utt = state['query']
                    self.states[id_] = state

                if command == "@next":
                    state = self.history[id_][int(parts[0])]
                    state['start'] = state['stop']
                    state['stop'] = state['stop'] + 5
                    utt = state['query']
                    self.states[id_] = state
            else:
                if id_ not in self.states:
                    self.states[id_] = {}

                self.states[id_]["start"] = 0
                self.states[id_]["stop"] = 5

            responses_batch, confidences_batch, state_batch = self.skills[0](
                [utt], self.history[id_], [self.states[id_]])

            # update `self.states` with retrieved results
            self.states[id_] = state_batch[0]
            self.states[id_]["query"] = utt

            items_batch, entropy_batch = responses_batch

            for batch_idx, items in enumerate(items_batch):

                self.history[id_].append(items)
                self.history[id_].append(self.states[id_])

                for idx, item in enumerate(items):
                    rich_message.add_control(_draw_item(item, idx, self.history[id_]))

                if len(items) == self.states[id_]['stop'] - self.states[id_]['start']:
                    buttons_frame = _draw_tail(entropy_batch[batch_idx], self.history[id_])
                    rich_message.add_control(buttons_frame)

        return [rich_message]

def _draw_tail(entropy, history):
    buttons_frame = ButtonsFrame(text="")
    buttons_frame.add_button(Button('More', "@next:"+str(len(history)-1)))
    caption = "Press More "

    if entropy:
        caption += "specify a " + entropy[0][1]
        for ent_value in entropy[0][2][:4]:
            button_a = Button(ent_value[0], f'@entropy:{len(history)-1}:{entropy[0][1]}:{ent_value[0]}')
            buttons_frame.add_button(button_a)

    buttons_frame.text = caption
    return buttons_frame

def _draw_item(item, idx, history):
    title = item['Title']
    if 'ListPrice' in item:
        title += " - **$" + item['ListPrice'].split('$')[1]+"**"

    buttons_frame = ButtonsFrame(text=title)
    buttons_frame.add_button(Button('Show details', "@details:"+str(len(history)-2)+":"+str(idx)))
    return buttons_frame

def show_details(item_data: Dict[Any, Any]) -> str:
    """Format catalog item output

    Parameters:
        item_data: item's attributes values

    Returns:
        [rich_message]: list of formatted rich message
    """

    txt = ""

    for key, value in item_data.items():
        txt += "**" + str(key) + "**" + ': ' + str(value) + "  \n"

    return txt

def make_agent() -> EcommerceAgent:
    """Make an agent

    Returns:
        agent: created Ecommerce agent
    """

    config_path = find_config('tfidf_retrieve')
    skill = build_model(config_path)
    agent = EcommerceAgent(skills=[skill])
    return agent

def main():
    """Parse parameters and run ms bot framework"""

    args = parser.parse_args()
    run_ms_bot_framework_server(agent_generator=make_agent,
                                app_id=args.ms_id,
                                app_secret=args.ms_secret,
                                stateful=True)

if __name__ == '__main__':
    main()


import keras.activations as kact
import keras.backend as kb
import keras.initializers as kinit
import keras.layers as kl
import numpy as np
from keras.engine.topology import InputSpec

INFTY = -100

class Highway(kl.Layer):

    def __init__(self, activation=None, bias_initializer=-1, **kwargs):
        super(Highway, self).__init__(**kwargs)
        self.activation = kact.get(activation)
        self.bias_initializer = bias_initializer
        if isinstance(self.bias_initializer, int):
            self.bias_initializer = kinit.constant(self.bias_initializer)
        self.input_spec = [InputSpec(min_ndim=2)]

    def build(self, input_shape):
        assert len(input_shape) >= 2
        input_dim = input_shape[-1]

        self.gate_kernel = self.add_weight(
            shape=(input_dim, input_dim), initializer='uniform', name='gate_kernel')
        self.gate_bias = self.add_weight(
            shape=(input_dim,), initializer=self.bias_initializer, name='gate_bias')
        self.dense_kernel = self.add_weight(
            shape=(input_dim, input_dim), initializer='uniform', name='dense_kernel')
        self.dense_bias = self.add_weight(
            shape=(input_dim,), initializer=self.bias_initializer, name='dense_bias')
        self.input_spec = InputSpec(min_ndim=2, axes={-1: input_dim})
        self.built = True

    def call(self, inputs, **kwargs):
        gate = kb.dot(inputs, self.gate_kernel)
        gate = kb.bias_add(gate, self.gate_bias, data_format="channels_last")
        gate = self.activation(gate)
        new_value = kb.dot(inputs, self.dense_kernel)
        new_value = kb.bias_add(new_value, self.dense_bias, data_format="channels_last")
        return gate * new_value + (1.0 - gate) * inputs

    def compute_output_shape(self, input_shape):
        return input_shape

def weighted_sum(first, second, sigma, first_threshold=-np.inf, second_threshold=np.inf):
    logit_probs = first * sigma + second * (1.0 - sigma)
    infty_tensor = kb.ones_like(logit_probs) * INFTY
    logit_probs = kb.switch(kb.greater(first, first_threshold), logit_probs, infty_tensor)
    logit_probs = kb.switch(kb.greater(second, second_threshold), logit_probs, infty_tensor)
    return logit_probs

class WeightedCombinationLayer(kl.Layer):

    """
    A class for weighted combination of probability distributions
    """

    def __init__(self, first_threshold=None, second_threshold=None,
                 use_dimension_bias=False, use_intermediate_layer=False,
                 intermediate_dim=64, intermediate_activation=None,
                 from_logits=False, return_logits=False,
                 bias_initializer=1.0, **kwargs):
        # if 'input_shape' not in kwargs:
        #     kwargs['input_shape'] = [(None, input_dim,), (None, input_dim)]
        super(WeightedCombinationLayer, self).__init__(**kwargs)
        self.first_threshold = first_threshold if first_threshold is not None else INFTY
        self.second_threshold = second_threshold if second_threshold is not None else INFTY
        self.use_dimension_bias = use_dimension_bias
        self.use_intermediate_layer = use_intermediate_layer
        self.intermediate_dim = intermediate_dim
        self.intermediate_activation = kact.get(intermediate_activation)
        self.from_logits = from_logits
        self.return_logits = return_logits
        self.bias_initializer = bias_initializer
        self.input_spec = [InputSpec(), InputSpec(), InputSpec()]

    def build(self, input_shape):
        assert len(input_shape) == 3
        assert input_shape[0] == input_shape[1]
        assert input_shape[0][:-1] == input_shape[2][:-1]

        input_dim, features_dim = input_shape[0][-1], input_shape[2][-1]
        if self.use_intermediate_layer:
            self.first_kernel = self.add_weight(
                shape=(features_dim, self.intermediate_dim),
                initializer="random_uniform", name='first_kernel')
            self.first_bias = self.add_weight(
                shape=(self.intermediate_dim,),
                initializer="random_uniform", name='first_bias')
        self.features_kernel = self.add_weight(
            shape=(features_dim, 1), initializer="random_uniform", name='kernel')
        self.features_bias = self.add_weight(
            shape=(1,), initializer=kinit.Constant(self.bias_initializer), name='bias')
        if self.use_dimension_bias:
            self.dimensions_bias = self.add_weight(
                shape=(input_dim,), initializer="random_uniform", name='dimension_bias')
        super(WeightedCombinationLayer, self).build(input_shape)

    def call(self, inputs, **kwargs):
        assert isinstance(inputs, list) and len(inputs) == 3
        first, second, features = inputs[0], inputs[1], inputs[2]
        if not self.from_logits:
            first = kb.clip(first, 1e-10, 1.0)
            second = kb.clip(second, 1e-10, 1.0)
            first_, second_ = kb.log(first), kb.log(second)
        else:
            first_, second_ = first, second
        # embedded_features.shape = (M, T, 1)
        if self.use_intermediate_layer:
            features = kb.dot(features, self.first_kernel)
            features = kb.bias_add(features, self.first_bias, data_format="channels_last")
            features = self.intermediate_activation(features)
        embedded_features = kb.dot(features, self.features_kernel)
        embedded_features = kb.bias_add(
            embedded_features, self.features_bias, data_format="channels_last")
        if self.use_dimension_bias:
            tiling_shape = [1] * (kb.ndim(first)-1) + [kb.shape(first)[-1]]
            embedded_features = kb.tile(embedded_features, tiling_shape)
            embedded_features = kb.bias_add(
                embedded_features, self.dimensions_bias, data_format="channels_last")
        sigma = kb.sigmoid(embedded_features)

        result = weighted_sum(first_, second_, sigma,
                              self.first_threshold, self.second_threshold)
        probs = kb.softmax(result)
        if self.return_logits:
            return [probs, result]
        return probs

    def compute_output_shape(self, input_shape):
        first_shape = input_shape[0]
        if self.return_logits:
            return [first_shape, first_shape]
        return first_shape

def TemporalDropout(inputs, dropout=0.0):
    """
    Drops with :dropout probability temporal steps of input 3D tensor
    """
    # TO DO: adapt for >3D tensors
    if dropout == 0.0:
        return inputs
    inputs_func = lambda x: kb.ones_like(inputs[:, :, 0:1])
    inputs_mask = kl.Lambda(inputs_func)(inputs)
    inputs_mask = kl.Dropout(dropout)(inputs_mask)
    tiling_shape = [1, 1, kb.shape(inputs)[2]] + [1] * (kb.ndim(inputs) - 3)
    inputs_mask = kl.Lambda(kb.tile, arguments={"n": tiling_shape},
                            output_shape=inputs._keras_shape[1:])(inputs_mask)
    answer = kl.Multiply()([inputs, inputs_mask])
    return answer

def positions_func(inputs, pad=0):
    """
    A layer filling i-th column of a 2D tensor with
    1+ln(1+i) when it contains a meaningful symbol
    and with 0 when it contains PAD
    """
    position_inputs = kb.cumsum(kb.ones_like(inputs, dtype="float32"), axis=1)
    position_inputs *= kb.cast(kb.not_equal(inputs, pad), "float32")
    return kb.log(1.0 + position_inputs)


"""
Copyright 2017 Neural Networks and Deep Learning lab, MIPT

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import collections
import gzip
import os
import re
import secrets
import shutil
import tarfile
import zipfile
from hashlib import md5
from itertools import chain
from logging import getLogger
from pathlib import Path
from typing import List, Union, Iterable, Optional, Sized, Sequence
from urllib.parse import urlencode, parse_qs, urlsplit, urlunsplit, urlparse

import numpy as np
import requests
from tqdm import tqdm

log = getLogger(__name__)

_MARK_DONE = '.done'

tqdm.monitor_interval = 0

def get_download_token():
    token_file = Path.home() / '.deeppavlov' / 'token'
    if not token_file.exists():
        if token_file.parent.is_file():
            token_file.parent.unlink()
        token_file.parent.mkdir(parents=True, exist_ok=True)
        token_file.write_text(secrets.token_urlsafe(32), encoding='utf8')

    return token_file.read_text(encoding='utf8').strip()

def simple_download(url: str, destination: [Path, str]):
    CHUNK = 32 * 1024

    destination = Path(destination)
    destination.parent.mkdir(parents=True, exist_ok=True)

    headers = {'dp-token': get_download_token()}
    r = requests.get(url, stream=True, headers=headers)
    total_length = int(r.headers.get('content-length', 0))

    log.info('Downloading from {} to {}'.format(url, destination))
    with destination.open('wb') as f, tqdm(total=total_length, unit='B', unit_scale=True) as pbar:
        done = False
        downloaded = 0
        while not done:
            for chunk in r.iter_content(chunk_size=CHUNK):
                if chunk:  # filter out keep-alive new chunks
                    downloaded += len(chunk)
                    pbar.update(len(chunk))
                    f.write(chunk)
            if downloaded < total_length:
                log.warn(f'Download stopped abruptly, trying to resume from {downloaded} to reach {total_length}')
                headers['Range'] = f'bytes={downloaded}-'
                r = requests.get(url, headers=headers, stream=True)
                if total_length - downloaded != int(r.headers['content-length']):
                    raise RuntimeError('It looks like the server does not support resuming downloads')
            else:
                done = True

def download(dest_file_path: [List[Union[str, Path]]], source_url: str, force_download=True):
    """Download a file from URL to one or several target locations

    Args:
        dest_file_path: path or list of paths to the file destination files (including file name)
        source_url: the source URL
        force_download: download file if it already exists, or not

    """

    if isinstance(dest_file_path, list):
        dest_file_paths = [Path(path) for path in dest_file_path]
    else:
        dest_file_paths = [Path(dest_file_path).absolute()]

    if not force_download:
        to_check = list(dest_file_paths)
        dest_file_paths = []
        for p in to_check:
            if p.exists():
                log.info(f'File already exists in {p}')
            else:
                dest_file_paths.append(p)

    if dest_file_paths:
        cache_dir = os.getenv('DP_CACHE_DIR')
        cached_exists = False
        if cache_dir:
            first_dest_path = Path(cache_dir) / md5(source_url.encode('utf8')).hexdigest()[:15]
            cached_exists = first_dest_path.exists()
        else:
            first_dest_path = dest_file_paths.pop()

        if not cached_exists:
            first_dest_path.parent.mkdir(parents=True, exist_ok=True)

            simple_download(source_url, first_dest_path)
        else:
            log.info(f'Found cached {source_url} in {first_dest_path}')

        for dest_path in dest_file_paths:
            dest_path.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy(str(first_dest_path), str(dest_path))

def untar(file_path, extract_folder=None):
    """Simple tar archive extractor

    Args:
        file_path: path to the tar file to be extracted
        extract_folder: folder to which the files will be extracted

    """
    file_path = Path(file_path)
    if extract_folder is None:
        extract_folder = file_path.parent
    extract_folder = Path(extract_folder)
    tar = tarfile.open(file_path)
    tar.extractall(extract_folder)
    tar.close()

def ungzip(file_path, extract_path: Path = None):
    """Simple .gz archive extractor

        Args:
            file_path: path to the gzip file to be extracted
            extract_path: path where the file will be extracted

        """
    CHUNK = 16 * 1024
    file_path = Path(file_path)
    extract_path = extract_path or file_path.with_suffix('')

    with gzip.open(file_path, 'rb') as fin, extract_path.open('wb') as fout:
        while True:
            block = fin.read(CHUNK)
            if not block:
                break
            fout.write(block)

def download_decompress(url: str, download_path: [Path, str], extract_paths=None):
    """Download and extract .tar.gz or .gz file to one or several target locations.
    The archive is deleted if extraction was successful.

    Args:
        url: URL for file downloading
        download_path: path to the directory where downloaded file will be stored
        until the end of extraction
        extract_paths: path or list of paths where contents of archive will be extracted
    """
    file_name = Path(urlparse(url).path).name
    download_path = Path(download_path)

    if extract_paths is None:
        extract_paths = [download_path]
    elif isinstance(extract_paths, list):
        extract_paths = [Path(path) for path in extract_paths]
    else:
        extract_paths = [Path(extract_paths)]

    cache_dir = os.getenv('DP_CACHE_DIR')
    extracted = False
    if cache_dir:
        cache_dir = Path(cache_dir)
        url_hash = md5(url.encode('utf8')).hexdigest()[:15]
        arch_file_path = cache_dir / url_hash
        extracted_path = cache_dir / (url_hash + '_extracted')
        extracted = extracted_path.exists()
        if not extracted and not arch_file_path.exists():
            simple_download(url, arch_file_path)
    else:
        arch_file_path = download_path / file_name
        simple_download(url, arch_file_path)
        extracted_path = extract_paths.pop()

    if not extracted:
        log.info('Extracting {} archive into {}'.format(arch_file_path, extracted_path))
        extracted_path.mkdir(parents=True, exist_ok=True)

        if file_name.endswith('.tar.gz'):
            untar(arch_file_path, extracted_path)
        elif file_name.endswith('.gz'):
            ungzip(arch_file_path, extracted_path / Path(file_name).with_suffix('').name)
        elif file_name.endswith('.zip'):
            with zipfile.ZipFile(arch_file_path, 'r') as zip_ref:
                zip_ref.extractall(extracted_path)
        else:
            raise RuntimeError(f'Trying to extract an unknown type of archive {file_name}')

        if not cache_dir:
            arch_file_path.unlink()

    for extract_path in extract_paths:
        for src in extracted_path.iterdir():
            dest = extract_path / src.name
            if src.is_dir():
                copytree(src, dest)
            else:
                extract_path.mkdir(parents=True, exist_ok=True)
                shutil.copy(str(src), str(dest))

def copytree(src: Path, dest: Path):
    dest.mkdir(parents=True, exist_ok=True)
    for f in src.iterdir():
        f_dest = dest / f.name
        if f.is_dir():
            copytree(f, f_dest)
        else:
            shutil.copy(str(f), str(f_dest))

def file_md5(fpath: Union[str, Path], chunk_size: int = 2**16) -> Optional[str]:
    fpath = Path(fpath)
    if not fpath.is_file():
        return None
    file_hash = md5()
    with fpath.open('rb') as f:
        for chunk in iter(lambda: f.read(chunk_size), b""):
            file_hash.update(chunk)
    return file_hash.hexdigest()

def load_vocab(vocab_path):
    vocab_path = Path(vocab_path)
    with vocab_path.open(encoding='utf8') as f:
        return f.read().split()

def mark_done(path):
    mark = Path(path) / _MARK_DONE
    mark.touch(exist_ok=True)

def is_done(path):
    mark = Path(path) / _MARK_DONE
    return mark.is_file()

def tokenize_reg(s):
    pattern = "[\w]+|[‑–—“”€№…’\"#$%&\'()+,-./:;<>?]"
    return re.findall(re.compile(pattern), s)

def get_all_dimensions(batch: Sequence, level: int = 0, res: Optional[List[List[int]]] = None) -> List[List[int]]:
    if not level:
        res = [[len(batch)]]
    if len(batch) and isinstance(batch[0], Sized) and not isinstance(batch[0], str):
        level += 1
        if len(res) <= level:
            res.append([])
        for item in batch:
            res[level].append(len(item))
            get_all_dimensions(item, level, res)
    return res

def get_dimensions(batch) -> List[int]:
    """"""
    return list(map(max, get_all_dimensions(batch)))

def zero_pad(batch, zp_batch=None, dtype=np.float32, padding=0):
    if zp_batch is None:
        dims = get_dimensions(batch)
        zp_batch = np.ones(dims, dtype=dtype) * padding
    if zp_batch.ndim == 1:
        zp_batch[:len(batch)] = batch
    else:
        for b, zp in zip(batch, zp_batch):
            zero_pad(b, zp)
    return zp_batch

def is_str_batch(batch):
    while True:
        if isinstance(batch, Iterable):
            if isinstance(batch, str):
                return True
            elif isinstance(batch, np.ndarray):
                return batch.dtype.kind == 'U'
            else:
                if len(batch) > 0:
                    batch = batch[0]
                else:
                    return True
        else:
            return False

def flatten_str_batch(batch):
    if isinstance(batch, str):
        return [batch]
    else:
        return chain(*[flatten_str_batch(sample) for sample in batch])

def zero_pad_truncate(batch, max_len, pad='post', trunc='post', dtype=np.float32):
    batch_size = len(batch)
    if isinstance(batch[0][0], (int, np.int)):
        padded_batch = np.zeros([batch_size, max_len], dtype=np.int32)
        for n, utterance in enumerate(batch):
            if len(utterance) > max_len:
                if trunc == 'post':
                    padded_batch[n, :] = utterance[:max_len]
                elif trunc == 'pre':
                    padded_batch[n, :] = utterance[-max_len:]
            else:
                if pad == 'post':
                    padded_batch[n, :len(utterance)] = utterance
                elif pad == 'pre':
                    padded_batch[n, -len(utterance):] = utterance
    else:
        n_features = len(batch[0][0])
        padded_batch = np.zeros([batch_size, max_len, n_features], dtype=dtype)
        for n, utterance in enumerate(batch):
            if len(utterance) > max_len:
                if trunc == 'post':
                    for k, token_features in enumerate(utterance[:max_len]):
                        padded_batch[n, k] = token_features
                elif trunc == 'pre':
                    for k, token_features in enumerate(utterance[-max_len:]):
                        padded_batch[n, k] = token_features
            else:
                if pad == 'post':
                    for k, token_features in enumerate(utterance):
                        padded_batch[n, k] = token_features
                elif pad == 'pre':
                    for k, token_features in enumerate(utterance):
                        padded_batch[n, k + max_len - len(utterance)] = token_features
    return padded_batch

def zero_pad_char(batch, dtype=np.float32):
    if len(batch) == 1 and len(batch[0]) == 0:
        return np.array([], dtype=dtype)
    batch_size = len(batch)
    max_len = max(len(utterance) for utterance in batch)
    max_token_len = max(len(ch) for token in batch for ch in token)
    if isinstance(batch[0][0][0], (int, np.int)):
        padded_batch = np.zeros([batch_size, max_len, max_token_len], dtype=np.int32)
        for n, utterance in enumerate(batch):
            for k, token in enumerate(utterance):
                padded_batch[n, k, :len(token)] = token
    else:
        n_features = len(batch[0][0][0])
        padded_batch = np.zeros([batch_size, max_len, max_token_len, n_features], dtype=dtype)
        for n, utterance in enumerate(batch):
            for k, token in enumerate(utterance):
                for q, char_features in enumerate(token):
                    padded_batch[n, k, q] = char_features
    return padded_batch

def get_all_elems_from_json(search_json, search_key):
    result = []
    if isinstance(search_json, dict):
        for key in search_json:
            if key == search_key:
                result.append(search_json[key])
            else:
                result.extend(get_all_elems_from_json(search_json[key], search_key))
    elif isinstance(search_json, list):
        for item in search_json:
            result.extend(get_all_elems_from_json(item, search_key))

    return result

def check_nested_dict_keys(check_dict: dict, keys: list):
    if isinstance(keys, list) and len(keys) > 0:
        element = check_dict
        for key in keys:
            if isinstance(element, dict) and key in element.keys():
                element = element[key]
            else:
                return False
        return True
    else:
        return False

def jsonify_data(data):
    if isinstance(data, (list, tuple)):
        result = [jsonify_data(item) for item in data]
    elif isinstance(data, dict):
        result = {}
        for key in data.keys():
            result[key] = jsonify_data(data[key])
    elif isinstance(data, np.ndarray):
        result = data.tolist()
    elif isinstance(data, (np.int_, np.intc, np.intp, np.int8, np.int16, np.int32,
                           np.int64, np.uint8, np.uint16, np.uint32, np.uint64)):
        result = int(data)
    elif isinstance(data, (np.float_, np.float16, np.float32, np.float64)):
        result = float(data)
    else:
        result = data
    return result

def chunk_generator(items_list, chunk_size):
    for i in range(0, len(items_list), chunk_size):
        yield items_list[i:i + chunk_size]

def update_dict_recursive(editable_dict: dict, editing_dict: dict) -> None:
    """Updates dict recursively

    You need to use this function to update dictionary if depth of editing_dict is more then 1

    Args:
        editable_dict: dictionary, that will be edited
        editing_dict: dictionary, that contains edits
    Returns:
        None
    """
    for k, v in editing_dict.items():
        if isinstance(v, collections.Mapping):
            update_dict_recursive(editable_dict.get(k, {}), v)
        else:
            editable_dict[k] = v

def path_set_md5(url):
    """Given a file URL, return a md5 query of the file

    Args:
        url: a given URL
    Returns:
        URL of the md5 file
    """
    scheme, netloc, path, query_string, fragment = urlsplit(url)
    path += '.md5'

    return urlunsplit((scheme, netloc, path, query_string, fragment))

def set_query_parameter(url, param_name, param_value):
    """Given a URL, set or replace a query parameter and return the modified URL.

    Args:
        url: a given  URL
        param_name: the parameter name to add
        param_value: the parameter value
    Returns:
        URL with the added parameter

    """
    scheme, netloc, path, query_string, fragment = urlsplit(url)
    query_params = parse_qs(query_string)

    query_params[param_name] = [param_value]
    new_query_string = urlencode(query_params, doseq=True)

    return urlunsplit((scheme, netloc, path, new_query_string, fragment))


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Optional

from deeppavlov.core.agent.rich_content import RichControl

class PlainText(RichControl):
    """Plain text message as a rich control.

    Args:
        text: Text of the message.

    Attributes:
        content: Text of the message.
    """
    def __init__(self, text: str) -> None:
        super(PlainText, self).__init__('plain_text')
        self.content: str = text

    def __str__(self) -> str:
        return self.content

    def json(self) -> dict:
        """Returns json compatible state of the PlainText instance.

        Returns:
            control_json: Json representation of PlainText state.
        """
        self.control_json['content'] = self.content
        return self.control_json

    def ms_bot_framework(self)-> dict:
        """Returns MS Bot Framework compatible state of the PlainText instance.

        Creating MS Bot Framework activity blank with "text" field populated.

        Returns:
            out_activity: MS Bot Framework representation of PlainText state.
        """
        out_activity = {}
        out_activity['type'] = 'message'
        out_activity['text'] = self.content
        return out_activity

    def alexa(self) -> dict:
        """Returns Amazon Alexa compatible state of the PlainText instance.

        Creating Amazon Alexa response blank with populated "outputSpeech" and
        "card sections.

        Returns:
            response: Amazon Alexa representation of PlainText state.
        """
        response = {
            'response': {
                'shouldEndSession': False,
                'outputSpeech': {
                    'type': 'PlainText',
                    'text': self.content},
                'card': {
                    'type': 'Simple',
                    'content': self.content
                }
            }
        }

        return response

class Button(RichControl):
    """Button with plain text callback.

    Args:
        name: Displayed name of the button.
        callback: Plain text returned as callback when button pressed.

    Attributes:
        name: Displayed name of the button.
        callback: Plain text returned as callback when button pressed.
    """
    def __init__(self, name: str, callback: str) -> None:
        super(Button, self).__init__('button')
        self.name: str = name
        self.callback: str = callback

    def json(self) -> dict:
        """Returns json compatible state of the Button instance.

        Returns:
            control_json: Json representation of Button state.
        """
        content = {}
        content['name'] = self.name
        content['callback'] = self.callback
        self.control_json['content'] = content
        return self.control_json

    def ms_bot_framework(self) -> dict:
        """Returns MS Bot Framework compatible state of the Button instance.

        Creates MS Bot Framework CardAction (button) with postBack value return.

        Returns:
            control_json: MS Bot Framework representation of Button state.
        """
        card_action = {}
        card_action['type'] = 'postBack'
        card_action['title'] = self.name
        card_action['value'] = self.callback = self.callback
        return card_action

class ButtonsFrame(RichControl):
    """ButtonsFrame is a container for several Buttons objects.

    ButtonsFrame embeds several Buttons and allows to post them
    in one channel message.

    Args:
        text: Text displayed with embedded buttons.

    Attributes:
        text: Text displayed with embedded buttons.
        content: Container with Button objects.
    """
    def __init__(self, text: Optional[str]=None) -> None:
        super(ButtonsFrame, self).__init__('buttons_frame')
        self.text: [str, None] = text
        self.content: list = []

    def add_button(self, button: Button):
        """Adds Button instance to RichMessage.

        Args:
            button: Button instance.
        """
        self.content.append(button)

    def json(self) -> dict:
        """Returns json compatible state of the ButtonsFrame instance.

        Returns json compatible state of the ButtonsFrame instance including
        all nested buttons.

        Returns:
            control_json: Json representation of ButtonsFrame state.
        """
        content = {}

        if self.text:
            content['text'] = self.text

        content['controls'] = [control.json() for control in self.content]

        self.control_json['content'] = content

        return self.control_json

    def ms_bot_framework(self) -> dict:
        """Returns MS Bot Framework compatible state of the ButtonsFrame instance.

        Creating MS Bot Framework activity blank with RichCard in "attachments". RichCard
        is populated with CardActions corresponding buttons embedded in ButtonsFrame.

        Returns:
            control_json: MS Bot Framework representation of ButtonsFrame state.
        """
        rich_card = {}

        buttons = [button.ms_bot_framework() for button in self.content]
        rich_card['buttons'] = buttons

        if self.text:
            rich_card['title'] = self.text

        attachments = [
            {
                "contentType": "application/vnd.microsoft.card.thumbnail",
                "content": rich_card
            }
        ]

        out_activity = {}
        out_activity['type'] = 'message'
        out_activity['attachments'] = attachments

        return out_activity


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import re
import string
from collections import Counter
from typing import List

from deeppavlov.core.common.metrics_registry import register_metric

@register_metric('squad_v2_em')
def squad_v2_exact_match(y_true: List[List[str]], y_predicted: List[str]) -> float:
    """ Calculates Exact Match score between y_true and y_predicted
        EM score uses the best matching y_true answer:
            if y_pred equal at least to one answer in y_true then EM = 1, else EM = 0

    The same as in SQuAD-v2.0

    Args:
        y_true: list of correct answers (correct answers are represented by list of strings)
        y_predicted: list of predicted answers

    Returns:
        exact match score : float
    """
    EM_total = sum(normalize_answer(prediction) in map(normalize_answer, ground_truth)
                   for ground_truth, prediction in zip(y_true, y_predicted))
    return 100 * EM_total / len(y_true) if len(y_true) > 0 else 0

@register_metric('squad_v1_em')
def squad_v1_exact_match(y_true: List[List[str]], y_predicted: List[str]) -> float:
    """ Calculates Exact Match score between y_true and y_predicted
        EM score uses the best matching y_true answer:
            if y_pred equal at least to one answer in y_true then EM = 1, else EM = 0
        Skips examples without an answer.
    Args:
        y_true: list of correct answers (correct answers are represented by list of strings)
        y_predicted: list of predicted answers
    Returns:
        exact match score : float
    """
    EM_total = 0
    count = 0
    for ground_truth, prediction in zip(y_true, y_predicted):
        if len(ground_truth[0]) == 0:
            # skip empty answers
            continue
        count += 1
        EMs = [int(normalize_answer(gt) == normalize_answer(prediction)) for gt in ground_truth]
        EM_total += max(EMs)
    return 100 * EM_total / count if count > 0 else 0

@register_metric('squad_v2_f1')
def squad_v2_f1(y_true: List[List[str]], y_predicted: List[str]) -> float:
    """ Calculates F-1 score between y_true and y_predicted
        F-1 score uses the best matching y_true answer

    The same as in SQuAD-v2.0

    Args:
        y_true: list of correct answers (correct answers are represented by list of strings)
        y_predicted: list of predicted answers

    Returns:
        F-1 score : float
    """
    f1_total = 0.0
    for ground_truth, prediction in zip(y_true, y_predicted):
        prediction_tokens = normalize_answer(prediction).split()
        f1s = []
        for gt in ground_truth:
            gt_tokens = normalize_answer(gt).split()
            if len(gt_tokens) == 0 or len(prediction_tokens) == 0:
                f1s.append(float(gt_tokens == prediction_tokens))
                continue
            common = Counter(prediction_tokens) & Counter(gt_tokens)
            num_same = sum(common.values())
            if num_same == 0:
                f1s.append(0.0)
                continue
            precision = 1.0 * num_same / len(prediction_tokens)
            recall = 1.0 * num_same / len(gt_tokens)
            f1 = (2 * precision * recall) / (precision + recall)
            f1s.append(f1)
        f1_total += max(f1s)
    return 100 * f1_total / len(y_true) if len(y_true) > 0 else 0

@register_metric('squad_v1_f1')
def squad_v1_f1(y_true: List[List[str]], y_predicted: List[str]) -> float:
    """ Calculates F-1 score between y_true and y_predicted
        F-1 score uses the best matching y_true answer

        Skips examples without an answer.
    Args:
        y_true: list of correct answers (correct answers are represented by list of strings)
        y_predicted: list of predicted answers
    Returns:
        F-1 score : float
    """
    f1_total = 0.0
    count = 0
    for ground_truth, prediction in zip(y_true, y_predicted):
        if len(ground_truth[0]) == 0:
            # skip empty answers
            continue
        count += 1
        prediction_tokens = normalize_answer(prediction).split()
        f1s = []
        for gt in ground_truth:
            gt_tokens = normalize_answer(gt).split()
            common = Counter(prediction_tokens) & Counter(gt_tokens)
            num_same = sum(common.values())
            if num_same == 0:
                f1s.append(0.0)
                continue
            precision = 1.0 * num_same / len(prediction_tokens)
            recall = 1.0 * num_same / len(gt_tokens)
            f1 = (2 * precision * recall) / (precision + recall)
            f1s.append(f1)
        f1_total += max(f1s)
    return 100 * f1_total / count if count > 0 else 0

def normalize_answer(s: str) -> str:
    def remove_articles(text):
        return re.sub(r'\b(a|an|the)\b', ' ', text)

    def white_space_fix(text):
        return ' '.join(text.split())

    def remove_punc(text):
        exclude = set(string.punctuation)
        return ''.join(ch for ch in text if ch not in exclude)

    def lower(text):
        return text.lower()

    return white_space_fix(remove_articles(remove_punc(lower(s))))


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import List

import numpy as np

from deeppavlov.core.common.metrics_registry import register_metric

def recall_at_k(y_true: List[int], y_pred: List[List[np.ndarray]], k: int):
    """
    Calculates recall at k ranking metric.

    Args:
        y_true: Labels. Not used in the calculation of the metric.
        y_predicted: Predictions.
            Each prediction contains ranking score of all ranking candidates for the particular data sample.
            It is supposed that the ranking score for the true candidate goes first in the prediction.

    Returns:
        Recall at k
    """
    num_examples = float(len(y_pred))
    predictions = np.array(y_pred)
    predictions = np.flip(np.argsort(predictions, -1), -1)[:, :k]
    num_correct = 0
    for el in predictions:
        if 0 in el:
            num_correct += 1
    return float(num_correct) / num_examples

@register_metric('r@1')
def r_at_1(y_true, y_pred):
    return recall_at_k(y_true, y_pred, k=1)

@register_metric('r@2')
def r_at_2(y_true, y_pred):
    return recall_at_k(y_true, y_pred, k=2)

@register_metric('r@5')
def r_at_5(labels, predictions):
    return recall_at_k(labels, predictions, k=5)

@register_metric('r@10')
def r_at_10(labels, predictions):
    return recall_at_k(labels, predictions, k=10)


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from logging import getLogger

import tensorflow as tf
from tensorflow.python.client import device_lib

log = getLogger(__name__)

_gpu_available = None

def check_gpu_existence():
    r"""Return True if at least one GPU is available"""
    global _gpu_available
    if _gpu_available is None:
        sess_config = tf.ConfigProto()
        sess_config.gpu_options.allow_growth = True
        try:
            with tf.Session(config=sess_config):
                device_list = device_lib.list_local_devices()
                _gpu_available = any(device.device_type == 'GPU' for device in device_list)
        except AttributeError as e:
            log.warning(f'Got an AttributeError `{e}`, assuming documentation building')
            _gpu_available = False
    return _gpu_available


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from pathlib import Path
from typing import Union, Dict, TypeVar

from deeppavlov.core.common.file import read_json, find_config

# noinspection PyShadowingBuiltins
_T = TypeVar('_T', str, float, bool, list, dict)

def _parse_config_property(item: _T, variables: Dict[str, Union[str, Path, float, bool, None]]) -> _T:
    """Recursively apply config's variables values to its property"""
    if isinstance(item, str):
        return item.format(**variables)
    elif isinstance(item, list):
        return [_parse_config_property(item, variables) for item in item]
    elif isinstance(item, dict):
        return {k: _parse_config_property(v, variables) for k, v in item.items()}
    else:
        return item

def parse_config(config: Union[str, Path, dict]) -> dict:
    """Read config's variables and apply their values to all its properties"""
    if isinstance(config, (str, Path)):
        config = read_json(find_config(config))

    variables = {
        'DEEPPAVLOV_PATH': os.getenv(f'DP_DEEPPAVLOV_PATH', Path(__file__).parent.parent.parent)
    }
    for name, value in config.get('metadata', {}).get('variables', {}).items():
        env_name = f'DP_{name}'
        if env_name in os.environ:
            value = os.getenv(env_name)
        variables[name] = value.format(**variables)

    return _parse_config_property(config, variables)

def expand_path(path: Union[str, Path]) -> Path:
    """Convert relative paths to absolute with resolving user directory."""
    return Path(path).expanduser().resolve()

def import_packages(packages: list) -> None:
    """Import packages from list to execute their code."""
    for package in packages:
        __import__(package)


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import inspect
from logging import getLogger
from typing import Dict, Any

from deeppavlov.core.commands.utils import expand_path, parse_config
from deeppavlov.core.common.errors import ConfigError
from deeppavlov.core.common.registry import get_model
from deeppavlov.core.models.component import Component

log = getLogger(__name__)

_refs = {}

def _resolve(val):
    if isinstance(val, str) and val.startswith('#'):
        component_id, *attributes = val[1:].split('.')
        try:
            val = _refs[component_id]
        except KeyError:
            e = ConfigError('Component with id "{id}" was referenced but not initialized'
                            .format(id=component_id))
            log.exception(e)
            raise e
        attributes = ['val'] + attributes
        val = eval('.'.join(attributes))
    return val

def _init_param(param, mode):
    if isinstance(param, str):
        param = _resolve(param)
    elif isinstance(param, (list, tuple)):
        param = [_init_param(p, mode) for p in param]
    elif isinstance(param, dict):
        if {'ref', 'class_name', 'config_path'}.intersection(param.keys()):
            param = from_params(param, mode=mode)
        else:
            param = {k: _init_param(v, mode) for k, v in param.items()}
    return param

def from_params(params: Dict, mode: str = 'infer', serialized: Any = None, **kwargs) -> Component:
    """Builds and returns the Component from corresponding dictionary of parameters."""
    # what is passed in json:
    config_params = {k: _resolve(v) for k, v in params.items()}

    # get component by reference (if any)
    if 'ref' in config_params:
        try:
            component = _refs[config_params['ref']]
            if serialized is not None:
                component.deserialize(serialized)
            return component
        except KeyError:
            e = ConfigError('Component with id "{id}" was referenced but not initialized'
                            .format(id=config_params['ref']))
            log.exception(e)
            raise e

    elif 'config_path' in config_params:
        from deeppavlov.core.commands.infer import build_model
        refs = _refs.copy()
        _refs.clear()
        config = parse_config(expand_path(config_params['config_path']))
        model = build_model(config, serialized=serialized)
        _refs.clear()
        _refs.update(refs)
        try:
            _refs[config_params['id']] = model
        except KeyError:
            pass
        return model

    cls_name = config_params.pop('class_name', None)
    if not cls_name:
        e = ConfigError('Component config has no `class_name` nor `ref` fields')
        log.exception(e)
        raise e
    cls = get_model(cls_name)

    # find the submodels params recursively
    config_params = {k: _init_param(v, mode) for k, v in config_params.items()}

    try:
        spec = inspect.getfullargspec(cls)
        if 'mode' in spec.args+spec.kwonlyargs or spec.varkw is not None:
            kwargs['mode'] = mode

        component = cls(**dict(config_params, **kwargs))
        try:
            _refs[config_params['id']] = component
        except KeyError:
            pass
    except Exception:
        log.exception("Exception in {}".format(cls))
        raise

    if serialized is not None:
        component.deserialize(serialized)
    return component


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from collections import namedtuple
from datetime import timedelta, datetime
from logging import getLogger
from queue import Queue
from threading import Timer, Thread
from typing import Optional, Dict

from OpenSSL.crypto import X509

from deeppavlov.agents.default_agent.default_agent import DefaultAgent
from deeppavlov.utils.alexa.conversation import Conversation
from deeppavlov.utils.alexa.ssl_tools import verify_cert, verify_signature

REQUEST_TIMESTAMP_TOLERANCE_SECS = 150
REFRESH_VALID_CERTS_PERIOD_SECS = 120

log = getLogger(__name__)

ValidatedCert = namedtuple('ValidatedCert', ['cert', 'expiration_timestamp'])

class Bot(Thread):
    """Contains agent (if not multi-instanced), conversations, validates Alexa requests and routes them to conversations.

    Args:
        agent_generator: Callback which generates DefaultAgent instance with alexa skill.
        config: Alexa skill configuration settings.
        input_queue: Queue for incoming requests from Alexa.
        output_queue: Queue for outcoming responses to Alexa.

    Attributes:
        config: Alexa skill configuration settings.
        conversations: Dict with current conversations, key - Alexa user ID, value - Conversation object.
        input_queue: Queue for incoming requests from Alexa.
        output_queue: Queue for outcoming responses to Alexa.
        valid_certificates: Dict where key - signature chain url, value - ValidatedCert instance.
        agent: Alexa skill agent if not multi-instance mode.
        agent_generator: Callback which generates DefaultAgent instance with alexa skill.
        timer: Timer which triggers periodical certificates with expired validation cleanup.
    """
    def __init__(self, agent_generator: callable, config: dict, input_queue: Queue, output_queue: Queue) -> None:
        super(Bot, self).__init__()
        self.config = config
        self.conversations: Dict[str, Conversation] = {}
        self.input_queue = input_queue
        self.output_queue = output_queue

        self.valid_certificates: Dict[str, ValidatedCert] = {}

        self.agent: Optional[DefaultAgent] = None
        self.agent_generator = agent_generator

        if not self.config['multi_instance']:
            self.agent = self._init_agent()
            log.info('New bot instance level agent initiated')

        self.timer = Timer(REFRESH_VALID_CERTS_PERIOD_SECS, self._refresh_valid_certs)
        self.timer.start()

    def run(self) -> None:
        """Thread run method implementation."""
        while True:
            request = self.input_queue.get()
            response = self._handle_request(request)
            self.output_queue.put(response)

    def _del_conversation(self, conversation_key: str) -> None:
        """Deletes Conversation instance.

        Args:
            conversation_key: Conversation key.
        """
        if conversation_key in self.conversations.keys():
            del self.conversations[conversation_key]
            log.info(f'Deleted conversation, key: {conversation_key}')

    def _init_agent(self) -> DefaultAgent:
        """Initiates Alexa skill agent from agent generator"""
        # TODO: Decide about multi-instance mode necessity.
        # If model multi-instancing is still necessary - refactor and remove
        agent = self.agent_generator()
        return agent

    def _refresh_valid_certs(self) -> None:
        """Conducts cleanup of periodical certificates with expired validation."""
        self.timer = Timer(REFRESH_VALID_CERTS_PERIOD_SECS, self._refresh_valid_certs)
        self.timer.start()

        expired_certificates = []

        for valid_cert_url, valid_cert in self.valid_certificates.items():
            valid_cert: ValidatedCert = valid_cert
            cert_expiration_time: datetime = valid_cert.expiration_timestamp
            if datetime.utcnow() > cert_expiration_time:
                expired_certificates.append(valid_cert_url)

        for expired_cert_url in expired_certificates:
            del self.valid_certificates[expired_cert_url]
            log.info(f'Validation period of {expired_cert_url} certificate expired')

    def _verify_request(self, signature_chain_url: str, signature: str, request_body: bytes) -> bool:
        """Conducts series of Alexa request verifications against Amazon Alexa requirements.

        Args:
            signature_chain_url: Signature certificate URL from SignatureCertChainUrl HTTP header.
            signature: Base64 decoded Alexa request signature from Signature HTTP header.
            request_body: full HTTPS request body
        Returns:
            result: True if verification was successful, False if not.
        """
        if signature_chain_url not in self.valid_certificates.keys():
            amazon_cert: X509 = verify_cert(signature_chain_url)
            if amazon_cert:
                amazon_cert_lifetime: timedelta = self.config['amazon_cert_lifetime']
                expiration_timestamp = datetime.utcnow() + amazon_cert_lifetime
                validated_cert = ValidatedCert(cert=amazon_cert, expiration_timestamp=expiration_timestamp)
                self.valid_certificates[signature_chain_url] = validated_cert
                log.info(f'Certificate {signature_chain_url} validated')
            else:
                log.error(f'Certificate {signature_chain_url} validation failed')
                return False
        else:
            validated_cert: ValidatedCert = self.valid_certificates[signature_chain_url]
            amazon_cert: X509 = validated_cert.cert

        if verify_signature(amazon_cert, signature, request_body):
            result = True
        else:
            log.error(f'Failed signature verification for request: {request_body.decode("utf-8", "replace")}')
            result = False

        return result

    def _handle_request(self, request: dict) -> dict:
        """Processes Alexa requests from skill server and returns responses to Alexa.

        Args:
            request: Dict with Alexa request payload and metadata.
        Returns:
            result: Alexa formatted or error response.
        """
        request_body: bytes = request['request_body']
        signature_chain_url: str = request['signature_chain_url']
        signature: str = request['signature']
        alexa_request: dict = request['alexa_request']

        if not self._verify_request(signature_chain_url, signature, request_body):
            return {'error': 'failed certificate/signature check'}

        timestamp_str = alexa_request['request']['timestamp']
        timestamp_datetime = datetime.strptime(timestamp_str, '%Y-%m-%dT%H:%M:%SZ')
        now = datetime.utcnow()

        delta = now - timestamp_datetime if now >= timestamp_datetime else timestamp_datetime - now

        if abs(delta.seconds) > REQUEST_TIMESTAMP_TOLERANCE_SECS:
            log.error(f'Failed timestamp check for request: {request_body.decode("utf-8", "replace")}')
            return {'error': 'failed request timestamp check'}

        conversation_key = alexa_request['session']['user']['userId']

        if conversation_key not in self.conversations.keys():
            if self.config['multi_instance']:
                conv_agent = self._init_agent()
                log.info('New conversation instance level agent initiated')
            else:
                conv_agent = self.agent

            self.conversations[conversation_key] = \
                Conversation(config=self.config,
                             agent=conv_agent,
                             conversation_key=conversation_key,
                             self_destruct_callback=lambda: self._del_conversation(conversation_key))

            log.info(f'Created new conversation, key: {conversation_key}')

        conversation = self.conversations[conversation_key]
        response = conversation.handle_request(alexa_request)

        return response


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import tensorflow as tf

def csoftmax_for_slice(input):
    """ It is a implementation of the constrained softmax (csoftmax) for slice.
        Based on the paper:
        https://andre-martins.github.io/docs/emnlp2017_final.pdf "Learning What's Easy: Fully Differentiable Neural Easy-First Taggers" (page 4)
    Args:
        input: A list of [input tensor, cumulative attention].
    Returns:
        output: A list of [csoftmax results, masks]
    """

    [ten, u] = input

    shape_t = ten.shape
    shape_u = u.shape

    ten -= tf.reduce_mean(ten)
    q = tf.exp(ten)
    active = tf.ones_like(u, dtype=tf.int32)
    mass = tf.constant(0, dtype=tf.float32)
    found = tf.constant(True, dtype=tf.bool)

    def loop(q_, mask, mass_, found_):
        q_list = tf.dynamic_partition(q_, mask, 2)
        condition_indices = tf.dynamic_partition(tf.range(tf.shape(q_)[0]), mask, 2)  # 0 element it False,
        #  1 element if true

        p = q_list[1] * (1.0 - mass_) / tf.reduce_sum(q_list[1])
        p_new = tf.dynamic_stitch(condition_indices, [q_list[0], p])

        # condition verification and mask modification
        less_mask = tf.cast(tf.less(u, p_new), tf.int32)  # 0 when u is bigger than p, 1 when u is less than p
        condition_indices = tf.dynamic_partition(tf.range(tf.shape(p_new)[0]), less_mask,
                                                 2)  # 0 when u is bigger than p, 1 when u is less than p

        split_p_new = tf.dynamic_partition(p_new, less_mask, 2)
        split_u = tf.dynamic_partition(u, less_mask, 2)

        alpha = tf.dynamic_stitch(condition_indices, [split_p_new[0], split_u[1]])
        mass_ += tf.reduce_sum(split_u[1])

        mask = mask * (tf.ones_like(less_mask) - less_mask)

        found_ = tf.cond(tf.equal(tf.reduce_sum(less_mask), 0),
                         lambda: False,
                         lambda: True)

        alpha = tf.reshape(alpha, q_.shape)

        return alpha, mask, mass_, found_

    (csoft, mask_, _, _) = tf.while_loop(cond=lambda _0, _1, _2, f: f,
                                         body=loop,
                                         loop_vars=(q, active, mass, found))

    return [csoft, mask_]

def csoftmax(tensor, inv_cumulative_att):
    """ It is a implementation of the constrained softmax (csoftmax).
        Based on the paper:
        https://andre-martins.github.io/docs/emnlp2017_final.pdf "Learning What's Easy: Fully Differentiable Neural Easy-First Taggers"
    Args:
        tensor: A tensorflow tensor is score. This tensor have dimensionality [None, n_tokens]
        inv_cumulative_att: A inverse cumulative attention tensor with dimensionality [None, n_tokens]
    Returns:
        cs: Tensor at the output with dimensionality [None, n_tokens]
    """
    shape_ten = tensor.shape
    shape_cum = inv_cumulative_att.shape

    merge_tensor = [tensor, inv_cumulative_att]
    cs, _ = tf.map_fn(csoftmax_for_slice, merge_tensor, dtype=[tf.float32, tf.float32])  # [bs, L]
    return cs

def attention_gen_step(hidden_for_sketch, hidden_for_attn_alignment, sketch, key, cum_att):
    """ It is a implementation one step of block of the Luong et al. attention mechanism with general score and the constrained softmax (csoftmax).
        Based on the papers:
        https://arxiv.org/abs/1508.04025 "Effective Approaches to Attention-based Neural Machine Translation"
        https://andre-martins.github.io/docs/emnlp2017_final.pdf "Learning What's Easy: Fully Differentiable Neural Easy-First Taggers"
    Args:
        hidden_for_sketch: A tensorflow tensor for a sketch computing. This tensor have dimensionality [None, max_num_tokens, sketch_hidden_size]
        hidden_for_attn_alignment: A tensorflow tensor is aligned for output during a performing. This tensor have dimensionality [None, max_num_tokens, hidden_size_for_attn_alignment]
        sketch: A previous step sketch tensor for a sketch computing. This tensor have dimensionality [None, sketch_hidden_size]
        key: A tensorflow tensor with dimensionality [None, None, key_size]
        cum_att: A cumulative attention tensor with dimensionality [None, max_num_tokens]
    Returns:
        next_sketch: Tensor of the current step sketch with dimensionality [None, sketch_hidden_size]
        att: Tensor of the current step attention with dimensionality [None, max_num_tokens]
        aligned_hidden_sketch: Tensor of aligned hidden state of current step with dimensionality [None, hidden_size_for_attn_alignment]
    """
    with tf.name_scope('attention_step'):
        sketch_dims = hidden_for_sketch.get_shape().as_list()
        batch_size = sketch_dims[0]
        num_tokens = sketch_dims[1]
        hidden_size = sketch_dims[2]
        attn_alignment_dims = hidden_for_attn_alignment.get_shape().as_list()
        attn_alignment_hidden_size = attn_alignment_dims[2]

        repeated_sketch = tf.tile(tf.reshape(sketch, [-1, 1, hidden_size]), (1,num_tokens, 1))
        concat_mem = tf.concat([hidden_for_sketch, repeated_sketch],-1)

        concat_mem = tf.reshape(concat_mem, [-1, num_tokens, 2*hidden_size]) # dirty trick
        reduce_mem = tf.layers.dense(concat_mem, hidden_size)

        projected_key = tf.layers.dense(key, hidden_size)
        t_key = tf.reshape(projected_key,[-1, hidden_size, 1])

        score = tf.reshape(tf.matmul(reduce_mem, t_key), [-1, num_tokens])

        inv_cum_att = tf.reshape(tf.ones_like(cum_att) - cum_att, [-1, num_tokens])
        att = csoftmax(score, inv_cum_att)

        t_reduce_mem = tf.transpose(reduce_mem, [0,2,1])
        t_hidden_for_attn_alignment = tf.transpose(hidden_for_attn_alignment, [0,2,1])

        r_att = tf.reshape(att, [-1, num_tokens, 1])

        next_sketch = tf.squeeze(tf.matmul(t_reduce_mem,r_att),-1)
        aligned_hidden_sketch = tf.squeeze(tf.matmul(t_hidden_for_attn_alignment,r_att),-1)
    return next_sketch, att, aligned_hidden_sketch

def attention_gen_block(hidden_for_sketch, hidden_for_attn_alignment, key, attention_depth):
    """ It is a implementation of the Luong et al. attention mechanism with general score and the constrained softmax (csoftmax).
        Based on the papers:
        https://arxiv.org/abs/1508.04025 "Effective Approaches to Attention-based Neural Machine Translation"
        https://andre-martins.github.io/docs/emnlp2017_final.pdf "Learning What's Easy: Fully Differentiable Neural Easy-First Taggers"
    Args:
        hidden_for_sketch: A tensorflow tensor for a sketch computing. This tensor have dimensionality [None, max_num_tokens, sketch_hidden_size]
        hidden_for_attn_alignment: A tensorflow tensor is aligned for output during a performing. This tensor have dimensionality [None, max_num_tokens, hidden_size_for_attn_alignment]
        key: A tensorflow tensor with dimensionality [None, None, key_size]
        attention_depth: Number of usage csoftmax
    Returns:
        final_aligned_hiddens: Tensor at the output with dimensionality [1, attention_depth, hidden_size_for_attn_alignment]
    """
    with tf.name_scope('attention_block'):
        sketch_dims = tf.shape(hidden_for_sketch)
        batch_size = sketch_dims[0]
        num_tokens = sketch_dims[1]
        hidden_size = sketch_dims[2]

        attn_alignment_dims = tf.shape(hidden_for_attn_alignment)
        attn_alignment_hidden_size = attn_alignment_dims[2]

        sketches = [tf.zeros(shape=[batch_size, hidden_size], dtype=tf.float32)]
        aligned_hiddens = []
        cum_att = tf.zeros(shape=[batch_size, num_tokens])  # cumulative attention
        for i in range(attention_depth):
            sketch, cum_att_, aligned_hidden = attention_gen_step(hidden_for_sketch, hidden_for_attn_alignment, sketches[-1], key, cum_att)
            sketches.append(sketch) #sketch
            aligned_hiddens.append(aligned_hidden) #sketch
            cum_att += cum_att_
        final_aligned_hiddens = tf.reshape(tf.transpose(tf.stack(aligned_hiddens), [1, 0, 2]),[1, attention_depth, attn_alignment_hidden_size])
    return final_aligned_hiddens

def attention_bah_step(hidden_for_sketch, hidden_for_attn_alignment, sketch, cum_att):
    """ It is a implementation one step of block of the Bahdanau et al. attention mechanism with concat score and the constrained softmax (csoftmax).
        Based on the papers:
            https://arxiv.org/abs/1409.0473 "Neural Machine Translation by Jointly Learning to Align and Translate"
            https://andre-martins.github.io/docs/emnlp2017_final.pdf "Learning What's Easy: Fully Differentiable Neural Easy-First Taggers"
    Args:
        hidden_for_sketch: A tensorflow tensor for a sketch computing. This tensor have dimensionality [None, max_num_tokens, sketch_hidden_size]
        hidden_for_attn_alignment: A tensorflow tensor is aligned for output during a performing. This tensor have dimensionality [None, max_num_tokens, hidden_size_for_attn_alignment]
        sketch: A previous step sketch tensor for a sketch computing. This tensor have dimensionality [None, sketch_hidden_size]
        key: A tensorflow tensor with dimensionality [None, None, key_size]
        cum_att: A cumulative attention tensor with dimensionality [None, max_num_tokens]
    Returns:
        next_sketch: Tensor of the current step sketch with dimensionality [None, sketch_hidden_size]
        att: Tensor of the current step attention with dimensionality [None, max_num_tokens]
        aligned_hidden_sketch: Tensor of aligned hidden state of current step with dimensionality [None, hidden_size_for_attn_alignment]
    """
    with tf.name_scope('attention_step'):
        sketch_dims = hidden_for_sketch.get_shape().as_list()
        batch_size = sketch_dims[0]
        num_tokens = sketch_dims[1]
        hidden_size = sketch_dims[2]
        attn_alignment_dims = hidden_for_attn_alignment.get_shape().as_list()
        attn_alignment_hidden_size = attn_alignment_dims[2]

        repeated_sketch = tf.tile(tf.reshape(sketch, [-1, 1, hidden_size]), (1,num_tokens, 1))
        concat_mem = tf.concat([hidden_for_sketch, repeated_sketch],-1)

        concat_mem = tf.reshape(concat_mem, [-1, num_tokens, 2*hidden_size]) # dirty trick
        reduce_mem = tf.layers.dense(concat_mem, hidden_size)

        score = tf.squeeze(tf.layers.dense(reduce_mem, units = 1,
                                    use_bias=False),-1)
        inv_cum_att = tf.reshape(tf.ones_like(cum_att) - cum_att, [-1, num_tokens])
        att = csoftmax(score, inv_cum_att)

        t_reduce_mem = tf.transpose(reduce_mem, [0,2,1])
        t_hidden_for_attn_alignment = tf.transpose(hidden_for_attn_alignment, [0,2,1])

        r_att = tf.reshape(att, [-1, num_tokens, 1])

        next_sketch = tf.squeeze(tf.matmul(t_reduce_mem,r_att),-1)
        aligned_hidden_sketch = tf.squeeze(tf.matmul(t_hidden_for_attn_alignment,r_att),-1)
    return next_sketch, att, aligned_hidden_sketch

def attention_bah_block(hidden_for_sketch, hidden_for_attn_alignment, attention_depth):
    """ It is a implementation of the Bahdanau et al. attention mechanism with concat score and the constrained softmax (csoftmax).
        Based on the papers:
            https://arxiv.org/abs/1409.0473 "Neural Machine Translation by Jointly Learning to Align and Translate"
            https://andre-martins.github.io/docs/emnlp2017_final.pdf "Learning What's Easy: Fully Differentiable Neural Easy-First Taggers"
    Args:
        hidden_for_sketch: A tensorflow tensor for a sketch computing. This tensor have dimensionality [None, max_num_tokens, sketch_hidden_size]
        hidden_for_attn_alignment: A tensorflow tensor is aligned for output during a performing. This tensor have dimensionality [None, max_num_tokens, hidden_size_for_attn_alignment]
        key: A tensorflow tensor with dimensionality [None, None, key_size]
        attention_depth: Number of usage csoftmax
    Returns:
        final_aligned_hiddens: Tensor at the output with dimensionality [1, attention_depth, hidden_size_for_attn_alignment]
    """
    with tf.name_scope('attention_block'):
        sketch_dims = tf.shape(hidden_for_sketch)
        batch_size = sketch_dims[0]
        num_tokens = sketch_dims[1]
        hidden_size = sketch_dims[2]

        attn_alignment_dims = tf.shape(hidden_for_attn_alignment)
        attn_alignment_hidden_size = attn_alignment_dims[2]

        sketches = [tf.zeros(shape=[batch_size, hidden_size], dtype=tf.float32)]
        aligned_hiddens = []
        cum_att = tf.zeros(shape=[batch_size, num_tokens])  # cumulative attention
        for i in range(attention_depth):
            sketch, cum_att_, aligned_hidden = attention_bah_step(hidden_for_sketch, hidden_for_attn_alignment, sketches[-1], cum_att)
            sketches.append(sketch) #sketch
            aligned_hiddens.append(aligned_hidden) #sketch
            cum_att += cum_att_
        final_aligned_hiddens = tf.reshape(tf.transpose(tf.stack(aligned_hiddens), [1, 0, 2]),[1, attention_depth, attn_alignment_hidden_size])
    return final_aligned_hiddens


# Copyright 2017 Neural Networks and Deep Learning lab, MIPT
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import importlib
import json
from logging import getLogger
from pathlib import Path

from deeppavlov.core.common.errors import ConfigError

logger = getLogger(__name__)

_registry_path = Path(__file__).parent / 'registry.json'
if _registry_path.exists():
    with _registry_path.open(encoding='utf-8') as f:
        _REGISTRY = json.load(f)
else:
    _REGISTRY = {}

def cls_from_str(name: str) -> type:
    """Returns a class object with the name given as a string."""
    try:
        module_name, cls_name = name.split(':')
    except ValueError:
        raise ConfigError('Expected class description in a `module.submodules:ClassName` form, but got `{}`'
                          .format(name))

    return getattr(importlib.import_module(module_name), cls_name)

def register(name: str = None) -> type:
    """
    Register classes that could be initialized from JSON configuration file.
    If name is not passed, the class name is converted to snake-case.
    """
    def decorate(model_cls: type, reg_name: str = None) -> type:
        model_name = reg_name or short_name(model_cls)
        global _REGISTRY
        cls_name = model_cls.__module__ + ':' + model_cls.__name__
        if model_name in _REGISTRY and _REGISTRY[model_name] != cls_name:
            logger.warning('Registry name "{}" has been already registered and will be overwritten.'.format(model_name))
        _REGISTRY[model_name] = cls_name
        return model_cls

    return lambda model_cls_name: decorate(model_cls_name, name)

def short_name(cls: type) -> str:
    """Returns just a class name (without package and module specification)."""
    return cls.__name__.split('.')[-1]

def get_model(name: str) -> type:
    """Returns a registered class object with the name given in the string."""
    if name not in _REGISTRY:
        if ':' not in name:
            raise ConfigError("Model {} is not registered.".format(name))
        return cls_from_str(name)
    return cls_from_str(_REGISTRY[name])

def list_models() -> list:
    """Returns a list of names of registered classes."""
    return list(_REGISTRY)