// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Runtime.InteropServices.ComTypes;
using Microsoft.Build.Shared;
using Microsoft.Build.Utilities;
using COMException = System.Runtime.InteropServices.COMException;
using Marshal = System.Runtime.InteropServices.Marshal;

#nullable disable

namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// Abstract base class for COM reference wrappers providing common functionality.
    /// This class hierarchy is used by the ResolveComReference task.Every class deriving from ComReference
    /// provides functionality for wrapping Com type libraries in a given way(for example AxReference, or PiaReference).
    /// </summary>
    internal abstract class ComReference
    {
        #region Constructors

        /// <summary>
        /// Internal constructor
        /// </summary>
        /// <param name="taskLoggingHelper">task logger instance used for logging</param>
        /// <param name="silent">true if this task should log only errors, no warnings or messages; false otherwise</param>
        /// <param name="referenceInfo">cached reference information (typelib pointer, original task item, typelib name etc.)</param>
        /// <param name="itemName">reference name (for better logging experience)</param>
        internal ComReference(TaskLoggingHelper taskLoggingHelper, bool silent, ComReferenceInfo referenceInfo, string itemName)
        {
            ReferenceInfo = referenceInfo;
            ItemName = itemName;
            Log = taskLoggingHelper;
            Silent = silent;
        }

        #endregion

        #region Properties

        /// <summary>
        /// various data for this reference (type lib attrs, name, path, ITypeLib pointer etc)
        /// </summary>
        internal virtual ComReferenceInfo ReferenceInfo { get; }

        /// <summary>
        /// item name as it appears in the project file
        /// (used for logging purposes, we use the actual typelib name for interesting operations)
        /// </summary>
        internal virtual string ItemName { get; }

        /// <summary>
        /// task used for logging messages
        /// </summary>
        protected internal TaskLoggingHelper Log { get; }

        /// <summary>
        /// True if this class should only log errors, but no messages or warnings.
        /// </summary>
        protected internal bool Silent { get; }

        /// <summary>
        /// lazy-init property, returns true if ADO 2.7 is installed on the machine
        /// </summary>
        internal static bool Ado27Installed
        {
            get
            {
                // if we already know the answer, return it
                if (ado27PropertyInitialized)
                {
                    return ado27Installed;
                }

                // not initialized? Find out if ADO 2.7 is installed
                ado27Installed = true;
                ado27PropertyInitialized = true;

                ITypeLib ado27 = null;

                try
                {
                    // see if ADO 2.7 is registered.
                    ado27 = (ITypeLib)NativeMethods.LoadRegTypeLib(ref s_guidADO27, 2, 7, 0);
                }
                catch (COMException ex)
                {
                    // it's not registered.
                    ado27Installed = false;
                    ado27ErrorMessage = ex.Message;
                }
                finally
                {
                    if (ado27 != null)
                    {
                        Marshal.ReleaseComObject(ado27);
                    }
                }

                return ado27Installed;
            }
        }

        internal static bool ado27PropertyInitialized;
        internal static bool ado27Installed;

        /// <summary>
        /// Error message if Ado27 is not installed on the machine (usually something like "type lib not registered")
        /// Only contains valid data if ADO 2.7 is not installed and Ado27Installed was called before
        /// </summary>
        internal static string Ado27ErrorMessage => ado27ErrorMessage;

        internal static string ado27ErrorMessage;

        #endregion

        #region Methods

        /// <summary>
        /// Given a TYPELIBATTR structure, generates a key that can be used in hashtables to identify it.
        /// </summary>
        internal static string UniqueKeyFromTypeLibAttr(TYPELIBATTR attr)
        {
            return $@"{attr.guid}|{attr.wMajorVerNum}.{attr.wMinorVerNum}|{attr.lcid}";
        }

        /// <summary>
        /// Compares two TYPELIBATTR structures
        /// </summary>
        internal static bool AreTypeLibAttrEqual(TYPELIBATTR attr1, TYPELIBATTR attr2)
        {
            return attr1.wMajorVerNum == attr2.wMajorVerNum &&
                attr1.wMinorVerNum == attr2.wMinorVerNum &&
                attr1.lcid == attr2.lcid &&
                attr1.guid == attr2.guid;
        }

        /// <summary>
        /// Helper method for retrieving type lib attributes for the given type lib
        /// </summary>
        internal static void GetTypeLibAttrForTypeLib(ref ITypeLib typeLib, out TYPELIBATTR typeLibAttr)
        {
            typeLib.GetLibAttr(out IntPtr pAttrs);

            // GetLibAttr should never return null, this is just to be safe
            if (pAttrs == IntPtr.Zero)
            {
                throw new COMException(
                    ResourceUtilities.GetResourceString("ResolveComReference.CannotGetTypeLibAttrForTypeLib"));
            }

            try
            {
                typeLibAttr = Marshal.PtrToStructure<TYPELIBATTR>(pAttrs);
            }
            finally
            {
                typeLib.ReleaseTLibAttr(pAttrs);
            }
        }

        /// <summary>
        /// Helper method for retrieving type attributes for a given type info
        /// </summary>
        /// <param name="typeInfo"></param>
        /// <param name="typeAttr"></param>
        /// <returns></returns>
        internal static void GetTypeAttrForTypeInfo(ITypeInfo typeInfo, out TYPEATTR typeAttr)
        {
            typeInfo.GetTypeAttr(out IntPtr pAttrs);

            // GetTypeAttr should never return null, this is just to be safe
            if (pAttrs == IntPtr.Zero)
            {
                throw new COMException(
                    ResourceUtilities.GetResourceString("ResolveComReference.CannotRetrieveTypeInformation"));
            }

            try
            {
                typeAttr = (TYPEATTR)Marshal.PtrToStructure(pAttrs, typeof(TYPEATTR));
            }
            finally
            {
                typeInfo.ReleaseTypeAttr(pAttrs);
            }
        }

        /// <summary>
        /// Helper method for retrieving type attributes for a given type info
        /// This method needs to also return the native pointer to be released when we're done with our VARDESC.
        /// It's not really possible to copy everything to a managed struct and then release the ptr immediately
        /// here, since VARDESCs contain other native pointers we may need to access.
        /// </summary>
        internal static void GetVarDescForVarIndex(ITypeInfo typeInfo, int varIndex, out VARDESC varDesc, out IntPtr varDescHandle)
        {
            typeInfo.GetVarDesc(varIndex, out IntPtr pVarDesc);

            // GetVarDesc should never return null, this is just to be safe
            if (pVarDesc == IntPtr.Zero)
            {
                throw new COMException(
                    ResourceUtilities.GetResourceString("ResolveComReference.CannotRetrieveTypeInformation"));
            }

            varDesc = (VARDESC)Marshal.PtrToStructure(pVarDesc, typeof(VARDESC));
            varDescHandle = pVarDesc;
        }

        /// <summary>
        /// Helper method for retrieving the function description structure for the given function index.
        /// This method needs to also return the native pointer to be released when we're done with our FUNCDESC.
        /// It's not really possible to copy everything to a managed struct and then release the ptr immediately
        /// here, since FUNCDESCs contain other native pointers we may need to access.
        /// </summary>
        internal static void GetFuncDescForDescIndex(ITypeInfo typeInfo, int funcIndex, out FUNCDESC funcDesc, out IntPtr funcDescHandle)
        {
            typeInfo.GetFuncDesc(funcIndex, out IntPtr pFuncDesc);

            // GetFuncDesc should never return null, this is just to be safe
            if (pFuncDesc == IntPtr.Zero)
            {
                throw new COMException(
                    ResourceUtilities.GetResourceString("ResolveComReference.CannotRetrieveTypeInformation"));
            }

            funcDesc = (FUNCDESC)Marshal.PtrToStructure(pFuncDesc, typeof(FUNCDESC));
            funcDescHandle = pFuncDesc;
        }

        /// <summary>
        /// Gets the name of given type library.
        /// </summary>
        internal static bool GetTypeLibNameForITypeLib(TaskLoggingHelper log, bool silent, ITypeLib typeLib, string typeLibId, out string typeLibName)
        {

            // see if the type library supports ITypeLib2
            if (!(typeLib is ITypeLib2 typeLib2))
            {
                // Looks like the type lib doesn't support it. Let's use the Marshal method.
                typeLibName = Marshal.GetTypeLibName(typeLib);
                return true;
            }

            // Get the custom attribute.  If anything fails then just return the
            // type library name.
            try
            {
                typeLib2.GetCustData(ref NativeMethods.GUID_TYPELIB_NAMESPACE, out object data);

                // if returned namespace is null or its type is not System.String, fall back to the default
                // way of getting the type lib name (just to be safe)
                if (data == null || !string.Equals(data.GetType().ToString(), "system.string", StringComparison.OrdinalIgnoreCase))
                {
                    typeLibName = Marshal.GetTypeLibName(typeLib);
                    return true;
                }

                // Strip off the DLL extension if it's there
                typeLibName = (string)data;

                if (typeLibName.Length >= 4)
                {
                    if (typeLibName.AsSpan().EndsWith(".dll".AsSpan(), StringComparison.OrdinalIgnoreCase))
                    {
                        typeLibName = typeLibName.Substring(0, typeLibName.Length - 4);
                    }
                }
            }
            catch (COMException ex)
            {
                // If anything fails log a warning and just return the type library name.
                if (!silent)
                {
                    log.LogWarningWithCodeFromResources("ResolveComReference.CannotAccessTypeLibName", typeLibId, ex.Message);
                }
                typeLibName = Marshal.GetTypeLibName(typeLib);
                return true;
            }

            return true;
        }

        /// <summary>
        /// Gets the name of given type library.
        /// </summary>
        internal static bool GetTypeLibNameForTypeLibAttrs(TaskLoggingHelper log, bool silent, TYPELIBATTR typeLibAttr, out string typeLibName)
        {
            typeLibName = "";
            ITypeLib typeLib = null;

            try
            {
                // load our type library
                try
                {
                    TYPELIBATTR attr = typeLibAttr;
                    typeLib = (ITypeLib)NativeMethods.LoadRegTypeLib(ref attr.guid, attr.wMajorVerNum, attr.wMinorVerNum, attr.lcid);
                }
                catch (COMException ex)
                {
                    if (!silent)
                    {
                        log.LogWarningWithCodeFromResources("ResolveComReference.CannotLoadTypeLib", typeLibAttr.guid, typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, ex.Message);
                    }

                    return false;
                }

                string typeLibId = log.FormatResourceString("ResolveComReference.TypeLibAttrId", typeLibAttr.guid.ToString(), typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum);

                return GetTypeLibNameForITypeLib(log, silent, typeLib, typeLibId, out typeLibName);
            }
            finally
            {
                if (typeLib != null)
                {
                    Marshal.ReleaseComObject(typeLib);
                }
            }
        }

        /// <summary>
        /// Strips type library number from a type library path (for example, "ref.dll\2" becomes "ref.dll")
        /// </summary>
        /// <param name="typeLibPath">type library path with possible typelib number appended to it</param>
        /// <param name="fileExists">Delegate to check whether the file exists</param>
        /// <returns>proper file path to the type library</returns>
        internal static string StripTypeLibNumberFromPath(string typeLibPath, FileExists fileExists)
        {
            bool lastChance = false;
            if (!string.IsNullOrEmpty(typeLibPath))
            {
                if (!fileExists(typeLibPath))
                {
                    // Strip the type library number
                    int lastSlash = typeLibPath.LastIndexOf('\\');

                    if (lastSlash != -1)
                    {
                        bool allNumbers = true;

                        for (int i = lastSlash + 1; i < typeLibPath.Length; i++)
                        {
                            if (!Char.IsDigit(typeLibPath[i]))
                            {
                                allNumbers = false;
                                break;
                            }
                        }

                        // If we had all numbers past the last slash then we're OK to strip
                        // the type library number
                        if (allNumbers)
                        {
                            typeLibPath = typeLibPath.Substring(0, lastSlash);
                            if (!fileExists(typeLibPath))
                            {
                                lastChance = true;
                            }
                        }
                        else
                        {
                            lastChance = true;
                        }
                    }
                    else
                    {
                        lastChance = true;
                    }
                }
            }

            // If we couldn't find the path directly, we'll use the same mechanism Windows uses to find
            // libraries.  LoadLibrary() will search all of the correct paths to find this module.  We can then
            // use GetModuleFileName() to determine the actual path from which the module was loaded.  This problem
            // was exposed in Vista where certain libraries are registered but are lacking paths in the registry,
            // so the old code would fail to find them on disk using the simplistic checks above.
            if (lastChance)
            {
                IntPtr libraryHandle = NativeMethodsShared.LoadLibrary(typeLibPath);
                if (IntPtr.Zero != libraryHandle)
                {
                    try
                    {
                        typeLibPath = GetModuleFileName(libraryHandle);
                    }
                    finally
                    {
                        NativeMethodsShared.FreeLibrary(libraryHandle);
                    }
                }
                else
                {
                    typeLibPath = "";
                }
            }

            return typeLibPath;
        }

        private static string GetModuleFileName(IntPtr handle)
        {
            char[] buffer = null;

            // Try increased buffer sizes if on longpath-enabled Windows
            for (int bufferSize = NativeMethodsShared.MAX_PATH; bufferSize <= NativeMethodsShared.MaxPath; bufferSize *= 2)
            {
                buffer = System.Buffers.ArrayPool<char>.Shared.Rent(bufferSize);
                try
                {
                    var handleRef = new System.Runtime.InteropServices.HandleRef(buffer, handle);
                    int pathLength = NativeMethodsShared.GetModuleFileName(handleRef, buffer, bufferSize);

                    bool isBufferTooSmall = (uint)Marshal.GetLastWin32Error() == NativeMethodsShared.ERROR_INSUFFICIENT_BUFFER;
                    if (pathLength != 0 && !isBufferTooSmall)
                    {
                        return new string(buffer, 0, pathLength);
                    }
                }
                finally
                {
                    System.Buffers.ArrayPool<char>.Shared.Return(buffer);
                }

                // Double check that the buffer is not insanely big
                ErrorUtilities.VerifyThrow(bufferSize <= int.MaxValue / 2, "Buffer size approaching int.MaxValue");
            }

            return string.Empty;
        }

        /// <summary>
        /// Gets the type lib path for given type lib attributes(reused almost verbatim from vsdesigner utils code)
        /// NOTE:  If there's a typelib number at the end of the path, does NOT strip it.
        /// </summary>
        internal static bool GetPathOfTypeLib(TaskLoggingHelper log, bool silent, ref TYPELIBATTR typeLibAttr, out string typeLibPath)
        {
            // Get which file the type library resides in.  If the appropriate
            // file cannot be found then a blank string is returned.
            typeLibPath = "";

            try
            {
                // Get the path from the registry
                // This call has known issues. See http://msdn.microsoft.com/en-us/library/ms221436.aspx for the method and
                // here for the fix http://support.microsoft.com/kb/982110. Most users from Win7 or Win2008R2 should have already received this post Win7SP1.
                // In Summary: The issue is about calls to The QueryPathOfRegTypeLib function not returning the correct path for a 32-bit version of a
                // registered type library in a 64-bit edition of Windows 7 or in Windows Server 2008 R2. It either returns the 64bit path or null.
                typeLibPath = NativeMethods.QueryPathOfRegTypeLib(ref typeLibAttr.guid, typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, typeLibAttr.lcid);
                typeLibPath = Environment.ExpandEnvironmentVariables(typeLibPath);
            }
            catch (COMException ex)
            {
                if (!silent)
                {
                    log.LogWarningWithCodeFromResources("ResolveComReference.CannotGetPathForTypeLib", typeLibAttr.guid, typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, ex.Message);
                }

                return false;
            }

            if (!string.IsNullOrEmpty(typeLibPath))
            {
                // We have to check for NULL here because QueryPathOfRegTypeLib() returns
                // a BSTR with a NULL character appended to it.
                if (typeLibPath[typeLibPath.Length - 1] == '\0')
                {
                    typeLibPath = typeLibPath.Substring(0, typeLibPath.Length - 1);
                }
            }

            if (!string.IsNullOrEmpty(typeLibPath))
            {
                return true;
            }

            if (!silent)
            {
                log.LogWarningWithCodeFromResources("ResolveComReference.CannotGetPathForTypeLib", typeLibAttr.guid, typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, "");
            }

            return false;
        }

        #region RemapAdoTypeLib guids

        // guids for RemapAdoTypeLib
        private static readonly Guid s_guidADO20 = new Guid("{00000200-0000-0010-8000-00AA006D2EA4}");
        private static readonly Guid s_guidADO21 = new Guid("{00000201-0000-0010-8000-00AA006D2EA4}");
        private static readonly Guid s_guidADO25 = new Guid("{00000205-0000-0010-8000-00AA006D2EA4}");
        private static readonly Guid s_guidADO26 = new Guid("{00000206-0000-0010-8000-00AA006D2EA4}");
        // unfortunately this cannot be readonly, since it's being passed by reference to LoadRegTypeLib
        private static Guid s_guidADO27 = new Guid("{EF53050B-882E-4776-B643-EDA472E8E3F2}");

        #endregion

        /// <summary>
        /// Tries to remap an ADO type library to ADO 2.7. If the type library passed in is an older ADO tlb,
        /// then remap it to ADO 2.7 if it's registered on the machine (!). Otherwise don't modify the typelib.
        /// Returns true if the type library passed in was successfully remapped.
        /// </summary>
        internal static bool RemapAdoTypeLib(TaskLoggingHelper log, bool silent, ref TYPELIBATTR typeLibAttr)
        {
            // we only care about ADO 2.0, 2.1, 2.5 or 2.6 here.
            if (typeLibAttr.wMajorVerNum == 2)
            {
                if ((typeLibAttr.wMinorVerNum == 0 && typeLibAttr.guid == s_guidADO20) ||
                    (typeLibAttr.wMinorVerNum == 1 && typeLibAttr.guid == s_guidADO21) ||
                    (typeLibAttr.wMinorVerNum == 5 && typeLibAttr.guid == s_guidADO25) ||
                    (typeLibAttr.wMinorVerNum == 6 && typeLibAttr.guid == s_guidADO26))
                {
                    // see if ADO 2.7 is registered.
                    if (!Ado27Installed)
                    {
                        if (!silent)
                        {
                            // it's not registered. Don't change the original typelib then.
                            log.LogWarningWithCodeFromResources("ResolveComReference.FailedToRemapAdoTypeLib", typeLibAttr.wMajorVerNum, typeLibAttr.wMinorVerNum, Ado27ErrorMessage);
                        }

                        return false;
                    }

                    typeLibAttr.guid = s_guidADO27;
                    typeLibAttr.wMajorVerNum = 2;
                    typeLibAttr.wMinorVerNum = 7;
                    typeLibAttr.lcid = 0;

                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Finds an existing wrapper for the specified component
        /// </summary>
        internal abstract bool FindExistingWrapper(out ComReferenceWrapperInfo wrapperInfo, DateTime componentTimestamp);

        #endregion
    }
}
