/// <summary>
        /// Gets the media objects having all tags specified in the search options. Guaranteed to not return null.
        /// Only media objects the user has permission to view are returned.
        /// </summary>
        /// <returns>An instance of <see cref="IGalleryObjectCollection" />.</returns>
        private IGalleryObjectCollection GetMediaObjectsHavingTags()
        {
            var galleryObjects = new GalleryObjectCollection();

            using (var repo = new MediaObjectRepository())
            {
                var qry = repo.Where(m =>
                                     m.Album.FKGalleryId == SearchOptions.GalleryId &&
                                     m.Metadata.Any(md => md.MetaName == TagType && md.MetadataTags.Any(mdt => SearchOptions.Tags.Contains(mdt.FKTagName))), m => m.Metadata);

                foreach (var moDto in RestrictForCurrentUser(qry))
                {
                    var mediaObject = Factory.GetMediaObjectFromDto(moDto, null);

                    // We have a media object that contains at least one of the tags. If we have multiple tags, do an extra test to ensure
                    // media object matches ALL of them. (I wasn't able to write the LINQ to do this for me, so it's an extra step.)
                    if (SearchOptions.Tags.Length == 1)
                    {
                        galleryObjects.Add(mediaObject);
                    }
                    else if (MetadataItemContainsAllTags(mediaObject.MetadataItems.First(md => md.MetadataItemName == TagType)))
                    {
                        galleryObjects.Add(mediaObject);
                    }
                }
            }

            return(galleryObjects);
        }
        /// <summary>
        /// Finds the gallery objects with the specified rating. Guaranteed to not return null. Albums cannot be
        /// rated and are thus not returned. Only items the current user is authorized to view are returned.
        /// </summary>
        /// <returns><see cref="IEnumerable&lt;IGalleryObject&gt;" />.</returns>
        private IEnumerable <IGalleryObject> FindMediaObjectsMatchingRating()
        {
            var galleryObjects = new GalleryObjectCollection();

            if (SearchOptions.Filter != GalleryObjectType.Album)
            {
                galleryObjects.AddRange(GetRatedMediaObjects(SearchOptions.MaxNumberResults));
            }

            var filteredGalleryObjects = FilterGalleryObjects(galleryObjects);

            if (filteredGalleryObjects.Count != galleryObjects.Count && filteredGalleryObjects.Count < SearchOptions.MaxNumberResults && galleryObjects.Count >= SearchOptions.MaxNumberResults)
            {
                // We lost some objects in the filter and now we have less than the desired MaxNumberResults. Get more.
                // Note: Performance can be very poor for large galleries when using a filter. For example, a gallery where 20 videos
                // were added and then 200,000 images were added, a search for the most recent 20 videos causes this algorithm
                // to load all 200,000 images into memory before finding the videos. The good news is that by default the filter
                // is for media objects, which will be very fast. If filters end up being commonly used, this algorithm should be improved.
                var       max      = SearchOptions.MaxNumberResults * 2;
                var       skip     = SearchOptions.MaxNumberResults;
                const int maxTries = 5;

                for (var i = 0; i < maxTries; i++)
                {
                    // Add items up to maxTries times, each time doubling the number of items to retrieve.
                    filteredGalleryObjects.AddRange(GetRatedMediaObjects(max, skip));

                    filteredGalleryObjects = FilterGalleryObjects(filteredGalleryObjects);

                    if (filteredGalleryObjects.Count >= SearchOptions.MaxNumberResults)
                    {
                        break;
                    }

                    if (i < (maxTries - 1))
                    {
                        skip = skip + max;
                        max  = max * 2;
                    }
                }

                if (filteredGalleryObjects.Count < SearchOptions.MaxNumberResults)
                {
                    // We still don't have enough objects. Search entire set of albums and media objects.
                    filteredGalleryObjects.AddRange(GetRatedMediaObjects(int.MaxValue, skip));

                    filteredGalleryObjects = FilterGalleryObjects(filteredGalleryObjects);
                }
            }

            if (SearchOptions.MaxNumberResults > 0 && filteredGalleryObjects.Count > SearchOptions.MaxNumberResults)
            {
                return(filteredGalleryObjects.OrderByDescending(g => g.DateAdded).Take(SearchOptions.MaxNumberResults));
            }

            return(filteredGalleryObjects);
        }
        /// <summary>
        /// Wraps the <paramref name="album" /> in a gallery object collection. When <paramref name="album" /> is null,
        /// an empty collection is returned. Guaranteed to no return null.
        /// </summary>
        /// <param name="album">The album.</param>
        /// <returns>An instance of <see cref="IGalleryObjectCollection" />.</returns>
        private static IEnumerable <IGalleryObject> WrapInGalleryObjectCollection(IAlbum album)
        {
            var result = new GalleryObjectCollection();

            if (album != null)
            {
                result.Add(album);
            }

            return(result);
        }
        /// <summary>
        /// Finds the gallery objects matching tags. Guaranteed to not return null. Call this function only when the search type
        /// is <see cref="GalleryObjectSearchType.SearchByTag" /> or <see cref="GalleryObjectSearchType.SearchByPeople" />.
        /// Only items the user has permission to view are returned.
        /// </summary>
        /// <returns>An instance of <see cref="IGalleryObjectCollection" />.</returns>
        private IEnumerable <IGalleryObject> FindItemsMatchingTags()
        {
            var galleryObjects = new GalleryObjectCollection();

            if (SearchOptions.Filter == GalleryObjectType.All || SearchOptions.Filter == GalleryObjectType.Album)
            {
                galleryObjects.AddRange(GetAlbumsHavingTags());
            }

            if (SearchOptions.Filter != GalleryObjectType.Album)
            {
                galleryObjects.AddRange(GetMediaObjectsHavingTags());
            }

            var filteredGalleryObjects = FilterGalleryObjects(galleryObjects);

            return(SearchOptions.MaxNumberResults > 0 ? filteredGalleryObjects.ToSortedList().Take(SearchOptions.MaxNumberResults) : filteredGalleryObjects);
        }
        /// <summary>
        /// Gets the <paramref name="top" /> most recently added media objects, skipping the first
        /// <paramref name="skip" /> objects. Only media objects the current user is authorized to
        /// view are returned.
        /// </summary>
        /// <param name="top">The number of items to retrieve.</param>
        /// <param name="skip">The number of items to skip over in the data store.</param>
        /// <returns><see cref="IEnumerable&lt;IGalleryObject&gt;" />.</returns>
        private IEnumerable <IGalleryObject> GetRecentlyAddedMediaObjects(int top, int skip = 0)
        {
            var galleryObjects = new GalleryObjectCollection();

            using (var repo = new MediaObjectRepository())
            {
                var qry = RestrictForCurrentUser(repo.Where(mo => mo.Album.FKGalleryId == SearchOptions.GalleryId)
                                                 .OrderByDescending(m => m.DateAdded))
                          .Skip(skip).Take(top)
                          .Include(m => m.Metadata);

                foreach (var mediaObject in qry)
                {
                    galleryObjects.Add(Factory.GetMediaObjectFromDto(mediaObject, null));
                }
            }

            return(galleryObjects);
        }
        private IEnumerable <IGalleryObject> GetMediaObjectsMatchingKeywords()
        {
            var galleryObjects = new GalleryObjectCollection();

            using (var repo = new MediaObjectRepository())
            {
                var qry = repo.Where(a => true, a => a.Metadata);

                qry = SearchOptions.SearchTerms.Aggregate(qry, (current, searchTerm) => current.Where(mo =>
                                                                                                      mo.Album.FKGalleryId == SearchOptions.GalleryId &&
                                                                                                      mo.Metadata.Any(md => md.Value.Contains(searchTerm))));

                qry = RestrictForCurrentUser(qry);

                foreach (var mediaObject in qry)
                {
                    galleryObjects.Add(Factory.GetMediaObjectFromDto(mediaObject, null));
                }
            }

            return(galleryObjects);
        }
        private IEnumerable <IGalleryObject> GetMediaObjectsHavingTitleOrCaption()
        {
            var galleryObjects = new GalleryObjectCollection();

            var metaTagsToSearch = new[] { MetadataItemName.Title, MetadataItemName.Caption };

            using (var repo = new MediaObjectRepository())
            {
                var qry = repo.Where(a => true, a => a.Metadata);

                qry = RestrictForCurrentUser(qry);

                qry = SearchOptions.SearchTerms.Aggregate(qry, (current, searchTerm) => current.Where(mo =>
                                                                                                      mo.Album.FKGalleryId == SearchOptions.GalleryId &&
                                                                                                      mo.Metadata.Any(md => metaTagsToSearch.Contains(md.MetaName) && md.Value.Contains(searchTerm))));

                foreach (var mediaObject in qry)
                {
                    galleryObjects.Add(Factory.GetMediaObjectFromDto(mediaObject, null));
                }
            }

            return(galleryObjects);
        }
        /// <summary>
        /// Gets the <paramref name="top" /> media objects having the specified rating, skipping the first
        /// <paramref name="skip" /> objects. Only media objects the current user is authorized to
        /// view are returned. Albums cannot be rated and are thus not returned.
        /// </summary>
        /// <param name="top">The number of items to retrieve.</param>
        /// <param name="skip">The number of items to skip over in the data store.</param>
        /// <returns><see cref="IEnumerable&lt;IGalleryObject&gt;" />.</returns>
        private IEnumerable <IGalleryObject> GetRatedMediaObjects(int top, int skip = 0)
        {
            var galleryObjects = new GalleryObjectCollection();

            if (AppSetting.Instance.ProviderDataStore == ProviderDataStore.SqlCe)
            {
                // SQL CE does not support the queries below, so throw an error. We should never get here because Utils.GetTopRatedUrl()
                // only generates an URL for SQL Server DB's, but better safe than sorry. FYI, these are the errors SQL CE generates:
                // Caused by String.IsNullOrEmpty(md.Value): "The specified argument value for the function is not valid. [ Argument # = 1,Name of function(if known) = LEN ]"
                // Caused by using md.Value in OrderBy: "Large objects (ntext and image) cannot be used in ORDER BY clauses."
                // One way to make these queries work is to change Metadata.Value from NTEXT to NVARCHAR(4000), but this is undesirable because we
                // want the max length to handle long meta values, especially in SQL Server, where these queries work fine. I looked into adding
                // conditional logic to change the column definition for SQL CE but leaving it unchanged for SQL Server, but it was complex and had risk.
                throw new NotSupportedException("SQL CE does not support the query syntax used in GalleryObjectSearcher.GetRatedMediaObjects(). Use SQL Server instead.");
            }

            using (var repo = new MediaObjectRepository())
            {
                IQueryable <MediaObjectDto> qry;

                switch (SearchOptions.SearchTerms[0].ToLowerInvariant())
                {
                case "highest": // Highest rated objects
                    qry = RestrictForCurrentUser(repo.Where(mo =>
                                                            mo.Album.FKGalleryId == SearchOptions.GalleryId &&
                                                            mo.Metadata.Any(md => md.MetaName == MetadataItemName.Rating && !String.IsNullOrEmpty(md.Value)))
                                                 .OrderByDescending(mo => mo.Metadata.Where(md => md.MetaName == MetadataItemName.Rating).Select(md => md.Value).FirstOrDefault())
                                                 .Include(mo => mo.Metadata)
                                                 .Skip(skip).Take(top));
                    break;

                case "lowest": // Lowest rated objects
                    qry = RestrictForCurrentUser(repo.Where(mo =>
                                                            mo.Album.FKGalleryId == SearchOptions.GalleryId
                                                            //&& mo.Metadata.Any(md => md.MetaName == MetadataItemName.Rating && md.Value == "5"))
                                                            && mo.Metadata.Any(md => md.MetaName == MetadataItemName.Rating && !String.IsNullOrEmpty(md.Value)))
                                                 .OrderBy(mo => mo.Metadata.Where(md => md.MetaName == MetadataItemName.Rating).Select(md => md.Value).FirstOrDefault())
                                                 .Include(mo => mo.Metadata)
                                                 .Skip(skip).Take(top));
                    break;

                case "none": // Having no rating
                    qry = RestrictForCurrentUser(repo.Where(mo =>
                                                            mo.Album.FKGalleryId == SearchOptions.GalleryId &&
                                                            mo.Metadata.Any(md => md.MetaName == MetadataItemName.Rating && String.IsNullOrEmpty(md.Value)))
                                                 .OrderBy(mo => mo.DateAdded)
                                                 .Include(mo => mo.Metadata)
                                                 .Skip(skip).Take(top));
                    break;

                default: // Look for a specific rating
                    var r = ParseRating(SearchOptions.SearchTerms[0]);
                    if (r != null)
                    {
                        qry = RestrictForCurrentUser(repo.Where(mo =>
                                                                mo.Album.FKGalleryId == SearchOptions.GalleryId &&
                                                                mo.Metadata.Any(md => md.MetaName == MetadataItemName.Rating && r.Contains(md.Value)))
                                                     .OrderBy(mo => mo.DateAdded)
                                                     .Include(mo => mo.Metadata)
                                                     .Skip(skip).Take(top));
                    }
                    else
                    {
                        // The search term is a string other than highest, lowest, none or a decimal. Don't return anything.
                        qry = repo.Where(mo => false);
                    }
                    break;
                }

                foreach (var mediaObject in qry)
                {
                    galleryObjects.Add(Factory.GetMediaObjectFromDto(mediaObject, null));
                }
            }

            return(galleryObjects);
        }
        /// <summary>
        /// Gets the <paramref name="top" /> media objects having the specified rating, skipping the first
        /// <paramref name="skip" /> objects. Only media objects the current user is authorized to
        /// view are returned. Albums cannot be rated and are thus not returned.
        /// </summary>
        /// <param name="top">The number of items to retrieve.</param>
        /// <param name="skip">The number of items to skip over in the data store.</param>
        /// <returns><see cref="IEnumerable&lt;IGalleryObject&gt;" />.</returns>
        /// <remarks>
        /// SQL CE does not support the queries used in this function. We should never get here because Utils.GetTopRatedUrl()
        /// only generates an URL for SQL Server DB's, but better safe than sorry. FYI, these are the errors SQL CE generates:
        /// Caused by String.IsNullOrEmpty(md.Value): "The specified argument value for the function is not valid. [ Argument # = 1,Name of function(if known) = LEN ]"
        /// Caused by using md.Value in OrderBy: "Large objects (ntext and image) cannot be used in ORDER BY clauses."
        /// One way to make these queries work is to change Metadata.Value from NTEXT to NVARCHAR(4000), but this is undesirable because we
        /// want the max length to handle long meta values, especially in SQL Server, where these queries work fine. I looked into adding
        /// conditional logic to change the column definition for SQL CE but leaving it unchanged for SQL Server, but it was complex and had risk.
        /// </remarks>
        private IEnumerable <IGalleryObject> GetRatedMediaObjects(int top, int skip = 0)
        {
            var galleryObjects = new GalleryObjectCollection();

            if (AppSetting.Instance.ProviderDataStore == ProviderDataStore.SqlCe)
            {
                throw new NotSupportedException("SQL CE does not support the query syntax used in GalleryObjectSearcher.GetRatedMediaObjects(). Use SQL Server instead.");
            }

            using (var repo = new MediaObjectRepository())
            {
                IQueryable <MediaObjectDto> qry;

                switch (SearchOptions.SearchTerms[0].ToLowerInvariant())
                {
                case "highest":     // Highest rated objects
                    qry = RestrictForCurrentUser(repo.Where(mo =>
                                                            mo.Album.FKGalleryId == SearchOptions.GalleryId &&
                                                            mo.Metadata.Any(md => md.MetaName == MetadataItemName.Rating && !String.IsNullOrEmpty(md.Value)))
                                                 .OrderByDescending(mo => mo.Metadata.Where(md => md.MetaName == MetadataItemName.Rating).Select(md => md.Value).FirstOrDefault())
                                                 .Include(mo => mo.Metadata)
                                                 .Skip(skip).Take(top));
                    break;

                case "lowest":     // Lowest rated objects
                    qry = RestrictForCurrentUser(repo.Where(mo =>
                                                            mo.Album.FKGalleryId == SearchOptions.GalleryId
                                                            //&& mo.Metadata.Any(md => md.MetaName == MetadataItemName.Rating && md.Value == "5"))
                                                            && mo.Metadata.Any(md => md.MetaName == MetadataItemName.Rating && !String.IsNullOrEmpty(md.Value)))
                                                 .OrderBy(mo => mo.Metadata.Where(md => md.MetaName == MetadataItemName.Rating).Select(md => md.Value).FirstOrDefault())
                                                 .Include(mo => mo.Metadata)
                                                 .Skip(skip).Take(top));
                    break;

                case "none":     // Having no rating
                    qry = RestrictForCurrentUser(repo.Where(mo =>
                                                            mo.Album.FKGalleryId == SearchOptions.GalleryId &&
                                                            mo.Metadata.Any(md => md.MetaName == MetadataItemName.Rating && String.IsNullOrEmpty(md.Value)))
                                                 .OrderBy(mo => mo.DateAdded)
                                                 .Include(mo => mo.Metadata)
                                                 .Skip(skip).Take(top));
                    break;

                default:     // Look for a specific rating
                    var r = ParseRating(SearchOptions.SearchTerms[0]);
                    if (r != null)
                    {
                        qry = RestrictForCurrentUser(repo.Where(mo =>
                                                                mo.Album.FKGalleryId == SearchOptions.GalleryId &&
                                                                mo.Metadata.Any(md => md.MetaName == MetadataItemName.Rating && r.Contains(md.Value)))
                                                     .OrderBy(mo => mo.DateAdded)
                                                     .Include(mo => mo.Metadata)
                                                     .Skip(skip).Take(top));
                    }
                    else
                    {
                        // The search term is a string other than highest, lowest, none or a decimal. Don't return anything.
                        qry = repo.Where(mo => false);
                    }
                    break;
                }

                foreach (var mediaObject in qry)
                {
                    galleryObjects.Add(Factory.GetMediaObjectFromDto(mediaObject, null));
                }
            }

            return(galleryObjects);
        }