// 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.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.DotNet.ApiCompatibility.Logging;
using Microsoft.DotNet.ApiSymbolExtensions;
using Microsoft.DotNet.ApiSymbolExtensions.Logging;

namespace Microsoft.DotNet.ApiCompatibility.Runner
{
    /// <summary>
    /// Enqueues work items and performs api compatibility checks on them.
    /// </summary>
    public class ApiCompatRunner(ISuppressibleLog log,
        ISuppressionEngine suppressionEngine,
        IApiComparerFactory apiComparerFactory,
        IAssemblySymbolLoaderFactory assemblySymbolLoaderFactory) : IApiCompatRunner
    {
        private readonly HashSet<ApiCompatRunnerWorkItem> _workItems = [];

        /// <inheritdoc />
        public IReadOnlyCollection<ApiCompatRunnerWorkItem> WorkItems => _workItems;

        /// <inheritdoc />
        public void ExecuteWorkItems()
        {
            log.LogMessage(MessageImportance.Low,
                string.Format(Resources.ApiCompatRunnerExecutingWorkItems,
                    _workItems.Count));

            foreach (ApiCompatRunnerWorkItem workItem in _workItems)
            {
                List<ElementContainer<IAssemblySymbol>> leftContainerList = CreateAssemblySymbols(workItem.Left, out bool resolvedExternallyProvidedAssemblyReferences);
                bool runWithReferences = resolvedExternallyProvidedAssemblyReferences;

                List<IEnumerable<ElementContainer<IAssemblySymbol>>> rightContainersList = new(workItem.Right.Count);
                foreach (IReadOnlyList<MetadataInformation> right in workItem.Right)
                {
                    List<ElementContainer<IAssemblySymbol>> rightContainers = CreateAssemblySymbols(right.ToImmutableArray(), out resolvedExternallyProvidedAssemblyReferences);
                    rightContainersList.Add(rightContainers);
                    runWithReferences &= resolvedExternallyProvidedAssemblyReferences;
                }

                // There must at least be one left and one right element in the container.
                // If assemblies symbols failed to load and nothing is to compare, skip this work item.
                if (leftContainerList.Count == 0 || rightContainersList.Count == 0)
                    continue;

                // Create and configure the work item specific api comparer
                IApiComparer apiComparer = apiComparerFactory.Create();
                apiComparer.Settings.StrictMode = workItem.Options.EnableStrictMode;
                apiComparer.Settings.WithReferences = runWithReferences;


                // Invoke the api comparer for the work item and operate on the difference result
                IEnumerable<CompatDifference> differences = apiComparer.GetDifferences(leftContainerList, rightContainersList);
                var differenceGroups = differences.GroupBy((c) => new { c.Left, c.Right });

                foreach (var differenceGroup in differenceGroups)
                {
                    // Log the difference header only if there are differences and errors aren't baselined.
                    bool logHeader = !suppressionEngine.BaselineAllErrors;

                    foreach (CompatDifference difference in differenceGroup)
                    {
                        Suppression suppression = new(difference.DiagnosticId)
                        {
                            Target = difference.ReferenceId,
                            Left = difference.Left.AssemblyId,
                            Right = difference.Right.AssemblyId,
                            IsBaselineSuppression = workItem.Options.IsBaselineComparison
                        };

                        // If the error is suppressed, don't log anything.
                        if (suppressionEngine.IsErrorSuppressed(suppression))
                            continue;

                        if (logHeader)
                        {
                            logHeader = false;
                            log.LogError(string.Format(Resources.ApiCompatibilityHeader,
                                difference.Left.AssemblyId,
                                difference.Right.AssemblyId,
                                workItem.Options.IsBaselineComparison ? difference.Left.FullPath : "left",
                                workItem.Options.IsBaselineComparison ? difference.Right.FullPath : "right"));
                        }

                        log.LogError(suppression,
                            difference.DiagnosticId,
                            difference.Message);
                    }
                }
            }

            _workItems.Clear();
        }

        private List<ElementContainer<IAssemblySymbol>> CreateAssemblySymbols(IReadOnlyList<MetadataInformation> metadataInformation,
            out bool resolvedExternallyProvidedAssemblyReferences)
        {
            string[] aggregatedReferences = metadataInformation.Where(m => m.References != null).SelectMany(m => m.References!).Distinct().ToArray();
            resolvedExternallyProvidedAssemblyReferences = aggregatedReferences.Length > 0;

            IAssemblySymbolLoader loader = assemblySymbolLoaderFactory.Create(resolvedExternallyProvidedAssemblyReferences);
            if (resolvedExternallyProvidedAssemblyReferences)
            {
                loader.AddReferenceSearchPaths(aggregatedReferences);
            }

            IReadOnlyList<IAssemblySymbol?>? assemblySymbols = null;
            // TODO: Come up with a better pattern to identify archives.
            string? archivePath = metadataInformation[0].FullPath.EndsWith(".nupkg") ? metadataInformation[0].FullPath : null;
            if (archivePath != null)
            {
                string[] relativePaths = metadataInformation.Select(add => add.AssemblyId).ToArray();
                assemblySymbols = loader.LoadAssembliesFromArchive(archivePath, relativePaths);
            }
            else
            {
                string[] assemblyPaths = metadataInformation.Select(add => add.FullPath).ToArray();
                assemblySymbols = loader.LoadAssemblies(assemblyPaths);
            }

            Debug.Assert(assemblySymbols.Count == metadataInformation.Count);
            List<ElementContainer<IAssemblySymbol>> elementContainerList = new(metadataInformation.Count);
            for (int i = 0; i < metadataInformation.Count; i++)
            {
                IAssemblySymbol? assemblySymbol = assemblySymbols[i];
                if (assemblySymbol == null)
                {
                    log.LogMessage(MessageImportance.High,
                        string.Format(Resources.AssemblyLoadError,
                            metadataInformation[i].AssemblyId));
                    continue;
                }

                elementContainerList.Add(new ElementContainer<IAssemblySymbol>(assemblySymbol, metadataInformation[i]));
            }

            return elementContainerList;
        }

        /// <inheritdoc />
        public void EnqueueWorkItem(ApiCompatRunnerWorkItem workItem)
        {
            // If the work item (left + options) is already part of the queue, add the new right assembly sets to the work item.
            if (_workItems.TryGetValue(workItem, out ApiCompatRunnerWorkItem actualWorkItem))
            {
                foreach (IReadOnlyList<MetadataInformation> right in workItem.Right)
                {
                    bool exists = false;
                    foreach (IReadOnlyList<MetadataInformation> actualRight in actualWorkItem.Right)
                    {
                        // If the new right is already part of the work item, do nothing.
                        if (actualRight.SequenceEqual(right))
                        {
                            exists = true;
                            break;
                        }
                    }

                    if (!exists)
                    {
                        actualWorkItem.Right.Add(right);
                    }
                }
            }
            else
            {
                _workItems.Add(workItem);
            }
        }
    }
}
