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

#include "textdocument.h"

#include <QStringList>

#include "edbee/models/changes/mergablechangegroup.h"
#include "edbee/models/changes/linedatachange.h"
#include "edbee/models/changes/selectionchange.h"
#include "edbee/models/changes/textchange.h"
#include "edbee/models/changes/textchangewithcaret.h"

#include "edbee/models/textlinedata.h"
#include "edbee/models/textdocumentfilter.h"
#include "edbee/models/textrange.h"
#include "edbee/models/textundostack.h"

#include "edbee/debug.h"

namespace edbee {


/// Constructs the textdocument
TextDocument::TextDocument(QObject* obj)
    : QObject(obj)
    , documentFilter_(nullptr)
    , documentFilterRef_(nullptr)
    , textLineDataManager_(nullptr)
{
    textLineDataManager_ = new TextLineDataManager();
}


/// Destroys the textdocument
TextDocument::~TextDocument()
{
    delete textLineDataManager_;
    delete documentFilter_;
}


/// Changes the number of reserved fields by the document
/// Increasing the amount will result in a realoc
/// Decreasting the fieldcount  reults in the lost of the 'old' fields
/// At least the 'PredefinedFieldCount' amont of fields are required
/// This method EMPTIES the undo-stack. So after this call all undo history is gone!
void TextDocument::setLineDataFieldsPerLine(size_t count)
{
    Q_ASSERT( count >= PredefinedFieldCount );
    lineDataManager()->setFieldsPerLine(count);
    textUndoStack()->clear();
}


void TextDocument::giveLineDataManager(TextLineDataManager* manager)
{
    delete textLineDataManager_;
    textLineDataManager_ = manager;
}


/// Gives a given data item to a text line
void TextDocument::giveLineData(size_t line, size_t field, TextLineData* dataItem)
{
    Q_ASSERT(line < lineCount());
    LineDataChange* change = new LineDataChange(line, field);
    change->giveLineData(dataItem);
    executeAndGiveChange(change, true);
}


/// Returns the line specific data at the given line
/// @param line the line number to retrieve the line data for
/// @param field the field to retrieve the data for
/// @return TextLineData the associated line data
TextLineData* TextDocument::getLineData(size_t line, size_t field)
{
    size_t len = lineDataManager()->length();
    // Q_ASSERT( len == lineCount() );  FIXME: disabled, issue with renderer retreiving linedata
    Q_ASSERT(line < len);
    return lineDataManager()->get(line, field);
}


/// Starts an undo group
/// @param group the textchange group that groups the undo operations
void TextDocument::beginUndoGroup(ChangeGroup* group)
{
    if (!group) {
        group = new ChangeGroup(nullptr);
    }
//    if( documentFilter() ) {
//        documentFilter()->filterBeginGroup( this, group );
//    }
    textUndoStack()->beginUndoGroup(group);
}


/// Ends the current undo group
/// @param coalesceId the coalesceId
/// @param flatten should the operation be flatten (flattens undo-group trees)
void TextDocument::endUndoGroup(int coalesceId, bool flatten)
{
//    if( documentFilter() ) {
//        TextChangeGroup* group = textUndoStack()->currentGroup();
//        documentFilter()->filterEndGroup( this, group, coalesceId, flatten );
//    }
    textUndoStack()->endUndoGroup(coalesceId,flatten);
}


/// Ends the undo group and discards all recorded information
/// Warning it does NOT undo all made changes!!!
void TextDocument::endUndoGroupAndDiscard()
{
    textUndoStack()->endUndoGroupAndDiscard();
}


/// this method return true if the undo stack is enabled
bool TextDocument::isUndoCollectionEnabled()
{
    return textUndoStack()->isCollectionEnabled();
}


/// Enables or disables the collection of undo commands
void TextDocument::setUndoCollectionEnabled(bool enabled)
{
    textUndoStack()->setCollectionEnabled(enabled);
}


/// This method should return true if the current change is the cause of an undo operation
bool TextDocument::isUndoRunning()
{
    return textUndoStack()->isUndoRunning();
}


/// Checks if currently an undo operation is running
bool TextDocument::isRedoRunning()
{
    return textUndoStack()->isRedoRunning();
}


/// Is it an undo or redo (which means all commands area already available)
bool TextDocument::isUndoOrRedoRunning()
{
    return isUndoRunning() || isRedoRunning();
}


/// Checks if the document is in a persited state
bool TextDocument::isPersisted()
{
    return textUndoStack()->isPersisted();
}


/// Calc this method to mark current state as persisted
void TextDocument::setPersisted(bool enabled)
{
    textUndoStack()->setPersisted(enabled);
}


/// Sets the document filter without tranfering the ownership
void TextDocument::setDocumentFilter(TextDocumentFilter* filter)
{
    delete documentFilter_;
    documentFilter_  = 0;
    documentFilterRef_ = filter;
}


/// this method sets the document filter
/// You can give a 0 pointer to delte the old filter!
void TextDocument::giveDocumentFilter(TextDocumentFilter* filter)
{
    delete documentFilter_;
    documentFilter_ = filter;
    documentFilterRef_ = filter;
}


/// This method returns the document filter
TextDocumentFilter* TextDocument::documentFilter()
{
    return documentFilterRef_;
}


/// Start the changes
void TextDocument::beginChanges(TextEditorController* controller)
{
    beginUndoGroup( new MergableChangeGroup( controller) );
}


/// Replaces the given rangeset
void TextDocument::replaceRangeSet(TextRangeSet& rangeSet, const QString& textIn, bool stickySelection)
{
    return replaceRangeSet(rangeSet, QStringList(textIn), stickySelection);
}


/// replaces the given rangeset
void TextDocument::replaceRangeSet(TextRangeSet& rangeSet, const QStringList& textsIn, bool stickySelection)
{
    QStringList texts = textsIn;
    if( documentFilter() ) {
        documentFilter()->filterReplaceRangeSet( this, rangeSet, texts );
    }

    rangeSet.beginChanges();
    ptrdiff_t delta = 0;
    size_t idx = 0, oldRangeCount = 0;
    while (idx < (oldRangeCount = rangeSet.rangeCount())) {
        TextRange& range = rangeSet.range(idx);
        QString text = texts.at(static_cast<qsizetype>(idx) % textsIn.size());  // rotating text-fetching

        range.setCaretBounded(this, static_cast<ptrdiff_t>(range.caret()) + delta);
        range.setAnchorBounded(this, static_cast<ptrdiff_t>(range.anchor()) + delta);

        delta += (text.length() - static_cast<ptrdiff_t>(range.length()));

        TextChangeWithCaret* change = new TextChangeWithCaret(range.min(), range.length(), text, std::string::npos);

        Change* effectiveChange = executeAndGiveChange(change, false);
        TextChangeWithCaret* effectiveChangeWithCaret = dynamic_cast<TextChangeWithCaret*>(effectiveChange);

        // when a new caret position is supplied (can only happen via a TextDocumentFilter)
        // access it and change the caret to the given position
        size_t caret = 0;
        if (effectiveChangeWithCaret && effectiveChangeWithCaret->caret() != std::string::npos) {
            caret = effectiveChangeWithCaret->caret();
        // Default caret location is change-independent: old location + length new text
        } else {
            caret = range.min() + static_cast<size_t>(text.length());
        }


        if (stickySelection) {
          range.set(range.min(), caret);
        } else {
          range.setCaret(caret);
          range.reset();
        }

        // next range
        if (rangeSet.rangeCount() < oldRangeCount) {
            qDebug() <<  "TEST TO SEE IF THIS REALLY HAPPENS!! I think it cannot happen. (but I'm not sure)";
            Q_ASSERT(false);

        // else we stay at the same location
        } else {
            ++idx;
        }

    }
    rangeSet.endChanges();
}


/// sets the selectioin for the current rangeset
/// The selection may never be empty
/// @param controller the controller to given the selection for
/// @param rangeSet the rangeset with the new selection
void TextDocument::giveSelection( TextEditorController* controller,  TextRangeSet* rangeSet)
{
    Q_ASSERT(rangeSet->rangeCount()>0);

    SelectionChange* selChange = new SelectionChange( controller );
    selChange->giveTextRangeSet( rangeSet );
    executeAndGiveChange( selChange, 0 );
}


//// end the changes
void TextDocument::endChanges(int coalesceId)
{
    endUndoGroup(coalesceId,true);
}


/// call this method to execute a change. The change is first passed to the filter
/// so the documentFilter can handle the processing of the change
/// When not filter is active the 'execute' method is called on the change
///
/// WARNING, you should never Access the change pointer given to this method
/// It's possible the change gets deleted
///
/// This method  should return the effective text-change (or 0 if no text-change has been stored)
Change *TextDocument::executeAndGiveChange(Change* change, int coalesceId )
{
    if( documentFilter() ) {
        return documentFilter()->filterChange( this, change, coalesceId );
    } else {

        beginUndoGroup();   // automatically group changes together (when changes happen on emission)
        change->execute( this );
        Change* result = giveChangeWithoutFilter( change, coalesceId );
        Q_UNUSED(result)
        endUndoGroup(coalesceId, true);
        return textUndoStack()->last();
//        return result;
    }
}


/// Appends the given text to the document
/// @param text the text to append
/// @param coalesceId (default 0) the coalesceId to use. Whe using the same number changes could be merged to one change. CoalesceId of 0 means no merging
void TextDocument::append(const QString& text, int coalesceId )
{
    replace(this->length(), 0, text, coalesceId);
}


/// Appends the given text
/// @param text the text to append
/// @param coalesceId (default 0) the coalesceId to use. Whe using the same number changes could be merged to one change. CoalesceId of 0 means no merging
void TextDocument::replace(size_t offset, size_t length, const QString& text, int coalesceId)
{
    executeAndGiveChange(new TextChange(offset, length, text), coalesceId);
}


/// Changes the compelte document text
/// @param text the new document text
void TextDocument::setText(const QString& text)
{
    replace(0, length(), text, 0);
}


/// begins the raw append modes. In raw append mode data is directly streamed
/// to the textdocument-buffer. No undo-data is collected and no events are fired
void TextDocument::rawAppendBegin()
{
    setUndoCollectionEnabled(false); // no undo's
    buffer()->rawAppendBegin();
}


/// When then raw appending is done. The events are fired that the document has been changed
/// The undo-collection is enabled again
void TextDocument::rawAppendEnd()
{
    buffer()->rawAppendEnd();
    setUndoCollectionEnabled(true);
}


/// Appends a single char in raw append mode
void TextDocument::rawAppend(QChar c)
{
    buffer()->rawAppend(c);
}


/// Appends an array of characters
void TextDocument::rawAppend(const QChar* chars, size_t length)
{
    buffer()->rawAppend(chars, length);
}


/// Returns the length of the document in characters
/// default implementation is to forward this call to the textbuffer
size_t TextDocument::length()
{
    return buffer()->length();
}


/// Returns the number of lines
size_t TextDocument::lineCount()
{
    return buffer()->lineCount();
}


/// Returns the character at the given position
QChar TextDocument::charAt(size_t idx)
{
    Q_ASSERT(idx != std::string::npos);
    return buffer()->charAt(idx);
}


/// returns the char at the given index if the index is valid
/// else the null character is returned
///
/// @param idx the index to retrieve
/// @return the character at the given index or the null-character
QChar TextDocument::charAtOrNull(size_t idx)
{
    if(idx < length() ) {
        return charAt(idx);
    }
    return QChar();
}


/// Retrieves the character-offset of the given line
/// @param line the line number (0-based) to retrieve the offset for
/// @return the character offset
size_t TextDocument::offsetFromLine(size_t line)
{
    Q_ASSERT(line != std::string::npos);
    return buffer()->offsetFromLine(line);
}


/// returns the line number which contains the given offset
/// @param offset the character offset
/// @return the line number (0 is the first line )
size_t TextDocument::lineFromOffset(size_t offset)
{
    Q_ASSERT(offset != std::string::npos);
    return buffer()->lineFromOffset(offset);
}


/// return the column position for the given offset and line
/// @param offset the offset position
/// @param line the line number which contains this offset. (When std::string::npos the line number is calculated)
/// @return the column position of the given offset
size_t TextDocument::columnFromOffsetAndLine(size_t offset, size_t line)
{
    return buffer()->columnFromOffsetAndLine(offset, line);
}


/// Returns the character offset of the given line and column
/// @param line the line number
/// @param column the column position
/// @return the character offset in the document
size_t TextDocument::offsetFromLineAndColumn(size_t line, size_t column)
{
    return buffer()->offsetFromLineAndColumn(line, column);
}


/// Returns the length of the given line
/// @param line the line number
/// @return the line length
size_t TextDocument::lineLength(size_t line)
{
    return buffer()->lineLength(line);
}


/// returns the length of the given lilne without the newline
/// @param line the line number
/// @return the length of the line excluding the newline character
size_t TextDocument::lineLengthWithoutNewline(size_t line)
{
    return buffer()->lineLengthWithoutNewline(line);
}


/// Returns the document text as a QString
/// @return the complete document context
QString TextDocument::text()
{
    return buffer()->text();
}


/// Returns the given part of the text
/// @param offset the character offset in the document
/// @param length the length of the part in characters
/// @return the text at the given positions
QString TextDocument::textPart(size_t offset, size_t length)
{
    return buffer()->textPart(offset, length);
}


/// Returns the given line without the trailing \n character
/// @param line the line number to retrieve the data for
/// @return the content at the given line
QString TextDocument::lineWithoutNewline(size_t line)
{
    return buffer()->lineWithoutNewline(line);
}


//// Returns the contents at the given line inclusive the trailing \n character
/// @pparam line the line number to retrieve
/// @return the line at the given position
QString TextDocument::line(size_t line)
{
    return buffer()->line(line);
}


} // edbee
