' Licensed to the .NET Foundation under one or more agreements.
' The .NET Foundation licenses this file to you under the MIT license.
' See the LICENSE file in the project root for more information.

Imports System.Collections.Generic
Imports System.Collections.Immutable
Imports System.Collections.ObjectModel
Imports System.Reflection.Metadata
Imports System.Runtime.InteropServices
Imports System.Threading
Imports Microsoft.CodeAnalysis.PooledObjects
Imports Microsoft.CodeAnalysis.VisualBasic.Symbols

Namespace Microsoft.CodeAnalysis.VisualBasic.Symbols.Metadata.PE

    ''' <summary>
    ''' The base class to represent a namespace imported from a PE/module.
    ''' Namespaces that differ only by casing in name are merged.
    ''' </summary>
    Friend MustInherit Class PENamespaceSymbol
        Inherits PEOrSourceOrMergedNamespaceSymbol

        ''' <summary>
        ''' A map of namespaces immediately contained within this namespace 
        ''' grouped by their name (case-insensitively).
        ''' </summary>
        Protected m_lazyMembers As Dictionary(Of String, ImmutableArray(Of Symbol))

        ''' <summary>
        ''' A map of types immediately contained within this namespace 
        ''' grouped by their name (case-insensitively).
        ''' </summary>
        Protected m_lazyTypes As Dictionary(Of String, ImmutableArray(Of PENamedTypeSymbol))

        ''' <summary>
        ''' A map of NoPia local types immediately contained in this assembly.
        ''' Maps fully-qualified type name to the row id.
        ''' </summary>
        Private _lazyNoPiaLocalTypes As Dictionary(Of String, TypeDefinitionHandle)

        ' Lazily filled in collection of all contained modules.
        Private _lazyModules As ImmutableArray(Of NamedTypeSymbol)

        ' Lazily filled in collection of all contained types.
        Private _lazyFlattenedTypes As ImmutableArray(Of NamedTypeSymbol)

        Friend NotOverridable Overrides ReadOnly Property Extent As NamespaceExtent
            Get
                Return New NamespaceExtent(Me.ContainingPEModule)
            End Get
        End Property

        Public Overrides Function GetModuleMembers() As ImmutableArray(Of NamedTypeSymbol)
            ' Since this gets called a LOT during binding, it's worth caching the result.

            If _lazyModules.IsDefault Then
                ' We have to read all the types to discover which ones are modules, so I'm not 
                ' sure there is any better strategy on first call then getting all type members
                ' and filtering them.
                ' Ordered.
                Dim modules = GetTypeMembers().WhereAsArray(Function(t) t.TypeKind = TYPEKIND.Module)
                ImmutableInterlocked.InterlockedCompareExchange(_lazyModules, modules, Nothing)
            End If

            Return _lazyModules
        End Function

        Public Overrides Function GetModuleMembers(name As String) As ImmutableArray(Of NamedTypeSymbol)
            ' This is not called during binding, so caching isn't very critical.
            Return GetTypeMembers(name).WhereAsArray(Function(t) t.TypeKind = TYPEKIND.Module)
        End Function

        Public NotOverridable Overloads Overrides Function GetMembers() As ImmutableArray(Of Symbol)
            EnsureAllMembersLoaded()

            Return m_lazyMembers.Flatten()
        End Function

        Friend Overrides ReadOnly Property EmbeddedSymbolKind As EmbeddedSymbolKind
            Get
                Return EmbeddedSymbolKind.None
            End Get
        End Property

        Public NotOverridable Overloads Overrides Function GetMembers(name As String) As ImmutableArray(Of Symbol)
            EnsureAllMembersLoaded()

            Dim m As ImmutableArray(Of Symbol) = Nothing

            If m_lazyMembers.TryGetValue(name, m) Then
                Return m
            End If

            Return ImmutableArray(Of Symbol).Empty
        End Function

        Public NotOverridable Overloads Overrides Function GetTypeMembers() As ImmutableArray(Of NamedTypeSymbol)
            Dim result = _lazyFlattenedTypes
            If Not result.IsDefault Then
                Return result
            End If

            EnsureAllMembersLoaded()
            result = StaticCast(Of NamedTypeSymbol).From(m_lazyTypes.Flatten())

            _lazyFlattenedTypes = result
            Return result
        End Function

        Public NotOverridable Overloads Overrides Function GetTypeMembers(name As String) As ImmutableArray(Of NamedTypeSymbol)
            EnsureAllMembersLoaded()

            Dim t As ImmutableArray(Of PENamedTypeSymbol) = Nothing

            If m_lazyTypes.TryGetValue(name, t) Then
                Return StaticCast(Of NamedTypeSymbol).From(t)
            End If

            Return ImmutableArray(Of NamedTypeSymbol).Empty
        End Function

        Public Overloads Overrides Function GetTypeMembers(name As String, arity As Integer) As ImmutableArray(Of NamedTypeSymbol)
            Return GetTypeMembers(name).WhereAsArray(Function(type, arity_) type.Arity = arity_, arity)
        End Function

        Public NotOverridable Overrides ReadOnly Property Locations As ImmutableArray(Of Location)
            Get
                Return StaticCast(Of Location).From(ContainingPEModule.MetadataLocation)
            End Get
        End Property

        Public Overrides ReadOnly Property DeclaringSyntaxReferences As ImmutableArray(Of SyntaxReference)
            Get
                Return ImmutableArray(Of SyntaxReference).Empty
            End Get
        End Property

        ''' <summary>
        ''' Returns PEModuleSymbol containing the namespace.
        ''' </summary>
        ''' <returns>PEModuleSymbol containing the namespace.</returns>
        Friend MustOverride ReadOnly Property ContainingPEModule As PEModuleSymbol

        Protected MustOverride Sub EnsureAllMembersLoaded()

        ''' <summary>
        ''' Initializes m_Namespaces and m_Types maps with information about 
        ''' namespaces and types immediately contained within this namespace.
        ''' </summary>
        ''' <param name="typesByNS">
        ''' The sequence of groups of TypeDef row ids for types contained within the namespace, 
        ''' recursively including those from nested namespaces. The row ids must be grouped by the 
        ''' fully-qualified namespace name in case-sensitive manner. There could be multiple groups 
        ''' for each fully-qualified namespace name. The groups must be sorted by their key in 
        ''' case-insensitive manner. Empty string must be used as namespace name for types 
        ''' immediately contained within Global namespace. Therefore, all types in THIS namespace, 
        ''' if any, must be in several first IGroupings.
        ''' </param>
        Protected Sub LoadAllMembers(typesByNS As IEnumerable(Of IGrouping(Of String, TypeDefinitionHandle)))
            Debug.Assert(typesByNS IsNot Nothing)

            ' A sequence of TypeDef handles for types immediately contained within this namespace.
            Dim nestedTypes As IEnumerable(Of IGrouping(Of String, TypeDefinitionHandle)) = Nothing

            ' A sequence with information about namespaces immediately contained within this namespace.
            ' For each pair:
            '    Key - contains simple name of a child namespace.
            '    Value - contains a sequence similar to the one passed to this function, but
            '            calculated for the child namespace. 
            Dim nestedNamespaces As IEnumerable(Of KeyValuePair(Of String, IEnumerable(Of IGrouping(Of String, TypeDefinitionHandle)))) = Nothing

            'TODO: Perhaps there is a cheaper way to calculate the length of the name without actually building it with ToDisplayString.
            Dim isGlobalNamespace As Boolean = Me.IsGlobalNamespace

            MetadataHelpers.GetInfoForImmediateNamespaceMembers(
                isGlobalNamespace,
                If(isGlobalNamespace, 0, ToDisplayString(SymbolDisplayFormat.QualifiedNameOnlyFormat).Length),
                typesByNS,
                CaseInsensitiveComparison.Comparer,
                nestedTypes, nestedNamespaces)

            LazyInitializeTypes(nestedTypes)

            LazyInitializeNamespaces(nestedNamespaces)
        End Sub

        ''' <summary>
        ''' Create symbols for nested namespaces and initialize m_Namespaces map.
        ''' </summary>
        Private Sub LazyInitializeNamespaces(
            childNamespaces As IEnumerable(Of KeyValuePair(Of String, IEnumerable(Of IGrouping(Of String, TypeDefinitionHandle))))
        )
            If m_lazyMembers Is Nothing Then

                Dim members As New Dictionary(Of String, ImmutableArray(Of Symbol))(CaseInsensitiveComparison.Comparer)

                ' Add namespaces
                For Each child In childNamespaces
                    Dim ns = New PENestedNamespaceSymbol(child.Key, Me, child.Value)
                    members.Add(ns.Name, ImmutableArray.Create(Of Symbol)(ns))
                Next

                ' Merge in the types

                For Each typeSymbols As ImmutableArray(Of PENamedTypeSymbol) In m_lazyTypes.Values
                    Dim name = typeSymbols(0).Name
                    Dim symbols As ImmutableArray(Of Symbol) = Nothing

                    If Not members.TryGetValue(name, symbols) Then
                        members.Add(name, StaticCast(Of Symbol).From(typeSymbols))
                    Else
                        members(name) = symbols.Concat(StaticCast(Of Symbol).From(typeSymbols))
                    End If
                Next

                Interlocked.CompareExchange(m_lazyMembers, members, Nothing)
            End If
        End Sub

        ''' <summary>
        ''' Create symbols for nested types and initialize m_Types map.
        ''' </summary>
        Private Sub LazyInitializeTypes(typeGroups As IEnumerable(Of IGrouping(Of String, TypeDefinitionHandle)))

            If m_lazyTypes Is Nothing Then

                Dim moduleSymbol = ContainingPEModule
                Dim children = ArrayBuilder(Of PENamedTypeSymbol).GetInstance()
                Dim skipCheckForPiaType = Not moduleSymbol.Module.ContainsNoPiaLocalTypes()
                Dim noPiaLocalTypes As Dictionary(Of String, TypeDefinitionHandle) = Nothing
                Dim isGlobal = Me.IsGlobalNamespace

                For Each g In typeGroups
                    For Each t In g
                        If skipCheckForPiaType OrElse Not moduleSymbol.Module.IsNoPiaLocalType(t) Then
                            Dim type = If(isGlobal,
                                          New PENamedTypeSymbol(moduleSymbol, Me, t),
                                          New PENamedTypeSymbolWithEmittedNamespaceName(moduleSymbol, Me, t, g.Key))
                            children.Add(type)
                        Else
                            ' The dictionary of NoPIA local types must be indexed by fully-qualified names
                            ' (namespace + type name), and key comparison must be case-sensitive. The
                            ' reason is this PENamespaceSymbol is a merged namespace of all namespaces
                            ' from the same module in metadata but with potentially different casing.
                            ' When resolving a NoPIA type, the casing must match however, so it is not
                            ' sufficient to simply use the type name. In C#, namespaces with different
                            ' casing are not merged so type name is sufficient there.
                            Try
                                Dim typeDefName As String = moduleSymbol.Module.GetTypeDefNameOrThrow(t)

                                If noPiaLocalTypes Is Nothing Then
                                    noPiaLocalTypes = New Dictionary(Of String, TypeDefinitionHandle)()
                                End If
                                Dim qualifiedName = MetadataHelpers.BuildQualifiedName(g.Key, typeDefName)
                                noPiaLocalTypes(qualifiedName) = t
                            Catch mrEx As BadImageFormatException
                            End Try
                        End If
                    Next
                Next

                Dim typesDict As Dictionary(Of String, ImmutableArray(Of PENamedTypeSymbol)) =
                    children.ToDictionary(Function(c) c.Name, CaseInsensitiveComparison.Comparer)
                children.Free()

                If _lazyNoPiaLocalTypes Is Nothing Then
                    Interlocked.CompareExchange(_lazyNoPiaLocalTypes, noPiaLocalTypes, Nothing)
                End If

                If Interlocked.CompareExchange(m_lazyTypes, typesDict, Nothing) Is Nothing Then
                    ' Build cache of TypeDef Tokens
                    ' Potentially this can be done in the background.
                    moduleSymbol.OnNewTypeDeclarationsLoaded(typesDict)
                End If
            End If
        End Sub

        ''' <summary>
        ''' For test purposes only.
        ''' </summary>
        Friend ReadOnly Property AreTypesLoaded As Boolean
            Get
                Return m_lazyTypes IsNot Nothing
            End Get
        End Property

        ''' <summary>
        ''' Return the set of types that should be checked for presence of extension methods in order to build
        ''' a map of extension methods for the namespace. 
        ''' </summary>
        Friend Overrides ReadOnly Property TypesToCheckForExtensionMethods As ImmutableArray(Of NamedTypeSymbol)
            Get
                If ContainingPEModule.MightContainExtensionMethods Then
                    ' Note that we are using GetTypeMembers rather than GetModuleMembers because non-Modules imported 
                    ' from metadata can contain extension methods.
                    Return Me.GetTypeMembers() ' Ordered.
                End If

                Return ImmutableArray(Of NamedTypeSymbol).Empty
            End Get
        End Property

        Friend Overloads Function LookupMetadataType(ByRef emittedTypeName As MetadataTypeName, <Out> ByRef isNoPiaLocalType As Boolean) As NamedTypeSymbol
            Dim result = LookupMetadataType(emittedTypeName)
            isNoPiaLocalType = False

            If TypeOf result Is MissingMetadataTypeSymbol Then
                EnsureAllMembersLoaded()
                Dim typeDef As TypeDefinitionHandle = Nothing

                ' See if this is a NoPia local type, which we should unify.
                If _lazyNoPiaLocalTypes IsNot Nothing AndAlso
                    _lazyNoPiaLocalTypes.TryGetValue(emittedTypeName.FullName, typeDef) Then

                    result = DirectCast(New MetadataDecoder(ContainingPEModule).GetTypeOfToken(typeDef, isNoPiaLocalType), NamedTypeSymbol)
                    Debug.Assert(isNoPiaLocalType)
                End If
            End If

            Return result
        End Function

    End Class

End Namespace
