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

using System.Buffers;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Drawing;
using System.Formats.Nrbf;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using Windows.Win32.System.Com;
using Windows.Win32.System.Com.StructuredStorage;
using Windows.Win32.System.Ole;
using Windows.Win32.System.Variant;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using System.Windows.Forms.Nrbf;
using RECTL = Windows.Win32.Foundation.RECTL;
using System.Windows.Forms.BinaryFormat;

namespace System.Windows.Forms;

public partial class Control
{
    // Helper methods for retrieving ActiveX properties.
    // We abstract these through another method so we do not force JIT the ActiveX codebase.

    private Color ActiveXAmbientBackColor
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        get => ActiveXInstance.AmbientBackColor;
    }

    private Color ActiveXAmbientForeColor
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        get => ActiveXInstance.AmbientForeColor;
    }

    private Font? ActiveXAmbientFont
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        get => ActiveXInstance.AmbientFont;
    }

    private bool ActiveXEventsFrozen
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        get => ActiveXInstance.EventsFrozen;
    }

    private IntPtr ActiveXHWNDParent
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        get => ActiveXInstance.HWNDParent;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private Region ActiveXMergeRegion(Region region) => ActiveXInstance.MergeRegion(region);

    [MethodImpl(MethodImplOptions.NoInlining)]
    private void ActiveXOnFocus(bool focus) => ActiveXInstance.OnFocus(focus);

    [MethodImpl(MethodImplOptions.NoInlining)]
    private void ActiveXViewChanged() => ActiveXInstance.ViewChangedInternal();

    [MethodImpl(MethodImplOptions.NoInlining)]
    private void ActiveXUpdateBounds(ref int x, ref int y, ref int width, ref int height, SET_WINDOW_POS_FLAGS flags)
        => ActiveXInstance.UpdateBounds(ref x, ref y, ref width, ref height, flags);

    /// <summary>
    ///  Retrieves the ActiveX control implementation for this control.
    ///  This will demand create the implementation if it does not already exist.
    /// </summary>
    private ActiveXImpl ActiveXInstance
    {
        get
        {
            ActiveXImpl? activeXImpl = (ActiveXImpl?)Properties.GetObject(s_activeXImplProperty);
            if (activeXImpl is null)
            {
                // Don't allow top level objects to be hosted as activeX controls.
                if (GetState(States.TopLevel))
                {
                    throw new NotSupportedException(SR.AXTopLevelSource);
                }

                activeXImpl = new ActiveXImpl(this);

                // PERF: IsActiveX is called quite a bit - checked everywhere from sizing to event raising. Using a
                // state bit to track instead of fetching from the property store.
                SetExtendedState(ExtendedStates.IsActiveX, true);
                Properties.SetObject(s_activeXImplProperty, activeXImpl);
            }

            return activeXImpl;
        }
    }

    /// <summary>
    ///  This class holds all of the state data for an ActiveX control and supplies the implementation for many of
    ///  the non-trivial methods.
    /// </summary>
    private sealed unsafe partial class ActiveXImpl : MarshalByRefObject, IWindowTarget, IDisposable
    {
        private const int HiMetricPerInch = 2540;
        private static readonly int s_viewAdviseOnlyOnce = BitVector32.CreateMask();
        private static readonly int s_viewAdvisePrimeFirst = BitVector32.CreateMask(s_viewAdviseOnlyOnce);
        private static readonly int s_eventsFrozen = BitVector32.CreateMask(s_viewAdvisePrimeFirst);
        private static readonly int s_changingExtents = BitVector32.CreateMask(s_eventsFrozen);
        private static readonly int s_saving = BitVector32.CreateMask(s_changingExtents);
        private static readonly int s_isDirty = BitVector32.CreateMask(s_saving);
        private static readonly int s_inPlaceActive = BitVector32.CreateMask(s_isDirty);
        private static readonly int s_inPlaceVisible = BitVector32.CreateMask(s_inPlaceActive);
        private static readonly int s_uiActive = BitVector32.CreateMask(s_inPlaceVisible);
        private static readonly int s_uiDead = BitVector32.CreateMask(s_uiActive);
        private static readonly int s_adjustingRect = BitVector32.CreateMask(s_uiDead);
        private static readonly SearchValues<char> s_whitespace = SearchValues.Create(" \r\n");

        private static Point s_logPixels = Point.Empty;
        private static OLEVERB[]? s_axVerbs;

        private readonly Control _control;
        private readonly IWindowTarget _controlWindowTarget;
        private Rectangle? _lastClipRect;

        private AgileComPointer<IOleClientSite>? _clientSite;
        private AgileComPointer<IOleInPlaceUIWindow>? _inPlaceUiWindow;
        private AgileComPointer<IOleInPlaceFrame>? _inPlaceFrame;
        private AgileComPointer<IOleAdviseHolder>? _adviseHolder;
        private IAdviseSink* _viewAdviseSink;
        private BitVector32 _activeXState;
        private readonly AmbientProperty[] _ambientProperties;
        private HACCEL _accelTable;
        private short _accelCount = -1;
        private RECT* _adjustRect; // temporary rect used during OnPosRectChange && SetObjectRects

        // Feature switch, when set to false, ActiveX is not supported in trimmed applications.
        [FeatureSwitchDefinition("System.Windows.Forms.ActiveXImpl.IsSupported")]
#pragma warning disable IDE0075 // Simplify conditional expression - the simpler expression is hard to read
        private static bool IsSupported { get; } =
            AppContext.TryGetSwitch("System.Windows.Forms.ActiveXImpl.IsSupported", out bool isSupported)
                ? isSupported
                : true;
#pragma warning restore IDE0075

        /// <summary>
        ///  Creates a new ActiveXImpl.
        /// </summary>
        internal ActiveXImpl(Control control)
        {
            _control = control;

            // We replace the control's window target with our own.  We
            // do this so we can handle the UI Dead ambient property.
            _controlWindowTarget = control.WindowTarget;
            control.WindowTarget = this;

            _activeXState = default;
            _ambientProperties =
            [
                new("Font", PInvokeCore.DISPID_AMBIENT_FONT),
                new("BackColor", PInvokeCore.DISPID_AMBIENT_BACKCOLOR),
                new("ForeColor", PInvokeCore.DISPID_AMBIENT_FORECOLOR)
            ];
        }

        /// <summary>
        ///  Retrieves the ambient back color for the control.
        /// </summary>
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        internal Color AmbientBackColor
        {
            get
            {
                AmbientProperty property = LookupAmbient(PInvokeCore.DISPID_AMBIENT_BACKCOLOR);

                if (property.Empty)
                {
                    using VARIANT value = GetAmbientProperty(PInvokeCore.DISPID_AMBIENT_BACKCOLOR);
                    if (value.vt is VARENUM.VT_I4 or VARENUM.VT_INT)
                    {
                        property.Value = ColorTranslator.FromOle(value.data.intVal);
                    }
                }

                return property.Value is null ? Color.Empty : (Color)property.Value;
            }
        }

        /// <summary>
        ///  Retrieves the ambient font for the control.
        /// </summary>
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        internal Font? AmbientFont
        {
            get
            {
                AmbientProperty property = LookupAmbient(PInvokeCore.DISPID_AMBIENT_FONT);

                if (property.Empty)
                {
                    using VARIANT value = GetAmbientProperty(PInvokeCore.DISPID_AMBIENT_FONT);
                    if (value.vt == VARENUM.VT_UNKNOWN)
                    {
                        using var font = ComScope<IFont>.TryQueryFrom(value.data.punkVal, out HRESULT hr);
                        if (hr.Succeeded)
                        {
                            try
                            {
                                property.Value = Font.FromHfont(font.Value->hFont);
                            }
                            catch (ArgumentException)
                            {
                                // Not a TrueType font.
                            }
                        }
                    }
                }

                return (Font?)property.Value;
            }
        }

        /// <summary>
        ///  Retrieves the ambient back color for the control.
        /// </summary>
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        internal Color AmbientForeColor
        {
            get
            {
                AmbientProperty property = LookupAmbient(PInvokeCore.DISPID_AMBIENT_FORECOLOR);

                if (property.Empty)
                {
                    using VARIANT value = GetAmbientProperty(PInvokeCore.DISPID_AMBIENT_FORECOLOR);
                    if (value.vt is VARENUM.VT_I4 or VARENUM.VT_INT)
                    {
                        property.Value = ColorTranslator.FromOle(value.data.intVal);
                    }
                }

                return property.Value is null ? Color.Empty : (Color)property.Value;
            }
        }

        /// <summary>
        ///  Determines if events should be frozen.
        /// </summary>
        [Browsable(false)]
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        internal bool EventsFrozen
        {
            get => _activeXState[s_eventsFrozen];
            set => _activeXState[s_eventsFrozen] = value;
        }

        /// <summary>
        ///  Provides access to the parent window handle when we are UI active.
        /// </summary>
        internal HWND HWNDParent { get; private set; }

        /// <summary>
        ///  Retrieves the number of logical pixels per inch on the primary monitor.
        /// </summary>
        private static Point LogPixels
        {
            get
            {
                if (s_logPixels.IsEmpty)
                {
                    s_logPixels = default;
                    using var dc = GetDcScope.ScreenDC;
                    s_logPixels.X = PInvokeCore.GetDeviceCaps(dc, GET_DEVICE_CAPS_INDEX.LOGPIXELSX);
                    s_logPixels.Y = PInvokeCore.GetDeviceCaps(dc, GET_DEVICE_CAPS_INDEX.LOGPIXELSY);
                }

                return s_logPixels;
            }
        }

        /// <inheritdoc cref="IOleObject.Advise(IAdviseSink*, uint*)"/>
        internal HRESULT Advise(IAdviseSink* pAdvSink, uint* token)
        {
            if (_adviseHolder is null)
            {
                IOleAdviseHolder* holder = null;
                HRESULT hr = PInvoke.CreateOleAdviseHolder(&holder);
                if (hr.Failed)
                {
                    return hr;
                }

                _adviseHolder = new(holder, takeOwnership: true);
            }

            using var adviseHolder = _adviseHolder.GetInterface();
            return adviseHolder.Value->Advise(pAdvSink, token);
        }

        /// <inheritdoc cref="IOleObject.Close(uint)"/>
        internal void Close(OLECLOSE dwSaveOption)
        {
            if (_activeXState[s_inPlaceActive])
            {
                InPlaceDeactivate();
            }

            if ((dwSaveOption == OLECLOSE.OLECLOSE_SAVEIFDIRTY || dwSaveOption == OLECLOSE.OLECLOSE_PROMPTSAVE)
                && _activeXState[s_isDirty])
            {
                if (_clientSite is not null)
                {
                    using var clientSite = _clientSite.GetInterface();
                    clientSite.Value->SaveObject();
                }

                SendOnSave();
            }
        }

        /// <inheritdoc cref="IOleObject.DoVerb(int, MSG*, IOleClientSite*, int, HWND, RECT*)"/>
        internal unsafe HRESULT DoVerb(
            OLEIVERB iVerb,
            MSG* lpmsg,
            IOleClientSite* pActiveSite,
            int lindex,
            HWND hwndParent,
            RECT* lprcPosRect)
        {
            switch (iVerb)
            {
                case OLEIVERB.OLEIVERB_SHOW:
                case OLEIVERB.OLEIVERB_INPLACEACTIVATE:
                case OLEIVERB.OLEIVERB_UIACTIVATE:
                case OLEIVERB.OLEIVERB_PRIMARY:
                    InPlaceActivate(iVerb);

                    // Now that we're active, send the lpmsg to the control if it is valid.
                    if (lpmsg is null)
                    {
                        break;
                    }

                    Control target = _control;

                    HWND hwnd = lpmsg->hwnd;
                    if (hwnd != _control.HWND && lpmsg->IsMouseMessage())
                    {
                        // Must translate message coordinates over to our HWND.
                        HWND hwndMap = hwnd.IsNull ? hwndParent : hwnd;
                        Point pt = new(PARAM.LOWORD(lpmsg->lParam), PARAM.HIWORD(lpmsg->lParam));

                        PInvoke.MapWindowPoints(hwndMap, _control, ref pt);

                        // Check to see if this message should really go to a child control, and if so, map the
                        // point into that child's window coordinates.
                        Control? realTarget = target.GetChildAtPoint(pt);
                        if (realTarget is not null && realTarget != target)
                        {
                            pt = WindowsFormsUtils.TranslatePoint(pt, target, realTarget);
                            target = realTarget;
                        }

                        lpmsg->lParam = PARAM.FromPoint(pt);
                    }

                    if (lpmsg->message == PInvoke.WM_KEYDOWN && lpmsg->wParam == (WPARAM)(nuint)VIRTUAL_KEY.VK_TAB)
                    {
                        target.SelectNextControl(null, ModifierKeys != Keys.Shift, tabStopOnly: true, nested: true, wrap: true);
                    }
                    else
                    {
                        PInvoke.SendMessage(target, lpmsg->message, lpmsg->wParam, lpmsg->lParam);
                    }

                    break;

                case OLEIVERB.OLEIVERB_HIDE:
                    UIDeactivate();
                    InPlaceDeactivate();
                    if (_activeXState[s_inPlaceVisible])
                    {
                        SetInPlaceVisible(false);
                    }

                    break;

                // All other verbs are not implemented.
                default:
                    return HRESULT.E_NOTIMPL;
            }

            return HRESULT.S_OK;
        }

        /// <inheritdoc cref="IViewObject.Interface.Draw(DVASPECT, int, void*, DVTARGETDEVICE*, HDC, HDC, RECTL*, RECTL*, nint, nuint)"/>
        internal HRESULT Draw(
            DVASPECT dwDrawAspect,
            int lindex,
            void* pvAspect,
            DVTARGETDEVICE* ptd,
            HDC hdcTargetDev,
            HDC hdcDraw,
            RECT* prcBounds,
            RECT* lprcWBounds,
            nint pfnContinue,
            nuint dwContinue)
        {
            // Support the aspects required for multi-pass drawing.
            switch (dwDrawAspect)
            {
                case DVASPECT.DVASPECT_CONTENT:
                case DVASPECT.DVASPECT_OPAQUE:
                case DVASPECT.DVASPECT_TRANSPARENT:
                    break;
                default:
                    return HRESULT.DV_E_DVASPECT;
            }

            // We can paint to an enhanced metafile, but not all GDI / GDI+ is
            // supported on classic metafiles.  We throw VIEW_E_DRAW in the hope that
            // the caller figures it out and sends us a different DC.

            OBJ_TYPE hdcType = (OBJ_TYPE)PInvokeCore.GetObjectType(hdcDraw);
            if (hdcType == OBJ_TYPE.OBJ_METADC)
            {
                return HRESULT.VIEW_E_DRAW;
            }

            Point pVp = default;
            Point pW = default;
            Size sWindowExt = default;
            Size sViewportExt = default;
            HDC_MAP_MODE iMode = HDC_MAP_MODE.MM_TEXT;

            if (!_control.IsHandleCreated)
            {
                _control.CreateHandle();
            }

            // If they didn't give us a rectangle, just copy over ours.
            if (prcBounds is not null)
            {
                RECT rc = *prcBounds;

                // To draw to a given rect, we scale the DC in such a way as to make the values it takes match our
                // own happy MM_TEXT. Then, we back-convert prcBounds so that we convert it to this coordinate
                // system. This puts us in the most similar coordinates as we currently use.
                Point p1 = new(rc.left, rc.top);
                Point p2 = new(rc.right - rc.left, rc.bottom - rc.top);
                PInvoke.LPtoDP(hdcDraw, [p1, p2]);

                iMode = (HDC_MAP_MODE)PInvoke.SetMapMode(hdcDraw, HDC_MAP_MODE.MM_ANISOTROPIC);
                PInvoke.SetWindowOrgEx(hdcDraw, 0, 0, &pW);
                PInvoke.SetWindowExtEx(hdcDraw, _control.Width, _control.Height, (SIZE*)&sWindowExt);
                PInvoke.SetViewportOrgEx(hdcDraw, p1.X, p1.Y, &pVp);
                PInvoke.SetViewportExtEx(hdcDraw, p2.X, p2.Y, (SIZE*)&sViewportExt);
            }

            // Now do the actual drawing.  We must ask all of our children to draw as well.
            try
            {
                nint flags = PInvoke.PRF_CHILDREN | PInvoke.PRF_CLIENT | PInvoke.PRF_ERASEBKGND | PInvoke.PRF_NONCLIENT;
                if (hdcType != OBJ_TYPE.OBJ_ENHMETADC)
                {
                    PInvoke.SendMessage(_control, PInvoke.WM_PRINT, (WPARAM)hdcDraw, (LPARAM)flags);
                }
                else
                {
                    _control.PrintToMetaFile(hdcDraw, flags);
                }
            }
            finally
            {
                // And clean up the DC
                if (prcBounds is not null)
                {
                    PInvoke.SetWindowOrgEx(hdcDraw, pW.X, pW.Y, lppt: null);
                    PInvoke.SetWindowExtEx(hdcDraw, sWindowExt.Width, sWindowExt.Height, lpsz: null);
                    PInvoke.SetViewportOrgEx(hdcDraw, pVp.X, pVp.Y, lppt: null);
                    PInvoke.SetViewportExtEx(hdcDraw, sViewportExt.Width, sViewportExt.Height, lpsz: null);
                    PInvoke.SetMapMode(hdcDraw, iMode);
                }
            }

            return HRESULT.S_OK;
        }

        /// <inheritdoc cref="IOleObject.EnumVerbs(IEnumOLEVERB**)"/>
        internal static IEnumOLEVERB* EnumVerbs()
        {
            if (s_axVerbs is null)
            {
                OLEVERB verbShow = new()
                {
                    lVerb = OLEIVERB.OLEIVERB_SHOW
                };

                s_axVerbs =
                [
                    new() { lVerb = OLEIVERB.OLEIVERB_SHOW },
                    new() { lVerb = OLEIVERB.OLEIVERB_INPLACEACTIVATE },
                    new() { lVerb = OLEIVERB.OLEIVERB_UIACTIVATE },
                    new() { lVerb = OLEIVERB.OLEIVERB_HIDE },
                    new() { lVerb = OLEIVERB.OLEIVERB_PRIMARY },
                ];
            }

            return ComHelpers.GetComPointer<IEnumOLEVERB>(new ActiveXVerbEnum(s_axVerbs));
        }

        /// <summary>
        ///  Converts the given string to a byte array.
        /// </summary>
        private static byte[] FromBase64WrappedString(string text)
        {
            if (text.AsSpan().ContainsAny(s_whitespace))
            {
                StringBuilder sb = new(text.Length);
                for (int i = 0; i < text.Length; i++)
                {
                    switch (text[i])
                    {
                        case ' ':
                        case '\r':
                        case '\n':
                            break;
                        default:
                            sb.Append(text[i]);
                            break;
                    }
                }

                return Convert.FromBase64String(sb.ToString());
            }
            else
            {
                return Convert.FromBase64String(text);
            }
        }

        /// <inheritdoc cref="IViewObject.GetAdvise(uint*, uint*, IAdviseSink**)"/>
        internal unsafe HRESULT GetAdvise(DVASPECT* pAspects, ADVF* pAdvf, IAdviseSink** ppAdvSink)
        {
            if (pAspects is not null)
            {
                *pAspects = DVASPECT.DVASPECT_CONTENT;
            }

            if (pAdvf is not null)
            {
                *pAdvf = 0;

                if (_activeXState[s_viewAdviseOnlyOnce])
                {
                    *pAdvf |= ADVF.ADVF_ONLYONCE;
                }

                if (_activeXState[s_viewAdvisePrimeFirst])
                {
                    *pAdvf |= ADVF.ADVF_PRIMEFIRST;
                }
            }

            if (ppAdvSink is not null)
            {
                *ppAdvSink = _viewAdviseSink;

                if (_viewAdviseSink is not null)
                {
                    _viewAdviseSink->AddRef();
                }
            }

            return HRESULT.S_OK;
        }

        /// <summary>
        ///  Helper function to retrieve an ambient property. Returns empty if not found.
        /// </summary>
        private VARIANT GetAmbientProperty(int dispid)
        {
            VARIANT property = default;

            if (_clientSite is null)
            {
                return property;
            }

            using var dispatch = _clientSite.TryGetInterface<IDispatch>(out HRESULT hr);
            if (hr.Failed)
            {
                return property;
            }

            hr = dispatch.Value->TryGetProperty(dispid, &property, PInvoke.LCID.USER_DEFAULT.RawValue);

            if (hr.Failed)
            {
                Debug.WriteLine($"Failed to get property for dispid {dispid}: {hr}");
            }

            return property;
        }

        /// <inheritdoc cref="IOleObject.GetClientSite(IOleClientSite**)"/>
        internal ComScope<IOleClientSite> GetClientSite() => _clientSite is null ? default : _clientSite.GetInterface();

        /// <inheritdoc cref="IOleControl.GetControlInfo(CONTROLINFO*)"/>
        internal unsafe HRESULT GetControlInfo(CONTROLINFO* pControlInfo)
        {
            if (_accelCount == -1)
            {
                List<char> mnemonicList = [];
                GetMnemonicList(_control, mnemonicList);

                _accelCount = (short)mnemonicList.Count;

                if (_accelCount > 0)
                {
                    // In the worst case we may have two accelerators per mnemonic: one lower case and
                    // one upper case, hence the * 2 below.
                    var accelerators = new ACCEL[_accelCount * 2];
                    ushort cmd = 0;
                    _accelCount = 0;

                    foreach (char ch in mnemonicList)
                    {
                        short scan = PInvoke.VkKeyScan(ch);
                        ushort key = (ushort)(scan & 0x00FF);
                        if (ch is >= 'A' and <= 'Z')
                        {
                            // Lowercase letter.
                            accelerators[_accelCount++] = new ACCEL
                            {
                                fVirt = ACCEL_VIRT_FLAGS.FALT | ACCEL_VIRT_FLAGS.FVIRTKEY,
                                key = key,
                                cmd = cmd
                            };

                            // Uppercase letter.
                            accelerators[_accelCount++] = new ACCEL
                            {
                                fVirt = ACCEL_VIRT_FLAGS.FALT | ACCEL_VIRT_FLAGS.FVIRTKEY | ACCEL_VIRT_FLAGS.FSHIFT,
                                key = key,
                                cmd = cmd
                            };
                        }
                        else
                        {
                            // Some non-printable character.
                            ACCEL_VIRT_FLAGS virt = ACCEL_VIRT_FLAGS.FALT | ACCEL_VIRT_FLAGS.FVIRTKEY;
                            if ((scan & 0x0100) != 0)
                            {
                                virt |= ACCEL_VIRT_FLAGS.FSHIFT;
                            }

                            accelerators[_accelCount++] = new ACCEL
                            {
                                fVirt = virt,
                                key = key,
                                cmd = cmd
                            };
                        }

                        cmd++;
                    }

                    // Now create an accelerator table and then free our memory.

                    if (!_accelTable.IsNull)
                    {
                        PInvoke.DestroyAcceleratorTable(new HandleRef<HACCEL>(_control, _accelTable));
                        _accelTable = HACCEL.Null;
                    }

                    fixed (ACCEL* pAccelerators = accelerators)
                    {
                        _accelTable = PInvoke.CreateAcceleratorTable(pAccelerators, _accelCount);
                    }
                }
            }

            pControlInfo->cAccel = (ushort)_accelCount;
            pControlInfo->hAccel = _accelTable;
            return HRESULT.S_OK;
        }

        /// <inheritdoc cref="IOleObject.GetExtent(DVASPECT, SIZE*)"/>
        internal unsafe void GetExtent(DVASPECT dwDrawAspect, Size* pSizel)
        {
            if (!dwDrawAspect.HasFlag(DVASPECT.DVASPECT_CONTENT))
            {
                ThrowHr(HRESULT.DV_E_DVASPECT);
            }

            Size size = _control.Size;
            Point pt = PixelToHiMetric(size.Width, size.Height);
            pSizel->Width = pt.X;
            pSizel->Height = pt.Y;
        }

        /// <summary>
        ///  Searches the control hierarchy of the given control and adds
        ///  the mnemonics for each control to mnemonicList.  Each mnemonic
        ///  is added as a char to the list.
        /// </summary>
        private static void GetMnemonicList(Control control, List<char> mnemonicList)
        {
            // Get the mnemonic for our control
            char mnemonic = WindowsFormsUtils.GetMnemonic(control.Text, true);
            if (mnemonic != 0)
            {
                mnemonicList.Add(mnemonic);
            }

            // And recurse for our children.
            foreach (Control c in control.Controls)
            {
                if (c is not null)
                {
                    GetMnemonicList(c, mnemonicList);
                }
            }
        }

        /// <summary>
        ///  Name to use for a stream: use the control's type name (max 31 chars, use the end chars
        ///  if it's longer than that)
        /// </summary>
        private string GetStreamName()
        {
            string streamName = _control.GetType().FullName!;
            int len = streamName.Length;
            if (len > 31)
            {
                // The max allowed length of the stream name is 31.
                streamName = streamName[(len - 31)..];
            }

            return streamName;
        }

        /// <inheritdoc cref="IOleWindow.GetWindow(HWND*)"/>
        internal unsafe HRESULT GetWindow(HWND* phwnd)
        {
            if (phwnd is null)
            {
                return HRESULT.E_POINTER;
            }

            if (!_activeXState[s_inPlaceActive])
            {
                *phwnd = HWND.Null;
                return HRESULT.E_FAIL;
            }

            *phwnd = (HWND)_control.Handle;
            return HRESULT.S_OK;
        }

        /// <summary>
        ///  Converts coordinates in HiMetric to pixels.  Used for ActiveX sourcing.
        /// </summary>
        private static Point HiMetricToPixel(int x, int y)
        {
            Point pt = new Point
            {
                X = (LogPixels.X * x + HiMetricPerInch / 2) / HiMetricPerInch,
                Y = (LogPixels.Y * y + HiMetricPerInch / 2) / HiMetricPerInch
            };
            return pt;
        }

        /// <summary>
        ///  In place activates this object.
        /// </summary>
        internal unsafe void InPlaceActivate(OLEIVERB verb)
        {
            // If we don't have a client site, then there's not much to do.
            // We also punt if this isn't an in-place site, since we can't go active then.

            if (_clientSite is null)
            {
                return;
            }

            using var inPlaceSite = _clientSite.TryGetInterface<IOleInPlaceSite>(out HRESULT hr);
            if (hr.Failed)
            {
                return;
            }

            // If we're not already active, go and do it.
            if (!_activeXState[s_inPlaceActive])
            {
                hr = inPlaceSite.Value->CanInPlaceActivate();
                if (hr != HRESULT.S_OK)
                {
                    if (hr.Succeeded)
                    {
                        hr = HRESULT.E_FAIL;
                    }

                    ThrowHr(hr);
                }

                inPlaceSite.Value->OnInPlaceActivate();

                _activeXState[s_inPlaceActive] = true;
            }

            // And if we're not visible, do that too.
            if (!_activeXState[s_inPlaceVisible])
            {
                OLEINPLACEFRAMEINFO inPlaceFrameInfo = new()
                {
                    cb = (uint)sizeof(OLEINPLACEFRAMEINFO)
                };

                // We are entering a secure context here.
                HWND hwndParent = default;
                hr = inPlaceSite.Value->GetWindow(&hwndParent);
                if (!hr.Succeeded)
                {
                    ThrowHr(hr);
                }

                RECT posRect = default;
                RECT clipRect = default;

                DisposeHelper.NullAndDispose(ref _inPlaceUiWindow);
                DisposeHelper.NullAndDispose(ref _inPlaceFrame);

                IOleInPlaceFrame* pFrame;
                IOleInPlaceUIWindow* pWindow;
                inPlaceSite.Value->GetWindowContext(
                    &pFrame,
                    &pWindow,
                    &posRect,
                    &clipRect,
                    &inPlaceFrameInfo).AssertSuccess();

                SetObjectRects(&posRect, &clipRect);

                _inPlaceFrame = pFrame is null ? null : new(pFrame, takeOwnership: true);
                _inPlaceUiWindow = pWindow is null ? null : new(pWindow, takeOwnership: true);

                // We are parenting ourselves directly to the host window. The host must implement the ambient property
                // DISPID_AMBIENT_MESSAGEREFLECT. If it doesn't, that means that the host won't reflect messages back
                // to us.
                HWNDParent = hwndParent;
                if (PInvoke.SetParent(_control, hwndParent).IsNull)
                {
                    throw new Win32Exception(Marshal.GetLastWin32Error(), SR.Win32SetParentFailed);
                }

                // Now create our handle if it hasn't already been done.
                _control.CreateControl();

                using var clientSite = _clientSite.GetInterface();
                clientSite.Value->ShowObject();

                SetInPlaceVisible(true);
                Debug.Assert(_activeXState[s_inPlaceVisible], "Failed to set inplacevisible");
            }

            // If we weren't asked to UIActivate, then we're done.
            if (verb is not OLEIVERB.OLEIVERB_PRIMARY and not OLEIVERB.OLEIVERB_UIACTIVATE)
            {
                return;
            }

            // If we're not already UI active, do sow now.
            if (_activeXState[s_uiActive])
            {
                return;
            }

            _activeXState[s_uiActive] = true;

            // Inform the container of our intent.
            inPlaceSite.Value->OnUIActivate();

            // Take the focus [which is what UI Activation is all about]
            if (!_control.ContainsFocus)
            {
                _control.Focus();
            }

            // Set ourselves up in the host.
            Debug.Assert(_inPlaceFrame is not null, "Setting us to visible should have created the in place frame");

            using var activeObject = ComHelpers.GetComScope<IOleInPlaceActiveObject>(_control);

            using var inPlaceFrame = _inPlaceFrame.GetInterface();
            inPlaceFrame.Value->SetActiveObject(activeObject, (PCWSTR)null);
            using var inPlaceUiWindow = _inPlaceUiWindow is null ? default : _inPlaceUiWindow.GetInterface();
            if (_inPlaceUiWindow is not null)
            {
                inPlaceUiWindow.Value->SetActiveObject(activeObject, (PCWSTR)null);
            }

            // We have to explicitly say we don't wany any border space.
            hr = inPlaceFrame.Value->SetBorderSpace(null);
            if (hr.Failed
                && hr != HRESULT.OLE_E_INVALIDRECT
                && hr != HRESULT.INPLACE_E_NOTOOLSPACE
                && hr != HRESULT.E_NOTIMPL)
            {
                hr.ThrowOnFailure();
            }

            if (_inPlaceUiWindow is not null)
            {
                hr = inPlaceUiWindow.Value->SetBorderSpace(null);
                if (hr.Failed
                    && hr != HRESULT.OLE_E_INVALIDRECT
                    && hr != HRESULT.INPLACE_E_NOTOOLSPACE
                    && hr != HRESULT.E_NOTIMPL)
                {
                    hr.ThrowOnFailure();
                }
            }
        }

        /// <inheritdoc cref="IOleInPlaceObject.InPlaceDeactivate"/>
        internal HRESULT InPlaceDeactivate()
        {
            // Only do this if we're already in place active.
            if (!_activeXState[s_inPlaceActive])
            {
                return HRESULT.S_OK;
            }

            // Deactivate us if we're UI active
            if (_activeXState[s_uiActive])
            {
                UIDeactivate();
            }

            // Some containers may call into us to save, and if we're still active we will try to deactivate and
            // recurse back into the container. So, set the state bits here first.
            _activeXState[s_inPlaceActive] = false;
            _activeXState[s_inPlaceVisible] = false;

            // Notify our site of our deactivation.
            if (_clientSite is not null)
            {
                using var inPlaceSite = _clientSite.TryGetInterface<IOleInPlaceSite>(out HRESULT hr);
                if (hr.Succeeded)
                {
                    inPlaceSite.Value->OnInPlaceDeactivate();
                }
            }

            _control.Visible = false;
            HWNDParent = default;

            DisposeHelper.NullAndDispose(ref _inPlaceUiWindow);
            DisposeHelper.NullAndDispose(ref _inPlaceFrame);

            return HRESULT.S_OK;
        }

        /// <inheritdoc cref="IPersistStorage.IsDirty"/>
        internal HRESULT IsDirty() => _activeXState[s_isDirty] ? HRESULT.S_OK : HRESULT.S_FALSE;

        /// <summary>
        ///  Looks at the property to see if it should be loaded / saved as a resource or  through a type converter.
        /// </summary>
        private bool IsResourceProperty(PropertyDescriptor property)
        {
            TypeConverter converter = property.Converter;

            // If we have a converter round trip through string or byte[], it should be used
            if ((converter.CanConvertTo(typeof(string)) && converter.CanConvertFrom(typeof(string)))
                || (converter.CanConvertTo(typeof(byte[])) && converter.CanConvertFrom(typeof(byte))))
            {
                return false;
            }

            // Otherwise we require the type explicitly implements ISerializable. Strangely, in the past this only
            // worked off of the current value. If the current value was null checking it for ISerializable would always
            // fail. This means properties would never load into a reference type property if it's current value was null.
            //
            // While we could always just check the property type for serializable this would break derived class scenarios
            // where it adds ISerializable but the property type doesn't have it. In this scenario it would still not work
            // if the value is null on load. Not enabling that scenario for now as it would require more refactoring.
            return property.PropertyType.IsAssignableTo(typeof(ISerializable))
                || property.GetValue(_control) is ISerializable;
        }

        /// <inheritdoc cref="IPersistStorage.Load(IStorage*)"/>
        internal HRESULT Load(IStorage* stg)
        {
            using ComScope<IStream> stream = new(null);
            HRESULT hr = stg->OpenStream(
                GetStreamName(),
                STGM.STGM_READ | STGM.STGM_SHARE_EXCLUSIVE,
                0,
                stream);

            if (hr == HRESULT.STG_E_FILENOTFOUND)
            {
                // For backward compatibility: We were earlier using GetType().FullName
                // as the stream name in v1. Lets see if a stream by that name exists.
                hr = stg->OpenStream(
                    GetType().FullName!,
                    STGM.STGM_READ | STGM.STGM_SHARE_EXCLUSIVE,
                    0,
                    stream);
            }

            if (hr.Succeeded)
            {
                Load(stream);
            }

            return hr;
        }

        /// <inheritdoc cref="IPersistStreamInit.Load(IStream*)"/>
        internal void Load(IStream* stream)
        {
            // We do everything through property bags because we support full fidelity
            // in them. So, load through that method.
            PropertyBagStream bagStream = new();
            bagStream.Read(stream);
            using var propertyBag = ComHelpers.GetComScope<IPropertyBag>(bagStream);
            Load(propertyBag, errorLog: null);
        }

        /// <inheritdoc cref="IPersistPropertyBag.Load(IPropertyBag*, IErrorLog*)"/>
        internal unsafe void Load(IPropertyBag* propertyBag, IErrorLog* errorLog)
        {
            if (!IsSupported)
            {
                throw new NotSupportedException(string.Format(SR.ControlNotSupportedInTrimming, nameof(ActiveXImpl)));
            }

            PropertyDescriptorCollection props = TypeDescriptor.GetProperties(
                _control,
                [DesignerSerializationVisibilityAttribute.Visible]);

            for (int i = 0; i < props.Count; i++)
            {
                PropertyDescriptor currentProperty = props[i];

                try
                {
                    object? value = null;
                    using (VARIANT variant = default)
                    {
                        fixed (char* n = currentProperty.Name)
                        {
                            if (propertyBag->Read(n, &variant, errorLog).Failed)
                            {
                                continue;
                            }
                        }

                        value = variant.ToObject();
                    }

                    if (value is null)
                    {
                        continue;
                    }

                    string? errorString = null;
                    HRESULT errorCode = HRESULT.S_OK;

                    try
                    {
                        if (!SetValue(currentProperty, value))
                        {
                            continue;
                        }
                    }
                    catch (Exception e)
                    {
                        errorString = e.ToString();
                        errorCode = e is ExternalException ee ? (HRESULT)ee.ErrorCode : HRESULT.E_FAIL;
                    }

                    if (errorString is not null && errorLog is not null)
                    {
                        using BSTR bstrSource = new(_control.GetType().FullName!);
                        using BSTR bstrDescription = new(errorString);
                        EXCEPINFO err = new()
                        {
                            bstrSource = bstrSource,
                            bstrDescription = bstrDescription,
                            scode = errorCode
                        };

                        errorLog->AddError(currentProperty.Name, in err);
                    }
                }
                catch (Exception ex)
                {
                    Debug.Fail("Unexpected failure reading property", ex.ToString());

                    if (ex.IsCriticalException())
                    {
                        throw;
                    }
                }
            }

            bool SetValue(PropertyDescriptor currentProperty, object data)
            {
                string? value = data as string ?? Convert.ToString(data, CultureInfo.InvariantCulture);

                if (value is null)
                {
                    Debug.Fail($"Couldn't convert {currentProperty.Name} to string.");
                    return false;
                }

                if (IsResourceProperty(currentProperty))
                {
                    // Resource property. We encode these as base 64 strings. To load them, we convert
                    // to a binary blob and then de-serialize.
                    using MemoryStream stream = new(Convert.FromBase64String(value), writable: false);
                    bool success = false;
                    object? deserialized = null;
                    try
                    {
                        SerializationRecord rootRecord = stream.Decode();
                        success = rootRecord.TryGetObject(out deserialized);
                    }
                    catch (Exception ex) when (!ex.IsCriticalException())
                    {
                    }

#pragma warning disable SYSLIB0011 // Type or member is obsolete
                    if (!success)
                    {
                        if (!DataObject.Composition.EnableUnsafeBinaryFormatterInNativeObjectSerialization)
                        {
                            throw new NotSupportedException(SR.BinaryFormatterNotSupported);
                        }

                        stream.Position = 0;
                        deserialized = new BinaryFormatter().Deserialize(stream); // CodeQL[SM03722, SM04191] : BinaryFormatter is intended to be used as a fallback for unsupported types. Users must explicitly opt into this behavior
                    }
#pragma warning restore

                    currentProperty.SetValue(_control, deserialized);
                    return true;
                }

                // Not a resource property.  Use TypeConverters to convert the string back to the data type.  We do
                // not check for CanConvertFrom here -- we the conversion fails the type converter will throw,
                // and we will log it into the COM error log.
                TypeConverter converter = currentProperty.Converter;
                Debug.Assert(
                    converter is not null,
                    $"No type converter for property '{currentProperty.Name}' on class {_control.GetType().FullName}");

                // Check to see if the type converter can convert from a string.  If it can,.
                // use that as it is the best format for IPropertyBag.  Otherwise, check to see
                // if it can convert from a byte array.  If it can, get the string, decode it
                // to a byte array, and then set the value.
                object? newValue = null;

                if (converter.CanConvertFrom(typeof(string)))
                {
                    newValue = converter.ConvertFromInvariantString(value);
                }
                else if (converter.CanConvertFrom(typeof(byte[])))
                {
                    newValue = converter.ConvertFrom(null, CultureInfo.InvariantCulture, FromBase64WrappedString(value));
                }

                currentProperty.SetValue(_control, newValue);
                return true;
            }
        }

        /// <summary>
        ///  Simple lookup to find the AmbientProperty corresponding to the given
        ///  dispid.
        /// </summary>
        private AmbientProperty LookupAmbient(int dispid)
        {
            for (int i = 0; i < _ambientProperties.Length; i++)
            {
                if (_ambientProperties[i].DispID == dispid)
                {
                    return _ambientProperties[i];
                }
            }

            Debug.Fail($"No ambient property for dispid {dispid}");
            return _ambientProperties[0];
        }

        /// <summary>
        ///  Merges the input region with the current clipping region, if any.
        /// </summary>
        internal Region MergeRegion(Region region)
        {
            if (_lastClipRect.HasValue)
            {
                region.Exclude(_lastClipRect.Value);
            }

            return region;
        }

        private static void CallParentPropertyChanged(Control control, string propName)
        {
            switch (propName)
            {
                case "BackColor":
                    control.OnParentBackColorChanged(EventArgs.Empty);
                    break;
                case "BackgroundImage":
                    control.OnParentBackgroundImageChanged(EventArgs.Empty);
                    break;
                case "BindingContext":
                    control.OnParentBindingContextChanged(EventArgs.Empty);
                    break;
                case "Enabled":
                    control.OnParentEnabledChanged(EventArgs.Empty);
                    break;
                case "Font":
                    control.OnParentFontChanged(EventArgs.Empty);
                    break;
                case "ForeColor":
                    control.OnParentForeColorChanged(EventArgs.Empty);
                    break;
                case "RightToLeft":
                    control.OnParentRightToLeftChanged(EventArgs.Empty);
                    break;
                case "Visible":
                    control.OnParentVisibleChanged(EventArgs.Empty);
                    break;
                default:
                    Debug.Fail($"There is no property change notification for: {propName} on Control.");
                    break;
            }
        }

        /// <inheritdoc cref="IOleControl.OnAmbientPropertyChange(int)"/>
        internal void OnAmbientPropertyChange(int dispID)
        {
            if (dispID == PInvokeCore.DISPID_UNKNOWN)
            {
                // Invalidate all properties. Ideally we should be checking each one, but
                // that's pretty expensive too.
                for (int i = 0; i < _ambientProperties.Length; i++)
                {
                    _ambientProperties[i].ResetValue();
                    CallParentPropertyChanged(_control, _ambientProperties[i].Name);
                }

                return;
            }

            // Look for a specific property that has changed.
            for (int i = 0; i < _ambientProperties.Length; i++)
            {
                if (_ambientProperties[i].DispID == dispID)
                {
                    _ambientProperties[i].ResetValue();
                    CallParentPropertyChanged(_control, _ambientProperties[i].Name);
                    return;
                }
            }

            // Special properties that we care about
            switch (dispID)
            {
                case PInvokeCore.DISPID_AMBIENT_UIDEAD:
                    using (VARIANT value = GetAmbientProperty(PInvokeCore.DISPID_AMBIENT_UIDEAD))
                    {
                        if (value.vt == VARENUM.VT_BOOL)
                        {
                            _activeXState[s_uiDead] = value.data.boolVal == VARIANT_BOOL.VARIANT_TRUE;
                        }
                    }

                    break;

                case PInvokeCore.DISPID_AMBIENT_DISPLAYASDEFAULT:
                    if (_control is IButtonControl ibuttonControl)
                    {
                        using VARIANT value = GetAmbientProperty(PInvokeCore.DISPID_AMBIENT_DISPLAYASDEFAULT);
                        if (value.vt == VARENUM.VT_BOOL)
                        {
                            ibuttonControl.NotifyDefault(value.data.boolVal == VARIANT_BOOL.VARIANT_TRUE);
                        }
                    }

                    break;
            }
        }

        /// <inheritdoc cref="IOleInPlaceActiveObject.OnDocWindowActivate(BOOL)"/>
        internal void OnDocWindowActivate(BOOL fActivate)
        {
            if (_activeXState[s_uiActive] && fActivate && _inPlaceFrame is not null)
            {
                // We have to explicitly say we don't wany any border space.
                using var inPlaceFrame = _inPlaceFrame.GetInterface();
                HRESULT hr = inPlaceFrame.Value->SetBorderSpace(null);
                if (!hr.Succeeded && hr != HRESULT.INPLACE_E_NOTOOLSPACE && hr != HRESULT.E_NOTIMPL)
                {
                    Marshal.ThrowExceptionForHR((int)hr);
                }
            }
        }

        /// <summary>
        ///  Called by Control when it gets the focus.
        /// </summary>
        internal void OnFocus(bool focus)
        {
            if (_activeXState[s_inPlaceActive] && _clientSite is not null)
            {
                using var controlSite = _clientSite.TryGetInterface<IOleControlSite>(out HRESULT hr);
                if (hr.Succeeded)
                {
                    controlSite.Value->OnFocus(focus);
                }
            }

            if (focus && _activeXState[s_inPlaceActive] && !_activeXState[s_uiActive])
            {
                InPlaceActivate(OLEIVERB.OLEIVERB_UIACTIVATE);
            }
        }

        /// <summary>
        ///  Converts coordinates in pixels to HiMetric.
        /// </summary>
        private static Point PixelToHiMetric(int x, int y) => new()
        {
            X = (HiMetricPerInch * x + (LogPixels.X >> 1)) / LogPixels.X,
            Y = (HiMetricPerInch * y + (LogPixels.Y >> 1)) / LogPixels.Y
        };

        /// <inheritdoc cref="IQuickActivate.QuickActivate(QACONTAINER*, QACONTROL*)"/>
        internal unsafe HRESULT QuickActivate(QACONTAINER* pQaContainer, QACONTROL* pQaControl)
        {
            if (pQaControl is null)
            {
                return HRESULT.E_FAIL;
            }

            // Hookup our ambient colors
            AmbientProperty prop = LookupAmbient(PInvokeCore.DISPID_AMBIENT_BACKCOLOR);
            prop.Value = ColorTranslator.FromOle((int)pQaContainer->colorBack);

            prop = LookupAmbient(PInvokeCore.DISPID_AMBIENT_FORECOLOR);
            prop.Value = ColorTranslator.FromOle((int)pQaContainer->colorFore);

            // And our ambient font
            if (pQaContainer->pFont is not null)
            {
                prop = LookupAmbient(PInvokeCore.DISPID_AMBIENT_FONT);

                try
                {
                    prop.Value = Font.FromHfont(pQaContainer->pFont->hFont);
                }
                catch (Exception e) when (!e.IsCriticalException())
                {
                    // Do NULL, so we just defer to the default font
                    prop.Value = null;
                }
            }

            // Now use the rest of the goo that we got passed in.
            pQaControl->cbSize = (uint)sizeof(QACONTROL);

            if (pQaContainer->pClientSite is not null)
            {
                SetClientSite(pQaContainer->pClientSite);
            }

            if (pQaContainer->pAdviseSink is not null)
            {
                SetAdvise(DVASPECT.DVASPECT_CONTENT, 0, (IAdviseSink*)pQaContainer->pAdviseSink);
            }

            _control.GetMiscStatus(DVASPECT.DVASPECT_CONTENT, out OLEMISC status);
            pQaControl->dwMiscStatus = status;

            // Advise the event sink so Visual Basic 6 can catch events raised from UserControls. VB6 expects the control
            // to do this during IQuickActivate, otherwise it will not hook events at runtime. We will do this if all of
            // the following are true:
            //
            //  1. The container (e.g., VB6) has supplied an event sink
            //  2. The control is a UserControl (this is only to limit the scope of the changed behavior)
            //  3. The UserControl has indicated it wants to expose events to COM via the ComSourceInterfacesAttribute

            if ((pQaContainer->pUnkEventSink is not null) && (_control is UserControl))
            {
                // Check if this control exposes events to COM.
                if (GetDefaultEventsInterface(_control.GetType()) is { } eventInterface)
                {
                    // Control doesn't explicitly implement IConnectionPointContainer, but it is generated with a CCW by
                    // COM interop.

                    using var container = ComHelpers.TryGetComScope<IConnectionPointContainer>(_control, out HRESULT hr);

                    hr.AssertSuccess();

                    using ComScope<IConnectionPoint> connectionPoint = new(null);
                    if (hr.Failed || container.Value->FindConnectionPoint(eventInterface.GUID, connectionPoint).Failed)
                    {
                        throw new ArgumentException(string.Format(SR.AXNoConnectionPoint, eventInterface.Name));
                    }

                    hr = connectionPoint.Value->Advise(pQaContainer->pUnkEventSink, out pQaControl->dwEventCookie);
                    Debug.WriteLineIf(hr.Failed, $"Failed to advise with {eventInterface.Name}.");
                }
            }

            return HRESULT.S_OK;

            // Get the default COM events interface declared on a .NET class.
            static Type? GetDefaultEventsInterface(Type controlType)
            {
                Type? eventInterface = null;

                if (!IsSupported)
                {
                    throw new NotSupportedException(string.Format(SR.ControlNotSupportedInTrimming, nameof(ActiveXImpl)));
                }

                // Get the first declared interface, if any.
                if (controlType.GetCustomAttributes<ComSourceInterfacesAttribute>(inherit: false).FirstOrDefault()
                    is { } comSourceInterfaces)
                {
                    string eventName = comSourceInterfaces.Value.AsSpan().SliceAtFirstNull().ToString();
                    eventInterface = controlType.Module.Assembly.GetType(eventName, throwOnError: false);
                    eventInterface ??= Type.GetType(eventName, throwOnError: false);
                }

                return eventInterface;
            }
        }

        /// <inheritdoc cref="IPersistStorage.Save(IStorage*, BOOL)"/>
        internal HRESULT Save(IStorage* storage, BOOL fSameAsLoad)
        {
            using ComScope<IStream> stream = new(null);
            HRESULT hr = storage->CreateStream(
                GetStreamName(),
                STGM.STGM_WRITE | STGM.STGM_SHARE_EXCLUSIVE | STGM.STGM_CREATE,
                0,
                0,
                stream);

            Debug.Assert(hr.Succeeded, "Stream should be non-null.");

            if (hr.Succeeded)
            {
                Save(stream, fClearDirty: true);
            }

            return hr;
        }

        /// <inheritdoc cref="IPersistStreamInit.Save(IStream*, BOOL)"/>
        internal void Save(IStream* stream, BOOL fClearDirty)
        {
            // We do everything through property bags because we support full fidelity in them.
            // So, save through that method.
            PropertyBagStream bagStream = new();

            using var propertyBag = ComHelpers.GetComScope<IPropertyBag>(bagStream);
            Save(propertyBag, fClearDirty, saveAllProperties: false);
            bagStream.Write(stream);
        }

        /// <inheritdoc cref="IPersistPropertyBag.Save(IPropertyBag*, BOOL, BOOL)"/>
        internal void Save(IPropertyBag* propertyBag, BOOL clearDirty, BOOL saveAllProperties)
        {
            if (!IsSupported)
            {
                throw new NotSupportedException(string.Format(SR.ControlNotSupportedInTrimming, nameof(ActiveXImpl)));
            }

            PropertyDescriptorCollection props = TypeDescriptor.GetProperties(
                _control,
                [DesignerSerializationVisibilityAttribute.Visible]);

            for (int i = 0; i < props.Count; i++)
            {
                PropertyDescriptor currentProperty = props[i];
                if (!saveAllProperties && !currentProperty.ShouldSerializeValue(_control))
                {
                    continue;
                }

                string? value = null;

                if (IsResourceProperty(currentProperty))
                {
                    // Resource property.  Save this to the bag as a 64bit encoded string.
                    using MemoryStream stream = new();
                    object sourceValue = currentProperty.GetValue(_control)!;
                    bool success = false;

                    try
                    {
                        success = WinFormsBinaryFormatWriter.TryWriteObject(stream, sourceValue);
                    }
                    catch (Exception ex) when (!ex.IsCriticalException())
                    {
                        Debug.Fail($"Failed to write with BinaryFormatWriter: {ex.Message}");
                    }

                    if (!success)
                    {
                        stream.SetLength(0);

                        if (!DataObject.Composition.EnableUnsafeBinaryFormatterInNativeObjectSerialization)
                        {
                            throw new NotSupportedException(SR.BinaryFormatterNotSupported);
                        }

#pragma warning disable SYSLIB0011 // Type or member is obsolete
                        new BinaryFormatter().Serialize(stream, sourceValue);
#pragma warning restore
                    }

                    using VARIANT data = (VARIANT)new BSTR(Convert.ToBase64String(
                        new ReadOnlySpan<byte>(stream.GetBuffer(), 0, (int)stream.Length)));
                    propertyBag->Write(currentProperty.Name, data);
                    continue;
                }

                // Not a resource property.  Persist this using standard type converters.
                TypeConverter converter = currentProperty.Converter;
                Debug.Assert(
                    converter is not null,
                    $"No type converter for property '{currentProperty.Name}' on class {_control.GetType().FullName}");

                if (converter.CanConvertFrom(typeof(string)))
                {
                    value = converter.ConvertToInvariantString(currentProperty.GetValue(_control));
                }
                else if (converter.CanConvertFrom(typeof(byte[])))
                {
                    byte[] data = (byte[])converter.ConvertTo(
                        context: null,
                        CultureInfo.InvariantCulture,
                        currentProperty.GetValue(_control),
                        typeof(byte[]))!;

                    value = Convert.ToBase64String(data);
                }

                if (value is not null)
                {
                    using VARIANT variant = (VARIANT)(new BSTR(value));
                    fixed (char* pszPropName = currentProperty.Name)
                    {
                        propertyBag->Write(pszPropName, &variant);
                    }
                }
            }

            if (clearDirty)
            {
                _activeXState[s_isDirty] = false;
            }
        }

        /// <summary>
        ///  Fires the OnSave event to all of our IAdviseSink listeners. Used for ActiveXSourcing.
        /// </summary>
        private void SendOnSave()
        {
            if (_adviseHolder is null)
            {
                return;
            }

            using var holder = _adviseHolder.GetInterface();
            holder.Value->SendOnSave();
        }

        /// <inheritdoc cref="IViewObject.SetAdvise(DVASPECT, uint, IAdviseSink*)"/>
        internal HRESULT SetAdvise(DVASPECT aspects, ADVF advf, IAdviseSink* pAdvSink)
        {
            // If it's not a content aspect, we don't support it.
            if (!aspects.HasFlag(DVASPECT.DVASPECT_CONTENT))
            {
                return HRESULT.DV_E_DVASPECT;
            }

            // Set up some flags to return from GetAdvise.
            _activeXState[s_viewAdvisePrimeFirst] = advf.HasFlag(ADVF.ADVF_PRIMEFIRST);
            _activeXState[s_viewAdviseOnlyOnce] = advf.HasFlag(ADVF.ADVF_ONLYONCE);

            DisposeHelper.NullAndRelease(ref _viewAdviseSink);

            if (pAdvSink is not null)
            {
                pAdvSink->AddRef();
            }

            _viewAdviseSink = pAdvSink;

            // Prime them if they want it [we need to store this so they can get flags later]
            if (_activeXState[s_viewAdvisePrimeFirst])
            {
                ViewChanged();
            }

            return HRESULT.S_OK;
        }

        /// <inheritdoc cref="IOleObject.SetClientSite(IOleClientSite*)"/>
        internal void SetClientSite(IOleClientSite* value)
        {
            DisposeHelper.NullAndDispose(ref _clientSite);

            if (value is not null)
            {
                // Callers don't increment the ref count when they pass IOleClientSite, it is up to us to do so as we're
                // maintaining a reference to the pointer. Validated this behavior with the ATL/MFC sources.
                //
                // https://learn.microsoft.com/windows/win32/api/oleidl/nf-oleidl-ioleobject-setclientsite#notes-to-implementers

                value->AddRef();
                _clientSite = new(value, takeOwnership: true);
                _control.Site = new AxSourcingSite(_control, _clientSite, "ControlAxSourcingSite");
            }
            else
            {
                _control.Site = null;
            }

            // Get the ambient properties that effect us.
            using VARIANT property = GetAmbientProperty(PInvokeCore.DISPID_AMBIENT_UIDEAD);
            if (property.vt == VARENUM.VT_BOOL)
            {
                bool uiDead = property.data.boolVal == VARIANT_BOOL.VARIANT_TRUE;
                _activeXState[s_uiDead] = uiDead;

                if (_control is IButtonControl buttonControl)
                {
                    buttonControl.NotifyDefault(uiDead);
                }
            }

            if (_clientSite is null && !_accelTable.IsNull)
            {
                PInvoke.DestroyAcceleratorTable(new HandleRef<HACCEL>(_control, _accelTable));
                _accelTable = HACCEL.Null;
                _accelCount = -1;
            }

            _control.OnTopMostActiveXParentChanged(EventArgs.Empty);
        }

        /// <inheritdoc cref="IOleObject.SetExtent(DVASPECT, SIZE*)"/>
        internal unsafe void SetExtent(DVASPECT dwDrawAspect, Size* pSizel)
        {
            if (!dwDrawAspect.HasFlag(DVASPECT.DVASPECT_CONTENT))
            {
                // We don't support any other aspects
                ThrowHr(HRESULT.DV_E_DVASPECT);
            }

            if (_activeXState[s_changingExtents])
            {
                return;
            }

            _activeXState[s_changingExtents] = true;

            try
            {
                Size size = new(HiMetricToPixel(pSizel->Width, pSizel->Height));

                // If we're in place active, let the in place site set our bounds.
                // Otherwise, just set it on our control directly.
                if (_activeXState[s_inPlaceActive] && _clientSite is not null)
                {
                    using var clientSite = _clientSite.TryGetInterface<IOleInPlaceSite>(out HRESULT hr);
                    if (hr.Succeeded)
                    {
                        Rectangle bounds = _control.Bounds;
                        bounds.Location = new Point(bounds.X, bounds.Y);
                        Size adjusted = new(size.Width, size.Height);
                        bounds.Width = adjusted.Width;
                        bounds.Height = adjusted.Height;
                        Debug.Assert(_clientSite is not null, "How can we setextent before we are sited??");

                        RECT posRect = bounds;
                        clientSite.Value->OnPosRectChange(&posRect);
                    }
                }

                _control.Size = size;

                // Check to see if the control overwrote our size with its own values.
                if (_control.Size.Equals(size))
                {
                    return;
                }

                _activeXState[s_isDirty] = true;

                // If we're not inplace active, then announce that the view changed.
                if (!_activeXState[s_inPlaceActive])
                {
                    ViewChanged();
                }

                // We need to call RequestNewObjectLayout here so we visually display our new extents.
                if (!_activeXState[s_inPlaceActive] && _clientSite is not null)
                {
                    using var clientSite = _clientSite.GetInterface();
                    clientSite.Value->RequestNewObjectLayout();
                }
            }
            finally
            {
                _activeXState[s_changingExtents] = false;
            }
        }

        /// <summary>
        ///  Marks our state as in place visible.
        /// </summary>
        private void SetInPlaceVisible(bool visible)
        {
            _activeXState[s_inPlaceVisible] = visible;
            _control.Visible = visible;
        }

        /// <inheritdoc cref="IOleInPlaceObject.SetObjectRects(RECT*, RECT*)"/>
        internal unsafe HRESULT SetObjectRects(RECT* lprcPosRect, RECT* lprcClipRect)
        {
            if (lprcPosRect is null || lprcClipRect is null)
            {
                return HRESULT.E_INVALIDARG;
            }

            Rectangle posRect = *lprcPosRect;

            // ActiveX expects to be notified when a control's bounds change, and also
            // intends to notify us through SetObjectRects when we report that the
            // bounds are about to change.  We implement this all on a control's Bounds
            // property, which doesn't use this callback mechanism.  The adjustRect
            // member handles this. If it is non-null, then we are being called in
            // response to an OnPosRectChange call.  In this case we do not
            // set the control bounds but set the bounds on the adjustRect.  When
            // this returns from the container and comes back to our OnPosRectChange
            // implementation, these new bounds will be handed back to the control
            // for the actual window change.
            if (_activeXState[s_adjustingRect])
            {
                *_adjustRect = posRect;
            }
            else
            {
                _activeXState[s_adjustingRect] = true;
                try
                {
                    _control.Bounds = posRect;
                }
                finally
                {
                    _activeXState[s_adjustingRect] = false;
                }
            }

            bool setRegion = false;

            if (_lastClipRect.HasValue)
            {
                // We had a clipping rectangle, we need to set the Control's Region even if we don't have a new
                // lprcClipRect to ensure it remove it in said case.
                _lastClipRect = null;
                setRegion = true;
            }

            if (lprcClipRect is not null)
            {
                // The container wants us to clip, so figure out if we really need to.
                Rectangle clipRect = *lprcClipRect;

                // Trident always sends an empty ClipRect, don't do an intersect in that case.
                Rectangle intersect = !clipRect.IsEmpty ? Rectangle.Intersect(posRect, clipRect) : posRect;

                if (!intersect.Equals(posRect))
                {
                    // Offset the rectangle back to client coordinates
                    RECT rcIntersect = intersect;
                    HWND hWndParent = PInvoke.GetParent(_control);

                    PInvoke.MapWindowPoints(hWndParent, _control, ref rcIntersect);

                    _lastClipRect = rcIntersect;
                    setRegion = true;
                }
            }

            // If our region has changed, set the new value.  We only do this if
            // the handle has been created, since otherwise the control will
            // merge our region automatically.
            if (setRegion)
            {
                _control.SetRegion(_control.Region);
            }

            // Forms^3 uses transparent overlay windows that appear to cause painting artifacts.
            _control.Invalidate();

            return HRESULT.S_OK;
        }

        /// <summary>
        ///  Throws the given hresult. This is used by ActiveX sourcing.
        /// </summary>
        [DoesNotReturn]
        internal static void ThrowHr(HRESULT hr) => throw new ExternalException(SR.ExternalException, (int)hr);

        /// <inheritdoc cref="IOleInPlaceActiveObject.TranslateAccelerator(MSG*)"/>
        internal unsafe HRESULT TranslateAccelerator(MSG* lpmsg)
        {
            if (lpmsg is null)
            {
                return HRESULT.E_POINTER;
            }

            bool needPreProcess = false;
            switch (lpmsg->message)
            {
                case PInvoke.WM_KEYDOWN:
                case PInvoke.WM_SYSKEYDOWN:
                case PInvoke.WM_CHAR:
                case PInvoke.WM_SYSCHAR:
                    needPreProcess = true;
                    break;
            }

            Message msg = Message.Create(lpmsg->hwnd, lpmsg->message, lpmsg->wParam, lpmsg->lParam);
            if (needPreProcess)
            {
                Control? target = FromChildHandle(lpmsg->hwnd);
                if (_control == target || _control.Contains(target))
                {
                    PreProcessControlState messageState = PreProcessControlMessageInternal(target, ref msg);
                    switch (messageState)
                    {
                        case PreProcessControlState.MessageProcessed:
                            // someone returned true from PreProcessMessage
                            // no need to dispatch the message, its already been coped with.
                            lpmsg->message = (uint)msg.MsgInternal;
                            lpmsg->wParam = msg.WParamInternal;
                            lpmsg->lParam = msg.LParamInternal;
                            return HRESULT.S_OK;
                        case PreProcessControlState.MessageNeeded:
                            // Here we need to dispatch the message ourselves
                            // otherwise the host may never send the key to our wndproc.

                            // Someone returned true from IsInputKey or IsInputChar
                            PInvoke.TranslateMessage(lpmsg);
                            if (PInvoke.IsWindowUnicode(lpmsg->hwnd))
                            {
                                PInvoke.DispatchMessage(lpmsg);
                            }
                            else
                            {
                                PInvoke.DispatchMessageA(lpmsg);
                            }

                            return HRESULT.S_OK;
                        case PreProcessControlState.MessageNotNeeded:
                            // in this case we'll check the site to see if it wants the message.
                            break;
                    }
                }
            }

            // SITE processing. We're not interested in the message, but the site may be.
            if (_clientSite is null)
            {
                return HRESULT.S_FALSE;
            }

            using var controlSite = _clientSite.TryGetInterface<IOleControlSite>(out HRESULT hr);
            if (hr.Failed)
            {
                return HRESULT.S_FALSE;
            }

            KEYMODIFIERS keyState = 0;
            if (PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_SHIFT) < 0)
            {
                keyState |= KEYMODIFIERS.KEYMOD_SHIFT;
            }

            if (PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_CONTROL) < 0)
            {
                keyState |= KEYMODIFIERS.KEYMOD_CONTROL;
            }

            if (PInvoke.GetKeyState((int)VIRTUAL_KEY.VK_MENU) < 0)
            {
                keyState |= KEYMODIFIERS.KEYMOD_ALT;
            }

            return controlSite.Value->TranslateAccelerator(lpmsg, keyState);
        }

        /// <inheritdoc cref="IOleInPlaceObject.UIDeactivate"/>
        internal HRESULT UIDeactivate()
        {
            // Only do this if we're UI active
            if (!_activeXState[s_uiActive])
            {
                return HRESULT.S_OK;
            }

            _activeXState[s_uiActive] = false;

            // Notify frame windows, if appropriate, that we're no longer ui-active.
            if (_inPlaceUiWindow is not null)
            {
                using var window = _inPlaceUiWindow.GetInterface();
                window.Value->SetActiveObject(null, (PCWSTR)null);
            }

            // May need this for SetActiveObject & OnUIDeactivate, so leave until function return.
            Debug.Assert(_inPlaceFrame is not null, "No inplace frame -- how did we go UI active?");

            if (_inPlaceFrame is not null)
            {
                using var frame = _inPlaceFrame.GetInterface();
                frame.Value->SetActiveObject(null, (PCWSTR)null);
            }

            if (_clientSite is not null)
            {
                using var inPlaceSite = _clientSite.TryGetInterface<IOleInPlaceSite>(out HRESULT hr);
                if (hr.Succeeded)
                {
                    inPlaceSite.Value->OnUIDeactivate(fUndoable: BOOL.FALSE);
                }
            }

            return HRESULT.S_OK;
        }

        /// <inheritdoc cref="IOleObject.Unadvise(uint)"/>
        internal HRESULT Unadvise(uint dwConnection)
        {
            if (_adviseHolder is null)
            {
                return HRESULT.E_FAIL;
            }

            using var holder = _adviseHolder.GetInterface();
            return holder.Value->Unadvise(dwConnection);
        }

        /// <summary>
        ///  Notifies our site that we have changed our size and location.
        /// </summary>
        internal unsafe void UpdateBounds(ref int x, ref int y, ref int width, ref int height, SET_WINDOW_POS_FLAGS flags)
        {
            if (_activeXState[s_adjustingRect] || !_activeXState[s_inPlaceVisible] || _clientSite is null)
            {
                return;
            }

            using var inPlaceSite = _clientSite.TryGetInterface<IOleInPlaceSite>(out HRESULT hr);
            if (hr.Failed)
            {
                return;
            }

            RECT rect = default;
            if (flags.HasFlag(SET_WINDOW_POS_FLAGS.SWP_NOMOVE))
            {
                rect.left = _control.Left;
                rect.top = _control.Top;
            }
            else
            {
                rect.left = x;
                rect.top = y;
            }

            if (flags.HasFlag(SET_WINDOW_POS_FLAGS.SWP_NOSIZE))
            {
                rect.right = rect.left + _control.Width;
                rect.bottom = rect.top + _control.Height;
            }
            else
            {
                rect.right = rect.left + width;
                rect.bottom = rect.top + height;
            }

            // This member variable may be modified by SetObjectRects by the container.
            _adjustRect = &rect;
            _activeXState[s_adjustingRect] = true;

            try
            {
                inPlaceSite.Value->OnPosRectChange(&rect);
            }
            finally
            {
                _adjustRect = null;
                _activeXState[s_adjustingRect] = false;
            }

            // On output, the new bounds will be reflected in  rc
            if (!flags.HasFlag(SET_WINDOW_POS_FLAGS.SWP_NOMOVE))
            {
                x = rect.left;
                y = rect.top;
            }

            if (!flags.HasFlag(SET_WINDOW_POS_FLAGS.SWP_NOSIZE))
            {
                width = rect.right - rect.left;
                height = rect.bottom - rect.top;
            }
        }

        /// <summary>
        ///  Notifies that the accelerator table needs to be updated due to a change in a control mnemonic.
        /// </summary>
        internal void UpdateAccelTable()
        {
            // Setting the count to -1 will recreate the table on demand (when GetControlInfo is called).
            _accelCount = -1;

            if (_clientSite is null)
            {
                return;
            }

            using var controlSite = _clientSite.TryGetInterface<IOleControlSite>(out HRESULT hr);
            if (hr.Failed)
            {
                return;
            }

            controlSite.Value->OnControlInfoChanged();
        }

        // This method is used by Reflection- don't change the signature.
        internal void ViewChangedInternal() => ViewChanged();

        /// <summary>
        ///  Notifies our view advise sink (if it exists) that the view has changed.
        /// </summary>
        private void ViewChanged()
        {
            // Send the view change notification to anybody listening.
            //
            // Note: Word2000 won't resize components correctly if an OnViewChange notification is sent while the
            // component is persisting it's state. The !s_saving check is to make sure we don't call OnViewChange
            // in this case.

            if (_viewAdviseSink is null || _activeXState[s_saving])
            {
                return;
            }

            _viewAdviseSink->OnViewChange((int)DVASPECT.DVASPECT_CONTENT, -1);

            if (_activeXState[s_viewAdviseOnlyOnce])
            {
                DisposeHelper.NullAndRelease(ref _viewAdviseSink);
            }
        }

        /// <summary>
        ///  Called when the window handle of the control has changed.
        /// </summary>
        void IWindowTarget.OnHandleChange(IntPtr newHandle) => _controlWindowTarget.OnHandleChange(newHandle);

        /// <summary>
        ///  Called to do control-specific processing for this window.
        /// </summary>
        void IWindowTarget.OnMessage(ref Message m)
        {
            if (_activeXState[s_uiDead])
            {
                if (m.IsMouseMessage())
                {
                    return;
                }

                if (m.Msg is >= ((int)PInvoke.WM_NCLBUTTONDOWN) and <= ((int)PInvoke.WM_NCMBUTTONDBLCLK))
                {
                    return;
                }

                if (m.IsKeyMessage())
                {
                    return;
                }
            }

            _controlWindowTarget.OnMessage(ref m);
        }

        public void Dispose()
        {
            // Disposing the client site handle can get us called back with SetClientSite(null). We need to
            // make sure that we clear the field before disposing it. To avoid similar problems, we do the same
            // pattern for every COM pointer.

            DisposeHelper.NullAndDispose(ref _inPlaceFrame);
            DisposeHelper.NullAndDispose(ref _inPlaceUiWindow);
            DisposeHelper.NullAndDispose(ref _clientSite);
            DisposeHelper.NullAndDispose(ref _adviseHolder);
        }
    }
}
