﻿// 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.

using System.Collections.Generic;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using Microsoft.CodeAnalysis.Debugging;

namespace Microsoft.CodeAnalysis.PdbSourceDocument
{
    internal static class SymbolSourceDocumentFinder
    {
        public static HashSet<DocumentHandle> FindDocumentHandles(EntityHandle handle, MetadataReader dllReader, MetadataReader pdbReader)
        {
            var docList = new HashSet<DocumentHandle>();

            switch (handle.Kind)
            {
                case HandleKind.MethodDefinition:
                    ProcessMethodDef((MethodDefinitionHandle)handle, dllReader, pdbReader, docList, processDeclaringType: true);
                    break;
                case HandleKind.TypeDefinition:
                    ProcessTypeDef((TypeDefinitionHandle)handle, dllReader, pdbReader, docList);
                    break;
                case HandleKind.FieldDefinition:
                    ProcessFieldDef((FieldDefinitionHandle)handle, dllReader, pdbReader, docList);
                    break;
                case HandleKind.PropertyDefinition:
                    ProcessPropertyDef((PropertyDefinitionHandle)handle, dllReader, pdbReader, docList);
                    break;
                case HandleKind.EventDefinition:
                    ProcessEventDef((EventDefinitionHandle)handle, dllReader, pdbReader, docList);
                    break;
            }

            return docList;
        }

        private static void ProcessMethodDef(MethodDefinitionHandle methodDefHandle, MetadataReader dllReader, MetadataReader pdbReader, HashSet<DocumentHandle> docList, bool processDeclaringType)
        {
            var mdi = pdbReader.GetMethodDebugInformation(methodDefHandle);
            if (!mdi.Document.IsNil)
            {
                docList.Add(mdi.Document);
                return;
            }

            if (!mdi.SequencePointsBlob.IsNil)
            {
                foreach (var point in mdi.GetSequencePoints())
                {
                    if (!point.Document.IsNil)
                    {
                        docList.Add(point.Document);
                        // No need to check the type if we found a document
                        processDeclaringType = false;
                    }
                }
            }

            // Not all methods have document info, for example synthesized constructors, so we also want
            // to get any documents from the declaring type
            if (processDeclaringType)
            {
                var methodDef = dllReader.GetMethodDefinition(methodDefHandle);
                var typeDefHandle = methodDef.GetDeclaringType();
                ProcessTypeDef(typeDefHandle, dllReader, pdbReader, docList);
            }
        }

        private static void ProcessEventDef(EventDefinitionHandle eventDefHandle, MetadataReader dllReader, MetadataReader pdbReader, HashSet<DocumentHandle> docList)
        {
            var eventDef = dllReader.GetEventDefinition(eventDefHandle);
            var accessors = eventDef.GetAccessors();
            if (!accessors.Adder.IsNil)
            {
                ProcessMethodDef(accessors.Adder, dllReader, pdbReader, docList, processDeclaringType: true);
            }

            if (!accessors.Remover.IsNil)
            {
                ProcessMethodDef(accessors.Remover, dllReader, pdbReader, docList, processDeclaringType: true);
            }

            if (!accessors.Raiser.IsNil)
            {
                ProcessMethodDef(accessors.Raiser, dllReader, pdbReader, docList, processDeclaringType: true);
            }

            foreach (var other in accessors.Others)
            {
                ProcessMethodDef(other, dllReader, pdbReader, docList, processDeclaringType: true);
            }
        }

        private static void ProcessPropertyDef(PropertyDefinitionHandle propertyDefHandle, MetadataReader dllReader, MetadataReader pdbReader, HashSet<DocumentHandle> docList)
        {
            var propertyDef = dllReader.GetPropertyDefinition(propertyDefHandle);
            var accessors = propertyDef.GetAccessors();
            if (!accessors.Getter.IsNil)
            {
                ProcessMethodDef(accessors.Getter, dllReader, pdbReader, docList, processDeclaringType: true);
            }

            if (!accessors.Setter.IsNil)
            {
                ProcessMethodDef(accessors.Setter, dllReader, pdbReader, docList, processDeclaringType: true);
            }

            foreach (var other in accessors.Others)
            {
                ProcessMethodDef(other, dllReader, pdbReader, docList, processDeclaringType: true);
            }
        }

        private static void ProcessFieldDef(FieldDefinitionHandle fieldDefHandle, MetadataReader dllReader, MetadataReader pdbReader, HashSet<DocumentHandle> docList)
        {
            var fieldDef = dllReader.GetFieldDefinition(fieldDefHandle);
            var typeDefHandle = fieldDef.GetDeclaringType();
            ProcessTypeDef(typeDefHandle, dllReader, pdbReader, docList);
        }

        private static void ProcessTypeDef(TypeDefinitionHandle typeDefHandle, MetadataReader dllReader, MetadataReader pdbReader, HashSet<DocumentHandle> docList, bool processContainingType = true)
        {
            AddDocumentsFromTypeDefinitionDocuments(typeDefHandle, pdbReader, docList);

            // We don't necessarily have all of the documents associated with the type
            var typeDef = dllReader.GetTypeDefinition(typeDefHandle);
            foreach (var methodDefHandle in typeDef.GetMethods())
            {
                ProcessMethodDef(methodDefHandle, dllReader, pdbReader, docList, processDeclaringType: false);
            }

            if (processContainingType && typeDef.IsNested)
            {
                // If this is a nested type, then we want to check the outer type too
                var containingType = typeDef.GetDeclaringType();
                if (!containingType.IsNil)
                {
                    ProcessTypeDef(containingType, dllReader, pdbReader, docList);
                }
            }

            // And of course if this is an outer type, the only document info might be from methods in
            // nested types
            var nestedTypes = typeDef.GetNestedTypes();
            foreach (var nestedType in nestedTypes)
            {
                ProcessTypeDef(nestedType, dllReader, pdbReader, docList, processContainingType: false);
            }
        }

        private static void AddDocumentsFromTypeDefinitionDocuments(TypeDefinitionHandle typeDefHandle, MetadataReader pdbReader, HashSet<DocumentHandle> docList)
        {
            var handles = pdbReader.GetCustomDebugInformation(typeDefHandle);
            foreach (var cdiHandle in handles)
            {
                var cdi = pdbReader.GetCustomDebugInformation(cdiHandle);
                var guid = pdbReader.GetGuid(cdi.Kind);
                if (guid == PortableCustomDebugInfoKinds.TypeDefinitionDocuments)
                {
                    if (((TypeDefinitionHandle)cdi.Parent).Equals(typeDefHandle))
                    {
                        var reader = pdbReader.GetBlobReader(cdi.Value);
                        while (reader.RemainingBytes > 0)
                        {
                            docList.Add(MetadataTokens.DocumentHandle(reader.ReadCompressedInteger()));
                        }
                    }
                }
            }
        }
    }
}
