// 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.IO;
using System.Linq;
using System.Reflection.PortableExecutable;
using Microsoft.CodeAnalysis.Debugging;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Emit;

/// <summary>
/// Provides access to compilation outputs based only on the path of the output asssembly.
/// If PDB path is known upfront use <see cref="CompilationOutputFiles"/> instead.
/// </summary>
internal sealed class CompilationOutputFilesWithImplicitPdbPath : CompilationOutputs
{
    public string? AssemblyFilePath { get; }

    public CompilationOutputFilesWithImplicitPdbPath(string? assemblyFilePath = null)
    {
        if (assemblyFilePath != null)
        {
            CompilerPathUtilities.RequireAbsolutePath(assemblyFilePath, nameof(assemblyFilePath));
        }

        AssemblyFilePath = assemblyFilePath;
    }

    public override string? AssemblyDisplayPath => AssemblyFilePath;

    // heuristic for error messages (determining the actual path requires opening the assembly):
    public override string PdbDisplayPath => Path.GetFileNameWithoutExtension(AssemblyFilePath) + ".pdb";

    protected override Stream? OpenAssemblyStream()
        => TryOpenFileStream(AssemblyFilePath);

    // Not gonna be called since we override OpenPdb.
    protected override Stream OpenPdbStream()
        => throw ExceptionUtilities.Unreachable();

    public override DebugInformationReaderProvider? OpenPdb()
    {
        var assemblyStream = OpenAssemblyStream();
        if (assemblyStream == null)
        {
            return null;
        }

        // find associated PDB
        string pdbPath;
        using (var peReader = new PEReader(assemblyStream))
        {
            var debugDirectory = peReader.ReadDebugDirectory();
            var embeddedPdbEntry = debugDirectory.FirstOrDefault(e => e.Type == DebugDirectoryEntryType.EmbeddedPortablePdb);
            if (embeddedPdbEntry.DataSize != 0)
            {
                return DebugInformationReaderProvider.CreateFromMetadataReader(peReader.ReadEmbeddedPortablePdbDebugDirectoryData(embeddedPdbEntry));
            }

            var codeViewEntry = debugDirectory.FirstOrDefault(e => e.Type == DebugDirectoryEntryType.CodeView);
            if (codeViewEntry.DataSize == 0)
            {
                return null;
            }

            pdbPath = peReader.ReadCodeViewDebugDirectoryData(codeViewEntry).Path;
        }

        // First try to use the full path as specified in the PDB, then look next to the assembly.
        var pdbStream =
            TryOpenFileStream(pdbPath) ??
            TryOpenFileStream(Path.Combine(Path.GetDirectoryName(AssemblyFilePath)!, PathUtilities.GetFileName(pdbPath)));

        return (pdbStream != null) ? DebugInformationReaderProvider.CreateFromStream(pdbStream) : null;
    }

    private static Stream? TryOpenFileStream(string? path)
    {
        if (path == null)
        {
            return null;
        }

        try
        {
            return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete);
        }
        catch (Exception e) when (e is FileNotFoundException or DirectoryNotFoundException)
        {
            return null;
        }
        catch (Exception e) when (e is not IOException)
        {
            throw new IOException(e.Message, e);
        }
    }
}
