/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

class CodeDocumentLine
{
public:
    CodeDocumentLine (const String::CharPointerType startOfLine,
                      const String::CharPointerType endOfLine,
                      const int lineLen,
                      const int numNewLineChars,
                      const int startInFile)
        : line (startOfLine, endOfLine),
          lineStartInFile (startInFile),
          lineLength (lineLen),
          lineLengthWithoutNewLines (lineLen - numNewLineChars)
    {
    }

    static void createLines (Array<CodeDocumentLine*>& newLines, StringRef text)
    {
        auto t = text.text;
        int charNumInFile = 0;
        bool finished = false;

        while (! (finished || t.isEmpty()))
        {
            auto startOfLine = t;
            auto startOfLineInFile = charNumInFile;
            int lineLength = 0;
            int numNewLineChars = 0;

            for (;;)
            {
                auto c = t.getAndAdvance();

                if (c == 0)
                {
                    finished = true;
                    break;
                }

                ++charNumInFile;
                ++lineLength;

                if (c == '\r')
                {
                    ++numNewLineChars;

                    if (*t == '\n')
                    {
                        ++t;
                        ++charNumInFile;
                        ++lineLength;
                        ++numNewLineChars;
                    }

                    break;
                }

                if (c == '\n')
                {
                    ++numNewLineChars;
                    break;
                }
            }

            newLines.add (new CodeDocumentLine (startOfLine, t, lineLength,
                                                numNewLineChars, startOfLineInFile));
        }

        jassert (charNumInFile == text.length());
    }

    bool endsWithLineBreak() const noexcept
    {
        return lineLengthWithoutNewLines != lineLength;
    }

    void updateLength() noexcept
    {
        lineLength = 0;
        lineLengthWithoutNewLines = 0;

        for (auto t = line.getCharPointer();;)
        {
            auto c = t.getAndAdvance();

            if (c == 0)
                break;

            ++lineLength;

            if (c != '\n' && c != '\r')
                lineLengthWithoutNewLines = lineLength;
        }
    }

    String line;
    int lineStartInFile, lineLength, lineLengthWithoutNewLines;
};

//==============================================================================
CodeDocument::Iterator::Iterator (const CodeDocument& doc) noexcept
    : document (&doc)
{}

CodeDocument::Iterator::Iterator (CodeDocument::Position p) noexcept
    : document (p.owner),
      line (p.getLineNumber()),
      position (p.getPosition())
{
    reinitialiseCharPtr();

    for (int i = 0; i < p.getIndexInLine(); ++i)
    {
        charPointer.getAndAdvance();

        if (charPointer.isEmpty())
        {
            position -= (p.getIndexInLine() - i);
            break;
        }
    }
}

CodeDocument::Iterator::Iterator() noexcept
    : document (nullptr)
{
}

CodeDocument::Iterator::~Iterator() noexcept {}


bool CodeDocument::Iterator::reinitialiseCharPtr() const
{
    /** You're trying to use a default constructed iterator. Bad idea! */
    jassert (document != nullptr);

    if (charPointer.getAddress() == nullptr)
    {
        if (auto* l = document->lines[line])
            charPointer = l->line.getCharPointer();
        else
            return false;
    }

    return true;
}

juce_wchar CodeDocument::Iterator::nextChar() noexcept
{
    for (;;)
    {
        if (! reinitialiseCharPtr())
            return 0;

        if (auto result = charPointer.getAndAdvance())
        {
            if (charPointer.isEmpty())
            {
                ++line;
                charPointer = nullptr;
            }

            ++position;
            return result;
        }

        ++line;
        charPointer = nullptr;
    }
}

void CodeDocument::Iterator::skip() noexcept
{
    nextChar();
}

void CodeDocument::Iterator::skipToEndOfLine() noexcept
{
    if (! reinitialiseCharPtr())
        return;

    position += (int) charPointer.length();
    ++line;
    charPointer = nullptr;
}

void CodeDocument::Iterator::skipToStartOfLine() noexcept
{
    if (! reinitialiseCharPtr())
        return;

    if (auto* l = document->lines [line])
    {
        auto startPtr = l->line.getCharPointer();
        position -= (int) startPtr.lengthUpTo (charPointer);
        charPointer = startPtr;
    }
}

juce_wchar CodeDocument::Iterator::peekNextChar() const noexcept
{
    if (! reinitialiseCharPtr())
        return 0;

    if (auto c = *charPointer)
        return c;

    if (auto* l = document->lines [line + 1])
        return l->line[0];

    return 0;
}

juce_wchar CodeDocument::Iterator::previousChar() noexcept
{
    if (! reinitialiseCharPtr())
        return 0;

    for (;;)
    {
        if (auto* l = document->lines[line])
        {
            if (charPointer != l->line.getCharPointer())
            {
                --position;
                --charPointer;
                break;
            }
        }

        if (line == 0)
            return 0;

        --line;

        if (auto* prev = document->lines[line])
            charPointer = prev->line.getCharPointer().findTerminatingNull();
    }

    return *charPointer;
}

juce_wchar CodeDocument::Iterator::peekPreviousChar() const noexcept
{
    if (! reinitialiseCharPtr())
        return 0;

    if (auto* l = document->lines[line])
    {
        if (charPointer != l->line.getCharPointer())
            return *(charPointer - 1);

        if (auto* prev = document->lines[line - 1])
            return *(prev->line.getCharPointer().findTerminatingNull() - 1);
    }

    return 0;
}

void CodeDocument::Iterator::skipWhitespace() noexcept
{
    while (CharacterFunctions::isWhitespace (peekNextChar()))
        skip();
}

bool CodeDocument::Iterator::isEOF() const noexcept
{
    return charPointer.getAddress() == nullptr && line >= document->lines.size();
}

bool CodeDocument::Iterator::isSOF() const noexcept
{
    return position == 0;
}

CodeDocument::Position CodeDocument::Iterator::toPosition() const
{
    if (auto* l = document->lines[line])
    {
        reinitialiseCharPtr();
        int indexInLine = 0;
        auto linePtr = l->line.getCharPointer();

        while (linePtr != charPointer && ! linePtr.isEmpty())
        {
            ++indexInLine;
            ++linePtr;
        }

        return CodeDocument::Position (*document, line, indexInLine);
    }

    if (isEOF())
    {
        if (auto* last = document->lines.getLast())
        {
            auto lineIndex = document->lines.size() - 1;
            return CodeDocument::Position (*document, lineIndex, last->lineLength);
        }
    }

    return CodeDocument::Position (*document, 0, 0);
}

//==============================================================================
CodeDocument::Position::Position() noexcept
{
}

CodeDocument::Position::Position (const CodeDocument& ownerDocument,
                                  const int lineNum, const int index) noexcept
    : owner (const_cast<CodeDocument*> (&ownerDocument)),
      line (lineNum), indexInLine (index)
{
    setLineAndIndex (lineNum, index);
}

CodeDocument::Position::Position (const CodeDocument& ownerDocument, int pos) noexcept
    : owner (const_cast<CodeDocument*> (&ownerDocument))
{
    setPosition (pos);
}

CodeDocument::Position::Position (const Position& other) noexcept
    : owner (other.owner), characterPos (other.characterPos), line (other.line),
      indexInLine (other.indexInLine)
{
    jassert (*this == other);
}

CodeDocument::Position::~Position()
{
    setPositionMaintained (false);
}

CodeDocument::Position& CodeDocument::Position::operator= (const Position& other)
{
    if (this != &other)
    {
        const bool wasPositionMaintained = positionMaintained;
        if (owner != other.owner)
            setPositionMaintained (false);

        owner = other.owner;
        line = other.line;
        indexInLine = other.indexInLine;
        characterPos = other.characterPos;
        setPositionMaintained (wasPositionMaintained);

        jassert (*this == other);
    }

    return *this;
}

bool CodeDocument::Position::operator== (const Position& other) const noexcept
{
    jassert ((characterPos == other.characterPos)
               == (line == other.line && indexInLine == other.indexInLine));

    return characterPos == other.characterPos
            && line == other.line
            && indexInLine == other.indexInLine
            && owner == other.owner;
}

bool CodeDocument::Position::operator!= (const Position& other) const noexcept
{
    return ! operator== (other);
}

void CodeDocument::Position::setLineAndIndex (const int newLineNum, const int newIndexInLine)
{
    jassert (owner != nullptr);

    if (owner->lines.size() == 0)
    {
        line = 0;
        indexInLine = 0;
        characterPos = 0;
    }
    else
    {
        if (newLineNum >= owner->lines.size())
        {
            line = owner->lines.size() - 1;

            auto& l = *owner->lines.getUnchecked (line);
            indexInLine = l.lineLengthWithoutNewLines;
            characterPos = l.lineStartInFile + indexInLine;
        }
        else
        {
            line = jmax (0, newLineNum);

            auto& l = *owner->lines.getUnchecked (line);

            if (l.lineLengthWithoutNewLines > 0)
                indexInLine = jlimit (0, l.lineLengthWithoutNewLines, newIndexInLine);
            else
                indexInLine = 0;

            characterPos = l.lineStartInFile + indexInLine;
        }
    }
}

void CodeDocument::Position::setPosition (const int newPosition)
{
    jassert (owner != nullptr);

    line = 0;
    indexInLine = 0;
    characterPos = 0;

    if (newPosition > 0)
    {
        int lineStart = 0;
        auto lineEnd = owner->lines.size();

        for (;;)
        {
            if (lineEnd - lineStart < 4)
            {
                for (int i = lineStart; i < lineEnd; ++i)
                {
                    auto& l = *owner->lines.getUnchecked (i);
                    auto index = newPosition - l.lineStartInFile;

                    if (index >= 0 && (index < l.lineLength || i == lineEnd - 1))
                    {
                        line = i;
                        indexInLine = jmin (l.lineLengthWithoutNewLines, index);
                        characterPos = l.lineStartInFile + indexInLine;
                    }
                }

                break;
            }
            else
            {
                auto midIndex = (lineStart + lineEnd + 1) / 2;

                if (newPosition >= owner->lines.getUnchecked (midIndex)->lineStartInFile)
                    lineStart = midIndex;
                else
                    lineEnd = midIndex;
            }
        }
    }
}

void CodeDocument::Position::moveBy (int characterDelta)
{
    jassert (owner != nullptr);

    if (characterDelta == 1)
    {
        setPosition (getPosition());

        // If moving right, make sure we don't get stuck between the \r and \n characters..
        if (line < owner->lines.size())
        {
            auto& l = *owner->lines.getUnchecked (line);

            if (indexInLine + characterDelta < l.lineLength
                 && indexInLine + characterDelta >= l.lineLengthWithoutNewLines + 1)
                ++characterDelta;
        }
    }

    setPosition (characterPos + characterDelta);
}

CodeDocument::Position CodeDocument::Position::movedBy (const int characterDelta) const
{
    CodeDocument::Position p (*this);
    p.moveBy (characterDelta);
    return p;
}

CodeDocument::Position CodeDocument::Position::movedByLines (const int deltaLines) const
{
    CodeDocument::Position p (*this);
    p.setLineAndIndex (getLineNumber() + deltaLines, getIndexInLine());
    return p;
}

juce_wchar CodeDocument::Position::getCharacter() const
{
    if (auto* l = owner->lines [line])
        return l->line [getIndexInLine()];

    return 0;
}

String CodeDocument::Position::getLineText() const
{
    if (auto* l = owner->lines [line])
        return l->line;

    return {};
}

void CodeDocument::Position::setPositionMaintained (const bool isMaintained)
{
    if (isMaintained != positionMaintained)
    {
        positionMaintained = isMaintained;

        if (owner != nullptr)
        {
            if (isMaintained)
            {
                jassert (! owner->positionsToMaintain.contains (this));
                owner->positionsToMaintain.add (this);
            }
            else
            {
                // If this happens, you may have deleted the document while there are Position objects that are still using it...
                jassert (owner->positionsToMaintain.contains (this));
                owner->positionsToMaintain.removeFirstMatchingValue (this);
            }
        }
    }
}

//==============================================================================
CodeDocument::CodeDocument() : undoManager (std::numeric_limits<int>::max(), 10000)
{
}

CodeDocument::~CodeDocument()
{
}

String CodeDocument::getAllContent() const
{
    return getTextBetween (Position (*this, 0),
                           Position (*this, lines.size(), 0));
}

String CodeDocument::getTextBetween (const Position& start, const Position& end) const
{
    if (end.getPosition() <= start.getPosition())
        return {};

    auto startLine = start.getLineNumber();
    auto endLine = end.getLineNumber();

    if (startLine == endLine)
    {
        if (auto* line = lines [startLine])
            return line->line.substring (start.getIndexInLine(), end.getIndexInLine());

        return {};
    }

    MemoryOutputStream mo;
    mo.preallocate ((size_t) (end.getPosition() - start.getPosition() + 4));

    auto maxLine = jmin (lines.size() - 1, endLine);

    for (int i = jmax (0, startLine); i <= maxLine; ++i)
    {
        auto& line = *lines.getUnchecked (i);
        auto len = line.lineLength;

        if (i == startLine)
        {
            auto index = start.getIndexInLine();
            mo << line.line.substring (index, len);
        }
        else if (i == endLine)
        {
            len = end.getIndexInLine();
            mo << line.line.substring (0, len);
        }
        else
        {
            mo << line.line;
        }
    }

    return mo.toUTF8();
}

int CodeDocument::getNumCharacters() const noexcept
{
    if (auto* lastLine = lines.getLast())
        return lastLine->lineStartInFile + lastLine->lineLength;

    return 0;
}

String CodeDocument::getLine (const int lineIndex) const noexcept
{
    if (auto* line = lines[lineIndex])
        return line->line;

    return {};
}

int CodeDocument::getMaximumLineLength() noexcept
{
    if (maximumLineLength < 0)
    {
        maximumLineLength = 0;

        for (auto* l : lines)
            maximumLineLength = jmax (maximumLineLength, l->lineLength);
    }

    return maximumLineLength;
}

void CodeDocument::deleteSection (const Position& startPosition, const Position& endPosition)
{
    deleteSection (startPosition.getPosition(), endPosition.getPosition());
}

void CodeDocument::deleteSection (const int start, const int end)
{
    remove (start, end, true);
}

void CodeDocument::insertText (const Position& position, const String& text)
{
    insertText (position.getPosition(), text);
}

void CodeDocument::insertText (const int insertIndex, const String& text)
{
    insert (text, insertIndex, true);
}

void CodeDocument::replaceSection (const int start, const int end, const String& newText)
{
    insertText (end, newText);
    deleteSection (start, end);
}

void CodeDocument::applyChanges (const String& newContent)
{
    const String corrected (StringArray::fromLines (newContent)
                                .joinIntoString (newLineChars));

    TextDiff diff (getAllContent(), corrected);

    for (auto& c : diff.changes)
    {
        if (c.isDeletion())
            remove (c.start, c.start + c.length, true);
        else
            insert (c.insertedText, c.start, true);
    }
}

void CodeDocument::replaceAllContent (const String& newContent)
{
    remove (0, getNumCharacters(), true);
    insert (newContent, 0, true);
}

bool CodeDocument::loadFromStream (InputStream& stream)
{
    remove (0, getNumCharacters(), false);
    insert (stream.readEntireStreamAsString(), 0, false);
    setSavePoint();
    clearUndoHistory();
    return true;
}

bool CodeDocument::writeToStream (OutputStream& stream)
{
    for (auto* l : lines)
    {
        auto temp = l->line; // use a copy to avoid bloating the memory footprint of the stored string.
        const char* utf8 = temp.toUTF8();

        if (! stream.write (utf8, strlen (utf8)))
            return false;
    }

    return true;
}

void CodeDocument::setNewLineCharacters (const String& newChars) noexcept
{
    jassert (newChars == "\r\n" || newChars == "\n" || newChars == "\r");
    newLineChars = newChars;
}

void CodeDocument::newTransaction()
{
    undoManager.beginNewTransaction (String());
}

void CodeDocument::undo()
{
    newTransaction();
    undoManager.undo();
}

void CodeDocument::redo()
{
    undoManager.redo();
}

void CodeDocument::clearUndoHistory()
{
    undoManager.clearUndoHistory();
}

void CodeDocument::setSavePoint() noexcept
{
    indexOfSavedState = currentActionIndex;
}

bool CodeDocument::hasChangedSinceSavePoint() const noexcept
{
    return currentActionIndex != indexOfSavedState;
}

//==============================================================================
static int getCharacterType (juce_wchar character) noexcept
{
    return (CharacterFunctions::isLetterOrDigit (character) || character == '_')
                ? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1);
}

CodeDocument::Position CodeDocument::findWordBreakAfter (const Position& position) const noexcept
{
    auto p = position;
    const int maxDistance = 256;
    int i = 0;

    while (i < maxDistance
            && CharacterFunctions::isWhitespace (p.getCharacter())
            && (i == 0 || (p.getCharacter() != '\n'
                            && p.getCharacter() != '\r')))
    {
        ++i;
        p.moveBy (1);
    }

    if (i == 0)
    {
        auto type = getCharacterType (p.getCharacter());

        while (i < maxDistance && type == getCharacterType (p.getCharacter()))
        {
            ++i;
            p.moveBy (1);
        }

        while (i < maxDistance
                && CharacterFunctions::isWhitespace (p.getCharacter())
                && (i == 0 || (p.getCharacter() != '\n'
                                && p.getCharacter() != '\r')))
        {
            ++i;
            p.moveBy (1);
        }
    }

    return p;
}

CodeDocument::Position CodeDocument::findWordBreakBefore (const Position& position) const noexcept
{
    auto p = position;
    const int maxDistance = 256;
    int i = 0;
    bool stoppedAtLineStart = false;

    while (i < maxDistance)
    {
        auto c = p.movedBy (-1).getCharacter();

        if (c == '\r' || c == '\n')
        {
            stoppedAtLineStart = true;

            if (i > 0)
                break;
        }

        if (! CharacterFunctions::isWhitespace (c))
            break;

        p.moveBy (-1);
        ++i;
    }

    if (i < maxDistance && ! stoppedAtLineStart)
    {
        auto type = getCharacterType (p.movedBy (-1).getCharacter());

        while (i < maxDistance && type == getCharacterType (p.movedBy (-1).getCharacter()))
        {
            p.moveBy (-1);
            ++i;
        }
    }

    return p;
}

void CodeDocument::findTokenContaining (const Position& pos, Position& start, Position& end) const noexcept
{
    auto isTokenCharacter = [] (juce_wchar c)  { return CharacterFunctions::isLetterOrDigit (c) || c == '.' || c == '_'; };

    end = pos;
    while (isTokenCharacter (end.getCharacter()))
        end.moveBy (1);

    start = end;
    while (start.getIndexInLine() > 0
            && isTokenCharacter (start.movedBy (-1).getCharacter()))
        start.moveBy (-1);
}

void CodeDocument::findLineContaining  (const Position& pos, Position& s, Position& e) const noexcept
{
    s.setLineAndIndex (pos.getLineNumber(), 0);
    e.setLineAndIndex (pos.getLineNumber() + 1, 0);
}

void CodeDocument::checkLastLineStatus()
{
    while (lines.size() > 0
            && lines.getLast()->lineLength == 0
            && (lines.size() == 1 || ! lines.getUnchecked (lines.size() - 2)->endsWithLineBreak()))
    {
        // remove any empty lines at the end if the preceding line doesn't end in a newline.
        lines.removeLast();
    }

    const CodeDocumentLine* const lastLine = lines.getLast();

    if (lastLine != nullptr && lastLine->endsWithLineBreak())
    {
        // check that there's an empty line at the end if the preceding one ends in a newline..
        lines.add (new CodeDocumentLine (StringRef(), StringRef(), 0, 0,
                                         lastLine->lineStartInFile + lastLine->lineLength));
    }
}

//==============================================================================
void CodeDocument::addListener    (CodeDocument::Listener* l)   { listeners.add (l); }
void CodeDocument::removeListener (CodeDocument::Listener* l)   { listeners.remove (l); }

//==============================================================================
struct CodeDocument::InsertAction final : public UndoableAction
{
    InsertAction (CodeDocument& doc, const String& t, const int pos) noexcept
        : owner (doc), text (t), insertPos (pos)
    {
    }

    bool perform() override
    {
        owner.currentActionIndex++;
        owner.insert (text, insertPos, false);
        return true;
    }

    bool undo() override
    {
        owner.currentActionIndex--;
        owner.remove (insertPos, insertPos + text.length(), false);
        return true;
    }

    int getSizeInUnits() override        { return text.length() + 32; }

    CodeDocument& owner;
    const String text;
    const int insertPos;

    JUCE_DECLARE_NON_COPYABLE (InsertAction)
};

void CodeDocument::insert (const String& text, const int insertPos, const bool undoable)
{
    if (text.isNotEmpty())
    {
        if (undoable)
        {
            undoManager.perform (new InsertAction (*this, text, insertPos));
        }
        else
        {
            Position pos (*this, insertPos);
            auto firstAffectedLine = pos.getLineNumber();

            auto* firstLine = lines[firstAffectedLine];
            auto textInsideOriginalLine = text;

            if (firstLine != nullptr)
            {
                auto index = pos.getIndexInLine();
                textInsideOriginalLine = firstLine->line.substring (0, index)
                                         + textInsideOriginalLine
                                         + firstLine->line.substring (index);
            }

            maximumLineLength = -1;
            Array<CodeDocumentLine*> newLines;
            CodeDocumentLine::createLines (newLines, textInsideOriginalLine);
            jassert (newLines.size() > 0);

            auto* newFirstLine = newLines.getUnchecked (0);
            newFirstLine->lineStartInFile = firstLine != nullptr ? firstLine->lineStartInFile : 0;
            lines.set (firstAffectedLine, newFirstLine);

            if (newLines.size() > 1)
                lines.insertArray (firstAffectedLine + 1, newLines.getRawDataPointer() + 1, newLines.size() - 1);

            int lineStart = newFirstLine->lineStartInFile;

            for (int i = firstAffectedLine; i < lines.size(); ++i)
            {
                auto& l = *lines.getUnchecked (i);
                l.lineStartInFile = lineStart;
                lineStart += l.lineLength;
            }

            checkLastLineStatus();
            auto newTextLength = text.length();

            for (auto* p : positionsToMaintain)
                if (p->getPosition() >= insertPos)
                    p->setPosition (p->getPosition() + newTextLength);

            listeners.call ([&] (Listener& l) { l.codeDocumentTextInserted (text, insertPos); });
        }
    }
}

//==============================================================================
struct CodeDocument::DeleteAction final : public UndoableAction
{
    DeleteAction (CodeDocument& doc, int start, int end) noexcept
        : owner (doc), startPos (start), endPos (end),
          removedText (doc.getTextBetween (CodeDocument::Position (doc, start),
                                           CodeDocument::Position (doc, end)))
    {
    }

    bool perform() override
    {
        owner.currentActionIndex++;
        owner.remove (startPos, endPos, false);
        return true;
    }

    bool undo() override
    {
        owner.currentActionIndex--;
        owner.insert (removedText, startPos, false);
        return true;
    }

    int getSizeInUnits() override    { return (endPos - startPos) + 32; }

    CodeDocument& owner;
    const int startPos, endPos;
    const String removedText;

    JUCE_DECLARE_NON_COPYABLE (DeleteAction)
};

void CodeDocument::remove (const int startPos, const int endPos, const bool undoable)
{
    if (endPos <= startPos)
        return;

    if (undoable)
    {
        undoManager.perform (new DeleteAction (*this, startPos, endPos));
    }
    else
    {
        Position startPosition (*this, startPos);
        Position endPosition (*this, endPos);

        maximumLineLength = -1;
        auto firstAffectedLine = startPosition.getLineNumber();
        auto endLine = endPosition.getLineNumber();
        auto& firstLine = *lines.getUnchecked (firstAffectedLine);

        if (firstAffectedLine == endLine)
        {
            firstLine.line = firstLine.line.substring (0, startPosition.getIndexInLine())
                           + firstLine.line.substring (endPosition.getIndexInLine());
            firstLine.updateLength();
        }
        else
        {
            auto& lastLine = *lines.getUnchecked (endLine);

            firstLine.line = firstLine.line.substring (0, startPosition.getIndexInLine())
                            + lastLine.line.substring (endPosition.getIndexInLine());
            firstLine.updateLength();

            int numLinesToRemove = endLine - firstAffectedLine;
            lines.removeRange (firstAffectedLine + 1, numLinesToRemove);
        }

        for (int i = firstAffectedLine + 1; i < lines.size(); ++i)
        {
            auto& l = *lines.getUnchecked (i);
            auto& previousLine = *lines.getUnchecked (i - 1);
            l.lineStartInFile = previousLine.lineStartInFile + previousLine.lineLength;
        }

        checkLastLineStatus();
        auto totalChars = getNumCharacters();

        for (auto* p : positionsToMaintain)
        {
            if (p->getPosition() > startPosition.getPosition())
                p->setPosition (jmax (startPos, p->getPosition() + startPos - endPos));

            if (p->getPosition() > totalChars)
                p->setPosition (totalChars);
        }

        listeners.call ([=] (Listener& l) { l.codeDocumentTextDeleted (startPos, endPos); });
    }
}

//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS

struct CodeDocumentTest final : public UnitTest
{
    CodeDocumentTest()
        : UnitTest ("CodeDocument", UnitTestCategories::text)
    {}

    void runTest() override
    {
        const juce::String jabberwocky ("'Twas brillig, and the slithy toves\n"
                                        "Did gyre and gimble in the wabe;\n"
                                        "All mimsy were the borogoves,\n"
                                        "And the mome raths outgrabe.\n\n"

                                        "'Beware the Jabberwock, my son!\n"
                                        "The jaws that bite, the claws that catch!\n"
                                        "Beware the Jubjub bird, and shun\n"
                                        "The frumious Bandersnatch!'");

        {
            beginTest ("Basic checks");
            CodeDocument d;
            d.replaceAllContent (jabberwocky);

            expectEquals (d.getNumLines(), 9);
            expect (d.getLine (0).startsWith ("'Twas brillig"));
            expect (d.getLine (2).startsWith ("All mimsy"));
            expectEquals (d.getLine (4), String ("\n"));
        }

        {
            beginTest ("Insert/replace/delete");

            CodeDocument d;
            d.replaceAllContent (jabberwocky);

            d.insertText (CodeDocument::Position (d, 0, 6), "very ");
            expect (d.getLine (0).startsWith ("'Twas very brillig"),
                    "Insert text within a line");

            d.replaceSection (74, 83, "Quite hungry");
            expectEquals (d.getLine (2), String ("Quite hungry were the borogoves,\n"),
                          "Replace section at start of line");

            d.replaceSection (11, 18, "cold");
            expectEquals (d.getLine (0), String ("'Twas very cold, and the slithy toves\n"),
                          "Replace section within a line");

            d.deleteSection (CodeDocument::Position (d, 2, 0), CodeDocument::Position (d, 2, 6));
            expectEquals (d.getLine (2), String ("hungry were the borogoves,\n"),
                          "Delete section within a line");

            d.deleteSection (CodeDocument::Position (d, 2, 6), CodeDocument::Position (d, 5, 11));
            expectEquals (d.getLine (2), String ("hungry Jabberwock, my son!\n"),
                          "Delete section across multiple line");
        }

        {
            beginTest ("Line splitting and joining");

            CodeDocument d;
            d.replaceAllContent (jabberwocky);
            expectEquals (d.getNumLines(), 9);

            const String splitComment ("Adding a newline should split a line into two.");
            d.insertText (49, "\n");

            expectEquals (d.getNumLines(), 10, splitComment);
            expectEquals (d.getLine (1), String ("Did gyre and \n"), splitComment);
            expectEquals (d.getLine (2), String ("gimble in the wabe;\n"), splitComment);

            const String joinComment ("Removing a newline should join two lines.");
            d.deleteSection (CodeDocument::Position (d, 0, 35),
                             CodeDocument::Position (d, 1, 0));

            expectEquals (d.getNumLines(), 9, joinComment);
            expectEquals (d.getLine (0), String ("'Twas brillig, and the slithy tovesDid gyre and \n"), joinComment);
            expectEquals (d.getLine (1), String ("gimble in the wabe;\n"), joinComment);
        }

        {
            beginTest ("Undo/redo");

            CodeDocument d;
            d.replaceAllContent (jabberwocky);
            d.newTransaction();
            d.insertText (30, "INSERT1");
            d.newTransaction();
            d.insertText (70, "INSERT2");
            d.undo();

            expect (d.getAllContent().contains ("INSERT1"), "1st edit should remain.");
            expect (! d.getAllContent().contains ("INSERT2"), "2nd edit should be undone.");

            d.redo();
            expect (d.getAllContent().contains ("INSERT2"), "2nd edit should be redone.");

            d.newTransaction();
            d.deleteSection (25, 90);
            expect (! d.getAllContent().contains ("INSERT1"), "1st edit should be deleted.");
            expect (! d.getAllContent().contains ("INSERT2"), "2nd edit should be deleted.");
            d.undo();
            expect (d.getAllContent().contains ("INSERT1"), "1st edit should be restored.");
            expect (d.getAllContent().contains ("INSERT2"), "1st edit should be restored.");

            d.undo();
            d.undo();
            expectEquals (d.getAllContent(), jabberwocky, "Original document should be restored.");
        }

        {
            beginTest ("Positions");

            CodeDocument d;
            d.replaceAllContent (jabberwocky);

            {
                const String comment ("Keeps negative positions inside document.");
                CodeDocument::Position p1 (d, 0, -3);
                CodeDocument::Position p2 (d, -8);
                expectEquals (p1.getLineNumber(), 0, comment);
                expectEquals (p1.getIndexInLine(), 0, comment);
                expectEquals (p1.getCharacter(), juce_wchar ('\''), comment);
                expectEquals (p2.getLineNumber(), 0, comment);
                expectEquals (p2.getIndexInLine(), 0, comment);
                expectEquals (p2.getCharacter(), juce_wchar ('\''), comment);
            }

            {
                const String comment ("Moving by character handles newlines correctly.");
                CodeDocument::Position p1 (d, 0, 35);
                p1.moveBy (1);
                expectEquals (p1.getLineNumber(), 1, comment);
                expectEquals (p1.getIndexInLine(), 0, comment);
                p1.moveBy (75);
                expectEquals (p1.getLineNumber(), 3, comment);
            }

            {
                const String comment1 ("setPositionMaintained tracks position.");
                const String comment2 ("setPositionMaintained tracks position following undos.");

                CodeDocument::Position p1 (d, 3, 0);
                p1.setPositionMaintained (true);
                expectEquals (p1.getCharacter(), juce_wchar ('A'), comment1);

                d.newTransaction();
                d.insertText (p1, "INSERT1");

                expectEquals (p1.getCharacter(), juce_wchar ('A'), comment1);
                expectEquals (p1.getLineNumber(), 3, comment1);
                expectEquals (p1.getIndexInLine(), 7, comment1);
                d.undo();
                expectEquals (p1.getIndexInLine(), 0, comment2);

                d.newTransaction();
                d.insertText (15, "\n");

                expectEquals (p1.getLineNumber(), 4, comment1);
                d.undo();
                expectEquals (p1.getLineNumber(), 3, comment2);
            }
        }

        {
            beginTest ("Iterators");

            CodeDocument d;
            d.replaceAllContent (jabberwocky);

            {
                const String comment1 ("Basic iteration.");
                const String comment2 ("Reverse iteration.");
                const String comment3 ("Reverse iteration stops at doc start.");
                const String comment4 ("Check iteration across line boundaries.");

                CodeDocument::Iterator it (d);
                expectEquals (it.peekNextChar(), juce_wchar ('\''), comment1);
                expectEquals (it.nextChar(), juce_wchar ('\''), comment1);
                expectEquals (it.nextChar(), juce_wchar ('T'), comment1);
                expectEquals (it.nextChar(), juce_wchar ('w'), comment1);
                expectEquals (it.peekNextChar(), juce_wchar ('a'), comment2);
                expectEquals (it.previousChar(), juce_wchar ('w'), comment2);
                expectEquals (it.previousChar(), juce_wchar ('T'), comment2);
                expectEquals (it.previousChar(), juce_wchar ('\''), comment2);
                expectEquals (it.previousChar(), juce_wchar (0), comment3);
                expect (it.isSOF(), comment3);

                while (it.peekNextChar() != juce_wchar ('D')) // "Did gyre..."
                    it.nextChar();

                expectEquals (it.nextChar(), juce_wchar ('D'), comment3);
                expectEquals (it.peekNextChar(), juce_wchar ('i'), comment3);
                expectEquals (it.previousChar(), juce_wchar ('D'), comment3);
                expectEquals (it.previousChar(), juce_wchar ('\n'), comment3);
                expectEquals (it.previousChar(), juce_wchar ('s'), comment3);
            }

            {
                const String comment1 ("Iterator created from CodeDocument::Position objects.");
                const String comment2 ("CodeDocument::Position created from Iterator objects.");
                const String comment3 ("CodeDocument::Position created from EOF Iterator objects.");

                CodeDocument::Position p (d, 6, 0); // "The jaws..."
                CodeDocument::Iterator it (p);

                expectEquals (it.nextChar(), juce_wchar ('T'), comment1);
                expectEquals (it.nextChar(), juce_wchar ('h'), comment1);
                expectEquals (it.previousChar(), juce_wchar ('h'), comment1);
                expectEquals (it.previousChar(), juce_wchar ('T'), comment1);
                expectEquals (it.previousChar(), juce_wchar ('\n'), comment1);
                expectEquals (it.previousChar(), juce_wchar ('!'), comment1);

                const auto p2 = it.toPosition();
                expectEquals (p2.getLineNumber(), 5, comment2);
                expectEquals (p2.getIndexInLine(), 30, comment2);

                while (! it.isEOF())
                    it.nextChar();

                const auto p3 = it.toPosition();
                expectEquals (p3.getLineNumber(), d.getNumLines() - 1, comment3);
                expectEquals (p3.getIndexInLine(), d.getLine (d.getNumLines() - 1).length(), comment3);
            }
        }
    }
};

static CodeDocumentTest codeDocumentTests;

#endif

} // namespace juce
