// 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.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using TASKDIALOGCONFIG_FooterIcon = Windows.Win32.UI.Controls.TASKDIALOGCONFIG._Anonymous2_e__Union;
using TASKDIALOGCONFIG_MainIcon = Windows.Win32.UI.Controls.TASKDIALOGCONFIG._Anonymous1_e__Union;
namespace System.Windows.Forms;

/// <summary>
///  A task dialog allows to display information and get simple input from the user. It is similar
///  to a <see cref="MessageBox"/> (in that it is formatted by the operating system) but provides
///  a lot more features.
/// </summary>
/// <remarks>
///  <para>
///   For more information, see
///   <see href="https://docs.microsoft.com/windows/desktop/Controls/task-dialogs-overview">
///    About Task Dialogs.
///   </see>
///  </para>
///  <para>
///   Note: In order to use the dialog, you need ensure <see cref="Application.EnableVisualStyles"/>
///   has been called before showing the dialog, or the application needs to be compiled with a
///   manifest that contains a dependency to Microsoft.Windows.Common-Controls (6.0.0.0).
///   Additionally, the current thread should use the single-threaded apartment (STA) model.
///  </para>
/// </remarks>
public partial class TaskDialog : IWin32Window
{
    /// <summary>
    ///  A self-defined window message that we post to the task dialog when
    ///  handling a <see cref="TASKDIALOG_NOTIFICATIONS.TDN_BUTTON_CLICKED"/>
    ///  notification, so that we will ignore further
    ///  <see cref="TASKDIALOG_NOTIFICATIONS.TDN_BUTTON_CLICKED"/> notifications
    ///  until we process the posted message.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   This is used to work-around a bug in the native task dialog, where
    ///   a <see cref="TASKDIALOG_NOTIFICATIONS.TDN_BUTTON_CLICKED"/> notification
    ///   seems to be sent twice to the callback when you "click" a button by
    ///   pressing its access key (mnemonic) and the dialog is still open when
    ///   continuing the message loop.
    ///  </para>
    ///  <para>
    ///   This work-around should not have negative effects, such as erroneously
    ///   ignoring a valid button clicked notification when the user presses the
    ///   button multiple times while the GUI thread is hangs - this seems
    ///   to work correctly, as our posted message will be processed before
    ///   further (valid) <see cref="TASKDIALOG_NOTIFICATIONS.TDN_BUTTON_CLICKED"/>
    ///   notifications are processed.
    ///  </para>
    ///  <para>
    ///   See documentation/repro in
    ///   /docs/src/System/Windows/Forms/TaskDialog/Issue_ButtonClickHandlerCalledTwice.md
    ///  </para>
    ///  <para>
    ///   Note: We use a WM_APP message with a high value (WM_USER is not
    ///   appropriate as it is private to the control class), in order to avoid
    ///   conflicts with WM_APP messages which other parts of the application
    ///   might want to send when they also subclassed the dialog window, although
    ///   that should be unlikely.
    ///  </para>
    /// </remarks>
    private const uint ContinueButtonClickHandlingMessage = PInvokeCore.WM_APP + 0x3FFF;

    private TaskDialogPage? _boundPage;

    /// <summary>
    ///  A queue of <see cref="TaskDialogPage"/>s that have been bound by
    ///  navigating the dialog, but don't yet reflect the state of the
    ///  native dialog because the corresponding
    ///  <see cref="TASKDIALOG_NOTIFICATIONS.TDN_NAVIGATED"/> notification was
    ///  not yet received.
    /// </summary>
    private readonly Queue<TaskDialogPage> _waitingNavigationPages = new();

    /// <summary>
    ///  The <see cref="IntPtr"/> of a <see cref="GCHandle"/> that represents this
    ///  <see cref="TaskDialog"/> instance.
    /// </summary>
    private IntPtr _instanceHandlePtr;

    private HWND _handle;

    private WindowSubclassHandler? _windowSubclassHandler;

    /// <summary>
    ///  Stores a value that indicates if the
    ///  <see cref="TaskDialogPage.Created"/> event has been called for the
    ///  current <see cref="TaskDialogPage"/> and so the corresponding
    ///  <see cref="TaskDialogPage.Destroyed"/> can be called later.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   This is used to prevent raising the
    ///   <see cref="TaskDialogPage.Destroyed"/> event without raising the
    ///   <see cref="TaskDialogPage.Created"/> event first (e.g. if navigation
    ///   fails).
    ///  </para>
    /// </remarks>
    private bool _raisedPageCreated;

    /// <summary>
    ///  A counter which is used to determine whether the dialog has been navigated
    ///  while being in a <see cref="TASKDIALOG_NOTIFICATIONS.TDN_BUTTON_CLICKED"/> handler.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   When the dialog navigates within a ButtonClicked handler, the handler should
    ///   always return <see cref="HRESULT.S_FALSE"/> to prevent the dialog from applying the
    ///   button that raised the handler as dialog result. Otherwise, this can lead to memory
    ///   access problems like <see cref="AccessViolationException"/>s, especially if the
    ///   previous dialog page had radio buttons (but the new ones do not).
    ///  </para>
    ///  <para>
    ///   See the comment in <see cref="HandleTaskDialogCallback"/> for more
    ///   information.
    ///  </para>
    ///  <para>
    ///   When the dialog navigates, it sets the <c>navigationIndex</c> to the current
    ///   <c>stackCount</c> value, so that the ButtonClicked handler can determine
    ///   if the dialog has been navigated after it was called.
    ///   Tracking the stack count and navigation index is necessary as there
    ///   can be multiple ButtonClicked handlers on the call stack, for example
    ///   if a ButtonClicked handler runs the message loop so that new click events
    ///   can be processed.
    ///  </para>
    /// </remarks>
    private (int stackCount, int navigationIndex) _buttonClickNavigationCounter;

    /// <summary>
    ///  The button designated as the dialog result by the handler for the
    ///  <see cref="TASKDIALOG_NOTIFICATIONS.TDN_BUTTON_CLICKED"/>
    ///  notification.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   This will be set the first time the
    ///   <see cref="TASKDIALOG_NOTIFICATIONS.TDN_BUTTON_CLICKED"/> handler returns
    ///   <see cref="HRESULT.S_OK"/> to cache the button instance,
    ///   so that <see cref="ShowDialog(IntPtr, TaskDialogPage, TaskDialogStartupLocation)"/> can then return it.
    ///  </para>
    ///  <para>
    ///   Additionally, this is used to check if there was already a
    ///   <see cref="TASKDIALOG_NOTIFICATIONS.TDN_BUTTON_CLICKED"/> handler that
    ///   returned <see cref="HRESULT.S_OK"/>, so that further
    ///   handles will return <see cref="HRESULT.S_FALSE"/> to
    ///   not override the previously set result.
    ///  </para>
    /// </remarks>
    private (TaskDialogButton button, int buttonID)? _resultButton;

    private bool _suppressButtonClickedEvent;

    /// <summary>
    ///  Specifies if the current code is called from within
    ///  <see cref="Navigate(TaskDialogPage)"/>.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   This is used to detect if you call <see cref="Navigate(TaskDialogPage)"/>
    ///   from within an event raised by <see cref="Navigate(TaskDialogPage)"/>,
    ///   which is not supported.
    ///  </para>
    /// </remarks>
    private bool _isInNavigate;

    /// <summary>
    ///  Specifies if the <see cref="HandleTaskDialogCallback"/> method should
    ///  currently ignore <see cref="TASKDIALOG_NOTIFICATIONS.TDN_BUTTON_CLICKED"/>
    ///  notifications.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   See <see cref="ContinueButtonClickHandlingMessage"/> for more information.
    ///  </para>
    /// </remarks>
    private bool _ignoreButtonClickedNotifications;

    /// <summary>
    ///  Specifies if the current <see cref="TASKDIALOG_NOTIFICATIONS.TDN_BUTTON_CLICKED"/> notification
    ///  is caused by our own code sending a <see cref="TASKDIALOG_MESSAGES.TDM_CLICK_BUTTON"/> message.
    /// </summary>
    private bool _buttonClickedProgrammatically;

    /// <summary>
    ///  Specifies if the currently showing task dialog already received the
    ///  <see cref="TASKDIALOG_NOTIFICATIONS.TDN_DESTROYED"/> notification.
    /// </summary>
    private bool _receivedDestroyedNotification;

    /// <summary>
    ///  Initializes a new instance of the <see cref="TaskDialog"/> class using the
    ///  specified task dialog page.
    /// </summary>
    private TaskDialog()
    {
    }

    /// <summary>
    ///  Gets the window handle of the task dialog window, or <see cref="IntPtr.Zero"/>
    ///  if the dialog is currently not being shown.
    /// </summary>
    public IntPtr Handle
    {
        get
        {
            // Check that the current thread is the same than the one which created the window,
            // similar to the check in Control.Handle. While the task dialog is showing, it must
            // only be accessed from the original thread because the implementation is not
            // thread-safe.
            if (Control.CheckForIllegalCrossThreadCalls && InvokeRequired)
            {
                throw new InvalidOperationException(string.Format(SR.IllegalCrossThreadCall, nameof(TaskDialog)));
            }

            return _handle;
        }
    }

    /// <summary>
    ///  Gets a value that indicates whether <see cref="ShowDialog(IntPtr, TaskDialogPage, TaskDialogStartupLocation)"/> is
    ///  currently being called.
    /// </summary>
    internal bool IsShown => _instanceHandlePtr != IntPtr.Zero;

    /// <summary>
    ///  Gets a value that indicates whether the native task dialog window has
    ///  been created and its handle is available using the <see cref="Handle"/>
    ///  property.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   This property can only be <see langword="true"/> if <see cref="IsShown"/> is
    ///   also <see langword="true"/>. However, normally this property should be equivalent
    ///   to <see cref="IsShown"/>, because when showing the dialog, the
    ///   callback should have been called setting the handle.
    ///  </para>
    /// </remarks>
    internal bool IsHandleCreated => _handle != IntPtr.Zero;

    internal bool InvokeRequired => IsHandleCreated &&
        PInvokeCore.GetWindowThreadProcessId(_handle, out _) != PInvokeCore.GetCurrentThreadId();

    /// <summary>
    ///  Gets or sets the current count of stack frames that are in the
    ///  <see cref="TaskDialogRadioButton.CheckedChanged"/> event for the
    ///  current task dialog.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   This is used by the <see cref="TaskDialogRadioButton.Checked"/> setter
    ///   so that it can disallow the change when the count is greater than zero.
    ///   Additionally, it is used to deny navigation of the task dialog in that
    ///   case.
    ///  </para>
    /// </remarks>
    internal int RadioButtonClickedStackCount { get; set; }

    private static void FreeConfig(IntPtr ptrToFree) => Marshal.FreeHGlobal(ptrToFree);

    // remove suppression if issue resolves https://github.com/dotnet/roslyn/issues/68526
#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant
    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
#pragma warning restore CS3016
    private static unsafe HRESULT HandleTaskDialogNativeCallback(
        HWND hwnd,
        uint msg,
        WPARAM wParam,
        LPARAM lParam,
        nint lpRefData) =>
        // Call the instance method by dereferencing the GC handle.
        (((GCHandle)lpRefData).Target as TaskDialog)!.HandleTaskDialogCallback(
            hwnd,
            (TASKDIALOG_NOTIFICATIONS)msg,
            wParam,
            lParam);

    private static bool IsTaskDialogButtonCommitting(TaskDialogButton? button)
    {
        // All custom button as well as all standard buttons except for the
        // "Help" button (if it is shown in the dialog) will close the
        // dialog. If the "Help" button is not shown in the task dialog,
        // "button" will be null or its "IsCreated" property returns false.
        // In that case the "Help" button would close the dialog, so we
        // return true.
        return !(button?.IsCreated == true && button.IsStandardButton &&
            button.StandardButtonResult == TaskDialogResult.Help);
    }

    private static TaskDialogButton CreatePlaceholderButton(TaskDialogResult result)
    {
        return new TaskDialogButton(result)
        {
            Visible = false
        };
    }

    /// <summary>
    ///  Shows the task dialog with the specified owner asynchronously.
    /// </summary>
    /// <param name="page">
    ///  The page instance that contains the contents which this task dialog will display.
    /// </param>
    /// <param name="startupLocation">
    ///  Gets or sets the position of the task dialog when it is shown.
    /// </param>
    /// <remarks>
    ///  <para>
    ///   Showing the dialog will bind the <paramref name="page"/> and its controls until
    ///   this method returns or the dialog is navigated to a different page.
    ///  </para>
    /// </remarks>
    /// <returns>
    ///  The <see cref="TaskDialogButton"/> which was clicked by the user to close the dialog.
    /// </returns>
    /// <exception cref="ArgumentNullException">
    ///  <paramref name="page"/> is <see langword="null"/>.
    /// </exception>
    /// <exception cref="InvalidOperationException">
    ///  The specified <paramref name="page"/> contains an invalid configuration.
    /// </exception>
    public static Task<TaskDialogButton> ShowDialogAsync(
        TaskDialogPage page,
        TaskDialogStartupLocation startupLocation = TaskDialogStartupLocation.CenterScreen)
            => ShowDialogAsync(IntPtr.Zero, page.OrThrowIfNull(), startupLocation);

    /// <summary>
    ///  Shows the task dialog with the specified owner asynchronously.
    /// </summary>
    /// <param name="page">
    ///  The page instance that contains the contents which this task dialog will display.
    /// </param>
    /// <param name="owner">
    ///  The owner window.
    /// </param>
    /// <param name="startupLocation">
    ///  Gets or sets the position of the task dialog when it is shown.
    /// </param>
    /// <remarks>
    ///  <para>
    ///   Showing the dialog will bind the <paramref name="page"/> and its controls until
    ///   this method returns or the dialog is navigated to a different page.
    ///  </para>
    /// </remarks>
    /// <returns>
    ///  The <see cref="TaskDialogButton"/> which was clicked by the user to close the dialog.
    /// </returns>
    /// <exception cref="ArgumentNullException">
    ///  <paramref name="page"/> is <see langword="null"/>.
    /// </exception>
    /// <exception cref="InvalidOperationException">
    ///  The specified <paramref name="page"/> contains an invalid configuration.
    /// </exception>
    public static Task<TaskDialogButton> ShowDialogAsync(
        IWin32Window owner,
        TaskDialogPage page,
        TaskDialogStartupLocation startupLocation = TaskDialogStartupLocation.CenterOwner) =>
            ShowDialogAsync(owner.Handle, page, startupLocation);

    /// <summary>
    ///  Shows the task dialog with the specified owner asynchronously.
    /// </summary>
    /// <param name="page">
    ///  The page instance that contains the contents which this task dialog will display.
    /// </param>
    /// <param name="hwndOwner">
    ///  The handle of the owner window, or <see cref="IntPtr.Zero"/> to show a modeless dialog.
    /// </param>
    /// <param name="startupLocation">
    ///  Gets or sets the position of the task dialog when it is shown.
    /// </param>
    /// <remarks>
    ///  <para>
    ///   Showing the dialog will bind the <paramref name="page"/> and its controls until
    ///   this method returns or the dialog is navigated to a different page.
    ///  </para>
    /// </remarks>
    /// <returns>
    ///  The <see cref="TaskDialogButton"/> which was clicked by the user to close the dialog.
    /// </returns>
    /// <exception cref="ArgumentNullException">
    ///  <paramref name="page"/> is <see langword="null"/>.
    /// </exception>
    /// <exception cref="InvalidOperationException">
    ///  The specified <paramref name="page"/> contains an invalid configuration.
    /// </exception>
    public static async Task<TaskDialogButton> ShowDialogAsync(
        nint hwndOwner,
        TaskDialogPage page,
        TaskDialogStartupLocation startupLocation = TaskDialogStartupLocation.CenterOwner)
    {
        ArgumentNullException.ThrowIfNull(page);

        var completion = new TaskCompletionSource<TaskDialogButton>();

        TaskDialog? dialog = null;

        if (SynchronizationContext.Current is null)
        {
            WindowsFormsSynchronizationContext.InstallIfNeeded();
        }

        var syncContext = SynchronizationContext.Current
            ?? throw new InvalidOperationException(SR.FormOrTaskDialog_NoSyncContextForShowAsync);

        syncContext.Post(_ => ShowDialogProc(), null);

        TaskDialogButton result;

        result = await completion.Task.ConfigureAwait(true);
        return result;

        void ShowDialogProc()
        {
            try
            {
                dialog = new();
                completion.TrySetResult(dialog.ShowDialogInternal(hwndOwner, page, startupLocation));
            }
            catch (Exception ex)
            {
                completion.TrySetException(ex);
            }
        }
    }

    /// <summary>
    ///  Shows the task dialog.
    /// </summary>
    /// <param name="page">
    ///  The page instance that contains the contents which this task dialog will display.
    /// </param>
    /// <param name="startupLocation">
    ///  Gets or sets the position of the task dialog when it is shown.
    /// </param>
    /// <remarks>
    ///  <para>
    ///   Showing the dialog will bind the <paramref name="page"/> and its controls until
    ///   this method returns or the dialog is navigated to a different page.
    ///  </para>
    /// </remarks>
    /// <returns>
    ///  The <see cref="TaskDialogButton"/> which was clicked by the user to close the dialog.
    /// </returns>
    /// <exception cref="ArgumentNullException">
    ///  <paramref name="page"/> is <see langword="null"/>.
    /// </exception>
    /// <exception cref="InvalidOperationException">
    ///  The specified <paramref name="page"/> contains an invalid configuration.
    /// </exception>
    public static TaskDialogButton ShowDialog(
        TaskDialogPage page,
        TaskDialogStartupLocation startupLocation = TaskDialogStartupLocation.CenterOwner)
        => ShowDialog(IntPtr.Zero, page.OrThrowIfNull(), startupLocation);

    /// <summary>
    ///  Shows the task dialog with the specified owner.
    /// </summary>
    /// <param name="page">
    ///  The page instance that contains the contents which this task dialog will display.
    /// </param>
    /// <param name="owner">The owner window, or <see langword="null"/> to show a modeless dialog.</param>
    /// <param name="startupLocation">
    ///  Gets or sets the position of the task dialog when it is shown.
    /// </param>
    /// <remarks>
    ///  <para>
    ///   Showing the dialog will bind the <paramref name="page"/> and its controls until
    ///   this method returns or the dialog is navigated to a different page.
    ///  </para>
    /// </remarks>
    /// <returns>
    ///  The <see cref="TaskDialogButton"/> which was clicked by the user to close the dialog.
    /// </returns>
    /// <exception cref="ArgumentNullException">
    ///  <paramref name="owner"/> is <see langword="null"/>
    ///  - or -
    ///  <paramref name="page"/> is <see langword="null"/>.
    /// </exception>
    /// <exception cref="InvalidOperationException">
    ///  The specified <paramref name="page"/> contains an invalid configuration.
    /// </exception>
    public static TaskDialogButton ShowDialog(
        IWin32Window owner,
        TaskDialogPage page,
        TaskDialogStartupLocation startupLocation = TaskDialogStartupLocation.CenterOwner)
        => ShowDialog(owner.OrThrowIfNull().Handle, page.OrThrowIfNull(), startupLocation);

    /// <summary>
    ///  Shows the task dialog with the specified owner.
    /// </summary>
    /// <param name="page">
    ///  The page instance that contains the contents which this task dialog will display.
    /// </param>
    /// <param name="hwndOwner">
    ///  The handle of the owner window, or <see cref="IntPtr.Zero"/> to show a
    ///  modeless dialog.
    /// </param>
    /// <param name="startupLocation">
    ///  Gets or sets the position of the task dialog when it is shown.
    /// </param>
    /// <remarks>
    ///  <para>
    ///   Showing the dialog will bind the <paramref name="page"/> and its controls until
    ///   this method returns or the dialog is navigated to a different page.
    ///  </para>
    /// </remarks>
    /// <returns>
    ///  The <see cref="TaskDialogButton"/> which was clicked by the user to close the dialog.
    /// </returns>
    /// <exception cref="ArgumentNullException">
    ///  <paramref name="page"/> is <see langword="null"/>.
    /// </exception>
    /// <exception cref="InvalidOperationException">
    ///  The specified <paramref name="page"/> contains an invalid configuration.
    /// </exception>
    public static unsafe TaskDialogButton ShowDialog(
        IntPtr hwndOwner,
        TaskDialogPage page,
        TaskDialogStartupLocation startupLocation = TaskDialogStartupLocation.CenterOwner)
    {
        ArgumentNullException.ThrowIfNull(page);

        TaskDialog dialog = new();
        return dialog.ShowDialogInternal(hwndOwner, page, startupLocation);
    }

    /// <summary>
    ///  Shows the task dialog with the specified owner.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   Showing the dialog will bind the <paramref name="page"/> and its controls until
    ///   this method returns or the dialog is navigated to a different page.
    ///  </para>
    /// </remarks>
    /// <returns>
    ///  The <see cref="TaskDialogButton"/> which was clicked by the user to close the dialog.
    /// </returns>
    private unsafe TaskDialogButton ShowDialogInternal(
        IntPtr hwndOwner,
        TaskDialogPage page,
        TaskDialogStartupLocation startupLocation)
    {
        // Recursive Show() is not possible because a TaskDialog instance can only
        // represent a single native dialog.
        if (IsShown)
        {
            throw new InvalidOperationException(string.Format(SR.TaskDialogInstanceAlreadyShown, nameof(TaskDialog)));
        }

        page.Validate();

        // Allocate a GCHandle which we will use for the callback data.
        var instanceHandle = GCHandle.Alloc(this);
        try
        {
            _instanceHandlePtr = (IntPtr)instanceHandle;

            // Bind the page and allocate the memory.
            BindPageAndAllocateConfig(
                page,
                hwndOwner,
                startupLocation,
                out IntPtr ptrToFree,
                out TASKDIALOGCONFIG* ptrTaskDialogConfig);

            _boundPage = page;
            try
            {
                // Note: When an uncaught exception occurs in the callback or a
                // WndProc handler, the CLR will manipulate the managed stack
                // ("unwind") so that it doesn't contain the transition to and
                // from native code. However, the TaskDialog still calls our
                // callback when the message loop continues, but as we already
                // freed the GCHandle, a NRE will occur (or other memory access
                // problems because the callback delegate for the subclassed
                // WndProc might already have been freed).
                //
                // Therefore, we need to catch all exceptions in the
                // native -> managed transition, and when one occurs, call
                // Application.OnThreadException().
                //
                // Note: The same issue can occur when using a message box with
                // WPF or WinForms: If you do MessageBox.Show() wrapped in a
                // try/catch on a button click, and before calling .Show() create
                // and start a timer which stops and throws an exception on its
                // Tick event, the application will crash with an
                // AccessViolationException as soon as you close the MessageBox.

                HRESULT returnValue;
                int resultButtonID;
                int dummyRadioButton;
                BOOL dummyVerificationFlag;
                try
                {
                    // Activate a theming scope so that the task dialog works without having to use an application
                    // manifest that enables common controls v6 (provided that Application.EnableVisualStyles()
                    // was called earlier). Otherwise, the "TaskDialogIndirect" entry point will not be available in
                    // dll.

                    using ThemingScope scope = new(Application.UseVisualStyles);
                    returnValue = PInvoke.TaskDialogIndirect(
                        ptrTaskDialogConfig,
                        &resultButtonID,
                        &dummyRadioButton,
                        &dummyVerificationFlag);
                }
                catch (EntryPointNotFoundException ex)
                {
                    throw new InvalidOperationException(string.Format(
                        SR.TaskDialogVisualStylesNotEnabled,
                        $"{nameof(Application)}.{nameof(Application.EnableVisualStyles)}"),
                        ex);
                }

                // Marshal.ThrowExceptionForHR will use the IErrorInfo on the
                // current thread if it exists.
                returnValue.ThrowOnFailure();

                // Normally, the returned button ID should always equal the cached
                // result button ID. However, in some cases when the dialog is closed
                // abnormally (e.g. when closing the main window while a modeless task
                // dialog is displayed), the dialog returns IDCANCEL (2) without
                // priorly raising the TDN_BUTTON_CLICKED notification.
                // Therefore, in that case we need to retrieve the button ourselves.
                if (resultButtonID == _resultButton?.buttonID)
                {
                    return _resultButton.Value.button;
                }

                return _boundPage.GetBoundButtonByID(resultButtonID) ??
                    CreatePlaceholderButton((TaskDialogResult)resultButtonID);
            }
            finally
            {
                // Free the memory.
                FreeConfig(ptrToFree);

                // The window handle should already have been cleared from the
                // TDN_DESTROYED notification. Otherwise, this would mean that
                // TaskDialogIndirect() returned due to an exception being
                // thrown (which means the native task dialog is still showing),
                // which we should avoid as it is not supported.
                // TODO: Maybe FailFast() in that case to prevent future errors.
                Debug.Assert(_handle == IntPtr.Zero);

                // Ensure to keep the callback delegate alive until
                // TaskDialogIndirect() returns (in case we could not undo the
                // subclassing). See comment in UnsubclassWindow().
                _windowSubclassHandler?.KeepCallbackDelegateAlive();
                // Then, clear the subclass handler. Note that this only works
                // correctly if we did not return from TaskDialogIndirect()
                // due to an exception being thrown (as mentioned above).
                _windowSubclassHandler = null;

                // Also, ensure the window handle and the
                // raiseClosed/raisePageDestroyed flags are is cleared even if
                // the TDN_DESTROYED notification did not occur (although that
                // should only happen when there was an exception).
                _handle = HWND.Null;
                _raisedPageCreated = false;

                // Clear cached objects and other fields.
                _resultButton = null;
                _ignoreButtonClickedNotifications = false;
                _receivedDestroyedNotification = false;

                // Unbind the page. The 'Destroyed' event of the TaskDialogPage
                // will already have been called from the callback.
                _boundPage.Unbind();
                _boundPage = null;

                // If we started navigating the dialog but navigation wasn't
                // successful, we also need to unbind the new pages.
                foreach (TaskDialogPage dialogPage in _waitingNavigationPages)
                {
                    dialogPage.Unbind();
                }

                _waitingNavigationPages.Clear();
            }
        }
        finally
        {
            _instanceHandlePtr = IntPtr.Zero;
            instanceHandle.Free();
        }
    }

    // Messages that can be sent to the dialog while it is being shown.

    /// <summary>
    ///  Closes the shown task dialog with <see cref="TaskDialogButton.Cancel"/> as resulting button.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   To close the dialog with a different result, call the
    ///   <see cref="TaskDialogButton.PerformClick"/> method of the
    ///   <see cref="TaskDialogButton"/> that you want to set as a result.
    ///  </para>
    ///  <para>
    ///   This method can be called while the dialog is waiting for
    ///   navigation to complete, whereas <see cref="TaskDialogButton.PerformClick"/>
    ///   would throw in that case. When calling this method, the
    ///   <see cref="TaskDialogButton.Click"/> event won't be raised.
    ///  </para>
    /// </remarks>
    public void Close()
    {
        _suppressButtonClickedEvent = true;
        try
        {
            // Send a click button message with the cancel result.
            // Note: We allow to click the cancel button even if we are waiting
            // for the TDN_NAVIGATED notification to occur, as in that case the
            // dialog will behave as if it still contains the controls of the
            // previous page, and we can click a standard button without the page
            // actually having to contain that button.
            ClickButton((int)TaskDialogResult.Cancel, false);
        }
        finally
        {
            _suppressButtonClickedEvent = false;
        }
    }

    /// <summary>
    ///  While the dialog is being shown, switches the progress bar mode to either a
    ///  marquee progress bar or to a regular progress bar.
    ///  For a marquee progress bar, you can enable or disable the marquee using
    ///  <see cref="SetProgressBarMarquee(bool, int)"/>.
    /// </summary>
    internal void SwitchProgressBarMode(bool marqueeProgressBar) => SendTaskDialogMessage(
        TASKDIALOG_MESSAGES.TDM_SET_MARQUEE_PROGRESS_BAR,
        (WPARAM)(BOOL)marqueeProgressBar);

    /// <summary>
    ///  While the dialog is being shown, enables or disables progress bar marquee when
    ///  a marquee progress bar is displayed.
    /// </summary>
    /// <param name="animationSpeed">
    ///  The time in milliseconds between marquee animation updates. If <c>0</c>, the
    ///  animation will be updated every 30 milliseconds.
    /// </param>
    internal unsafe void SetProgressBarMarquee(bool enableMarquee, int animationSpeed = 0)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(animationSpeed);

        SendTaskDialogMessage(
            TASKDIALOG_MESSAGES.TDM_SET_PROGRESS_BAR_MARQUEE,
            (WPARAM)(BOOL)enableMarquee,
            (LPARAM)animationSpeed);
    }

    /// <summary>
    ///  While the dialog is being shown, sets the progress bar range.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   The default range is 0 to 100.
    ///  </para>
    /// </remarks>
    internal unsafe void SetProgressBarRange(int min, int max)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(min);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(min, ushort.MaxValue);

        ArgumentOutOfRangeException.ThrowIfNegative(max);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(max, ushort.MaxValue);

        SendTaskDialogMessage(
            TASKDIALOG_MESSAGES.TDM_SET_PROGRESS_BAR_RANGE,
            default,
            LPARAM.MAKELPARAM(min, max));
    }

    /// <summary>
    ///  While the dialog is being shown, sets the progress bar position.
    /// </summary>
    /// <param name="pos"></param>
    internal void SetProgressBarPosition(int pos)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(pos);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(pos, ushort.MaxValue);

        SendTaskDialogMessage(
            TASKDIALOG_MESSAGES.TDM_SET_PROGRESS_BAR_POS,
            (WPARAM)pos);
    }

    /// <summary>
    ///  While the dialog is being shown, sets the progress bar state.
    /// </summary>
    /// <param name="state"></param>
    internal void SetProgressBarState(uint state) => SendTaskDialogMessage(
        TASKDIALOG_MESSAGES.TDM_SET_PROGRESS_BAR_STATE,
        (WPARAM)(int)state);

    /// <summary>
    ///  While the dialog is being shown, sets the checkbox to the specified state.
    /// </summary>
    internal void ClickCheckBox(bool isChecked, bool focus = false) => SendTaskDialogMessage(
        TASKDIALOG_MESSAGES.TDM_CLICK_VERIFICATION,
        (WPARAM)(BOOL)isChecked,
        (LPARAM)(BOOL)focus);

    internal void SetButtonElevationRequiredState(int buttonID, bool requiresElevation) => SendTaskDialogMessage(
        TASKDIALOG_MESSAGES.TDM_SET_BUTTON_ELEVATION_REQUIRED_STATE,
        (WPARAM)buttonID,
        (LPARAM)(BOOL)requiresElevation);

    internal void SetButtonEnabled(int buttonID, bool enable) => SendTaskDialogMessage(
        TASKDIALOG_MESSAGES.TDM_ENABLE_BUTTON,
        (WPARAM)buttonID,
        (LPARAM)(BOOL)enable);

    internal void SetRadioButtonEnabled(int radioButtonID, bool enable) => SendTaskDialogMessage(
        TASKDIALOG_MESSAGES.TDM_ENABLE_RADIO_BUTTON,
        (WPARAM)radioButtonID,
        (LPARAM)(BOOL)enable);

    internal void ClickButton(int buttonID, bool checkWaitingForNavigation = true)
    {
        // Allow the handler of the TDN_BUTTON_CLICKED notification to detect that
        // the notification is caused by ourselves sending a TDM_CLICK_BUTTON message.
        _buttonClickedProgrammatically = true;
        try
        {
            SendTaskDialogMessage(
                TASKDIALOG_MESSAGES.TDM_CLICK_BUTTON,
                (WPARAM)buttonID,
                default,
                checkWaitingForNavigation);
        }
        finally
        {
            // In most cases the flag will already have been set to false in the
            // TDN_BUTTON_CLICKED handler, but we do it again in case the notification
            // wasn't handled for some reason or an exception was thrown.
            _buttonClickedProgrammatically = false;
        }
    }

    internal void ClickRadioButton(int radioButtonID) => SendTaskDialogMessage(
        TASKDIALOG_MESSAGES.TDM_CLICK_RADIO_BUTTON,
        (WPARAM)radioButtonID);

    internal unsafe void UpdateTextElement(TASKDIALOG_ELEMENTS element, string? text)
    {
        // Instead of null, we must specify the empty string; otherwise the update
        // would be ignored.
        text ??= string.Empty;

        // We can just pin the string because sending the message should take
        // a very short time only and the string will not be modified.
        fixed (char* textPtr = text)
        {
            // Note: SetElementText will resize the dialog while UpdateElementText
            // will not (which would lead to clipped controls), so we use the former.
            SendTaskDialogMessage(TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT, (WPARAM)(int)element, (LPARAM)textPtr);
        }
    }

    internal void UpdateIconElement(TASKDIALOG_ICON_ELEMENTS element, IntPtr icon)
    {
        // Note: Updating the icon doesn't cause the task dialog to update
        // its size; in contrast to the text updates where we use a
        // TDM_SET_... message.
        //
        // For example, if you initially didn't specify an icon but later want to
        // set one, the dialog contents might get clipped.
        //
        // To fix this, we call UpdateWindowSize() after updating the icon, to
        // force the task dialog to update its size.
        SendTaskDialogMessage(TASKDIALOG_MESSAGES.TDM_UPDATE_ICON, (WPARAM)(int)element, (LPARAM)icon);
        UpdateWindowSize();
    }

    internal void UpdateCaption(string? caption)
    {
        // We must not allow to change the caption if we are currently
        // waiting for a TDN_NAVIGATED notification, because in that case
        // the task dialog will already have set the caption from the new page.
        DenyIfDialogNotUpdatable();

        // Note: Because we use SetWindowText() directly (as there is no task
        // dialog message for setting the title), there is a small discrepancy
        // between specifying an empty title in the TASKDIALOGCONFIG structure
        // and setting an empty title with this method: An empty string (or null)
        // in the TASKDIALOGCONFIG structure causes the dialog title to be the
        // executable name (e.g. "MyApplication.exe"), but using an empty string
        // (or null) with SetWindowText() causes the window title to be empty.
        // Therefore, we replicate the task dialog behavior by also using the
        // executable's name as title if the string is null or empty.
        if (TaskDialogPage.IsNativeStringNullOrEmpty(caption))
        {
            caption = Path.GetFileName(PInvoke.GetModuleFileNameLongPath(HINSTANCE.Null));
        }

        PInvoke.SetWindowText(_handle, caption);
    }

    private HRESULT HandleTaskDialogCallback(
        HWND hWnd,
        TASKDIALOG_NOTIFICATIONS notification,
        WPARAM wParam,
        LPARAM lParam)
    {
        Debug.Assert(_boundPage is not null);

        // Set the hWnd as this may be the first time that we get it.
        bool isFirstNotification = _handle.IsNull;
        _handle = hWnd;

        try
        {
            if (isFirstNotification)
            {
                // Subclass the window as early as possible after the window handle
                // is available.
                SubclassWindow();
            }

            switch (notification)
            {
                case TASKDIALOG_NOTIFICATIONS.TDN_CREATED:
                    _boundPage.ApplyInitialization();

                    // Don't raise the Created event of the bound page if we are
                    // waiting for the TDN_NAVIGATED notification, because that means
                    // the user has already navigated the dialog in one of the
                    // previous events, so eventually the TDN_NAVIGATED notification
                    // will occur where we will raise the Created event for the new
                    // page.
                    if (_waitingNavigationPages.Count == 0 && !_raisedPageCreated)
                    {
                        _raisedPageCreated = true;
                        _boundPage.OnCreated(EventArgs.Empty);
                    }

                    break;

                case TASKDIALOG_NOTIFICATIONS.TDN_NAVIGATED:
                    // Indicate to the ButtonClicked handlers currently on the stack
                    // that we received the TDN_NAVIGATED notification.
                    _buttonClickNavigationCounter.navigationIndex = _buttonClickNavigationCounter.stackCount;

                    // We can now unbind the previous page and then switch to the
                    // new page.
                    _boundPage.Unbind();
                    _boundPage = _waitingNavigationPages.Dequeue();

                    if (_waitingNavigationPages.Count == 0)
                    {
                        // Apply the initialization only if there are no further outstanding
                        // navigations. Otherwise, this might throw an InvalidOperationException,
                        // because the update methods would deny updating the dialog when there
                        // are further outstanding navigations, as that could cause the layout
                        // of the dialog to be changed erroneously (see comment in
                        // DenyIfDialogNotUpdatable).
                        _boundPage.ApplyInitialization();

                        // Also, raise the event only if we don't wait for yet another navigation
                        // (this is the same as we do in the TDN_CREATED handler).
                        Debug.Assert(!_raisedPageCreated);
                        if (!_raisedPageCreated)
                        {
                            _raisedPageCreated = true;
                            _boundPage.OnCreated(EventArgs.Empty);
                        }
                    }

                    break;

                case TASKDIALOG_NOTIFICATIONS.TDN_DESTROYED:
                    _receivedDestroyedNotification = true;

                    // Note: When multiple dialogs are shown (so Show() will occur
                    // multiple times in the call stack) and a previously opened
                    // dialog is closed, the Destroyed notification for the closed
                    // dialog will only occur after the newer dialogs are also
                    // closed.
                    try
                    {
                        // Only raise the destroyed/closed events if the corresponding
                        // created/opened events have been called. For example, when
                        // trying to show the dialog with an invalid configuration
                        // (so an error HResult will be returned), the callback is
                        // invoked only one time with the TDN_DESTROYED notification
                        // without being invoked with the TDN_CREATED notification.
                        if (_raisedPageCreated)
                        {
                            _raisedPageCreated = false;
                            _boundPage.OnDestroyed(EventArgs.Empty);
                        }
                    }
                    finally
                    {
                        // Undo the subclassing as the window handle is about to
                        // be destroyed.
                        UnsubclassWindow();

                        // Clear the dialog handle, because according to the docs, we
                        // must not continue to send any notifications to the dialog
                        // after the callback function has returned from being called
                        // with the 'Destroyed' notification.
                        _handle = HWND.Null;
                    }

                    break;

                case TASKDIALOG_NOTIFICATIONS.TDN_BUTTON_CLICKED:
                    // Only do the special handling (ignoring the notification and
                    // setting the _ignoreButtonClickedNotifications flag) if this
                    // BUTTON_CLICKED notification is not caused by our own code.
                    if (_buttonClickedProgrammatically)
                    {
                        // Clear the flag as early as possible.
                        _buttonClickedProgrammatically = false;
                    }
                    else
                    {
                        // Check if we should ignore this notification. If we process
                        // it, we set a flag to ignore further TDN_BUTTON_CLICKED
                        // notifications, and we post a message to the task dialog
                        // that, when we process it, causes us to reset the flag.
                        // This is used to work-around the access key bug in the
                        // native task dialog - see the remarks of the
                        // "ContinueButtonClickHandlingMessage" for more information.
                        if (_ignoreButtonClickedNotifications)
                        {
                            return HRESULT.S_FALSE;
                        }

                        // Post the message, and then set the flag to ignore further
                        // notifications until we receive the posted message.
                        if (PInvokeCore.PostMessage(hWnd, ContinueButtonClickHandlingMessage))
                        {
                            _ignoreButtonClickedNotifications = true;
                        }
                    }

                    int buttonID = (int)wParam;
                    TaskDialogButton? button = _boundPage.GetBoundButtonByID(buttonID);

                    bool applyButtonResult = true;
                    if (button is { } && !_suppressButtonClickedEvent)
                    {
                        // Note: When the event handler returned true but we received
                        // a TDN_NAVIGATED notification within the handler (e.g. by
                        // running the message loop there), the buttonID of the handler
                        // would be set as the dialog's result even if this ID is from
                        // the dialog page before the dialog was navigated.
                        // Additionally, memory access problems like
                        // AccessViolationExceptions seem to occur in this situation
                        // (especially if the dialog also had radio buttons before the
                        // navigation; these would also be set as result of the dialog),
                        // probably because this scenario isn't an expected usage of
                        // the native TaskDialog.
                        //
                        // See documentation/repro in
                        // /docs/src/System/Windows/Forms/TaskDialog/Issue_AccessViolation_NavigationInButtonClicked.md
                        //
                        // To fix the memory access problems, we simply always return
                        // S_FALSE when the callback received a TDN_NAVIGATED
                        // notification within the Button.Click event handler.
                        checked
                        {
                            _buttonClickNavigationCounter.stackCount++;
                        }

                        try
                        {
                            applyButtonResult = button.HandleButtonClicked();

                            // Check if the index was set to the current stack count,
                            // which means we received a TDN_NAVIGATED notification
                            // while we called the handler. In that case we need to
                            // return S_FALSE to prevent the dialog from closing
                            // (and applying the previous ButtonID and RadioButtonID
                            // as results).
                            if (_buttonClickNavigationCounter.navigationIndex >=
                                _buttonClickNavigationCounter.stackCount)
                            {
                                applyButtonResult = false;
                            }
                        }
                        finally
                        {
                            _buttonClickNavigationCounter.stackCount--;
                            _buttonClickNavigationCounter.navigationIndex = Math.Min(
                                _buttonClickNavigationCounter.navigationIndex,
                                _buttonClickNavigationCounter.stackCount);
                        }
                    }

                    // If the button would close the dialog, raise the Closing event
                    // so that the user can cancel the close.
                    if (applyButtonResult && IsTaskDialogButtonCommitting(button))
                    {
                        // For consistency, we only raise the event (and allow the handler
                        // to return S_OK) if it was not already raised for a previous
                        // handler which already set a button result. Otherwise, we
                        // would either raise the "Closing" event multiple times (which
                        // wouldn't make sense) or we would allow a later handler to
                        // override the previously set result, which would mean the
                        // button returned from Show() would not match one specified
                        // in the "Closing" event's args.
                        if (_resultButton is not null)
                        {
                            applyButtonResult = false;
                        }
                        else
                        {
                            // If we didn't find the button (e.g. when specifying
                            // AllowCancel but not adding a "Cancel" button), we need
                            // to create a new instance and save it, so that we can
                            // return that instance after TaskDialogIndirect() returns.
                            button ??= CreatePlaceholderButton((TaskDialogResult)buttonID);

                            // Cache the result button if we return S_OK.
                            _resultButton = (button, buttonID);
                        }
                    }

                    return applyButtonResult ? HRESULT.S_OK : HRESULT.S_FALSE;

                case TASKDIALOG_NOTIFICATIONS.TDN_RADIO_BUTTON_CLICKED:
                    int radioButtonID = (int)wParam;
                    TaskDialogRadioButton radioButton = _boundPage.GetBoundRadioButtonByID(radioButtonID)!;

                    checked
                    {
                        RadioButtonClickedStackCount++;
                    }

                    try
                    {
                        radioButton.HandleRadioButtonClicked();
                    }
                    finally
                    {
                        RadioButtonClickedStackCount--;
                    }

                    break;

                case TASKDIALOG_NOTIFICATIONS.TDN_EXPANDO_BUTTON_CLICKED:
                    _boundPage.Expander!.HandleExpandoButtonClicked((nint)wParam != IntPtr.Zero);
                    break;

                case TASKDIALOG_NOTIFICATIONS.TDN_VERIFICATION_CLICKED:
                    _boundPage.Verification!.HandleCheckBoxClicked((nint)wParam != IntPtr.Zero);
                    break;

                case TASKDIALOG_NOTIFICATIONS.TDN_HELP:
                    _boundPage.OnHelpRequest(EventArgs.Empty);
                    break;

                case TASKDIALOG_NOTIFICATIONS.TDN_HYPERLINK_CLICKED:
                    string? linkHref = Marshal.PtrToStringUni(lParam);
                    Debug.Assert(linkHref is not null);
                    _boundPage.OnLinkClicked(new TaskDialogLinkClickedEventArgs(linkHref));
                    break;
            }
        }
        catch (Exception ex) when (CanCatchCallbackException())
        {
            // When an exception occurs, handle it by calling the application's
            // ThreadException handler.
            // It is important that we don't let such exception bubble up to
            // the native -> managed transition, as otherwise the CLR would
            // unwind the stack even though the task dialog is still shown,
            // which means invalid memory access may occur if the callback
            // is called again as we already freed the object pointer.
            HandleCallbackException(ex);
        }

        return HRESULT.S_OK;
    }

    /// <summary>
    ///  While the dialog is being shown, recreates the dialog from the specified
    ///  <paramref name="page"/>.
    /// </summary>
    internal unsafe void Navigate(TaskDialogPage page)
    {
        // We allow to navigate the dialog even if the previous navigation did
        // not complete yet, as this seems to work in the native implementation.
        DenyIfDialogNotUpdatable(checkWaitingForNavigation: false);

        // Don't allow to navigate the dialog when we are in a
        // TDN_RADIO_BUTTON_CLICKED notification, because the dialog doesn't
        // seem to correctly handle this (e.g. when running the message loop
        // after navigation, an AccessViolationException would occur after
        // the handler returns).
        // Note: Actually, the problem is when we receive a TDN_NAVIGATED
        // notification within a TDN_RADIO_BUTTON_CLICKED notification (due
        // to running the message loop there), but we can only prevent this
        // by not allowing to send the TDM_NAVIGATE_PAGE message here
        // (and then disallow to send any TDM_CLICK_RADIO_BUTTON messages
        // until we receive the TDN_NAVIGATED notification).
        // See:
        // https://github.com/dotnet/winforms/issues/146#issuecomment-466784079
        // and /docs/src/System/Windows/Forms/TaskDialog/Issue_AccessViolation_NavigationInRadioButtonClicked.md
        if (RadioButtonClickedStackCount > 0)
        {
            throw new InvalidOperationException(string.Format(
                SR.TaskDialogCannotNavigateWithinRadioButtonCheckedChanged,
                $"{nameof(TaskDialogRadioButton)}.{nameof(TaskDialogRadioButton.CheckedChanged)}"));
        }

        // Don't allow to navigate the dialog if called from an event handler
        // (TaskDialogPage.Destroyed) that is raised from within this method.
        if (_isInNavigate)
        {
            throw new InvalidOperationException(SR.TaskDialogCannotNavigateWithinNavigationEventHandler);
        }

        // Don't allow navigation if the dialog window is already closed (and
        // therefore has set a result button), because that would result in
        // weird/undefined behavior (e.g. returning IDCANCEL (2) as button result
        // even though a different button has already been set as result).
        // Additionally, we check if we received the TDN_DESTROYED notification, in case
        // the dialog was closed abnormally without a prior TDN_BUTTON_CLICKED
        // notification (e.g. when closing the main application window while a modeless
        // task dialog is showing).
        if (_resultButton is not null || _receivedDestroyedNotification)
        {
            throw new InvalidOperationException(SR.TaskDialogCannotNavigateClosedDialog);
        }

        _isInNavigate = true;
        try
        {
            page.Validate();

            // Need to raise the "Destroyed" event for the current page. The
            // "Created" event for the new page will occur from the callback.
            // Note: "this.raisedPageCreated" should always be true here.
            if (_raisedPageCreated)
            {
                _raisedPageCreated = false;
                _boundPage!.OnDestroyed(EventArgs.Empty);

                // Need to check again if the dialog has not already been closed,
                // since the Destroyed event handler could have performed a
                // button click that closed the dialog.
                // TODO: Another option would be to disallow button clicks while
                // within the event handler.
                if (_resultButton is not null)
                {
                    throw new InvalidOperationException(SR.TaskDialogCannotNavigateClosedDialog);
                }

                // Also, we need to validate the page again. For example, the user
                // might change the properties of the new page or its controls
                // within the "Destroyed" event so that it would no longer be
                // valid, or e.g. navigate a different dialog to that page in
                // the meantime (although admittedly that would be a very
                // strange pattern).
                page.Validate();
            }
        }
        finally
        {
            _isInNavigate = false;
        }

        // Note: We don't unbind the previous page here - this will be done
        // when the TDN_NAVIGATED notification occurs, because technically
        // the controls of both the previous page AND the new page exist
        // on the native Task Dialog until the TDN_NAVIGATED notification
        // occurs, and the dialog behaves as if it currently still showing
        // the previous page (which can be verified using the behavior of
        // the "Help" button, where simulating a click to that button will
        // raise the "Help" event if the dialog considers the button to
        // be shown, and otherwise will close the dialog without raising
        // the "Help" event; also, if you updated e.g. the dialog's text or
        // instruction during that time, these changes would be lost when
        // the TDN_NAVIGATED notification occurs).
        BindPageAndAllocateConfig(
            page,
            IntPtr.Zero,
            startupLocation: default,
            out IntPtr ptrToFree,
            out TASKDIALOGCONFIG* ptrTaskDialogConfig);
        try
        {
            // Enqueue the page before sending the message. This ensures
            // that if the native task dialog's behavior is ever changed
            // to raise the TDN_NAVIGATED notification recursively from
            // sending the TDM_NAVIGATE_PAGE message, we can correctly
            // process the page in the callback.
            _waitingNavigationPages.Enqueue(page);
            try
            {
                // Note: If the task dialog cannot be recreated with the
                // new page, the dialog will close and TaskDialogIndirect()
                // returns with an error code; but this will not be
                // noticeable in the SendMessage() return value.
                SendTaskDialogMessage(
                    TASKDIALOG_MESSAGES.TDM_NAVIGATE_PAGE,
                    default,
                    (LPARAM)ptrTaskDialogConfig,
                    checkWaitingForNavigation: false);
            }
            catch
            {
                // Since navigation failed, we need to remove our page
                // from the queue.
                // However, this should not happen because
                // SendTaskDialogMessage() shouldn't throw here.
                int originalCount = _waitingNavigationPages.Count;
                for (int i = 0; i < originalCount; i++)
                {
                    TaskDialogPage element = _waitingNavigationPages.Dequeue();
                    if (element != page)
                    {
                        _waitingNavigationPages.Enqueue(element);
                    }
                }

                throw;
            }
        }
        catch
        {
            page.Unbind();
            throw;
        }
        finally
        {
            // We can now free the memory because SendMessage does not
            // return until the message has been processed.
            FreeConfig(ptrToFree);
        }
    }

    private unsafe void BindPageAndAllocateConfig(
        TaskDialogPage page,
        IntPtr hwndOwner,
        TaskDialogStartupLocation startupLocation,
        out nint ptrToFree,
        out TASKDIALOGCONFIG* ptrTaskDialogConfig)
    {
        page.Bind(
            this,
            out TASKDIALOG_FLAGS flags,
            out TASKDIALOG_COMMON_BUTTON_FLAGS standardButtonFlags,
            out IEnumerable<(int buttonID, string text)> customButtonElements,
            out IEnumerable<(int buttonID, string text)> radioButtonElements,
            out TASKDIALOGCONFIG_MainIcon mainIcon,
            out TASKDIALOGCONFIG_FooterIcon footerIcon,
            out int defaultButtonID,
            out int defaultRadioButtonID);

        try
        {
            if (startupLocation == TaskDialogStartupLocation.CenterOwner)
            {
                flags |= TASKDIALOG_FLAGS.TDF_POSITION_RELATIVE_TO_WINDOW;
            }

            checked
            {
                // First, calculate the necessary memory size we need to allocate for
                // all structs and strings.
                // Note: Each Align() call when calculating the size must correspond
                // with a Align() call when incrementing the pointer.
                // Use a byte pointer so we can use byte-wise pointer arithmetics.
                byte* sizeToAllocate = (byte*)0;
                sizeToAllocate += sizeof(TASKDIALOGCONFIG);

                // Strings in TasDialogConfig
                Align(ref sizeToAllocate, sizeof(char));
                sizeToAllocate += SizeOfString(page.Caption);
                sizeToAllocate += SizeOfString(page.Heading);
                sizeToAllocate += SizeOfString(page.Text);
                sizeToAllocate += SizeOfString(page.Verification?.Text);
                sizeToAllocate += SizeOfString(page.Expander?.Text);
                sizeToAllocate += SizeOfString(page.Expander?.ExpandedButtonText);
                sizeToAllocate += SizeOfString(page.Expander?.CollapsedButtonText);
                sizeToAllocate += SizeOfString(page.Footnote?.Text);

                // Buttons array
                if (customButtonElements.Any())
                {
                    // Note: Theoretically we would not need to align the pointer here
                    // since the packing of the structure is set to 1. Note that this
                    // can cause an unaligned write when assigning the structure (the
                    // same happens with TaskDialogConfig).
                    Align(ref sizeToAllocate);
                    sizeToAllocate += sizeof(TASKDIALOG_BUTTON) * customButtonElements.Count();

                    // Strings in buttons array
                    Align(ref sizeToAllocate, sizeof(char));
                    foreach ((int buttonID, string text) in customButtonElements)
                    {
                        sizeToAllocate += SizeOfString(text);
                    }
                }

                // Radio buttons array
                if (radioButtonElements.Any())
                {
                    // See comment above regarding alignment.
                    Align(ref sizeToAllocate);
                    sizeToAllocate += sizeof(TASKDIALOG_BUTTON) * radioButtonElements.Count();

                    // Strings in radio buttons array
                    Align(ref sizeToAllocate, sizeof(char));
                    foreach ((int buttonID, string text) in radioButtonElements)
                    {
                        sizeToAllocate += SizeOfString(text);
                    }
                }

                // Allocate the memory block. We add additional bytes to ensure we can
                // align the returned pointer to IntPtr.Size (the biggest align size
                // that we will use).
                ptrToFree = Marshal.AllocHGlobal((nint)(sizeToAllocate + (IntPtr.Size - 1)));
                try
                {
                    // Align the pointer before using it. This is important since we also
                    // started with an aligned "address" value (0) when calculating the
                    // required allocation size. We must use the same size that we added
                    // as additional size when allocating the memory.
                    byte* currentPtr = (byte*)ptrToFree;
                    Align(ref currentPtr);
                    ptrTaskDialogConfig = (TASKDIALOGCONFIG*)currentPtr;

                    ref TASKDIALOGCONFIG taskDialogConfig = ref *ptrTaskDialogConfig;
                    currentPtr += sizeof(TASKDIALOGCONFIG);

                    // Assign the structure with the constructor syntax, which will
                    // automatically initialize its other members with their default
                    // value.
                    Align(ref currentPtr, sizeof(char));
                    taskDialogConfig = new TASKDIALOGCONFIG()
                    {
                        cbSize = (uint)sizeof(TASKDIALOGCONFIG),
                        hwndParent = (HWND)hwndOwner,
                        dwFlags = flags,
                        dwCommonButtons = standardButtonFlags,
                        mainIcon = mainIcon,
                        footerIcon = footerIcon,
                        pszWindowTitle = MarshalString(page.Caption),
                        pszMainInstruction = MarshalString(page.Heading),
                        pszContent = MarshalString(page.Text),
                        pszVerificationText = MarshalString(page.Verification?.Text),
                        pszExpandedInformation = MarshalString(page.Expander?.Text),
                        pszExpandedControlText = MarshalString(page.Expander?.ExpandedButtonText),
                        pszCollapsedControlText = MarshalString(page.Expander?.CollapsedButtonText),
                        pszFooter = MarshalString(page.Footnote?.Text),
                        nDefaultButton = defaultButtonID,
                        nDefaultRadioButton = defaultRadioButtonID,
                        pfCallback = &HandleTaskDialogNativeCallback,
                        lpCallbackData = _instanceHandlePtr
                    };

                    // Buttons array
                    if (customButtonElements.Any())
                    {
                        int customButtonCount = customButtonElements.Count();

                        Align(ref currentPtr);
                        var customButtonStructs = (TASKDIALOG_BUTTON*)currentPtr;
                        taskDialogConfig.pButtons = customButtonStructs;
                        taskDialogConfig.cButtons = (uint)customButtonCount;
                        currentPtr += sizeof(TASKDIALOG_BUTTON) * customButtonCount;

                        Align(ref currentPtr, sizeof(char));
                        int i = 0;
                        foreach ((int buttonID, string text) in customButtonElements)
                        {
                            customButtonStructs[i] = new TASKDIALOG_BUTTON()
                            {
                                nButtonID = buttonID,
                                pszButtonText = MarshalString(text)
                            };

                            i++;
                        }
                    }

                    // Radio buttons array
                    if (radioButtonElements.Any())
                    {
                        int radioButtonCount = radioButtonElements.Count();

                        Align(ref currentPtr);
                        var radioButtonStructs = (TASKDIALOG_BUTTON*)currentPtr;
                        taskDialogConfig.pRadioButtons = radioButtonStructs;
                        taskDialogConfig.cRadioButtons = (uint)radioButtonCount;
                        currentPtr += sizeof(TASKDIALOG_BUTTON) * radioButtonCount;

                        Align(ref currentPtr, sizeof(char));
                        int i = 0;
                        foreach ((int buttonID, string text) in radioButtonElements)
                        {
                            radioButtonStructs[i] = new TASKDIALOG_BUTTON()
                            {
                                nButtonID = buttonID,
                                pszButtonText = MarshalString(text)
                            };

                            i++;
                        }
                    }

                    Debug.Assert(currentPtr == (long)ptrTaskDialogConfig + sizeToAllocate);

                    char* MarshalString(string? str)
                    {
                        if (str is null)
                        {
                            return null;
                        }

                        fixed (char* strPtr = str)
                        {
                            // Copy the string. The C# language specification guarantees
                            // that a char* value produced by using the 'fixed'
                            // statement on a string always points to a null-terminated
                            // string, so we don't need to copy a NUL character
                            // separately.
                            long bytesToCopy = SizeOfString(str);
                            Buffer.MemoryCopy(strPtr, currentPtr, bytesToCopy, bytesToCopy);

                            byte* ptrToReturn = currentPtr;
                            currentPtr += bytesToCopy;
                            return (char*)ptrToReturn;
                        }
                    }
                }
                catch
                {
                    Marshal.FreeHGlobal(ptrToFree);
                    throw;
                }
            }
        }
        catch
        {
            // Unbind the page, then rethrow the exception.
            page.Unbind();
            throw;
        }

        return;

        static void Align(ref byte* currentPtr, int? alignment = null)
        {
            if (alignment <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(alignment));
            }

            // Align the pointer to the next align size. If not specified,
            // we will use the pointer (register) size.
            nuint add = (nuint)(alignment ?? IntPtr.Size) - 1;
            currentPtr = (byte*)(((nuint)currentPtr + add) & ~add);
        }

        static long SizeOfString(string? str)
        {
            return str is null ? 0 : ((long)str.Length + 1) * sizeof(char);
        }
    }

    private void SubclassWindow()
    {
        if (_windowSubclassHandler is not null)
        {
            throw new InvalidOperationException();
        }

        // Subclass the window.
        _windowSubclassHandler = new WindowSubclassHandler(this);
        _windowSubclassHandler.Open();
    }

    private void UnsubclassWindow()
    {
        if (_windowSubclassHandler is not null)
        {
            try
            {
                _windowSubclassHandler.Dispose();
                _windowSubclassHandler = null;
            }
            catch (Exception ex) when (ex is InvalidOperationException or Win32Exception)
            {
                // Ignore. This could happen for example if some other code
                // also subclassed the window after us but didn't correctly
                // revert it. However, this can mean that the callback can
                // still be called until TaskDialogIndirect() returns, so we
                // need to keep the delegate alive until that happens.
            }
        }
    }

    private void DenyIfBound()
    {
        if (_boundPage is not null)
        {
            throw new InvalidOperationException(SR.TaskDialogCannotSetPropertyOfShownDialog);
        }
    }

    private void DenyIfDialogNotUpdatable(bool checkWaitingForNavigation = true)
    {
        // Use the Handle getter (rather than HandleCreated) to check for illegal
        // cross thread calls.
        if (Handle == IntPtr.Zero)
        {
            throw new InvalidOperationException(SR.TaskDialogCanUpdateStateOnlyWhenShown);
        }

        // When we wait for the navigated event to occur, also don't allow to
        // update the dialog because that could produce an unexpected state
        // (because it will change its size for the new page, but if we then
        // updated e.g. the text or instruction, it would update its size again
        // for the current page, and it would keep the (wrong) size after
        // navigation).
        // An exception is e.g. a button click (as that doesn't manipulate
        // the layout) so that the user can close the dialog even though we are
        // waiting for the navigation to finish.
        if (_waitingNavigationPages.Count > 0 && checkWaitingForNavigation)
        {
            throw new InvalidOperationException(string.Format(
                SR.TaskDialogCannotUpdateAfterNavigation,
                $"{nameof(TaskDialogPage)}.{nameof(TaskDialogPage.Created)}"));
        }
    }

    private static bool CanCatchCallbackException()
    {
        // Catch all exceptions, except when the NativeWindow indicates
        // that a debuggable WndProc callback should be used, in which
        // case we don't catch any exception. This is so that an attached
        // debugger can stop at the original location where the exception
        // was thrown.
        return !NativeWindow.WndProcShouldBeDebuggable;
    }

    /// <summary>
    ///  Called when an exception occurs in dispatching messages through
    ///  the task dialog callback or its window procedure.
    /// </summary>
    private static void HandleCallbackException(Exception e) => Application.OnThreadException(e);

    private void SendTaskDialogMessage(
        TASKDIALOG_MESSAGES message,
        WPARAM wParam,
        LPARAM lParam = default,
        bool checkWaitingForNavigation = true)
    {
        DenyIfDialogNotUpdatable(checkWaitingForNavigation);

        PInvokeCore.SendMessage(
            _handle,
            (uint)message,
            wParam,
            lParam);
    }

    /// <summary>
    ///  Forces the task dialog to update its window size according to its contents.
    /// </summary>
    private void UpdateWindowSize()
    {
        DenyIfDialogNotUpdatable();

        // Force the task dialog to update its size by doing an arbitrary
        // update of one of its text elements (as the TDM_SET_ELEMENT_TEXT
        // causes the size/layout to be updated).
        // We use the MainInstruction because it cannot contain hyperlinks
        // (and therefore there is no risk that one of the links loses focus).
        UpdateTextElement(TASKDIALOG_ELEMENTS.TDE_MAIN_INSTRUCTION, _boundPage!.Heading);
    }
}
