// edbee - Copyright (c) 2012-2025 by Rick Blommers and contributors
// SPDX-License-Identifier: MIT

#include "textrange.h"

#include <QStringList>

#include "textdocument.h"
#include "texteditorconfig.h"
#include "edbee/debug.h"

namespace edbee {

/// Compares selection ranges
bool TextRange::lessThan(TextRange &r1, TextRange &r2)
{
    return r1.min() < r2.min();
}


/// Makes sure the caret isn't in-between a unicode boundary
/// refs #19 - Dirty hack to improve caret-movement by skipping non-BMP characters
void TextRange::fixCaretForUnicode(TextDocument *doc, ptrdiff_t direction)
{
    if (caret_ >= doc->length()) return;

    // unicode-emoji hack (Really really dirty!!)
    int code = doc->charAt(caret_).unicode();
    if (0xDC00 <= code && code <= 0xDFFF) {
        if (direction > 0) {
            setCaretBounded(doc, caret_ + 1);
        } else {
            setCaretBounded(doc, caret_ - 1);
        }
    }
}


/// Sets the anchor to the given location, and forces the anchor to say between the document bounds
/// @param doc document to set the anchor for
/// @param anchor the anchor location to set
void TextRange::setAnchorBounded(TextDocument* doc, size_t anchor)
{
    setAnchor(qBound(static_cast<size_t>(0), anchor, doc->length()));
}


/// Sets the caret to the given location, and forces the caret to say between the document bounds
/// @param doc the document (used for checking the document bounds)
/// @param caret the caret position to set
void TextRange::setAnchorBounded(TextDocument *doc, ptrdiff_t anchor)
{
    setAnchor(static_cast<size_t>(qBound(static_cast<ptrdiff_t>(0), anchor, static_cast<ptrdiff_t>(doc->length()))));
}


/// Sets the caret to the given location, and forces the caret to say between the document bounds
/// @param doc the document (used for checking the document bounds)
/// @param caret the caret position to set
void TextRange::setCaretBounded(TextDocument* doc, size_t caret)
{
    setCaret(qBound(static_cast<size_t>(0), caret, doc->length()));
}


/// Sets the caret to the given location, and forces the caret to say between the document bounds
/// @param doc the document (used for checking the document bounds)
/// @param caret the caret position to set
void TextRange::setCaretBounded(TextDocument *doc, ptrdiff_t caret)
{
    setCaret(static_cast<size_t>(qBound(static_cast<ptrdiff_t>(0), caret, static_cast<ptrdiff_t>(doc->length()))));
}


/// Changes the length by modifying the max-variable
void TextRange::setLength(size_t newLength)
{
    size_t& vMin = minVar();
    size_t& vMax = maxVar();
    vMax = vMin + newLength;
}


/// This method converts a text-selection range to a string helpfull with debuggin
QString TextRange::toString() const
{
    return QStringLiteral("%1>%2").arg(anchor_).arg(caret_);
}


/// Moves the caret the given amount
/// @param doc the document to move the caret for
/// @param amount the amount to move
void TextRange::moveCaret(TextDocument* doc, ptrdiff_t amount)
{
    setCaretBounded(doc, static_cast<ptrdiff_t>(caret_) + amount);
    fixCaretForUnicode(doc, amount);
}


/// move the caret or deselect the given amount
/// @param doc the document to move the caret in
/// @param amount the amount to move
void TextRange::moveCaretOrDeselect(TextDocument* doc, ptrdiff_t amount)
{
    // when there's a selection clear it (move the caret to the right side
    if (hasSelection()) {
        if (amount < 0) {
            setCaret(min());
        } else {
            setCaret(max());
        }
        setAnchor(caret());

    // just moves the caret
    } else {
        setCaretBounded(doc, static_cast<ptrdiff_t>(caret_) + amount);
    }
    fixCaretForUnicode(doc, amount);
}


/// This method charactes after the given char group
/// When moving to the left the cursor is placed AFTER the last character
/// @param doc the text document
/// @param var the initial position
/// @param amount the amount to move
/// returns the new position
size_t TextRange::moveWhileChar(TextDocument* doc, size_t pos, ptrdiff_t amount, const QString& chars)
{
    size_t docLength = doc->length();
    if (amount < 0) {
        --pos;   // first move left
        while (pos != std::string::npos && chars.indexOf(doc->charAt(pos)) >= 0) {
            --pos;
        }
        ++pos;
    } else {
        while(pos < docLength && chars.indexOf(doc->charAt(pos)) >= 0) {
            ++pos;
        }
    }
    return pos;
}


/// This method charactes until the given chargroup is found
/// When moving to the LEFT the cursor is placed AFTER the found character
size_t TextRange::moveUntilChar(TextDocument* doc, size_t pos, ptrdiff_t amount, const QString& chars)
{
    size_t docLength = doc->length();
    if (amount < 0) {
        --pos;
        while (pos != std::string::npos && chars.indexOf(doc->charAt(pos)) == -1) {
            --pos;
        }
        ++pos;
    } else {
        while (pos < docLength && chars.indexOf(doc->charAt(pos)) == -1) {
            ++pos;
        }
    }
    return pos;
}


/// moves the caret while a character is moving
void TextRange::moveCaretWhileChar(TextDocument* doc, ptrdiff_t amount, const QString& chars)
{
    caret_ = moveWhileChar(doc, caret_, amount, chars);
}


void TextRange::moveCaretUntilChar(TextDocument *doc, ptrdiff_t amount, const QString& chars)
{
    caret_ = moveUntilChar(doc, caret_, amount, chars);
}


void TextRange::moveAnchortWhileChar(TextDocument *doc, ptrdiff_t amount, const QString &chars)
{
    anchor_ = moveWhileChar(doc, anchor_, amount, chars);
}


void TextRange::moveAnchorUntilChar(TextDocument *doc, ptrdiff_t amount, const QString &chars)
{
    anchor_ = moveWhileChar(doc, anchor_, amount, chars);
}


/// Moves the caret to/from the given seperator
void TextRange::moveCaretByCharGroup(TextDocument *doc, ptrdiff_t amount, const QString& whitespace, const QStringList& characterGroups)
{
    size_t count = static_cast<size_t>(qAbs(amount));

    for (size_t i = 0; i < count; ++i) {

        // first 'skip' the whitespaces
        size_t oldCaret = caret_;
        moveCaretWhileChar(doc, amount, whitespace);

        // find the character group
        QChar chr;
        if (amount < 0) {
            if( caret_ == 0 ) return;
            chr = doc->charAt(caret_ - 1);
        } else {
            if (caret_ == doc->length()) return;
            chr = doc->charAt(caret_);
        }

        // newline is a special operation :(
        if (chr == '\n') {
            if (caret_ == oldCaret) {
                if (amount < 0) {
                    caret_ -= 1;
                    Q_ASSERT(caret_ != std::string::npos);
                } else {
                    caret_ += 1;

                }
            }
        } else {
            // while the character is found
            bool found = false;
            for (qsizetype i=0, cnt = characterGroups.length(); i < cnt; ++i) {
                const QString& group = characterGroups.at(i);
                if (group.indexOf(chr) >= 0) {
                    moveCaretWhileChar(doc, amount, group);
                    found = true;
                    break;
                }
            }

            // all other characters are valid
            if (!found) {
                QString str = characterGroups.join("");
                str.append(whitespace);
                str.append('\n');
                moveCaretUntilChar(doc, amount, str);
            }
        }
    }
}


/// Moves the caret to a line boundary
/// @param doc the document this range operates on
/// @param amount the direction to move to
/// @par    am whitespace the characters that need to be interpreted as whitespace
void TextRange::moveCaretToLineBoundary(TextDocument* doc, ptrdiff_t amount, const QString& whitespace)
{
    TextBuffer* buf     = doc->buffer();
    size_t caret           = caret_;
    size_t line            = doc->lineFromOffset(caret);
    size_t offsetNextLine  = doc->offsetFromLine(line + 1);
    if (amount < 0) {
        size_t lineStart = doc->offsetFromLine(line);

        // find the first word
        size_t wordStart = buf->findCharPosWithinRangeOrClamp(lineStart, 1u, whitespace, false, lineStart, offsetNextLine);
        if (caret > wordStart || lineStart ==  caret) {
            caret = wordStart;
        } else {
            caret = lineStart;
        }
    } else {
        caret = offsetNextLine;
        if (line != doc->lineCount() - 1) {
            --caret;
        }
    }
    setCaretBounded(doc, caret);
}


/// Moves the caret to a word boundary  (used for word dragging selections)
void TextRange::moveCaretToWordBoundaryAtOffset(TextDocument *doc, size_t newOffset)
{
    TextEditorConfig* config = doc->config();

    // left direction
    if (newOffset < anchor()) {
        setAnchor(max());
        setCaret(newOffset);
        moveCaretByCharGroup(doc, -1, config->whitespaces(), config->charGroups());
    // right direction
    } else {
        setCaret(newOffset);
        moveCaretByCharGroup(doc, 1, config->whitespaces(), config->charGroups());
    }
}


/// Moves the caret to a word boundary  (used for word dragging selections)
void TextRange::moveCaretToLineBoundaryAtOffset(TextDocument *doc, size_t newOffset)
{
    size_t firstLine = doc->lineFromOffset(min());
    size_t lastLine = doc->lineFromOffset(max());

    // changed offset
    setCaret(newOffset);

    size_t newLine = doc->lineFromOffset(newOffset);

    // left direction
    if( newLine < lastLine ) {
        this->caret_ = doc->offsetFromLine(newLine);
        this->anchor_ = doc->offsetFromLine(lastLine);
    // right direction
    } else if( newLine > firstLine) {
        this->anchor_ = doc->offsetFromLine(firstLine);
        this->caret_ = doc->offsetFromLine(newLine+1);
    }
}


/// Expands the selection range so it only consists of full lines
/// amount specifies the amount (and the direction) of the expansions
/// -1 means expand lines to top (and add extra lines)
/// 1 means expand lines at the bottom (and add extras lines)
/// 0 is a special case, it moves the caret to the start of the current line and expands to the end of the line. It does not add lines
void TextRange::expandToFullLine(TextDocument* doc, ptrdiff_t amount)
{
    size_t minOffset = min();
    size_t maxOffset = max();

    // select the current line (everse caret
    if (amount == 0) {
        minOffset = doc->offsetFromLine(doc->lineFromOffset(minOffset));
        maxOffset = doc->offsetFromLine(doc->lineFromOffset(maxOffset) + 1);
        caret_ = minOffset;
        anchor_ = maxOffset;
    } else if( amount > 0 ) {
        minOffset = doc->offsetFromLine(doc->lineFromOffset(minOffset));
        maxOffset = doc->offsetFromLine(doc->lineFromOffset(maxOffset) + static_cast<size_t>(amount));

        caret_ = maxOffset;
        anchor_ = minOffset;
    } else {
        size_t minLine = doc->lineFromOffset(minOffset);
        size_t minLineStartOffset = doc->offsetFromLine(minLine);

        // only select line above if the full line isn't selected yet
        if (minOffset == minLineStartOffset) {
            if (minOffset > 0) { --minOffset; }
        }
        ++amount;
        minOffset = doc->offsetFromLine(doc->lineFromOffset(minOffset) - static_cast<size_t>(qAbs(amount)));

        // select to eol if required
        size_t maxLine = doc->lineFromOffset(maxOffset);
        size_t maxLineStartOffset = doc->offsetFromLine(maxLine);
        if (maxLineStartOffset != maxOffset) {
            maxOffset = doc->offsetFromLine(doc->lineFromOffset(maxOffset) + 1);
        }

        caret_ = minOffset;
        anchor_ = maxOffset;
    }
}


/// Deselects the last character if it's a newline.
void TextRange::deselectTrailingNewLine(TextDocument* doc)
{
    if (caret_ != anchor_ && doc->charAtOrNull(max() - 1) == '\n') {
        --maxVar();
    }
}


/// Internal method, for finding a character group with the given character
static QString findCharGroup(QChar c, const QString& whitespace, const QStringList& characterGroups)
{
    QString foundGroup;
    if (whitespace.indexOf(c) >= 0) {
        foundGroup = whitespace;
    } else {
        for (qsizetype i = 0, cnt = static_cast<qsizetype>(characterGroups.length()); i < cnt; ++i) {
            if (characterGroups.at(i).indexOf(c) >= 0) {
                foundGroup = characterGroups.at(i);
                break;
            }
        }
    }
    return foundGroup;
}


/// Expands the selection to a words
void TextRange::expandToWord(TextDocument *doc, const QString& whitespace, const QStringList& characterGroups)
{
    size_t& min = minVar();
    size_t& max = maxVar();

    // first check which character is under the caret to find the character grouop
    if (min > 0) {
        QString group = findCharGroup(doc->charAt(min - 1), whitespace, characterGroups);
        if (group.isEmpty()) {
            group = characterGroups.join("").append(whitespace);     // the 'else' group
            min = moveUntilChar(doc, min, -1, group);
        } else {
            min = moveWhileChar(doc, min, -1, group);
        }
    }

    if (max < doc->length()) {
        QString group = findCharGroup(doc->charAt(max), whitespace, characterGroups);
        if (group.isEmpty()) {
            group = characterGroups.join("").append(whitespace);     // the 'else' group
            max = moveUntilChar(doc, max, 1, group);
        } else {
            max = moveWhileChar(doc, max, 1, group);
        }
    }
}


void TextRange::expandToIncludeRange(TextRange& range)
{
    size_t& min = minVar();
    size_t& max = maxVar();
    min = qMin(min, range.min());
    max = qMax(max, range.max());
}


/// Sets the bounds
void TextRange::forceBounds(TextDocument *doc)
{
    setAnchorBounded(doc, anchor_);
    setCaretBounded(doc, caret_ );
}


/// This method checks if the two ranges are equal
/// @param range the range to compare it to
bool TextRange::equals(const TextRange& range)
{
    return range.caret_ == caret_ && range.anchor_ == anchor_;
}


/// Checks if two ranges touch eachother. Touching means that the start and end of two ranges are onto eachother
/// Possible situations ( [ = anchor, > = caret, ( = don't care )
///  (A)(B) or (B)(A)
bool TextRange::touches(TextRange& range)
{
    size_t min1 = min();
    size_t max1 = max();
    size_t min2 = range.min();
    size_t max2 = range.max();
    return (max1 == min2 || max2 == min1);
}


/// checks if the given position is in this textrange
bool TextRange::contains(size_t pos)
{
    return min() <= pos && pos < max();
}

/// some extra validations to assert non negative values
void TextRange::assertValid() const
{
    Q_ASSERT(caret_ < std::string::npos - length());
    Q_ASSERT(anchor_ < std::string::npos - length());
}


//=========================================================================


TextRangeSetBase::TextRangeSetBase(TextDocument* doc)
    : textDocumentRef_(doc)
    , changing_(0)
{
}


/// this method returns the last range
TextRange &TextRangeSetBase::lastRange()
{
    return range(rangeCount() - 1);
}


/// the first range
TextRange &TextRangeSetBase::firstRange()
{
    return range(0);
}


/// Returns the range index at the given offset
/// @param offset the offset to check
/// @return the offset index or (npos) if not found
size_t TextRangeSetBase::rangeIndexAtOffset(size_t offset)
{
    // find the range of this offset
    for (size_t i = 0, cnt = rangeCount(); i < cnt; ++i) {
        TextRange& found = this->range(i);
        size_t minOffset = found.min();
        size_t maxOffset = found.max();
        if (minOffset <= offset && offset <= maxOffset) {
            return i;
        }
    }
    return std::string::npos;
}


/// returns the range indices that are being overlapped by the given offsetBegin and offsetEnd
/// @param offsetBegin the offset to search
/// @param offsetEnd the end-offset to search
/// @param firstIndex(out) The first index found (npos if not found)
/// @param lastIndex(out) The last index found (npos if not found)
/// @return true if the range is found
bool TextRangeSetBase::rangesBetweenOffsets(size_t offsetBegin, size_t offsetEnd, size_t& firstIndex, size_t& lastIndex)
{
    firstIndex = std::string::npos;
    lastIndex  = std::string::npos;

    /// Todo optimize with a binairy search
    for (size_t i = 0, cnt = rangeCount(); i < cnt; ++i) {
        TextRange& range = this->range(i);
        size_t minOffset = range.min();
        size_t maxOffset = range.max();

        if ((offsetBegin <= minOffset && minOffset <= offsetEnd) || (minOffset <= offsetBegin && offsetBegin <= maxOffset)) {
            if (firstIndex == std::string::npos) firstIndex = i;
            lastIndex = i;
        }
    }

    return firstIndex != std::string::npos;
}


/// returns the range indices that are being overlapped by the given offsetBegin and offsetEnd
/// @param offsetBegin the offset to search
/// @param offsetEnd the end-offset to search
/// @param firstIndex(out) The first index found (npos if not found)
/// @param lastIndex(out) The last index found (npos if not found)
/// @return true if the range is found (firstIndex and lastIndex are filled
bool TextRangeSetBase::rangesBetweenOffsetsExlusiveEnd(size_t offsetBegin, size_t offsetEnd, size_t &firstIndex, size_t &lastIndex)
{
    firstIndex = std::string::npos;
    lastIndex  = std::string::npos;
    /// Todo optimize with a binairy search
    for (size_t i = 0, cnt = rangeCount(); i < cnt; ++i) {
        TextRange& range = this->range(i);
        size_t minOffset = range.min();
        size_t maxOffset = range.max();

        if ((offsetBegin <= minOffset && minOffset < offsetEnd) || (minOffset <= offsetBegin && offsetBegin < maxOffset)) {
            if (firstIndex == std::string::npos) firstIndex = i;
            lastIndex = i;
        }
    }
    return firstIndex != std::string::npos;
}


/// Returns the range indices that are being used on the given line
/// @return true if the range is found (firstIndex and lastIndex are filled
bool TextRangeSetBase::rangesAtLine(size_t line, size_t& firstIndex, size_t& lastIndex)
{
    TextDocument* doc = textDocument();
    size_t offsetBegin = doc->offsetFromLine(line);
    size_t offsetEnd   = doc->offsetFromLine(line + 1) - 1;
    return rangesBetweenOffsets(offsetBegin, offsetEnd, firstIndex, lastIndex);
}


/// Returns the range indices that are being used on the given line (Excluding the last offset)
/// @return true if the range is found (firstIndex and lastIndex are filled
bool TextRangeSetBase::rangesAtLineExclusiveEnd(size_t line, size_t &firstIndex, size_t &lastIndex)
{
    TextDocument* doc = textDocument();
    size_t offsetBegin = doc->offsetFromLine(line);
    size_t offsetEnd   = doc->offsetFromLine(line + 1) - 1;
    return rangesBetweenOffsetsExlusiveEnd(offsetBegin, offsetEnd, firstIndex, lastIndex);
}


/// This method checks if there's a selection available
/// A selection is an range with a different anchor then it's caret
bool TextRangeSetBase::hasSelection()
{
    for (size_t i = 0, cnt = rangeCount(); i < cnt; ++i) {
        if (range(i).hasSelection()) return true;
    }
    return false;
}


/// This method checks if two selections are equal
bool TextRangeSetBase::equals(TextRangeSetBase& sel)
{
    if (sel.rangeCount() != rangeCount()) return false;
    for (size_t i = rangeCount() - 1; i != std::string::npos; --i) {
        if (!range(i).equals(sel.range(i))) return false;
    }
    return true;
}


/// Replaces all ranges with the supplied ranges
void TextRangeSetBase::replaceAll(const TextRangeSetBase& base)
{
    clear();
    for (size_t i = 0, cnt = base.rangeCount(); i < cnt; ++i) {
        addRange(base.constRange(i));
    }
}


/// This method returns all selected text
/// For every filled selection range a line is returned
QString TextRangeSetBase::getSelectedText()
{
    TextDocument* doc = textDocument();
    QString buffer;
    for (size_t i=0, cnt = rangeCount(); i < cnt; ++i) {
        TextRange& range = this->range(i);
        if (range.hasSelection()) {
            buffer.append(doc->textPart(range.min(), range.length()));
            buffer.append("\n");
        }
    }
    if (!buffer.isEmpty()) {
        buffer.remove(buffer.length() - 1, 1);  // remove the last(newline character)
    }
    return buffer;
}


/// Returns ALL lines that are touched by the selection. This means
/// Full lines are always returned
QString TextRangeSetBase::getSelectedTextExpandedToFullLines()
{
    TextDocument* doc = textDocument();
    QString buffer;
    size_t lastLine = std::string::npos;
    for (size_t i = 0, cnt = rangeCount(); i < cnt; ++i) {
        TextRange& range = this->range(i);
        size_t min = range.min();
        size_t max = range.max();
        size_t line = doc->lineFromOffset(min);
        size_t maxLine = doc->lineFromOffset(max);

        // skip the current line if it's the same as last one
        if (line == lastLine) { ++line; }
        while (line <= maxLine) {
            buffer.append(doc->lineWithoutNewline(line));
            buffer.append("\n");
            ++line;
        }
        lastLine = line;
    }
    return buffer;
}


/// This method converts the selection ranges as a string, in the format:   anchor>caret,anchor>caret
QString TextRangeSetBase::rangesAsString() const
{
    QString str;
    for (size_t i = 0, cnt = rangeCount(); i < cnt; ++i) {
        const TextRange& range = constRange(i);
        if (!str.isEmpty()) str.append(",");
        str.append(range.toString());
    }
    return str;
}


/// This method resets all anchors to the positions of the carets
void TextRangeSetBase::resetAnchors()
{
    for (size_t i = 0, cnt = rangeCount(); i < cnt; ++i) {
        TextRange& range = this->range(i);
        range.reset();
    }
}


/// This method moves all carets to the anchor positions
void TextRangeSetBase::clearSelection()
{
    for (size_t i=0, cnt = rangeCount(); i < cnt; ++i) {
        TextRange& range = this->range(i);
        range.clearSelection();
    }
}


/// This method starts the changes
void TextRangeSetBase::beginChanges()
{
    Q_ASSERT(changing_ < 10);  // 10 times nesting is a LOT!!
    ++changing_;
}


void TextRangeSetBase::endChanges()
{
    Q_ASSERT(changing_ > 0);
    --changing_;
    processChangesIfRequired();
}


/// Ends the changes without processing.
/// WARNING, you should ONLY call this method if the operation you performed kept the rangeset
/// in a valid state. This (at least) means the ranges need to be sorted
void TextRangeSetBase::endChangesWithoutProcessing()
{
    Q_ASSERT(changing_ > 0);
    --changing_;
}


/// Checks if the current rangeset is in a changing state
bool TextRangeSetBase::changing() const
{
    return changing_ != 0;
}


/// An union operation
/// This method adds all text selection-items.

void TextRangeSetBase::addTextRanges(const TextRangeSetBase& sel)
{
    for (size_t i = 0, cnt = sel.rangeCount(); i < cnt; ++i) {
        addRange( sel.constRange(i) );
    }
    processChangesIfRequired(true );
}


/// The difference operation
/// Substracts the text-selection from the current selection
void TextRangeSetBase::substractTextRanges(const TextRangeSetBase& sel)
{
    ++changing_;
    for (size_t i = 0, cnt = sel.rangeCount(); i < cnt; ++i) {
        const TextRange& r = sel.constRange(i);
        substractRange(r.min(), r.max());
    }
    --changing_;
    processChangesIfRequired();
}


/// This method substracts a single range from the ranges list
void TextRangeSetBase::substractRange(size_t minB, size_t maxB)
{
    if (rangeCount() == 0) return;

    beginChanges();
    for (size_t i = rangeCount() -1; i != std::string::npos; --i) {
        TextRange& rangeItem = range(i);
        size_t& minA = rangeItem.minVar();
        size_t& maxA = rangeItem.maxVar();

        // A: [             ]
        // B:     [XXXXX]
        // =  [   ]     [   ]
        if ((minA < minB && minB < maxA) && (minA<maxB && maxB < maxA))
        {
            /// TODO Add support for copyrange
            addRange( maxB, maxA );
            maxA = minB;
        }
        // A:     [    ]
        // B: [XXXXXXXXXXXXX]
        // =:    (leeg)
        else if ((minB <= minA && minA <= maxB) && (minB <= maxA && maxA <= maxB))
        {
            removeRange(i);
        }
        // A: [     ]
        // B:    [XXXXXX]
        // =: [  ]
        else if (minA < minB && minB < maxA)
        {
            maxA = minB;
        }
        // A:     [     ]
        // B: [XXXXXX]
        // =:        [  ]
        else if (minA < maxB && maxB < maxA)
        {
            minA = maxB;
        }
    }
    endChanges();
}


/// Expands the selection so it selects full lines
void TextRangeSetBase::expandToFullLines(ptrdiff_t amount)
{
    for (size_t i = 0, cnt = rangeCount(); i < cnt; ++i) {
        range(i).expandToFullLine(textDocument(), amount);
    }
    processChangesIfRequired();
}


/// Expands the selection to full words
void TextRangeSetBase::expandToWords(const QString& whitespace, const QStringList& characterGroups)
{
    for (size_t i = 0, cnt = rangeCount(); i < cnt; ++i) {
        range(i).expandToWord(textDocument(), whitespace, characterGroups);
    }
    processChangesIfRequired();
}


/// Selects the word at the given offset
/// @param offset the offset of the word to select
void TextRangeSetBase::selectWordAt(size_t offset, const QString& whitespace, const QStringList& characterGroups)
{
    TextRange newRange(offset, offset);
    newRange.expandToWord(textDocument(), whitespace, characterGroups);
    addRange(newRange);
    processChangesIfRequired();
}


/// Toggles a word selection at the given location
/// The idea is the following, double-click an empty place to select the word at the given location
/// Double click an existing selection to remove the selection (and caret)
void TextRangeSetBase::toggleWordSelectionAt(size_t offset, const QString& whitespace, const QStringList& characterGroups)
{
    size_t idx = rangeIndexAtOffset(offset);

    // range found
    if (idx != std::string::npos) {
        TextRange& found = this->range(idx);

        // found a range with selection
        if (found.hasSelection()) {

            // when multiple range, just remove the range
            if (rangeCount() > 1) {
                removeRange(idx);
                return;

            // when this is the last range, just place the caret
            } else {
                found.set(offset, offset);
                return;
            }
        }
    }
    // default operation is select word at
    selectWordAt(offset, whitespace, characterGroups);
}


/// Moves the carets by character
void TextRangeSetBase::moveCarets(ptrdiff_t amount)
{
    for (size_t i = 0, cnt = rangeCount(); i < cnt; ++i) {
        range(i).moveCaret(textDocument(), amount);
    }
    processChangesIfRequired();
}


/// Moves the carets or deslects the given character
void TextRangeSetBase::moveCaretsOrDeselect(ptrdiff_t amount)
{
    for (size_t i = 0, cnt = rangeCount(); i < cnt; ++i) {
        range(i).moveCaretOrDeselect(textDocument(), amount);
    }
    processChangesIfRequired();
}


/// Moves the carets
void TextRangeSetBase::moveCaretsByCharGroup(ptrdiff_t amount, const QString& whitespace, const QStringList& characterGroups)
{
    for (size_t rangeIdx=rangeCount() - 1; rangeIdx != std::string::npos; --rangeIdx) {
        range(rangeIdx).moveCaretByCharGroup(textDocument(), amount, whitespace, characterGroups);
    }
    processChangesIfRequired();
}


/// Moves all carets to the given line boundary (line-boundary automatically switches between column 0 and first non-whitespace character)
/// @param direction the direction < 0 to the start of the line (or first char)  > 0 to the end of the line
/// @param whitespace the characters to see as whitespace
void TextRangeSetBase::moveCaretsToLineBoundary(ptrdiff_t direction, const QString& whitespace)
{
    // process all carets
    for (size_t rangeIdx = rangeCount() - 1; rangeIdx != std::string::npos; --rangeIdx) {
        range(rangeIdx).moveCaretToLineBoundary(textDocument(), direction, whitespace);
    }
    processChangesIfRequired();
}


/// Merges overlapping ranges
/// @param joinBorders if joinborders is set, borders next to eachother are also interpretted as overlap. (inclusive/exclusive switch)
void TextRangeSetBase::mergeOverlappingRanges(bool joinBorders)
{
    // check the ranges
    beginChanges();
    for (size_t i = rangeCount() - 1; i != std::string::npos; --i) {
        TextRange& range1 = range(i);
        size_t min1 = range1.min();
        size_t max1 = range1.max();

        // check overlap with all other ranges
        for (size_t j = i - 1; j != std::string::npos; --j) {
            TextRange& range2 = range(j);
            size_t min2 = range2.min();
            size_t max2 = range2.max();

            // Overlappping possibilities:
            // 1: [        ]
            // 2    [XXX]
            // =  (delete 1)
            if (min1 <= min2 && max2 <= max1) {
                removeRange(j);
                --i;
                continue;
            }

            // Overlappping possibilities:
            // 1:     [   ]
            // 2    [XXXXXXX]
            // =  (delete 1)
            if (min2 <= min1 && max1 <= max2) {
                removeRange(i);
                //selectionRanges_.remove(i);
                break;
            }

            // Overlappping possibilities:
            // 1:       [        ]
            // 2    [XXXXXX]
            // =  [ min2, max1]     (update range 2, delete range 1 )
            if (
               (min2 < min1 && min1 < max2 && max2 < max1)
               || (joinBorders && min2 <= min1 && min1 <= max2 && max2 <= max1)
            )
            {
                range2.maxVar() = max1;
                //selectionRanges_.remove(i);
                removeRange(i);
                break;
            }

            // Overlappping possibilities:
            // 1:   [        ]
            // 2         [XXXXXXX]
            // =  [ min1, max2]     (update range 2, delete range 1 )
            if (
               (min1 < min2 && min2 < max1 && max1 < max2)
               || (joinBorders && min1 <= min2 && min2 <= max1 && max1 <= max2)
            )
            {
                range2.minVar() = min1;
                //selectionRanges_.remove(i);
                removeRange(i);
                break;
            }
        }
    }
    endChanges();
}


/// Sets the first range item
/// @param anchor the anchor of the selection
/// @param caret the caret position
/// @param index the default range index (default 0)
void TextRangeSetBase::setRange(size_t anchor, size_t caret, size_t index)
{
    range(index).set(anchor, caret);
}


/// Sets the range at the given index.
/// Make sure the given index exists!!
/// @param the range to use
/// @param index the index of the range to change (when not given index 0 is assumed)
void TextRangeSetBase::setRange(const TextRange& range, size_t index)
{
    setRange(range.anchor(), range.caret(), index);
}


/// This method process the changes if required
void TextRangeSetBase::processChangesIfRequired(bool joinBorders)
{
    if (!changing_) {
        ++changing_; // prevent changing by functions below:
        mergeOverlappingRanges(joinBorders);
        sortRanges();
        --changing_;
    }
}


/// Returns the associated textdocument
/// @return the associated text document
TextDocument*TextRangeSetBase::textDocument() const
{
    return textDocumentRef_;
}


/// This method adds (or removes) the given spatial length at the given location.
///
/// It adjusts all locations, anchors with the given locations. It automatically moves carets,
/// 'removes' selection etc.
///
/// @param pos the position to add the spatial length to
/// @param length the length of the text that's changed
/// @param newLength the new length of the text
/// @param sticky, when sticky the caret/anchor is sticky and isn't moved if the change happens at the same location
void TextRangeSetBase::changeSpatial(size_t pos, size_t length, size_t newLength, bool sticky , bool performDelete)
{
    if (rangeCount() == 0) return;

    ptrdiff_t stickyDelta = sticky ? 0 : -1;

    // change the ranges
    ptrdiff_t endPos = static_cast<ptrdiff_t>(pos + length);
    ptrdiff_t delta = static_cast<ptrdiff_t>(newLength) - static_cast<ptrdiff_t>(length);
    ptrdiff_t newEndPos = static_cast<ptrdiff_t>(endPos) + delta;
    ptrdiff_t spos = static_cast<ptrdiff_t>(pos);

    beginChanges();
    for (size_t i = rangeCount() - 1; i != std::string::npos; --i) {
        TextRange& range = this->range(i);

        ptrdiff_t min = static_cast<ptrdiff_t>(range.min());
        ptrdiff_t max = static_cast<ptrdiff_t>(range.max());

        // cut 'off' the endpos
        if (spos <= min && min < endPos) {
            min = endPos;
            // when the range becomes invalid simply remove it
            if (min > max) {
                if (performDelete) {
                    removeRange(i);
                    continue;
                } else {
                    range.set(pos, pos);
                    continue;
                }
            }
        }

        // anchor
        if (spos < min && min < endPos) {
            min = newEndPos;
        } else if (min > (spos + stickyDelta)) {
            min += delta;
        }

        // caret
        if (spos < max && max < endPos) {
            max  = newEndPos;
        } else if (max > (spos + stickyDelta)) {
            max += delta;
        }

        Q_ASSERT(min >= 0);
        Q_ASSERT(max >= 0);
        range.set(static_cast<size_t>(min), static_cast<size_t>(max));
    }
    endChanges();
}


// =====================================


/// Constructs a textrange set
/// @param doc the document for this rangeset
TextRangeSet::TextRangeSet(TextDocument* doc)
    : TextRangeSetBase(doc)
{
}


/// the copy constructor for copying a selection
/// @param sel the ranges to copy
TextRangeSet::TextRangeSet(const TextRangeSet& sel)
    : TextRangeSetBase(sel.textDocument())
{
    selectionRanges_ = sel.selectionRanges_;
}


// An operation to copy the selection with a pointer
TextRangeSet::TextRangeSet(const TextRangeSet* sel)
    : TextRangeSetBase(sel->textDocument())
{
    selectionRanges_ = sel->selectionRanges_;
}


/// copy the value from the rangeset
TextRangeSet& TextRangeSet::operator=(const TextRangeSet& sel)
{
    textDocumentRef_ = sel.textDocumentRef_;
    selectionRanges_ = sel.selectionRanges_;
    return *this;
}


///// This method clones the text selection
TextRangeSet *TextRangeSet::clone() const
{
    return new TextRangeSet(*this);
}


/// returns the selection range
TextRange& TextRangeSet::range(size_t idx)
{
    Q_ASSERT(idx != std::string::npos);
    Q_ASSERT(idx < static_cast<size_t>(selectionRanges_.size()));
    return selectionRanges_[static_cast<qsizetype>(idx)];
}


/// Returns a const reference from the
const TextRange &TextRangeSet::constRange(size_t idx) const
{
    Q_ASSERT(idx < static_cast<size_t>(selectionRanges_.size()));
    return selectionRanges_.at(static_cast<qsizetype>(idx));
}


/// Adds a text range
void TextRangeSet::addRange(size_t anchor, size_t caret)
{
    selectionRanges_.append(TextRange(anchor, caret));
    processChangesIfRequired();
}


/// adds a range
void TextRangeSet::addRange(const TextRange& range)
{
    addRange(range.anchor(), range.caret());
}


/// this method removes the range
void TextRangeSet::removeRange(size_t idx)
{
    selectionRanges_.remove(static_cast<qsizetype>(idx));
    processChangesIfRequired();
}


/// This method removes ALL carets except the 'global' selection
void TextRangeSet::clear()
{
    selectionRanges_.clear();
}


/// Converts the range to a single range selection
void TextRangeSet::toSingleRange()
{
    selectionRanges_.remove(1, selectionRanges_.size() - 1);
}


/// Sorts the ranges
void TextRangeSet::sortRanges()
{
    std::sort(selectionRanges_.begin(), selectionRanges_.end(), TextRange::lessThan);
}

/// asserts all range site items are valied
void TextRangeSet::assertValid() const
{
    for (qsizetype i = 0, cnt = selectionRanges_.size(); i < cnt; i++) {
        selectionRanges_.at(i).assertValid();
    }

}


// =====================================



/// Constructs the dynamic textrange set with a document
/// @param doc the document to use
/// @param stickyMode is the stickymode enabled
/// @param parent the parent owner
DynamicTextRangeSet::DynamicTextRangeSet(TextDocument* doc, bool stickyMode, bool deleteMode, QObject* parent)
    : QObject(parent)
    , TextRangeSet(doc)
    , stickyMode_(stickyMode)
    , deleteMode_(deleteMode)
{
    connect(textDocument(), &TextDocument::textChanged, this, &DynamicTextRangeSet::textChanged);
}


/// Constructs the dynamic textrange set with an existing rangeset
/// @param sel the text range to copy
/// @param stickyMode is the stickymode enabled
/// @param parent the parent owner
DynamicTextRangeSet::DynamicTextRangeSet(const TextRangeSet& sel, bool stickyMode, bool deleteMode, QObject* parent)
    : QObject(parent)
    , TextRangeSet(sel)
    , stickyMode_(stickyMode)
    , deleteMode_(deleteMode)
{
    connect(textDocument(), &TextDocument::textChanged, this, &DynamicTextRangeSet::textChanged);
}


/// constructs the dynamic rangeset
/// @param sel the text range to copy
/// @param stickyMode is the stickymode enabled
/// @param parent the parent owner
DynamicTextRangeSet::DynamicTextRangeSet(const TextRangeSet* sel, bool stickyMode, bool deleteMode, QObject* parent)
    : QObject(parent)
    , TextRangeSet(sel)
    , stickyMode_(stickyMode)
    , deleteMode_(deleteMode)
{
    connect(textDocument(), &TextDocument::textChanged, this, &DynamicTextRangeSet::textChanged);
}


/// destructs the reviever
DynamicTextRangeSet::~DynamicTextRangeSet()
{
    disconnect();
}


/// Is the stickymode enabled
/// @param mode the mode that is sticky
void DynamicTextRangeSet::setStickyMode(bool mode)
{
    stickyMode_ = mode;
}


/// returns the current stickymode
bool DynamicTextRangeSet::stickyMode() const
{
    return stickyMode_;
}


/// Sets the delete mode
void DynamicTextRangeSet::setDeleteMode(bool mode)
{
    deleteMode_ = mode;
}


/// Returns the current delete mode
bool DynamicTextRangeSet::deleteMode() const
{
    return deleteMode_;
}


/// This method is notified if a change happens to the textbuffer
void DynamicTextRangeSet::textChanged(edbee::TextBufferChange change, QString oldText)
{
    Q_UNUSED(oldText)
    changeSpatial(change.offset(), change.length(), change.newTextLength(), stickyMode_, deleteMode_);
}


} // edbee
