﻿// 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);
            }
        }
    }
}
