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

namespace System.Windows.Forms;

/// <summary>
///  Represents an expander button and the associated expanded area
///  of a task dialog.
/// </summary>
public sealed class TaskDialogExpander : TaskDialogControl
{
    private string? _text;
    private string? _expandedButtonText;
    private string? _collapsedButtonText;
    private TaskDialogExpanderPosition _expanderPosition;
    private bool _expanded;
    private bool _updateTextOnInitialization;

    /// <summary>
    ///  Occurs when the value of the <see cref="Expanded"/> property changes while
    ///  this control is shown in a task dialog.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   This event will only occur when the expanded state is changed by the user,
    ///   because it isn't possible to programmatically change the <see cref="Expanded"/>
    ///   property while this control is shown in a task dialog.
    ///  </para>
    /// </remarks>
    public event EventHandler? ExpandedChanged;

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

    /// <summary>
    ///  Initializes a new instance of the <see cref="TaskDialogExpander"/> class
    ///  using the given text.
    /// </summary>
    /// <param name="text">The text to be displayed in the dialog's expanded area.</param>
    public TaskDialogExpander(string? text)
        : this()
    {
        _text = text;
    }

    /// <summary>
    ///  Gets or sets the text to be displayed in the dialog's expanded area.
    /// </summary>
    /// <value>
    ///  The text to be displayed in the dialog's expanded area. The default value is
    ///  <see langword="null"/>.
    /// </value>
    /// <remarks>
    ///  <para>
    ///   This control will only be shown if this property is not <see langword="null"/> or an empty string.
    ///  </para>
    ///  <para>
    ///   This property can be set while the dialog is shown.</para>
    ///  </remarks>
    ///  <exception cref="InvalidOperationException">
    ///   The property is set on an expander instance that is currently bound to a task dialog, but it's not visible as its initial
    ///   <see cref="Text"/> property value was <see langword="null"/> or an empty string.
    ///   - or -
    ///   The property is set on an expander instance that is currently bound to a task dialog, but the dialog
    ///   has just started navigating to a different page.
    ///  </exception>
    public string? Text
    {
        get => _text;
        set
        {
            DenyIfBoundAndNotCreated();

            if (BoundPage is not null)
            {
                // If we are bound but waiting for initialization (e.g. immediately after
                // starting a navigation), we buffer the change until we apply the
                // initialization (when navigation is finished).
                if (BoundPage.WaitingForInitialization)
                {
                    _updateTextOnInitialization = true;
                }
                else
                {
                    BoundPage.BoundDialog!.UpdateTextElement(
                        TASKDIALOG_ELEMENTS.TDE_EXPANDED_INFORMATION, value);
                }
            }

            _text = value;
        }
    }

    /// <summary>
    ///  Gets or sets the text to be displayed in the expander button when it
    ///  is in the expanded state.
    /// </summary>
    /// <value>
    ///  The text that is to be displayed in the expander button when it
    ///  is in the expanded state, or <see langword="null"/> or an empty string to use a
    ///  text provided by the operating system. The default value is <see langword="null"/>.
    /// </value>
    /// <exception cref="InvalidOperationException">
    ///  The property is set and this expander instance is currently bound to a task dialog.
    /// </exception>
    public string? ExpandedButtonText
    {
        get => _expandedButtonText;
        set
        {
            DenyIfBound();

            _expandedButtonText = value;
        }
    }

    /// <summary>
    ///  Gets or sets the text to be displayed in the expander button when it
    ///  is in the collapsed state.
    /// </summary>
    /// <value>
    ///  The text that is to be displayed in the expander button when it
    ///  is in the collapsed state, or <see langword="null"/> or an empty string to use a
    ///  text provided by the operating system. The default value is <see langword="null"/>.
    /// </value>
    /// <exception cref="InvalidOperationException">
    ///  The property is set and this expander instance is currently bound to a task dialog.
    /// </exception>
    public string? CollapsedButtonText
    {
        get => _collapsedButtonText;
        set
        {
            DenyIfBound();

            _collapsedButtonText = value;
        }
    }

    /// <summary>
    ///  Gets or sets a value that indicates whether the expander button is in the
    ///  expanded state (so that the dialog's expanded area is visible).
    /// </summary>
    /// <value>
    ///  <see langword="true"/> if the expander button is in the expanded state; <see langword="false"/> if
    ///  it's in the collapsed state. The default value is <see langword="false"/>.
    /// </value>
    /// <exception cref="InvalidOperationException">
    ///  The property is set and this expander instance is currently bound to a task dialog.
    /// </exception>
    public bool Expanded
    {
        get => _expanded;
        set
        {
            // The Task Dialog doesn't provide a message type to click the expando
            // button, so we don't allow to change this property (it will however
            // be updated when we receive an ExpandoButtonClicked notification).
            // TODO: Should we throw only if the new value is different than the
            // old one?
            DenyIfBound();

            _expanded = value;
        }
    }

    /// <summary>
    ///  Gets or sets the <see cref="TaskDialogExpanderPosition"/> that specifies where
    ///  the expanded area of the task dialog is to be displayed.
    /// </summary>
    /// <value>
    ///  The <see cref="TaskDialogExpanderPosition"/> that specifies where the expanded area
    ///  of the task dialog is to be displayed. The default is
    ///  <see cref="TaskDialogExpanderPosition.AfterText"/>.
    /// </value>
    /// <exception cref="InvalidOperationException">
    ///  The property is set and this expander instance is currently bound to a task dialog.
    /// </exception>
    public TaskDialogExpanderPosition Position
    {
        get => _expanderPosition;
        set
        {
            SourceGenerated.EnumValidator.Validate(value);

            DenyIfBound();

            _expanderPosition = value;
        }
    }

    internal override bool IsCreatable => base.IsCreatable && !TaskDialogPage.IsNativeStringNullOrEmpty(_text);

    /// <summary>
    ///  Returns a string that represents the current <see cref="TaskDialogExpander"/> control.
    /// </summary>
    /// <returns>A string that contains the control text.</returns>
    public override string ToString() => _text ?? base.ToString() ?? string.Empty;

    internal void HandleExpandoButtonClicked(bool expanded)
    {
        _expanded = expanded;
        OnExpandedChanged(EventArgs.Empty);
    }

    private protected override TASKDIALOG_FLAGS BindCore()
    {
        TASKDIALOG_FLAGS flags = base.BindCore();

        _updateTextOnInitialization = false;

        if (_expanded)
        {
            flags |= TASKDIALOG_FLAGS.TDF_EXPANDED_BY_DEFAULT;
        }

        if (_expanderPosition == TaskDialogExpanderPosition.AfterFootnote)
        {
            flags |= TASKDIALOG_FLAGS.TDF_EXPAND_FOOTER_AREA;
        }

        return flags;
    }

    private protected override void ApplyInitializationCore()
    {
        base.ApplyInitializationCore();

        if (_updateTextOnInitialization)
        {
            Text = _text;
            _updateTextOnInitialization = false;
        }
    }

    private void OnExpandedChanged(EventArgs e) => ExpandedChanged?.Invoke(this, e);
}
