// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.ComponentModel.Composition.Primitives;
using System.Linq;
using Microsoft.Internal;

namespace System.ComponentModel.Composition.Hosting
{
    public partial class FilteredCatalog
    {
        /// <summary>
        /// Creates a new instance of the <see cref="FilteredCatalog"/> that conatains all the parts from the original filtered catalog and all their dependencies.
        /// </summary>
        /// <returns></returns>
        public FilteredCatalog IncludeDependencies()
        {
            return IncludeDependencies(i => i.Cardinality == ImportCardinality.ExactlyOne);
        }

        /// <summary>
        /// Creates a new instance of the <see cref="FilteredCatalog"/> that conatains all the parts from the original filtered catalog and all their dependencies that
        /// can be reached via imports that match the specified filter.
        /// </summary>
        /// <param name="importFilter">The import filter.</param>
        /// <returns></returns>
        public FilteredCatalog IncludeDependencies(Func<ImportDefinition, bool> importFilter)
        {
            Requires.NotNull(importFilter, nameof(importFilter));
            ThrowIfDisposed();

            return Traverse(new DependenciesTraversal(this, importFilter));
        }

        /// <summary>
        /// Creates a new instance of the <see cref="FilteredCatalog"/> that conatains all the parts from the original filtered catalog and all their dependents.
        /// </summary>
        /// <returns></returns>
        public FilteredCatalog IncludeDependents()
        {
            return IncludeDependents(i => i.Cardinality == ImportCardinality.ExactlyOne);
        }

        /// <summary>
        /// Creates a new instance of the <see cref="FilteredCatalog"/> that conatains all the parts from the original filtered catalog and all their dependents that
        /// can be reached via imports that match the specified filter.
        /// </summary>
        /// <param name="importFilter">The import filter.</param>
        /// <returns></returns>
        public FilteredCatalog IncludeDependents(Func<ImportDefinition, bool> importFilter)
        {
            Requires.NotNull(importFilter, nameof(importFilter));
            ThrowIfDisposed();

            return Traverse(new DependentsTraversal(this, importFilter));
        }

        private FilteredCatalog Traverse(IComposablePartCatalogTraversal traversal)
        {
            ArgumentNullException.ThrowIfNull(traversal);

            // we make sure that the underlyiong catalog cannot change while we are doing the trasversal
            // After thaty traversal is done, the freeze is lifted, and the catalog is free to change, but the changes
            // cannot affect partitioning
            FreezeInnerCatalog();

            try
            {
                traversal.Initialize();
                var traversalClosure = GetTraversalClosure(_innerCatalog.Where(_filter), traversal);
                return new FilteredCatalog(_innerCatalog, traversalClosure.Contains);
            }
            finally
            {
                UnfreezeInnerCatalog();
            }
        }

        private static HashSet<ComposablePartDefinition> GetTraversalClosure(IEnumerable<ComposablePartDefinition> parts, IComposablePartCatalogTraversal traversal)
        {
            ArgumentNullException.ThrowIfNull(traversal);

            var traversedParts = new HashSet<ComposablePartDefinition>();
            GetTraversalClosure(parts, traversedParts, traversal);
            return traversedParts;
        }

        private static void GetTraversalClosure(IEnumerable<ComposablePartDefinition> parts, HashSet<ComposablePartDefinition> traversedParts, IComposablePartCatalogTraversal traversal)
        {
            foreach (var part in parts)
            {
                if (traversedParts.Add(part))
                {
                    if (traversal.TryTraverse(part, out IEnumerable<ComposablePartDefinition>? partsToTraverse))
                    {
                        GetTraversalClosure(partsToTraverse, traversedParts, traversal);
                    }
                }
            }
        }

        private void FreezeInnerCatalog()
        {
            if (_innerCatalog is INotifyComposablePartCatalogChanged innerNotifyCatalog)
            {
                innerNotifyCatalog.Changing += ThrowOnRecomposition;
            }
        }

        private void UnfreezeInnerCatalog()
        {
            if (_innerCatalog is INotifyComposablePartCatalogChanged innerNotifyCatalog)
            {
                innerNotifyCatalog.Changing -= ThrowOnRecomposition;
            }
        }

        private static void ThrowOnRecomposition(object? sender, ComposablePartCatalogChangeEventArgs e)
        {
            throw new ChangeRejectedException();
        }
    }
}
