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

using System.Collections;
using System.Globalization;

namespace System.ComponentModel.Design;

/// <summary>
///  The menu command service allows designers to add and respond to
///  menu and toolbar items. It is based on two interfaces. Designers
///  request IMenuCommandService to add menu command handlers, while
///  the document or tool window forwards IOleCommandTarget requests
///  to this object.
/// </summary>
public class MenuCommandService : IMenuCommandService, IDisposable
{
    private IServiceProvider? _serviceProvider;
    private readonly Dictionary<Guid, List<MenuCommand>> _commandGroups;
    private readonly Lock _commandGroupsLock = new();
    private readonly EventHandler _commandChangedHandler;
    private MenuCommandsChangedEventHandler? _commandsChangedHandler;
    private List<DesignerVerb>? _globalVerbs;
    private ISelectionService? _selectionService;

    // This is the set of verbs we offer through the Verbs property.
    // It consists of the global verbs + any verbs that the currently
    // selected designer wants to offer. This collection changes with the
    // current selection.
    private DesignerVerbCollection? _currentVerbs;

    // This is the type that we last picked up verbs from so we know when we need to refresh.
    private Type? _verbSourceType;

    /// <summary>
    ///  Creates a new menu command service.
    /// </summary>
    public MenuCommandService(IServiceProvider? serviceProvider)
    {
        _serviceProvider = serviceProvider;
        _commandGroups = [];
        _commandChangedHandler = OnCommandChanged;
        TypeDescriptor.Refreshed += OnTypeRefreshed;
    }

    /// <summary>
    ///  This event is thrown whenever a MenuCommand is removed or added
    /// </summary>
    public event MenuCommandsChangedEventHandler? MenuCommandsChanged
    {
        add => _commandsChangedHandler += value;
        remove => _commandsChangedHandler -= value;
    }

    /// <summary>
    ///  Retrieves a set of verbs that are global to all objects on the design
    ///  surface. This set of verbs will be merged with individual component verbs.
    ///  In the case of a name conflict, the component verb will NativeMethods.
    /// </summary>
    public virtual DesignerVerbCollection Verbs
    {
        get
        {
            EnsureVerbs();
            return _currentVerbs!;
        }
    }

    /// <summary>
    ///  Adds a menu command to the document. The menu command must already exist
    ///  on a menu; this merely adds a handler for it.
    /// </summary>
    public virtual void AddCommand(MenuCommand command)
    {
        ArgumentNullException.ThrowIfNull(command);

        CommandID commandId = command.CommandID!;

        // If the command already exists, it is an error to add
        // a duplicate.
        //
        if (((IMenuCommandService)this).FindCommand(commandId) is not null)
        {
            throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, SR.MenuCommandService_DuplicateCommand, commandId.ToString()));
        }

        lock (_commandGroupsLock)
        {
            if (!_commandGroups.TryGetValue(commandId.Guid, out List<MenuCommand>? commandsList))
            {
                commandsList =
                [
                    command
                ];
                _commandGroups.Add(commandId.Guid, commandsList);
            }
            else
            {
                commandsList.Add(command);
            }
        }

        command.CommandChanged += _commandChangedHandler;

        // Raise event
        OnCommandsChanged(new MenuCommandsChangedEventArgs(MenuCommandsChangedType.CommandAdded, command));
    }

    /// <summary>
    ///  Adds a verb to the set of global verbs. Individual components should
    ///  use the Verbs property of their designer, rather than call this method.
    ///  This method is intended for objects that want to offer a verb that is
    ///  available regardless of what components are selected.
    /// </summary>
    [MemberNotNull(nameof(_globalVerbs))]
    public virtual void AddVerb(DesignerVerb verb)
    {
        ArgumentNullException.ThrowIfNull(verb);

        _globalVerbs ??= [];
        _globalVerbs.Add(verb);
        OnCommandsChanged(new MenuCommandsChangedEventArgs(MenuCommandsChangedType.CommandAdded, verb));
        EnsureVerbs();
        if (!((IMenuCommandService)this).Verbs.Contains(verb))
        {
            ((IMenuCommandService)this).Verbs.Add(verb);
        }
    }

    /// <summary>
    ///  Disposes of this service.
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
    }

    /// <summary>
    ///  Disposes of this service.
    /// </summary>
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _selectionService?.SelectionChanging -= OnSelectionChanging;
            _selectionService = null;

            if (_serviceProvider is not null)
            {
                _serviceProvider = null!;
                TypeDescriptor.Refreshed -= OnTypeRefreshed;
            }

            lock (_commandGroupsLock)
            {
                foreach (KeyValuePair<Guid, List<MenuCommand>> group in _commandGroups)
                {
                    List<MenuCommand> commands = group.Value;
                    foreach (MenuCommand command in commands)
                    {
                        command.CommandChanged -= _commandChangedHandler;
                    }

                    commands.Clear();
                }
            }
        }
    }

    /// <summary>
    ///  Ensures that the verb list has been created.
    /// </summary>
    protected void EnsureVerbs()
    {
        // We apply global verbs only if the base component is the
        // currently selected object.
        //
        bool useGlobalVerbs = false;

        if (_currentVerbs is null && _serviceProvider is not null)
        {
            if (_selectionService is null)
            {
                if (TryGetService(out _selectionService))
                {
                    _selectionService.SelectionChanging += OnSelectionChanging;
                }
            }

            int verbCount = 0;
            DesignerVerbCollection? localVerbs = null;
            List<DesignerVerb> designerActionVerbs = []; // we instantiate this one here...

            if (_selectionService?.SelectionCount == 1 && TryGetService(out IDesignerHost? designerHost))
            {
                if (_selectionService.PrimarySelection is IComponent selectedComponent &&
                    !TypeDescriptor.GetAttributes(selectedComponent).Contains(InheritanceAttribute.InheritedReadOnly))
                {
                    useGlobalVerbs = (selectedComponent == designerHost.RootComponent);

                    // LOCAL VERBS
                    IDesigner? designer = designerHost.GetDesigner(selectedComponent);
                    if (designer is not null)
                    {
                        localVerbs = designer.Verbs;
                        if (localVerbs is not null)
                        {
                            verbCount += localVerbs.Count;
                            _verbSourceType = selectedComponent.GetType();
                        }
                        else
                        {
                            _verbSourceType = null;
                        }
                    }

                    // DesignerAction Verbs
                    if (TryGetService(out DesignerActionService? daSvc))
                    {
                        DesignerActionListCollection actionLists = daSvc.GetComponentActions(selectedComponent);
                        if (actionLists is not null)
                        {
                            foreach (DesignerActionList list in actionLists)
                            {
                                DesignerActionItemCollection dai = list.GetSortedActionItems();
                                if (dai is not null)
                                {
                                    for (int i = 0; i < dai.Count; i++)
                                    {
                                        if (dai[i] is DesignerActionMethodItem dami && dami.IncludeAsDesignerVerb)
                                        {
                                            EventHandler handler = new(dami.Invoke);
                                            DesignerVerb verb = new(dami.DisplayName!, handler);
                                            designerActionVerbs.Add(verb);
                                            verbCount++;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            // GLOBAL VERBS
            if (_globalVerbs is null)
            {
                useGlobalVerbs = false;
            }
            else if (useGlobalVerbs)
            {
                verbCount += _globalVerbs.Count;
            }

            // merge all
            Dictionary<string, int> buildVerbs = new(verbCount, StringComparer.OrdinalIgnoreCase);
            List<DesignerVerb> verbsOrder = [];

            // PRIORITY ORDER FROM HIGH TO LOW: LOCAL VERBS - DESIGNERACTION VERBS - GLOBAL VERBS
            if (useGlobalVerbs)
            {
                for (int i = 0; i < _globalVerbs!.Count; i++)
                {
                    string key = _globalVerbs[i].Text;
                    verbsOrder.Add(_globalVerbs[i]);
                    buildVerbs[key] = verbsOrder.Count - 1;
                }
            }

            if (designerActionVerbs.Count > 0)
            {
                for (int i = 0; i < designerActionVerbs.Count; i++)
                {
                    DesignerVerb designerActionVerb = designerActionVerbs[i];
                    verbsOrder.Add(designerActionVerb);
                    buildVerbs[designerActionVerb.Text] = verbsOrder.Count - 1;
                }
            }

            if (localVerbs is not null && localVerbs.Count > 0)
            {
                for (int i = 0; i < localVerbs.Count; i++)
                {
                    DesignerVerb localVerb = localVerbs[i]!;
                    verbsOrder.Add(localVerb);
                    buildVerbs[localVerb.Text] = verbsOrder.Count - 1;
                }
            }

            // look for duplicate, prepare the result table
            DesignerVerb[] result = new DesignerVerb[buildVerbs.Count];
            int j = 0;
            for (int i = 0; i < verbsOrder.Count; i++)
            {
                DesignerVerb value = verbsOrder[i];
                string key = value.Text;
                if (buildVerbs[key] == i)
                { // there's not been a duplicate for this entry
                    result[j] = value;
                    j++;
                }
            }

            _currentVerbs = new(result);
        }
    }

    /// <summary>
    ///  Searches for the given command ID and returns the MenuCommand
    ///  associated with it.
    /// </summary>
    public MenuCommand? FindCommand(CommandID commandID)
    {
        return FindCommand(commandID.Guid, commandID.ID);
    }

    /// <summary>
    ///  Locates the requested command. This will throw an appropriate
    ///  ComFailException if the command couldn't be found.
    /// </summary>
    protected MenuCommand? FindCommand(Guid guid, int id)
    {
        // Search in the list of commands only if the command group is known
        List<MenuCommand>? commands;
        lock (_commandGroupsLock)
        {
            _commandGroups.TryGetValue(guid, out commands);
        }

        if (commands is not null)
        {
            foreach (MenuCommand command in commands)
            {
                if (command.CommandID!.ID == id)
                {
                    return command;
                }
            }
        }

        // Next, search the verb list as well.
        EnsureVerbs();
        if (_currentVerbs is not null)
        {
            int currentID = StandardCommands.VerbFirst.ID;
            foreach (DesignerVerb verb in _currentVerbs)
            {
                CommandID cid = verb.CommandID!;

                if (cid.ID == id)
                {
                    if (cid.Guid.Equals(guid))
                    {
                        return verb;
                    }
                }

                // We assign virtual sequential IDs to verbs we get from the component. This allows users
                // to not worry about assigning these IDs themselves.
                if (currentID == id && cid.Guid.Equals(guid))
                {
                    return verb;
                }

                if (cid.Equals(StandardCommands.VerbFirst))
                {
                    currentID++;
                }
            }
        }

        return null;
    }

    /// <summary>
    ///  Get the command list for a given GUID
    /// </summary>
    protected ICollection? GetCommandList(Guid guid)
    {
        List<MenuCommand>? commands;
        lock (_commandGroupsLock)
        {
            _commandGroups.TryGetValue(guid, out commands);
        }

        return commands;
    }

    protected object? GetService(Type serviceType)
    {
        ArgumentNullException.ThrowIfNull(serviceType);
        return _serviceProvider?.GetService(serviceType);
    }

    private protected bool TryGetService<T>([NotNullWhen(true)] out T? service) where T : class
    {
        service = GetService(typeof(T)) as T;
        return service is not null;
    }

    /// <summary>
    ///  Invokes a command on the local form or in the global environment.
    ///  The local form is first searched for the given command ID. If it is
    ///  found, it is invoked. Otherwise the command ID is passed to the
    ///  global environment command handler, if one is available.
    /// </summary>
    public virtual bool GlobalInvoke(CommandID commandID)
    {
        // try to find it locally
        MenuCommand? cmd = ((IMenuCommandService)this).FindCommand(commandID);
        if (cmd is not null)
        {
            cmd.Invoke();
            return true;
        }

        return false;
    }

    /// <summary>
    ///  Invokes a command on the local form or in the global environment.
    ///  The local form is first searched for the given command ID. If it is
    ///  found, it is invoked. Otherwise the command ID is passed to the
    ///  global environment command handler, if one is available.
    /// </summary>
    public virtual bool GlobalInvoke(CommandID commandId, object arg)
    {
        // try to find it locally
        MenuCommand? cmd = ((IMenuCommandService)this).FindCommand(commandId);
        if (cmd is not null)
        {
            cmd.Invoke(arg);
            return true;
        }

        return false;
    }

    /// <summary>
    ///  This is called by a menu command when it's status has changed.
    /// </summary>
    private void OnCommandChanged(object? sender, EventArgs e)
    {
        OnCommandsChanged(new MenuCommandsChangedEventArgs(MenuCommandsChangedType.CommandChanged, (MenuCommand?)sender));
    }

    protected virtual void OnCommandsChanged(MenuCommandsChangedEventArgs e)
    {
        _commandsChangedHandler?.Invoke(this, e);
    }

    /// <summary>
    ///  Called by TypeDescriptor when a type changes. If this type is currently holding
    ///  our verb, invalidate the list.
    /// </summary>
    private void OnTypeRefreshed(RefreshEventArgs e)
    {
        if (_verbSourceType is not null && _verbSourceType.IsAssignableFrom(e.TypeChanged))
        {
            _currentVerbs = null;
        }
    }

    /// <summary>
    ///  This is called by the selection service when the selection has changed. Here
    ///  we invalidate our verb list.
    /// </summary>
    private void OnSelectionChanging(object? sender, EventArgs e)
    {
        if (_currentVerbs is not null)
        {
            _currentVerbs = null;
            OnCommandsChanged(new MenuCommandsChangedEventArgs(MenuCommandsChangedType.CommandChanged, null));
        }
    }

    /// <summary>
    ///  Removes the given menu command from the document.
    /// </summary>
    public virtual void RemoveCommand(MenuCommand command)
    {
        ArgumentNullException.ThrowIfNull(command);

        lock (_commandGroupsLock)
        {
            if (_commandGroups.TryGetValue(command.CommandID!.Guid, out List<MenuCommand>? commands))
            {
                if (commands.Remove(command))
                {
                    // If there are no more commands in this command group, remove the group
                    if (commands.Count == 0)
                    {
                        _commandGroups.Remove(command.CommandID.Guid);
                    }

                    command.CommandChanged -= _commandChangedHandler;

                    OnCommandsChanged(new MenuCommandsChangedEventArgs(MenuCommandsChangedType.CommandRemoved, command));
                }

                return;
            }
        }
    }

    /// <summary>
    ///  Removes the given verb from the document.
    /// </summary>
    public virtual void RemoveVerb(DesignerVerb verb)
    {
        ArgumentNullException.ThrowIfNull(verb);

        if (_globalVerbs is not null)
        {
            if (_globalVerbs.Remove(verb))
            {
                EnsureVerbs();
                if (((IMenuCommandService)this).Verbs.Contains(verb))
                {
                    ((IMenuCommandService)this).Verbs.Remove(verb);
                }

                OnCommandsChanged(new MenuCommandsChangedEventArgs(MenuCommandsChangedType.CommandRemoved, verb));
            }
        }
    }

    /// <summary>
    ///  Shows the context menu with the given command ID at the given location.
    /// </summary>
    public virtual void ShowContextMenu(CommandID menuID, int x, int y)
    {
    }
}
