private static void DetermineCategoryChanges(
            CategoryGroup categoryGroup,
            string originalName,
            string updatedName,
            ProfileChangeResult result)
        {
            // Categories are stored with case insensitive keys to avoid duplicates
            // So we don't care if just the case has changed for the category link
            if (HasStringChanged(originalName, updatedName, StringComparison.OrdinalIgnoreCase))
            {
                if (string.IsNullOrWhiteSpace(originalName) == false)
                {
                    // There was previous a gender assigned
                    result.AddChange(categoryGroup, originalName, CategoryLinkChangeType.Remove);

                    result.ProfileChanged = true;
                }

                if (string.IsNullOrWhiteSpace(updatedName) == false)
                {
                    // There is a new gender assigned
                    result.AddChange(categoryGroup, updatedName, CategoryLinkChangeType.Add);

                    result.ProfileChanged = true;
                }
            }
        }
        private static void DetermineCategoryChanges(Profile original, UpdatableProfile updated,
                                                     bool findAllCategoryChanges, ProfileChangeResult result)
        {
            // Find the category changes
            DetermineCategoryChanges(CategoryGroup.Gender, original.Gender, updated.Gender, result);

            if (findAllCategoryChanges == false &&
                result.ProfileChanged)
            {
                // We only need to find a single change which we have
                return;
            }

            // Check for changes to languages
            DetermineCategoryChanges(CategoryGroup.Language, original.Languages, updated.Languages,
                                     findAllCategoryChanges, result);

            if (findAllCategoryChanges == false &&
                result.ProfileChanged)
            {
                // We only need to find a single change which we have
                return;
            }

            // Check for changes to skills
            var originalSkillNames = original.Skills.Select(x => x.Name).ToList();
            var updatedSkillNames  = updated.Skills.Select(x => x.Name).ToList();

            DetermineCategoryChanges(CategoryGroup.Skill, originalSkillNames, updatedSkillNames, findAllCategoryChanges,
                                     result);
        }
        public ProfileChangeResult RemoveAllCategoryLinks(UpdatableProfile profile)
        {
            Ensure.Any.IsNotNull(profile, nameof(profile));

            var result = new ProfileChangeResult();

            DetermineAllCategoryRemovalChanges(profile, result);

            return(result);
        }
        public async Task Execute(Profile profile, ProfileChangeResult changes, CancellationToken cancellationToken)
        {
            Ensure.Any.IsNotNull(profile, nameof(profile));
            Ensure.Any.IsNotNull(changes, nameof(changes));

            if (changes.CategoryChanges.Count > 0)
            {
                await UpdateCategories(profile, changes, cancellationToken).ConfigureAwait(false);
            }

            if (changes.ProfileChanged)
            {
                await UpdateProfile(profile, cancellationToken).ConfigureAwait(false);
            }
        }
        private static void DetermineAllCategoryAddChanges(UpdatableProfile profile, ProfileChangeResult result)
        {
            // Remove gender link
            DetermineCategoryChanges(CategoryGroup.Gender, null, profile.Gender, result);

            // Check for changes to languages
            var emptyCategoryNames = new List <string>();

            // Remove all language links
            DetermineCategoryChanges(CategoryGroup.Language, emptyCategoryNames, profile.Languages, true, result);

            var skillNames = profile.Skills.Select(x => x.Name).ToList();

            // Remove all skill links
            DetermineCategoryChanges(CategoryGroup.Skill, emptyCategoryNames, skillNames, true, result);
        }
        private static void DetermineCategoryChanges(CategoryGroup categoryGroup, ICollection <string> originalNames,
                                                     ICollection <string> updatedNames, bool findAllCategoryChanges, ProfileChangeResult result)
        {
            foreach (var originalName in originalNames)
            {
                var matchingUpdatedName =
                    updatedNames.FirstOrDefault(x => x.Equals(originalName, StringComparison.OrdinalIgnoreCase));

                if (matchingUpdatedName == null)
                {
                    // This category has been removed from profile
                    result.AddChange(categoryGroup, originalName, CategoryLinkChangeType.Remove);

                    result.ProfileChanged = true;
                }

                if (findAllCategoryChanges == false &&
                    result.ProfileChanged)
                {
                    // We only need to find a single change which we have
                    return;
                }
            }

            foreach (var updatedName in updatedNames)
            {
                var matchingOriginalName =
                    originalNames.FirstOrDefault(x => x.Equals(updatedName, StringComparison.OrdinalIgnoreCase));

                if (matchingOriginalName == null)
                {
                    // This category has been added to the profile
                    result.AddChange(categoryGroup, updatedName, CategoryLinkChangeType.Add);

                    result.ProfileChanged = true;
                }

                if (findAllCategoryChanges == false &&
                    result.ProfileChanged)
                {
                    // We only need to find a single change which we have
                    return;
                }
            }
        }
        public ProfileChangeResult CalculateChanges(Profile original, UpdatableProfile updated)
        {
            Ensure.Any.IsNotNull(original, nameof(original));
            Ensure.Any.IsNotNull(updated, nameof(updated));

            var result = new ProfileChangeResult();
            var canUpdateCategoryLinks = true;

            if (original.Status == ProfileStatus.Hidden &&
                updated.Status == ProfileStatus.Hidden)
            {
                // We don't calculate any changes to category links for a hidden profiles that are still hidden
                // If we do look for category links to determine if the profile should be saved, we can exit once at least one is found
                canUpdateCategoryLinks = false;
            }
            else if (original.BannedAt != null)
            {
                // We don't calculate any changes to category links for a banned profile
                // If we do look for category links to determine if the profile should be saved, we can exit once at least one is found
                canUpdateCategoryLinks = false;
            }

            if (original.Status != ProfileStatus.Hidden &&
                updated.Status == ProfileStatus.Hidden)
            {
                // The profile is being hidden
                // Remove all the existing category links
                // We will remove all the links from the original profile
                // This is because if there are any changes from the original profile to the updated profile,
                // they aren't stored yet in the links so we don't care
                DetermineAllCategoryRemovalChanges(original, result);
            }
            else if (original.Status == ProfileStatus.Hidden &&
                     updated.Status != ProfileStatus.Hidden)
            {
                // The profile is being displayed after being hidden
                // We need to add all the links to the updated profile
                DetermineAllCategoryAddChanges(updated, result);
            }
            else
            {
                // Find the category changes between the original and updated profiles
                DetermineCategoryChanges(original, updated, canUpdateCategoryLinks, result);
            }

            if (result.ProfileChanged == false)
            {
                result.ProfileChanged = HasProfileChanged(original, updated);
            }

            if (result.ProfileChanged == false)
            {
                // The profile properties have not changed
                // We haven't checked for category items added or removed yet, but we also need to check
                // if any skills have been changed
                // Search for changes to skill metadata
                result.ProfileChanged = HaveSkillsChanged(original.Skills, updated.Skills);
            }

            if (canUpdateCategoryLinks == false)
            {
                // We may have calculated category link changes in order to figure out if the profile should be saved
                // but we are not going make any category link changes so we need to clear them out
                result.CategoryChanges.Clear();
            }

            return(result);
        }
        private async Task UpdateCategories(
            Profile profile,
            ProfileChangeResult changes,
            CancellationToken cancellationToken)
        {
            var cacheItemsToRemove = new List <ProfileFilter>();

            // Get the current categories
            var categories =
                (await _categoryStore.GetAllCategories(cancellationToken).ConfigureAwait(false)).FastToList();

            var categoryTasks = new List <Task>();

            // Write all category link changes
            foreach (var categoryChange in changes.CategoryChanges)
            {
                // Get the category
                var category = categories.FirstOrDefault(
                    x => x.Group == categoryChange.CategoryGroup && x.Name.Equals(
                        categoryChange.CategoryName,
                        StringComparison.OrdinalIgnoreCase));

                if (category == null)
                {
                    // We haven't seen this category before
                    category = new Category
                    {
                        Group     = categoryChange.CategoryGroup,
                        LinkCount = 0,
                        Name      = categoryChange.CategoryName,
                        Reviewed  = false,
                        Visible   = false
                    };

                    // Trigger a notification that a new category is being added to the system
                    var newCategoryTriggerTask = _eventTrigger.NewCategory(category, cancellationToken);

                    categoryTasks.Add(newCategoryTriggerTask);

                    categories.Add(category);
                }

                if (categoryChange.ChangeType == CategoryLinkChangeType.Add)
                {
                    // This is a new link between the profile and the category
                    category.LinkCount++;
                }
                else
                {
                    // We are removing the link between the profile and the category
                    category.LinkCount--;
                }

                var change = new CategoryLinkChange
                {
                    ChangeType = categoryChange.ChangeType,
                    ProfileId  = profile.Id
                };

                // Store the link update
                var categoryLinkTask = _linkStore.StoreCategoryLink(
                    category.Group,
                    category.Name,
                    change,
                    cancellationToken);

                categoryTasks.Add(categoryLinkTask);

                var filter = new ProfileFilter
                {
                    CategoryGroup = category.Group,
                    CategoryName  = category.Name
                };

                cacheItemsToRemove.Add(filter);

                // Update the category data
                var categoryTask = _categoryStore.StoreCategory(category, cancellationToken);

                categoryTasks.Add(categoryTask);
            }

            // Run all the category task changes together
            await Task.WhenAll(categoryTasks).ConfigureAwait(false);

            UpdateCacheStore(categories, cacheItemsToRemove);
        }