/// <summary>
        /// Spreading activation recommender.
        /// If the user is logged in, we remove books already rated by him from the recommendation.
        /// </summary>
        /// <param name="bookId">Id of book on which will the recommendation be based</param>
        /// <param name="userId">Id of the signed in user</param>
        /// <param name="howMany">Depth of activation spreading</param>
        /// <returns>List of books with the highest activation value</returns>
        public List <int> Recommend(int bookId, string userId, int howMany = 6)
        {
            // initialize the activation graph
            GraphOfBooks graph = new GraphOfBooks();

            graph.AddVertex(bookId, START_ACTIVATION_VALUE);

            List <int> bookIDs = new List <int>();

            bookIDs.Add(bookId);

            // start recursive spreading activation
            RunOneLevelBFS(maxLevel, bookIDs, graph);

            graph.RemoveVertex(bookId);
            List <Tuple <int, double> > bookIDVWithActivationValues =
                graph.ExportBookIDVerticesWithActivationValues();

            BookRecommenderContext      bd = new BookRecommenderContext();
            List <Tuple <int, double> > bookIdsWithActivationValuesWithoutRated =
                removeAlreadyRatedBooks(bookIDVWithActivationValues, userId, bd);

            // sorting books according to their importance from the highest to the lowest activation value
            List <Tuple <int, double> > sortedBookIdsWithActivationValues =
                bookIdsWithActivationValuesWithoutRated.OrderBy(v => v.Item2).Reverse().ToList();

            // return sorted sublist of recommendation
            return(sortedBookIdsWithActivationValues.Select(v => v.Item1).Take(howMany).ToList());
        }
        private void RunOneLevelBFS(int levelI, List <int> bookIDsPreviousLevel,
                                    GraphOfBooks graph)
        {
            // end of recursion
            if (levelI == 0)
            {
                return;
            }

            SpreadingRecommenderCache model = simCacheModel.spreadingRecommenderCache;

            // get next level of similar verteces
            List <int> bookIDsForNextLevel = new List <int>();

            foreach (int bookIdFromI in bookIDsPreviousLevel)
            {
                List <Tuple <int, int> > simBooksWithQuantitiesUnsortedI =
                    model.GetSimilaritiesBooksWithQuantitiesByAll(bookIdFromI);

                List <Tuple <int, int> > simBooksWithQuantitiesSortedI =
                    simBooksWithQuantitiesUnsortedI.OrderBy(v => v.Item2).Reverse().ToList();

                List <Tuple <int, int> > simBooksWithQuantitiesI =
                    simBooksWithQuantitiesSortedI.Take(numberOfSimilarNeighbors).ToList();

                // add neighbors for next level
                bookIDsForNextLevel.AddRange(
                    simBooksWithQuantitiesI.Select(b => b.Item1));

                // vertex was inserted to graph in the last iteration
                double?activationValueObjI = graph.GetActivationValueOfVertex(bookIdFromI);

                int numberOfneighborsI = simBooksWithQuantitiesI.Select(b => b.Item2).Sum();

                if (numberOfneighborsI == 0)
                {
                    continue;
                }

                // update current vertex
                graph.SetVertex(bookIdFromI, activationValueObjI.Value * MULTIPLICATOR_OF_ACTIVATION_PRESERVING);

                // computes difference of activation value for distribution to neighbors
                double activationValueForDistributionI =
                    activationValueObjI.Value * (1 - MULTIPLICATOR_OF_ACTIVATION_PRESERVING) / numberOfneighborsI;

                // re-compute weights of neighbors
                foreach (Tuple <int, int> neighborBookIdsWithQuantityI in simBooksWithQuantitiesI)
                {
                    int    neighborBookIdI   = neighborBookIdsWithQuantityI.Item1;
                    double neighborQuantityI = neighborBookIdsWithQuantityI.Item2;

                    double activationValueDiff =
                        neighborQuantityI * activationValueForDistributionI;

                    graph.IncreaseActivationValueOfVertex(neighborBookIdI, activationValueDiff);
                }
            }

            List <Tuple <int, int> > bookIDsForNextLevelAndTheirQuantities = bookIDsForNextLevel.GroupBy(b => b)
                                                                             .Select(group => new Tuple <int, int>(group.Key, group.Count())).ToList();

            List <Tuple <int, int> > sortedBookIDsForNextLevelAndTheirQuantities =
                bookIDsForNextLevelAndTheirQuantities.OrderBy(v => v.Item2).Reverse().ToList();


            List <int> bookIDsForNextLevelNoDuplicate = sortedBookIDsForNextLevelAndTheirQuantities
                                                        .Select(b => b.Item1).Take(numberOfNeighborsForNextLevel).ToList();

            if (DEBUG)
            {
                System.Console.WriteLine($"Level {levelI} finished, next leven itemCount: {bookIDsPreviousLevel.Count}");
                graph.printTopKOfGraph(10);
            }

            // recursion for next level
            RunOneLevelBFS(levelI - 1, bookIDsForNextLevelNoDuplicate, graph);
        }