// 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.Drawing.Design;
using System.Globalization;
using System.Windows.Forms.VisualStyles;
using Windows.Win32.UI.Accessibility;
using Windows.Win32.UI.Input.Ime;

namespace System.Windows.Forms;

/// <summary>
///  MaskedTextBox control definition class.
///  Uses the services from the System.ComponentModel.MaskedTextBoxProvider class.
///  Search Microsoft SPO for "MaskEdit.doc" to see spec
/// </summary>
[DefaultEvent(nameof(MaskInputRejected))]
[DefaultBindingProperty(nameof(Text))]
[DefaultProperty(nameof(Mask))]
[Designer($"System.Windows.Forms.Design.MaskedTextBoxDesigner, {AssemblyRef.SystemDesign}")]
[SRDescription(nameof(SR.DescriptionMaskedTextBox))]
public partial class MaskedTextBox : TextBoxBase
{
    // Consider: The MaskedTextBox control, when initialized with a non-null/empty mask, processes all
    // WM_CHAR messages and always sets the text using the SetWindowText Windows function in the furthest base
    // class. This means that the underlying Edit control won't enable Undo operations and the context
    // menu behavior will be a bit different (for instance Copy option is enabled when PasswordChar is set).
    // To provide Undo functionality and make the context menu behave like the Edit control, we would have
    // to implement our own. For more info about how to do this, see:
    // https://docs.microsoft.com/en-us/archive/msdn-magazine/2000/november/c-q-a-filetype-icon-detector-app-custom-context-menus-unreferenced-variables-and-string-conversions

    private const bool Forward = true;
    private const bool Backward = false;
    private const string NullMask = "<>"; // any char/str is OK here.

    private static readonly object s_maskInputRejectedEvent = new();
    private static readonly object s_validationCompletedEvent = new();
    private static readonly object s_textAlignChangedEvent = new();
    private static readonly object s_isOverwriteModeChangedEvent = new();
    private static readonly object s_maskChangedEvent = new();

    // The native edit control's default password char (per thread). See corresponding property for more info.
    private static char s_systemPwdChar;

    // Values to track changes in IME composition string (if any). Having const variables is a bit more efficient
    // than having an enum (which creates a class).
    private const byte ImeConversionNone = 0;       // no conversion has been performed in the composition string.
    private const byte ImeConversionUpdate = 1;     // the char being composed has been updated but not converted yet.
    private const byte ImeConversionCompleted = 2;  // the char being composed has been fully converted.

    /////////  Instance fields

    // Used for keeping selection when prompt is hidden on leave (text changes).
    private int _lastSelLength;

    // Used for caret positioning.
    private int _caretTestPos;

    // Bit mask - Determines when the Korean IME composition string is completed so converted character can be processed.
    private static readonly int s_imeEndingComposition = BitVector32.CreateMask();

    // Bit mask - Determines when the Korean IME is completing a composition, used when forcing conversion.
    private static readonly int s_imeCompleting = BitVector32.CreateMask(s_imeEndingComposition);

    // Used for handling characters that have a modifier (Ctrl-A, Shift-Del...).
    private static readonly int s_handleKeyPress = BitVector32.CreateMask(s_imeCompleting);

    // Bit mask - Used to simulate a null mask. Needed since a MaskedTextProvider object cannot be
    // initialized with a null mask but we need one even in this case as a backend for
    // default properties. This is to support creating a MaskedTextBox with the default
    // constructor, specially at design time.
    private static readonly int s_isNullMask = BitVector32.CreateMask(s_handleKeyPress);

    // Bit mask - Used in conjunction with get_Text to return the text that is actually set in the native
    // control. This is required to be able to measure text correctly (GetPreferredSize) and
    // to compare against during set_Text (to bail if the same and not to raise TextChanged event).
    private static readonly int s_queryBaseText = BitVector32.CreateMask(s_isNullMask);

    // If true, the input text is rejected whenever a character does not comply with the mask; a MaskInputRejected
    // event is fired for the failing character.
    // If false, characters in the input string are processed one by one accepting the ones that comply
    // with the mask and raising the MaskInputRejected event for the rejected ones.
    private static readonly int s_rejectInputOnFirstFailure = BitVector32.CreateMask(s_queryBaseText);

    // Bit masks for boolean properties.
    private static readonly int s_hidePromptOnLeave = BitVector32.CreateMask(s_rejectInputOnFirstFailure);
    private static readonly int s_beepOnError = BitVector32.CreateMask(s_hidePromptOnLeave);
    private static readonly int s_useSystemPasswordChar = BitVector32.CreateMask(s_beepOnError);
    private static readonly int s_insertToggled = BitVector32.CreateMask(s_useSystemPasswordChar);
    private static readonly int s_cutCopyIncludePrompt = BitVector32.CreateMask(s_insertToggled);
    private static readonly int s_cutCopyIncludeLiterals = BitVector32.CreateMask(s_cutCopyIncludePrompt);

    /////////  Properties backend fields. See corresponding property comments for more info.

    private char _passwordChar; // control's pwd char, it could be different from the one displayed if using system password.
    private Type? _validatingType;
    private IFormatProvider? _formatProvider;
    private MaskedTextProvider _maskedTextProvider;
    private InsertKeyMode _insertMode;
    private HorizontalAlignment _textAlign;

    // Bit vector to represent bool variables.
    private BitVector32 _flagState;

    /// <summary>
    ///  Constructs the MaskedTextBox with the specified MaskedTextProvider object.
    /// </summary>
    public MaskedTextBox()
    {
        MaskedTextProvider maskedTextProvider = new(NullMask, CultureInfo.CurrentCulture);
        _flagState[s_isNullMask] = true;
        Initialize(maskedTextProvider);
    }

    /// <summary>
    ///  Constructs the MaskedTextBox with the specified MaskedTextProvider object.
    /// </summary>
    public MaskedTextBox(string mask)
    {
        ArgumentNullException.ThrowIfNull(mask);

        MaskedTextProvider maskedTextProvider = new(mask, CultureInfo.CurrentCulture);
        _flagState[s_isNullMask] = false;
        Initialize(maskedTextProvider);
    }

    /// <summary>
    ///  Constructs the MaskedTextBox with the specified MaskedTextProvider object.
    /// </summary>
    public MaskedTextBox(MaskedTextProvider maskedTextProvider)
    {
        ArgumentNullException.ThrowIfNull(maskedTextProvider);

        _flagState[s_isNullMask] = false;
        Initialize(maskedTextProvider);
    }

    /// <summary>
    ///  Initializes the object with the specified MaskedTextProvider object and default
    ///  property values.
    /// </summary>
    [MemberNotNull(nameof(_maskedTextProvider))]
    private void Initialize(MaskedTextProvider maskedTextProvider)
    {
        Debug.Assert(maskedTextProvider is not null, "Initializing from a null MaskProvider ref.");

        _maskedTextProvider = maskedTextProvider;

        // set the initial display text.
        if (!_flagState[s_isNullMask])
        {
            SetWindowText();
        }

        // set default values.
        _passwordChar = _maskedTextProvider.PasswordChar;
        _insertMode = InsertKeyMode.Default;

        _flagState[s_hidePromptOnLeave] = false;
        _flagState[s_beepOnError] = false;
        _flagState[s_useSystemPasswordChar] = false;
        _flagState[s_rejectInputOnFirstFailure] = false;

        // CutCopyMaskFormat - set same defaults as TextMaskFormat (IncludePromptAndLiterals).
        // It is a lot easier to handle this flags individually since that's the way the MaskedTextProvider does it.
        _flagState[s_cutCopyIncludePrompt] = _maskedTextProvider.IncludePrompt;
        _flagState[s_cutCopyIncludeLiterals] = _maskedTextProvider.IncludeLiterals;

        // fields for internal use.
        _flagState[s_handleKeyPress] = true;
        _caretTestPos = 0;
    }

    ///////////////////  Properties
    ///
    /// <summary>
    ///  Unsupported method/property.
    /// </summary>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public new bool AcceptsTab
    {
        get { return false; }
        set { }
    }

    /// <summary>
    ///  Specifies whether the prompt character should be treated as a valid input character or not.
    ///  The setter resets the underlying MaskedTextProvider object and attempts
    ///  to add the existing input text (if any) using the new mask, failure is ignored.
    ///  This property has no particular effect if no mask has been set.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxAllowPromptAsInputDescr))]
    [DefaultValue(true)]
    public bool AllowPromptAsInput
    {
        get
        {
            return _maskedTextProvider.AllowPromptAsInput;
        }
        set
        {
            if (value != _maskedTextProvider.AllowPromptAsInput)
            {
                // Recreate masked text provider since this property is read-only.
                MaskedTextProvider newProvider = new(
                    _maskedTextProvider.Mask,
                    _maskedTextProvider.Culture,
                    value,
                    _maskedTextProvider.PromptChar,
                    _maskedTextProvider.PasswordChar,
                    _maskedTextProvider.AsciiOnly);

                SetMaskedTextProvider(newProvider);
            }
        }
    }

    /// <summary>
    ///  Unsupported method/property.
    /// </summary>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public new event EventHandler? AcceptsTabChanged
    {
        add { }
        remove { }
    }

    /// <summary>
    ///  Specifies whether only ASCII characters are accepted as valid input.
    ///  This property has no particular effect if no mask has been set.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxAsciiOnlyDescr))]
    [RefreshProperties(RefreshProperties.Repaint)]
    [DefaultValue(false)]
    public bool AsciiOnly
    {
        get
        {
            return _maskedTextProvider.AsciiOnly;
        }

        set
        {
            if (value != _maskedTextProvider.AsciiOnly)
            {
                // Recreate masked text provider since this property is read-only.
                MaskedTextProvider newProvider = new(
                    _maskedTextProvider.Mask,
                    _maskedTextProvider.Culture,
                    _maskedTextProvider.AllowPromptAsInput,
                    _maskedTextProvider.PromptChar,
                    _maskedTextProvider.PasswordChar,
                    value);

                SetMaskedTextProvider(newProvider);
            }
        }
    }

    /// <summary>
    ///  Specifies whether to play a beep when the input is not valid according to the mask.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxBeepOnErrorDescr))]
    [DefaultValue(false)]
    public bool BeepOnError
    {
        get
        {
            return _flagState[s_beepOnError];
        }
        set
        {
            _flagState[s_beepOnError] = value;
        }
    }

    /// <summary>
    ///  Gets a value indicating whether the user can undo the previous operation in a text box control.
    ///  Unsupported method/property.
    ///  WndProc ignores EM_CANUNDO.
    /// </summary>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public new bool CanUndo
    {
        get
        {
            return false;
        }
    }

    /// <summary>
    ///  Returns the parameters needed to create the handle. Inheriting classes
    ///  can override this to provide extra functionality. They should not,
    ///  however, forget to call base.getCreateParams() first to get the struct
    ///  filled up with the basic info.
    /// </summary>
    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;

            // Translate for Rtl if necessary
            HorizontalAlignment align = RtlTranslateHorizontal(_textAlign);
            cp.ExStyle &= ~(int)WINDOW_EX_STYLE.WS_EX_RIGHT;   // WS_EX_RIGHT overrides the ES_XXXX alignment styles
            switch (align)
            {
                case HorizontalAlignment.Left:
                    cp.Style |= PInvoke.ES_LEFT;
                    break;
                case HorizontalAlignment.Center:
                    cp.Style |= PInvoke.ES_CENTER;
                    break;
                case HorizontalAlignment.Right:
                    cp.Style |= PInvoke.ES_RIGHT;
                    break;
            }

            return cp;
        }
    }

    /// <summary>
    ///  The culture that determines the value of the localizable mask language separators and placeholders.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxCultureDescr))]
    [RefreshProperties(RefreshProperties.Repaint)]
    public CultureInfo Culture
    {
        get
        {
            return _maskedTextProvider.Culture;
        }

        set
        {
            ArgumentNullException.ThrowIfNull(value);

            if (!_maskedTextProvider.Culture.Equals(value))
            {
                // Recreate masked text provider since this property is read-only.
                MaskedTextProvider newProvider = new(
                    _maskedTextProvider.Mask,
                    value,
                    _maskedTextProvider.AllowPromptAsInput,
                    _maskedTextProvider.PromptChar,
                    _maskedTextProvider.PasswordChar,
                    _maskedTextProvider.AsciiOnly);

                SetMaskedTextProvider(newProvider);
            }
        }
    }

    /// <summary>
    ///  Specifies the formatting options for text cut/copied to the clipboard (Whether the mask returned from the Text
    ///  property includes Literals and/or prompt characters).
    ///  When prompt characters are excluded, they are returned as spaces in the string returned.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxCutCopyMaskFormat))]
    [RefreshProperties(RefreshProperties.Repaint)]
    [DefaultValue(MaskFormat.IncludeLiterals)]
    public MaskFormat CutCopyMaskFormat
    {
        get
        {
            if (_flagState[s_cutCopyIncludePrompt])
            {
                if (_flagState[s_cutCopyIncludeLiterals])
                {
                    return MaskFormat.IncludePromptAndLiterals;
                }

                return MaskFormat.IncludePrompt;
            }

            if (_flagState[s_cutCopyIncludeLiterals])
            {
                return MaskFormat.IncludeLiterals;
            }

            return MaskFormat.ExcludePromptAndLiterals;
        }

        set
        {
            // valid values are 0x0 to 0x3
            SourceGenerated.EnumValidator.Validate(value);

            if (value == MaskFormat.IncludePrompt)
            {
                _flagState[s_cutCopyIncludePrompt] = true;
                _flagState[s_cutCopyIncludeLiterals] = false;
            }
            else if (value == MaskFormat.IncludeLiterals)
            {
                _flagState[s_cutCopyIncludePrompt] = false;
                _flagState[s_cutCopyIncludeLiterals] = true;
            }
            else // value == MaskFormat.IncludePromptAndLiterals || value == MaskFormat.ExcludePromptAndLiterals
            {
                bool include = value == MaskFormat.IncludePromptAndLiterals;
                _flagState[s_cutCopyIncludePrompt] = include;
                _flagState[s_cutCopyIncludeLiterals] = include;
            }
        }
    }

    /// <summary>
    ///  Specifies the IFormatProvider to be used when parsing the string to the ValidatingType.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public IFormatProvider? FormatProvider
    {
        get
        {
            return _formatProvider;
        }

        set
        {
            _formatProvider = value;
        }
    }

    /// <summary>
    ///  Specifies whether the PromptCharacter is displayed when the control loses focus.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxHidePromptOnLeaveDescr))]
    [RefreshProperties(RefreshProperties.Repaint)]
    [DefaultValue(false)]
    public bool HidePromptOnLeave
    {
        get
        {
            return _flagState[s_hidePromptOnLeave];
        }
        set
        {
            if (_flagState[s_hidePromptOnLeave] != value)
            {
                _flagState[s_hidePromptOnLeave] = value;

                // If the control is not focused and there are available edit positions (mask not full) we need to
                // update the displayed text.
                if (!_flagState[s_isNullMask] && !Focused && !MaskFull && !DesignMode)
                {
                    SetWindowText();
                }
            }
        }
    }

    /// <summary>
    ///  Specifies whether to include mask literal characters when formatting the text.
    /// </summary>
    private bool IncludeLiterals
    {
        get
        {
            return _maskedTextProvider.IncludeLiterals;
        }
        set
        {
            _maskedTextProvider.IncludeLiterals = value;
        }
    }

    /// <summary>
    ///  Specifies whether to include the mask prompt character when formatting the text in places
    ///  where an edit char has not being assigned.
    /// </summary>
    private bool IncludePrompt
    {
        get
        {
            return _maskedTextProvider.IncludePrompt;
        }
        set
        {
            _maskedTextProvider.IncludePrompt = value;
        }
    }

    /// <summary>
    ///  Specifies the text insertion mode of the text box. This can be used to simulated the Access masked text
    ///  control behavior where insertion is set to TextInsertionMode.AlwaysOverwrite
    ///  This property has no particular effect if no mask has been set.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxInsertKeyModeDescr))]
    [DefaultValue(InsertKeyMode.Default)]
    public InsertKeyMode InsertKeyMode
    {
        get
        {
            return _insertMode;
        }
        set
        {
            // valid values are 0x0 to 0x2
            SourceGenerated.EnumValidator.Validate(value);

            if (_insertMode != value)
            {
                bool isOverwrite = IsOverwriteMode;
                _insertMode = value;

                if (isOverwrite != IsOverwriteMode)
                {
                    OnIsOverwriteModeChanged(EventArgs.Empty);
                }
            }
        }
    }

    /// <summary>
    ///  Overridden to handle unsupported RETURN key.
    /// </summary>
    protected override bool IsInputKey(Keys keyData)
    {
        if ((keyData & Keys.KeyCode) == Keys.Return)
        {
            return false;
        }

        return base.IsInputKey(keyData);
    }

    /// <summary>
    ///  Specifies whether text insertion mode in 'on' or not.
    /// </summary>
    [Browsable(false)]
    public bool IsOverwriteMode
    {
        get
        {
            if (_flagState[s_isNullMask])
            {
                return false; // EditBox always inserts.
            }

            switch (_insertMode)
            {
                case InsertKeyMode.Overwrite:
                    return true;

                case InsertKeyMode.Insert:
                    return false;

                case InsertKeyMode.Default:

                    // Note that the insert key state should be per process and its initial state insert, this is the
                    // behavior of apps like WinWord, WordPad and VS; so we have to keep track of it and not query its
                    // system value.
                    // return Control.IsKeyLocked(Keys.Insert);
                    return _flagState[s_insertToggled];

                default:
                    Debug.Fail("Invalid InsertKeyMode. This code path should have never been executed.");
                    return false;
            }
        }
    }

    /// <summary>
    ///  Event to notify when the insert mode has changed. This is required for data binding.
    /// </summary>
    [SRCategory(nameof(SR.CatPropertyChanged))]
    [SRDescription(nameof(SR.MaskedTextBoxIsOverwriteModeChangedDescr))]
    public event EventHandler? IsOverwriteModeChanged
    {
        add => Events.AddHandler(s_isOverwriteModeChangedEvent, value);
        remove => Events.RemoveHandler(s_isOverwriteModeChangedEvent, value);
    }

    /// <summary>
    ///  Unsupported method/property.
    /// </summary>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [AllowNull]
    public new string[] Lines
    {
        get
        {
            string[] lines;

            _flagState[s_queryBaseText] = true;
            try
            {
                lines = base.Lines;
            }
            finally
            {
                _flagState[s_queryBaseText] = false;
            }

            return lines;
        }
        set { }
    }

    /// <summary>
    ///  The mask applied to this control. The setter resets the underlying MaskedTextProvider object and attempts
    ///  to add the existing input text (if any) using the new mask, failure is ignored.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxMaskDescr))]
    [RefreshProperties(RefreshProperties.Repaint)]
    [DefaultValue("")]
    [MergableProperty(false)]
    [Localizable(true)]
    [AllowNull]
    [Editor($"System.Windows.Forms.Design.MaskPropertyEditor, {AssemblyRef.SystemDesign}", typeof(UITypeEditor))]
    public string Mask
    {
        get
        {
            return _flagState[s_isNullMask] ? string.Empty : _maskedTextProvider.Mask;
        }
        set
        {
            //
            // We don't do anything if:
            // 1. IsNullOrEmpty( value )->[Reset control] && _flagState[IS_NULL_MASK]==>Already Reset.
            // 2. !IsNullOrEmpty( value )->[Set control] && !_flagState[IS_NULL_MASK][control is set] &&
            //    [value is the same]==>No need to update.
            //
            if (_flagState[s_isNullMask] == string.IsNullOrEmpty(value) && (_flagState[s_isNullMask] || value == _maskedTextProvider.Mask))
            {
                return;
            }

            string? text = null;
            string? newMask = value;

            // We need to update the _flagState[IS_NULL_MASK]field before raising any events (when setting the maskedTextProvider) so
            // querying for properties from an event handler returns the right value (i.e: Text).

            if (string.IsNullOrEmpty(value)) // Resetting the control, the native edit control will be in charge.
            {
                // Need to get the formatted & unformatted text before resetting the mask, they'll be used to determine whether we need to
                // raise the TextChanged event.
                string formattedText = TextOutput;
                string unformattedText = _maskedTextProvider.ToString(false, false);

                _flagState[s_isNullMask] = true;

                if (_maskedTextProvider.IsPassword)
                {
                    SetEditControlPasswordChar(_maskedTextProvider.PasswordChar);
                }

                // Set the window text to the unformatted text before raising events. Also, TextChanged needs to be raised after MaskChanged so
                // pass false to SetWindowText 'raiseTextChanged' param.
                SetWindowText(unformattedText, false, false);

                EventArgs e = EventArgs.Empty;

                OnMaskChanged(e);

                if (unformattedText != formattedText)
                {
                    OnTextChanged(e);
                }

                newMask = NullMask;
            }
            else    // Setting control to a new value.
            {
                foreach (char c in value)
                {
                    if (!MaskedTextProvider.IsValidMaskChar(c))
                    {
                        // Same message as in SR.MaskedTextProviderMaskInvalidChar in System.txt
                        throw new ArgumentException(SR.MaskedTextBoxMaskInvalidChar);
                    }
                }

                if (_flagState[s_isNullMask])
                {
                    // If this.IsNullMask, we are setting the mask to a new value; in this case we need to get the text because
                    // the underlying MTP does not have it (used as a property backend only) and pass it to SetMaskedTextProvider
                    // method below to update the provider.

                    text = Text;
                }
            }

            // Recreate masked text provider since this property is read-only.
            MaskedTextProvider newProvider = new(
                newMask!,
                _maskedTextProvider.Culture,
                _maskedTextProvider.AllowPromptAsInput,
                _maskedTextProvider.PromptChar,
                _maskedTextProvider.PasswordChar,
                _maskedTextProvider.AsciiOnly);

            // text is null when setting to a different mask value or when resetting the mask to null.
            // text is not null only when setting the mask from null to some value.
            SetMaskedTextProvider(newProvider, text);
        }
    }

    /// <summary>
    ///  Event to notify when the mask has changed.
    /// </summary>
    [SRCategory(nameof(SR.CatPropertyChanged))]
    [SRDescription(nameof(SR.MaskedTextBoxMaskChangedDescr))]
    public event EventHandler? MaskChanged
    {
        add => Events.AddHandler(s_maskChangedEvent, value);
        remove => Events.RemoveHandler(s_maskChangedEvent, value);
    }

    /// <summary>
    ///  Specifies whether the test string required input positions, as specified by the mask, have
    ///  all been assigned.
    /// </summary>
    [Browsable(false)]
    public bool MaskCompleted
    {
        get
        {
            return _maskedTextProvider.MaskCompleted;
        }
    }

    /// <summary>
    ///  Specifies whether all inputs (required and optional) have been provided into the mask successfully.
    /// </summary>
    [Browsable(false)]
    public bool MaskFull
    {
        get
        {
            return _maskedTextProvider.MaskFull;
        }
    }

    /// <summary>
    ///  Returns a copy of the control's internal MaskedTextProvider. This is useful for user's to provide
    ///  cloning semantics for the control (we don't want to do it) w/o incurring in any perf penalty since
    ///  some of the properties require recreating the underlying provider when they are changed.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public MaskedTextProvider? MaskedTextProvider
    {
        get
        {
            return _flagState[s_isNullMask] ? null : (MaskedTextProvider)_maskedTextProvider.Clone();
        }
    }

    /// <summary>
    ///  Event to notify when an input has been rejected according to the mask.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxMaskInputRejectedDescr))]
    public event MaskInputRejectedEventHandler? MaskInputRejected
    {
        add => Events.AddHandler(s_maskInputRejectedEvent, value);
        remove => Events.RemoveHandler(s_maskInputRejectedEvent, value);
    }

    /// <summary>
    ///  Unsupported method/property.
    ///  WndProc ignores EM_LIMITTEXT &amp; this is a virtual method.
    /// </summary>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public override int MaxLength
    {
        get => base.MaxLength;
        set { }
    }

    /// <summary>
    ///  Unsupported method/property.
    ///  virtual method.
    /// </summary>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public override bool Multiline
    {
        get { return false; }
        set { }
    }

    /// <summary>
    ///  Unsupported method/property.
    /// </summary>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public new event EventHandler? MultilineChanged
    {
        add { }
        remove { }
    }

    /// <summary>
    ///  Specifies the character to be used in the formatted string in place of editable characters, if
    ///  set to any printable character, the text box becomes a password text box, to reset it use the null
    ///  character.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxPasswordCharDescr))]
    [RefreshProperties(RefreshProperties.Repaint)]
    [DefaultValue('\0')]
    public char PasswordChar
    {
        get
        {
            // The password char could be the one set in the control or the system password char,
            // in any case the maskedTextProvider has the correct one.
            return _maskedTextProvider.PasswordChar;
        }
        set
        {
            if (!MaskedTextProvider.IsValidPasswordChar(value)) // null character accepted (resets value)
            {
                // Same message as in SR.MaskedTextProviderInvalidCharError.
                throw new ArgumentException(SR.MaskedTextBoxInvalidCharError);
            }

            if (_passwordChar != value)
            {
                if (value == _maskedTextProvider.PromptChar)
                {
                    // Prompt and password chars must be different.
                    throw new InvalidOperationException(SR.MaskedTextBoxPasswordAndPromptCharError);
                }

                _passwordChar = value;

                // UseSystemPasswordChar take precedence over PasswordChar...Let's check.
                if (!UseSystemPasswordChar)
                {
                    _maskedTextProvider.PasswordChar = value;

                    if (_flagState[s_isNullMask])
                    {
                        SetEditControlPasswordChar(value);
                    }
                    else
                    {
                        SetWindowText();
                    }

                    VerifyImeRestrictedModeChanged();
                }
            }
        }
    }

    /// <summary>
    ///  Determines if the control is in password protect mode.
    /// </summary>
    private protected override bool PasswordProtect
        => _maskedTextProvider.IsPassword;

    /// <summary>
    ///  Specifies the prompt character to be used in the formatted string for unsupplied characters.
    /// </summary>
    [SRCategory(nameof(SR.CatAppearance))]
    [SRDescription(nameof(SR.MaskedTextBoxPromptCharDescr))]
    [RefreshProperties(RefreshProperties.Repaint)]
    [Localizable(true)]
    [DefaultValue('_')]
    public char PromptChar
    {
        get
        {
            return _maskedTextProvider.PromptChar;
        }
        set
        {
            if (!MaskedTextProvider.IsValidInputChar(value))
            {
                // This message is the same as the one in SR.MaskedTextProviderInvalidCharError.
                throw new ArgumentException(SR.MaskedTextBoxInvalidCharError);
            }

            if (_maskedTextProvider.PromptChar != value)
            {
                // We need to check maskedTextProvider password char in case it is using the system password.
                if (value == _passwordChar || value == _maskedTextProvider.PasswordChar)
                {
                    // Prompt and password chars must be different.
                    throw new InvalidOperationException(SR.MaskedTextBoxPasswordAndPromptCharError);
                }

                // Recreate masked text provider to be consistent with AllowPromptAsInput
                // - current text may have chars with same value as new prompt.
                MaskedTextProvider newProvider = new(
                    _maskedTextProvider.Mask,
                    _maskedTextProvider.Culture,
                    _maskedTextProvider.AllowPromptAsInput,
                    value,
                    _maskedTextProvider.PasswordChar,
                    _maskedTextProvider.AsciiOnly);

                SetMaskedTextProvider(newProvider);
            }
        }
    }

    /// <summary>
    ///  Overwrite base class' property.
    /// </summary>
    public new bool ReadOnly
    {
        get => base.ReadOnly;

        set
        {
            if (ReadOnly != value)
            {
                // if true, this disables IME in the base class.
                base.ReadOnly = value;

                if (!_flagState[s_isNullMask])
                {
                    // Prompt will be hidden.
                    SetWindowText();
                }
            }
        }
    }

    /// <summary>
    ///  Specifies whether to include the mask prompt character when formatting the text in places
    ///  where an edit char has not being assigned.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxRejectInputOnFirstFailureDescr))]
    [DefaultValue(false)]
    public bool RejectInputOnFirstFailure
    {
        get
        {
            return _flagState[s_rejectInputOnFirstFailure];
        }
        set
        {
            _flagState[s_rejectInputOnFirstFailure] = value;
        }
    }

    /// <summary>
    ///  Specifies whether to reset and skip the current position if editable, when the input character
    ///  has the same value as the prompt. This property takes precedence over AllowPromptAsInput.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxResetOnPrompt))]
    [DefaultValue(true)]
    public bool ResetOnPrompt
    {
        get
        {
            return _maskedTextProvider.ResetOnPrompt;
        }
        set
        {
            _maskedTextProvider.ResetOnPrompt = value;
        }
    }

    /// <summary>
    ///  Specifies whether to reset and skip the current position if editable, when the input
    ///  is the space character.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxResetOnSpace))]
    [DefaultValue(true)]
    public bool ResetOnSpace
    {
        get
        {
            return _maskedTextProvider.ResetOnSpace;
        }
        set
        {
            _maskedTextProvider.ResetOnSpace = value;
        }
    }

    /// <summary>
    ///  Specifies whether to skip the current position if non-editable and the input character has
    ///  the same value as the literal at that position.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxSkipLiterals))]
    [DefaultValue(true)]
    public bool SkipLiterals
    {
        get
        {
            return _maskedTextProvider.SkipLiterals;
        }
        set
        {
            _maskedTextProvider.SkipLiterals = value;
        }
    }

    /// <summary>
    ///  The currently selected text (if any) in the control.
    /// </summary>
    [AllowNull]
    public override string SelectedText
    {
        get
        {
            if (_flagState[s_isNullMask])
            {
                return base.SelectedText;
            }

            return GetSelectedText();
        }
        set
        {
            SetSelectedTextInternal(value, true);
        }
    }

    internal override void SetSelectedTextInternal(string? value, bool clearUndo)
    {
        if (_flagState[s_isNullMask])
        {
            base.SetSelectedTextInternal(value, true); // Operates as a regular text box base.
            return;
        }

        PasteInt(value);
    }

    /// <summary>
    ///  Set the composition string as the result string.
    /// </summary>
    private void ImeComplete()
    {
        _flagState[s_imeCompleting] = true;
        ImeNotify(NOTIFY_IME_INDEX.CPS_COMPLETE);
    }

    /// <summary>
    ///  Notifies the IMM about changes to the status of the IME input context.
    /// </summary>
    private void ImeNotify(NOTIFY_IME_INDEX action)
    {
        HIMC inputContext = PInvoke.ImmGetContext(this);

        if (inputContext != IntPtr.Zero)
        {
            try
            {
                PInvoke.ImmNotifyIME(inputContext, NOTIFY_IME_ACTION.NI_COMPOSITIONSTR, action, 0);
            }
            finally
            {
                PInvoke.ImmReleaseContext(this, inputContext);
            }
        }
        else
        {
            Debug.Fail("Could not get IME input context.");
        }
    }

    /// <summary>
    ///  Sets the underlying edit control's password char to the one obtained from this.PasswordChar.
    ///  This is used when the control is passworded and this.flagState[IS_NULL_MASK].
    /// </summary>
    private void SetEditControlPasswordChar(char pwdChar)
    {
        if (IsHandleCreated)
        {
            // This message does not return a value.
            PInvokeCore.SendMessage(this, PInvokeCore.EM_SETPASSWORDCHAR, (WPARAM)pwdChar);
            Invalidate();
        }
    }

    /// <summary>
    ///  The value of the Edit control default password char.
    /// </summary>
    private static char SystemPasswordChar
    {
        get
        {
            if (s_systemPwdChar == '\0')
            {
                // We need to temporarily create an edit control to get the default password character.
                // We cannot use this control because we would have to reset the native control's password char to use
                // the default one so we can get it; this would change the text displayed in the box (even for a short time)
                // opening a sec hole.

                using TextBox txtBox = new();
                txtBox.UseSystemPasswordChar = true; // this forces the creation of the control handle.

                s_systemPwdChar = txtBox.PasswordChar;
            }

            return s_systemPwdChar;
        }
    }

    internal override bool SupportsUiaProviders => true;

    /// <summary>
    ///  The Text setter validates the input char by char, raising the MaskInputRejected event for invalid chars.
    ///  The Text getter returns the formatted text according to the IncludeLiterals and IncludePrompt properties.
    /// </summary>
    [Editor($"System.Windows.Forms.Design.MaskedTextBoxTextEditor, {AssemblyRef.SystemDesign}", typeof(UITypeEditor))]
    [SRCategory(nameof(SR.CatAppearance))]
    [RefreshProperties(RefreshProperties.Repaint)]
    [Bindable(true)]
    [DefaultValue("")]
    [AllowNull]
    [Localizable(true)]
    public override string Text
    {
        get
        {
            if (_flagState[s_isNullMask] || _flagState[s_queryBaseText])
            {
                return base.Text;
            }

            return TextOutput;
        }
        set
        {
            if (_flagState[s_isNullMask])
            {
                base.Text = value;
                return;
            }

            if (string.IsNullOrEmpty(value))
            {
                // reset the input text.
                Delete(Keys.Delete, 0, _maskedTextProvider.Length);
            }
            else
            {
                if (RejectInputOnFirstFailure)
                {
                    string oldText = TextOutput;
                    if (_maskedTextProvider.Set(value, out _caretTestPos, out MaskedTextResultHint hint))
                    {
                        if (TextOutput != oldText)
                        {
                            SetText();
                        }

                        SelectionStart = ++_caretTestPos;
                    }
                    else
                    {
                        OnMaskInputRejected(new MaskInputRejectedEventArgs(_caretTestPos, hint));
                    }
                }
                else
                {
                    Replace(value, /*startPosition*/ 0, /*selectionLen*/ _maskedTextProvider.Length);
                }
            }
        }
    }

    /// <summary>
    ///  Returns the length of the displayed text.
    /// </summary>
    [Browsable(false)]
    public override int TextLength
    {
        get
        {
            if (_flagState[s_isNullMask])
            {
                return base.TextLength;
            }

            // On older platforms TextBoxBase.TextLength calls Text.Length directly and
            // does not query the window for the actual text length.
            // If TextMaskFormat is set to a anything different from IncludePromptAndLiterals
            // or HidePromptOnLeave is true the return value may be incorrect because the
            // Text property value and the display text may be different.
            // We need to handle this here.
            return GetFormattedDisplayString().Length;
        }
    }

    /// <summary>
    ///  The formatted text, it is what the Text getter returns when a mask has been applied to the control.
    ///  The text format follows the IncludeLiterals and IncludePrompt properties (See MaskedTextProvider.ToString()).
    /// </summary>
    private string TextOutput
    {
        get
        {
            Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");
            return _maskedTextProvider.ToString();
        }
    }

    /// <summary>
    ///  Gets or sets how text is aligned in the control.
    ///  Note: This code is duplicated in TextBox for simplicity.
    /// </summary>
    [Localizable(true)]
    [SRCategory(nameof(SR.CatAppearance))]
    [DefaultValue(HorizontalAlignment.Left)]
    [SRDescription(nameof(SR.TextBoxTextAlignDescr))]
    public HorizontalAlignment TextAlign
    {
        get
        {
            return _textAlign;
        }
        set
        {
            if (_textAlign != value)
            {
                // verify that 'value' is a valid enum type...
                // valid values are 0x0 to 0x2
                SourceGenerated.EnumValidator.Validate(value);

                _textAlign = value;
                RecreateHandle();
                OnTextAlignChanged(EventArgs.Empty);
            }
        }
    }

    /// <summary>
    ///  Event to notify the text alignment has changed.
    /// </summary>
    [SRCategory(nameof(SR.CatPropertyChanged))]
    [SRDescription(nameof(SR.RadioButtonOnTextAlignChangedDescr))]
    public event EventHandler? TextAlignChanged
    {
        add => Events.AddHandler(s_textAlignChangedEvent, value);

        remove => Events.RemoveHandler(s_textAlignChangedEvent, value);
    }

    /// <summary>
    ///  Specifies the formatting options for text output (Whether the mask returned from the Text
    ///  property includes Literals and/or prompt characters).
    ///  When prompt characters are excluded, they're returned as spaces in the string returned.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxTextMaskFormat))]
    [RefreshProperties(RefreshProperties.Repaint)]
    [DefaultValue(MaskFormat.IncludeLiterals)]
    public MaskFormat TextMaskFormat
    {
        get
        {
            if (IncludePrompt)
            {
                if (IncludeLiterals)
                {
                    return MaskFormat.IncludePromptAndLiterals;
                }

                return MaskFormat.IncludePrompt;
            }

            if (IncludeLiterals)
            {
                return MaskFormat.IncludeLiterals;
            }

            return MaskFormat.ExcludePromptAndLiterals;
        }

        set
        {
            if (TextMaskFormat == value)
            {
                return;
            }

            // valid values are 0x0 to 0x3
            SourceGenerated.EnumValidator.Validate(value);

            // Changing the TextMaskFormat will likely change the 'output' text (Text getter value). Cache old value to
            // verify it against the new value and raise OnTextChange if needed.
            string? oldText = _flagState[s_isNullMask] ? null : TextOutput;

            if (value == MaskFormat.IncludePrompt)
            {
                IncludePrompt = true;
                IncludeLiterals = false;
            }
            else if (value == MaskFormat.IncludeLiterals)
            {
                IncludePrompt = false;
                IncludeLiterals = true;
            }
            else // value == MaskFormat.IncludePromptAndLiterals || value == MaskFormat.ExcludePromptAndLiterals
            {
                bool include = value == MaskFormat.IncludePromptAndLiterals;
                IncludePrompt = include;
                IncludeLiterals = include;
            }

            if (oldText is not null && oldText != TextOutput)
            {
                OnTextChanged(EventArgs.Empty);
            }
        }
    }

    /// <summary>
    ///  Provides some interesting information for the TextBox control in String form.
    ///  Returns the test string (no password, including literals and prompt).
    /// </summary>
    public override string ToString()
    {
        if (_flagState[s_isNullMask])
        {
            return base.ToString();
        }

        // base.ToString will call Text, we want to always display prompt and literals.
        bool includePrompt = IncludePrompt;
        bool includeLits = IncludeLiterals;
        string str;
        try
        {
            IncludePrompt = IncludeLiterals = true;
            str = base.ToString();
        }
        finally
        {
            IncludePrompt = includePrompt;
            IncludeLiterals = includeLits;
        }

        return str;
    }

    /// <summary>
    ///  Event to notify when the validating object completes parsing the formatted text.
    /// </summary>
    [SRCategory(nameof(SR.CatFocus))]
    [SRDescription(nameof(SR.MaskedTextBoxTypeValidationCompletedDescr))]
    public event TypeValidationEventHandler? TypeValidationCompleted
    {
        add => Events.AddHandler(s_validationCompletedEvent, value);
        remove => Events.RemoveHandler(s_validationCompletedEvent, value);
    }

    /// <summary>
    ///  Indicates if the text in the edit control should appear as the default password character.
    ///  This property has precedence over the PasswordChar property.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [SRDescription(nameof(SR.MaskedTextBoxUseSystemPasswordCharDescr))]
    [RefreshProperties(RefreshProperties.Repaint)]
    [DefaultValue(false)]
    public bool UseSystemPasswordChar
    {
        get
        {
            return _flagState[s_useSystemPasswordChar];
        }
        set
        {
            if (value != _flagState[s_useSystemPasswordChar])
            {
                if (value)
                {
                    if (SystemPasswordChar == PromptChar)
                    {
                        // Prompt and password chars must be different.
                        throw new InvalidOperationException(SR.MaskedTextBoxPasswordAndPromptCharError);
                    }

                    _maskedTextProvider.PasswordChar = SystemPasswordChar;
                }
                else
                {
                    // _passwordChar could be '\0', in which case we are resetting the display to show the input char.
                    _maskedTextProvider.PasswordChar = _passwordChar;
                }

                _flagState[s_useSystemPasswordChar] = value;

                if (_flagState[s_isNullMask])
                {
                    SetEditControlPasswordChar(_maskedTextProvider.PasswordChar);
                }
                else
                {
                    SetWindowText();
                }

                VerifyImeRestrictedModeChanged();
            }
        }
    }

    /// <summary>
    ///  Type of the object to be used to parse the text when the user leaves the control.
    ///  A ValidatingType object must implement a method with one fo the following signature:
    ///  public static Object Parse(string)
    ///  public static Object Parse(string, IFormatProvider)
    ///  See DateTime.Parse(...) for an example.
    /// </summary>
    [Browsable(false)]
    [DefaultValue(null)]
    public Type? ValidatingType
    {
        get
        {
            return _validatingType;
        }
        set
        {
            if (_validatingType != value)
            {
                _validatingType = value;
            }
        }
    }

    /// <summary>
    ///  Unsupported method/property.
    /// </summary>
    [Browsable(false)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public new bool WordWrap
    {
        get { return false; }
        set { }
    }

    //////////////  Methods

    /// <summary>
    ///  Clears information about the most recent operation from the undo buffer of the control.
    ///  Unsupported property/method.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new void ClearUndo()
    {
    }

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

    /// <summary>
    ///  Creates a handle for this control. This method is called by the framework, this should
    ///  not be called directly. Inheriting classes should always call <c>base.CreateHandle</c> when overriding this method.
    ///  Overridden to be able to set the control text with the masked (passworded) value when recreating
    ///  handle, since the underlying native edit control is not aware of it.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    protected override void CreateHandle()
    {
        if (!_flagState[s_isNullMask] && RecreatingHandle)
        {
            // update cached text value in Control. Don't preserve caret, cannot query for selection start at this time.
            SetWindowText(GetFormattedDisplayString(), false, false);
        }

        base.CreateHandle();
    }

    /// <summary>
    ///  Deletes characters from the control's text according to the key pressed (Delete/Backspace).
    ///  Returns true if something gets actually deleted, false otherwise.
    /// </summary>
    private void Delete(Keys keyCode, int startPosition, int selectionLen)
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");
        Debug.Assert(keyCode is Keys.Delete or Keys.Back, $"Delete called with keyCode == {keyCode}");
        Debug.Assert(startPosition >= 0 && ((startPosition + selectionLen) <= _maskedTextProvider.Length), "Invalid position range.");

        // On backspace, moving the start position back by one has the same effect as delete. If text is selected, there is no
        // need for moving the position back.

        _caretTestPos = startPosition;

        if (selectionLen == 0)
        {
            if (keyCode == Keys.Back)
            {
                if (startPosition == 0) // At beginning of string, backspace does nothing.
                {
                    return;
                }

                startPosition--; // so it can be treated as delete.
            }
            else // (keyCode == Keys.Delete)
            {
                if ((startPosition + selectionLen) == _maskedTextProvider.Length) // At end of string, delete does nothing.
                {
                    return;
                }
            }
        }

        int endPos = selectionLen > 0 ? startPosition + selectionLen - 1 : startPosition;

        string oldText = TextOutput;
        if (_maskedTextProvider.RemoveAt(startPosition, endPos, out int tempPos, out MaskedTextResultHint hint))
        {
            if (TextOutput != oldText)
            {
                SetText();
                _caretTestPos = startPosition;
            }
            else
            {
                // If succeeded but nothing removed, the caret should move as follows:
                // 1. If selectionLen > 0, or on back and hint == SideEffect: move to selectionStart.
                // 2. If hint == NoEffect, On Delete move to next edit position, if any or not already in one.
                //    On back move to the next edit postion at the left if no more assigned position at the right,
                //    in such case find an assigned position and move one past or one position left if no assigned pos found
                //    (taken care by 'startPosition--' above).
                // 3. If hint == SideEffect, on Back move like arrow key, (startPosition is already moved, startPosition-- above).

                if (selectionLen > 0)
                {
                    _caretTestPos = startPosition;
                }
                else
                {
                    if (hint == MaskedTextResultHint.NoEffect) // Case 2.
                    {
                        if (keyCode == Keys.Delete)
                        {
                            _caretTestPos = _maskedTextProvider.FindEditPositionFrom(startPosition, Forward);
                        }
                        else
                        {
                            if (_maskedTextProvider.FindAssignedEditPositionFrom(startPosition, Forward) == MaskedTextProvider.InvalidIndex)
                            {
                                // No assigned position at the right, nothing to shift then move to the next assigned position at the
                                // left (if any).
                                _caretTestPos = _maskedTextProvider.FindAssignedEditPositionFrom(startPosition, Backward);
                            }
                            else
                            {
                                // there are assigned positions at the right so move to an edit position at the left to get ready for
                                // removing the character on it or just shifting the characters at the right
                                _caretTestPos = _maskedTextProvider.FindEditPositionFrom(startPosition, Backward);
                            }

                            if (_caretTestPos != MaskedTextProvider.InvalidIndex)
                            {
                                _caretTestPos++; // backspace gets ready to remove one position past the edit position.
                            }
                        }

                        if (_caretTestPos == MaskedTextProvider.InvalidIndex)
                        {
                            _caretTestPos = startPosition;
                        }
                    }
                    else // (hint == MaskedTextProvider.OperationHint.SideEffect)
                    {
                        if (keyCode == Keys.Back)  // Case 3.
                        {
                            _caretTestPos = startPosition;
                        }
                    }
                }
            }
        }
        else
        {
            OnMaskInputRejected(new MaskInputRejectedEventArgs(tempPos, hint));
        }

        // Reposition caret. Call base.SelectInternal for perf reasons.
        // this.SelectionLength = 0;
        // this.SelectionStart  = _caretTestPos; // new caret position.
        base.SelectInternal(_caretTestPos, 0, _maskedTextProvider.Length);

        return;
    }

    /// <summary>
    ///  Returns the character nearest to the given point.
    /// </summary>
    public override char GetCharFromPosition(Point pt)
    {
        char ch;

        _flagState[s_queryBaseText] = true;
        try
        {
            ch = base.GetCharFromPosition(pt);
        }
        finally
        {
            _flagState[s_queryBaseText] = false;
        }

        return ch;
    }

    /// <summary>
    ///  Returns the index of the character nearest to the given point.
    /// </summary>
    public override int GetCharIndexFromPosition(Point pt)
    {
        int index;

        _flagState[s_queryBaseText] = true;
        try
        {
            index = base.GetCharIndexFromPosition(pt);
        }
        finally
        {
            _flagState[s_queryBaseText] = false;
        }

        return index;
    }

    /// <summary>
    ///  Returns the position of the last input character (or if available, the next edit position).
    ///  This is used by base.AppendText.
    /// </summary>
    internal override int GetEndPosition()
    {
        if (_flagState[s_isNullMask])
        {
            return base.GetEndPosition();
        }

        int pos = _maskedTextProvider.FindEditPositionFrom(_maskedTextProvider.LastAssignedPosition + 1, Forward);

        if (pos == MaskedTextProvider.InvalidIndex)
        {
            pos = _maskedTextProvider.LastAssignedPosition + 1;
        }

        return pos;
    }

    /// <summary>
    ///  Unsupported method/property.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new int GetFirstCharIndexOfCurrentLine()
    {
        return 0;
    }

    /// <summary>
    ///  Unsupported method/property.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new int GetFirstCharIndexFromLine(int lineNumber)
    {
        return 0;
    }

    /// <summary>
    ///  Gets the string in the text box following the formatting parameters includePrompt and includeLiterals and
    ///  honoring the PasswordChar property.
    /// </summary>
    private string GetFormattedDisplayString()
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");

        bool includePrompt;

        if (ReadOnly) // Always hide prompt.
        {
            includePrompt = false;
        }
        else if (DesignMode) // Not RO and at design time, always show prompt.
        {
            includePrompt = true;
        }
        else // follow HidePromptOnLeave property.
        {
            includePrompt = !(HidePromptOnLeave && !Focused);
        }

        return _maskedTextProvider.ToString(/*ignorePwdChar */ false, includePrompt, /*includeLiterals*/ true, 0, _maskedTextProvider.Length);
    }

    /// <summary>
    ///  Unsupported method/property.
    ///  virtual method.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public override int GetLineFromCharIndex(int index)
    {
        return 0;
    }

    /// <summary>
    ///  Returns the location of the character at the given index.
    /// </summary>
    public override Point GetPositionFromCharIndex(int index)
    {
        Point pos;

        _flagState[s_queryBaseText] = true;
        try
        {
            pos = base.GetPositionFromCharIndex(index);
        }
        finally
        {
            _flagState[s_queryBaseText] = false;
        }

        return pos;
    }

    /// <summary>
    ///  Need to override this method so when get_Text is called we return the text that is actually
    ///  painted in the control so measuring text works on the actual text and not the formatted one.
    /// </summary>
    internal override Size GetPreferredSizeCore(Size proposedConstraints)
    {
        Size size;

        _flagState[s_queryBaseText] = true;
        try
        {
            size = base.GetPreferredSizeCore(proposedConstraints);
        }
        finally
        {
            _flagState[s_queryBaseText] = false;
        }

        return size;
    }

    /// <summary>
    ///  The selected text in the control according to the CutCopyMaskFormat properties (IncludePrompt/IncludeLiterals).
    ///  This is used in Cut/Copy operations (SelectedText).
    ///  The prompt character is always replaced with a blank character.
    /// </summary>
    private string GetSelectedText()
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");

        GetSelectionStartAndLength(out int selStart, out int selLength);

        if (selLength == 0)
        {
            return string.Empty;
        }

        bool includePrompt = (CutCopyMaskFormat & MaskFormat.IncludePrompt) != 0;
        bool includeLiterals = (CutCopyMaskFormat & MaskFormat.IncludeLiterals) != 0;

        return _maskedTextProvider.ToString(ignorePasswordChar: true, includePrompt, includeLiterals, selStart, selLength);
    }

    protected override unsafe void OnBackColorChanged(EventArgs e)
    {
        base.OnBackColorChanged(e);

        // Force repainting of the entire window frame.
        if (Application.RenderWithVisualStyles && IsHandleCreated && BorderStyle == BorderStyle.Fixed3D)
        {
            PInvoke.RedrawWindow(this, lprcUpdate: null, HRGN.Null, REDRAW_WINDOW_FLAGS.RDW_INVALIDATE | REDRAW_WINDOW_FLAGS.RDW_FRAME);
        }
    }

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

        if (IsAccessibilityObjectCreated)
        {
            AccessibilityObject.SetFocus();
        }
    }

    /// <summary>
    ///  Overridden to update the newly created handle with the settings of the PasswordChar properties
    ///  if no mask has been set.
    /// </summary>
    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        SetSelectionOnHandle();

        if (_flagState[s_isNullMask] && _maskedTextProvider.IsPassword)
        {
            SetEditControlPasswordChar(_maskedTextProvider.PasswordChar);
        }
    }

    /// <summary>
    ///  Raises the IsOverwriteModeChanged event.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    protected virtual void OnIsOverwriteModeChanged(EventArgs e)
    {
        if (Events[s_isOverwriteModeChangedEvent] is EventHandler eh)
        {
            eh(this, e);
        }
    }

    /// <summary>
    ///  Raises the <see cref="Control.KeyDown"/> event.
    /// </summary>
    protected override void OnKeyDown(KeyEventArgs e)
    {
        base.OnKeyDown(e);

        if (_flagState[s_isNullMask])
        {
            // Operates as a regular text box base.
            return;
        }

        Keys keyCode = e.KeyCode;

        // Special-case Return & Esc since they generate invalid characters we should not process OnKeyPress.
        if (keyCode is Keys.Return or Keys.Escape)
        {
            _flagState[s_handleKeyPress] = false;
        }

        // Insert is toggled when not modified with some other key (ctrl, shift...). Note that shift-Insert is
        // same as paste.
        if (keyCode == Keys.Insert && e.Modifiers == Keys.None && _insertMode == InsertKeyMode.Default)
        {
            _flagState[s_insertToggled] = !_flagState[s_insertToggled];
            OnIsOverwriteModeChanged(EventArgs.Empty);
            return;
        }

        if (e.Control && char.IsLetter((char)keyCode))
        {
            switch (keyCode)
            {
                // Unsupported keys should not be handled to allow generating the corresponding message
                // which is handled in the WndProc.
                // case Keys.Z:  // ctrl-z == Undo.
                // case Keys.Y:  // ctrl-y == Redo.
                //    e.Handled = true;
                //    return;

                // Note: Ctrl-Insert (Copy -Shortcut.CtrlIns) and Shift-Insert (Paste - Shortcut.ShiftIns) are
                // handled by the base class and behavior depend on ShortcutsEnabled property.

                // Special cases: usually cases where the native edit control would modify the mask.
                case Keys.H:  // ctrl-h == Backspace == '\b'
                    keyCode = Keys.Back; // handle it below.
                    break;

                default:
                    // Next OnKeyPress should not be handled to allow Ctrl-<x/c/v/a> to be processed in the
                    // base class so corresponding messages can be generated (WM_CUT/WM_COPY/WM_PASTE).
                    // Combined characters don't generate OnKeyDown by themselves but they generate OnKeyPress.
                    _flagState[s_handleKeyPress] = false;
                    return;
            }
        }

        if (keyCode is Keys.Delete or Keys.Back) // Deletion keys.
        {
            if (!ReadOnly)
            {
                GetSelectionStartAndLength(out int startPosition, out int selectionLen);

                switch (e.Modifiers)
                {
                    case Keys.Shift:
                        if (keyCode == Keys.Delete)
                        {
                            keyCode = Keys.Back;
                        }

                        goto default;

                    case Keys.Control:
                        if (selectionLen == 0) // In other case, the selected text should be deleted.
                        {
                            if (keyCode == Keys.Delete) // delete to the end of the string.
                            {
                                selectionLen = _maskedTextProvider.Length - startPosition;
                            }
                            else // ( keyCode == Keys.Back ) // delete to the beginning of the string.
                            {
                                selectionLen = startPosition == _maskedTextProvider.Length /*at end of text*/ ? startPosition : startPosition + 1;
                                startPosition = 0;
                            }
                        }

                        goto default;

                    default:
                        if (!_flagState[s_handleKeyPress])
                        {
                            _flagState[s_handleKeyPress] = true;
                        }

                        break;
                }

                //
                // Handle special case when using Korean IME and ending a composition.
                //
                /*  This code is no longer needed after fixing

*/

                Delete(keyCode, startPosition, selectionLen);
                e.SuppressKeyPress = true;
            }
        }
    }

    /// <summary>
    ///  Raises the <see cref="Control.KeyPress"/> event.
    /// </summary>
    protected override void OnKeyPress(KeyPressEventArgs e)
    {
        base.OnKeyPress(e);

        if (_flagState[s_isNullMask])
        {
            // Operates as a regular text box base.
            return;
        }

        // This key may be a combined key involving a letter, like Ctrl-A; let the native control handle it.
        if (!_flagState[s_handleKeyPress])
        {
            _flagState[s_handleKeyPress] = true;

            // When the combined key involves a letter, the final character is not a letter. There are some
            // Ctrl combined keys that generate a letter and can be confusing; we do not mean to pass those
            // characters to the underlying Edit control. These combinations are: Ctrl-F<#> and Ctrl-Atl-<someKey>
            if (!char.IsLetter(e.KeyChar))
            {
                return;
            }
        }

        if (!ReadOnly)
        {
            // At this point the character needs to be processed ...

            GetSelectionStartAndLength(out int selectionStart, out int selectionLen);

            string oldText = TextOutput;
            if (PlaceChar(e.KeyChar, selectionStart, selectionLen, IsOverwriteMode, out MaskedTextResultHint hint))
            {
                if (TextOutput != oldText)
                {
                    SetText(); // Now set the text in the display.
                }

                SelectionStart = ++_caretTestPos; // caretTestPos is updated in PlaceChar.

                if (ImeModeConversion.InputLanguageTable == ImeModeConversion.KoreanTable)
                {
                    // Korean IMEs complete composition when a character has been fully converted, so the composition string
                    // is only one-character long; once composed we block the IME if there isn't more room in the test string.

                    int editPos = _maskedTextProvider.FindUnassignedEditPositionFrom(_caretTestPos, Forward);
                    if (editPos == MaskedTextProvider.InvalidIndex)
                    {
                        ImeComplete();  // Force completion of composition.
                    }
                }
            }
            else
            {
                OnMaskInputRejected(new MaskInputRejectedEventArgs(_caretTestPos, hint)); // caretTestPos is updated in PlaceChar.
            }

            if (selectionLen > 0)
            {
                SelectionLength = 0;
            }

            e.Handled = true;
        }
    }

    /// <summary>
    ///  Raises the <see cref="Control.KeyUp"/> event.
    /// </summary>
    protected override void OnKeyUp(KeyEventArgs e)
    {
        base.OnKeyUp(e);

        // KeyUp is the last message to be processed so it is the best place to reset these flags.

        if (_flagState[s_imeCompleting])
        {
            _flagState[s_imeCompleting] = false;
        }

        if (_flagState[s_imeEndingComposition])
        {
            _flagState[s_imeEndingComposition] = false;
        }

        if (IsHandleCreated && IsAccessibilityObjectCreated && ContainsNavigationKeyCode(e.KeyCode))
        {
            AccessibilityObject?.RaiseAutomationEvent(UIA_EVENT_ID.UIA_Text_TextSelectionChangedEventId);
        }
    }

    /// <summary>
    ///  Raises the MaskChanged event.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    protected virtual void OnMaskChanged(EventArgs e)
    {
        if (Events[s_maskChangedEvent] is EventHandler eh)
        {
            eh(this, e);
        }
    }

    /// <summary>
    ///  Raises the MaskInputRejected event.
    /// </summary>
    private void OnMaskInputRejected(MaskInputRejectedEventArgs e)
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");

        if (BeepOnError)
        {
            Media.SoundPlayer sp = new();
            sp.Play();
        }

        if (Events[s_maskInputRejectedEvent] is MaskInputRejectedEventHandler eh)
        {
            eh(this, e);
        }
    }

    protected override void OnMouseDown(MouseEventArgs e)
    {
        base.OnMouseDown(e);

        if (IsHandleCreated && IsAccessibilityObjectCreated)
        {
            // As there is no corresponding windows notification
            // about text selection changed for TextBox assuming
            // that any mouse down on textbox leads to change of
            // the caret position and thereby change the selection.
            AccessibilityObject.RaiseAutomationEvent(UIA_EVENT_ID.UIA_Text_TextSelectionChangedEventId);
        }
    }

    /// <summary>
    ///  Unsupported method/property.
    ///  virtual method.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    protected override void OnMultilineChanged(EventArgs e)
    {
    }

    /// <summary>
    ///  Raises the TextAlignChanged event.
    /// </summary>
    protected virtual void OnTextAlignChanged(EventArgs e)
    {
        if (Events[s_textAlignChangedEvent] is EventHandler eh)
        {
            eh(this, e);
        }
    }

    /// <summary>
    ///  Raises the TypeValidationCompleted event.
    /// </summary>
    private void OnTypeValidationCompleted(TypeValidationEventArgs e)
    {
        if (Events[s_validationCompletedEvent] is TypeValidationEventHandler eh)
        {
            eh(this, e);
        }
    }

    /// <summary>
    ///  Raises the  System.Windows.Forms.Control.Validating event.
    ///  Overridden here to be able to control the order validating events are
    ///  raised [TypeValidationCompleted - Validating - Validated - Leave - KillFocus]
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    protected override void OnValidating(CancelEventArgs e)
    {
        // Note: It seems impractical to perform type validation here if the control is read only but we need
        // to be consistent with other TextBoxBase controls which don't check for RO; and we don't want
        // to fix them to avoid introducing breaking changes.
        PerformTypeValidation(e);
        base.OnValidating(e);
    }

    /// <summary>
    ///  Raises the TextChanged event and related Input/Output text events when mask is null.
    ///  Overridden here to be able to control order of text changed events.
    /// </summary>
    protected override void OnTextChanged(EventArgs e)
    {
        // A text changed event handler will most likely query for the Text value, we need to return the
        // formatted one.
        bool queryBaseText = _flagState[s_queryBaseText];
        _flagState[s_queryBaseText] = false;
        try
        {
            base.OnTextChanged(e);
        }
        finally
        {
            _flagState[s_queryBaseText] = queryBaseText;
        }
    }

    /// <summary>
    ///  Replaces the current selection in the text box specified by the startPosition and selectionLen parameters
    ///  with the contents of the supplied string.
    /// </summary>
    private void Replace(string text, int startPosition, int selectionLen)
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");
        Debug.Assert(text is not null, "text is null.");

        // Clone the MaskedTextProvider so text properties are not modified until the paste operation is
        // completed. This is needed in case one of these properties is retrieved in a MaskedInputRejected
        // event handler (clipboard text is attempted to be set into the input text char by char).

        MaskedTextProvider clonedProvider = (MaskedTextProvider)_maskedTextProvider.Clone();

        // Cache the current caret position so we restore it in case the text does not change.
        int currentCaretPos = _caretTestPos;

        // First replace characters in the selection (if any and if any edit positions) until completed, or the test position falls
        // outside the selection range, or there's no more room in the test string for editable characters.
        // Then insert any remaining characters from the input.

        MaskedTextResultHint hint = MaskedTextResultHint.NoEffect;
        int endPos = startPosition + selectionLen - 1;

        if (RejectInputOnFirstFailure)
        {
            bool succeeded;

            succeeded = (startPosition > endPos) ?
                clonedProvider.InsertAt(text, startPosition, out _caretTestPos, out hint) :
                clonedProvider.Replace(text, startPosition, endPos, out _caretTestPos, out hint);

            if (!succeeded)
            {
                OnMaskInputRejected(new MaskInputRejectedEventArgs(_caretTestPos, hint));
            }
        }
        else
        {
            int testPos;

            foreach (char ch in text)
            {
                if (!_maskedTextProvider.VerifyEscapeChar(ch, startPosition))  // char won't be escaped, find and edit position for it.
                {
                    // Observe that we look for a position w/o respecting the selection length, because the input text could be larger than
                    // the number of edit positions in the selection.
                    testPos = clonedProvider.FindEditPositionFrom(startPosition, Forward);

                    if (testPos == MaskedTextProvider.InvalidIndex)
                    {
                        // this will continue to execute (fail) until the end of the text so we fire the event for each remaining char.
                        OnMaskInputRejected(new MaskInputRejectedEventArgs(startPosition, MaskedTextResultHint.UnavailableEditPosition));
                        continue;
                    }

                    startPosition = testPos;
                }

                int length = endPos >= startPosition ? 1 : 0;

                // if length > 0 we are (re)placing the input char in the current startPosition, otherwise we are inserting the input.
                bool replace = length > 0;

                if (PlaceChar(clonedProvider, ch, startPosition, length, replace, out MaskedTextResultHint tempHint))
                {
                    // caretTestPos is updated in PlaceChar call.
                    startPosition = _caretTestPos + 1;

                    // place char will insert or replace a single character so the hint must be success, and that will be the final operation
                    // result hint.
                    if (tempHint == MaskedTextResultHint.Success && hint != tempHint)
                    {
                        hint = tempHint;
                    }
                }
                else
                {
                    OnMaskInputRejected(new MaskInputRejectedEventArgs(startPosition, tempHint));
                }
            }

            if (selectionLen > 0)
            {
                // At this point we have processed all characters from the input text (if any) but still need to
                // remove remaining characters from the selected text (if editable and valid chars).

                if (startPosition <= endPos
                    && !clonedProvider.RemoveAt(startPosition, endPos, out _caretTestPos, out MaskedTextResultHint tempHint))
                {
                    OnMaskInputRejected(new MaskInputRejectedEventArgs(_caretTestPos, tempHint));
                }
            }
        }

        bool updateText = TextOutput != clonedProvider.ToString();

        // Always set the mtp, the formatted text could be the same but the assigned positions may be different.
        _maskedTextProvider = clonedProvider;

        // Update text if needed.
        if (updateText)
        {
            SetText();

            // Update caret position.
            _caretTestPos = startPosition;
            base.SelectInternal(_caretTestPos, 0, _maskedTextProvider.Length);
        }
        else
        {
            _caretTestPos = currentCaretPos;
        }

        return;
    }

    /// <summary>
    ///  Pastes specified text over the currently selected text (if any) shifting upper characters if
    ///  input is longer than selected text, and/or removing remaining characters from the selection if
    ///  input contains less characters.
    /// </summary>
    private void PasteInt(string? text)
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");

        GetSelectionStartAndLength(out int selStart, out int selLength);

        if (string.IsNullOrEmpty(text))
        {
            Delete(Keys.Delete, selStart, selLength);
        }
        else
        {
            Replace(text, selStart, selLength);
        }
    }

    /// <summary>
    ///  Performs validation of the input string using the provided ValidatingType object (if any).
    ///  Returns an object created from the formatted text.
    ///  If the CancelEventArgs param is not null, it is assumed the control is leaving focus and
    ///  the validation event chain is being executed (TypeValidationCompleted - Validating - Validated...);
    ///  the value of the CancelEventArgs.Cancel property is the same as the TypeValidationEventArgs.Cancel
    ///  on output (Cancel provides proper handling of focus shifting at the Control class level).
    ///  Note: The text being validated does not include prompt chars.
    /// </summary>
    private object? PerformTypeValidation(CancelEventArgs? e)
    {
        object? parseRetVal = null;

        if (_validatingType is not null)
        {
            string? message = null;

            if (!_flagState[s_isNullMask] && !_maskedTextProvider.MaskCompleted)
            {
                message = SR.MaskedTextBoxIncompleteMsg;
            }
            else
            {
                string textValue;

                if (!_flagState[s_isNullMask]) // replace prompt with space.
                {
                    textValue = _maskedTextProvider.ToString(/*includePrompt*/ false, IncludeLiterals);
                }
                else
                {
                    textValue = base.Text;
                }

                try
                {
                    parseRetVal = Formatter.ParseObject(
                        textValue,
                        _validatingType,
                        typeof(string),
                        targetConverter: null,
                        sourceConverter: null,
                        _formatProvider,
                        formattedNullValue: null,
                        Formatter.GetDefaultDataSourceNullValue(_validatingType));
                }
                catch (Exception exception) when (!exception.IsCriticalException())
                {
                    if (exception.InnerException is not null)
                    {
                        // Outer exception is a generic TargetInvocationException.
                        exception = exception.InnerException;
                    }

                    message = $"{exception.GetType()}: {exception.Message}";
                }
            }

            bool isValidInput = false;
            if (message is null)
            {
                isValidInput = true;
                message = SR.MaskedTextBoxTypeValidationSucceeded;
            }

            TypeValidationEventArgs tve = new(_validatingType, isValidInput, parseRetVal, message);
            OnTypeValidationCompleted(tve);

            if (e is not null)
            {
                e.Cancel = tve.Cancel;
            }
        }

        return parseRetVal;
    }

    /// <summary>
    ///  Insert or replaces the specified character into the control's text and updates the caret position.
    ///  If overwrite is true, it replaces the character at the selection start position.
    /// </summary>
    private bool PlaceChar(char ch, int startPosition, int length, bool overwrite,
        out MaskedTextResultHint hint)
    {
        return PlaceChar(_maskedTextProvider, ch, startPosition, length, overwrite, out hint);
    }

    /// <summary>
    ///  Override version to be able to perform the operation on a cloned provider.
    /// </summary>
    private bool PlaceChar(MaskedTextProvider provider, char ch, int startPosition, int length, bool overwrite,
        out MaskedTextResultHint hint)
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");

        _caretTestPos = startPosition;

        if (startPosition < _maskedTextProvider.Length)
        {
            if (length > 0)  // Replacing selection with input char.
            {
                int endPos = startPosition + length - 1;
                return provider.Replace(ch, startPosition, endPos, out _caretTestPos, out hint);
            }
            else
            {
                if (overwrite)
                {
                    // overwrite character at next edit position from startPosition (inclusive).
                    return provider.Replace(ch, startPosition, out _caretTestPos, out hint);
                }
                else // insert.
                {
                    return provider.InsertAt(ch, startPosition, out _caretTestPos, out hint);
                }
            }
        }

        hint = MaskedTextResultHint.UnavailableEditPosition;
        return false;
    }

    /// <summary>
    ///  Implements the handling of Ctrl+A (select all). Note: Code copied from TextBox.
    /// </summary>
    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
        //
        // The base class should be called first because it implements ShortcutsEnabled,
        // which takes precedence over Ctrl+A
        //
        bool msgProcessed = base.ProcessCmdKey(ref msg, keyData);

        if (!msgProcessed)
        {
            if ((int)keyData == (int)Shortcut.CtrlA)
            {
                SelectAll();
                msgProcessed = true; // This prevents generating a WM_CHAR for 'A'.
            }
        }

        return msgProcessed;
    }

    /// <summary>
    ///  We need to override this method so we can handle input language changes properly. Control
    ///  doesn't handle the WM_CHAR messages generated after WM_IME_CHAR messages, it passes them
    ///  to DefWndProc (the characters would be displayed in the text box always).
    /// </summary>
    protected internal override bool ProcessKeyMessage(ref Message m)
    {
        // call base's method so the WM_CHAR and other messages are processed; this gives Control the
        // chance to flush all pending WM_CHAR processing after WM_IME_CHAR messages are generated.

        bool msgProcessed = base.ProcessKeyMessage(ref m);

        if (_flagState[s_isNullMask])
        {
            return msgProcessed; // Operates as a regular text box base.
        }

        // If this WM_CHAR message is sent after WM_IME_CHAR, we ignore it since we already processed
        // the corresponding WM_IME_CHAR message.
        if (m.Msg == PInvokeCore.WM_CHAR && ImeWmCharsToIgnore > 0)
        {
            return true;    // meaning, we handled the message so it is not passed to the default WndProc.
        }

        return msgProcessed;
    }

    /// <summary>
    ///  Design time support for resetting Culture property..
    /// </summary>
    private void ResetCulture()
    {
        Culture = CultureInfo.CurrentCulture;
    }

    /// <summary>
    ///  Unsupported method/property.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new void ScrollToCaret()
    {
    }

    /// <summary>
    ///  Sets the underlying MaskedTextProvider object. Used when the control is initialized
    ///  and one of its properties, backed up by the MaskedTextProvider, changes; this requires
    ///  recreating the provider because it is immutable.
    /// </summary>
    private void SetMaskedTextProvider(MaskedTextProvider newProvider)
    {
        SetMaskedTextProvider(newProvider, null);
    }

    /// <summary>
    ///  Overload to allow for passing the text when the mask is being changed from null,
    ///  in this case the maskedTextProvider holds backend info only (not the text).
    /// </summary>
    private void SetMaskedTextProvider(MaskedTextProvider newProvider, string? textOnInitializingMask)
    {
        Debug.Assert(newProvider is not null, "Initializing from a null MaskProvider ref.");

        // Set R/W properties.
        newProvider.IncludePrompt = _maskedTextProvider.IncludePrompt;
        newProvider.IncludeLiterals = _maskedTextProvider.IncludeLiterals;
        newProvider.SkipLiterals = _maskedTextProvider.SkipLiterals;
        newProvider.ResetOnPrompt = _maskedTextProvider.ResetOnPrompt;
        newProvider.ResetOnSpace = _maskedTextProvider.ResetOnSpace;

        // If mask not initialized and not initializing it, the new provider is just a property backend.
        // Change won't have any effect in text.
        if (_flagState[s_isNullMask] && textOnInitializingMask is null)
        {
            _maskedTextProvider = newProvider;
            return;
        }

        int testPos = 0;
        bool raiseOnMaskInputRejected;
        MaskedTextResultHint hint = MaskedTextResultHint.NoEffect;
        MaskedTextProvider oldProvider = _maskedTextProvider;

        // Attempt to add previous text.
        // If the mask is the same, we need to preserve the caret and character positions if the text is added successfully.
        bool preserveCharPos = oldProvider.Mask == newProvider.Mask;

        // Cache text output text before setting the new provider to determine whether we need to raise the TextChanged event.
        string oldText;

        // NOTE: Whenever changing the MTP, the text is lost if any character in the old text violates the new provider's mask.

        if (textOnInitializingMask is not null) // Changing Mask (from null), which is the only RO property that requires passing text.
        {
            oldText = textOnInitializingMask;
            raiseOnMaskInputRejected = !newProvider.Set(textOnInitializingMask, out testPos, out hint);
        }
        else
        {
            oldText = TextOutput;

            // We need to attempt to set the input characters one by one in the edit positions so they are not
            // escaped.
            int assignedCount = oldProvider.AssignedEditPositionCount;
            int srcPos = 0;
            int dstPos = 0;

            while (assignedCount > 0)
            {
                srcPos = oldProvider.FindAssignedEditPositionFrom(srcPos, Forward);
                Debug.Assert(srcPos != MaskedTextProvider.InvalidIndex, "InvalidIndex unexpected at this time.");

                if (preserveCharPos)
                {
                    dstPos = srcPos;
                }
                else
                {
                    dstPos = newProvider.FindEditPositionFrom(dstPos, Forward);

                    if (dstPos == MaskedTextProvider.InvalidIndex)
                    {
                        newProvider.Clear();

                        testPos = newProvider.Length;
                        hint = MaskedTextResultHint.UnavailableEditPosition;
                        break;
                    }
                }

                if (!newProvider.Replace(oldProvider[srcPos], dstPos, out testPos, out hint))
                {
                    preserveCharPos = false;
                    newProvider.Clear();
                    break;
                }

                srcPos++;
                dstPos++;
                assignedCount--;
            }

            raiseOnMaskInputRejected = !MaskedTextProvider.GetOperationResultFromHint(hint);
        }

        // Set provider.
        _maskedTextProvider = newProvider;

        if (_flagState[s_isNullMask])
        {
            _flagState[s_isNullMask] = false;
        }

        // Raising events need to be done only after the new provider has been set so the MTB is in a state where properties
        // can be queried from event handlers safely.
        if (raiseOnMaskInputRejected)
        {
            OnMaskInputRejected(new MaskInputRejectedEventArgs(testPos, hint));
        }

        if (newProvider.IsPassword)
        {
            // Reset native edit control so the MaskedTextBox will take control over the characters that
            // need to be replaced with the password char (the input text characters).
            // MTB takes over.
            SetEditControlPasswordChar('\0');
        }

        EventArgs e = EventArgs.Empty;

        if (textOnInitializingMask is not null /*changing mask from null*/ || oldProvider.Mask != newProvider.Mask)
        {
            OnMaskChanged(e);
        }

        SetWindowText(GetFormattedDisplayString(), oldText != TextOutput, preserveCharPos);
    }

    /// <summary>
    ///  Sets the control's text to the formatted text obtained from the underlying MaskedTextProvider.
    ///  TextChanged is raised always, this assumes the display or the output text changed.
    ///  The caret position is lost (unless cached somewhere else like when losing the focus).
    ///  This is the common way of changing the text in the control.
    /// </summary>
    private void SetText()
    {
        SetWindowText(GetFormattedDisplayString(), true, false);
    }

    /// <summary>
    ///  Sets the control's text to the formatted text obtained from the underlying MaskedTextProvider.
    ///  TextChanged is not raised. [PasswordChar]
    ///  The caret position is preserved.
    /// </summary>
    private void SetWindowText()
    {
        SetWindowText(GetFormattedDisplayString(), false, true);
    }

    /// <summary>
    ///  Sets the text directly in the underlying edit control to the value specified.
    ///  The 'raiseTextChangedEvent' param determines whether TextChanged event is raised or not.
    ///  The 'preserveCaret' param determines whether an attempt to preserve the caret position should be made or not
    ///  after the call to SetWindowText (WindowText) is performed.
    /// </summary>
    private void SetWindowText(string text, bool raiseTextChangedEvent, bool preserveCaret)
    {
        _flagState[s_queryBaseText] = true;

        try
        {
            if (preserveCaret)
            {
                _caretTestPos = SelectionStart;
            }

            WindowText = text;  // this calls Win32::SetWindowText directly, no OnTextChanged raised.

            if (raiseTextChangedEvent)
            {
                OnTextChanged(EventArgs.Empty);
            }

            if (preserveCaret)
            {
                SelectionStart = _caretTestPos;
            }
        }
        finally
        {
            _flagState[s_queryBaseText] = false;
        }
    }

    /// <summary>
    ///  Design time support for checking if Culture value in the designer should be serialized.
    /// </summary>
    private bool ShouldSerializeCulture()
    {
        return !CultureInfo.CurrentCulture.Equals(Culture);
    }

    /// <summary>
    ///  Undoes the last edit operation in the text box.
    ///  Unsupported property/method.
    ///  WndProc ignores EM_UNDO.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public new void Undo()
    {
    }

    /// <summary>
    ///  Forces type validation. Returns the validated text value.
    /// </summary>
    public object? ValidateText()
    {
        return PerformTypeValidation(null);
    }

    /// <summary>
    ///  Deletes all input characters in the current selection.
    /// </summary>
    private bool WmClear()
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");

        if (!ReadOnly)
        {
            GetSelectionStartAndLength(out int selStart, out int selLength);
            Delete(Keys.Delete, selStart, selLength);
            return true;
        }

        return false;
    }

    /// <summary>
    ///  Copies current selection text to the clipboard, formatted according to the IncludeLiterals properties but
    ///  ignoring the prompt character.
    ///  Returns true if the operation succeeded, false otherwise.
    /// </summary>
    private bool WmCopy()
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");

        if (_maskedTextProvider.IsPassword) // cannot copy password to clipboard.
        {
            return false;
        }

        string text = GetSelectedText();

        try
        {
            if (text.Length == 0)
            {
                Clipboard.Clear();
            }
            else
            {
                Clipboard.SetText(text);
            }
        }
        catch (Exception ex) when (!ex.IsCriticalException())
        {
            // Note: Sometimes the above operation throws but it successfully sets the
            // data in the clipboard. This usually happens when the Application's Main
            // is not attributed with [STAThread].
        }

        return true;
    }

    /// <summary>
    ///  Processes the WM_IME_COMPOSITION message when using Korean IME.
    ///  Korean IME uses the control's caret as the composition string (it processes only one character at a time),
    ///  we need to have special message handling for it.
    ///  Returns true if the message is handled, false otherwise.
    /// </summary>
    private bool WmImeComposition(ref Message m)
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");

#if DEBUG
        if (ReadOnly || _maskedTextProvider.IsPassword)
        {
            // This should have been already handled by the ReadOnly, PasswordChar and ImeMode properties.
            Debug.Assert(ImeMode == ImeMode.Disable, "IME enabled when in RO or Pwd mode.");
        }
#endif
        // Non-Korean IMEs complete composition when all characters in the string has been composed (when user hits enter);
        // Currently, we don't support checking the composition string characters because it would require similar logic
        // as the MaskedTextBox itself.

        if (ImeModeConversion.InputLanguageTable == ImeModeConversion.KoreanTable)
        {
            byte imeConversionType = ImeConversionNone;

            // Check if there's an update to the composition string:
            if ((m.LParamInternal & (int)IME_COMPOSITION_STRING.GCS_COMPSTR) != 0)
            {
                // The character in the composition has been updated but not yet converted.
                imeConversionType = ImeConversionUpdate;
            }
            else if ((m.LParamInternal & (int)IME_COMPOSITION_STRING.GCS_RESULTSTR) != 0)
            {
                // The character(s) in the composition has been fully converted.
                imeConversionType = ImeConversionCompleted;
            }

            // Process any update in the composition string.
            if (imeConversionType != ImeConversionNone)
            {
                if (_flagState[s_imeEndingComposition])
                {
                    // If IME is completing the conversion, we don't want to process further characters.
                    return _flagState[s_imeCompleting];
                }
            }
        }

        return false; // message not handled.
    }

    /// <summary>
    ///  Processes the WM_IME_STARTCOMPOSITION message.
    ///  Returns true if the message is handled, false otherwise.
    /// </summary>
    private bool WmImeStartComposition()
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");

        // Position the composition window in a valid place.

        GetSelectionStartAndLength(out int startPosition, out int selectionLen);

        int startEditPos = _maskedTextProvider.FindEditPositionFrom(startPosition, Forward);

        if (startEditPos != MaskedTextProvider.InvalidIndex)
        {
            if (selectionLen > 0 && (ImeModeConversion.InputLanguageTable == ImeModeConversion.KoreanTable))
            {
                // Korean IME: We need to delete the selected text and reposition the caret so the IME processes one
                // character only, otherwise it would overwrite the selection with the caret (composition string),
                // deleting a portion of the mask.

                int endEditPos = _maskedTextProvider.FindEditPositionFrom(startPosition + selectionLen - 1, Backward);

                if (endEditPos >= startEditPos)
                {
                    selectionLen = endEditPos - startEditPos + 1;
                    Delete(Keys.Delete, startEditPos, selectionLen);
                }
                else
                {
                    ImeComplete();
                    OnMaskInputRejected(new MaskInputRejectedEventArgs(startPosition, MaskedTextResultHint.UnavailableEditPosition));
                    return true;
                }
            }

            // update caret position.
            if (startPosition != startEditPos)
            {
                _caretTestPos = startEditPos;
                SelectionStart = _caretTestPos;
            }

            SelectionLength = 0;
        }
        else
        {
            ImeComplete();
            OnMaskInputRejected(new MaskInputRejectedEventArgs(startPosition, MaskedTextResultHint.UnavailableEditPosition));
            return true;
        }

        return false;
    }

    /// <summary>
    ///  Processes the WM_PASTE message. Copies the text from the clipboard, if is valid,
    ///  formatted according to the mask applied to this control.
    ///  Returns true if the operation succeeded, false otherwise.
    /// </summary>
    private void WmPaste()
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");

        if (ReadOnly)
        {
            return;
        }

        // Get the text from the clipboard.

        string text;

        try
        {
            text = Clipboard.GetText();
        }
        catch (Exception ex) when (!ex.IsCriticalException())
        {
            Debug.Fail(ex.ToString());
            return;
        }

        PasteInt(text);
    }

    private void WmPrint(ref Message m)
    {
        base.WndProc(ref m);
        if (((nint)m.LParamInternal & PInvoke.PRF_NONCLIENT) != 0
            && Application.RenderWithVisualStyles && BorderStyle == BorderStyle.Fixed3D)
        {
            using Graphics g = Graphics.FromHdc((HDC)m.WParamInternal);
            Rectangle rect = new(0, 0, Size.Width - 1, Size.Height - 1);
            using var pen = VisualStyleInformation.TextControlBorder.GetCachedPenScope();
            g.DrawRectangle(pen, rect);
            rect.Inflate(-1, -1);
            g.DrawRectangle(SystemPens.Window, rect);
        }
    }

    /// <summary>
    ///  We need to override the WndProc method to have full control over what characters can be
    ///  displayed in the text box; particularly, we have special handling when IME is turned on.
    /// </summary>
    protected override void WndProc(ref Message m)
    {
        // Handle messages for special cases (unsupported operations or cases where mask doesn not matter).
        switch (m.MsgInternal)
        {
            case PInvokeCore.WM_PRINT:
                WmPrint(ref m);
                return;
            case PInvokeCore.WM_CONTEXTMENU:
            case (int)PInvokeCore.EM_CANUNDO:
                base.ClearUndo(); // resets undo buffer.
                base.WndProc(ref m);
                return;

            case (int)PInvokeCore.EM_SCROLLCARET:  // No scroll for single-line control.
            case (int)PInvokeCore.EM_LIMITTEXT:    // Max/Min text is defined by the mask.
            case (int)PInvokeCore.EM_UNDO:
            case PInvokeCore.WM_UNDO:
                return;

            default:
                break;  // continue.
        }

        if (_flagState[s_isNullMask])
        {
            base.WndProc(ref m); // Operates as a regular text box base.
            return;
        }

        switch (m.MsgInternal)
        {
            case PInvokeCore.WM_IME_STARTCOMPOSITION:
                if (WmImeStartComposition())
                {
                    break;
                }

                goto default;

            case PInvokeCore.WM_IME_ENDCOMPOSITION:
                _flagState[s_imeEndingComposition] = true;
                goto default;

            case PInvokeCore.WM_IME_COMPOSITION:
                if (WmImeComposition(ref m))
                {
                    break;
                }

                goto default;

            case PInvokeCore.WM_CUT:
                if (!ReadOnly && WmCopy())
                {
                    WmClear();
                }

                break;

            case PInvokeCore.WM_COPY:
                WmCopy();
                break;

            case PInvokeCore.WM_PASTE:
                WmPaste();
                break;

            case PInvokeCore.WM_CLEAR:
                WmClear();
                break;

            case PInvokeCore.WM_KILLFOCUS:
                base.WndProc(ref m);
                WmKillFocus();
                break;

            case PInvokeCore.WM_SETFOCUS:
                WmSetFocus();
                base.WndProc(ref m);
                break;

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

    /// <summary>
    ///  Processes the WM_KILLFOCUS message. Updates control's text replacing prompt chars with space.
    /// </summary>
    private void WmKillFocus()
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");

        GetSelectionStartAndLength(out _caretTestPos, out _lastSelLength);

        if (HidePromptOnLeave && !MaskFull)
        {
            SetWindowText(); // Update text w/ no prompt.

            // We need to update selection info in case the control is queried for it while it doesn't have the focus.
            base.SelectInternal(_caretTestPos, _lastSelLength, _maskedTextProvider.Length);
        }
    }

    /// <summary>
    ///  Processes the WM_SETFOCUS message. Updates control's text with formatted text according to
    ///  the include prompt property.
    /// </summary>
    private void WmSetFocus()
    {
        Debug.Assert(!_flagState[s_isNullMask], "This method must be called when a Mask is provided.");

        if (HidePromptOnLeave && !MaskFull) // Prompt will show up.
        {
            SetWindowText();
        }

        // Restore previous selection. Do this always (as opposed to within the condition above as in WmKillFocus)
        // because HidePromptOnLeave could have changed while the control did not have the focus.
        base.SelectInternal(_caretTestPos, _lastSelLength, _maskedTextProvider.Length);
    }
}
