// 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;

namespace System.Windows.Forms;

internal unsafe class WebBrowserContainer : IOleContainer.Interface, IOleInPlaceFrame.Interface, IOleInPlaceUIWindow.Interface, IOleWindow.Interface
{
    private readonly WebBrowserBase _parent;

    /// <summary>
    ///  May be null, in which case all this container does is forward [de]activation messages to the requisite container.
    /// </summary>
    private IContainer? _associatedContainer;
    private WebBrowserBase? _siteUIActive;
    private WebBrowserBase? _siteActive;
    private readonly HashSet<Control> _containerCache = [];
    private HashSet<Control>? _components;
    private WebBrowserBase? _controlInEditMode;

    internal WebBrowserContainer(WebBrowserBase parent) => _parent = parent;

    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> list = [];
            ListAXControls(list, true);
            if (list.Count > 0)
            {
                object[] temp = new object[list.Count];
                list.CopyTo(temp, 0);
                *ppenum = ComHelpers.GetComPointer<IEnumUnknown>(new AxHost.EnumUnknown(temp));
                return HRESULT.S_OK;
            }
        }

        *ppenum = ComHelpers.GetComPointer<IEnumUnknown>(new AxHost.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;

    HRESULT IOleInPlaceFrame.Interface.SetActiveObject(IOleInPlaceActiveObject* pActiveObject, PCWSTR pszObjName)
    {
        if (pActiveObject is null)
        {
            _controlInEditMode?.SetEditMode(WebBrowserHelper.AXEditMode.None);
            _controlInEditMode = null;

            return HRESULT.S_OK;
        }

        WebBrowserBase? control = null;

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

        IOleClientSite* clientSite;
        oleObject.Value->GetClientSite(&clientSite);
        object clientSiteObject = ComHelpers.GetObjectForIUnknown(clientSite);
        if (clientSiteObject is WebBrowserSiteBase webBrowserSiteBase)
        {
            control = webBrowserSiteBase.Host;
        }

        if (_controlInEditMode is not null)
        {
            Debug.Fail($"Control {_controlInEditMode} did not reset its edit mode to null.");
            _controlInEditMode.SetSelectionStyle(WebBrowserHelper.SelectionStyle.Selected);
            _controlInEditMode.SetEditMode(WebBrowserHelper.AXEditMode.None);
        }

        if (control is null)
        {
            _controlInEditMode = null;
        }
        else
        {
            if (!control.IsUserMode)
            {
                _controlInEditMode = control;
                control.SetEditMode(WebBrowserHelper.AXEditMode.Object);
                control.AddSelectionHandler();
                control.SetSelectionStyle(WebBrowserHelper.SelectionStyle.Active);
            }
        }

        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;

    //
    // Private helper methods:
    //
    private void ListAXControls(List<object> list, bool fuseOcx)
    {
        HashSet<Control>? components = GetComponents();
        if (components is null)
        {
            return;
        }

        foreach (Control ctl in components.ToArray())
        {
            if (ctl is WebBrowserBase webBrowserBase)
            {
                if (fuseOcx)
                {
                    object? activeX = webBrowserBase._activeXInstance;
                    if (activeX is not null)
                    {
                        list.Add(activeX);
                    }
                }
                else
                {
                    list.Add(ctl);
                }
            }
        }
    }

    private HashSet<Control>? GetComponents()
    {
        FillComponentsTable(GetParentsContainer());
        return _components;
    }

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

    private IContainer? GetParentIContainer()
    {
        ISite? site = _parent.Site;
        return site is not null && site.DesignMode ? site.Container : null;
    }

    private void FillComponentsTable(IContainer? container)
    {
        if (container is not null)
        {
            ComponentCollection comps = container.Components;
            if (comps is not null)
            {
                _components = [];
                foreach (IComponent comp in comps)
                {
                    if (comp is Control ctrl && comp != _parent && comp.Site is not null)
                    {
                        _components.Add(ctrl);
                    }
                }

                return;
            }
        }

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

        bool checkHashSet = true;
        Control[] ctls = [.. _containerCache];
        if (ctls is not null)
        {
            if (ctls.Length > 0 && _components is null)
            {
                _components = [];
                checkHashSet = false;
            }

            for (int i = 0; i < ctls.Length; i++)
            {
                if (checkHashSet)
                {
                    _components?.Add(ctls[i]);
                }
            }
        }

        GetAllChildren(_parent);
    }

    private void GetAllChildren(Control ctl)
    {
        if (ctl is null)
        {
            return;
        }

        _components ??= [];

        if (ctl != _parent && !_components.Contains(ctl))
        {
            _components.Add(ctl);
        }

        foreach (Control c in ctl.Controls)
        {
            GetAllChildren(c);
        }
    }

    private bool RegisterControl(WebBrowserBase ctl)
    {
        if (ctl.Site is not ISite site || site.Container is not IContainer container)
        {
            return false;
        }

        if (_associatedContainer is not null)
        {
            return container == _associatedContainer;
        }

        _associatedContainer = container;
        if (site.TryGetService(out IComponentChangeService? service))
        {
            service.ComponentRemoved += OnComponentRemoved;
        }

        return true;
    }

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

    //
    // Internal helper methods:
    //
    internal void AddControl(Control ctl)
    {
        if (_containerCache.Contains(ctl))
        {
            throw new ArgumentException(string.Format(SR.AXDuplicateControl, GetNameForControl(ctl)), nameof(ctl));
        }

        _containerCache.Add(ctl);

        if (_associatedContainer is not null || ctl.Site is not ISite site)
        {
            return;
        }

        _associatedContainer = site.Container;
        if (site.TryGetService(out IComponentChangeService? service))
        {
            service.ComponentRemoved += OnComponentRemoved;
        }
    }

    internal void RemoveControl(Control ctl)
    {
        // ctl may not be in containerCache: Remove is a no-op if it's not there.
        _containerCache.Remove(ctl);
    }

    internal static WebBrowserContainer? FindContainerForControl(WebBrowserBase ctl)
    {
        if (ctl is not null)
        {
            if (ctl._container is not null)
            {
                return ctl._container;
            }

            ScrollableControl? containingControl = ctl.ContainingControl;
            if (containingControl is not null)
            {
                WebBrowserContainer container = ctl.CreateWebBrowserContainer();
                if (container.RegisterControl(ctl))
                {
                    container.AddControl(ctl);
                    return container;
                }
            }
        }

        return null;
    }

    internal static string GetNameForControl(Control ctl)
        => ctl.Site is not null ? ctl.Site.Name ?? string.Empty : ctl.Name ?? string.Empty;

    internal void OnUIActivate(WebBrowserBase site)
    {
        // The ShDocVw control repeatedly calls OnUIActivate() with the same
        // site. This causes the assert below to fire.
        //
        if (_siteUIActive == site)
        {
            return;
        }

        if (_siteUIActive is not null && _siteUIActive != site)
        {
            WebBrowserBase tempSite = _siteUIActive;
            tempSite.AXInPlaceObject!.UIDeactivate();
        }

        site.AddSelectionHandler();
        Debug.Assert(_siteUIActive is null, "Object did not call OnUIDeactivate");
        _siteUIActive = site;
        ContainerControl? containingControl = site.ContainingControl;
        if (containingControl is not null && containingControl.Contains(site))
        {
            containingControl.SetActiveControl(site);
        }
    }

    internal void OnUIDeactivate(WebBrowserBase site)
    {
#if DEBUG
        if (_siteUIActive is not null)
        {
            Debug.Assert(_siteUIActive == site, "deactivating when not active...");
        }
#endif // DEBUG

        _siteUIActive = null;
        site.RemoveSelectionHandler();
        site.SetSelectionStyle(WebBrowserHelper.SelectionStyle.Selected);
        site.SetEditMode(WebBrowserHelper.AXEditMode.None);
    }

    internal void OnInPlaceDeactivate(WebBrowserBase site)
    {
        if (_siteActive == site)
        {
            _siteActive = null;
            ContainerControl? parentContainer = _parent.FindContainerControlInternal();
            parentContainer?.SetActiveControl(null);
        }
    }

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

    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);
}
