public Statistic GetStatistic(StatisticFunc statisticFunc, Filter filter)
        {
            if (filter.GroupingPeriod == null)
            {
                filter.GroupingPeriod = "date";
            }
            if (filter.Per == null)
            {
                filter.Per = "none";
            }

            var senders = messageService.GetAllSenders();

            var statistic = Get(statisticFunc, senders, filter.FromDate, filter.ToDate, filter.GroupingPeriod);

            if (filter.Per != "none")
            {
                Statistic perStatistic;
                switch (filter.Per)
                {
                case "message":
                    perStatistic = this.Get(statisticFuncsService.TotalNumberOfMessages(), senders, filter.FromDate, filter.ToDate, filter.GroupingPeriod);
                    break;

                case "word":
                    perStatistic = this.Get(statisticFuncsService.TotalNumberOfWords(), senders, filter.FromDate, filter.ToDate, filter.GroupingPeriod);
                    break;

                case "character":
                    perStatistic = this.Get(statisticFuncsService.TotalNumberOfCharacters(), senders, filter.FromDate, filter.ToDate, filter.GroupingPeriod);
                    break;

                default:
                    throw new ArgumentOutOfRangeException("Per cannot be: " + filter.Per);
                }

                Func <float, float, float> calculatePer;
                if (filter.PerReciprocal)
                {
                    calculatePer = (f, s) => f == 0 ? 0 : s / f;
                }
                else
                {
                    calculatePer = (f, s) => s == 0 ? 0 : f / s;
                }

                statistic.Total = calculatePer(statistic.Total, perStatistic.Total);

                foreach (var senderId in statistic.TotalBySenders.Keys)
                {
                    if (perStatistic.TotalBySenders.TryGetValue(senderId, out float value))
                    {
                        statistic.TotalBySenders[senderId] = calculatePer(statistic.TotalBySenders[senderId], value);
                    }
                    else
                    {
                        statistic.TotalBySenders[senderId] = 0;
                    }
                }

                for (int i = 0; i < statistic.TimePeriods.Count; i++)
                {
                    var j = perStatistic.TimePeriods.IndexOf(statistic.TimePeriods[i]);
                    if (j == -1)
                    {
                        foreach (var senderId in statistic.ValuesBySendersOnTimePeriods.Keys)
                        {
                            statistic.ValuesBySendersOnTimePeriods[senderId][i] = 0;
                        }
                        statistic.TotalsOnTimePeriods[i] = 0;
                    }
                    else
                    {
                        foreach (var senderId in statistic.ValuesBySendersOnTimePeriods.Keys)
                        {
                            if (perStatistic.ValuesBySendersOnTimePeriods.TryGetValue(senderId, out List <float> forPeriods))
                            {
                                var value = forPeriods[j];
                                statistic.ValuesBySendersOnTimePeriods[senderId][i] = calculatePer(statistic.ValuesBySendersOnTimePeriods[senderId][i], value);
                            }
                            else
                            {
                                statistic.ValuesBySendersOnTimePeriods[senderId][i] = 0;
                            }
                        }

                        statistic.TotalsOnTimePeriods[i] = calculatePer(statistic.TotalsOnTimePeriods[i], perStatistic.TotalsOnTimePeriods[j]);
                    }
                }
            }

            statistic.Filter.GroupingPeriod = filter.GroupingPeriod;

            return(statistic);
        }
        public List <StatisticCacheDTO> GetStatisticCaches(StatisticFunc statisticFunc, DateTime?from, DateTime?to)
        {
            var allMessages = messageService.GetAllMessages().OrderBy(o => o.NormalizedSentDate).Decompile();

            DateTime firstMessageDate;
            DateTime lastMessageDate;

            if (from != null)
            {
                var firstMessage = allMessages.FirstOrDefault(o => o.NormalizedSentDate >= from.Value);
                if (firstMessage == null)
                {
                    return(new List <StatisticCacheDTO>());
                }

                firstMessageDate = firstMessage.NormalizedSentDate;
            }
            else
            {
                firstMessageDate = allMessages.FirstOrDefault().NormalizedSentDate;
            }

            if (to != null)
            {
                var lastMessage = allMessages.LastOrDefault(o => o.NormalizedSentDate <= to.Value);
                if (lastMessage == null)
                {
                    return(new List <StatisticCacheDTO>());
                }

                lastMessageDate = lastMessage.NormalizedSentDate;
            }
            else
            {
                lastMessageDate = allMessages.LastOrDefault().NormalizedSentDate;
            }

            if (firstMessageDate > lastMessageDate)
            {
                return(new List <StatisticCacheDTO>());
            }

            var caches = mainDbContext.StatisticCaches
                         .Include(o => o.ForSenders)
                         .Where(o => o.StatisticName == statisticFunc.Name)
                         .Where(o => o.ForDate >= firstMessageDate && o.ForDate <= lastMessageDate).ToList();

            var senders = messageService.GetAllSenders();

            var days = Math.Round((lastMessageDate - firstMessageDate).TotalDays) + 1;

            if (caches.Count < days)
            {
                var messages = messageService.GetAllMessages().FilterDateRange(firstMessageDate, lastMessageDate).ToList();
                for (int i = 0; i < days; i++)
                {
                    var date = firstMessageDate.AddDays(i);

                    if (caches.Find(o => o.ForDate == date) == null)
                    {
                        var newCache = new StatisticCache()
                        {
                            StatisticName = statisticFunc.Name,
                            ForDate       = date,
                            ForSenders    = senders.Select(sender => new StatisticCacheForSender()
                            {
                                SenderId = sender.Id,
                                Total    = statisticFunc.Func(messages.AsQueryable().Filter(sender, date))
                            }).ToList()
                        };
                        mainDbContext.StatisticCaches.Add(newCache);
                        caches.Add(newCache);
                    }
                }

                mainDbContext.SaveChanges();
            }

            var result = caches.Select(o => new StatisticCacheDTO()
            {
                Id               = o.Id,
                ForDate          = o.ForDate,
                StatisticName    = o.StatisticName,
                TotalsForSenders = senders.ToDictionary(s => s.Id, s =>
                {
                    var forSender = o.ForSenders.FirstOrDefault(f => f.Sender == s);
                    if (forSender != null)
                    {
                        return(forSender.Total);
                    }
                    else
                    {
                        return(0);
                    }
                })
            }).ToList();

            return(result);
        }
        private Statistic Get(StatisticFunc statisticFunc, List <Sender> senders, DateTime?fromDate, DateTime?toDate, string groupingPeriod)
        {
            List <DateTime> timePeriods;
            Dictionary <int, List <float> > valuesBySendersOnTimePeriods;

            if (groupingPeriod == "timeOfDay" || groupingPeriod == "hour")
            {
                var filteredMessages = messageService.GetAllMessages().FilterDateRange(fromDate, toDate).ToList();


                Func <Message, DateTime> groupingSelector = null;
                switch (groupingPeriod)
                {
                case "timeOfDay":
                    groupingSelector = o => new DateTime(2021, 1, 1, o.SentDateTime.Hour, 0, 0);
                    break;

                case "hour":
                    groupingSelector = o => new DateTime(o.SentDateTime.Year, o.SentDateTime.Month, o.SentDateTime.Day, o.SentDateTime.Hour, 0, 0);
                    break;

                default:
                    throw new ArgumentOutOfRangeException("Grouping period cannot be: " + groupingPeriod);
                }

                var tp = timePeriods = filteredMessages.GroupBy(groupingSelector).Distinct().Select(o => o.Key).OrderBy(o => o).ToList();

                valuesBySendersOnTimePeriods =
                    senders
                    .ToDictionary(o => o.Id, o => tp
                                  .Select(p => filteredMessages
                                          .AsQueryable()
                                          .Filter(o)
                                          .Where(o => groupingSelector(o) == p))
                                  .AsParallel()
                                  .AsOrdered()
                                  .Select(i => (float)statisticFunc.Func(i))
                                  .ToList());
            }
            else
            {
                var caches = statisticCacheService.GetStatisticCaches(statisticFunc, fromDate, toDate);

                Func <StatisticCacheDTO, DateTime> groupingSelector = null;
                switch (groupingPeriod)
                {
                case "date":
                    groupingSelector = o => o.ForDate;
                    break;

                case "week":
                    groupingSelector = o => o.ForDate.AddDays(-(((int)o.ForDate.DayOfWeek + 6) % 7));
                    break;

                case "month":
                    groupingSelector = o => new DateTime(o.ForDate.Year, o.ForDate.Month, 1);
                    break;

                default:
                    throw new ArgumentOutOfRangeException("Grouping period cannot be: " + groupingPeriod);
                }

                var tp = timePeriods = caches.AsEnumerable().GroupBy(groupingSelector).Distinct().Select(o => o.Key).OrderBy(o => o).ToList();

                valuesBySendersOnTimePeriods =
                    senders
                    .ToDictionary(o => o.Id, s => timePeriods
                                  .Select(p => caches
                                          .Where(o => groupingSelector(o) == p)
                                          .Sum(o => (float)o.TotalsForSenders[s.Id]))
                                  .ToList());
            }

            // izbaci sve dane/sate/tjedne/mjesece u kojima ima totalno 0 poruka/rijeci/znakova
            for (int i = 0; i < timePeriods.Count; i++)
            {
                if (valuesBySendersOnTimePeriods.Sum(o => o.Value[i]) == 0)
                {
                    timePeriods.RemoveAt(i);
                    valuesBySendersOnTimePeriods.ForEach(o => o.Value.RemoveAt(i));
                    i--;
                }
            }

            var          totalBySenders      = valuesBySendersOnTimePeriods.ToDictionary(o => o.Key, o => o.Value.Sum());
            var          total               = totalBySenders.Values.Sum();
            List <float> totalsOnTimePeriods = new List <float>();

            for (int i = 0; i < timePeriods.Count; i++)
            {
                totalsOnTimePeriods.Add(valuesBySendersOnTimePeriods.Sum(o => o.Value[i]));
            }

            var result = new Statistic()
            {
                Total  = total,
                Filter = new Filter()
                {
                    FromDate       = fromDate,
                    ToDate         = toDate,
                    GroupingPeriod = groupingPeriod,
                    Per            = "none"
                },
                Senders        = senders.ToDictionary(s => s.Id, s => SenderDTO.From(s)),
                TotalBySenders = totalBySenders,
                TimePeriods    = timePeriods,
                ValuesBySendersOnTimePeriods = valuesBySendersOnTimePeriods,
                TotalsOnTimePeriods          = totalsOnTimePeriods,
                StatisticName = statisticFunc.Name
            };

            return(result);
        }