using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using Unity.CodeEditor.Document;
using Unity.CodeEditor.Indentation;
using Unity.CodeEditor.Platform;
using Unity.CodeEditor.Rendering;
using Unity.CodeEditor.Utils;
using Unity.PlasticSCM.Editor.UI;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

namespace Unity.CodeEditor.Editing
{
    internal class TextArea : VisualElement
    {
        private bool _isFocused;
        private TextEditorOptions _options;
        private TextDocument _document;
        private IReadOnlySectionProvider _readOnlySectionProvider = NoReadOnlySections.Instance;
        private bool _rigthClickMovesCaret;
        private SelectionMouseHandler _selectionMouseHandler;
        private EditingCommandHandler _editingCommandHandler;
        private CaretNavigationCommandHandler _caretNavigationCommandHandler;
        private VisualElement _leftMarginsContainer;

        internal bool IsFocused => _isFocused;

        #region Caret handling on document changes

        private void OnDocumentChanging(object sender, DocumentChangeEventArgs e)
        {
            Caret.OnDocumentChanging();
        }

        private void OnDocumentChanged(object sender, DocumentChangeEventArgs e)
        {
            Caret.OnDocumentChanged(e);
            Selection = _selection.UpdateOnDocumentChange(e);
        }

        private void OnUpdateStarted(object sender, EventArgs e)
        {
            Document.UndoStack.PushOptional(new RestoreCaretAndSelectionUndoAction(this));
        }

        private void OnUpdateFinished(object sender, EventArgs e)
        {
            Caret.OnDocumentUpdateFinished();
        }

        private sealed class RestoreCaretAndSelectionUndoAction : IUndoableOperation
        {
            // keep textarea in weak reference because the IUndoableOperation is stored with the document
            private readonly WeakReference _textAreaReference;

            private readonly TextViewPosition _caretPosition;
            private readonly Selection _selection;

            internal RestoreCaretAndSelectionUndoAction(TextArea textArea)
            {
                _textAreaReference = new WeakReference(textArea);
                // Just save the old caret position, no need to validate here.
                // If we restore it, we'll validate it anyways.
                _caretPosition = textArea.Caret.NonValidatedPosition;
                _selection = textArea.Selection;
            }

            public void Undo()
            {
                var textArea = (TextArea)_textAreaReference.Target;
                if (textArea != null)
                {
                    textArea.Caret.Position = _caretPosition;
                    textArea.Selection = _selection;
                }
            }

            public void Redo()
            {
                // redo=undo: we just restore the caret/selection state
                Undo();
            }
        }
        #endregion

        #region TextView property

        /// <summary>
        /// Gets the text view used to display text in this text area.
        /// </summary>
        internal TextView TextView { get; }

        #endregion

        #region Document property
        /// <summary>
        /// Gets/Sets the document displayed by the text editor.
        /// </summary>
        internal TextDocument Document
        {
            get
            {
                return _document;
            }
            set
            {
                TextDocument oldValue = _document;
                _document = value;
                OnDocumentChanged(oldValue, value);
            }
        }

        /// <inheritdoc/>
        internal event EventHandler<DocumentChangedEventArgs> DocumentChanged;

        /// <summary>
        /// Gets if the the document displayed by the text editor is readonly
        /// </summary>
        internal bool IsReadOnly
        {
            get => ReadOnlySectionProvider == ReadOnlySectionDocument.Instance;
        }

        private void OnDocumentChanged(TextDocument oldValue, TextDocument newValue)
        {
            if (oldValue != null)
            {
                TextDocumentWeakEventManager.Changing.RemoveHandler(oldValue, OnDocumentChanging);
                TextDocumentWeakEventManager.Changed.RemoveHandler(oldValue, OnDocumentChanged);
                TextDocumentWeakEventManager.UpdateStarted.RemoveHandler(oldValue, OnUpdateStarted);
                TextDocumentWeakEventManager.UpdateFinished.RemoveHandler(oldValue, OnUpdateFinished);
            }
            TextView.Document = newValue;
            if (newValue != null)
            {
                TextDocumentWeakEventManager.Changing.AddHandler(newValue, OnDocumentChanging);
                TextDocumentWeakEventManager.Changed.AddHandler(newValue, OnDocumentChanged);
                TextDocumentWeakEventManager.UpdateStarted.AddHandler(newValue, OnUpdateStarted);
                TextDocumentWeakEventManager.UpdateFinished.AddHandler(newValue, OnUpdateFinished);

                MarkDirtyRepaint();
            }
            // Reset caret location and selection: this is necessary because the caret/selection might be invalid
            // in the new document (e.g. if new document is shorter than the old document).
            Caret.Location = new TextLocation(1, 1);
            ClearSelection();
            DocumentChanged?.Invoke(this, new DocumentChangedEventArgs(oldValue, newValue));
            //CommandManager.InvalidateRequerySuggested();
        }
        #endregion

        #region Options property

        /// <summary>
        /// Options property.
        /// </summary>
        internal TextEditorOptions Options
        {
            get
            {
                return _options;
            }
            set
            {
                TextEditorOptions oldValue = _options;
                _options = value;
                OnOptionsChanged(oldValue, value);
            }
        }

        /// <summary>
        /// Occurs when a text editor option has changed.
        /// </summary>
        internal event PropertyChangedEventHandler OptionChanged;

        private void OnOptionChanged(object sender, PropertyChangedEventArgs e)
        {
            OnOptionChanged(e);
        }

        /// <summary>
        /// Raises the <see cref="OptionChanged"/> event.
        /// </summary>
        protected virtual void OnOptionChanged(PropertyChangedEventArgs e)
        {
            OptionChanged?.Invoke(this, e);
        }

        private void OnOptionsChanged(TextEditorOptions oldValue, TextEditorOptions newValue)
        {
            if (oldValue != null)
            {
                PropertyChangedWeakEventManager.RemoveHandler(oldValue, OnOptionChanged);
            }
            TextView.Options = newValue;
            if (newValue != null)
            {
                PropertyChangedWeakEventManager.AddHandler(newValue, OnOptionChanged);
            }
            OnOptionChanged(new PropertyChangedEventArgs(null));
        }
        #endregion

        /// <summary>
        /// Creates a new TextArea instance.
        /// </summary>
        internal TextArea() : this(new TextView())
        {
        }

        /// <summary>
        /// Creates a new TextArea instance.
        /// </summary>
        protected TextArea(TextView textView)
        {
            focusable = true;
            style.flexDirection = FlexDirection.Row;
            style.flexGrow = 1;
            style.backgroundColor = TextEditorColors.Background;

            _selectionMouseHandler = new SelectionMouseHandler(this);
            _editingCommandHandler = new EditingCommandHandler(this);
            _caretNavigationCommandHandler = new CaretNavigationCommandHandler(this);

            TextView = textView ?? throw new ArgumentNullException(nameof(textView));
            Options = textView.Options;

            _selection = EmptySelection = new EmptySelection(this);

            textView.Services.AddService(this);

            _selectionLayer = new SelectionLayer(this);
            textView.InsertLayer(_selectionLayer, KnownLayer.Selection, LayerInsertionPosition.Replace);

            Caret = new Caret(this);
            Caret.PositionChanged += (sender, e) => RequestSelectionValidation();
            Caret.PositionChanged += CaretPositionChanged;

            LeftMargins.CollectionChanged += LeftMargins_CollectionChanged;

            _leftMarginsContainer = new VisualElement();
            _leftMarginsContainer.style.flexDirection = FlexDirection.Row;
            _leftMarginsContainer.style.flexShrink = 0;
            Add(_leftMarginsContainer);
            Add(textView);

            RegisterCallback<KeyDownEvent>(OnKeyDownEvent);
            RegisterCallback<NavigationMoveEvent>(OnNavigationMoveEvent);
            RegisterCallback<FocusInEvent>(OnFocusIn);
            RegisterCallback<FocusOutEvent>(OnFocusOut);
            RegisterCallback<PointerDownEvent>(OnPointerDownEvent);
            RegisterCallback<PointerUpEvent>(OnPointerUpEvent);
            RegisterCallback<PointerMoveEvent>(OnPointerMoveEvent);
        }

        internal void Dispose()
        {
            UnregisterCallback<KeyDownEvent>(OnKeyDownEvent);
            UnregisterCallback<NavigationMoveEvent>(OnNavigationMoveEvent);
            UnregisterCallback<FocusInEvent>(OnFocusIn);
            UnregisterCallback<FocusOutEvent>(OnFocusOut);
            UnregisterCallback<PointerDownEvent>(OnPointerDownEvent);
            UnregisterCallback<PointerUpEvent>(OnPointerUpEvent);
            UnregisterCallback<PointerMoveEvent>(OnPointerMoveEvent);
        }

        private void OnKeyDownEvent(KeyDownEvent e)
        {
            _caretNavigationCommandHandler.OnKeyDown(e);
            _editingCommandHandler.OnKeyDown(e);
            ProcessTextInput(e);
        }

        private void OnNavigationMoveEvent(NavigationMoveEvent e)
        {
#if UNITY_2022_1_OR_NEWER
            if (IsReadOnly)
                return;

            // Tab and Shift+Tab generate a NavigationMoveEvent with direction
            // Next/Previous. Consuming it prevents the focus controller from
            // moving focus away from the text editor.
            if (e.direction == NavigationMoveEvent.Direction.Next ||
                e.direction == NavigationMoveEvent.Direction.Previous)
            {
                e.StopPropagation();
                #if UNITY_6000_0_OR_NEWER
                    focusController.IgnoreEvent(e);
                #else
                    e.PreventDefault();
                #endif
            }
#endif
        }

        private void OnFocusIn(FocusInEvent e)
        {
            OnFocus();
        }

        private void OnFocusOut(FocusOutEvent e)
        {
            OnLostFocus();
        }

        private void OnPointerDownEvent(PointerDownEvent e)
        {
            var localPos = e.localPosition;
            var textViewPos = TextView.WorldToLocal(e.position);
            TextView.OnPointerPressed(textViewPos);
            _selectionMouseHandler.OnPointerPressed(e);
            OnFocus();
            this.CapturePointer(e.pointerId);
        }

        private void OnPointerUpEvent(PointerUpEvent e)
        {
            var textViewPos = TextView.WorldToLocal(e.position);
            TextView.OnPointerReleased(textViewPos);
            _selectionMouseHandler.OnPointerReleased(e);
            this.ReleasePointer(e.pointerId);
        }

        private void OnPointerMoveEvent(PointerMoveEvent e)
        {
            if (this.HasPointerCapture(e.pointerId))
            {
                TextView.OnPointerMoved();
                _selectionMouseHandler.OnPointerMoved(e);
            }
        }

        #region Force caret to stay inside selection

        private bool _ensureSelectionValidRequested;
        private int _allowCaretOutsideSelection;

        private void RequestSelectionValidation()
        {
            if (!_ensureSelectionValidRequested && _allowCaretOutsideSelection == 0)
            {
                _ensureSelectionValidRequested = true;
                EditorDispatcher.Dispatch(EnsureSelectionValid);
            }
        }

        /// <summary>
        /// Code that updates only the caret but not the selection can cause confusion when
        /// keys like 'Delete' delete the (possibly invisible) selected text and not the
        /// text around the caret.
        ///
        /// So we'll ensure that the caret is inside the selection.
        /// (when the caret is not in the selection, we'll clear the selection)
        ///
        /// This method is invoked using the Dispatcher so that code may temporarily violate this rule
        /// (e.g. most 'extend selection' methods work by first setting the caret, then the selection),
        /// it's sufficient to fix it after any event handlers have run.
        /// </summary>
        private void EnsureSelectionValid()
        {
            _ensureSelectionValidRequested = false;
            if (_allowCaretOutsideSelection == 0)
            {
                if (!_selection.IsEmpty && !_selection.Contains(Caret.Offset))
                {
                    ClearSelection();
                }
            }
        }

        /// <summary>
        /// Temporarily allows positioning the caret outside the selection.
        /// Dispose the returned IDisposable to revert the allowance.
        /// </summary>
        /// <remarks>
        /// The text area only forces the caret to be inside the selection when other events
        /// have finished running (using the dispatcher), so you don't have to use this method
        /// for temporarily positioning the caret in event handlers.
        /// This method is only necessary if you want to run the dispatcher, e.g. if you
        /// perform a drag'n'drop operation.
        /// </remarks>
        internal IDisposable AllowCaretOutsideSelection()
        {
            EditorDispatcher.VerifyMainThreadAccess();
            _allowCaretOutsideSelection++;
            return new CallbackOnDispose(
                delegate
                {
                    EditorDispatcher.VerifyMainThreadAccess();
                    _allowCaretOutsideSelection--;
                    RequestSelectionValidation();
                });
        }
        #endregion

        #region Properties

        /// <summary>
        /// Gets the Caret used for this text area.
        /// </summary>
        internal Caret Caret { get; }

                /// <summary>
        /// Scrolls the text view so that the requested line is in the middle.
        /// If the textview can be scrolled.
        /// </summary>
        /// <param name="line">The line to scroll to.</param>
        internal void ScrollToLine(int line)
        {
            var viewPortLines = (int)TextView.ScrollViewport.y;

            if (viewPortLines < Document.LineCount)
            {
                ScrollToLine(line, 2, viewPortLines / 2);
            }
        }

        /// <summary>
        /// Scrolls the textview to a position with n lines above and below it.
        /// </summary>
        /// <param name="line">the requested line number.</param>
        /// <param name="linesEitherSide">The number of lines above and below.</param>
        internal void ScrollToLine(int line, int linesEitherSide)
        {
            ScrollToLine(line, linesEitherSide, linesEitherSide);
        }

        /// <summary>
        /// Scrolls the textview to a position with n lines above and below it.
        /// </summary>
        /// <param name="line">the requested line number.</param>
        /// <param name="linesAbove">The number of lines above.</param>
        /// <param name="linesBelow">The number of lines below.</param>
        internal void ScrollToLine(int line, int linesAbove, int linesBelow)
        {
            var offset = line - linesAbove;

            if (offset < 0)
            {
                offset = 0;
            }

            this.TextView.MakeVisible(new Rect(1, offset, 0, 1));

            offset = line + linesBelow;

            if (offset >= 0)
            {
                this.TextView.MakeVisible(new Rect(1, offset, 0, 1));
            }
        }

        private void CaretPositionChanged(object sender, EventArgs e)
        {
            // TODO: review!! ScrollToLine is not correct
            /*if (TextView == null)
                return;

            TextView.HighlightedLine = Caret.Line;

            ScrollToLine(Caret.Line, 2);*/
        }

        #endregion

        /// <summary>
        /// Gets the collection of margins displayed to the left of the text view.
        /// </summary>
        internal ObservableCollection<AbstractMargin> LeftMargins { get; } = new ObservableCollection<AbstractMargin>();

        private void LeftMargins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.OldItems != null)
            {
                foreach (var item in e.OldItems)
                {
                    if (item is ITextViewConnect c)
                        c.RemoveFromTextView(TextView);
                    if (item is VisualElement ve)
                        ve.RemoveFromHierarchy();
                }
            }
            if (e.NewItems != null)
            {
                foreach (var item in e.NewItems)
                {
                    if (item is ITextViewConnect c)
                        c.AddToTextView(TextView);
                    if (item is VisualElement ve && _leftMarginsContainer != null)
                        _leftMarginsContainer.Add(ve);
                }
            }
        }

        /// <summary>
        /// Gets/Sets an object that provides read-only sections for the text area.
        /// </summary>
        internal IReadOnlySectionProvider ReadOnlySectionProvider
        {
            get => _readOnlySectionProvider;
            set => _readOnlySectionProvider = value ?? throw new ArgumentNullException(nameof(value));
        }

        /// <summary>
        /// Determines whether caret position should be changed to the mouse position when you right click or not.
        /// </summary>
        internal bool RightClickMovesCaret
        {
            get => _rigthClickMovesCaret;
            set => _rigthClickMovesCaret = value;
        }

        internal readonly Selection EmptySelection;
        private Selection _selection;
        private SelectionLayer _selectionLayer;

        /// <summary>
        /// Occurs when the selection has changed.
        /// </summary>
        internal event EventHandler SelectionChanged;

        /// <summary>
        /// Gets/Sets the selection in this text area.
        /// </summary>

        internal Selection Selection
        {
            get => _selection;
            set
            {
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                if (value.TextArea != this)
                    throw new ArgumentException("Cannot use a Selection instance that belongs to another text area.");
                if (!Equals(_selection, value))
                {
                    if (TextView != null)
                    {
                        var oldSegment = _selection.SurroundingSegment;
                        var newSegment = value.SurroundingSegment;
                        if (!Selection.EnableVirtualSpace && (_selection is SimpleSelection && value is SimpleSelection && oldSegment != null && newSegment != null))
                        {
                            // perf optimization:
                            // When a simple selection changes, don't redraw the whole selection, but only the changed parts.
                            var oldSegmentOffset = oldSegment.Offset;
                            var newSegmentOffset = newSegment.Offset;
                            if (oldSegmentOffset != newSegmentOffset)
                            {
                                TextView.Redraw(Math.Min(oldSegmentOffset, newSegmentOffset),
                                                Math.Abs(oldSegmentOffset - newSegmentOffset));
                            }
                            var oldSegmentEndOffset = oldSegment.EndOffset;
                            var newSegmentEndOffset = newSegment.EndOffset;
                            if (oldSegmentEndOffset != newSegmentEndOffset)
                            {
                                TextView.Redraw(Math.Min(oldSegmentEndOffset, newSegmentEndOffset),
                                                Math.Abs(oldSegmentEndOffset - newSegmentEndOffset));
                            }
                        }
                        else
                        {
                            TextView.Redraw(oldSegment);
                            TextView.Redraw(newSegment);
                        }
                    }
                    _selection = value;
                    _selectionLayer.InvalidateSelection();
                    SelectionChanged?.Invoke(this, EventArgs.Empty);
                    // a selection change causes commands like copy/paste/etc. to change status
                    //CommandManager.InvalidateRequerySuggested();
                }
            }
        }

        /// <summary>
        /// Clears the current selection.
        /// </summary>
        internal void ClearSelection()
        {
            Selection = EmptySelection;
        }

        Color mSelectionForeground;

        /// <summary>
        /// Gets/Sets the foreground brush used for selected text.
        /// </summary>
        internal Color SelectionForeground
        {
            get => mSelectionForeground;
            set
            {
                mSelectionForeground = value;
                TextView.Redraw();
            }
        }

        #region Focus Handling (Show/Hide Caret)

        internal void OnFocus()
        {
            _isFocused = true;
            Caret.Show();
        }

        internal void OnLostFocus()
        {
            _isFocused = false;
            Caret.Hide();
        }

        void ProcessTextInput(KeyDownEvent e)
        {
            if (e.character != 0 && TextView.Font != null && TextView.Font.HasCharacter(e.character))
            {
                TextInputEventArgs inputEventArgs = new TextInputEventArgs()
                {
                    Text = e.character.ToString(),
                };

                e.StopPropagation();
                PerformTextInput(inputEventArgs);
            }
        }

        #endregion

        #region OnTextInput / RemoveSelectedText / ReplaceSelectionWithText
        /// <summary>
        /// Occurs when the TextArea receives text input.
        /// but occurs immediately before the TextArea handles the TextInput event.
        /// </summary>
        internal event EventHandler<TextInputEventArgs> TextEntering;

        /// <summary>
        /// Occurs when the TextArea receives text input.
        /// but occurs immediately after the TextArea handles the TextInput event.
        /// </summary>
        internal event EventHandler<TextInputEventArgs> TextEntered;

        /// <summary>
        /// Raises the TextEntering event.
        /// </summary>
        protected virtual void OnTextEntering(TextInputEventArgs e)
        {
            TextEntering?.Invoke(this, e);
        }

        /// <summary>
        /// Raises the TextEntered event.
        /// </summary>
        protected virtual void OnTextEntered(TextInputEventArgs e)
        {
            TextEntered?.Invoke(this, e);
        }

        /*protected override void OnTextInput(TextInputEventArgs e)
        {
            base.OnTextInput(e);
            if (!e.Handled && Document != null)
            {
                if (string.IsNullOrEmpty(e.Text) || e.Text == "\x1b" || e.Text == "\b" || e.Text == "\u007f")
                {
                    // TODO: check this
                    // ASCII 0x1b = ESC.
                    // produces a TextInput event with that old ASCII control char
                    // when Escape is pressed. We'll just ignore it.

                    // A deadkey followed by backspace causes a textinput event for the BS character.

                    // Similarly, some shortcuts like Alt+Space produce an empty TextInput event.
                    // We have to ignore those (not handle them) to keep the shortcut working.
                    return;
                }
                HideMouseCursor();
                PerformTextInput(e);
                e.Handled = true;
            }
        }*/

        /// <summary>
        /// Performs text input.
        /// This raises the <see cref="TextEntering"/> event, replaces the selection with the text,
        /// and then raises the <see cref="TextEntered"/> event.
        /// </summary>
        internal void PerformTextInput(string text)
        {
            var e = new TextInputEventArgs
            {
                Text = text,
            };
            PerformTextInput(e);
        }

        /// <summary>
        /// Performs text input.
        /// This raises the <see cref="TextEntering"/> event, replaces the selection with the text,
        /// and then raises the <see cref="TextEntered"/> event.
        /// </summary>
        internal void PerformTextInput(TextInputEventArgs e)
        {
            if (e == null)
                throw new ArgumentNullException(nameof(e));
            if (Document == null)
                throw ThrowUtil.NoDocumentAssigned();
            OnTextEntering(e);
            if (!e.Handled)
            {
                if (e.Text == "\n" || e.Text == "\r" || e.Text == "\r\n")
                    ReplaceSelectionWithNewLine();
                else
                {
                    // TODO
                    //if (OverstrikeMode && Selection.IsEmpty && Document.GetLineByNumber(Caret.Line).EndOffset > Caret.Offset)
                    //    EditingCommands.SelectRightByCharacter.Execute(null, this);
                    ReplaceSelectionWithText(e.Text);
                }
                OnTextEntered(e);
                Caret.BringCaretToView();
            }
        }

        private void ReplaceSelectionWithNewLine()
        {
            var newLine = TextUtilities.GetNewLineFromDocument(Document, Caret.Line);
            using (Document.RunUpdate())
            {
                ReplaceSelectionWithText(newLine);
                if (IndentationStrategy != null)
                {
                    var line = Document.GetLineByNumber(Caret.Line);
                    var deletable = GetDeletableSegments(line);
                    if (deletable.Length == 1 && deletable[0].Offset == line.Offset && deletable[0].Length == line.Length)
                    {
                        // use indentation strategy only if the line is not read-only
                        IndentationStrategy.IndentLine(Document, line);
                    }
                }
            }
        }

        internal void ReplaceSelectionWithText(string newText)
        {
            if (newText == null)
                throw new ArgumentNullException(nameof(newText));
            if (Document == null)
                throw ThrowUtil.NoDocumentAssigned();
            _selection.ReplaceSelectionWithText(newText);
        }

        internal ISegment[] GetDeletableSegments(ISegment segment)
        {
            var deletableSegments = ReadOnlySectionProvider.GetDeletableSegments(segment);
            if (deletableSegments == null)
                throw new InvalidOperationException("ReadOnlySectionProvider.GetDeletableSegments returned null");
            var array = deletableSegments.ToArray();
            var lastIndex = segment.Offset;
            foreach (var t in array)
            {
                if (t.Offset < lastIndex)
                    throw new InvalidOperationException("ReadOnlySectionProvider returned incorrect segments (outside of input segment / wrong order)");
                lastIndex = t.EndOffset;
            }
            if (lastIndex > segment.EndOffset)
                throw new InvalidOperationException("ReadOnlySectionProvider returned incorrect segments (outside of input segment / wrong order)");
            return array;
        }
        #endregion

        #region IndentationStrategy property

        /// <summary>
        /// Gets/Sets the indentation strategy used when inserting new lines.
        /// </summary>
        internal IIndentationStrategy IndentationStrategy { get; set; }

        #endregion

        internal void RemoveSelectedText()
        {
            if (Document == null)
                throw ThrowUtil.NoDocumentAssigned();
            _selection.ReplaceSelectionWithText(string.Empty);
#if DEBUG
            if (!_selection.IsEmpty)
            {
                foreach (var s in _selection.Segments)
                {
                    Debug.Assert(!ReadOnlySectionProvider.GetDeletableSegments(s).Any());
                }
            }
#endif
        }

        /// <summary>
        /// Occurs when text inside the TextArea was copied.
        /// </summary>
        internal event EventHandler<TextEventArgs> TextCopied;

        internal void OnTextCopied(TextEventArgs e)
        {
            TextCopied?.Invoke(this, e);
        }

        /// <summary>
        /// EventArgs with text.
        /// </summary>
        internal class TextEventArgs : EventArgs
        {
            /// <summary>
            /// Gets the text.
            /// </summary>
            internal string Text { get; }

            /// <summary>
            /// Creates a new TextEventArgs instance.
            /// </summary>
            internal TextEventArgs(string text)
            {
                Text = text ?? throw new ArgumentNullException(nameof(text));
            }
        }
    }
}