"""

    PaCo Recommender Algorithm
    [Co-Clustering Algorithm]

    Literature:
        Michail Vlachos, Francesco Fusco, Charalambos Mavroforakis, Anastasios Kyrillidis, and
        Vassilios G. Vassiliadis:
        Improving Co-Cluster Quality with Application to Product Recommendations. 2014.
        http://dl.acm.org/citation.cfm?id=2661980

"""

from collections import defaultdict
import numpy as np

from caserec.clustering.paco import PaCo
from caserec.evaluation.item_recommendation import ItemRecommendationEvaluation
from caserec.utils.process_data import ReadFile


class PaCoRecommender(object):
    def __init__(self, train_file, test_file=None, output_file=None, k_row=None, l_col=None,
                 density_low=0.01, as_binary=True, min_density=0.3):

        """
        PaCo for Item Recommendation

        This algorithm predicts a rank for each user using a co-clustering algorithm

        Usage::

            >> PaCoRecommender(train, test).compute()
            >> PaCoRecommender(train, test, as_binary=True).compute()

        :param train_file: File which contains the train set. This file needs to have at least 3 columns
        (user item feedback_value).
        :type train_file: str

        :param test_file: File which contains the test set. This file needs to have at least 3 columns
        (user item feedback_value).
        :type test_file: str, default None

        :param output_file: File with dir to write the final predictions
        :type output_file: str, default None

        :param k_row: Number of clusters generated by k-means in rows
        :type k_row: int, default None

        :param l_col: (int) Number of clusters generated by k-means in rows
        :type l_col: int, default None

        :param density_low: Threshold to change the density matrix values
        :type density_low: float, default 0.008

        :param as_binary: If True, the explicit feedback will be transform to binary
        :type as_binary: bool, default True

        :param min_density: Considers bi-clusters until min-density
        :type min_density: float, default 0.3

        """
        self.recommender_name = 'PaCo Recommender Algorithm'

        self.train_file = train_file
        self.test_file = test_file
        if test_file is not None:
            self.test_set = ReadFile(test_file).read()
        self.train_set = ReadFile(train_file, as_binary=as_binary).read()
        self.output_file = output_file
        self.k_row = k_row
        self.l_col = l_col
        self.density_low = density_low
        self.min_density = min_density

        self.users = self.train_set['users']
        self.items = self.train_set['items']

        self.item_to_item_id = {}
        self.item_id_to_item = {}
        self.user_to_user_id = {}
        self.user_id_to_user = {}

        for i, item in enumerate(self.items):
            self.item_to_item_id.update({item: i})
            self.item_id_to_item.update({i: item})
        for u, user in enumerate(self.users):
            self.user_to_user_id.update({user: u})
            self.user_id_to_user.update({u: user})

        self.predictions = []
        self.uns_items = defaultdict()
        self.co_clustering = None

    def run_co_clustering(self):
        self.co_clustering = PaCo(self.train_file, k_row=self.k_row, l_col=self.l_col, density_low=self.density_low)
        self.co_clustering.fit()
        if len(self.co_clustering.density) == 1:
            raise ValueError('Error: Co-clustering generated only 1 bi-cluster!')

    def recommender(self):
        for n, k in enumerate(self.co_clustering.list_row):
            cols = self.co_clustering.density[n].argsort()
            cols = np.array(cols).ravel()[::-1]

            for u_idx in k:
                user = self.user_id_to_user[u_idx]
                unseen_items = set()
                for l in cols:
                    if self.co_clustering.density[n, l] != 0 and self.co_clustering.density[n, l] != 1 and \
                            self.co_clustering.density[n, l] >= self.min_density:
                        for i_idx in self.co_clustering.list_col[l]:
                            item = self.item_id_to_item[i_idx]
                            if self.train_set['feedback'][user].get(item, -1) == -1:
                                unseen_items.add(item)

                self.uns_items[user] = unseen_items

        for user in self.train_set['users']:
            ranking = []
            for item in self.uns_items[user]:
                rui = len(self.train_set['users_viewed_item'][item])
                ranking.append((user, item, rui))
            self.predictions += sorted(ranking, key=lambda x: -x[2])[:10]

        if self.output_file is not None:
            with open(self.output_file, 'w') as fw:
                for sample in self.predictions:
                    fw.write("%d\t%d\t%f\n" % (sample[0], sample[1], sample[2]))

    def compute(self, verbose=True, metrics=list(['PREC', 'RECALL', 'MAP', 'NDCG']), verbose_evaluation=True,
                as_table=False, table_sep='\t'):

        if verbose:
            print("[Case Recommender: Item Recommendation > %s]\n" % self.recommender_name)

        self.run_co_clustering()
        self.recommender()

        if self.test_file is not None:
            ItemRecommendationEvaluation(metrics=metrics, as_table=as_table, table_sep=table_sep,
                                         verbose=verbose_evaluation).evaluate_recommender(self.predictions,
                                                                                          self.test_set)
