// 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.Drawing.Design;

namespace System.Windows.Forms;

/// <summary>
///  Provides pop-up or online Help for controls.
/// </summary>
[ProvideProperty("HelpString", typeof(Control))]
[ProvideProperty("HelpKeyword", typeof(Control))]
[ProvideProperty("HelpNavigator", typeof(Control))]
[ProvideProperty("ShowHelp", typeof(Control))]
[ToolboxItemFilter("System.Windows.Forms")]
[SRDescription(nameof(SR.DescriptionHelpProvider))]
public class HelpProvider : Component, IExtenderProvider
{
    private readonly Dictionary<Control, string?> _helpStrings = [];
    private readonly Dictionary<Control, bool> _showHelp = [];
    private readonly List<Control> _boundControls = [];
    private readonly Dictionary<Control, string?> _keywords = [];
    private readonly Dictionary<Control, HelpNavigator> _navigators = [];

    /// <summary>
    ///  Initializes a new instance of the <see cref="HelpProvider"/> class.
    /// </summary>
    public HelpProvider()
    {
    }

    /// <summary>
    ///  Gets or sets a string indicating the name of the Help file associated with this
    ///  <see cref="HelpProvider"/> object.
    /// </summary>
    [Localizable(true)]
    [DefaultValue(null)]
    [Editor($"System.Windows.Forms.Design.HelpNamespaceEditor, {Assemblies.SystemDesign}", typeof(UITypeEditor))]
    [SRDescription(nameof(SR.HelpProviderHelpNamespaceDescr))]
    public virtual string? HelpNamespace { get; set; }

    [SRCategory(nameof(SR.CatData))]
    [Localizable(false)]
    [Bindable(true)]
    [SRDescription(nameof(SR.ControlTagDescr))]
    [DefaultValue(null)]
    [TypeConverter(typeof(StringConverter))]
    public object? Tag { get; set; }

    /// <summary>
    ///  Determines if the help provider can offer it's extender properties to the specified target
    ///  object.
    /// </summary>
    public virtual bool CanExtend(object? target) => target is Control;

    /// <summary>
    ///  Retrieves the Help Keyword displayed when the user invokes Help for the specified control.
    /// </summary>
    [DefaultValue(null)]
    [Localizable(true)]
    [SRDescription(nameof(SR.HelpProviderHelpKeywordDescr))]
    public virtual string? GetHelpKeyword(Control ctl)
    {
        ArgumentNullException.ThrowIfNull(ctl);
        return _keywords.TryGetValue(ctl, out string? value) ? value : null;
    }

    /// <summary>
    ///  Retrieves the contents of the pop-up help window for the specified control.
    /// </summary>
    [DefaultValue(HelpNavigator.AssociateIndex)]
    [Localizable(true)]
    [SRDescription(nameof(SR.HelpProviderNavigatorDescr))]
    public virtual HelpNavigator GetHelpNavigator(Control ctl)
    {
        ArgumentNullException.ThrowIfNull(ctl);
        return _navigators.TryGetValue(ctl, out HelpNavigator value) ? value : HelpNavigator.AssociateIndex;
    }

    /// <summary>
    ///  Retrieves the contents of the pop-up help window for the specified control.
    /// </summary>
    [DefaultValue(null)]
    [Localizable(true)]
    [SRDescription(nameof(SR.HelpProviderHelpStringDescr))]
    public virtual string? GetHelpString(Control ctl)
    {
        ArgumentNullException.ThrowIfNull(ctl);
        return _helpStrings.TryGetValue(ctl, out string? value) ? value : null;
    }

    /// <summary>
    ///  Retrieves a value indicating whether Help displays for the specified control.
    /// </summary>
    [Localizable(true)]
    [SRDescription(nameof(SR.HelpProviderShowHelpDescr))]
    public virtual bool GetShowHelp(Control ctl)
    {
        ArgumentNullException.ThrowIfNull(ctl);
        return _showHelp.TryGetValue(ctl, out bool value) && value;
    }

    /// <summary>
    ///  Handles the help event for any bound controls.
    /// </summary>
    private void OnControlHelp(object? sender, HelpEventArgs hevent)
    {
        if (sender is not Control ctl)
        {
            return;
        }

        string? helpString = GetHelpString(ctl);
        string? keyword = GetHelpKeyword(ctl);
        HelpNavigator navigator = GetHelpNavigator(ctl);

        if (!GetShowHelp(ctl) || hevent is null)
        {
            return;
        }

        if (Control.MouseButtons != MouseButtons.None && !string.IsNullOrEmpty(helpString))
        {
            Help.ShowPopup(ctl, helpString, hevent.MousePos);
            hevent.Handled = true;
            return;
        }

        // If we have a help file, and help keyword we try F1 help next
        if (HelpNamespace is not null)
        {
            if (!string.IsNullOrEmpty(keyword))
            {
                Help.ShowHelp(ctl, HelpNamespace, navigator, keyword);
            }
            else
            {
                Help.ShowHelp(ctl, HelpNamespace, navigator);
            }

            hevent.Handled = true;
            return;
        }

        // So at this point we don't have a help keyword, so try to display the whats this help
        if (!string.IsNullOrEmpty(helpString))
        {
            Help.ShowPopup(ctl, helpString, hevent.MousePos);
            hevent.Handled = true;
        }
    }

    /// <summary>
    ///  Handles the help event for any bound controls.
    /// </summary>
    private void OnQueryAccessibilityHelp(object? sender, QueryAccessibilityHelpEventArgs e)
    {
        if (sender is not Control ctl)
        {
            return;
        }

        e.HelpString = GetHelpString(ctl);
        e.HelpKeyword = GetHelpKeyword(ctl);
        e.HelpNamespace = HelpNamespace;
    }

    /// <summary>
    ///  Specifies a Help string associated with a control.
    /// </summary>
    public virtual void SetHelpString(Control ctl, string? helpString)
    {
        ArgumentNullException.ThrowIfNull(ctl);

        _helpStrings[ctl] = helpString;
        if (!string.IsNullOrEmpty(helpString))
        {
            SetShowHelp(ctl, true);
        }

        UpdateEventBinding(ctl);
    }

    /// <summary>
    ///  Specifies the Help keyword to display when the user invokes Help for a control.
    /// </summary>
    public virtual void SetHelpKeyword(Control ctl, string? keyword)
    {
        ArgumentNullException.ThrowIfNull(ctl);

        _keywords[ctl] = keyword;
        if (!string.IsNullOrEmpty(keyword))
        {
            SetShowHelp(ctl, true);
        }

        UpdateEventBinding(ctl);
    }

    /// <summary>
    ///  Specifies the Help keyword to display when the user invokes Help for a control.
    /// </summary>
    public virtual void SetHelpNavigator(Control ctl, HelpNavigator navigator)
    {
        ArgumentNullException.ThrowIfNull(ctl);

        SourceGenerated.EnumValidator.Validate(navigator, nameof(navigator));

        _navigators[ctl] = navigator;
        SetShowHelp(ctl, true);
        UpdateEventBinding(ctl);
    }

    /// <summary>
    ///  Specifies whether Help is displayed for a given control.
    /// </summary>
    public virtual void SetShowHelp(Control ctl, bool value)
    {
        ArgumentNullException.ThrowIfNull(ctl);

        _showHelp[ctl] = value;
        UpdateEventBinding(ctl);
    }

    /// <summary>
    ///  Used by the designer
    /// </summary>
    internal bool ShouldSerializeShowHelp(Control ctl)
    {
        ArgumentNullException.ThrowIfNull(ctl);

        return _showHelp.ContainsKey(ctl);
    }

    /// <summary>
    ///  Used by the designer
    /// </summary>
    public virtual void ResetShowHelp(Control ctl)
    {
        ArgumentNullException.ThrowIfNull(ctl);

        _showHelp.Remove(ctl);
    }

    /// <summary>
    ///  Binds/unbinds event handlers to ctl
    /// </summary>
    private void UpdateEventBinding(Control ctl)
    {
        bool showHelp = GetShowHelp(ctl);
        bool isBound = _boundControls.Contains(ctl);
        if (showHelp && !isBound)
        {
            ctl.HelpRequested += OnControlHelp;
            ctl.QueryAccessibilityHelp += OnQueryAccessibilityHelp;
            _boundControls.Add(ctl);
        }
        else if (!showHelp && isBound)
        {
            ctl.HelpRequested -= OnControlHelp;
            ctl.QueryAccessibilityHelp -= OnQueryAccessibilityHelp;
            _boundControls.Remove(ctl);
        }
    }

    /// <summary>
    ///  Returns a string representation for this control.
    /// </summary>
    public override string ToString() => $"{base.ToString()}, HelpNamespace: {HelpNamespace}";
}
