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

using Windows.Win32.Foundation;
using Windows.Win32.System.Com;

#if NET
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Lifetime = Windows.Win32.System.Com.Lifetime<Windows.Win32.System.Com.IDataObject.Vtbl, System.Private.Windows.Ole.DataObjectProxy>;
#else
using Windows.Win32;
#endif

namespace System.Private.Windows.Ole;

/// <summary>
///  Emulates an OLE proxy for unit testing purposes.
/// </summary>
internal unsafe class DataObjectProxy : IDataObject.Interface, IDisposable
{
    // Agile for ensured cleanup
    private readonly AgileComPointer<IDataObject> _agileOriginal;
    private readonly IDataObject* _original;
    private readonly AgileComPointer<IDataObject> _agileProxy;
    public IDataObject* Proxy { get; }

    public DataObjectProxy(IDataObject* original)
    {
        // Don't track disposal, we depend on finalization for testing.

        _original = original;
        _agileOriginal = new(
#if DEBUG
            original, takeOwnership: true, trackDisposal: false
#else
            original, takeOwnership: true
#endif
            );

        Proxy =
#if NET
            CCW.Create(this);
#else
            ComHelpers.GetComPointer<IDataObject>(this);
#endif

        _agileProxy = new(
#if DEBUG
            Proxy, takeOwnership: true, trackDisposal: false
#else
            Proxy, takeOwnership: true
#endif
            );
    }

    public HRESULT GetData(FORMATETC* pformatetcIn, STGMEDIUM* pmedium) => _original->GetData(pformatetcIn, pmedium);
    public HRESULT GetDataHere(FORMATETC* pformatetc, STGMEDIUM* pmedium) => _original->GetDataHere(pformatetc, pmedium);
    public HRESULT QueryGetData(FORMATETC* pformatetc) => _original->QueryGetData(pformatetc);
    public HRESULT GetCanonicalFormatEtc(FORMATETC* pformatectIn, FORMATETC* pformatetcOut) => _original->GetCanonicalFormatEtc(pformatectIn, pformatetcOut);
    public HRESULT SetData(FORMATETC* pformatetc, STGMEDIUM* pmedium, BOOL fRelease) => _original->SetData(pformatetc, pmedium, fRelease);
    public HRESULT EnumFormatEtc(uint dwDirection, IEnumFORMATETC** ppenumFormatEtc) => _original->EnumFormatEtc(dwDirection, ppenumFormatEtc);
    public HRESULT DAdvise(FORMATETC* pformatetc, uint advf, IAdviseSink* pAdvSink, uint* pdwConnection) => _original->DAdvise(pformatetc, advf, pAdvSink, pdwConnection);
    public HRESULT DUnadvise(uint dwConnection) => _original->DUnadvise(dwConnection);
    public HRESULT EnumDAdvise(IEnumSTATDATA** ppenumAdvise) => _original->EnumDAdvise(ppenumAdvise);

    public void Dispose()
    {
        _agileOriginal.Dispose();
        _agileProxy.Dispose();
    }

#if NET
    internal static class CCW
    {
        private static readonly IDataObject.Vtbl* s_vtable = AllocateVTable();

        private static IDataObject.Vtbl* AllocateVTable()
        {
            // Allocate and create a singular VTable for this type projection.
            IDataObject.Vtbl* vtable = (IDataObject.Vtbl*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(CCW), sizeof(IDataObject.Vtbl));

            // IUnknown
            vtable->QueryInterface_1 = &QueryInterface;
            vtable->AddRef_2 = &AddRef;
            vtable->Release_3 = &Release;
            vtable->GetData_4 = &GetData;
            vtable->GetDataHere_5 = &GetDataHere;
            vtable->QueryGetData_6 = &QueryGetData;
            vtable->GetCanonicalFormatEtc_7 = &GetCanonicalFormatEtc;
            vtable->SetData_8 = &SetData;
            vtable->EnumFormatEtc_9 = &EnumFormatEtc;
            vtable->DAdvise_10 = &DAdvise;
            vtable->DUnadvise_11 = &DUnadvise;
            vtable->EnumDAdvise_12 = &EnumDAdvise;
            return vtable;
        }

        /// <summary>
        ///  Creates a manual COM Callable Wrapper for the given <paramref name="object"/>.
        /// </summary>
        public static IDataObject* Create(DataObjectProxy @object) =>
            (IDataObject*)Lifetime.Allocate(@object, s_vtable);

        [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
        private static HRESULT QueryInterface(IDataObject* @this, Guid* iid, void** ppObject)
        {
            if (iid is null || ppObject is null)
            {
                return HRESULT.E_POINTER;
            }

            if (iid->Equals(IDataObject.IID_Guid) || iid->Equals(IUnknown.IID_Guid))
            {
                *ppObject = @this;
                Lifetime.AddRef(@this);
                return HRESULT.S_OK;
            }

            *ppObject = null;
            DataObjectProxy? proxy = Lifetime.GetObject(@this);
            if (proxy is null)
            {
                return HRESULT.E_NOINTERFACE;
            }

            // Unwrap our "proxy" object by calling the the original object. This should roughly match the
            // OLE proxy behavior which returns it's own pointer for the IID_IUnknown and IID_IDataObject interfaces.
            return proxy._original->QueryInterface(iid, ppObject);
        }

        [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
        private static uint AddRef(IDataObject* @this) => Lifetime.AddRef(@this);

        [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
        private static uint Release(IDataObject* @this) => Lifetime.Release(@this);

        [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
        private static HRESULT GetData(IDataObject* @this, FORMATETC* pFormatetc, STGMEDIUM* pMedium) =>
            Lifetime.GetObject(@this)?.GetData(pFormatetc, pMedium) ?? HRESULT.COR_E_OBJECTDISPOSED;

        [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
        private static HRESULT GetDataHere(IDataObject* @this, FORMATETC* pFormatetc, STGMEDIUM* pMedium) =>
            Lifetime.GetObject(@this)?.GetDataHere(pFormatetc, pMedium) ?? HRESULT.COR_E_OBJECTDISPOSED;

        [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
        private static HRESULT QueryGetData(IDataObject* @this, FORMATETC* pFormatetc) =>
            Lifetime.GetObject(@this)?.QueryGetData(pFormatetc) ?? HRESULT.COR_E_OBJECTDISPOSED;

        [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
        private static HRESULT GetCanonicalFormatEtc(IDataObject* @this, FORMATETC* pFormatetcIn, FORMATETC* pFormatetcOut) =>
            Lifetime.GetObject(@this)?.GetCanonicalFormatEtc(pFormatetcIn, pFormatetcOut) ?? HRESULT.COR_E_OBJECTDISPOSED;

        [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
        private static HRESULT SetData(IDataObject* @this, FORMATETC* pFormatetc, STGMEDIUM* pMedium, BOOL fRelease) =>
            Lifetime.GetObject(@this)?.SetData(pFormatetc, pMedium, fRelease) ?? HRESULT.COR_E_OBJECTDISPOSED;

        [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
        private static HRESULT EnumFormatEtc(IDataObject* @this, uint dwDirection, IEnumFORMATETC** ppEnumFormatEtc) =>
            Lifetime.GetObject(@this)?.EnumFormatEtc(dwDirection, ppEnumFormatEtc) ?? HRESULT.COR_E_OBJECTDISPOSED;

        [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
        private static HRESULT DAdvise(IDataObject* @this, FORMATETC* pFormatetc, uint advf, IAdviseSink* pAdvSink, uint* pdwConnection) =>
            Lifetime.GetObject(@this)?.DAdvise(pFormatetc, advf, pAdvSink, pdwConnection) ?? HRESULT.COR_E_OBJECTDISPOSED;

        [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
        private static HRESULT DUnadvise(IDataObject* @this, uint dwConnection) =>
            Lifetime.GetObject(@this)?.DUnadvise(dwConnection) ?? HRESULT.COR_E_OBJECTDISPOSED;

        [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
        private static HRESULT EnumDAdvise(IDataObject* @this, IEnumSTATDATA** ppEnumAdvise) =>
            Lifetime.GetObject(@this)?.EnumDAdvise(ppEnumAdvise) ?? HRESULT.COR_E_OBJECTDISPOSED;
    }
#endif
}
