import numpy as np
import pandas


# Class for Popularity based Recommender System model
class popularity_recommender_py():
    def __init__(self):
        self.train_data = None
        self.user = None
        self.song = None
        self.popularity_recommendations = None

    # Create the popularity based recommender system model
    def create(self, train_data, user, song):
        self.train_data = train_data
        self.user = user
        self.song = song

        # Get a count of users for each unique song as recommendation score
        train_data_grouped = train_data.groupby([self.song]).agg({self.user: 'count'}).reset_index()
        train_data_grouped.rename(columns={'user': 'score'}, inplace=True)

        # Sort the songs based upon recommendation score
        train_data_sort = train_data_grouped.sort_values(['score', self.song], ascending=[0, 1])

        # Generate a recommendation rank based upon score
        train_data_sort['Rank'] = train_data_sort['score'].rank(ascending=0, method='first')

        # Get the top 10 recommendations
        self.popularity_recommendations = train_data_sort.head(10)

    # Use the popularity based recommender system model to
    # make recommendations
    def recommend(self, user):
        user_recommendations = self.popularity_recommendations

        # Add user column for which the recommendations are being generated
        user_recommendations['user'] = user

        # Bring user column to the front
        cols = user_recommendations.columns.tolist()
        cols = cols[-1:] + cols[:-1]
        user_recommendations = user_recommendations[cols]

        return user_recommendations


# Class for song similarity based Recommender System model
class song_similarity_recommender_py():
    def __init__(self):
        self.train_data = None
        self.user = None
        self.song = None
        self.cooccurence_matrix = None
        self.songs_dict = None
        self.rev_songs_dict = None
        self.song_similarity_recommendations = None

    # Get unique songs (songs) corresponding to a given user
    def get_user_songs(self, user):
        user_data = self.train_data[self.train_data[self.user] == user]
        user_songs = list(user_data[self.song].unique())

        return user_songs

    # Get unique users for a given song (song)
    def get_song_users(self, song):
        song_data = self.train_data[self.train_data[self.song] == song]
        song_users = set(song_data[self.user].unique())

        return song_users

    # Get unique songs (songs) in the training data
    def get_all_songs_train_data(self):
        all_songs = list(self.train_data[self.song].unique())

        return all_songs

    # Construct cooccurence matrix
    def construct_cooccurence_matrix(self, user_songs, all_songs):

        ####################################
        # Get users for all songs in user_songs.
        ####################################
        user_songs_users = []
        for i in range(0, len(user_songs)):
            user_songs_users.append(self.get_song_users(user_songs[i]))

        ###############################################
        # Initialize the song cooccurence matrix of size
        # len(user_songs) X len(songs)
        ###############################################
        cooccurence_matrix = np.matrix(np.zeros(shape=(len(user_songs), len(all_songs))), float)

        #############################################################
        # Calculate similarity between user songs and all unique songs
        # in the training data
        #############################################################
        for i in range(0, len(all_songs)):
            # Calculate unique listeners (users) of song (song) i
            songs_i_data = self.train_data[self.train_data[self.song] == all_songs[i]]
            users_i = set(songs_i_data[self.user].unique())

            for j in range(0, len(user_songs)):

                # Get unique listeners (users) of song (song) j
                users_j = user_songs_users[j]

                # Calculate intersection of listeners of songs i and j
                users_intersection = users_i.intersection(users_j)

                # Calculate cooccurence_matrix[i,j] as Jaccard Index
                if len(users_intersection) != 0:
                    # Calculate union of listeners of songs i and j
                    users_union = users_i.union(users_j)

                    cooccurence_matrix[j, i] = float(len(users_intersection)) / float(len(users_union))
                else:
                    cooccurence_matrix[j, i] = 0

        return cooccurence_matrix

    # Use the cooccurence matrix to make top recommendations
    def generate_top_recommendations(self, user, cooccurence_matrix, all_songs, user_songs):
        print("Non zero values in cooccurence_matrix :%d" % np.count_nonzero(cooccurence_matrix))

        # Calculate a weighted average of the scores in cooccurence matrix for all user songs.
        user_sim_scores = cooccurence_matrix.sum(axis=0) / float(cooccurence_matrix.shape[0])
        user_sim_scores = np.array(user_sim_scores)[0].tolist()

        # Sort the indices of user_sim_scores based upon their value
        # Also maintain the corresponding score
        sort_index = sorted(((e, i) for i, e in enumerate(list(user_sim_scores))), reverse=True)

        # Create a dataframe from the following
        columns = ['user', 'song', 'score', 'rank']
        # index = np.arange(1) # array of numbers for the number of samples
        df = pandas.DataFrame(columns=columns)

        # Fill the dataframe with top 10 song based recommendations
        rank = 1
        for i in range(0, len(sort_index)):
            if ~np.isnan(sort_index[i][0]) and all_songs[sort_index[i][1]] not in user_songs and rank <= 10:
                df.loc[len(df)] = [user, all_songs[sort_index[i][1]], sort_index[i][0], rank]
                rank = rank + 1

        # Handle the case where there are no recommendations
        if df.shape[0] == 0:
            print("The current user has no songs for training the song similarity based recommendation model.")
            return -1
        else:
            return df

    # Create the song similarity based recommender system model
    def create(self, train_data, user, song):
        self.train_data = train_data
        self.user = user
        self.song = song

    # Use the song similarity based recommender system model to
    # make recommendations
    def recommend(self, user):

        ########################################
        # A. Get all unique songs for this user
        ########################################
        user_songs = self.get_user_songs(user)

        print("No. of unique songs for the user: %d" % len(user_songs))

        ######################################################
        # B. Get all unique songs (songs) in the training data
        ######################################################
        all_songs = self.get_all_songs_train_data()

        print("no. of unique songs in the training set: %d" % len(all_songs))

        ###############################################
        # C. Construct song cooccurence matrix of size
        # len(user_songs) X len(songs)
        ###############################################
        cooccurence_matrix = self.construct_cooccurence_matrix(user_songs, all_songs)

        #######################################################
        # D. Use the cooccurence matrix to make recommendations
        #######################################################
        df_recommendations = self.generate_top_recommendations(user, cooccurence_matrix, all_songs, user_songs)

        return df_recommendations

    # Get similar songs to given songs
    def get_similar_songs(self, song_list):

        user_songs = song_list

        ######################################################
        # B. Get all unique songs (songs) in the training data
        ######################################################
        all_songs = self.get_all_songs_train_data()

        print("no. of unique songs in the training set: %d" % len(all_songs))

        ###############################################
        # C. Construct song cooccurence matrix of size
        # len(user_songs) X len(songs)
        ###############################################
        cooccurence_matrix = self.construct_cooccurence_matrix(user_songs, all_songs)

        #######################################################
        # D. Use the cooccurence matrix to make recommendations
        #######################################################
        user = ""
        df_recommendations = self.generate_top_recommendations(user, cooccurence_matrix, all_songs, user_songs)

        return df_recommendations