// 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.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis;

namespace Microsoft.AspNetCore.OpenApi.SourceGenerators;

/// <summary>
/// Visits the assembly symbols to collect public types, properties, and methods that might
/// contain XML documentation comments.
/// </summary>
internal sealed class AssemblyTypeSymbolsVisitor(IAssemblySymbol assemblySymbol, CancellationToken cancellation) : SymbolVisitor
{
    private readonly CancellationToken _cancellationToken = cancellation;
    private readonly HashSet<INamedTypeSymbol> _exportedTypes = new(SymbolEqualityComparer.Default);
    private readonly HashSet<IPropertySymbol> _exportedProperties = new(SymbolEqualityComparer.Default);
    private readonly HashSet<IMethodSymbol> _exportedMethods = new(SymbolEqualityComparer.Default);

    public ImmutableArray<INamedTypeSymbol> GetPublicTypes() => [.. _exportedTypes];
    public ImmutableArray<IPropertySymbol> GetPublicProperties() => [.. _exportedProperties];
    public ImmutableArray<IMethodSymbol> GetPublicMethods() => [.. _exportedMethods];

    public void VisitAssembly() => VisitAssembly(assemblySymbol);

    public override void VisitAssembly(IAssemblySymbol symbol)
    {
        _cancellationToken.ThrowIfCancellationRequested();
        symbol.GlobalNamespace.Accept(this);
    }

    public override void VisitNamespace(INamespaceSymbol symbol)
    {
        foreach (var namespaceOrType in symbol.GetMembers())
        {
            _cancellationToken.ThrowIfCancellationRequested();
            namespaceOrType.Accept(this);
        }
    }

    public override void VisitNamedType(INamedTypeSymbol type)
    {
        _cancellationToken.ThrowIfCancellationRequested();
        if (type.IsGenericType)
        {
            // Because type comments are based on the Unbound generic type, i.e. List`1, we want to use the unbound generic type
            type = type.OriginalDefinition;
        }

        if (!IsAccessibleType(type) || !_exportedTypes.Add(type))
        {
            return;
        }

        var nestedTypes = type.GetTypeMembers();

        foreach (var nestedType in nestedTypes)
        {
            _cancellationToken.ThrowIfCancellationRequested();
            nestedType.Accept(this);
        }

        var properties = type.GetMembers().OfType<IPropertySymbol>();
        foreach (var property in properties)
        {
            _cancellationToken.ThrowIfCancellationRequested();
            if (IsAccessibleType(property) && _exportedProperties.Add(property))
            {
                property.Type.Accept(this);
            }
        }
        var methods = type.GetMembers().OfType<IMethodSymbol>();
        foreach (var method in methods)
        {
            _cancellationToken.ThrowIfCancellationRequested();
            if (IsAccessibleType(method) && _exportedMethods.Add(method))
            {
                method.Accept(this);
            }
        }
    }

    private bool IsAccessibleType(ISymbol symbol) =>
        SymbolEqualityComparer.Default.Equals(symbol.ContainingAssembly, assemblySymbol)
            && (symbol.DeclaredAccessibility == Accessibility.Public ||
                symbol.DeclaredAccessibility == Accessibility.Internal ||
                symbol.DeclaredAccessibility == Accessibility.ProtectedOrInternal);
}
