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

using Microsoft.CodeAnalysis;
using Microsoft.DotNet.ApiCompatibility.Mapping;
using Microsoft.DotNet.ApiCompatibility.Rules;

namespace Microsoft.DotNet.ApiCompatibility
{
    /// <summary>
    /// Performs api comparison based on ISymbol inputs.
    /// </summary>
    public class ApiComparer : IApiComparer
    {
        private readonly IDifferenceVisitorFactory _differenceVisitorFactory;
        private readonly IElementMapperFactory _elementMapperFactory;

        /// <inheritdoc />
        public ApiComparerSettings Settings { get; }

        public ApiComparer(IRuleFactory ruleFactory,
            ApiComparerSettings? settings = null,
            IDifferenceVisitorFactory? differenceVisitorFactory = null,
            IRuleContext? ruleContext = null,
            Func<IRuleFactory, IRuleContext, IRuleRunner>? ruleRunnerFactory = null,
            Func<IRuleRunner, IElementMapperFactory>? elementMapperFactory = null)
        {
            ruleContext ??= new RuleContext();
            IRuleRunner ruleRunner = ruleRunnerFactory?.Invoke(ruleFactory, ruleContext) ?? new RuleRunner(ruleFactory, ruleContext);

            _differenceVisitorFactory = differenceVisitorFactory ?? new DifferenceVisitorFactory();
            _elementMapperFactory = elementMapperFactory?.Invoke(ruleRunner) ?? new ElementMapperFactory(ruleRunner);
            Settings = settings ?? new();

            ruleRunner.InitializeRules(Settings);
        }

        /// <inheritdoc />
        public IEnumerable<CompatDifference> GetDifferences(IAssemblySymbol left,
            IAssemblySymbol right)
        {
            return GetDifferences(new ElementContainer<IAssemblySymbol>(left, MetadataInformation.DefaultLeft),
                new ElementContainer<IAssemblySymbol>(right, MetadataInformation.DefaultRight));
        }

        /// <inheritdoc />
        public IEnumerable<CompatDifference> GetDifferences(ElementContainer<IAssemblySymbol> left,
            ElementContainer<IAssemblySymbol> right)
        {
            IAssemblyMapper assemblyMapper = _elementMapperFactory.CreateAssemblyMapper(Settings, rightCount: 1);
            assemblyMapper.AddElement(left, ElementSide.Left);
            assemblyMapper.AddElement(right, ElementSide.Right);

            IDifferenceVisitor visitor = _differenceVisitorFactory.Create();
            visitor.Visit(assemblyMapper);

            return visitor.CompatDifferences;
        }

        /// <inheritdoc />
        public IEnumerable<CompatDifference> GetDifferences(IEnumerable<ElementContainer<IAssemblySymbol>> left,
            IEnumerable<ElementContainer<IAssemblySymbol>> right)
        {
            IAssemblySetMapper assemblySetMapper = _elementMapperFactory.CreateAssemblySetMapper(Settings, rightCount: 1);
            assemblySetMapper.AddElement(left, ElementSide.Left);
            assemblySetMapper.AddElement(right, ElementSide.Right);

            IDifferenceVisitor visitor = _differenceVisitorFactory.Create();
            visitor.Visit(assemblySetMapper);

            return visitor.CompatDifferences;
        }

        /// <inheritdoc />
        public IEnumerable<CompatDifference> GetDifferences(IEnumerable<IAssemblySymbol> left,
            IEnumerable<IAssemblySymbol> right)
        {
            List<ElementContainer<IAssemblySymbol>> transformedLeft = [];
            foreach (IAssemblySymbol assemblySymbol in left)
            {
                transformedLeft.Add(new ElementContainer<IAssemblySymbol>(assemblySymbol, MetadataInformation.DefaultLeft));
            }

            List<ElementContainer<IAssemblySymbol>> transformedRight = [];
            foreach (IAssemblySymbol assemblySymbol in right)
            {
                transformedRight.Add(new ElementContainer<IAssemblySymbol>(assemblySymbol, MetadataInformation.DefaultRight));
            }

            return GetDifferences(transformedLeft, transformedRight);
        }

        /// <inheritdoc />
        public IEnumerable<CompatDifference> GetDifferences(ElementContainer<IAssemblySymbol> left,
            IReadOnlyList<ElementContainer<IAssemblySymbol>> right)
        {
            int rightCount = right.Count;
            IAssemblyMapper assemblyMapper = _elementMapperFactory.CreateAssemblyMapper(Settings, rightCount);
            assemblyMapper.AddElement(left, ElementSide.Left);
            for (int i = 0; i < rightCount; i++)
            {
                assemblyMapper.AddElement(right[i], ElementSide.Right, i);
            }

            IDifferenceVisitor visitor = _differenceVisitorFactory.Create();
            visitor.Visit(assemblyMapper);

            return SortCompatDifferencesByInputMetadata(visitor.CompatDifferences.ToLookup(c => c.Right, t => t), right);
        }

        /// <inheritdoc />
        public IEnumerable<CompatDifference> GetDifferences(IEnumerable<ElementContainer<IAssemblySymbol>> left,
            IReadOnlyList<IEnumerable<ElementContainer<IAssemblySymbol>>> right)
        {
            IAssemblySetMapper assemblySetMapper = _elementMapperFactory.CreateAssemblySetMapper(Settings, right.Count);
            assemblySetMapper.AddElement(left, ElementSide.Left);
            for (int rightIndex = 0; rightIndex < right.Count; rightIndex++)
            {
                assemblySetMapper.AddElement(right[rightIndex], ElementSide.Right, rightIndex);
            }

            IDifferenceVisitor visitor = _differenceVisitorFactory.Create();
            visitor.Visit(assemblySetMapper);

            return SortCompatDifferencesByInputMetadata(visitor.CompatDifferences.ToLookup(c => c.Left, t => t), left);
        }

        // Sort the compat differences by the order of the passed in metadata.
        private static List<CompatDifference> SortCompatDifferencesByInputMetadata(ILookup<MetadataInformation, CompatDifference> compatDifferencesLookup,
            IEnumerable<ElementContainer<IAssemblySymbol>> inputMetadata)
        {
            HashSet<MetadataInformation> processedMetadata = [];
            List<CompatDifference> sortedCompatDifferences = [];

            foreach (ElementContainer<IAssemblySymbol> elementContainer in inputMetadata)
            {
                sortedCompatDifferences.AddRange(compatDifferencesLookup[elementContainer.MetadataInformation]);
                processedMetadata.Add(elementContainer.MetadataInformation);
            }

            foreach (IGrouping<MetadataInformation, CompatDifference> compatDifferenceGroup in compatDifferencesLookup)
            {
                if (processedMetadata.Contains(compatDifferenceGroup.Key))
                    continue;

                sortedCompatDifferences.AddRange(compatDifferenceGroup);
            }

            return sortedCompatDifferences;
        }
    }
}
