# MIT License

# Copyright (c) 2022 Ming Zhong

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import numpy as np
from nltk import sent_tokenize

from .scorer import UniEvaluator
from .utils import add_question


class SumEvaluator:
    def __init__(self, model_name_or_path, max_length=1024, device="cuda:0", cache_dir=None):
        """Set up evaluator for text summarization"""
        self.scorer = UniEvaluator(
            model_name_or_path="MingZhong/unieval-sum" if model_name_or_path == "" else model_name_or_path,
            max_length=max_length,
            device=device,
            cache_dir=cache_dir,
        )
        self.task = "summarization"
        self.dimensions = ["coherence", "consistency", "fluency", "relevance"]

    def evaluate(self, data, category, dims=None, overall=True):
        """
        Get the scores of all the given dimensions

        category: The category to be evaluated.

        dims: A list of dimensions to be evaluated. If dims is None, SumEvaluator will evaluate
              four dimensions: coherence, consistency, fluency, relevance.

        overall: indicates whether the overall score is to be calculated.
                 Overall score can be customized to a combination of scores based on different
                 dimensions. The default here is the average score of all the given dimensions.
        """
        n_data = len(data)
        eval_scores = [{} for _ in range(n_data)]

        if dims == None:
            eval_dims = self.dimensions
        else:
            assert isinstance(dims, list)
            eval_dims = dims

        for dim in eval_dims:
            # Calculate average sentence-level scores for 'consistency' and 'fluency'
            if dim == "consistency" or dim == "fluency":
                src_list, output_list = [], []
                n_sents = []  # the number of sentences in each generated summary
                for i in range(n_data):
                    source = data[i]["source"]
                    system_outputs = sent_tokenize(data[i]["system_output"])
                    n_sents.append(len(system_outputs))
                    for j in range(len(system_outputs)):
                        src_list.append(source)
                        output_list.append(system_outputs[j])
                input_list = add_question(dimension=dim, output=output_list, src=src_list, task=self.task)
                sent_score = self.scorer.score(input_list, self.task, category, dim)

                # Get average score for each sample
                start_idx = 0
                score = []
                for cur_n_sent in n_sents:
                    # prevent denominator from being 0
                    score.append(sum(sent_score[start_idx : start_idx + cur_n_sent]) / (cur_n_sent + 1e-6))
                    start_idx += cur_n_sent

            # Calculate summary-level score for 'coherence' and 'relevance'
            elif dim == "coherence" or dim == "relevance":
                src_list, output_list, ref_list = [], [], []
                for i in range(n_data):
                    src_list.append(data[i]["source"])
                    output_list.append(data[i]["system_output"])
                    if dim == "relevance":
                        ref_list.append(data[i]["reference"])
                input_list = add_question(dimension=dim, output=output_list, src=src_list, ref=ref_list, task=self.task)
                score = self.scorer.score(input_list, self.task, category, dim)

            # Please customize other dimensions here for summarization
            else:
                raise NotImplementedError(
                    "The input format for this dimension is still undefined. \
                                           Please customize it first."
                )

            for i in range(n_data):
                eval_scores[i][dim] = score[i]

        # Customize your overall score here.
        if overall == True:
            for i in range(n_data):
                eval_scores[i]["overall"] = np.mean(list(eval_scores[i].values()))

        return eval_scores


class DialogEvaluator:
    def __init__(self, model_name_or_path, max_length=1024, device="cuda:0", cache_dir=None):
        """Set up evaluator for dialogues"""
        self.scorer = UniEvaluator(
            model_name_or_path="MingZhong/unieval-dialog" if model_name_or_path == "" else model_name_or_path,
            max_length=max_length,
            device=device,
            cache_dir=cache_dir,
        )
        self.task = "dialogue"
        self.dimensions = ["naturalness", "coherence", "engagingness", "groundedness", "understandability"]

    def evaluate(self, data, category, dims=None, overall=True):
        """
        Get the scores of all the given dimensions

        category: The category to be evaluated.

        dims: A list of dimensions to be evaluated. If dims is None, DialogEvaluator will evaluate
              five dimensions: naturalness, coherence, engagingness, groundedness and understandability.

        overall: indicates whether the overall score is to be calculated.
                 Overall score can be customized to a combination of scores based on different
                 dimensions. The default here is the average score of all the given dimensions.
        """
        n_data = len(data)
        eval_scores = [{} for _ in range(n_data)]

        if dims == None:
            eval_dims = self.dimensions
        else:
            assert isinstance(dims, list)
            eval_dims = dims

        for dim in eval_dims:
            # Calculate summation score for 'engagingness'
            if dim == "engagingness":
                src_list, output_list, context_list = [], [], []
                n_sents = []  # the number of sentences in each generated response
                for i in range(n_data):
                    source = data[i]["source"]
                    context = data[i]["context"]
                    system_outputs = sent_tokenize(data[i]["system_output"])
                    n_sents.append(len(system_outputs))
                    for j in range(len(system_outputs)):
                        src_list.append(source)
                        context_list.append(context)
                        output_list.append(system_outputs[j])
                input_list = add_question(
                    dimension=dim, output=output_list, src=src_list, context=context_list, task=self.task
                )
                sent_score = self.scorer.score(input_list, self.task, category, dim)

                # Get the summation score for each sample
                start_idx = 0
                score = []
                for cur_n_sent in n_sents:
                    score.append(sum(sent_score[start_idx : start_idx + cur_n_sent]))
                    start_idx += cur_n_sent

            # Calculate turn-level score for other dimensions
            elif dim in ["naturalness", "coherence", "groundedness", "understandability"]:
                src_list, output_list, context_list = [], [], []
                for i in range(n_data):
                    src_list.append(data[i]["source"])
                    output_list.append(data[i]["system_output"])
                    context_list.append(data[i]["context"])
                input_list = add_question(
                    dimension=dim, output=output_list, src=src_list, context=context_list, task=self.task
                )
                score = self.scorer.score(input_list, self.task, category, dim)

            # Please customize other dimensions here for summarization
            else:
                raise NotImplementedError(
                    "The input format for this dimension is still undefined. \
                                           Please customize it first."
                )

            for i in range(n_data):
                eval_scores[i][dim] = score[i]

        # Customize your overall score here.
        if overall == True:
            for i in range(n_data):
                eval_scores[i]["overall"] = np.mean(list(eval_scores[i].values()))

        return eval_scores


class D2tEvaluator:
    def __init__(self, model_name_or_path, max_length=1024, device="cuda:0", cache_dir=None):
        """Set up evaluator for data-to-text"""
        self.scorer = UniEvaluator(
            model_name_or_path="MingZhong/unieval-sum" if model_name_or_path == "" else model_name_or_path,
            max_length=max_length,
            device=device,
            cache_dir=cache_dir,
        )
        self.task = "data2text"
        self.dimensions = ["naturalness", "informativeness"]

    def evaluate(self, data, category, dims=None, overall=True):
        """
        Get the scores of all the given dimensions

        category: The category to be evaluated.

        dims: A list of dimensions to be evaluated. If dims is None, D2tEvaluator will evaluate
              two dimensions: naturalness and informativeness.

        overall: indicates whether the overall score is to be calculated.
                 Overall score can be customized to a combination of scores based on different
                 dimensions. The default here is the average score of all the given dimensions.
        """
        n_data = len(data)
        eval_scores = [{} for _ in range(n_data)]

        if dims == None:
            eval_dims = self.dimensions
        else:
            assert isinstance(dims, list)
            eval_dims = dims

        for dim in eval_dims:
            output_list, ref_list = [], []
            for i in range(n_data):
                output_list.append(data[i]["system_output"])
                ref_list.append(data[i]["reference"])

            input_list = add_question(dimension=dim, output=output_list, ref=ref_list, task=self.task)
            score = self.scorer.score(input_list, self.task, category, dim)

            for i in range(n_data):
                eval_scores[i][dim] = score[i]

        # Customize your overall score here.
        if overall == True:
            for i in range(n_data):
                eval_scores[i]["overall"] = np.mean(list(eval_scores[i].values()))

        return eval_scores


class FactEvaluator:
    def __init__(self, model_name_or_path, max_length=1024, device="cuda:0", cache_dir=None):
        """Set up evaluator for factual consistency detection"""
        self.scorer = UniEvaluator(
            model_name_or_path="MingZhong/unieval-fact" if model_name_or_path == "" else model_name_or_path,
            max_length=max_length,
            device=device,
            cache_dir=cache_dir,
        )
        self.task = "fact"
        self.dim = "consistency"

    def evaluate(self, data, category):
        """
        Get the factual consistency score (only 1 dimension for this task)

        category: The category to be evaluated.
        """
        n_data = len(data)
        eval_scores = [{} for _ in range(n_data)]

        # Calculate average sentence-level scores for factual consistency
        src_list, output_list = [], []
        n_sents = []  # the number of sentences in the claim
        for i in range(n_data):
            source = data[i]["source"]
            system_outputs = sent_tokenize(data[i]["system_output"])
            n_sents.append(len(system_outputs))
            for j in range(len(system_outputs)):
                src_list.append(source)
                output_list.append(system_outputs[j])
        input_list = add_question(dimension=self.dim, output=output_list, src=src_list, task=self.task)
        sent_score = self.scorer.score(input_list, self.task, category, self.dim)

        # Get average score for each sample
        start_idx = 0
        score = []
        for cur_n_sent in n_sents:
            score.append(sum(sent_score[start_idx : start_idx + cur_n_sent]) / cur_n_sent)
            start_idx += cur_n_sent

        for i in range(n_data):
            eval_scores[i][self.dim] = score[i]

        return eval_scores


def get_evaluator(task, model_name_or_path="", max_length=1024, device="cuda:0", cache_dir=None):
    assert task in ["summarization", "dialogue", "data2text", "fact"]
    if task == "summarization":
        return SumEvaluator(
            model_name_or_path=model_name_or_path, max_length=max_length, device=device, cache_dir=cache_dir
        )
    elif task == "dialogue":
        return DialogEvaluator(
            model_name_or_path=model_name_or_path, max_length=max_length, device=device, cache_dir=cache_dir
        )
    elif task == "data2text":
        return D2tEvaluator(
            model_name_or_path=model_name_or_path, max_length=max_length, device=device, cache_dir=cache_dir
        )
    elif task == "fact":
        return FactEvaluator(
            model_name_or_path=model_name_or_path, max_length=max_length, device=device, cache_dir=cache_dir
        )
    else:
        raise NotImplementedError(
            "Other tasks are not implemented, \
                                   please customize specific tasks here."
        )
