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

#include "mergablechangegroup.h"

#include "edbee/models/changes/abstractrangedchange.h"
#include "edbee/models/changes/selectionchange.h"
#include "edbee/models/changes/textchange.h"
#include "edbee/models/changes/linedatalistchange.h"
#include "edbee/models/textrange.h"
#include "edbee/views/textselection.h"
#include "edbee/texteditorcontroller.h"
#include "edbee/util/util.h"

#include "edbee/debug.h"

namespace edbee {


MergableChangeGroup::MergableChangeGroup(TextEditorController* controller)
    : ChangeGroup(controller)
    , previousSelection_(nullptr)
    , newSelection_(nullptr)
{
    if (controller) {
       previousSelection_ = new TextRangeSet(*(controller->textSelection()));
    }
}


MergableChangeGroup::~MergableChangeGroup()
{
    qDeleteAll(textChangeList_);
    qDeleteAll(lineDataTextChangeList_);
    qDeleteAll(miscChangeList_);

    delete newSelection_;
    delete previousSelection_;
}


/// default not discardable
bool MergableChangeGroup::isDiscardable()
{
    return false;
}


/// the group is closed, we must 'store' the selection
void MergableChangeGroup::groupClosed()
{
    if (controller()) {
        delete newSelection_;
        newSelection_ = new TextRangeSet(*(controller()->textSelection())) ;
    }
}


/// Executes this textchange
/// @param document the document to execute this operation for
void MergableChangeGroup::execute(TextDocument* document)
{
    ChangeGroup::execute(document);
    if (newSelection_) {
        TextRangeSet* currentSelection = dynamic_cast<TextRangeSet*>(controller()->textSelection());
        *currentSelection = *newSelection_ ;
    }
}


/// Can be called to revert the operation
/// reverts the given operation
void MergableChangeGroup::revert(TextDocument* document)
{
    ChangeGroup::revert(document);
    if (previousSelection_) {
        TextRangeSet* currentSelection = dynamic_cast<TextRangeSet*>(controller()->textSelection());
        *currentSelection = *previousSelection_;
    }
}


/// Adds the given delta to the changes
void MergableChangeGroup::addOffsetDeltaToChanges(QList<AbstractRangedChange*>& changes, size_t fromIndex, ptrdiff_t delta)
{
    for (qsizetype i = static_cast<qsizetype>(fromIndex), cnt = changes.size(); i < cnt; ++i) {
        AbstractRangedChange* s2 = changes.at(i);
        s2->addOffset(delta);
    }
}


/// This method finds the insert index for the given offset
/// @param offset the offset of the change
/// @return the inertindex used for inserting the data
qsizetype MergableChangeGroup::findInsertIndexForOffset(QList<AbstractRangedChange*>& changes, size_t offset)
{
    qsizetype insertIndex = 0;
    for (qsizetype i=0, cnt = changes.size(); i < cnt; ++i){
        AbstractRangedChange* change = changes.at(i);
        if (change->offset() < offset) {
            insertIndex = i + 1;
        }
    }
    return insertIndex;
}


/// Tries to merge the given textchagne
/// @param newChange the new change to merge
/// @param delta (out) the delta applied to this change
/// @return the merged index or -1 if not merged!
qsizetype MergableChangeGroup::mergeChange(QList<AbstractRangedChange*>& changes, TextDocument* doc, AbstractRangedChange* newChange, ptrdiff_t& delta)
{
    for (qsizetype i=0, cnt = changes.size(); i < cnt; ++i){
        AbstractRangedChange* change = changes.at(i);

        // we need the previous length and new-length to know how the delta is changed of the other items
        ptrdiff_t prevNewLength = static_cast<ptrdiff_t>(change->docLength());
        ptrdiff_t prevContentLength = static_cast<ptrdiff_t>(change->storedLength());

        // try to merge it
        if (change->giveAndMerge(doc, newChange)) {

            // apply the delta (newLength and length is reversed when in undo state!)
            delta += (static_cast<ptrdiff_t>(change->docLength()) - prevNewLength) - (static_cast<ptrdiff_t>(change->storedLength()) - prevContentLength);
            return i;
        }
    }
    return -1;
}


/// When a normal merge was succesful (with mergeTextChange) we need to inverseMerge the other overlapping textchanges
/// @param mergedAtIndex the index where the first merge was succeeded
/// @param orgStartOffset the offset of the orgingal merged textchange
/// @param orgEndoOffset the end offset of the original merged textchange
/// @param delta the current delta used for offset calculating
void MergableChangeGroup::inverseMergeRemainingOverlappingChanges(QList<AbstractRangedChange*>& changes, TextDocument* doc, qsizetype mergedAtIndex, size_t orgStartOffset, size_t orgEndOffset, ptrdiff_t delta)
{
    AbstractRangedChange* mergedChange = changes.at(mergedAtIndex);

    for (qsizetype i = mergedAtIndex + 1; i < changes.size(); ++i) {
        AbstractRangedChange* nextChange= changes.at(i);

        // only perform merging if the change overlapped a previous change
        if (nextChange->offset() < orgEndOffset && orgStartOffset < (nextChange->offset() + nextChange->docLength())) {

            // take the delta of the previous change before the merge
            ptrdiff_t tmpDelta = static_cast<ptrdiff_t>(mergedChange->storedLength()) - static_cast<ptrdiff_t>(mergedChange->docLength()) + delta;

            // alter the delta, so we find the correct merge index
            nextChange->addOffset(tmpDelta);

            // notice the 'inversion of the merge. We apply the merged change to the next change
            if (nextChange->giveAndMerge(doc, mergedChange) ) {
                mergedChange = nextChange;
                changes.removeAt(mergedAtIndex);
                --i;
            } else {
                // remove the temporary delta
                nextChange->addOffset(-tmpDelta);
                break;  // cannot merge, we're done
            }
        } else {
            break;
        }
    }
}


/// gives the given textchange to the merge routine
void MergableChangeGroup::giveChangeToList(QList<AbstractRangedChange*>& changes, TextDocument* doc, AbstractRangedChange* newChange)
{
    //qlog_info() << "giveSingleTextChange" << newChange->toString();
    // remember the orginal ranges so we know which changes are affected by this new change
    size_t orgStartOffset = newChange->offset();
    size_t orgEndOffset = newChange->offset() + newChange->storedLength();

    // some variables to remebmer
    ptrdiff_t addDeltaFromIndex = 0; //static_cast<ptrdiff_t>(size());  // From which change index should the delta be added
    ptrdiff_t delta = 0;

    // First try to merge this new change
    qsizetype mergedAtIndex = mergeChange(changes, doc, newChange, delta);

    // when we could merge the change,
    // we need to try to merge it with next overlapping changes
    if (mergedAtIndex >= 0) {
        inverseMergeRemainingOverlappingChanges(changes, doc, mergedAtIndex, orgStartOffset, orgEndOffset, delta);
        addDeltaFromIndex = mergedAtIndex + 1;

    // not merged? then we need to add the change at the given index
    } else {
        // find the insert index
        qsizetype insertIndex = findInsertIndexForOffset(changes, newChange->offset());

        // just insert change
        textChangeList_.insert(insertIndex, newChange);
        addDeltaFromIndex = insertIndex + 1;

        // apply the delta (newLength and length is reversed when in undo state!)
        delta += newChange->storedLength() - newChange->docLength();
    }

    // next apply the delta to the following change
    Q_ASSERT(addDeltaFromIndex >= 0);
    addOffsetDeltaToChanges( changes, static_cast<size_t>(addDeltaFromIndex), delta);
}


/// Gives a single textchange
void MergableChangeGroup::giveSingleTextChange(TextDocument* doc, TextChange* change)
{
    giveChangeToList(textChangeList_, doc, change);
}


/// gives a line data list text change
void MergableChangeGroup::giveLineDataListTextChange(TextDocument* doc, LineDataListChange* newChange)
{
    Q_UNUSED(doc);
    lineDataTextChangeList_.append(newChange);  // no merge for the moment
/* TODO:  Work in progress below. This algorithm seems identical to the one used with SingleTextChanges
 *  we need to abstract the interfaces and create a general merge-ranges operation
 *
    // try to merge the change
    int mergeIndex = -1;
    int delta = 0;
    int addDeltaIndex = lineDataTextChangeList_.size();

    for( int i=0, cnt=lineDataTextChangeList_.size(); i<cnt; ++i ) {
        LineDataListTextChange* change = lineDataTextChangeList_.at(i);

        // we need the previous length and new-length to know how the delta is changed of the other items
        int prevLength = change->length();
        int prevNewLength = change->newLength();

        if( change->giveAndMerge( doc, newChange ) ) {
            mergeIndex = i;
            delta += (change->length()-prevLength) - (change->newLength()-prevNewLength);
            break;
        }
    }

    // when merge succeeded, add the delta index
    if( mergeIndex >= 0 ) {
        addDeltaIndex = mergeIndex+1;
    }

    // when not merged, find the insert index
    if( mergeIndex < 0 ) {
        // STILL a TODO
        lineDataTextChangeList_.append(newChange);
    }

    // next add delta offsets from the given index
    for(int i = addDeltaIndex,cnt=lineDataTextChangeList_.size(); i<cnt; ++i ) {
        LineDataListTextChange* s2 = lineDataTextChangeList_.at(i);
        s2->addDeltaToLine(delta);
    }
*/
}


/// Gives the change
void MergableChangeGroup::giveChange(TextDocument* doc, Change* change)
{
    // a single text change
    TextChange* textChange = dynamic_cast<TextChange*>(change);
    if (textChange) {
        giveSingleTextChange(doc, textChange);
        return;
    }

    // a list text change
    LineDataListChange* lineDataTextChange = dynamic_cast<LineDataListChange*>(change);
    if (lineDataTextChange) {
        giveLineDataListTextChange(doc, lineDataTextChange);
        return;
    }

    // a selection change simply is moved to the new selection object
    SelectionChange* selectionChange = dynamic_cast<SelectionChange*>(change);
    if (selectionChange) {
        delete newSelection_;
        newSelection_ = selectionChange->takeRangeSet();

        /// Simply delete the change, the ComplexTextChange automatically records the last change selection on the undoGroupEnd
        delete change;
        return;
    }

    // a group changes, just add all the groups
    ChangeGroup* group = dynamic_cast<ChangeGroup*>(change);
    if (group) {
        moveChangesFromGroup(doc,group);
        delete group;
        return;
    }

    // other changes are (currently) added to a misch change list. And are pretty scary for now :)
    miscChangeList_.append(change);
}


///returns the textchange at the given index
Change* MergableChangeGroup::at(size_t idx)
{
    qsizetype qidx = static_cast<qsizetype>(idx);

    // plain text changes
    if (qidx < textChangeList_.size()) {
        return textChangeList_.at(qidx);
    }
    // line-data changes
    qidx -= textChangeList_.size();
    if (qidx < lineDataTextChangeList_.size()) {
        return lineDataTextChangeList_.at(qidx);
    }
    // other changes
    qidx -= lineDataTextChangeList_.size();
    Q_ASSERT(qidx < miscChangeList_.size());
    return miscChangeList_.at(qidx);
}


/// Takes the given item
Change*MergableChangeGroup::take(size_t idx)
{
    qsizetype qidx = static_cast<qsizetype>(idx);

    // plain text changes
    if (qidx < textChangeList_.size()) {
        return textChangeList_.takeAt(qidx);
    }
    // line-data changes
    qidx -= textChangeList_.size();
    if (qidx < lineDataTextChangeList_.size()) {
        return lineDataTextChangeList_.takeAt(qidx);
    }
    // other changes
    qidx -= lineDataTextChangeList_.size();
    Q_ASSERT(qidx < miscChangeList_.size());
    return miscChangeList_.takeAt(qidx);
}


/// returns the number of elements
size_t MergableChangeGroup::size()
{
    return static_cast<size_t>(textChangeList_.size() + lineDataTextChangeList_.size() + miscChangeList_.size());
}


/// clears all items
void MergableChangeGroup::clear(bool performDelete)
{
    // delete
    if(performDelete) {
        qDeleteAll(textChangeList_);
        qDeleteAll(lineDataTextChangeList_);
        qDeleteAll(miscChangeList_);
    }

    // clear the changes
    textChangeList_.clear();
    lineDataTextChangeList_.clear();
    miscChangeList_.clear();
}


/// Tries to merge the textchange with this text change
bool MergableChangeGroup::giveAndMerge(TextDocument* document, Change* textChange)
{
    giveChange(document, textChange);
    return true;
}


/// Converts this textchange to a textual representation
QString MergableChangeGroup::toString()
{
    return QStringLiteral("Complex::%1").arg(ChangeGroup::toString());
}


/// Converts the textchangeList as as string
/// The format is the following:
/// <offset>:<length>:<str>,...
///
/// sample:
/// 0:2:ABC,1:2:QW
QString MergableChangeGroup::toSingleTextChangeTestString()
{
    QString result;
    foreach (AbstractRangedChange* abstractChange, textChangeList_) {
        TextChange* change = dynamic_cast<TextChange*>(abstractChange);
        if (change) {
            if (!result.isEmpty()) result.append(",");
            result.append(QStringLiteral("%1:%2:%3").arg(change->offset()).arg(change->docLength()).arg(change->storedText()));
        }
    }
    return result;
}


/// Moves all textchanges from the given group to this group
/// @param group the group to move the selection from
void MergableChangeGroup::moveChangesFromGroup(TextDocument* doc, ChangeGroup* group)
{
    //qlog_info() << "moveChangeFromGroup **************** (MERGE)";
    //qlog_info() << "A:" << this->toString();
    //qlog_info() << "B:" << group->toString();

    // process all changes
    for (size_t i = 0, cnt = group->size(); i < cnt; ++i) {
        Change* change = group->at(i);

        // handle nested groups
        if (change->isGroup()) {
            ChangeGroup* childGroup = dynamic_cast<ChangeGroup*>(change);
            if (childGroup) {
                moveChangesFromGroup(doc, childGroup);
                delete childGroup;
                continue;
            }
        }

        // merge the the change
        giveChange(doc, change);
    }
    group->clear(false); // no delete we've taken the ownership

    // next we need to compress the changes (merging changes that are next to eachother)
    compressChanges(doc);

    // when it's a complex text change, also move the selection to this group
    MergableChangeGroup* complexGroup = dynamic_cast<MergableChangeGroup*>(group);
    if (complexGroup)  {
        qSwap(newSelection_, complexGroup->newSelection_);
    }
}


/// merges the given textchange as a group.
/// @param document the document this merge is for
/// @param textChange the textchange to merge
/// @return true if the textchange was merged as group.
bool MergableChangeGroup::mergeAsGroup(TextDocument* document, Change* textChange)
{
    Q_UNUSED(document);
    if (!textChange->isGroup()) { return false; }

    ChangeGroup* group = dynamic_cast<ChangeGroup*>(textChange);
    if(!group) { return false; }

    // move all changes from the other group to this group
    moveChangesFromGroup(document, group);

    delete textChange;
    return true;
}


// merge the text selection
/// @param document the document this merge is for
/// @param textChange the textchange to merge
/// @return true if the textchange was merged as group.
bool MergableChangeGroup::mergeAsSelection(TextDocument* document, Change* textChange)
{
    Q_UNUSED(document);
    SelectionChange* selectionChange = dynamic_cast<SelectionChange*>(textChange);
    if (!selectionChange) { return false; }

    // simply take the selection from the textchange and make this the new selection
    delete newSelection_;
    newSelection_ = selectionChange->takeRangeSet();
    delete textChange;
    return true;
}


/// Compresses the textchanges
/// @param document the document to merge the changes for
void MergableChangeGroup::compressTextChanges(TextDocument* document)
{
    // compress single text changes
    for (qsizetype i = 0;  i< textChangeList_.size(); ++i) {
        AbstractRangedChange* change1 = textChangeList_.at(i);

        // find the next text change
        for (qsizetype j = i + 1; j < textChangeList_.size(); ++j) {
            AbstractRangedChange* change2 = textChangeList_.at(j);
            if( change1->giveAndMerge( document, change2 ) ) {
                textChangeList_.takeAt(j);  // just take it, the item is already deleted by the merge
                --j;
            }
        } // for j
    } // for i
}


/// This is going to be the magic method that's going to merge all changes
/// @param document the document to merge the changes for
void MergableChangeGroup::compressChanges(TextDocument* document)
{
    compressTextChanges(document);
}


} // edbee
