// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Unity.CodeEditor.Document;
using Unity.CodeEditor.Text;
using Unity.CodeEditor.Utils;
using UnityEngine;
using Debug = System.Diagnostics.Debug;
using LogicalDirection = Unity.CodeEditor.Document.LogicalDirection;

namespace Unity.CodeEditor.Rendering
{
    /// <summary>
    /// Represents a visual line in the document.
    /// A visual line usually corresponds to one DocumentLine, but it can span multiple lines if
    /// all but the first are collapsed.
    /// </summary>
    internal sealed class VisualLine
    {
        internal const int LENGTH_LIMIT = 3000;

        private enum LifetimePhase : byte
        {
            Generating,
            Transforming,
            Live,
            Disposed
        }
        private readonly TextView _textView;

        private List<VisualLineElement> _elements;
        internal bool HasInlineObjects;
        private LifetimePhase _phase;

        /// <summary>
        /// Gets the document to which this VisualLine belongs.
        /// </summary>
        internal TextDocument Document { get; }

        /// <summary>
        /// Gets the first document line displayed by this visual line.
        /// </summary>
        internal DocumentLine FirstDocumentLine { get; }

        /// <summary>
        /// Gets the last document line displayed by this visual line.
        /// </summary>
        internal DocumentLine LastDocumentLine { get; private set; }

        /// <summary>
        /// Gets a read-only collection of line elements.
        /// </summary>
        internal ReadOnlyCollection<VisualLineElement> Elements { get; private set; }

        private ReadOnlyCollection<TextLine> _textLines;

        /// <summary>
        /// Gets a read-only collection of text lines.
        /// </summary>
        internal ReadOnlyCollection<TextLine> TextLines
        {
            get
            {
                if (_phase < LifetimePhase.Live)
                    throw new InvalidOperationException();
                return _textLines;
            }
        }

        /// <summary>
        /// Gets the start offset of the VisualLine inside the document.
        /// This is equivalent to <c>FirstDocumentLine.Offset</c>.
        /// </summary>
        internal int StartOffset => FirstDocumentLine.Offset;

        /// <summary>
        /// Length in visual line coordinates.
        /// </summary>
        internal int VisualLength { get; private set; }

        /// <summary>
        /// Length in visual line coordinates including the end of line marker, if TextEditorOptions.ShowEndOfLine is enabled.
        /// </summary>
        internal int VisualLengthWithEndOfLineMarker
        {
            get
            {
                var length = VisualLength;
                if (_textView.Options.ShowEndOfLine && LastDocumentLine.NextLine != null) length++;
                return length;
            }
        }

        /// <summary>
        /// Gets the height of the visual line in device-independent pixels.
        /// </summary>
        internal float Height { get; private set; }

        /// <summary>
        /// Gets the Y position of the line. This is measured in device-independent pixels relative to the start of the document.
        /// </summary>
        internal float VisualTop { get; set; }

        internal VisualLine(TextView textView, DocumentLine firstDocumentLine)
        {
            Debug.Assert(textView != null);
            Debug.Assert(firstDocumentLine != null);
            _textView = textView;
            Document = textView.Document;
            FirstDocumentLine = firstDocumentLine;
        }

        internal void ConstructVisualElements(ITextRunConstructionContext context, IReadOnlyList<VisualLineElementGenerator> generators)
        {
            Debug.Assert(_phase == LifetimePhase.Generating);
            foreach (var g in generators)
            {
                g.StartGeneration(context);
            }
            _elements = new List<VisualLineElement>();
            PerformVisualElementConstruction(generators);
            foreach (var g in generators)
            {
                g.FinishGeneration();
            }

            var textParagraphProperties = context.TextParagraphProperties;
            foreach (var element in _elements)
            {
                element.SetTextParagraphProperties(textParagraphProperties);
            }
            this.Elements = new ReadOnlyCollection<VisualLineElement>(_elements);
            CalculateOffsets();
            _phase = LifetimePhase.Transforming;
        }

        void PerformVisualElementConstruction(IReadOnlyList<VisualLineElementGenerator> generators)
        {
            var lineLength = FirstDocumentLine.Length;
            var offset = FirstDocumentLine.Offset;
            var currentLineEnd = offset + lineLength;
            LastDocumentLine = FirstDocumentLine;
            var askInterestOffset = 0; // 0 or 1

            while (offset + askInterestOffset <= currentLineEnd)
            {
                var textPieceEndOffset = currentLineEnd;
                foreach (var g in generators)
                {
                    g.CachedInterest = g.GetFirstInterestedOffset(offset + askInterestOffset);
                    if (g.CachedInterest != -1)
                    {
                        if (g.CachedInterest < offset)
                            throw new ArgumentOutOfRangeException(g.GetType().Name + ".GetFirstInterestedOffset",
                                                                  g.CachedInterest,
                                                                  "GetFirstInterestedOffset must not return an offset less than startOffset. Return -1 to signal no interest.");
                        if (g.CachedInterest < textPieceEndOffset)
                            textPieceEndOffset = g.CachedInterest;
                    }
                }
                Debug.Assert(textPieceEndOffset >= offset);
                if (textPieceEndOffset > offset)
                {
                    var textPieceLength = textPieceEndOffset - offset;

                    _elements.Add(new VisualLineText(this, textPieceLength));

                    offset = textPieceEndOffset;
                }
                // If no elements constructed / only zero-length elements constructed:
                // do not asking the generators again for the same location (would cause endless loop)
                askInterestOffset = 1;
                foreach (var g in generators)
                {
                    if (g.CachedInterest == offset)
                    {
                        var element = g.ConstructElement(offset);
                        if (element != null)
                        {
                            _elements.Add(element);
                            if (element.DocumentLength > 0)
                            {
                                // a non-zero-length element was constructed
                                askInterestOffset = 0;
                                offset += element.DocumentLength;
                                if (offset > currentLineEnd)
                                {
                                    var newEndLine = Document.GetLineByOffset(offset);
                                    currentLineEnd = newEndLine.Offset + newEndLine.Length;
                                    this.LastDocumentLine = newEndLine;
                                    if (currentLineEnd < offset)
                                    {
                                        throw new InvalidOperationException(
                                            "The VisualLineElementGenerator " + g.GetType().Name +
                                            " produced an element which ends within the line delimiter");
                                    }
                                }
                                break;
                            }
                        }
                    }
                }
            }
        }

        private void CalculateOffsets()
        {
            var visualOffset = 0;
            var textOffset = 0;
            foreach (var element in _elements)
            {
                element.VisualColumn = visualOffset;
                element.RelativeTextOffset = textOffset;
                visualOffset += element.VisualLength;
                textOffset += element.DocumentLength;
            }
            VisualLength = visualOffset;
            Debug.Assert(textOffset == LastDocumentLine.EndOffset - FirstDocumentLine.Offset);
        }

        internal void RunTransformers(ITextRunConstructionContext context, IReadOnlyList<IVisualLineTransformer> transformers)
        {
            Debug.Assert(_phase == LifetimePhase.Transforming);
            foreach (var transformer in transformers)
            {
                transformer.Transform(context, _elements);
            }
            _phase = LifetimePhase.Live;
        }

        /// <summary>
        /// Replaces the single element at <paramref name="elementIndex"/> with the specified elements.
        /// The replacement operation must preserve the document length, but may change the visual length.
        /// </summary>
        /// <remarks>
        /// This method may only be called by line transformers.
        /// </remarks>
        internal void ReplaceElement(int elementIndex, params VisualLineElement[] newElements)
        {
            ReplaceElement(elementIndex, 1, newElements);
        }

        /// <summary>
        /// Replaces <paramref name="count"/> elements starting at <paramref name="elementIndex"/> with the specified elements.
        /// The replacement operation must preserve the document length, but may change the visual length.
        /// </summary>
        /// <remarks>
        /// This method may only be called by line transformers.
        /// </remarks>
        internal void ReplaceElement(int elementIndex, int count, params VisualLineElement[] newElements)
        {
            if (_phase != LifetimePhase.Transforming)
                throw new InvalidOperationException("This method may only be called by line transformers.");
            var oldDocumentLength = 0;
            for (var i = elementIndex; i < elementIndex + count; i++)
            {
                oldDocumentLength += _elements[i].DocumentLength;
            }
            var newDocumentLength = 0;
            foreach (var newElement in newElements)
            {
                newDocumentLength += newElement.DocumentLength;
            }
            if (oldDocumentLength != newDocumentLength)
                throw new InvalidOperationException("Old elements have document length " + oldDocumentLength + ", but new elements have length " + newDocumentLength);
            _elements.RemoveRange(elementIndex, count);
            _elements.InsertRange(elementIndex, newElements);
            CalculateOffsets();
        }

        internal void SetTextLines(List<TextLine> textLines)
        {
            _textLines = new ReadOnlyCollection<TextLine>(textLines);
            Height = 0;
            foreach (var line in textLines)
                Height += line.Height;
        }

        /// <summary>
        /// Gets the visual column from a document offset relative to the first line start.
        /// </summary>
        internal int GetVisualColumn(int relativeTextOffset)
        {
            ThrowUtil.CheckNotNegative(relativeTextOffset, "relativeTextOffset");
            foreach (var element in _elements)
            {
                if (element.RelativeTextOffset <= relativeTextOffset
                    && element.RelativeTextOffset + element.DocumentLength >= relativeTextOffset)
                {
                    return element.GetVisualColumn(relativeTextOffset);
                }
            }
            return VisualLength;
        }

        /// <summary>
        /// Gets the document offset (relative to the first line start) from a visual column.
        /// </summary>
        internal int GetRelativeOffset(int visualColumn)
        {
            ThrowUtil.CheckNotNegative(visualColumn, "visualColumn");
            var documentLength = 0;
            foreach (var element in _elements)
            {
                if (element.VisualColumn <= visualColumn
                    && element.VisualColumn + element.VisualLength > visualColumn)
                {
                    return element.GetRelativeOffset(visualColumn);
                }
                documentLength += element.DocumentLength;
            }
            return documentLength;
        }

        /// <summary>
        /// Gets the text line containing the specified visual column.
        /// </summary>
        internal TextLine GetTextLine(int visualColumn)
        {
            return GetTextLine(visualColumn, false);
        }

        /// <summary>
        /// Gets the text line containing the specified visual column.
        /// </summary>
        internal TextLine GetTextLine(int visualColumn, bool isAtEndOfLine)
        {
            if (visualColumn < 0)
                throw new ArgumentOutOfRangeException(nameof(visualColumn));
            if (visualColumn >= VisualLengthWithEndOfLineMarker)
                return TextLines[TextLines.Count - 1];
            foreach (var line in TextLines)
            {
                if (isAtEndOfLine ? visualColumn <= line.Length : visualColumn < line.Length)
                    return line;
                visualColumn -= line.Length;
            }
            throw new InvalidOperationException("Shouldn't happen (VisualLength incorrect?)");
        }

        /// <summary>
        /// Gets the visual top from the specified text line.
        /// </summary>
        /// <returns>Distance in device-independent pixels
        /// from the top of the document to the top of the specified text line.</returns>
        internal float GetTextLineVisualYPosition(TextLine textLine, VisualYPosition yPositionMode)
        {
            if (textLine == null)
                throw new ArgumentNullException(nameof(textLine));
            var pos = VisualTop;
            foreach (var tl in TextLines)
            {
                if (tl == textLine)
                {
                    switch (yPositionMode)
                    {
                        case VisualYPosition.LineTop:
                            return pos;
                        case VisualYPosition.LineMiddle:
                            return pos + tl.Height / 2;
                        case VisualYPosition.LineBottom:
                            return pos + tl.Height;
                        case VisualYPosition.TextTop:
                            return pos + tl.LineRect.yMin - _textView.DefaultBaseline;
                        case VisualYPosition.TextBottom:
                            return pos + tl.LineRect.yMin - _textView.DefaultBaseline + _textView.DefaultLineHeight;
                        case VisualYPosition.TextMiddle:
                            return pos + tl.LineRect.yMin - _textView.DefaultBaseline + _textView.DefaultLineHeight / 2;
                        case VisualYPosition.Baseline:
                            return pos + tl.LineRect.yMin;
                        default:
                            throw new ArgumentException("Invalid yPositionMode:" + yPositionMode);
                    }
                }
                pos += tl.Height;
            }
            throw new ArgumentException("textLine is not a line in this VisualLine");
        }

        /// <summary>
        /// Gets the start visual column from the specified text line.
        /// </summary>
        internal int GetTextLineVisualStartColumn(TextLine textLine)
        {
            if (!TextLines.Contains(textLine))
                throw new ArgumentException("textLine is not a line in this VisualLine");

            return TextLines.TakeWhile(tl => tl != textLine).Sum(tl => tl.Length);
        }

        /// <summary>
        /// Gets a TextLine by the visual position.
        /// </summary>
        internal TextLine GetTextLineByVisualYPosition(double visualTop)
        {
            const double epsilon = 0.0001;
            var pos = VisualTop;
            foreach (var tl in TextLines)
            {
                pos += tl.Height;
                if (visualTop + epsilon < pos)
                    return tl;
            }

            return TextLines[TextLines.Count - 1];
        }

        /// <summary>
        /// Gets the visual position from the specified visualColumn.
        /// </summary>
        /// <returns>Position in device-independent pixels
        /// relative to the top left of the document.</returns>
        internal Vector2 GetVisualPosition(int visualColumn, VisualYPosition yPositionMode)
        {
            var textLine = GetTextLine(visualColumn);
            var xPos = GetTextLineVisualXPosition(textLine, visualColumn);
            var yPos = GetTextLineVisualYPosition(textLine, yPositionMode);
            return new Vector2(xPos, yPos);
        }

        internal Vector2 GetVisualPosition(int visualColumn, bool isAtEndOfLine, VisualYPosition yPositionMode)
        {
            var textLine = GetTextLine(visualColumn, isAtEndOfLine);
            var xPos = GetTextLineVisualXPosition(textLine, visualColumn);
            var yPos = GetTextLineVisualYPosition(textLine, yPositionMode);
            return new Vector2(xPos, yPos);
        }

        /// <summary>
        /// Gets the distance to the left border of the text area of the specified visual column.
        /// The visual column must belong to the specified text line.
        /// </summary>
        internal float GetTextLineVisualXPosition(TextLine textLine, int visualColumn)
        {
            if (textLine == null)
                throw new ArgumentNullException(nameof(textLine));

            var xPos = textLine.GetDistanceFromCharacterHit(new CharacterHit(Math.Min(visualColumn,
                VisualLengthWithEndOfLineMarker)));

            if (visualColumn > VisualLengthWithEndOfLineMarker)
            {
                xPos += (visualColumn - VisualLengthWithEndOfLineMarker) * _textView.WideSpaceWidth;
            }

            return xPos;
        }

        /// <summary>
        /// Gets the visual column from a document position (relative to top left of the document).
        /// If the user clicks between two visual columns, rounds to the nearest column.
        /// </summary>
        internal int GetVisualColumn(Vector2 point)
        {
            return GetVisualColumn(point, _textView.Options.EnableVirtualSpace);
        }

        /// <summary>
        /// Gets the visual column from a document position (relative to top left of the document).
        /// If the user clicks between two visual columns, rounds to the nearest column.
        /// </summary>
        internal int GetVisualColumn(Vector2 point, bool allowVirtualSpace)
        {
            return GetVisualColumn(GetTextLineByVisualYPosition(point.y), point.x, allowVirtualSpace);
        }

        internal int GetVisualColumn(Vector2 point, bool allowVirtualSpace, out bool isAtEndOfLine)
        {
            var textLine = GetTextLineByVisualYPosition(point.y);
            var vc = GetVisualColumn(textLine, point.x, allowVirtualSpace);
            isAtEndOfLine = (vc >= GetTextLineVisualStartColumn(textLine) + textLine.Length);
            return vc;
        }

        /// <summary>
        /// Gets the visual column from a document position (relative to top left of the document).
        /// If the user clicks between two visual columns, rounds to the nearest column.
        /// </summary>
        internal int GetVisualColumn(TextLine textLine, float xPos, bool allowVirtualSpace)
        {
            if (xPos > textLine.WidthIncludingTrailingWhitespace)
            {
                if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1])
                {
                    var virtualX = (int)Math.Round((xPos - textLine.WidthIncludingTrailingWhitespace) / _textView.WideSpaceWidth, MidpointRounding.AwayFromZero);
                    return VisualLengthWithEndOfLineMarker + virtualX;
                }
            }

            var ch = textLine.GetCharacterHitFromDistance(xPos);

            return ch.FirstCharacterIndex + ch.TrailingLength;
        }

        /// <summary>
        /// Validates the visual column and returns the correct one.
        /// </summary>
        internal int ValidateVisualColumn(TextViewPosition position, bool allowVirtualSpace)
        {
            return ValidateVisualColumn(Document.GetOffset(position.Location), position.VisualColumn, allowVirtualSpace);
        }

        /// <summary>
        /// Validates the visual column and returns the correct one.
        /// </summary>
        internal int ValidateVisualColumn(int offset, int visualColumn, bool allowVirtualSpace)
        {
            var firstDocumentLineOffset = FirstDocumentLine.Offset;
            if (visualColumn < 0)
            {
                return GetVisualColumn(offset - firstDocumentLineOffset);
            }
            var offsetFromVisualColumn = GetRelativeOffset(visualColumn);
            offsetFromVisualColumn += firstDocumentLineOffset;
            if (offsetFromVisualColumn != offset)
            {
                return GetVisualColumn(offset - firstDocumentLineOffset);
            }
            if (visualColumn > VisualLength && !allowVirtualSpace)
            {
                return VisualLength;
            }
            return visualColumn;
        }

        /// <summary>
        /// Gets the visual column from a document position (relative to top left of the document).
        /// If the user clicks between two visual columns, returns the first of those columns.
        /// </summary>
        internal int GetVisualColumnFloor(Vector2 point)
        {
            return GetVisualColumnFloor(point, _textView.Options.EnableVirtualSpace);
        }

        /// <summary>
        /// Gets the visual column from a document position (relative to top left of the document).
        /// If the user clicks between two visual columns, returns the first of those columns.
        /// </summary>
        internal int GetVisualColumnFloor(Vector2 point, bool allowVirtualSpace)
        {
            return GetVisualColumnFloor(point, allowVirtualSpace, out _);
        }

        internal int GetVisualColumnFloor(Vector2 point, bool allowVirtualSpace, out bool isAtEndOfLine)
        {
            var textLine = GetTextLineByVisualYPosition(point.y);
            if (point.x > textLine.WidthIncludingTrailingWhitespace)
            {
                isAtEndOfLine = true;
                if (allowVirtualSpace && textLine == TextLines[TextLines.Count - 1])
                {
                    // clicking virtual space in the last line
                    var virtualX = (int)((point.x - textLine.WidthIncludingTrailingWhitespace) / _textView.WideSpaceWidth);
                    return VisualLengthWithEndOfLineMarker + virtualX;
                }

                // GetCharacterHitFromDistance returns a hit with FirstCharacterIndex=last character in line
                // and TrailingLength=1 when clicking behind the line, so the floor function needs to handle this case
                // specially and return the line's end column instead.
                return GetTextLineVisualStartColumn(textLine) + textLine.Length;
            }

            isAtEndOfLine = false;

            var ch = textLine.GetCharacterHitFromDistance(point.x);

            return ch.FirstCharacterIndex;
        }

        /// <summary>
        /// Gets the text view position from the specified visual column.
        /// </summary>
        internal TextViewPosition GetTextViewPosition(int visualColumn)
        {
            var documentOffset = GetRelativeOffset(visualColumn) + FirstDocumentLine.Offset;
            return new TextViewPosition(Document.GetLocation(documentOffset), visualColumn);
        }

        /// <summary>
        /// Gets the text view position from the specified visual position.
        /// If the position is within a character, it is rounded to the next character boundary.
        /// </summary>
        /// <param name="visualPosition">The position in device-independent pixels relative
        /// to the top left corner of the document.</param>
        /// <param name="allowVirtualSpace">Controls whether positions in virtual space may be returned.</param>
        internal TextViewPosition GetTextViewPosition(Vector2 visualPosition, bool allowVirtualSpace)
        {
            var visualColumn = GetVisualColumn(visualPosition, allowVirtualSpace, out var isAtEndOfLine);
            var documentOffset = GetRelativeOffset(visualColumn) + FirstDocumentLine.Offset;
            var pos = new TextViewPosition(Document.GetLocation(documentOffset), visualColumn)
            {
                IsAtEndOfLine = isAtEndOfLine
            };
            return pos;
        }

        /// <summary>
        /// Gets the text view position from the specified visual position.
        /// If the position is inside a character, the position in front of the character is returned.
        /// </summary>
        /// <param name="visualPosition">The position in device-independent pixels relative
        /// to the top left corner of the document.</param>
        /// <param name="allowVirtualSpace">Controls whether positions in virtual space may be returned.</param>
        internal TextViewPosition GetTextViewPositionFloor(Vector2 visualPosition, bool allowVirtualSpace)
        {
            var visualColumn = GetVisualColumnFloor(visualPosition, allowVirtualSpace, out var isAtEndOfLine);
            var documentOffset = GetRelativeOffset(visualColumn) + FirstDocumentLine.Offset;
            var pos = new TextViewPosition(Document.GetLocation(documentOffset), visualColumn)
            {
                IsAtEndOfLine = isAtEndOfLine
            };
            return pos;
        }

        /// <summary>
        /// Gets whether the visual line was disposed.
        /// </summary>
        internal bool IsDisposed => _phase == LifetimePhase.Disposed;

        internal void Dispose()
        {
            if (_phase == LifetimePhase.Disposed)
            {
                return;
            }

            Debug.Assert(_phase == LifetimePhase.Live);

            _phase = LifetimePhase.Disposed;
        }

        /// <summary>
        /// Gets the next possible caret position after visualColumn, or -1 if there is no caret position.
        /// </summary>
        internal int GetNextCaretPosition(int visualColumn, LogicalDirection direction, CaretPositioningMode mode, bool allowVirtualSpace)
        {
            if (!HasStopsInVirtualSpace(mode))
                allowVirtualSpace = false;

            if (_elements.Count == 0)
            {
                // special handling for empty visual lines:
                if (allowVirtualSpace)
                {
                    if (direction == LogicalDirection.Forward)
                        return Math.Max(0, visualColumn + 1);
                    if (visualColumn > 0)
                        return visualColumn - 1;
                    return -1;
                }
                // even though we don't have any elements,
                // there's a single caret stop at visualColumn 0
                if (visualColumn < 0 && direction == LogicalDirection.Forward)
                    return 0;
                if (visualColumn > 0 && direction == LogicalDirection.Backward)
                    return 0;
                return -1;
            }

            int i;
            if (direction == LogicalDirection.Backward)
            {
                // Search Backwards:
                // If the last element doesn't handle line borders, return the line end as caret stop

                if (visualColumn > VisualLength && !_elements[_elements.Count - 1].HandlesLineBorders && HasImplicitStopAtLineEnd())
                {
                    if (allowVirtualSpace)
                        return visualColumn - 1;
                    return VisualLength;
                }
                // skip elements that start after or at visualColumn
                for (i = _elements.Count - 1; i >= 0; i--)
                {
                    if (_elements[i].VisualColumn < visualColumn)
                        break;
                }
                // search last element that has a caret stop
                for (; i >= 0; i--)
                {
                    var pos = _elements[i].GetNextCaretPosition(
                        Math.Min(visualColumn, _elements[i].VisualColumn + _elements[i].VisualLength + 1),
                        direction, mode);
                    if (pos >= 0)
                        return pos;
                }
                // If we've found nothing, and the first element doesn't handle line borders,
                // return the line start as normal caret stop.
                if (visualColumn > 0 && !_elements[0].HandlesLineBorders && HasImplicitStopAtLineStart(mode))
                    return 0;
            }
            else
            {
                // Search Forwards:
                // If the first element doesn't handle line borders, return the line start as caret stop
                if (visualColumn < 0 && !_elements[0].HandlesLineBorders && HasImplicitStopAtLineStart(mode))
                    return 0;
                // skip elements that end before or at visualColumn
                for (i = 0; i < _elements.Count; i++)
                {
                    if (_elements[i].VisualColumn + _elements[i].VisualLength > visualColumn)
                        break;
                }
                // search first element that has a caret stop
                for (; i < _elements.Count; i++)
                {
                    var pos = _elements[i].GetNextCaretPosition(
                        Math.Max(visualColumn, _elements[i].VisualColumn - 1),
                        direction, mode);
                    if (pos >= 0)
                        return pos;
                }
                // if we've found nothing, and the last element doesn't handle line borders,
                // return the line end as caret stop
                if ((allowVirtualSpace || !_elements[_elements.Count - 1].HandlesLineBorders) && HasImplicitStopAtLineEnd())
                {
                    if (visualColumn < VisualLength)
                        return VisualLength;
                    if (allowVirtualSpace)
                        return visualColumn + 1;
                }
            }
            // we've found nothing, return -1 and let the caret search continue in the next line
            return -1;
        }

        private static bool HasStopsInVirtualSpace(CaretPositioningMode mode)
        {
            return mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint;
        }

        private static bool HasImplicitStopAtLineStart(CaretPositioningMode mode)
        {
            return mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint;
        }

        private static bool HasImplicitStopAtLineEnd() => true;

        private VisualLineDrawingVisual _visual;

        internal VisualLineDrawingVisual Render()
        {
            Debug.Assert(_phase == LifetimePhase.Live);

            if (_visual == null)
            {
                _visual = new VisualLineDrawingVisual(this);
            }

            return _visual;
        }
    }

    // TODO: can inherit from Layoutable, but dev tools crash
    internal sealed class VisualLineDrawingVisual
    {
        internal VisualLine VisualLine { get; }
        internal float LineHeight { get; }
        internal bool IsAdded { get; set; }
        internal Vector2 RenderTransform { get; set; }

        internal VisualLineDrawingVisual(VisualLine visualLine)
        {
            VisualLine = visualLine;
            LineHeight = VisualLine.TextLines.Sum(textLine => textLine.Height);
        }

        // Rendering is now handled by TextLayer which creates Label elements for each line.
        // This class just stores the layout/transform information.
    }
}