// 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.Collections.Generic;
using System.Runtime.InteropServices.ComTypes;
using Xunit;
using IFixedTypeInfo = Microsoft.Build.Tasks.IFixedTypeInfo;
using Marshal = System.Runtime.InteropServices.Marshal;
using VarEnum = System.Runtime.InteropServices.VarEnum;

#nullable disable

namespace Microsoft.Build.UnitTests
{
    /// <summary>
    /// A generic interface for creating composite type infos - e.g. a safe array of pointers to something
    /// </summary>
    public interface ICompositeTypeInfo
    {
        TYPEDESC CreateTypeDesc(IntPtr finalTypeHRef, MockUnmanagedMemoryHelper memoryHelper);

        MockTypeInfo GetFinalTypeInfo();
    }

    /// <summary>
    /// Safe array composite type info
    /// </summary>
    public class ArrayCompositeTypeInfo : ICompositeTypeInfo
    {
        private ICompositeTypeInfo _baseElementType;

        public ArrayCompositeTypeInfo(ICompositeTypeInfo baseElement)
        {
            _baseElementType = baseElement;
        }

        #region ICreateTypeDesc Members

        public TYPEDESC CreateTypeDesc(IntPtr finalTypeHRef, MockUnmanagedMemoryHelper memoryHelper)
        {
            TYPEDESC typeDesc;
            typeDesc.vt = (short)VarEnum.VT_SAFEARRAY;
            typeDesc.lpValue = memoryHelper.AllocateHandle(Marshal.SizeOf<TYPEDESC>());
            Marshal.StructureToPtr(_baseElementType.CreateTypeDesc(finalTypeHRef, memoryHelper), typeDesc.lpValue, false);
            return typeDesc;
        }

        /// <summary>
        /// Defer to the base element to get the final type info - this will eventually terminate at a MockTypeInfo node 
        /// which returns itself
        /// </summary>
        /// <returns></returns>
        public MockTypeInfo GetFinalTypeInfo()
        {
            return _baseElementType.GetFinalTypeInfo();
        }

        #endregion
    }

    /// <summary>
    /// Pointer composite type info
    /// </summary>
    public class PtrCompositeTypeInfo : ICompositeTypeInfo
    {
        private ICompositeTypeInfo _baseElementType;

        public PtrCompositeTypeInfo(ICompositeTypeInfo baseElement)
        {
            _baseElementType = baseElement;
        }

        #region ICompositeTypeInfo Members

        public TYPEDESC CreateTypeDesc(IntPtr finalTypeHRef, MockUnmanagedMemoryHelper memoryHelper)
        {
            TYPEDESC typeDesc;
            typeDesc.vt = (short)VarEnum.VT_PTR;
            typeDesc.lpValue = memoryHelper.AllocateHandle(Marshal.SizeOf<TYPEDESC>());
            Marshal.StructureToPtr(_baseElementType.CreateTypeDesc(finalTypeHRef, memoryHelper), typeDesc.lpValue, false);
            return typeDesc;
        }

        public MockTypeInfo GetFinalTypeInfo()
        {
            return _baseElementType.GetFinalTypeInfo();
        }

        #endregion
    }

    /// <summary>
    /// All the information necessary to describe a single function signature
    /// </summary>
    public struct FuncInfo
    {
        public FuncInfo(ICompositeTypeInfo[] parameters, ICompositeTypeInfo returnType)
        {
            this.parameters = parameters;
            this.returnType = returnType;
        }

        public ICompositeTypeInfo[] parameters;
        public ICompositeTypeInfo returnType;
    }

    /// <summary>
    /// Mock class for the ITypeInfo interface
    /// </summary>
    public class MockTypeInfo : ITypeInfo, ICompositeTypeInfo, IFixedTypeInfo
    {
        private static int s_HREF_IMPLTYPES_OFFSET = 1000;
        private static int s_HREF_VARS_OFFSET = 2000;
        private static int s_HREF_FUNCSRET_OFFSET = 3000;
        private static int s_HREF_FUNCSPARAM_OFFSET = 4000;
        private static int s_HREF_FUNCSPARAM_OFFSET_PERFUNC = 100;
        private static int s_HREF_RANGE = 999;

        private MockTypeLib _containingTypeLib;

        public MockTypeLib ContainingTypeLib
        {
            set
            {
                _containingTypeLib = value;
            }
        }

        private int _indexInContainingTypeLib;

        public int IndexInContainingTypeLib
        {
            set
            {
                _indexInContainingTypeLib = value;
            }
        }

        private string _typeName;

        public string TypeName
        {
            set
            {
                _typeName = value;
            }
        }

        private TYPEATTR _typeAttributes;

        private List<MockTypeInfo> _implementedTypes;
        private List<ICompositeTypeInfo> _definedVariables;
        private List<FuncInfo> _definedFunctions;

        private MockUnmanagedMemoryHelper _memoryHelper;

        private MockFaultInjectionHelper<MockTypeLibrariesFailurePoints> _faultInjector;

        /// <summary>
        /// Default constructor
        /// </summary>
        public MockTypeInfo()
        {
            _implementedTypes = new List<MockTypeInfo>();
            _definedVariables = new List<ICompositeTypeInfo>();
            _definedFunctions = new List<FuncInfo>();

            _memoryHelper = new MockUnmanagedMemoryHelper();

            // each type has a unique guid
            _typeAttributes.guid = Guid.NewGuid();

            // default typekind value is TKIND_ENUM so just pick something else that doesn't have a special meaning in the code
            // (we skip enum type infos)
            _typeAttributes.typekind = TYPEKIND.TKIND_INTERFACE;
        }

        /// <summary>
        /// Use a known guid for creating this type info
        /// </summary>
        /// <param name="guid"></param>
        public MockTypeInfo(Guid guid)
            : this()
        {
            _typeAttributes.guid = guid;
        }

        /// <summary>
        /// Use a custom type kind
        /// </summary>
        /// <param name="typeKind"></param>
        public MockTypeInfo(TYPEKIND typeKind)
            : this()
        {
            _typeAttributes.typekind = typeKind;
        }

        /// <summary>
        /// Adds implementedType to the list of this type's implemented interfaces
        /// </summary>
        /// <param name="implementedType"></param>
        public void ImplementsInterface(MockTypeInfo implementedType)
        {
            _typeAttributes.cImplTypes++;
            _implementedTypes.Add(implementedType);
        }

        /// <summary>
        /// Adds implementedType to the list of this type's defined variables
        /// </summary>
        /// <param name="implementedType"></param>
        public void DefinesVariable(ICompositeTypeInfo variableType)
        {
            _typeAttributes.cVars++;
            _definedVariables.Add(variableType);
        }

        /// <summary>
        /// Adds a new function signature to the list of this type's implemented functions
        /// </summary>
        /// <param name="implementedType"></param>
        public void DefinesFunction(MockTypeInfo[] parameters, MockTypeInfo returnType)
        {
            _typeAttributes.cFuncs++;
            _definedFunctions.Add(new FuncInfo(parameters, returnType));
        }

        /// <summary>
        /// Sets the fault injection object for this type
        /// </summary>
        /// <param name="faultInjector"></param>
        public void SetFaultInjector(MockFaultInjectionHelper<MockTypeLibrariesFailurePoints> faultInjector)
        {
            _faultInjector = faultInjector;
        }

        /// <summary>
        /// Helper method for verifying there are no memory leaks
        /// </summary>
        public void AssertAllHandlesReleased()
        {
            _memoryHelper.AssertAllHandlesReleased();
        }

        #region IFixedTypeInfo members

        void IFixedTypeInfo.GetRefTypeOfImplType(int index, out System.IntPtr href)
        {
            Assert.True(index >= 0 && index < _typeAttributes.cImplTypes);

            _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetRefTypeOfImplType);

            href = ((System.IntPtr)index + s_HREF_IMPLTYPES_OFFSET);
        }

        void IFixedTypeInfo.GetRefTypeInfo(System.IntPtr hRef, out IFixedTypeInfo ppTI)
        {
            _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetRefTypeInfo);
            int hRefInt = (int)hRef;

            if (hRefInt >= s_HREF_IMPLTYPES_OFFSET && hRefInt <= s_HREF_IMPLTYPES_OFFSET + s_HREF_RANGE)
            {
                ppTI = _implementedTypes[hRefInt - s_HREF_IMPLTYPES_OFFSET];
            }
            else if (hRefInt >= s_HREF_VARS_OFFSET && hRefInt <= s_HREF_VARS_OFFSET + s_HREF_RANGE)
            {
                ppTI = _definedVariables[hRefInt - s_HREF_VARS_OFFSET].GetFinalTypeInfo();
            }
            else if (hRefInt >= s_HREF_FUNCSRET_OFFSET && hRefInt <= s_HREF_FUNCSRET_OFFSET + s_HREF_RANGE)
            {
                ppTI = _definedFunctions[hRefInt - s_HREF_FUNCSRET_OFFSET].returnType.GetFinalTypeInfo();
            }
            else if (hRefInt >= s_HREF_FUNCSPARAM_OFFSET && hRefInt <= s_HREF_FUNCSPARAM_OFFSET + s_HREF_RANGE)
            {
                ppTI = _definedFunctions[(hRefInt - s_HREF_FUNCSPARAM_OFFSET) / s_HREF_FUNCSPARAM_OFFSET_PERFUNC].parameters[(hRefInt - s_HREF_FUNCSPARAM_OFFSET) % s_HREF_FUNCSPARAM_OFFSET_PERFUNC].GetFinalTypeInfo();
            }
            else
            {
                ppTI = null;
                Assert.True(false, "unexpected hRef value");
            }
        }

        #endregion 

        #region Implemented ITypeInfo members

        public void GetContainingTypeLib(out ITypeLib ppTLB, out int pIndex)
        {
            _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetContainingTypeLib);

            ppTLB = _containingTypeLib;
            pIndex = _indexInContainingTypeLib;
        }

        public void GetTypeAttr(out IntPtr ppTypeAttr)
        {
            // Fail BEFORE allocating the handle to avoid leaks. If the real COM object fails in this method
            // and doesn't return the handle or clean it up itself there's not much we can do to avoid the leak.
            _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetTypeAttr);

            ppTypeAttr = _memoryHelper.AllocateHandle(Marshal.SizeOf<TYPEATTR>());
            Marshal.StructureToPtr(_typeAttributes, ppTypeAttr, false);
        }

        public void ReleaseTypeAttr(IntPtr pTypeAttr)
        {
            _memoryHelper.FreeHandle(pTypeAttr);

            // Fail AFTER releasing the handle to avoid leaks. If the real COM object fails in this method
            // there's really nothing we can do to avoid leaking stuff
            _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_ReleaseTypeAttr);
        }

        public void GetRefTypeOfImplType(int index, out int href)
        {
            Assert.True(index >= 0 && index < _typeAttributes.cImplTypes);

            _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetRefTypeOfImplType);

            href = index + s_HREF_IMPLTYPES_OFFSET;
        }

        public void GetRefTypeInfo(int hRef, out ITypeInfo ppTI)
        {
            _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetRefTypeInfo);

            if (hRef >= s_HREF_IMPLTYPES_OFFSET && hRef <= s_HREF_IMPLTYPES_OFFSET + s_HREF_RANGE)
            {
                ppTI = _implementedTypes[hRef - s_HREF_IMPLTYPES_OFFSET];
            }
            else if (hRef >= s_HREF_VARS_OFFSET && hRef <= s_HREF_VARS_OFFSET + s_HREF_RANGE)
            {
                ppTI = _definedVariables[hRef - s_HREF_VARS_OFFSET].GetFinalTypeInfo();
            }
            else if (hRef >= s_HREF_FUNCSRET_OFFSET && hRef <= s_HREF_FUNCSRET_OFFSET + s_HREF_RANGE)
            {
                ppTI = _definedFunctions[hRef - s_HREF_FUNCSRET_OFFSET].returnType.GetFinalTypeInfo();
            }
            else if (hRef >= s_HREF_FUNCSPARAM_OFFSET && hRef <= s_HREF_FUNCSPARAM_OFFSET + s_HREF_RANGE)
            {
                ppTI = _definedFunctions[(hRef - s_HREF_FUNCSPARAM_OFFSET) / s_HREF_FUNCSPARAM_OFFSET_PERFUNC].parameters[(hRef - s_HREF_FUNCSPARAM_OFFSET) % s_HREF_FUNCSPARAM_OFFSET_PERFUNC].GetFinalTypeInfo();
            }
            else
            {
                ppTI = null;
                Assert.True(false, "unexpected hRef value");
            }
        }

        public void GetVarDesc(int index, out IntPtr ppVarDesc)
        {
            // Fail BEFORE allocating the handle to avoid leaks. If the real COM object fails in this method
            // and doesn't return the handle or clean it up itself there's not much we can do to avoid the leak.
            _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetVarDesc);

            ppVarDesc = _memoryHelper.AllocateHandle(Marshal.SizeOf<VARDESC>());

            _memoryHelper.EnterSubAllocationScope(ppVarDesc);
            VARDESC varDesc = new VARDESC();
            varDesc.elemdescVar.tdesc = _definedVariables[index].CreateTypeDesc(new IntPtr(index + s_HREF_VARS_OFFSET), _memoryHelper);
            _memoryHelper.ExitSubAllocationScope();

            Marshal.StructureToPtr(varDesc, ppVarDesc, false);
        }

        public void ReleaseVarDesc(IntPtr pVarDesc)
        {
            _memoryHelper.FreeHandle(pVarDesc);

            // Fail AFTER releasing the handle to avoid leaks. If the real COM object fails in this method
            // there's really nothing we can do to avoid leaking stuff
            _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_ReleaseVarDesc);
        }

        public void GetFuncDesc(int index, out IntPtr ppFuncDesc)
        {
            // Fail BEFORE allocating the handle to avoid leaks. If the real COM object fails in this method
            // and doesn't return the handle or clean it up itself there's not much we can do to avoid the leak.
            _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetFuncDesc);

            ppFuncDesc = _memoryHelper.AllocateHandle(Marshal.SizeOf<FUNCDESC>());

            _memoryHelper.EnterSubAllocationScope(ppFuncDesc);
            FUNCDESC funcDesc = new FUNCDESC();

            funcDesc.lprgelemdescParam = _memoryHelper.AllocateHandle(_definedFunctions[index].parameters.Length * Marshal.SizeOf<ELEMDESC>());
            funcDesc.cParams = (short)_definedFunctions[index].parameters.Length;

            for (int i = 0; i < _definedFunctions[index].parameters.Length; i++)
            {
                ELEMDESC elemDesc = new ELEMDESC();
                elemDesc.tdesc = _definedFunctions[index].parameters[i].CreateTypeDesc(
                    new IntPtr((index * s_HREF_FUNCSPARAM_OFFSET_PERFUNC) + i + s_HREF_FUNCSPARAM_OFFSET), _memoryHelper);

                Marshal.StructureToPtr(
                    elemDesc,
                    new IntPtr(funcDesc.lprgelemdescParam.ToInt64() + (i * Marshal.SizeOf<ELEMDESC>())),
                    false);
            }

            funcDesc.elemdescFunc.tdesc = _definedFunctions[index].returnType.CreateTypeDesc(
                new IntPtr(index + s_HREF_FUNCSRET_OFFSET), _memoryHelper);
            _memoryHelper.ExitSubAllocationScope();

            Marshal.StructureToPtr(funcDesc, ppFuncDesc, false);
        }

        public void ReleaseFuncDesc(IntPtr pFuncDesc)
        {
            _memoryHelper.FreeHandle(pFuncDesc);

            // Fail AFTER releasing the handle to avoid leaks. If the real COM object fails in this method
            // there's really nothing we can do to avoid leaking stuff
            _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_ReleaseFuncDesc);
        }

        public void GetDocumentation(int index, out string strName, out string strDocString, out int dwHelpContext, out string strHelpFile)
        {
            Assert.Equal(-1, index);

            _faultInjector.FailurePointThrow(MockTypeLibrariesFailurePoints.ITypeInfo_GetDocumentation);

            strName = _typeName;
            strDocString = "garbage";
            dwHelpContext = -1;
            strHelpFile = "garbage^2";
        }

        #endregion

        #region ICreateTypeDesc Members

        public TYPEDESC CreateTypeDesc(IntPtr finalTypeHRef, MockUnmanagedMemoryHelper memoryHelper)
        {
            TYPEDESC typeDesc;
            typeDesc.vt = (short)VarEnum.VT_USERDEFINED;
            typeDesc.lpValue = finalTypeHRef;
            return typeDesc;
        }

        public MockTypeInfo GetFinalTypeInfo()
        {
            return this;
        }

        #endregion

        #region Stubbed ITypeInfo members

        public void AddressOfMember(int memid, INVOKEKIND invKind, out IntPtr ppv)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public void CreateInstance(object pUnkOuter, ref Guid riid, out object ppvObj)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public void GetDllEntry(int memid, INVOKEKIND invKind, IntPtr pBstrDllName, IntPtr pBstrName, IntPtr pwOrdinal)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public void GetIDsOfNames(string[] rgszNames, int cNames, int[] pMemId)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public void GetImplTypeFlags(int index, out IMPLTYPEFLAGS pImplTypeFlags)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public void GetMops(int memid, out string pBstrMops)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public void GetNames(int memid, string[] rgBstrNames, int cMaxNames, out int pcNames)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public void GetTypeComp(out ITypeComp ppTComp)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public void Invoke(object pvInstance, int memid, short wFlags, ref DISPPARAMS pDispParams, IntPtr pVarResult, IntPtr pExcepInfo, out int puArgErr)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        #endregion
    }
}
