// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using System;
using System.Diagnostics;

namespace Microsoft.DiaSymReader
{
    public static class SymUnmanagedWriterFactory
    {
        /// <summary>
        /// Creates a Windows PDB writer.
        /// </summary>
        /// <param name="metadataProvider"><see cref="ISymWriterMetadataProvider"/> implementation.</param>
        /// <param name="options">Options.</param>
        /// <remarks>
        /// Tries to load the implementation of the PDB writer from Microsoft.DiaSymReader.Native.{platform}.dll library first.
        /// It searches for this library in the directory Microsoft.DiaSymReader.dll is loaded from, 
        /// the application directory, the %WinDir%\System32 directory, and user directories in the DLL search path, in this order.
        /// If not found in the above locations and <see cref="SymUnmanagedWriterCreationOptions.UseAlternativeLoadPath"/> option is specified
        /// the directory specified by MICROSOFT_DIASYMREADER_NATIVE_ALT_LOAD_PATH environment variable is also searched.
        /// If the Microsoft.DiaSymReader.Native.{platform}.dll library can't be found and <see cref="SymUnmanagedWriterCreationOptions.UseComRegistry"/> 
        /// option is specified checks if the PDB reader is available from a globally registered COM object. This COM object is provided 
        /// by .NET Framework and has limited functionality (features like determinism and source link are not supported).
        /// </remarks>
        /// <exception cref="ArgumentNullException"><paramref name="metadataProvider"/>is null.</exception>
        /// <exception cref="DllNotFoundException">The SymWriter implementation is not available or failed to load.</exception>
        /// <exception cref="SymUnmanagedWriterException">Error creating the PDB writer. See inner exception for root cause.</exception>
        public static SymUnmanagedWriter CreateWriter(
            ISymWriterMetadataProvider metadataProvider, 
            SymUnmanagedWriterCreationOptions options = SymUnmanagedWriterCreationOptions.Default)
        {
            if (metadataProvider == null)
            {
                throw new ArgumentNullException(nameof(metadataProvider));
            }

            var symWriter = SymUnmanagedFactory.CreateObject(
                createReader: false,
                useAlternativeLoadPath: (options & SymUnmanagedWriterCreationOptions.UseAlternativeLoadPath) != 0,
                useComRegistry: (options & SymUnmanagedWriterCreationOptions.UseComRegistry) != 0,
                moduleName: out var implModuleName,
                loadException: out var loadException);

            if (symWriter == null)
            {
                Debug.Assert(loadException != null);

                if (loadException is DllNotFoundException)
                {
                    throw loadException;
                }

                throw new DllNotFoundException(loadException.Message, loadException);
            }

            if (!(symWriter is ISymUnmanagedWriter5 symWriter5))
            {
                throw new SymUnmanagedWriterException(new NotSupportedException(), implModuleName);
            }

            object metadataEmitAndImport = new SymWriterMetadataAdapter(metadataProvider);
            var pdbStream = new ComMemoryStream();

            try
            {
                if ((options & SymUnmanagedWriterCreationOptions.Deterministic) != 0)
                {
                    if (symWriter is ISymUnmanagedWriter8 symWriter8)
                    {
                        symWriter8.InitializeDeterministic(metadataEmitAndImport, pdbStream);
                    }
                    else
                    {
                        throw new NotSupportedException();
                    }
                }
                else
                {
                    // The file name is irrelevant as long as it's specified.
                    // SymWriter only uses it for filling CodeView debug directory data when asked for them, but we never do.
                    symWriter5.Initialize(metadataEmitAndImport, "filename.pdb", pdbStream, fullBuild: true);
                }
            }
            catch (Exception e)
            {
                throw new SymUnmanagedWriterException(e, implModuleName);
            }

            return new SymUnmanagedWriterImpl(pdbStream, symWriter5, implModuleName);
        }
    }
}
