# coding=utf-8
"""
© 2017. Case Recommender All Rights Reserved (License GPL3)

This file contains item recommendation measures:
    MAP
    Precision
    Recall
    NDCG

    Class:

        - ItemRecommendationEvaluation

    Methods:

        - default_evaluation: returns:
            * prec@1, recall@1, ndcg@1, map@1 ... prec@10, recall@10, ndcg@10, map@10 and map_total
            * with only_map=True: map@1, map@3, map@5, map@10 and map_total
            * with only_ndcg=True: ndcg@1, ndcg@3, ndcg@5, ndcg@10

        - simple_evaluation: Evaluation only one ranking

        - all_but_one_evaluation: All-but-one Protocol

        - folds_evaluation: Evaluate N-Cross-Fold-Validation

        - evaluation_ranking: This method is used to build evaluate process into the recommenders files

"""

import numpy as np
from caserec.utils.extra_functions import check_error_file
from caserec.utils.read_file import ReadFile

__author__ = 'Arthur Fortes'


def ndcg_at_k(ranking):
    """
    Calculate the value of ndcg in ranking

    :param ranking: (numpy array) ranking
    :return: (float) value of ndcg
    """
    ranking = np.asfarray(ranking)
    r_ideal = np.asfarray(sorted(ranking, reverse=True))
    dcg_ideal = r_ideal[0] + np.sum(r_ideal[1:] / np.log2(np.arange(2, r_ideal.size + 1)))
    dcg_ranking = ranking[0] + np.sum(ranking[1:] / np.log2(np.arange(2, ranking.size + 1)))

    return dcg_ranking / dcg_ideal


class ItemRecommendationEvaluation(object):
    def __init__(self, space_type='\t', only_map=False, only_ndcg=False, n_ranks=list([1, 3, 5, 10])):
        """
        Class to evaluate rankings in a item recommendation scenario

        :param space_type: (string) delimiter (e.g ',' ' ' '\t')
        :param only_map: (bool) return only map values
        :param only_ndcg: (bool) return only ndcg values
        :param n_ranks: (list of int) list of positions to evaluate the ranking
        """
        self.space_type = space_type
        self.only_map = only_map
        self.only_ndcg = only_ndcg
        self.n_ranks = n_ranks

    def default_evaluation(self, ranking, test):
        """
        Calculate all the measures for item recommendation scenario

        :param ranking: (dict) dict generated by ReadFile(...).return_information()
        :param test:  (dict) dict generated by ReadFile(...).return_information()
        :return: (dict) values of the quality of ranking
        """

        avg_prec_total = list()
        final_values_dict = dict()
        num_user = len(test['users'])

        for i, n in enumerate(self.n_ranks):
            if n < 1:
                raise ValueError('Error: N must >= 1.')

            partial_precision = list()
            partial_recall = list()
            avg_prec_total = list()
            partial_ndcg = list()

            for user in test['users']:
                avg_prec_sum = 0

                hit_cont = 0
                # Generate user intersection list between the recommended items and test.
                list_feedback = set(list(ranking['du_order'].get(user, []))[:n])
                intersection = list(list_feedback.intersection(test['du_order'][user]))

                if len(intersection) > 0:
                    partial_precision.append((float(len(intersection)) / float(n)))
                    partial_recall.append((float(len(intersection)) / float(len(test['du'][user]))))

                    ig_ranking = np.zeros(n)
                    for item in intersection:
                        hit_cont += 1
                        avg_prec_sum += (float(hit_cont) / float(list(
                            ranking['du_order'][user])[:n].index(item) + 1))
                        ig_ranking[ranking['du_order'][user].index(item)] = 1

                    partial_ndcg.append(ndcg_at_k(ig_ranking))
                    avg_prec_total.append(float(avg_prec_sum) / float(len(test['du_order'][user])))

            # calculate final precision and recall
            if not self.only_map and not self.only_ndcg:
                final_values_dict.update({
                    'Prec@' + str(n): round(sum(partial_precision) / float(num_user), 5),
                    'Recall@' + str(n): round(sum(partial_recall) / float(num_user), 5),
                })

            if not self.only_map:
                final_values_dict.update({'NDCG@' + str(n): round(sum(partial_ndcg) / float(num_user), 5)})

            if not self.only_ndcg:
                final_values_dict.update({'MAP@' + str(n): round(sum(avg_prec_total) / float(num_user), 5)})

        if not self.only_ndcg:
            final_values_dict.update({'MAP': round(sum(avg_prec_total) / float(num_user), 5)})

        return final_values_dict

    def simple_evaluation(self, file_result, file_test):
        """
        A simple evaluation method to return the quality of a ranking

        :param file_result: (file) ranking file to evaluate
        :param file_test: (file) test file
        :return: Values of evaluation
        """

        # Verify that the files are valid
        check_error_file(file_result)
        check_error_file(file_test)

        predict = ReadFile(file_result, space_type=self.space_type).return_information()
        test = ReadFile(file_test, space_type=self.space_type).return_information()

        return self.default_evaluation(predict, test)

    def all_but_one_evaluation(self, file_result, file_test):
        """
        All-but-one Protocol: Considers only one pair (u, i) from the test set to evaluate the ranking

        :param file_result: (file) ranking file to evaluate
        :param file_test: (file) test file
        :return: Values of evaluation
        """

        # Verify that the files are valid
        check_error_file(file_result)
        check_error_file(file_test)

        predict = ReadFile(file_result, space_type=self.space_type).return_information()
        test = ReadFile(file_test, space_type=self.space_type).return_information()

        for user in test['users']:
            test['du'][user] = [list(test['du'][user])[0]]

        return self.default_evaluation(predict, test)

    def folds_evaluation(self, folds_dir, n_folds, name_prediction, name_test, type_recommendation='SimpleEvaluation',
                         no_deviation=False):
        """
        Evaluation N-Fold-Validation

        :param folds_dir: (string) fold of the files
        :param n_folds: (int) number of folds to evaluate
        :param name_prediction: (string) name of the ranking file [p.s: same in all folds]
        :param name_test: (string) name of the test file [p.s: same in all folds]
        :param type_recommendation: (string) choice 'SimpleEvaluation' or 'AllButOne'
        :param no_deviation: (bool) if true returns the standard deviation
        :return: Values of evaluation
        """
        result = list()
        list_results = list()

        for fold in range(n_folds):
            prediction = folds_dir + str(fold) + '/' + name_prediction
            test = folds_dir + str(fold) + '/' + name_test

            if type_recommendation == "SimpleEvaluation":
                result.append(self.simple_evaluation(prediction, test))
            elif type_recommendation == "AllButOne":
                result.append(self.all_but_one_evaluation(prediction, test))
            else:
                raise ValueError('Error: Invalid recommendation type!')

        for i in range(len(result[0])):
            list_partial = list()
            for j in range(n_folds):
                list_partial.append(result[j][i])
            if no_deviation:
                list_results.append(list_partial)
            else:
                list_results.append([np.mean(list_partial), np.std(list_partial)])

        return list_results

    def evaluation_ranking(self, ranking, test_file):
        ranking_dict = {'du_order': {}}
        test = ReadFile(test_file, space_type=self.space_type).return_information()

        for sample in ranking:
            ranking_dict['du_order'].setdefault(sample[0], list()).append(sample[1])

        return self.default_evaluation(ranking_dict, test)
