﻿// 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;
using System.Composition;
using System.IO;
using System.Reflection.PortableExecutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PdbSourceDocument;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.VisualStudio.Debugger.Contracts.SourceLink;
using Microsoft.VisualStudio.Debugger.Contracts.SymbolLocator;

namespace Microsoft.VisualStudio.LanguageServices.PdbSourceDocument
{
    [Export(typeof(ISourceLinkService)), Shared]
    internal class SourceLinkService : ISourceLinkService
    {
        private readonly IDebuggerSymbolLocatorService _debuggerSymbolLocatorService;
        private readonly IDebuggerSourceLinkService _debuggerSourceLinkService;
        private readonly IPdbSourceDocumentLogger? _logger;

        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public SourceLinkService(
            IDebuggerSymbolLocatorService debuggerSymbolLocatorService,
            IDebuggerSourceLinkService debuggerSourceLinkService,
            [Import(AllowDefault = true)] IPdbSourceDocumentLogger? logger)
        {
            _debuggerSymbolLocatorService = debuggerSymbolLocatorService;
            _debuggerSourceLinkService = debuggerSourceLinkService;
            _logger = logger;
        }

        public async Task<PdbFilePathResult?> GetPdbFilePathAsync(string dllPath, PEReader peReader, bool useDefaultSymbolServers, CancellationToken cancellationToken)
        {
            var hasCodeViewEntry = false;
            uint timeStamp = 0;
            CodeViewDebugDirectoryData codeViewEntry = default;
            using var _ = ArrayBuilder<PdbChecksum>.GetInstance(out var checksums);
            foreach (var entry in peReader.ReadDebugDirectory())
            {
                if (entry.Type == DebugDirectoryEntryType.PdbChecksum)
                {
                    var checksum = peReader.ReadPdbChecksumDebugDirectoryData(entry);
                    checksums.Add(new PdbChecksum(checksum.AlgorithmName, checksum.Checksum));
                }
                else if (entry.Type == DebugDirectoryEntryType.CodeView && entry.IsPortableCodeView)
                {
                    hasCodeViewEntry = true;
                    timeStamp = entry.Stamp;
                    codeViewEntry = peReader.ReadCodeViewDebugDirectoryData(entry);
                }
            }

            if (!hasCodeViewEntry)
                return null;

            var pdbInfo = new SymbolLocatorPdbInfo(
                Path.GetFileName(codeViewEntry.Path),
                codeViewEntry.Guid,
                (uint)codeViewEntry.Age,
                timeStamp,
                checksums.ToImmutable(),
                dllPath,
                codeViewEntry.Path);

            var flags = useDefaultSymbolServers
                ? SymbolLocatorSearchFlags.ForceNuGetSymbolServer | SymbolLocatorSearchFlags.ForceMsftSymbolServer
                : SymbolLocatorSearchFlags.None;
            var result = await _debuggerSymbolLocatorService.LocateSymbolFileAsync(pdbInfo, flags, progress: null, cancellationToken).ConfigureAwait(false);

            if (result.Found && result.SymbolFilePath is not null)
            {
                return new PdbFilePathResult(result.SymbolFilePath);
            }
            else if (_logger is not null)
            {
                // We log specific info from the debugger if there is a failure, but the caller will log general failure
                // information otherwise
                _logger.Log(result.Status);
                _logger.Log(result.Log);
            }

            return null;
        }

        public async Task<SourceFilePathResult?> GetSourceFilePathAsync(string url, string relativePath, CancellationToken cancellationToken)
        {
            var result = await _debuggerSourceLinkService.GetSourceLinkAsync(url, relativePath, allowInteractiveLogin: false, cancellationToken).ConfigureAwait(false);

            if (result.Status == SourceLinkResultStatus.Succeeded && result.Path is not null)
            {
                return new SourceFilePathResult(result.Path);
            }
            else if (_logger is not null && result.Log is not null)
            {
                // We log specific info from the debugger if there is a failure, but the caller will log general failure
                // information otherwise.
                _logger.Log(result.Log);
            }

            return null;
        }
    }
}
