// 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.Specialized;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms.Layout;
using Microsoft.Win32;
using Windows.Win32.UI.Controls.Dialogs;
using Windows.Win32.UI.Controls.RichEdit;
using static Interop.Richedit;

namespace System.Windows.Forms;

/// <summary>
///  Rich Text control. The RichTextBox is a control that contains formatted text.
///  It supports font selection, boldface, and other type attributes.
/// </summary>
[Docking(DockingBehavior.Ask)]
[Designer($"System.Windows.Forms.Design.RichTextBoxDesigner, {Assemblies.SystemDesign}")]
[SRDescription(nameof(SR.DescriptionRichTextBox))]
public partial class RichTextBox : TextBoxBase
{
    /// <summary>
    ///  Paste special flags.
    /// </summary>
    internal const int INPUT = 0x0001;
    internal const int OUTPUT = 0x0002;
    internal const int DIRECTIONMASK = INPUT | OUTPUT;
    internal const int ANSI = 0x0004;
    internal const int UNICODE = 0x0008;
    internal const int FORMATMASK = ANSI | UNICODE;
    internal const int TEXTLF = 0x0010;
    internal const int TEXTCRLF = 0x0020;
    internal const int RTF = 0x0040;
    internal const int KINDMASK = TEXTLF | TEXTCRLF | RTF;

    // This is where we store the Rich Edit library.
    private static IntPtr s_moduleHandle;

    private const string SZ_RTF_TAG = "{\\rtf";
    private const int CHAR_BUFFER_LEN = 512;

    // Event objects
    private static readonly object s_hscrollEvent = new();
    private static readonly object s_linkActivateEvent = new();
    private static readonly object s_imeChangeEvent = new();
    private static readonly object s_protectedEvent = new();
    private static readonly object s_requestSizeEvent = new();
    private static readonly object s_selectionChangeEvent = new();
    private static readonly object s_vscrollEvent = new();

    // Persistent state
    //
    private int _bulletIndent;
    private int _rightMargin;
    private string? _textRtf; // If not null, takes precedence over cached Text value
    private string? _textPlain;
    private Color _selectionBackColorToSetOnHandleCreated;
    private RichTextBoxLanguageOptions _languageOption = RichTextBoxLanguageOptions.AutoFont | RichTextBoxLanguageOptions.DualFont;

    // Non-persistent state
    //
    private static int s_logPixelsX;
    private static int s_logPixelsY;
    private Stream? _editStream;
    private float _zoomMultiplier = 1.0f;

    // used to decide when to fire the selectionChange event.
    private int _curSelStart;
    private int _curSelEnd;
    private short _curSelType;
    private object? _oleCallback;

    private static int[]? s_shortcutsToDisable;
    private static int s_richEditMajorVersion = 3;

    private BitVector32 _richTextBoxFlags;
    private static readonly BitVector32.Section s_autoWordSelectionSection = BitVector32.CreateSection(1);
    private static readonly BitVector32.Section s_showSelBarSection = BitVector32.CreateSection(1, s_autoWordSelectionSection);
    private static readonly BitVector32.Section s_autoUrlDetectSection = BitVector32.CreateSection(1, s_showSelBarSection);
    private static readonly BitVector32.Section s_fInCtorSection = BitVector32.CreateSection(1, s_autoUrlDetectSection);
    private static readonly BitVector32.Section s_protectedErrorSection = BitVector32.CreateSection(1, s_fInCtorSection);
    private static readonly BitVector32.Section s_linkcursorSection = BitVector32.CreateSection(1, s_protectedErrorSection);
    private static readonly BitVector32.Section s_allowOleDropSection = BitVector32.CreateSection(1, s_linkcursorSection);
    private static readonly BitVector32.Section s_suppressTextChangedEventSection = BitVector32.CreateSection(1, s_allowOleDropSection);
    private static readonly BitVector32.Section s_callOnContentsResizedSection = BitVector32.CreateSection(1, s_suppressTextChangedEventSection);
    private static readonly BitVector32.Section s_richTextShortcutsEnabledSection = BitVector32.CreateSection(1, s_callOnContentsResizedSection);
    private static readonly BitVector32.Section s_allowOleObjectsSection = BitVector32.CreateSection(1, s_richTextShortcutsEnabledSection);
    private static readonly BitVector32.Section s_scrollBarsSection = BitVector32.CreateSection((short)RichTextBoxScrollBars.ForcedBoth, s_allowOleObjectsSection);
    private static readonly BitVector32.Section s_enableAutoDragDropSection = BitVector32.CreateSection(1, s_scrollBarsSection);

    /// <summary>
    ///  Constructs a new RichTextBox.
    /// </summary>
    public RichTextBox()
    {
        InConstructor = true;
        _richTextBoxFlags[s_autoWordSelectionSection] = 0; // This is false by default
        DetectUrls = true;
        ScrollBars = RichTextBoxScrollBars.Both;
        RichTextShortcutsEnabled = true;
        MaxLength = int.MaxValue;
        Multiline = true;
        AutoSize = false;
        _curSelStart = _curSelEnd = _curSelType = -1;
        InConstructor = false;
    }

    /// <summary>
    ///  RichTextBox controls have built-in drag and drop support, but AllowDrop, DragEnter, DragDrop
    ///  may still be used: this should be hidden in the property grid, but not in code
    /// </summary>
    [Browsable(false)]
    public override bool AllowDrop
    {
        get => _richTextBoxFlags[s_allowOleDropSection] != 0;
        set
        {
            _richTextBoxFlags[s_allowOleDropSection] = value ? 1 : 0;
            UpdateOleCallback();
        }
    }

    internal bool AllowOleObjects
    {
        get => _richTextBoxFlags[s_allowOleObjectsSection] != 0;
        set
        {
            _richTextBoxFlags[s_allowOleObjectsSection] = value ? 1 : 0;
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the size
    ///  of the control automatically adjusts when the font assigned to the control
    ///  is changed.
    ///
    ///  Note: this works differently than other Controls' AutoSize, so we're hiding
    ///  it to avoid confusion.
    /// </summary>
    [DefaultValue(false)]
    [RefreshProperties(RefreshProperties.Repaint)]
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public override bool AutoSize
    {
        get => base.AutoSize;
        set => base.AutoSize = value;
    }

    /// <summary>
    ///  Controls whether whether mouse selection snaps to whole words.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.RichTextBoxAutoWordSelection))]
    public bool AutoWordSelection
    {
        get => _richTextBoxFlags[s_autoWordSelectionSection] != 0;
        set
        {
            _richTextBoxFlags[s_autoWordSelectionSection] = value ? 1 : 0;
            if (IsHandleCreated)
            {
                PInvokeCore.SendMessage(
                    this,
                    PInvokeCore.EM_SETOPTIONS,
                    (WPARAM)(int)(value ? PInvoke.ECOOP_OR : PInvoke.ECOOP_XOR),
                    (LPARAM)(int)PInvoke.ECO_AUTOWORDSELECTION);
            }
        }
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public override Image? BackgroundImage
    {
        get => base.BackgroundImage;
        set => base.BackgroundImage = value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event EventHandler? BackgroundImageChanged
    {
        add => base.BackgroundImageChanged += value;
        remove => base.BackgroundImageChanged -= value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public override ImageLayout BackgroundImageLayout
    {
        get => base.BackgroundImageLayout;
        set => base.BackgroundImageLayout = value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event EventHandler? BackgroundImageLayoutChanged
    {
        add => base.BackgroundImageLayoutChanged += value;
        remove => base.BackgroundImageLayoutChanged -= value;
    }

    /// <summary>
    ///  Returns the amount of indent used in a RichTextBox control when
    ///  SelectionBullet is set to true.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(0)]
    [Localizable(true)]
    [SRDescription(nameof(SR.RichTextBoxBulletIndent))]
    public int BulletIndent
    {
        get => _bulletIndent;

        set
        {
            ArgumentOutOfRangeException.ThrowIfNegative(value);

            _bulletIndent = value;

            // Call to update the control only if the bullet is set.
            if (IsHandleCreated && SelectionBullet)
            {
                SelectionBullet = true;
            }
        }
    }

    private bool CallOnContentsResized
    {
        get => _richTextBoxFlags[s_callOnContentsResizedSection] != 0;
        set => _richTextBoxFlags[s_callOnContentsResizedSection] = value ? 1 : 0;
    }

    internal override bool CanRaiseTextChangedEvent => !SuppressTextChangedEvent;

    /// <summary>
    ///  Whether or not there are actions that can be Redone on the RichTextBox control.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxCanRedoDescr))]
    public bool CanRedo => IsHandleCreated && (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_CANREDO) != 0;

    protected override CreateParams CreateParams
    {
        get
        {
            // Check for library
            if (s_moduleHandle == IntPtr.Zero)
            {
                s_moduleHandle = PInvoke.LoadLibraryFromSystemPathIfAvailable(Libraries.RichEdit41);

                int lastWin32Error = Marshal.GetLastWin32Error();

                // This code has been here since the inception of the project,
                // we can't determine why we have to compare w/ 32 here.
                // This fails on 3-GB mode, (once the dll is loaded above 3GB memory space)
                if ((ulong)s_moduleHandle < 32)
                {
                    throw new Win32Exception(lastWin32Error, string.Format(SR.LoadDLLError, Libraries.RichEdit41));
                }

                string path = PInvoke.GetModuleFileNameLongPath(new HINSTANCE(s_moduleHandle));
                FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(path);

                Debug.Assert(versionInfo is not null && !string.IsNullOrEmpty(versionInfo.ProductVersion), "Couldn't get the version info for the richedit dll");
                if (versionInfo is not null && !string.IsNullOrEmpty(versionInfo.ProductVersion))
                {
                    // Note: this only allows for one digit version
                    if (int.TryParse(versionInfo.ProductVersion.AsSpan(0, 1), out int parsedValue))
                    {
                        s_richEditMajorVersion = parsedValue;
                    }
                }
            }

            CreateParams cp = base.CreateParams;
            cp.ClassName = PInvoke.MSFTEDIT_CLASS;

            if (Multiline)
            {
                if (((int)ScrollBars & RichTextBoxConstants.RTB_HORIZ) != 0 && !WordWrap)
                {
                    // RichEd infers word wrap from the absence of horizontal scroll bars
                    cp.Style |= (int)WINDOW_STYLE.WS_HSCROLL;
                    if (((int)ScrollBars & RichTextBoxConstants.RTB_FORCE) != 0)
                    {
                        cp.Style |= (int)PInvoke.ES_DISABLENOSCROLL;
                    }
                }

                if (((int)ScrollBars & RichTextBoxConstants.RTB_VERT) != 0)
                {
                    cp.Style |= (int)WINDOW_STYLE.WS_VSCROLL;
                    if (((int)ScrollBars & RichTextBoxConstants.RTB_FORCE) != 0)
                    {
                        cp.Style |= (int)PInvoke.ES_DISABLENOSCROLL;
                    }
                }
            }

            // Remove the WS_BORDER style from the control, if we're trying to set it,
            // to prevent the control from displaying the single point rectangle around the 3D border
            if (EffectiveVisualStylesMode < VisualStylesMode.Net11
                && BorderStyle == BorderStyle.FixedSingle
                && ((cp.Style & (int)WINDOW_STYLE.WS_BORDER) != 0))
            {
                cp.Style &= ~(int)WINDOW_STYLE.WS_BORDER;
                cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_CLIENTEDGE;
            }

            return cp;
        }
    }

    /// <summary>
    ///  RichEdit (<c>MSFTEDIT</c>) reserves its own border and scrollbar space during
    ///  <c>WM_NCCALCSIZE</c>, so the modern Visual Styles padding band is carved on top of the client
    ///  rectangle the native handler produces.
    /// </summary>
    private protected override bool ReservesNativeNonClientArea => true;

    /// <summary>
    ///  RichEdit reserves the scrollbar space itself while processing <c>WM_NCCALCSIZE</c> (see
    ///  <see cref="ReservesNativeNonClientArea"/>). Return the configured reservation for preferred-size
    ///  measurement; the native client-area calculation explicitly excludes it.
    /// </summary>
    private protected override Padding GetScrollBarPadding()
    {
        Padding padding = Padding.Empty;

        if (Multiline && !WordWrap && (ScrollBars & RichTextBoxScrollBars.Horizontal) != 0)
        {
            padding.Bottom = SystemInformation.GetHorizontalScrollBarHeightForDpi(DeviceDpiInternal);
        }

        if (Multiline && (ScrollBars & RichTextBoxScrollBars.Vertical) != 0)
        {
            padding.Right = SystemInformation.GetVerticalScrollBarWidthForDpi(DeviceDpiInternal);
        }

        return padding;
    }

    /// <summary>
    ///  Controls whether or not the rich edit control will automatically highlight URLs.
    ///  By default, this is true. Note that changing this property will not update text that is
    ///  already present in the RichTextBox control; it only affects text which is entered after the
    ///  property is changed.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.RichTextBoxDetectURLs))]
    public bool DetectUrls
    {
        get => _richTextBoxFlags[s_autoUrlDetectSection] != 0;
        set
        {
            if (value != DetectUrls)
            {
                _richTextBoxFlags[s_autoUrlDetectSection] = value ? 1 : 0;
                if (IsHandleCreated)
                {
                    PInvokeCore.SendMessage(this, PInvokeCore.EM_AUTOURLDETECT, (WPARAM)(BOOL)(value));
                    RecreateHandle();
                }
            }
        }
    }

    protected override Size DefaultSize => new(100, 96);

    /// <summary>
    ///  We can't just enable drag/drop of text by default: it's a breaking change.
    ///  Should be false by default.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.RichTextBoxEnableAutoDragDrop))]
    public bool EnableAutoDragDrop
    {
        get => _richTextBoxFlags[s_enableAutoDragDropSection] != 0;
        set
        {
            _richTextBoxFlags[s_enableAutoDragDropSection] = value ? 1 : 0;
            UpdateOleCallback();
        }
    }

    public override Color ForeColor
    {
        get => base.ForeColor;
        set
        {
            if (IsHandleCreated)
            {
                if (InternalSetForeColor(value))
                {
                    base.ForeColor = value;
                }
            }
            else
            {
                base.ForeColor = value;
            }
        }
    }

    [AllowNull]
    public override Font Font
    {
        get => base.Font;
        set
        {
            if (!IsHandleCreated || PInvokeCore.GetWindowTextLength(this) <= 0)
            {
                base.Font = value;
                return;
            }

            if (value is null)
            {
                base.Font = null;
                SetCharFormatFont(selectionOnly: false, Font);
                return;
            }

            try
            {
                Font? font = GetCharFormatFont(selectionOnly: false);
                if (font is null || !font.Equals(value))
                {
                    SetCharFormatFont(selectionOnly: false, value);

                    // Update controlfont from "resolved" font from the attempt to set the document font.
                    CallOnContentsResized = true;
                    base.Font = GetCharFormatFont(selectionOnly: false);
                }
            }
            finally
            {
                CallOnContentsResized = false;
            }
        }
    }

    internal override Size GetPreferredSizeCore(Size proposedConstraints)
    {
        if (EffectiveVisualStylesMode >= VisualStylesMode.Net11)
        {
            // TextBoxBase owns the modern inset, user Padding, and scrollbar geometry.
            return base.GetPreferredSizeCore(proposedConstraints);
        }

        Size scrollBarPadding = Size.Empty;

        // If the RTB is multiline, we won't have a horizontal scrollbar.
        if (!WordWrap && Multiline && (ScrollBars & RichTextBoxScrollBars.Horizontal) != 0)
        {
            scrollBarPadding.Height += SystemInformation.HorizontalScrollBarHeight;
        }

        if (Multiline && (ScrollBars & RichTextBoxScrollBars.Vertical) != 0)
        {
            scrollBarPadding.Width += SystemInformation.VerticalScrollBarWidth;
        }

        // Subtract the scroll bar padding before measuring
        proposedConstraints -= scrollBarPadding;

        Size prefSize = base.GetPreferredSizeCore(proposedConstraints);

        return prefSize + scrollBarPadding;
    }

    private bool InConstructor
    {
        get => _richTextBoxFlags[s_fInCtorSection] != 0;
        set => _richTextBoxFlags[s_fInCtorSection] = value ? 1 : 0;
    }

    /// <summary>
    ///  Sets or gets the rich text box control' language option.
    ///  The IMF_AUTOFONT flag is set by default.
    ///  The IMF_AUTOKEYBOARD and IMF_IMECANCELCOMPLETE flags are cleared by default.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public RichTextBoxLanguageOptions LanguageOption
    {
        get => IsHandleCreated
            ? (RichTextBoxLanguageOptions)(int)PInvokeCore.SendMessage(this, PInvokeCore.EM_GETLANGOPTIONS)
            : _languageOption;
        set
        {
            if (LanguageOption != value)
            {
                _languageOption = value;
                if (IsHandleCreated)
                {
                    PInvokeCore.SendMessage(this, PInvokeCore.EM_SETLANGOPTIONS, 0, (nint)value);
                }
            }
        }
    }

    private bool LinkCursor
    {
        get => _richTextBoxFlags[s_linkcursorSection] != 0;
        set => _richTextBoxFlags[s_linkcursorSection] = value ? 1 : 0;
    }

    [DefaultValue(int.MaxValue)]
    public override int MaxLength
    {
        get => base.MaxLength;
        set => base.MaxLength = value;
    }

    [DefaultValue(true)]
    public override bool Multiline
    {
        get => base.Multiline;
        set => base.Multiline = value;
    }

    private bool ProtectedError
    {
        get => _richTextBoxFlags[s_protectedErrorSection] != 0;
        set => _richTextBoxFlags[s_protectedErrorSection] = value ? 1 : 0;
    }

    private protected override void RaiseAccessibilityTextChangedEvent()
    {
        // Do not do anything because Win32 provides unmanaged Text pattern for RichTextBox
    }

    /// <summary>
    ///  Returns the name of the action that will be performed if the user
    ///  Redo's their last Undone operation. If no operation can be redone,
    ///  an empty string ("") is returned.
    /// </summary>
    // NOTE: This is overridable, because we want people to be able to
    //      mess with the names if necessary...?
    [SRCategory(nameof(SR.CatBehavior))]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxRedoActionNameDescr))]
    public string RedoActionName
    {
        get
        {
            if (!CanRedo)
            {
                return string.Empty;
            }

            int n = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_GETREDONAME);
            return GetEditorActionName(n);
        }
    }

    // Description: Specifies whether rich text formatting keyboard shortcuts are enabled.
    [DefaultValue(true)]
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public bool RichTextShortcutsEnabled
    {
        get => _richTextBoxFlags[s_richTextShortcutsEnabledSection] != 0;
        set
        {
            s_shortcutsToDisable ??= [(int)Shortcut.CtrlL, (int)Shortcut.CtrlR, (int)Shortcut.CtrlE, (int)Shortcut.CtrlJ];

            _richTextBoxFlags[s_richTextShortcutsEnabledSection] = value ? 1 : 0;
        }
    }

    /// <summary>
    ///  The right margin of a RichTextBox control. A nonzero margin implies WordWrap.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(0)]
    [Localizable(true)]
    [SRDescription(nameof(SR.RichTextBoxRightMargin))]
    public unsafe int RightMargin
    {
        get => _rightMargin;
        set
        {
            if (_rightMargin != value)
            {
                ArgumentOutOfRangeException.ThrowIfNegative(value);

                _rightMargin = value;

                if (value == 0)
                {
                    // Once you set EM_SETTARGETDEVICE to something nonzero, RichEd will assume
                    // word wrap forever and ever.
                    RecreateHandle();
                }
                else if (IsHandleCreated)
                {
                    using CreateDcScope hdc = new("DISPLAY");
                    PInvokeCore.SendMessage(this, PInvokeCore.EM_SETTARGETDEVICE, (WPARAM)hdc, Pixel2Twip(value, true));
                }
            }
        }
    }

    /// <summary>
    ///  The text of a RichTextBox control, including all Rtf codes.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxRTF))]
    [RefreshProperties(RefreshProperties.All)]
    public string? Rtf
    {
        get
        {
            if (IsHandleCreated)
            {
                return StreamOut(PInvoke.SF_RTF);
            }
            else if (_textPlain is not null)
            {
                ForceHandleCreate();
                return StreamOut(PInvoke.SF_RTF);
            }
            else
            {
                return _textRtf;
            }
        }
        set
        {
            value ??= string.Empty;

            if (value.Equals(Rtf))
            {
                return;
            }

            ForceHandleCreate();
            _textRtf = value;
            StreamIn(value, PInvoke.SF_RTF);
            if (CanRaiseTextChangedEvent)
            {
                OnTextChanged(EventArgs.Empty);
            }
        }
    }

    /// <summary>
    ///  The current scrollbar settings for a multi-line rich edit control.
    ///  Possible return values are given by the RichTextBoxScrollBars enumeration.
    /// </summary>
    [SRCategory(nameof(SR.CatAppearance))]
    [DefaultValue(RichTextBoxScrollBars.Both)]
    [Localizable(true)]
    [SRDescription(nameof(SR.RichTextBoxScrollBars))]
    public RichTextBoxScrollBars ScrollBars
    {
        get => (RichTextBoxScrollBars)_richTextBoxFlags[s_scrollBarsSection];
        set
        {
            SourceGenerated.EnumValidator.Validate(value);

            if (value != ScrollBars)
            {
                using (LayoutTransaction.CreateTransactionIf(AutoSize, ParentInternal, this, PropertyNames.ScrollBars))
                {
                    _richTextBoxFlags[s_scrollBarsSection] = (int)value;
                    CommonProperties.xClearPreferredSizeCache(this);
                    RecreateHandle();
                }
            }
        }
    }

    /// <summary>
    ///  The alignment of the paragraphs in a RichTextBox control.
    /// </summary>
    [DefaultValue(HorizontalAlignment.Left)]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelAlignment))]
    public unsafe HorizontalAlignment SelectionAlignment
    {
        get
        {
            HorizontalAlignment selectionAlignment = HorizontalAlignment.Left;

            ForceHandleCreate();
            PARAFORMAT pf = new()
            {
                cbSize = (uint)sizeof(PARAFORMAT)
            };

            // Get the format for our currently selected paragraph.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_GETPARAFORMAT, 0, ref pf);

            // check if alignment has been set yet
            if ((PFM.ALIGNMENT & pf.dwMask) != 0)
            {
                switch (pf.wAlignment)
                {
                    case PFA.LEFT:
                        selectionAlignment = HorizontalAlignment.Left;
                        break;

                    case PFA.RIGHT:
                        selectionAlignment = HorizontalAlignment.Right;
                        break;

                    case PFA.CENTER:
                        selectionAlignment = HorizontalAlignment.Center;
                        break;
                }
            }

            return selectionAlignment;
        }
        set
        {
            // valid values are 0x0 to 0x2
            SourceGenerated.EnumValidator.Validate(value);

            ForceHandleCreate();
            PARAFORMAT pf = new()
            {
                cbSize = (uint)sizeof(PARAFORMAT),
                dwMask = PFM.ALIGNMENT
            };

            switch (value)
            {
                case HorizontalAlignment.Left:
                    pf.wAlignment = PFA.LEFT;
                    break;

                case HorizontalAlignment.Right:
                    pf.wAlignment = PFA.RIGHT;
                    break;

                case HorizontalAlignment.Center:
                    pf.wAlignment = PFA.CENTER;
                    break;
            }

            // Set the format for our current paragraph or selection.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETPARAFORMAT, 0, ref pf);
        }
    }

    /// <summary>
    ///  Determines if a paragraph in the RichTextBox control
    ///  contains the current selection or insertion point has the bullet style.
    /// </summary>
    [DefaultValue(false)]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelBullet))]
    public unsafe bool SelectionBullet
    {
        get
        {
            RichTextBoxSelectionAttribute selectionBullet = RichTextBoxSelectionAttribute.None;

            ForceHandleCreate();
            PARAFORMAT pf = new()
            {
                cbSize = (uint)sizeof(PARAFORMAT)
            };

            // Get the format for our currently selected paragraph.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_GETPARAFORMAT, 0, ref pf);

            // check if alignment has been set yet
            if ((PFM.NUMBERING & pf.dwMask) != 0)
            {
                if (pf.wNumbering == PARAFORMAT_NUMBERING.PFN_BULLET)
                {
                    selectionBullet = RichTextBoxSelectionAttribute.All;
                }
            }
            else
            {
                // For paragraphs with mixed SelectionBullets, we just return false
                return false;
            }

            return selectionBullet == RichTextBoxSelectionAttribute.All;
        }
        set
        {
            ForceHandleCreate();

            PARAFORMAT pf = new()
            {
                cbSize = (uint)sizeof(PARAFORMAT),
                dwMask = PFM.NUMBERING | PFM.OFFSET
            };

            if (!value)
            {
                pf.wNumbering = 0;
                pf.dxOffset = 0;
            }
            else
            {
                pf.wNumbering = PARAFORMAT_NUMBERING.PFN_BULLET;
                pf.dxOffset = Pixel2Twip(_bulletIndent, true);
            }

            // Set the format for our current paragraph or selection.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETPARAFORMAT, 0, ref pf);
        }
    }

    /// <summary>
    ///  Determines whether text in the RichTextBox control
    ///  appears on the baseline (normal), as a superscript above the baseline,
    ///  or as a subscript below the baseline.
    /// </summary>
    [DefaultValue(0)]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelCharOffset))]
    public unsafe int SelectionCharOffset
    {
        get
        {
            ForceHandleCreate();
            CHARFORMAT2W cf = GetCharFormat(true);
            return Twip2Pixel(cf.yOffset, false);
        }
        set
        {
            ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 2000);
            ArgumentOutOfRangeException.ThrowIfLessThan(value, -2000);

            ForceHandleCreate();
            CHARFORMAT2W cf = new()
            {
                cbSize = (uint)sizeof(CHARFORMAT2W),
                dwMask = CFM_MASK.CFM_OFFSET,
                yOffset = Pixel2Twip(value, false)
            };

            // Set the format information.
            //
            // SendMessage will force the handle to be created if it hasn't already. Normally,
            // we would cache property values until the handle is created - but for this property,
            // it's far more simple to just create the handle.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETCHARFORMAT, PInvoke.SCF_SELECTION, ref cf);
        }
    }

    /// <summary>
    ///  The color of the currently selected text in the RichTextBox control.
    /// </summary>
    /// <returns>The color or <see cref="Color.Empty"/> if the selection has more than one color.</returns>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelColor))]
    public Color SelectionColor
    {
        get
        {
            Color selColor = Color.Empty;

            ForceHandleCreate();
            CHARFORMAT2W cf = GetCharFormat(true);
            // if the effects member contains valid info
            if ((cf.dwMask & CFM_MASK.CFM_COLOR) != 0)
            {
                selColor = ColorTranslator.FromOle(cf.crTextColor);
            }

            return selColor;
        }
        set
        {
            ForceHandleCreate();
            CHARFORMAT2W cf = GetCharFormat(true);
            cf.dwMask = CFM_MASK.CFM_COLOR;
            cf.dwEffects = 0;
            cf.crTextColor = ColorTranslator.ToWin32(value);

            // Set the format information.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETCHARFORMAT, (WPARAM)PInvoke.SCF_SELECTION, ref cf);
        }
    }

    /// <summary>
    ///  The background color of the currently selected text in the RichTextBox control.
    ///  Returns Color.Empty if the selection has more than one color.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelBackColor))]
    public unsafe Color SelectionBackColor
    {
        get
        {
            Color selColor = Color.Empty;

            if (IsHandleCreated)
            {
                CHARFORMAT2W cf2 = GetCharFormat(true);
                // If the effects member contains valid info
                if ((cf2.dwEffects & CFE_EFFECTS.CFE_AUTOBACKCOLOR) != 0)
                {
                    selColor = BackColor;
                }
                else if ((cf2.dwMask & CFM_MASK.CFM_BACKCOLOR) != 0)
                {
                    selColor = ColorTranslator.FromOle(cf2.crBackColor);
                }
            }
            else
            {
                selColor = _selectionBackColorToSetOnHandleCreated;
            }

            return selColor;
        }
        set
        {
            // Note: don't compare the value to the old value here: it's possible that
            // you have a different range selected.
            _selectionBackColorToSetOnHandleCreated = value;
            if (IsHandleCreated)
            {
                CHARFORMAT2W cf2 = new()
                {
                    cbSize = (uint)sizeof(CHARFORMAT2W)
                };

                if (value == Color.Empty)
                {
                    cf2.dwEffects = CFE_EFFECTS.CFE_AUTOBACKCOLOR;
                }
                else
                {
                    cf2.dwMask = CFM_MASK.CFM_BACKCOLOR;
                    cf2.crBackColor = ColorTranslator.ToWin32(value);
                }

                PInvokeCore.SendMessage(this, PInvokeCore.EM_SETCHARFORMAT, (WPARAM)PInvoke.SCF_SELECTION, ref cf2);
            }
        }
    }

    /// <summary>
    ///  The font used to display the currently selected text
    ///  or the characters(s) immediately following the insertion point in the
    ///  RichTextBox control. Null if the selection has more than one font.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelFont))]
    [DisallowNull]
    public Font? SelectionFont
    {
        get => GetCharFormatFont(true);
        set => SetCharFormatFont(true, value);
    }

    /// <summary>
    ///  The distance (in pixels) between the left edge of the first line of text
    ///  in the selected paragraph(s) (as specified by the SelectionIndent property)
    ///  and the left edge of subsequent lines of text in the same paragraph(s).
    /// </summary>
    [DefaultValue(0)]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelHangingIndent))]
    public unsafe int SelectionHangingIndent
    {
        get
        {
            int selHangingIndent = 0;

            ForceHandleCreate();
            PARAFORMAT pf = new()
            {
                cbSize = (uint)sizeof(PARAFORMAT)
            };

            // Get the format for our currently selected paragraph.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_GETPARAFORMAT, 0, ref pf);

            // Check if alignment has been set yet.
            if ((PFM.OFFSET & pf.dwMask) != 0)
            {
                selHangingIndent = pf.dxOffset;
            }

            return Twip2Pixel(selHangingIndent, true);
        }
        set
        {
            ForceHandleCreate();

            PARAFORMAT pf = new()
            {
                cbSize = (uint)sizeof(PARAFORMAT),
                dwMask = PFM.OFFSET,
                dxOffset = Pixel2Twip(value, true)
            };

            // Set the format for our current paragraph or selection.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETPARAFORMAT, 0, ref pf);
        }
    }

    /// <summary>
    ///  The distance (in pixels) between the left edge of the RichTextBox control and
    ///  the left edge of the text that is selected or added at the current
    ///  insertion point.
    /// </summary>
    [DefaultValue(0)]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelIndent))]
    public unsafe int SelectionIndent
    {
        get
        {
            int selIndent = 0;

            ForceHandleCreate();
            PARAFORMAT pf = new()
            {
                cbSize = (uint)sizeof(PARAFORMAT)
            };

            // Get the format for our currently selected paragraph.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_GETPARAFORMAT, 0, ref pf);

            // Check if alignment has been set yet.
            if ((PFM.STARTINDENT & pf.dwMask) != 0)
            {
                selIndent = pf.dxStartIndent;
            }

            return Twip2Pixel(selIndent, true);
        }
        set
        {
            ForceHandleCreate();

            PARAFORMAT pf = new()
            {
                cbSize = (uint)sizeof(PARAFORMAT),
                dwMask = PFM.STARTINDENT,
                dxStartIndent = Pixel2Twip(value, true)
            };

            // Set the format for our current paragraph or selection.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETPARAFORMAT, 0, ref pf);
        }
    }

    /// <summary>
    ///  Gets or sets the number of characters selected in the text
    ///  box.
    /// </summary>
    [SRCategory(nameof(SR.CatAppearance))]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.TextBoxSelectionLengthDescr))]
    public override int SelectionLength
    {
        get
        {
            if (!IsHandleCreated)
            {
                return base.SelectionLength;
            }

            // RichTextBox allows the user to select the EOF character,
            // but we don't want to include this in the SelectionLength.
            // So instead of sending EM_GETSEL, we just obtain the SelectedText and return
            // the length of it.
            //
            return SelectedText.Length;
        }
        set => base.SelectionLength = value;
    }

    /// <summary>
    ///  true if the current selection prevents any changes to its contents.
    /// </summary>
    [DefaultValue(false)]
    [SRDescription(nameof(SR.RichTextBoxSelProtected))]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public bool SelectionProtected
    {
        get
        {
            ForceHandleCreate();
            return GetCharFormat(CFM_MASK.CFM_PROTECTED, CFE_EFFECTS.CFE_PROTECTED) == RichTextBoxSelectionAttribute.All;
        }
        set
        {
            ForceHandleCreate();
            SetCharFormat(CFM_MASK.CFM_PROTECTED, value ? CFE_EFFECTS.CFE_PROTECTED : 0, RichTextBoxSelectionAttribute.All);
        }
    }

    /// <summary>
    ///  The currently selected text of a RichTextBox control, including
    ///  all Rtf codes.
    /// </summary>
    [DefaultValue("")]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelRTF))]
    [AllowNull]
    public string SelectedRtf
    {
        get
        {
            ForceHandleCreate();
            return StreamOut(PInvoke.SFF_SELECTION | PInvoke.SF_RTF);
        }
        set
        {
            ForceHandleCreate();
            value ??= string.Empty;

            StreamIn(value, PInvoke.SFF_SELECTION | PInvoke.SF_RTF);
        }
    }

    /// <summary>
    ///  The distance (in pixels) between the right edge of the RichTextBox control and
    ///  the right edge of the text that is selected or added at the current
    ///  insertion point.
    /// </summary>
    [DefaultValue(0)]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelRightIndent))]
    public unsafe int SelectionRightIndent
    {
        get
        {
            int selRightIndent = 0;

            ForceHandleCreate();

            PARAFORMAT pf = new()
            {
                cbSize = (uint)sizeof(PARAFORMAT)
            };

            // Get the format for our currently selected paragraph.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_GETPARAFORMAT, 0, ref pf);

            // Check if alignment has been set yet.
            if ((PFM.RIGHTINDENT & pf.dwMask) != 0)
            {
                selRightIndent = pf.dxRightIndent;
            }

            return Twip2Pixel(selRightIndent, true);
        }
        set
        {
            ArgumentOutOfRangeException.ThrowIfNegative(value);

            ForceHandleCreate();
            PARAFORMAT pf = new()
            {
                cbSize = (uint)sizeof(PARAFORMAT),
                dwMask = PFM.RIGHTINDENT,
                dxRightIndent = Pixel2Twip(value, true)
            };

            // Set the format for our current paragraph or selection.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETPARAFORMAT, 0, ref pf);
        }
    }

    /// <summary>
    ///  The absolute tab positions (in pixels) of text in a RichTextBox control.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelTabs))]
    [AllowNull]
    public unsafe int[] SelectionTabs
    {
        get
        {
            int[] selTabs = [];

            ForceHandleCreate();
            PARAFORMAT pf = new()
            {
                cbSize = (uint)sizeof(PARAFORMAT)
            };

            // get the format for our currently selected paragraph
            PInvokeCore.SendMessage(this, PInvokeCore.EM_GETPARAFORMAT, 0, ref pf);

            // check if alignment has been set yet
            if ((PFM.TABSTOPS & pf.dwMask) != 0)
            {
                selTabs = new int[pf.cTabCount];
                for (int x = 0; x < pf.cTabCount; x++)
                {
                    selTabs[x] = Twip2Pixel(pf.rgxTabs[x], true);
                }
            }

            return selTabs;
        }
        set
        {
            // Verify the argument, and throw an error if is bad
            if (value is not null && value.Length > PInvoke.MAX_TAB_STOPS)
            {
                throw new ArgumentOutOfRangeException(nameof(value), SR.SelTabCountRange);
            }

            ForceHandleCreate();
            PARAFORMAT pf = new()
            {
                cbSize = (uint)sizeof(PARAFORMAT)
            };

            // get the format for our currently selected paragraph because
            // we need to get the number of tabstops to copy
            PInvokeCore.SendMessage(this, PInvokeCore.EM_GETPARAFORMAT, 0, ref pf);

            pf.cTabCount = (short)((value is null) ? 0 : value.Length);
            pf.dwMask = PFM.TABSTOPS;
            for (int x = 0; x < pf.cTabCount; x++)
            {
                pf.rgxTabs[x] = Pixel2Twip(value![x], true);
            }

            // Set the format for our current paragraph or selection.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETPARAFORMAT, 0, ref pf);
        }
    }

    /// <summary>
    ///  The currently selected text of a RichTextBox control; consists of a
    ///  zero length string if no characters are selected.
    /// </summary>
    [DefaultValue("")]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelText))]
    [AllowNull]
    public override string SelectedText
    {
        get
        {
            ForceHandleCreate();
            return GetTextEx(GETTEXTEX_FLAGS.GT_SELECTION);
        }
        set
        {
            ForceHandleCreate();
            value ??= string.Empty;
            StreamIn(value, PInvoke.SFF_SELECTION | PInvoke.SF_TEXT | PInvoke.SF_UNICODE);
        }
    }

    /// <summary>
    ///  The type of the current selection. The returned value is one
    ///  of the values enumerated in RichTextBoxSelectionType.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxSelTypeDescr))]
    public RichTextBoxSelectionTypes SelectionType
    {
        get
        {
            ForceHandleCreate();
            if (SelectionLength > 0)
            {
                int n = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_SELECTIONTYPE);
                return (RichTextBoxSelectionTypes)n;
            }
            else
            {
                return RichTextBoxSelectionTypes.Empty;
            }
        }
    }

    /// <summary>
    ///  Whether or not the left edge of the control will have a "selection margin" which
    ///  can be used to select entire lines
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.RichTextBoxSelMargin))]
    public bool ShowSelectionMargin
    {
        get { return _richTextBoxFlags[s_showSelBarSection] != 0; }
        set
        {
            if (value != ShowSelectionMargin)
            {
                _richTextBoxFlags[s_showSelBarSection] = value ? 1 : 0;
                if (IsHandleCreated)
                {
                    PInvokeCore.SendMessage(
                        this,
                        PInvokeCore.EM_SETOPTIONS,
                        (WPARAM)(int)(value ? PInvoke.ECOOP_OR : PInvoke.ECOOP_XOR),
                        (LPARAM)(int)PInvoke.ECO_SELECTIONBAR);
                }
            }
        }
    }

    [Localizable(true)]
    [RefreshProperties(RefreshProperties.All)]
    [AllowNull]
    public override string Text
    {
        get
        {
            if (IsDisposed)
            {
                return base.Text;
            }

            if (RecreatingHandle || GetAnyDisposingInHierarchy())
            {
                // We can return any old garbage if we're in the process of recreating the handle
                return string.Empty;
            }

            if (!IsHandleCreated && _textRtf is null)
            {
                return _textPlain is not null
                    ? _textPlain
                    : base.Text;
            }
            else
            {
                // if the handle is created, we are golden, however
                // if the handle isn't created, but textRtf was
                // specified, we need the RichEdit to translate
                // for us, so we must create the handle;
                //
                ForceHandleCreate();

                return GetTextEx();
            }
        }
        set
        {
            using (LayoutTransaction.CreateTransactionIf(AutoSize, ParentInternal, this, PropertyNames.Text))
            {
                _textRtf = null;
                if (!IsHandleCreated)
                {
                    _textPlain = value;
                }
                else
                {
                    _textPlain = null;
                    value ??= string.Empty;

                    StreamIn(value, PInvoke.SF_TEXT | PInvoke.SF_UNICODE);
                    // reset Modified
                    PInvokeCore.SendMessage(this, PInvokeCore.EM_SETMODIFY);
                }
            }
        }
    }

    private bool SuppressTextChangedEvent
    {
        get { return _richTextBoxFlags[s_suppressTextChangedEventSection] != 0; }
        set
        {
            bool oldValue = SuppressTextChangedEvent;
            if (value != oldValue)
            {
                _richTextBoxFlags[s_suppressTextChangedEventSection] = value ? 1 : 0;
                CommonProperties.xClearPreferredSizeCache(this);
            }
        }
    }

    [Browsable(false)]
    public override unsafe int TextLength
    {
        get
        {
            GETTEXTLENGTHEX gtl = new()
            {
                flags = GETTEXTLENGTHEX_FLAGS.GTL_NUMCHARS,
                codepage = 1200u /* CP_UNICODE */
            };

            return (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_GETTEXTLENGTHEX, (WPARAM)(&gtl));
        }
    }

    /// <summary>
    ///  Returns the name of the action that will be undone if the user
    ///  Undo's their last operation. If no operation can be undone, it will
    ///  return an empty string ("").
    /// </summary>
    // NOTE: This is overridable, because we want people to be able to
    //      mess with the names if necessary...?
    [SRCategory(nameof(SR.CatBehavior))]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.RichTextBoxUndoActionNameDescr))]
    public string UndoActionName
    {
        get
        {
            if (!CanUndo)
            {
                return string.Empty;
            }

            int n = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_GETUNDONAME);
            return GetEditorActionName(n);
        }
    }

    private static string GetEditorActionName(int actionID)
    {
        switch (actionID)
        {
            case 0:
                return SR.RichTextBox_IDUnknown;
            case 1:
                return SR.RichTextBox_IDTyping;
            case 2:
                return SR.RichTextBox_IDDelete;
            case 3:
                return SR.RichTextBox_IDDragDrop;
            case 4:
                return SR.RichTextBox_IDCut;
            case 5:
                return SR.RichTextBox_IDPaste;
            default:
                goto
            case 0;
        }
    }

    /// <summary>
    ///  The current zoom level for the RichTextBox control. This may be between 1/64 and 64. 1.0 indicates
    ///  no zoom (i.e. normal viewing). Zoom works best with TrueType fonts;
    ///  for non-TrueType fonts, ZoomFactor will be treated as the nearest whole number.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(1.0f)]
    [Localizable(true)]
    [SRDescription(nameof(SR.RichTextBoxZoomFactor))]
    public unsafe float ZoomFactor
    {
        get
        {
            if (IsHandleCreated)
            {
                int numerator = 0;
                int denominator = 0;
                PInvokeCore.SendMessage(this, PInvokeCore.EM_GETZOOM, (WPARAM)(&numerator), ref denominator);

                _zoomMultiplier = (numerator != 0) && (denominator != 0)
                    ? numerator / ((float)denominator)
                    : 1.0f;

                return _zoomMultiplier;
            }
            else
            {
                return _zoomMultiplier;
            }
        }

        set
        {
            if (!float.IsNaN(value))
            {
                ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(value, 0.015625f);
                ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(value, 64.0f);
            }

            if (value != _zoomMultiplier)
            {
                SendZoomFactor(value);
            }
        }
    }

    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.RichTextBoxContentsResized))]
    public event ContentsResizedEventHandler? ContentsResized
    {
        add => Events.AddHandler(s_requestSizeEvent, value);
        remove => Events.RemoveHandler(s_requestSizeEvent, value);
    }

    /// <summary>
    ///  RichTextBox controls have built-in drag and drop support, but AllowDrop, DragEnter, DragDrop
    ///  may still be used: this should be hidden in the property grid, but not in code
    /// </summary>
    [Browsable(false)]
    public new event DragEventHandler? DragDrop
    {
        add => base.DragDrop += value;
        remove => base.DragDrop -= value;
    }

    /// <summary>
    ///  RichTextBox controls have built-in drag and drop support, but AllowDrop, DragEnter, DragDrop
    ///  may still be used: this should be hidden in the property grid, but not in code
    /// </summary>
    [Browsable(false)]
    public new event DragEventHandler? DragEnter
    {
        add => base.DragEnter += value;
        remove => base.DragEnter -= value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event EventHandler? DragLeave
    {
        add => base.DragLeave += value;
        remove => base.DragLeave -= value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event DragEventHandler? DragOver
    {
        add => base.DragOver += value;
        remove => base.DragOver -= value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event GiveFeedbackEventHandler? GiveFeedback
    {
        add => base.GiveFeedback += value;
        remove => base.GiveFeedback -= value;
    }

    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new event QueryContinueDragEventHandler? QueryContinueDrag
    {
        add => base.QueryContinueDrag += value;
        remove => base.QueryContinueDrag -= value;
    }

    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.RichTextBoxHScroll))]
    public event EventHandler? HScroll
    {
        add => Events.AddHandler(s_hscrollEvent, value);
        remove => Events.RemoveHandler(s_hscrollEvent, value);
    }

    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.RichTextBoxLinkClick))]
    public event LinkClickedEventHandler? LinkClicked
    {
        add => Events.AddHandler(s_linkActivateEvent, value);
        remove => Events.RemoveHandler(s_linkActivateEvent, value);
    }

    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.RichTextBoxIMEChange))]
    public event EventHandler? ImeChange
    {
        add => Events.AddHandler(s_imeChangeEvent, value);
        remove => Events.RemoveHandler(s_imeChangeEvent, value);
    }

    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.RichTextBoxProtected))]
    public event EventHandler? Protected
    {
        add => Events.AddHandler(s_protectedEvent, value);
        remove => Events.RemoveHandler(s_protectedEvent, value);
    }

    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.RichTextBoxSelChange))]
    public event EventHandler? SelectionChanged
    {
        add => Events.AddHandler(s_selectionChangeEvent, value);
        remove => Events.RemoveHandler(s_selectionChangeEvent, value);
    }

    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.RichTextBoxVScroll))]
    public event EventHandler? VScroll
    {
        add => Events.AddHandler(s_vscrollEvent, value);
        remove => Events.RemoveHandler(s_vscrollEvent, value);
    }

    /// <summary>
    ///  Returns a boolean indicating whether the RichTextBoxConstants control can paste the
    ///  given clipboard format.
    /// </summary>
    public bool CanPaste(DataFormats.Format clipFormat)
        => PInvokeCore.SendMessage(this, PInvokeCore.EM_CANPASTE, (WPARAM)clipFormat.Id) != 0;

    // DrawToBitmap doesn't work for this control, so we should hide it. We'll
    // still call base so that this has a chance to work if it can.
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new void DrawToBitmap(Bitmap bitmap, Rectangle targetBounds)
    {
        base.DrawToBitmap(bitmap, targetBounds);
    }

    private unsafe int EditStreamProc(nint dwCookie, nint buf, int cb, out int transferred)
    {
        int ret = 0;    // assume that everything is Okay

        byte[] bytes = new byte[cb];

        int cookieVal = (int)dwCookie;

        transferred = 0;
        try
        {
            switch (cookieVal & DIRECTIONMASK)
            {
                case OUTPUT:
                    {
                        _editStream ??= new MemoryStream();

                        switch (cookieVal & KINDMASK)
                        {
                            case RTF:
                            case TEXTCRLF:
                                Marshal.Copy(buf, bytes, 0, cb);
                                _editStream.Write(bytes, 0, cb);
                                break;
                            case TEXTLF:
                                // Strip out \r characters so that we consistently return
                                // \n for linefeeds. In a future version the RichEdit control
                                // may support a SF_NOXLATCRLF flag which would do this for
                                // us. Internally the RichEdit stores the text with only
                                // a \n, so we want to keep that the same here.
                                //
                                if ((cookieVal & UNICODE) != 0)
                                {
                                    Debug.Assert(cb % 2 == 0, "EditStreamProc call out of cycle. Expected to always get character boundary calls");
                                    int requestedCharCount = cb / 2;
                                    int consumedCharCount = 0;

                                    fixed (byte* pb = bytes)
                                    {
                                        char* pChars = (char*)pb;
                                        char* pBuffer = (char*)(long)buf;

                                        for (int i = 0; i < requestedCharCount; i++)
                                        {
                                            if (*pBuffer == '\r')
                                            {
                                                pBuffer++;
                                                continue;
                                            }

                                            *pChars = *pBuffer;
                                            pChars++;
                                            pBuffer++;
                                            consumedCharCount++;
                                        }
                                    }

                                    _editStream.Write(bytes, 0, consumedCharCount * 2);
                                }
                                else
                                {
                                    int requestedCharCount = cb;
                                    int consumedCharCount = 0;

                                    fixed (byte* pb = bytes)
                                    {
                                        byte* pChars = pb;
                                        byte* pBuffer = (byte*)(long)buf;

                                        for (int i = 0; i < requestedCharCount; i++)
                                        {
                                            if (*pBuffer == (byte)'\r')
                                            {
                                                pBuffer++;
                                                continue;
                                            }

                                            *pChars = *pBuffer;
                                            pChars++;
                                            pBuffer++;
                                            consumedCharCount++;
                                        }
                                    }

                                    _editStream.Write(bytes, 0, consumedCharCount);
                                }

                                break;
                        }

                        // set up number of bytes transferred
                        transferred = cb;
                        break;
                    }

                case INPUT:
                    {
                        // Several customers complained that they were getting Random NullReference exceptions inside EditStreamProc.
                        // We had a case of a customer using Everett bits and another case of a customer using Whidbey Beta1 bits.
                        // We don't have a repro in house which makes it problematic to determine the cause for this behavior.
                        // Looking at the code it seems that the only possibility for editStream to be null is when the user
                        // calls RichTextBox::LoadFile(Stream, RichTextBoxStreamType) with a null Stream.
                        // However, the user said that his app is not using LoadFile method.
                        // The only possibility left open is that the native Edit control sends random calls into EditStreamProc.
                        // We have to guard against this.
                        if (_editStream is not null)
                        {
                            transferred = _editStream.Read(bytes, 0, cb);

                            Marshal.Copy(bytes, 0, buf, transferred);
                            // set up number of bytes transferred
                            if (transferred < 0)
                            {
                                transferred = 0;
                            }
                        }
                        else
                        {
                            // Set transferred to 0 so the native Edit controls knows that they should stop calling our EditStreamProc.
                            transferred = 0;
                        }

                        break;
                    }
            }
        }
#if DEBUG
        catch (IOException e)
        {
            Debug.Fail("Failed to edit proc operation.", e.ToString());
            transferred = 0;
            ret = 1;
        }
#else
        catch (IOException)
        {
            transferred = 0;
            ret = 1;
        }
#endif

        return ret;       // tell the RichTextBoxConstants how we are doing 0 - Okay, 1 - quit
    }

    /// <summary>
    ///  Searches the text in a RichTextBox control for a given string.
    /// </summary>
    public int Find(string str)
    {
        return Find(str, 0, 0, RichTextBoxFinds.None);
    }

    /// <summary>
    ///  Searches the text in a RichTextBox control for a given string.
    /// </summary>
    public int Find(string str, RichTextBoxFinds options)
    {
        return Find(str, 0, 0, options);
    }

    /// <summary>
    ///  Searches the text in a RichTextBox control for a given string.
    /// </summary>
    public int Find(string str, int start, RichTextBoxFinds options)
    {
        return Find(str, start, -1, options);
    }

    /// <summary>
    ///  Searches the text in a RichTextBox control for a given string.
    /// </summary>
    public unsafe int Find(string str, int start, int end, RichTextBoxFinds options)
    {
        ArgumentNullException.ThrowIfNull(str);

        // Perform the find, will return ubyte position
        int position = FindInternal(str, start, end, options);

        // if we didn't find anything, or we don't have to select what was found,
        // we're done
        bool selectWord = (options & RichTextBoxFinds.NoHighlight) != RichTextBoxFinds.NoHighlight;
        if (position != -1 && selectWord)
        {
            // Select the string found, this is done in ubyte units
            CHARRANGE chrg = new()
            {
                cpMin = position
            };

            // Look for kashidas in the string. A kashida is an Arabic visual justification character
            // that's not semantically meaningful. Searching for ABC might find AB_C (where A,B, and C
            // represent Arabic characters and _ represents a kashida). We should highlight the text
            // including the kashida.
            const char kashida = (char)0x640;
            ReadOnlySpan<char> kashidaString = [kashida];

            // Using FindInternal here because RichEdit handles position/length differently than .NET strings
            // depending on characters and text formatting elements involved.
            int startIndex = FindInternal(kashidaString, position, position + str.Length, options);
            if (startIndex == -1)
            {
                // No kashida in the string
                chrg.cpMax = position + str.Length;
            }
            else
            {
                // There's at least one kashida
                int searchingCursor; // index into search string
                int foundCursor; // index into Text
                for (searchingCursor = startIndex, foundCursor = position + startIndex; searchingCursor < str.Length;
                    searchingCursor++, foundCursor++)
                {
                    while (FindInternal(kashidaString, foundCursor, foundCursor + 1, options) != -1 && str[searchingCursor] != kashida)
                    {
                        foundCursor++;
                    }
                }

                chrg.cpMax = foundCursor;
            }

            PInvokeCore.SendMessage(this, PInvokeCore.EM_EXSETSEL, 0, ref chrg);
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SCROLLCARET);
        }

        return position;

        unsafe int FindInternal(ReadOnlySpan<char> str, int start, int end, RichTextBoxFinds options)
        {
            int textLen = TextLength;
            ArgumentOutOfRangeException.ThrowIfNegative(start);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(start, textLen);

            if (end < -1)
            {
                throw new ArgumentOutOfRangeException(nameof(end), end, string.Format(SR.RichTextFindEndInvalid, end));
            }

            if (end == -1)
            {
                end = textLen;
            }

            if (start > end)
            {
                throw new ArgumentException(string.Format(SR.RichTextFindEndInvalid, end));
            }

            FINDTEXTW ft = default;
            if ((options & RichTextBoxFinds.Reverse) != RichTextBoxFinds.Reverse)
            {
                // normal
                ft.chrg.cpMin = start;
                ft.chrg.cpMax = end;
            }
            else
            {
                // reverse
                ft.chrg.cpMin = end;
                ft.chrg.cpMax = start;
            }

            // force complete search if we ended up with a zero length search
            if (ft.chrg.cpMin == ft.chrg.cpMax)
            {
                if ((options & RichTextBoxFinds.Reverse) != RichTextBoxFinds.Reverse)
                {
                    ft.chrg.cpMin = 0;
                    ft.chrg.cpMax = -1;
                }
                else
                {
                    ft.chrg.cpMin = textLen;
                    ft.chrg.cpMax = 0;
                }
            }

            // set up the options for the search
            FINDREPLACE_FLAGS findOptions = 0;
            if ((options & RichTextBoxFinds.WholeWord) == RichTextBoxFinds.WholeWord)
            {
                findOptions |= FINDREPLACE_FLAGS.FR_WHOLEWORD;
            }

            if ((options & RichTextBoxFinds.MatchCase) == RichTextBoxFinds.MatchCase)
            {
                findOptions |= FINDREPLACE_FLAGS.FR_MATCHCASE;
            }

            if ((options & RichTextBoxFinds.Reverse) != RichTextBoxFinds.Reverse)
            {
                // The default for RichEdit 2.0 is to search in reverse
                findOptions |= FINDREPLACE_FLAGS.FR_DOWN;
            }

            // Perform the find, will return ubyte position
            int position;
            fixed (char* pText = str)
            {
                ft.lpstrText = pText;
                position = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_FINDTEXT, (WPARAM)(uint)findOptions, ref ft);
            }

            return position;
        }
    }

    /// <summary>
    ///  Searches the text in a RichTextBox control for the given characters.
    /// </summary>
    public int Find(char[] characterSet)
    {
        return Find(characterSet, 0, -1);
    }

    /// <summary>
    ///  Searches the text in a RichTextBox control for the given characters.
    /// </summary>
    public int Find(char[] characterSet, int start)
    {
        return Find(characterSet, start, -1);
    }

    /// <summary>
    ///  Searches the text in a RichTextBox control for the given characters.
    /// </summary>
    public int Find(char[] characterSet, int start, int end)
    {
        // Code used to support ability to search backwards and negate character sets.
        // The API no longer supports this, but in case we change our mind, I'm leaving
        // the ability in here.
        bool forward = true;
        bool negate = false;

        int textLength = TextLength;

        ArgumentNullException.ThrowIfNull(characterSet);
        ArgumentOutOfRangeException.ThrowIfNegative(start);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(start, textLength);

        if (end != -1)
        {
            ArgumentOutOfRangeException.ThrowIfLessThan(end, start);
        }

        // Don't do anything if we get nothing to look for
        if (characterSet.Length == 0)
        {
            return -1;
        }

        textLength = PInvokeCore.GetWindowTextLength(this);
        if (start == end)
        {
            start = 0;
            end = textLength;
        }

        if (end == -1)
        {
            end = textLength;
        }

        CHARRANGE chrg = default; // The range of characters we have searched
        chrg.cpMax = chrg.cpMin = start;

        // Use the TEXTRANGE to move our text buffer forward
        // or backwards within the main text
        TEXTRANGE txrg = new()
        {
            chrg = new CHARRANGE
            {
                cpMin = chrg.cpMin,
                cpMax = chrg.cpMax
            }
        };

        // Characters we have slurped into memory in order to search
        UnicodeCharBuffer charBuffer = new(CHAR_BUFFER_LEN + 1);
        txrg.lpstrText = charBuffer.AllocCoTaskMem();
        if (txrg.lpstrText == 0)
        {
            throw new OutOfMemoryException();
        }

        try
        {
            bool done = false;

            // We want to loop as long as it takes. This loop will grab a
            // chunk of text out from the control as directed by txrg.chrg;
            while (!done)
            {
                if (forward)
                {
                    // Move forward by starting at the end of the
                    // previous text window and extending by the
                    // size of our buffer
                    txrg.chrg.cpMin = chrg.cpMax;
                    txrg.chrg.cpMax += CHAR_BUFFER_LEN;
                }
                else
                {
                    // Move backwards by anchoring at the start
                    // of the previous buffer window, and backing
                    // up by the desired size of our buffer
                    txrg.chrg.cpMax = chrg.cpMin;
                    txrg.chrg.cpMin -= CHAR_BUFFER_LEN;

                    // We need to keep our request within the
                    // lower bound of zero
                    if (txrg.chrg.cpMin < 0)
                    {
                        txrg.chrg.cpMin = 0;
                    }
                }

                if (end != -1)
                {
                    txrg.chrg.cpMax = Math.Min(txrg.chrg.cpMax, end);
                }

                // go get the text in this range, if we didn't get any text then punt
                int len;
                len = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_GETTEXTRANGE, 0, ref txrg);
                if (len == 0)
                {
                    chrg.cpMax = chrg.cpMin = -1; // Hit end of control without finding what we wanted
                    break;
                }

                // get the data from RichTextBoxConstants into a string for us to use.
                charBuffer.PutCoTaskMem(txrg.lpstrText);
                string str = charBuffer.GetString();

                // Loop through our text
                if (forward)
                {
                    // Start at the beginning of the buffer
                    for (int x = 0; x < len; x++)
                    {
                        // Is it in char set?
                        bool found = GetCharInCharSet(str[x], characterSet, negate);

                        if (found)
                        {
                            done = true;
                            break;
                        }

                        // Advance the buffer
                        chrg.cpMax++;
                    }
                }
                else
                { // Span reverse.
                    int x = len;
                    while (x-- != 0)
                    {
                        // Is it in char set?
                        bool found = GetCharInCharSet(str[x], characterSet, negate);

                        if (found)
                        {
                            done = true;
                            break;
                        }

                        // Bring the selection back while keeping it anchored
                        chrg.cpMin--;
                    }
                }
            }
        }
        finally
        {
            // release the resources we got for our GETTEXTRANGE operation.
            if (txrg.lpstrText != IntPtr.Zero)
            {
                Marshal.FreeCoTaskMem(txrg.lpstrText);
            }
        }

        int index = (forward) ? chrg.cpMax : chrg.cpMin;
        return index;
    }

    private void ForceHandleCreate()
    {
        if (!IsHandleCreated)
        {
            CreateHandle();
        }
    }

    // Sends set color message to HWND; doesn't call Control.SetForeColor
    private bool InternalSetForeColor(Color value)
    {
        CHARFORMAT2W cf = GetCharFormat(false);
        if ((cf.dwMask & CFM_MASK.CFM_COLOR) != 0
            && ColorTranslator.ToWin32(value) == cf.crTextColor)
        {
            return true;
        }

        cf.dwMask = CFM_MASK.CFM_COLOR;
        cf.dwEffects = 0;
        cf.crTextColor = ColorTranslator.ToWin32(value);
        return SetCharFormat(PInvoke.SCF_ALL, cf);
    }

    private unsafe CHARFORMAT2W GetCharFormat(bool fSelection)
    {
        CHARFORMAT2W cf = new()
        {
            cbSize = (uint)sizeof(CHARFORMAT2W)
        };

        PInvokeCore.SendMessage(this, PInvokeCore.EM_GETCHARFORMAT, (WPARAM)(fSelection ? PInvoke.SCF_SELECTION : PInvoke.SCF_DEFAULT), ref cf);
        return cf;
    }

    private RichTextBoxSelectionAttribute GetCharFormat(CFM_MASK mask, CFE_EFFECTS effect)
    {
        RichTextBoxSelectionAttribute charFormat = RichTextBoxSelectionAttribute.None;

        // check to see if the control has been created
        if (IsHandleCreated)
        {
            CHARFORMAT2W cf = GetCharFormat(true);
            // if the effects member contains valid info
            if ((cf.dwMask & mask) != 0)
            {
                // if the text has the desired effect
                if ((cf.dwEffects & effect) != 0)
                {
                    charFormat = RichTextBoxSelectionAttribute.All;
                }
            }
        }

        return charFormat;
    }

    private Font? GetCharFormatFont(bool selectionOnly)
    {
        ForceHandleCreate();

        CHARFORMAT2W cf = GetCharFormat(selectionOnly);
        if ((cf.dwMask & CFM_MASK.CFM_FACE) == 0)
        {
            return null;
        }

        float fontSize = 13;
        if ((cf.dwMask & CFM_MASK.CFM_SIZE) != 0)
        {
            fontSize = cf.yHeight / (float)20.0;
            if (fontSize == 0 && cf.yHeight > 0)
            {
                fontSize = 1;
            }
        }

        FontStyle style = FontStyle.Regular;
        if ((cf.dwMask & CFM_MASK.CFM_BOLD) != 0 && (cf.dwEffects & CFE_EFFECTS.CFE_BOLD) != 0)
        {
            style |= FontStyle.Bold;
        }

        if ((cf.dwMask & CFM_MASK.CFM_ITALIC) != 0 && (cf.dwEffects & CFE_EFFECTS.CFE_ITALIC) != 0)
        {
            style |= FontStyle.Italic;
        }

        if ((cf.dwMask & CFM_MASK.CFM_STRIKEOUT) != 0 && (cf.dwEffects & CFE_EFFECTS.CFE_STRIKEOUT) != 0)
        {
            style |= FontStyle.Strikeout;
        }

        if ((cf.dwMask & CFM_MASK.CFM_UNDERLINE) != 0 && (cf.dwEffects & CFE_EFFECTS.CFE_UNDERLINE) != 0)
        {
            style |= FontStyle.Underline;
        }

        try
        {
            return new Font(cf.FaceName.ToString(), fontSize, style, GraphicsUnit.Point, cf.bCharSet);
        }
        catch
        {
        }

        return null;
    }

    /// <summary>
    ///  Returns the index of the character nearest to the given point.
    /// </summary>
    public override int GetCharIndexFromPosition(Point pt)
    {
        Point wpt = new(pt.X, pt.Y);
        int index = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_CHARFROMPOS, 0, ref wpt);

        string t = Text;
        // EM_CHARFROMPOS will return an invalid number if the last character in the RichEdit
        // is a newline.
        //
        if (index >= t.Length)
        {
            index = Math.Max(t.Length - 1, 0);
        }

        return index;
    }

    private static bool GetCharInCharSet(char c, char[] charSet, bool negate)
    {
        bool match = false;
        int charSetLen = charSet.Length;

        // Loop through the given character set and compare for a match
        for (int i = 0; !match && i < charSetLen; i++)
        {
            match = c == charSet[i];
        }

        return negate ? !match : match;
    }

    /// <summary>
    ///  Returns the number of the line containing a specified character position
    ///  in a RichTextBox control. Note that this returns the physical line number
    ///  and not the conceptual line number. For example, if the first conceptual
    ///  line (line number 0) word-wraps and extends to the second line, and if
    ///  you pass the index of a overflowed character, GetLineFromCharIndex would
    ///  return 1 and not 0.
    /// </summary>
    public override int GetLineFromCharIndex(int index)
        => (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_EXLINEFROMCHAR, 0, index);

    /// <summary>
    ///  Scrolls the caret into view, showing as much of the surrounding text as possible. This overrides
    ///  the base edit-control behavior so the RichEdit-specific handling lives with the RichTextBox.
    /// </summary>
    private protected override unsafe void ScrollToCaretCore()
    {
        using ComScope<IRichEditOle> richEdit = new(null);

        if (PInvokeCore.SendMessage(
            hWnd: this,
            Msg: PInvokeCore.EM_GETOLEINTERFACE,
            wParam: 0,
            lParam: (void**)richEdit) == 0)
        {
            // No OLE interface available; fall back to the plain edit behavior.
            base.ScrollToCaretCore();
            return;
        }

        using var textDocument = richEdit.TryQuery<ITextDocument>(out HRESULT hr);

        if (hr.Succeeded)
        {
            // When the user calls RichTextBox::ScrollToCaret we want the RichTextBox to show as much text as
            // possible. Here is how we do that:
            //
            //  1. We scroll the RichTextBox all the way to the bottom so the last line of text is the last visible line.
            //  2. We get the first visible line.
            //  3. If the first visible line is smaller than the start of the selection, then we are done:
            //      The selection fits inside the RichTextBox display rectangle.
            //  4. Otherwise, scroll the selection to the top of the RichTextBox.

            GetSelectionStartAndLength(out int selStart, out int selLength);
            int selStartLine = GetLineFromCharIndex(selStart);

            using ComScope<ITextRange> windowTextRange = new(null);
            textDocument.Value->Range(WindowText.Length - 1, WindowText.Length - 1, windowTextRange).ThrowOnFailure();

            // 1. Scroll the RichTextBox all the way to the bottom
            windowTextRange.Value->ScrollIntoView((int)tomConstants.tomEnd).ThrowOnFailure();

            // 2. Get the first visible line.
            int firstVisibleLine = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_GETFIRSTVISIBLELINE);

            // 3. If the first visible line is smaller than the start of the selection, we are done.
            if (firstVisibleLine <= selStartLine)
            {
                return;
            }
            else
            {
                // 4. Scroll the selection to the top of the RichTextBox.
                using ComScope<ITextRange> selectionTextRange = new(null);
                textDocument.Value->Range(selStart, selStart + selLength, selectionTextRange).ThrowOnFailure();
                selectionTextRange.Value->ScrollIntoView((int)tomConstants.tomStart).ThrowOnFailure();
                return;
            }
        }

        base.ScrollToCaretCore();
    }

    /// <summary>
    ///  Returns the location of the character at the given index.
    /// </summary>
    public override unsafe Point GetPositionFromCharIndex(int index)
    {
        if (s_richEditMajorVersion == 2)
        {
            return base.GetPositionFromCharIndex(index);
        }

        if (index < 0 || index > Text.Length)
        {
            return Point.Empty;
        }

        Point position = default;
        PInvokeCore.SendMessage(this, PInvokeCore.EM_POSFROMCHAR, (WPARAM)(&position), index);
        return position;
    }

    private bool GetProtectedError()
    {
        if (ProtectedError)
        {
            ProtectedError = false;
            return true;
        }

        return false;
    }

    /// <summary>
    ///  Loads the contents of the given RTF or text file into a RichTextBox control.
    /// </summary>
    public void LoadFile(string path)
    {
        LoadFile(path, RichTextBoxStreamType.RichText);
    }

    /// <summary>
    ///  Loads the contents of a RTF or text into a RichTextBox control.
    /// </summary>
    public void LoadFile(string path, RichTextBoxStreamType fileType)
    {
        // valid values are 0x0 to 0x4
        SourceGenerated.EnumValidator.Validate(fileType, nameof(fileType));

        FileStream file = new(path, FileMode.Open, FileAccess.Read, FileShare.Read);
        try
        {
            LoadFile(file, fileType);
        }
        finally
        {
            file.Close();
        }
    }

    /// <summary>
    ///  Loads the contents of a RTF or text into a RichTextBox control.
    /// </summary>
    public void LoadFile(Stream data, RichTextBoxStreamType fileType)
    {
        ArgumentNullException.ThrowIfNull(data);

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

        uint flags;
        switch (fileType)
        {
            case RichTextBoxStreamType.RichText:
                flags = PInvoke.SF_RTF;
                break;
            case RichTextBoxStreamType.PlainText:
                Rtf = string.Empty;
                flags = PInvoke.SF_TEXT;
                break;
            case RichTextBoxStreamType.UnicodePlainText:
                flags = PInvoke.SF_UNICODE | PInvoke.SF_TEXT;
                break;
            default:
                throw new ArgumentException(SR.InvalidFileType);
        }

        StreamIn(data, flags);
    }

    protected override void OnBackColorChanged(EventArgs e)
    {
        if (IsHandleCreated)
        {
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETBKGNDCOLOR, 0, BackColor.ToWin32());
        }

        base.OnBackColorChanged(e);
    }

    protected override void OnRightToLeftChanged(EventArgs e)
    {
        base.OnRightToLeftChanged(e);
        // When the RTL property is changed, here's what happens. Let's assume that we change from
        // RTL.No to RTL.Yes.

        // 1. RecreateHandle is called.
        // 2. In RTB.OnHandleDestroyed, we cache off any RTF that might have been set.
        //     The RTB has been set to the empty string, so we do get RTF back. The RTF
        //     contains formatting info, but doesn't contain any reading-order info,
        //     so RichEdit defaults to LTR reading order.
        // 3. In RTB.OnHandleCreated, we check if we have any cached RTF, and if so,
        //     we want to set the RTF to that value. This is to ensure that the original
        //     text doesn't get lost.
        // 4. In the RTF setter, we get the current RTF, compare it to the old RTF, and
        //     since those are not equal, we set the RichEdit content to the old RTF.
        // 5. But... since the original RTF had no reading-order info, the reading-order
        //     will default to LTR.

        // That's why in Everett we set the text back since that clears the RTF, thus restoring
        // the reading order to that of the window style. The problem here is that when there's
        // no initial text (the empty string), then WindowText would not actually set the text,
        // and we were left with the LTR reading order. There's no longer any initial text (as in Everett,
        // e.g richTextBox1), since we changed the designers to not set text. Sigh...

        // So the fix is to force windowtext, whether or not that window text is equal to what's already there.
        // Note that in doing so we will lose any formatting info you might have set on the RTF. We are okay with that.

        // We use WindowText rather than Text because this way we can avoid
        // spurious TextChanged events.
        //
        string oldText = WindowText;
        ForceWindowText(null);
        ForceWindowText(oldText);
    }

    /// <summary>
    ///  Fires an event when the user changes the control's contents
    ///  are either smaller or larger than the control's window size.
    /// </summary>
    protected virtual void OnContentsResized(ContentsResizedEventArgs e)
    {
        ((ContentsResizedEventHandler?)Events[s_requestSizeEvent])?.Invoke(this, e);
    }

    protected override void OnGotFocus(EventArgs e)
    {
        base.OnGotFocus(e);

        // Use parent's accessible object because RichTextBox doesn't support UIA Providers, and its
        // AccessibilityObject doesn't get created even when assistive tech (e.g. Narrator) is used
        if (Parent?.IsAccessibilityObjectCreated == true)
        {
            Parent.AccessibilityObject.InternalRaiseAutomationNotification(
                Automation.AutomationNotificationKind.Other,
                Automation.AutomationNotificationProcessing.MostRecent,
                Text);
        }
    }

    protected override void OnHandleCreated(EventArgs e)
    {
        // base.OnHandleCreated is called somewhere in the middle of this

        _curSelStart = _curSelEnd = _curSelType = -1;

        // We will always set the control to use the maximum text, it defaults to 32k..
        // This must be done before we start loading files, because some files may
        // be larger than 32k.
        //
        UpdateMaxLength();

        // This is needed so that the control will fire change and update events
        // even if it is hidden
        PInvokeCore.SendMessage(
            this,
            PInvokeCore.EM_SETEVENTMASK,
            0,
            (nint)(PInvoke.ENM_PROTECTED | PInvoke.ENM_SELCHANGE |
                     PInvoke.ENM_DROPFILES | PInvoke.ENM_REQUESTRESIZE |
                     PInvoke.ENM_IMECHANGE | PInvoke.ENM_CHANGE |
                     PInvoke.ENM_UPDATE | PInvoke.ENM_SCROLL |
                     PInvoke.ENM_KEYEVENTS | PInvoke.ENM_MOUSEEVENTS |
                     PInvoke.ENM_SCROLLEVENTS | PInvoke.ENM_LINK));

        int rm = _rightMargin;
        _rightMargin = 0;
        RightMargin = rm;

        PInvokeCore.SendMessage(this, PInvokeCore.EM_AUTOURLDETECT, (WPARAM)(DetectUrls ? 1 : 0));
        if (_selectionBackColorToSetOnHandleCreated != Color.Empty)
        {
            SelectionBackColor = _selectionBackColorToSetOnHandleCreated;
        }

        // Initialize colors before initializing RTF, otherwise CFE_AUTOCOLOR will be in effect
        // and our text will all be Color.WindowText.
        bool autoWordSelection = AutoWordSelection;
        AutoWordSelection = autoWordSelection;

        PInvokeCore.SendMessage(this, PInvokeCore.EM_SETBKGNDCOLOR, (WPARAM)0, (LPARAM)BackColor);
        InternalSetForeColor(ForeColor);

        // base sets the Text property. It's important to do this *after* setting EM_AUTOUrlDETECT.
        base.OnHandleCreated(e);

        // For some reason, we need to set the OleCallback before setting the RTF property.
        UpdateOleCallback();

        // RTF property takes precedence over Text property
        //
        try
        {
            SuppressTextChangedEvent = true;
            if (_textRtf is not null)
            {
                // setting RTF calls back on Text, which relies on textRTF being null
                string text = _textRtf;
                _textRtf = null;
                Rtf = text;
            }
            else if (_textPlain is not null)
            {
                string text = _textPlain;
                _textPlain = null;
                Text = text;
            }
        }
        finally
        {
            SuppressTextChangedEvent = false;
        }

        // Since we can't send EM_SETSEL until RTF has been set,
        // we can't rely on base to do it for us.
        SetSelectionOnHandle();

        if (ShowSelectionMargin)
        {
            // If you call SendMessage instead of PostMessage, the control
            // will resize itself to the size of the parent's client area. Don't know why...
            PInvokeCore.PostMessage(
                this,
                PInvokeCore.EM_SETOPTIONS,
                (WPARAM)(int)PInvoke.ECOOP_OR,
                (LPARAM)(int)PInvoke.ECO_SELECTIONBAR);
        }

        if (_languageOption != LanguageOption)
        {
            LanguageOption = _languageOption;
        }

        ClearUndo();

        SendZoomFactor(_zoomMultiplier);

        // RichEdit does not send the WM_CTLCOLOR messages that a plain EDIT control uses to lazily trigger
        // the modern Visual Styles client-area carve, so provoke it explicitly once the handle exists.
        // This is a no-op unless EffectiveVisualStylesMode is Net11 or above.
        RecalculateVisualStylesClientArea();

        SystemEvents.UserPreferenceChanged += UserPreferenceChangedHandler;
    }

    protected override void OnHandleDestroyed(EventArgs e)
    {
        base.OnHandleDestroyed(e);

        if (!InConstructor)
        {
            _textRtf = Rtf;
            if (_textRtf!.Length == 0)
            {
                _textRtf = null;
            }
        }

        _oleCallback = null;
        SystemEvents.UserPreferenceChanged -= UserPreferenceChangedHandler;
    }

    /// <summary>
    ///  Fires an event when the user clicks a RichTextBox control's horizontal scroll bar.
    /// </summary>
    protected virtual void OnHScroll(EventArgs e)
    {
        ((EventHandler?)Events[s_hscrollEvent])?.Invoke(this, e);
    }

    /// <summary>
    ///  Fires an event when the user clicks on a link in a rich-edit control.
    /// </summary>
    protected virtual void OnLinkClicked(LinkClickedEventArgs e)
    {
        ((LinkClickedEventHandler?)Events[s_linkActivateEvent])?.Invoke(this, e);
    }

    /// <summary>
    ///  Fires an event when the user changes the control's IME conversion status.
    /// </summary>
    protected virtual void OnImeChange(EventArgs e)
    {
        ((EventHandler?)Events[s_imeChangeEvent])?.Invoke(this, e);
    }

    /// <summary>
    ///  Fires an event when the user is taking an action that would change
    ///  a protected range of text in the RichTextBox control.
    /// </summary>
    protected virtual void OnProtected(EventArgs e)
    {
        ProtectedError = true;
        ((EventHandler?)Events[s_protectedEvent])?.Invoke(this, e);
    }

    /// <summary>
    ///  Fires an event when the current selection of text in the RichTextBox
    ///  control has changed or the insertion point has moved.
    /// </summary>
    protected virtual void OnSelectionChanged(EventArgs e)
    {
        ((EventHandler?)Events[s_selectionChangeEvent])?.Invoke(this, e);
    }

    /// <summary>
    ///  Fires an event when the user clicks a RichTextBox control's vertical
    ///  scroll bar.
    /// </summary>
    protected virtual void OnVScroll(EventArgs e)
    {
        ((EventHandler?)Events[s_vscrollEvent])?.Invoke(this, e);
    }

    /// <summary>
    ///  Pastes the contents of the clipboard in the given clipboard format.
    /// </summary>
    public void Paste(DataFormats.Format clipFormat)
    {
        PInvokeCore.SendMessage(this, PInvokeCore.EM_PASTESPECIAL, (WPARAM)clipFormat.Id);
    }

    protected override bool ProcessCmdKey(ref Message m, Keys keyData)
    {
        if (!RichTextShortcutsEnabled)
        {
            foreach (int shortcutValue in s_shortcutsToDisable!)
            {
                if ((int)keyData == shortcutValue)
                {
                    return true;
                }
            }
        }

        return base.ProcessCmdKey(ref m, keyData);
    }

    /// <summary>
    ///  Redoes the last undone editing operation.
    /// </summary>
    public void Redo() => PInvokeCore.SendMessage(this, PInvokeCore.EM_REDO);

    // NOTE: Undo is implemented on TextBox

    /// <summary>
    ///  Saves the contents of a RichTextBox control to a file.
    /// </summary>
    public void SaveFile(string path)
    {
        SaveFile(path, RichTextBoxStreamType.RichText);
    }

    /// <summary>
    ///  Saves the contents of a RichTextBox control to a file.
    /// </summary>
    public void SaveFile(string path, RichTextBoxStreamType fileType)
    {
        // valid values are 0x0 to 0x4
        SourceGenerated.EnumValidator.Validate(fileType, nameof(fileType));

        FileStream file = File.Create(path);
        try
        {
            SaveFile(file, fileType);
        }
        finally
        {
            file.Close();
        }
    }

    /// <summary>
    ///  Saves the contents of a RichTextBox control to a file.
    /// </summary>
    public void SaveFile(Stream data, RichTextBoxStreamType fileType)
    {
        uint flags = fileType switch
        {
            RichTextBoxStreamType.RichText => PInvoke.SF_RTF,
            RichTextBoxStreamType.PlainText => PInvoke.SF_TEXT,
            RichTextBoxStreamType.UnicodePlainText => PInvoke.SF_UNICODE | PInvoke.SF_TEXT,
            RichTextBoxStreamType.RichNoOleObjs => PInvoke.SF_RTFNOOBJS,
            RichTextBoxStreamType.TextTextOleObjs => PInvoke.SF_TEXTIZED,
            _ => throw new InvalidEnumArgumentException(nameof(fileType), (int)fileType, typeof(RichTextBoxStreamType)),
        };

        StreamOut(data, flags, includeCrLfs: true);
    }

    /// <summary>
    ///  Core Zoom calculation and message passing (used by ZoomFactor property and CreateHandle()
    /// </summary>
    private void SendZoomFactor(float zoom)
    {
        int numerator;
        int denominator;

        if (zoom == 1.0f)
        {
            denominator = 0;
            numerator = 0;
        }
        else
        {
            denominator = 1000;
            float multiplier = 1000 * zoom;
            numerator = (int)Math.Ceiling(multiplier);
            if (numerator >= 64000)
            {
                numerator = (int)Math.Floor(multiplier);
            }
        }

        if (IsHandleCreated)
        {
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETZOOM, (WPARAM)numerator, (LPARAM)denominator);
        }

        _zoomMultiplier = numerator != 0
            ? numerator / ((float)denominator)
            : 1.0f;
    }

    private unsafe bool SetCharFormat(CFM_MASK mask, CFE_EFFECTS effect, RichTextBoxSelectionAttribute charFormat)
    {
        // check to see if the control has been created
        if (IsHandleCreated)
        {
            CHARFORMAT2W cf = new()
            {
                cbSize = (uint)sizeof(CHARFORMAT2W),
                dwMask = mask,
                dwEffects = charFormat switch
                {
                    RichTextBoxSelectionAttribute.All => effect,
                    RichTextBoxSelectionAttribute.None => 0,
                    _ => throw new ArgumentException(SR.UnknownAttr),
                }
            };

            // Set the format information.
            return PInvokeCore.SendMessage(this, PInvokeCore.EM_SETCHARFORMAT, (WPARAM)PInvoke.SCF_SELECTION, ref cf) != 0;
        }

        return false;
    }

    private bool SetCharFormat(uint charRange, CHARFORMAT2W cf)
    {
        return PInvokeCore.SendMessage(this, PInvokeCore.EM_SETCHARFORMAT, (WPARAM)charRange, ref cf) != 0;
    }

    private unsafe void SetCharFormatFont(bool selectionOnly, Font value)
    {
        ForceHandleCreate();

        CFM_MASK dwMask = CFM_MASK.CFM_FACE | CFM_MASK.CFM_SIZE | CFM_MASK.CFM_BOLD |
            CFM_MASK.CFM_ITALIC | CFM_MASK.CFM_STRIKEOUT | CFM_MASK.CFM_UNDERLINE |
            CFM_MASK.CFM_CHARSET;

        CFE_EFFECTS dwEffects = 0;
        if (value.Bold)
        {
            dwEffects |= CFE_EFFECTS.CFE_BOLD;
        }

        if (value.Italic)
        {
            dwEffects |= CFE_EFFECTS.CFE_ITALIC;
        }

        if (value.Strikeout)
        {
            dwEffects |= CFE_EFFECTS.CFE_STRIKEOUT;
        }

        if (value.Underline)
        {
            dwEffects |= CFE_EFFECTS.CFE_UNDERLINE;
        }

        LOGFONTW logFont = value.ToLogicalFont();
        CHARFORMAT2W charFormat = new()
        {
            cbSize = (uint)sizeof(CHARFORMAT2W),
            dwMask = dwMask,
            dwEffects = dwEffects,
            yHeight = (int)(value.SizeInPoints * 20),
            bCharSet = (byte)logFont.lfCharSet,
            bPitchAndFamily = logFont.lfPitchAndFamily,
            FaceName = logFont.FaceName
        };

        PInvokeCore.SendMessage(
            this,
            PInvokeCore.EM_SETCHARFORMAT,
            (WPARAM)(selectionOnly ? PInvoke.SCF_SELECTION : PInvoke.SCF_ALL),
            ref charFormat);
    }

    private static void SetupLogPixels()
    {
        using var dc = GetDcScope.ScreenDC;
        s_logPixelsX = PInvokeCore.GetDeviceCaps(dc, GET_DEVICE_CAPS_INDEX.LOGPIXELSX);
        s_logPixelsY = PInvokeCore.GetDeviceCaps(dc, GET_DEVICE_CAPS_INDEX.LOGPIXELSY);
    }

    private static int Pixel2Twip(int v, bool xDirection)
    {
        SetupLogPixels();
        int logP = xDirection ? s_logPixelsX : s_logPixelsY;
        return (int)((((double)v) / logP) * 72.0 * 20.0);
    }

    private static int Twip2Pixel(int v, bool xDirection)
    {
        SetupLogPixels();
        int logP = xDirection ? s_logPixelsX : s_logPixelsY;
        return (int)(((v / 20.0) / 72.0) * logP);
    }

    private void StreamIn(string str, uint flags)
    {
        if (str.Length == 0)
        {
            // Destroy the selection if callers was setting selection text
            if ((PInvoke.SFF_SELECTION & flags) != 0)
            {
                PInvokeCore.SendMessage(this, PInvokeCore.WM_CLEAR);
                ProtectedError = false;
                return;
            }

            // WM_SETTEXT is allowed even if we have protected text
            PInvokeCore.SendMessage(this, PInvokeCore.WM_SETTEXT, 0, string.Empty);
            return;
        }

        // Rather than work only some of the time with null characters,
        // we're going to be consistent and never work with them.
        int nullTerminatedLength = str.IndexOf((char)0);
        if (nullTerminatedLength != -1)
        {
            str = str[..nullTerminatedLength];
        }

        // Get the string into a byte array
        byte[] encodedBytes;
        if ((flags & PInvoke.SF_UNICODE) != 0)
        {
            encodedBytes = Encoding.Unicode.GetBytes(str);
        }
        else
        {
            // Encode using the default code page.
            encodedBytes = (CodePagesEncodingProvider.Instance.GetEncoding(0) ?? Encoding.UTF8).GetBytes(str);
        }

        _editStream = new MemoryStream(encodedBytes.Length);
        _editStream.Write(encodedBytes, 0, encodedBytes.Length);
        _editStream.Position = 0;
        StreamIn(_editStream, flags);
    }

    private void StreamIn(Stream data, uint flags)
    {
        // Clear out the selection only if we are replacing all the text.
        if ((flags & PInvoke.SFF_SELECTION) == 0)
        {
            CHARRANGE range = default;
            PInvokeCore.SendMessage(this, PInvokeCore.EM_EXSETSEL, 0, ref range);
        }

        try
        {
            _editStream = data;
            Debug.Assert(data is not null, "StreamIn passed a null stream");

            // If SF_RTF is requested then check for the RTF tag at the start
            // of the file. We don't load if the tag is not there.

            if ((flags & PInvoke.SF_RTF) != 0)
            {
                long streamStart = _editStream.Position;

                byte[] bytes = new byte[SZ_RTF_TAG.Length];
                try
                {
                    // When we reading with `ReadExactly`, we usually will get overshoot at this point anyway in most cases
                    // and then know that the format cannot be correct. So, we're packing this and throw, but we still need
                    // to throw an `ArgumentException` to not introduce a breaking change.
                    _editStream.ReadExactly(bytes, (int)streamStart, SZ_RTF_TAG.Length);
                }
                catch (EndOfStreamException ex)
                {
                    throw new ArgumentException(SR.InvalidFileFormat, ex);
                }

                // Encode using the default encoding.
                string str = (CodePagesEncodingProvider.Instance.GetEncoding(0) ?? Encoding.UTF8).GetString(bytes);
                if (!SZ_RTF_TAG.Equals(str))
                {
                    throw new ArgumentException(SR.InvalidFileFormat);
                }

                // put us back at the start of the file
                _editStream.Position = streamStart;
            }

            int cookieVal = 0;

            // set up structure to do stream operation
            EDITSTREAM es = default;

            cookieVal = (flags & PInvoke.SF_UNICODE) != 0
                ? INPUT | UNICODE
                : INPUT | ANSI;

            if ((flags & PInvoke.SF_RTF) != 0)
            {
                cookieVal |= RTF;
            }
            else
            {
                cookieVal |= TEXTLF;
            }

            es.dwCookie = (UIntPtr)cookieVal;
            EDITSTREAMCALLBACK callback = EditStreamProc;
            es.pfnCallback = Marshal.GetFunctionPointerForDelegate(callback);

            // gives us TextBox compatible behavior, programmatic text change shouldn't
            // be limited...
            PInvokeCore.SendMessage(this, PInvokeCore.EM_EXLIMITTEXT, 0, int.MaxValue);

            // go get the text for the control
            PInvokeCore.SendMessage(this, PInvokeCore.EM_STREAMIN, (WPARAM)flags, ref es);
            GC.KeepAlive(callback);

            UpdateMaxLength();

            // If we failed to load because of protected
            // text then return protect event was fired so no
            // exception is required for the error
            if (GetProtectedError())
            {
                return;
            }

            if (es.dwError != 0)
            {
                throw new InvalidOperationException(SR.LoadTextError);
            }

            // set the modify tag on the control
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETMODIFY, (WPARAM)(-1));

            // EM_GETLINECOUNT will cause the RichTextBox to recalculate its line indexes
            PInvokeCore.SendMessage(this, PInvokeCore.EM_GETLINECOUNT);
        }
        finally
        {
            // release any storage space held.
            _editStream = null;
        }
    }

    private string StreamOut(uint flags)
    {
        MemoryStream stream = new();
        StreamOut(stream, flags, false);
        stream.Position = 0;
        int streamLength = (int)stream.Length;
        string result = string.Empty;

        if (streamLength > 0)
        {
            byte[] bytes = new byte[streamLength];
            stream.Read(bytes, 0, streamLength);

            if ((flags & PInvoke.SF_UNICODE) != 0)
            {
                result = Encoding.Unicode.GetString(bytes, 0, bytes.Length);
            }
            else
            {
                // Convert from the current code page
                result = (CodePagesEncodingProvider.Instance.GetEncoding(0) ?? Encoding.UTF8).GetString(bytes, 0, bytes.Length);
            }

            // Trimming off a null char is usually a sign of incorrect marshalling.
            // We should consider removing this in the future, but it would need to
            // be checked against input strings with a trailing null.

            if (!string.IsNullOrEmpty(result) && (result[^1] == '\0'))
            {
                result = result[..^1];
            }
        }

        return result;
    }

    private void StreamOut(Stream data, uint flags, bool includeCrLfs)
    {
        // set up the EDITSTREAM structure for the callback.
        _editStream = data;

        try
        {
            int cookieVal = 0;
            EDITSTREAM es = default;

            cookieVal = (flags & PInvoke.SF_UNICODE) != 0 ? OUTPUT | UNICODE : OUTPUT | ANSI;

            if ((flags & PInvoke.SF_RTF) != 0)
            {
                cookieVal |= RTF;
            }
            else
            {
                if (includeCrLfs)
                {
                    cookieVal |= TEXTCRLF;
                }
                else
                {
                    cookieVal |= TEXTLF;
                }
            }

            es.dwCookie = (UIntPtr)cookieVal;
            EDITSTREAMCALLBACK callback = EditStreamProc;
            es.pfnCallback = Marshal.GetFunctionPointerForDelegate(callback);

            // Get Text
            PInvokeCore.SendMessage(this, PInvokeCore.EM_STREAMOUT, (WPARAM)flags, ref es);
            GC.KeepAlive(callback);

            // check to make sure things went well
            if (es.dwError != 0)
            {
                throw new InvalidOperationException(SR.SaveTextError);
            }
        }
        finally
        {
            // release any storage space held.
            _editStream = null;
        }
    }

    private unsafe string GetTextEx(GETTEXTEX_FLAGS flags = GETTEXTEX_FLAGS.GT_DEFAULT)
    {
        Debug.Assert(IsHandleCreated);

        // Unicode UTF-16, little endian byte order (BMP of ISO 10646); available only to managed applications
        // https://docs.microsoft.com/windows/win32/intl/code-page-identifiers
        const int UNICODE = 1200;

        GETTEXTLENGTHEX gtl = new GETTEXTLENGTHEX
        {
            codepage = UNICODE,
            flags = GETTEXTLENGTHEX_FLAGS.GTL_DEFAULT
        };

        if (flags.HasFlag(GETTEXTEX_FLAGS.GT_USECRLF))
        {
            gtl.flags |= GETTEXTLENGTHEX_FLAGS.GTL_USECRLF;
        }

        GETTEXTLENGTHEX* pGtl = &gtl;
        int expectedLength = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_GETTEXTLENGTHEX, (WPARAM)pGtl);
        if (expectedLength == (int)HRESULT.E_INVALIDARG)
            throw new Win32Exception(expectedLength);

        // buffer has to have enough space for final \0. Without this, the last character is missing!
        // in case flags contains GT_SELECTION we'll allocate too much memory (for the whole text and not just the selection),
        // but there's no appropriate flag for EM_GETTEXTLENGTHEX
        int maxLength = (expectedLength + 1) * sizeof(char);

        GETTEXTEX gt = new GETTEXTEX
        {
            cb = (uint)maxLength,
            flags = flags,
            codepage = UNICODE,
        };

        BufferScope<char> buffer = new(maxLength);
        GETTEXTEX* pGt = &gt;
        fixed (char* b = buffer)
        {
            int actualLength = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_GETTEXTEX, (WPARAM)pGt, (LPARAM)b);

            // The default behavior of EM_GETTEXTEX is to normalize line endings to '\r'
            // (see: GT_DEFAULT, https://docs.microsoft.com/windows/win32/api/richedit/ns-richedit-gettextex#members),
            // whereas previously we would normalize to '\n'. Unfortunately we can only ask for '\r\n' line endings
            // via GT.USECRLF, but unable to ask for '\n'. Unless GT.USECRLF was set,
            // convert '\r' with '\n' to retain the original behavior.
            if (!flags.HasFlag(GETTEXTEX_FLAGS.GT_USECRLF))
            {
                int index = 0;
                while (index < actualLength)
                {
                    if (b[index] == '\r')
                    {
                        b[index] = '\n';
                    }

                    index++;
                }
            }

            return buffer[..actualLength].ToString();
        }
    }

    private void UpdateOleCallback()
    {
        if (!IsHandleCreated)
        {
            return;
        }

        if (_oleCallback is null)
        {
            AllowOleObjects = true;

            _oleCallback = CreateRichEditOleCallback();
            using var oleCallback = ComHelpers.GetComScope<IRichEditOleCallback>(_oleCallback);
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETOLECALLBACK, 0, (nint)oleCallback);
        }

        PInvokeCore.DragAcceptFiles(this, fAccept: false);
    }

    // Note: RichTextBox doesn't work like other controls as far as setting ForeColor/
    // BackColor -- you need to send messages to update the colors
    private void UserPreferenceChangedHandler(object o, UserPreferenceChangedEventArgs e)
    {
        if (IsHandleCreated)
        {
            if (BackColor.IsSystemColor)
            {
                PInvokeCore.SendMessage(this, PInvokeCore.EM_SETBKGNDCOLOR, 0, BackColor.ToWin32());
            }

            if (ForeColor.IsSystemColor)
            {
                InternalSetForeColor(ForeColor);
            }
        }
    }

    protected override AccessibleObject CreateAccessibilityInstance() => new ControlAccessibleObject(this);

    /// <summary>
    ///  Creates the IRichEditOleCallback compatible object for handling RichEdit callbacks. For more
    ///  information look up the MSDN info on this interface. This is designed to be a back door of
    ///  sorts, which is why it is fairly obscure, and uses the RichEdit name instead of RichTextBox.
    /// </summary>
    protected virtual object CreateRichEditOleCallback() => new OleCallback(this);

    /// <summary>
    ///  Handles link messages (mouse move, down, up, dblclk, etc)
    /// </summary>
    private unsafe void EnLinkMsgHandler(ref Message m)
    {
        ENLINK enlink;
        enlink = *(ENLINK*)(nint)m.LParamInternal;

        switch ((uint)enlink.msg)
        {
            case PInvokeCore.WM_SETCURSOR:
                LinkCursor = true;
                m.ResultInternal = (LRESULT)1;
                return;
            // Mouse-down triggers Url; this matches Outlook 2000's behavior.
            case PInvokeCore.WM_LBUTTONDOWN:
                string linktext = CharRangeToString(enlink.charrange);
                if (!string.IsNullOrEmpty(linktext))
                {
                    OnLinkClicked(new LinkClickedEventArgs(linktext, enlink.charrange.cpMin, enlink.charrange.cpMax - enlink.charrange.cpMin));
                }

                m.ResultInternal = (LRESULT)1;
                return;
        }

        m.ResultInternal = (LRESULT)0;
        return;
    }

    /// <summary>
    ///  Converts a CHARRANGE to a string.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   The behavior of this is dependent on the current window class name being used.
    ///   We have to create a CharBuffer of the type of RichTextBox DLL we're using,
    ///   not based on the SystemCharWidth.
    ///  </para>
    /// </remarks>
    private string CharRangeToString(CHARRANGE c)
    {
        TEXTRANGE txrg = new()
        {
            chrg = c
        };

        Debug.Assert((c.cpMax - c.cpMin) > 0, "CHARRANGE was null or negative - can't do it!");
        if (c.cpMax - c.cpMin <= 0)
        {
            return string.Empty;
        }

        int characters = (c.cpMax - c.cpMin) + 1; // +1 for null termination
        UnicodeCharBuffer charBuffer = new(characters);
        nint unmanagedBuffer = charBuffer.AllocCoTaskMem();
        if (unmanagedBuffer == 0)
        {
            throw new OutOfMemoryException(SR.OutOfMemory);
        }

        txrg.lpstrText = unmanagedBuffer;
        int len = (int)PInvokeCore.SendMessage(this, PInvokeCore.EM_GETTEXTRANGE, 0, ref txrg);
        Debug.Assert(len != 0, "CHARRANGE from RichTextBox was bad! - impossible?");
        charBuffer.PutCoTaskMem(unmanagedBuffer);
        if (txrg.lpstrText != 0)
        {
            Marshal.FreeCoTaskMem(unmanagedBuffer);
        }

        string result = charBuffer.GetString();
        return result;
    }

    internal override void UpdateMaxLength()
    {
        if (IsHandleCreated)
        {
            PInvokeCore.SendMessage(this, PInvokeCore.EM_EXLIMITTEXT, 0, (IntPtr)MaxLength);
        }
    }

    private void WmReflectCommand(ref Message m)
    {
        // We check if we're in the middle of handle creation because
        // the rich edit control fires spurious events during this time.
        if (m.LParamInternal == Handle && !GetState(States.CreatingHandle))
        {
            switch ((uint)m.WParamInternal.HIWORD)
            {
                case PInvoke.EN_HSCROLL:
                    OnHScroll(EventArgs.Empty);
                    break;
                case PInvoke.EN_VSCROLL:
                    OnVScroll(EventArgs.Empty);
                    break;
                default:
                    base.WndProc(ref m);
                    break;
            }
        }
        else
        {
            base.WndProc(ref m);
        }
    }

    internal unsafe void WmReflectNotify(ref Message m)
    {
        if (m.HWnd != Handle)
        {
            base.WndProc(ref m);
            return;
        }

        NMHDR* nmhdr = (NMHDR*)(nint)m.LParamInternal;
        switch (nmhdr->code)
        {
            case PInvoke.EN_LINK:
                EnLinkMsgHandler(ref m);
                break;
            case PInvoke.EN_DROPFILES:
                HDROP endropfiles = (HDROP)((ENDROPFILES*)m.LParamInternal)->hDrop;

                // Only look at the first file.
                using (BufferScope<char> buffer = new((int)PInvokeCore.MAX_PATH + 1))
                {
                    fixed (char* b = buffer)
                    {
                        uint length = PInvokeCore.DragQueryFile(endropfiles, iFile: 0, b, cch: (uint)buffer.Length);
                        if (length != 0)
                        {
                            // Try to load the file as RTF.
                            string path = buffer[..(int)length].ToString();

                            try
                            {
                                LoadFile(path, RichTextBoxStreamType.RichText);
                            }
                            catch
                            {
                                // We failed to load as rich text, try again as plain text.
                                try
                                {
                                    LoadFile(path, RichTextBoxStreamType.PlainText);
                                }
                                catch
                                {
                                }
                            }
                        }
                    }
                }

                // Confirm that we did the drop
                m.ResultInternal = (LRESULT)1;
                break;

            case PInvoke.EN_REQUESTRESIZE:
                if (!CallOnContentsResized)
                {
                    REQRESIZE* reqResize = (REQRESIZE*)(nint)m.LParamInternal;
                    if (BorderStyle == BorderStyle.Fixed3D)
                    {
                        reqResize->rc.bottom++;
                    }

                    OnContentsResized(new ContentsResizedEventArgs(reqResize->rc));
                }

                break;

            case PInvoke.EN_SELCHANGE:
                SELCHANGE* selChange = (SELCHANGE*)(nint)m.LParamInternal;
                WmSelectionChange(*selChange);
                break;

            case PInvoke.EN_PROTECTED:
                {
                    ENPROTECTED enprotected;

                    enprotected = *(ENPROTECTED*)(nint)m.LParamInternal;

                    switch ((uint)enprotected.msg)
                    {
                        case PInvokeCore.EM_SETCHARFORMAT:
                            // Allow change of protected style
                            CHARFORMAT2W* charFormat = (CHARFORMAT2W*)enprotected.lParam;
                            if ((charFormat->dwMask & CFM_MASK.CFM_PROTECTED) != 0)
                            {
                                m.ResultInternal = (LRESULT)0;
                                return;
                            }

                            break;

                        // Throw an exception for the following
                        case PInvokeCore.EM_SETPARAFORMAT:
                        case PInvokeCore.EM_REPLACESEL:
                            break;

                        case PInvokeCore.EM_STREAMIN:
                            // Don't allow STREAMIN to replace protected selection
                            if ((unchecked((uint)(long)enprotected.wParam) & PInvoke.SFF_SELECTION) != 0)
                            {
                                break;
                            }

                            m.ResultInternal = (LRESULT)0;
                            return;

                        // Allow the following
                        case PInvokeCore.WM_COPY:
                        case PInvokeCore.WM_SETTEXT:
                        case PInvokeCore.EM_EXLIMITTEXT:
                            m.ResultInternal = (LRESULT)0;
                            return;

                        // Beep and disallow change for all other messages
                        default:
                            PInvoke.MessageBeep(MESSAGEBOX_STYLE.MB_OK);
                            break;
                    }

                    OnProtected(EventArgs.Empty);
                    m.ResultInternal = (LRESULT)1;
                    break;
                }

            default:
                base.WndProc(ref m);
                break;
        }
    }

    private void WmSelectionChange(SELCHANGE selChange)
    {
        int selStart = selChange.chrg.cpMin;
        int selEnd = selChange.chrg.cpMax;
        short selType = (short)selChange.seltyp;

        // The IME retains characters in the composition window even after MaxLength
        // has been reached in the rich edit control. So, if the Hangul or HangulFull IME is in use, and the
        // number of characters in the control is equal to MaxLength, and the selection start equals the
        // selection end (nothing is currently selected), then kill and restore focus to the control. Then,
        // to prevent any further partial composition from occurring, post a message back to myself to select
        // the last character being composed so that any further composition will occur within the context of
        // the string contained within the control.
        //
        // Since the IME window completes the composition string when the control loses focus and the
        // EIMES_COMPLETECOMPSTRKILLFOCUS status type is set in the control by the EM_SETIMESTATUS message,
        // simply killing focus and resetting focus to the control will force the contents of the composition
        // window to be removed. This forces the undo buffer to be emptied and the backspace key will properly
        // remove the last completed character typed.

        // Is either the Hangul or HangulFull IME currently in use?
        if (ImeMode is ImeMode.Hangul or ImeMode.HangulFull)
        {
            // Is the IME CompositionWindow open?
            LRESULT compMode = PInvokeCore.SendMessage(this, PInvokeCore.EM_GETIMECOMPMODE);
            if (compMode != PInvoke.ICM_NOTOPEN)
            {
                int textLength = PInvokeCore.GetWindowTextLength(this);
                if (selStart == selEnd && textLength == MaxLength)
                {
                    PInvokeCore.SendMessage(this, PInvokeCore.WM_KILLFOCUS);
                    PInvokeCore.SendMessage(this, PInvokeCore.WM_SETFOCUS);
                    PInvokeCore.PostMessage(this, PInvokeCore.EM_SETSEL, (WPARAM)(selEnd - 1), (LPARAM)selEnd);
                }
            }
        }

        if (selStart != _curSelStart || selEnd != _curSelEnd || selType != _curSelType)
        {
            _curSelStart = selStart;
            _curSelEnd = selEnd;
            _curSelType = selType;
            OnSelectionChanged(EventArgs.Empty);
        }
    }

    private void WmSetFont(ref Message m)
    {
        // This function would normally cause two TextChanged events to be fired, one
        // from the base.WndProc, and another from InternalSetForeColor.
        // To prevent this, we suppress the first event fire.
        //
        try
        {
            SuppressTextChangedEvent = true;
            base.WndProc(ref m);
        }
        finally
        {
            SuppressTextChangedEvent = false;
        }

        InternalSetForeColor(ForeColor);
    }

    protected override unsafe void WndProc(ref Message m)
    {
        switch (m.MsgInternal)
        {
            case PInvokeCore.WM_PAINT:

                // Important: We need to let to run the base
                // renderer first in the case of the RTF control.
                base.WndProc(ref m);

                if (Handle == m.HWND
                    && !Enabled
                    && Application.IsDarkModeEnabled)
                {
                    // If the control is disabled, we don't want to let the RTF control
                    // paint anything else. We will paint the background and the unformatted
                    // text ourselves, so we don't want the RTF control to paint the background
                    // and the text in the foreground color.
                    using Graphics g = Graphics.FromHwndInternal(Handle);

                    // Paint the background
                    g.FillRectangle(SystemBrushes.ControlDark, ClientRectangle);

                    // Paint the text
                    TextRenderer.DrawText(
                        g,
                        Text,
                        Font,
                        ClientRectangle,
                        SystemColors.GrayText,
                        TextFormatFlags.Left
                        | TextFormatFlags.Top
                        | TextFormatFlags.WordBreak
                        | TextFormatFlags.EndEllipsis);

                    return;
                }

                break;

            case MessageId.WM_REFLECT_NOTIFY:
                WmReflectNotify(ref m);
                break;

            case MessageId.WM_REFLECT_COMMAND:
                WmReflectCommand(ref m);
                break;

            case PInvokeCore.WM_SETCURSOR:
                // NOTE: RichTextBox uses the WM_SETCURSOR message over links to allow us to
                //      change the cursor to a hand. It does this through a synchronous notification
                //      message. So we have to pass the message to the DefWndProc first, and
                //      then, if we receive a notification message in the meantime (indicated by
                //      changing "LinkCursor", we set it to a hand. Otherwise, we call the
                //      WM_SETCURSOR implementation on Control to set it to the user's selection for
                //      the RichTextBox's cursor.
                LinkCursor = false;
                DefWndProc(ref m);
                if (LinkCursor && !Cursor.Equals(Cursors.WaitCursor))
                {
                    Cursor.Current = Cursors.Hand;
                    m.ResultInternal = (LRESULT)1;
                }
                else
                {
                    base.WndProc(ref m);
                }

                break;

            case PInvokeCore.WM_SETFONT:
                WmSetFont(ref m);
                break;

            case PInvokeCore.WM_IME_NOTIFY:
                OnImeChange(EventArgs.Empty);
                base.WndProc(ref m);
                break;

            case PInvokeCore.WM_GETDLGCODE:
                base.WndProc(ref m);
                m.ResultInternal = (LRESULT)(AcceptsTab ? m.ResultInternal | (nint)PInvoke.DLGC_WANTTAB : m.ResultInternal & ~(nint)PInvoke.DLGC_WANTTAB);
                break;

            case PInvokeCore.WM_GETOBJECT:
                base.WndProc(ref m);

                // OLEACC.DLL uses window class names to identify standard control types. But WinForms controls use app-specific window
                // classes. Usually this doesn't matter, because system controls always identify their window class explicitly through
                // the WM_GETOBJECT+OBJID_QUERYCLASSNAMEIDX message. But RICHEDIT20 doesn't do that - so we must do it ourselves.
                // Otherwise OLEACC will treat rich edit controls as custom controls, so the accessible Role and Value will be wrong.
                if ((int)m.LParamInternal == (int)OBJECT_IDENTIFIER.OBJID_QUERYCLASSNAMEIDX)
                {
                    m.ResultInternal = (LRESULT)(65536 + 30);
                }

                break;

            case PInvokeCore.WM_RBUTTONUP:
                // since RichEdit eats up the WM_CONTEXTMENU message, we need to force DefWndProc
                // to spit out this message again on receiving WM_RBUTTONUP message. By setting UserMouse
                // style to true, we effectively let the WmMouseUp method in Control.cs to generate
                // the WM_CONTEXTMENU message for us.
                bool oldStyle = GetStyle(ControlStyles.UserMouse);
                SetStyle(ControlStyles.UserMouse, true);
                base.WndProc(ref m);
                SetStyle(ControlStyles.UserMouse, oldStyle);
                break;

            case PInvokeCore.WM_VSCROLL:
                {
                    base.WndProc(ref m);
                    SCROLLBAR_COMMAND loWord = (SCROLLBAR_COMMAND)m.WParamInternal.LOWORD;
                    if (loWord == SCROLLBAR_COMMAND.SB_THUMBTRACK)
                    {
                        OnVScroll(EventArgs.Empty);
                    }
                    else if (loWord == SCROLLBAR_COMMAND.SB_THUMBPOSITION)
                    {
                        OnVScroll(EventArgs.Empty);
                    }

                    break;
                }

            case PInvokeCore.WM_HSCROLL:
                {
                    base.WndProc(ref m);
                    SCROLLBAR_COMMAND loWord = (SCROLLBAR_COMMAND)m.WParamInternal.LOWORD;
                    if (loWord == SCROLLBAR_COMMAND.SB_THUMBTRACK)
                    {
                        OnHScroll(EventArgs.Empty);
                    }
                    else if (loWord == SCROLLBAR_COMMAND.SB_THUMBPOSITION)
                    {
                        OnHScroll(EventArgs.Empty);
                    }

                    break;
                }

            default:
                base.WndProc(ref m);
                break;
        }
    }
}
