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

using System.ComponentModel;
using System.ComponentModel.Design;
using Windows.Win32.System.Com;
using Windows.Win32.System.Ole;
using Windows.Win32.System.Variant;

namespace System.Windows.Forms;

public abstract partial class AxHost
{
    internal unsafe partial class AxContainer :
        UnknownDispatch,
        IOleContainer.Interface,
        IOleInPlaceFrame.Interface,
        IOleInPlaceUIWindow.Interface,
        IOleWindow.Interface,
        IManagedWrapper<IOleContainer, IOleInPlaceFrame, IOleInPlaceUIWindow, IOleWindow, IDispatch, IDispatchEx>
    {
        internal ContainerControl _parent;

        // The associated container may be null, in which case all this container does is
        // forward [de]activation messages to the requisite container.
        private IContainer? _associatedContainer;

        private AxHost? _siteUIActive;
        private AxHost? _siteActive;
        private bool _formAlreadyCreated;
        private readonly HashSet<AxHost> _containerCache = [];
        private int _lockCount;
        private HashSet<Control>? _components;
        private Dictionary<Control, ExtenderProxy>? _extenderCache;
        private AxHost? _controlInEditMode;
        private readonly Lock _lock = new();

        internal AxContainer(ContainerControl parent)
        {
            _parent = parent;
            if (parent.Created)
            {
                FormCreated();
            }
        }

        protected override unsafe HRESULT Invoke(
            int dispId,
            uint lcid,
            DISPATCH_FLAGS flags,
            DISPPARAMS* parameters,
            VARIANT* result,
            EXCEPINFO* exceptionInfo,
            uint* argumentError)
        {
            // This was provided via IReflect. No members were actually exposed via IReflect, so the only way this could
            // ever come in was as a [DISPID] string. Not clear if this was accidental or intentional.

            string name = $"[DISPID={dispId}]";
            foreach (AxHost control in _containerCache)
            {
                if (GetNameForControl(control).Equals(name))
                {
                    IExtender.Interface? extender = GetExtenderProxyForControl(control);
                    if (extender is null)
                    {
                        Debug.WriteLine($"No proxy for {name}, returning null");
                        result = default;
                    }
                    else
                    {
                        *result = (VARIANT)ComHelpers.GetComPointer<IUnknown>(extender);
                    }

                    return HRESULT.S_OK;
                }
            }

            return HRESULT.DISP_E_MEMBERNOTFOUND;
        }

        internal ExtenderProxy? GetExtenderProxyForControl(Control control)
        {
            ExtenderProxy? extender = null;
            if (_extenderCache is null)
            {
                _extenderCache = [];
            }
            else
            {
                _extenderCache.TryGetValue(control, out extender);
            }

            if (extender is null)
            {
                if (control != _parent && !GetComponents().Contains(control))
                {
                    AxContainer? container = FindContainerForControl(control);
                    if (container is not null)
                    {
                        extender = new ExtenderProxy(control, container);
                    }
                    else
                    {
                        return null;
                    }
                }
                else
                {
                    extender = new ExtenderProxy(control, this);
                }

                _extenderCache.Add(control, extender);
            }

            return extender;
        }

        internal static string GetNameForControl(Control control)
        {
            string? name = control.Site is { } site ? site.Name : control.Name;
            return name ?? string.Empty;
        }

        internal void AddControl(AxHost control)
        {
            lock (_lock)
            {
                if (_containerCache.Contains(control))
                {
                    throw new ArgumentException(string.Format(SR.AXDuplicateControl, GetNameForControl(control)), nameof(control));
                }

                _containerCache.Add(control);

                if (_associatedContainer is null)
                {
                    if (control.Site is { } site)
                    {
                        _associatedContainer = site.Container;
                        if (site.TryGetService(out IComponentChangeService? changeService))
                        {
                            changeService.ComponentRemoved += OnComponentRemoved;
                        }
                    }
                }
            }
        }

        internal void RemoveControl(AxHost control)
        {
            lock (_lock)
            {
                _containerCache.Remove(control);
            }
        }

        internal IEnumUnknown.Interface EnumControls(Control control, uint dwOleContF, ENUM_CONTROLS_WHICH_FLAGS dwWhich)
        {
            GetComponents();
            _lockCount++;

            Control? additionalControl;

            try
            {
                // Results are IUnknown
                List<object>? results = null;

                bool selected = dwWhich.HasFlag(ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_FSELECTED);
                bool reverse = dwWhich.HasFlag(ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_FREVERSEDIR);

                // Note that Visual Basic actually ignores the next/prev flags. We will not.
                bool onlyNext = dwWhich.HasFlag(ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_FONLYAFTER);
                bool onlyPrevious = dwWhich.HasFlag(ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_FONLYBEFORE);

                dwWhich &= ~(ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_FSELECTED
                    | ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_FREVERSEDIR
                    | ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_FONLYAFTER
                    | ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_FONLYBEFORE);

                if (onlyNext && onlyPrevious)
                {
                    Debug.Fail("onlyNext && onlyPrevious are both set");
                    throw s_invalidArgumentException;
                }

                if (dwWhich is ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_CONTAINER or ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_CONTAINED && (onlyNext || onlyPrevious))
                {
                    Debug.Fail("GC_WCH_FONLYNEXT or FONLYPREV used with CONTAINER or CONTAINED");
                    throw s_invalidArgumentException;
                }

                int first = 0;
                int last = -1; // meaning all
                Control[] controls = [];
                switch (dwWhich)
                {
                    default:
                        Debug.Fail("Bad GC_WCH");
                        throw s_invalidArgumentException;
                    case ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_CONTAINED:
                        controls = control.GetChildControlsInTabOrder(handleCreatedOnly: false);
                        additionalControl = null;
                        break;
                    case ENUM_CONTROLS_WHICH_FLAGS.GCW_WCH_SIBLING:
                        if (control.ParentInternal is { } parent)
                        {
                            controls = parent.GetChildControlsInTabOrder(handleCreatedOnly: false);
                            if (onlyPrevious)
                            {
                                last = control.TabIndex;
                            }
                            else if (onlyNext)
                            {
                                first = control.TabIndex + 1;
                            }
                        }

                        additionalControl = null;
                        break;
                    case ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_CONTAINER:
                        results = [];
                        additionalControl = null;
                        MaybeAdd(results, control, selected, (OLECONTF)dwOleContF, allowContainingControls: false);

                        while (control is not null)
                        {
                            if (FindContainerForControl(control) is { } container)
                            {
                                MaybeAdd(results, container._parent, selected, (OLECONTF)dwOleContF, allowContainingControls: true);
                                control = container._parent;
                            }
                            else
                            {
                                break;
                            }
                        }

                        break;
                    case ENUM_CONTROLS_WHICH_FLAGS.GC_WCH_ALL:
                        controls = [.. GetComponents()];
                        additionalControl = _parent;
                        break;
                }

                if (results is null)
                {
                    results = [];
                    if (last == -1 && controls is not null)
                    {
                        last = controls.Length;
                    }

                    if (additionalControl is not null)
                    {
                        MaybeAdd(results, additionalControl, selected, (OLECONTF)dwOleContF, allowContainingControls: false);
                    }

                    if (controls is not null)
                    {
                        for (int i = first; i < last; i++)
                        {
                            MaybeAdd(results, controls[i], selected, (OLECONTF)dwOleContF, allowContainingControls: false);
                        }
                    }
                }

                if (reverse)
                {
                    results.Reverse();
                }

                return new EnumUnknown([.. results]);
            }
            finally
            {
                _lockCount--;
                if (_lockCount == 0)
                {
                    _components = null;
                }
            }
        }

        private void MaybeAdd(List<object> controls, Control control, bool selected, OLECONTF flags, bool allowContainingControls)
        {
            if (!allowContainingControls && control != _parent && !GetComponents().Contains(control))
            {
                return;
            }

            if (selected)
            {
                if (control.Site is not { } site
                    || !site.TryGetService(out ISelectionService? selectionService)
                    || !selectionService.GetComponentSelected(this))
                {
                    return;
                }
            }

            if (control is AxHost hostControl && flags.HasFlag(OLECONTF.OLECONTF_EMBEDDINGS))
            {
                controls.Add(hostControl.GetOcx()!);
            }
            else if (flags.HasFlag(OLECONTF.OLECONTF_OTHERS))
            {
                if (GetExtenderProxyForControl(control) is { } proxy)
                {
                    controls.Add(proxy);
                }
            }
        }

        [MemberNotNull(nameof(_components))]
        private void FillComponentsTable(IContainer? container)
        {
            if (container?.Components is { } components)
            {
                _components = [];
                foreach (IComponent component in components)
                {
                    if (component is Control control && component != _parent && component.Site is not null)
                    {
                        _components.Add(control);
                    }
                }

                return;
            }

            Debug.Assert(_parent.Site is null, "Parent is sited but we could not find IContainer");

            if (_containerCache.Count > 0)
            {
                if (_components is null)
                {
                    _components = [];
                }
                else
                {
                    foreach (AxHost control in _containerCache)
                    {
                        _components.Add(control);
                    }
                }
            }

            GetAllChildren(_parent);
            Debug.Assert(_components is not null);

            [MemberNotNull(nameof(_components))]
            void GetAllChildren(Control control)
            {
                _components ??= [];

                if (control != _parent)
                {
                    _components.Add(control);
                }

                foreach (Control child in control.Controls)
                {
                    GetAllChildren(child);
                }
            }
        }

        private HashSet<Control> GetComponents()
        {
            if (_lockCount == 0)
            {
                FillComponentsTable(GetParentsContainer());
            }

            Debug.Assert(_components is not null);

            return _components!;
        }

        private IContainer? GetParentIsDesigned()
            => _parent.Site is { } site && site.DesignMode ? site.Container : null;

        private IContainer? GetParentsContainer()
        {
            IContainer? container = GetParentIsDesigned();
            Debug.Assert(
                container is null || _associatedContainer is null || (container == _associatedContainer),
                "mismatch between getIPD & aContainer");
            return container ?? _associatedContainer;
        }

        private bool RegisterControl(AxHost control)
        {
            if (control.Site is { } site && site.Container is { } container)
            {
                if (_associatedContainer is not null)
                {
                    return container == _associatedContainer;
                }
                else
                {
                    _associatedContainer = container;
                    if (site.TryGetService(out IComponentChangeService? changeService))
                    {
                        changeService.ComponentRemoved += OnComponentRemoved;
                    }

                    return true;
                }
            }

            return false;
        }

        private void OnComponentRemoved(object? sender, ComponentEventArgs e)
        {
            if (sender == _associatedContainer && e.Component is AxHost control)
            {
                RemoveControl(control);
            }
        }

        internal static AxContainer? FindContainerForControl(Control control)
        {
            if (control is AxHost axHost)
            {
                if (axHost._container is { } existing)
                {
                    return existing;
                }

                if (axHost.ContainingControl?.CreateAxContainer() is { } container)
                {
                    if (container.RegisterControl(axHost))
                    {
                        container.AddControl(axHost);
                        return container;
                    }
                }
            }

            return null;
        }

        internal void OnInPlaceDeactivate(AxHost site)
        {
            if (_siteActive == site)
            {
                _siteActive = null;
                if (site.GetSiteOwnsDeactivation())
                {
                    _parent.ActiveControl = null;
                }
                else
                {
                    // We need to tell the form to switch activation to the next item
                    Debug.Fail("Control is calling OnInPlaceDeactivate by itself");
                }
            }
        }

        internal void OnUIDeactivate(AxHost site)
        {
            Debug.Assert(_siteUIActive is null || _siteUIActive == site, "Deactivating when not active");

            _siteUIActive = null;
            site.RemoveSelectionHandler();
            site.SetSelectionStyle(1);
            site._editMode = EDITM_NONE;
        }

        internal void OnUIActivate(AxHost site)
        {
            if (_siteUIActive == site)
            {
                return;
            }

            if (_siteUIActive is not null && _siteUIActive != site)
            {
                AxHost tempSite = _siteUIActive;
                bool ownDisposing = tempSite.GetAxState(s_ownDisposing);
                try
                {
                    tempSite.SetAxState(s_ownDisposing, true);
                    using var inPlaceObject = tempSite.GetComScope<IOleInPlaceObject>();
                    inPlaceObject.Value->UIDeactivate().AssertSuccess();
                }
                finally
                {
                    tempSite.SetAxState(s_ownDisposing, ownDisposing);
                }
            }

            site.AddSelectionHandler();

            // The ShDocVw control repeatedly calls OnUIActivate() with the same site.
            // This causes the assert below to fire.
            Debug.Assert(_siteUIActive is null, "Object did not call OnUIDeactivate");
            _siteUIActive = site;
            if (site.ContainingControl is { } container)
            {
                container.ActiveControl = site;
            }
        }

        internal void ControlCreated(AxHost invoker)
        {
            if (_formAlreadyCreated)
            {
                if (invoker.IsUserMode() && invoker.AwaitingDefreezing())
                {
                    invoker.FreezeEvents(false);
                }
            }
            else
            {
                // The form will be created in the future.
                _parent.CreateAxContainer();
            }
        }

        internal void FormCreated()
        {
            if (_formAlreadyCreated)
            {
                return;
            }

            _formAlreadyCreated = true;

            List<AxHost> hostControls = [];
            foreach (Control control in GetComponents())
            {
                if (control is AxHost hostControl)
                {
                    hostControls.Add(hostControl);
                }
            }

            foreach (AxHost hostControl in hostControls)
            {
                if (hostControl.GetOcState() >= OC_RUNNING && hostControl.IsUserMode() && hostControl.AwaitingDefreezing())
                {
                    hostControl.FreezeEvents(freeze: false);
                }
            }
        }

        HRESULT IParseDisplayName.Interface.ParseDisplayName(
            IBindCtx* pbc,
            PWSTR pszDisplayName,
            uint* pchEaten,
            IMoniker** ppmkOut)
            => ((IOleContainer.Interface)this).ParseDisplayName(pbc, pszDisplayName, pchEaten, ppmkOut);

        // IOleContainer methods:
        HRESULT IOleContainer.Interface.ParseDisplayName(
            IBindCtx* pbc,
            PWSTR pszDisplayName,
            uint* pchEaten,
            IMoniker** ppmkOut)
        {
            if (ppmkOut is not null)
            {
                *ppmkOut = null;
            }

            return HRESULT.E_NOTIMPL;
        }

        HRESULT IOleContainer.Interface.EnumObjects(uint grfFlags, IEnumUnknown** ppenum)
        {
            if (ppenum is null)
            {
                return HRESULT.E_POINTER;
            }

            if (((OLECONTF)grfFlags).HasFlag(OLECONTF.OLECONTF_EMBEDDINGS))
            {
                Debug.Assert(_parent is not null);

                List<object> oleControls = [];
                foreach (Control control in GetComponents())
                {
                    if (control is AxHost hostControl)
                    {
                        oleControls.Add(hostControl.GetOcx()!);
                    }
                }

                if (oleControls.Count > 0)
                {
                    *ppenum = ComHelpers.GetComPointer<IEnumUnknown>(new EnumUnknown([.. oleControls]));
                    return HRESULT.S_OK;
                }
            }

            *ppenum = ComHelpers.GetComPointer<IEnumUnknown>(new EnumUnknown(null));
            return HRESULT.S_OK;
        }

        HRESULT IOleContainer.Interface.LockContainer(BOOL fLock) => HRESULT.E_NOTIMPL;

        // IOleInPlaceFrame methods:
        HRESULT IOleInPlaceFrame.Interface.GetWindow(HWND* phwnd)
        {
            if (phwnd is null)
            {
                return HRESULT.E_POINTER;
            }

            *phwnd = _parent.HWND;
            return HRESULT.S_OK;
        }

        HRESULT IOleInPlaceFrame.Interface.ContextSensitiveHelp(BOOL fEnterMode) => HRESULT.S_OK;

        HRESULT IOleInPlaceFrame.Interface.GetBorder(RECT* lprectBorder) => HRESULT.E_NOTIMPL;

        HRESULT IOleInPlaceFrame.Interface.RequestBorderSpace(RECT* pborderwidths) => HRESULT.E_NOTIMPL;

        HRESULT IOleInPlaceFrame.Interface.SetBorderSpace(RECT* pborderwidths) => HRESULT.E_NOTIMPL;

        internal void OnExitEditMode(AxHost ctl)
        {
            Debug.Assert(_controlInEditMode is null || _controlInEditMode == ctl, "who is exiting edit mode?");
            if (_controlInEditMode is null || _controlInEditMode != ctl)
            {
                return;
            }

            _controlInEditMode = null;
        }

        HRESULT IOleInPlaceFrame.Interface.SetActiveObject(IOleInPlaceActiveObject* pActiveObject, PCWSTR pszObjName)
        {
            if (_siteUIActive is { } activeHost)
            {
                var existing = activeHost._iOleInPlaceActiveObjectExternal;
                activeHost._iOleInPlaceActiveObjectExternal = pActiveObject is null
                    ? null
                    : new(pActiveObject, takeOwnership: false);

                existing?.Dispose();
            }

            if (pActiveObject is null)
            {
                _controlInEditMode?._editMode = EDITM_NONE;
                _controlInEditMode = null;

                return HRESULT.S_OK;
            }

            using var oleObject = ComScope<IOleObject>.TryQueryFrom(pActiveObject, out HRESULT hr);
            if (hr.Failed)
            {
                return HRESULT.S_OK;
            }

            AxHost? host = null;
            using ComScope<IOleClientSite> clientSite = new(null);
            hr = oleObject.Value->GetClientSite(clientSite);
            Debug.Assert(hr.Succeeded);

            if (ComHelpers.TryGetObjectForIUnknown(clientSite.AsUnknown, takeOwnership: false, out OleInterfaces? interfaces))
            {
                host = interfaces.GetAxHost();
            }

            if (_controlInEditMode is not null)
            {
                Debug.Fail($"control {_controlInEditMode} did not reset its edit mode to null");
                _controlInEditMode.SetSelectionStyle(1);
                _controlInEditMode._editMode = EDITM_NONE;
            }

            if (host is null)
            {
                _controlInEditMode = null;
            }
            else
            {
                if (!host.IsUserMode())
                {
                    _controlInEditMode = host;
                    host._editMode = EDITM_OBJECT;
                    host.AddSelectionHandler();
                    host.SetSelectionStyle(2);
                }
            }

            return HRESULT.S_OK;
        }

        HRESULT IOleInPlaceFrame.Interface.InsertMenus(HMENU hmenuShared, OLEMENUGROUPWIDTHS* lpMenuWidths) => HRESULT.S_OK;

        HRESULT IOleInPlaceFrame.Interface.SetMenu(HMENU hmenuShared, nint holemenu, HWND hwndActiveObject) => HRESULT.E_NOTIMPL;

        HRESULT IOleInPlaceFrame.Interface.RemoveMenus(HMENU hmenuShared) => HRESULT.E_NOTIMPL;

        HRESULT IOleInPlaceFrame.Interface.SetStatusText(PCWSTR pszStatusText) => HRESULT.E_NOTIMPL;

        HRESULT IOleInPlaceFrame.Interface.EnableModeless(BOOL fEnable) => HRESULT.E_NOTIMPL;

        HRESULT IOleInPlaceFrame.Interface.TranslateAccelerator(MSG* lpmsg, ushort wID) => HRESULT.S_FALSE;

        HRESULT IOleInPlaceUIWindow.Interface.GetWindow(HWND* phwnd)
            => ((IOleInPlaceFrame.Interface)this).GetWindow(phwnd);

        HRESULT IOleInPlaceUIWindow.Interface.ContextSensitiveHelp(BOOL fEnterMode)
            => ((IOleInPlaceFrame.Interface)this).ContextSensitiveHelp(fEnterMode);

        HRESULT IOleInPlaceUIWindow.Interface.GetBorder(RECT* lprectBorder)
            => ((IOleInPlaceFrame.Interface)this).GetBorder(lprectBorder);

        HRESULT IOleInPlaceUIWindow.Interface.RequestBorderSpace(RECT* pborderwidths)
            => ((IOleInPlaceFrame.Interface)this).RequestBorderSpace(pborderwidths);

        HRESULT IOleInPlaceUIWindow.Interface.SetBorderSpace(RECT* pborderwidths)
            => ((IOleInPlaceFrame.Interface)this).SetBorderSpace(pborderwidths);

        HRESULT IOleInPlaceUIWindow.Interface.SetActiveObject(IOleInPlaceActiveObject* pActiveObject, PCWSTR pszObjName)
            => ((IOleInPlaceFrame.Interface)this).SetActiveObject(pActiveObject, pszObjName);

        HRESULT IOleWindow.Interface.GetWindow(HWND* phwnd)
            => ((IOleInPlaceFrame.Interface)this).GetWindow(phwnd);

        HRESULT IOleWindow.Interface.ContextSensitiveHelp(BOOL fEnterMode)
            => ((IOleInPlaceFrame.Interface)this).ContextSensitiveHelp(fEnterMode);

        protected override void Dispose(bool disposing)
        {
            if (_extenderCache is { } cache)
            {
                foreach (var extender in cache.Values)
                {
                    extender.Dispose();
                }

                cache.Clear();
            }

            base.Dispose(disposing);
        }
    }
}
