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

#include "change.h"

#include "edbee/models/textdocument.h"
#include "edbee/models/changes/textchange.h"

#include "edbee/debug.h"

namespace edbee {

/// a virtual empty destructor
Change::~Change()
{
}


/// Reverts the given operation
void Change::revert(TextDocument*)
{
    Q_ASSERT(false);
}


/// Gives the change and merges it if possible. This method should return false if the change couldn't be merged.
/// When returning true the ownership of the given textchange is transfered to this class.
/// @param document the document this change is for
/// @param textChange the textchange
/// @return true if the merge succeeded. The textChange ownership is only tranfered if true if returend
bool Change::giveAndMerge(TextDocument* document, Change* textChange)
{
    Q_UNUSED(document)
    Q_UNUSED(textChange )
    return false;
}


/// Return true if the change can be reverted
bool Change::canUndo()
{
    return false;
}


/// This flag is used to mark this stack item as non-persistence requirable
/// The default behaviour is that every textchange requires persistence. It is also possible to
/// have certain changes that do not require persitence but should be placed on the undo stack
bool Change::isPersistenceRequired()
{
    return true;
}


/// A text command can belong to a controller/view
/// When it's a view only command. The undo only applies only to this view
/// warning a DOCUMENT change may NEVER return a controllerContext!!
TextEditorController*Change::controllerContext()
{
    return nullptr;
}


/// Checks if the given change is a document change
bool Change::isDocumentChange()
{
     return controllerContext() == nullptr;
}


/// return if this change is a group change. When an object is group change
/// it should be inherited by TextChangeGroup
bool Change::isGroup()
{
    return false;
}


//--------------------------------------------------------------


/// Empty change doesn't do anything
bool EmptyDocumentChange::isPersistenceRequired()
{
    return false;
}


/// does nothing
void EmptyDocumentChange::execute(TextDocument*)
{
}

/// does nothing
void EmptyDocumentChange::revert(TextDocument*)
{
}

/// returns the name of the textchange
QString EmptyDocumentChange::toString()
{
     return "EmptyDocumentTextChange";
}


//--------------------------------------------------------------


/// A controller specific textcommand. Warning you should NOT modify the textdocument!
/// @param controller the controller this change is for
ControllerChange::ControllerChange(TextEditorController* controller)
    : controllerRef_(controller)
{
}


/// A text command can belong to a controller/view
/// When it's a view only command. The undo only applies only to this view
TextEditorController* ControllerChange::controllerContext()
{
    return controllerRef_;
}


/// returns the controller
TextEditorController* ControllerChange::controller()
{
    return controllerRef_;
}


//--------------------------------------------------------------


/// default contructor
/// @param controller the controller this groups belongs to
ChangeGroup::ChangeGroup(TextEditorController* controller)
    : ControllerChange( controller )
{
}


/// The destructor deletes all added textchanges
ChangeGroup::~ChangeGroup()
{
    qDeleteAll(changeList_);
    changeList_.clear();
}


/// A group change is a group change, so this method returns true :)
bool ChangeGroup::isGroup()
{
    return true;
}


/// This method is called it the group is discardable. A discardable group will be optimized away if
/// the group is empty, or if there's a single item in the group. A none-discardable group will
/// always remain
bool ChangeGroup::isDiscardable()
{
    return true;
}

/// This method is called if the group is closed and is added to the stack
/// Default implementation is to do nothing
void ChangeGroup::groupClosed()
{
}


/// executes this command group
/// @param document the document the document to execute this for
void ChangeGroup::execute(TextDocument* document)
{
    Q_UNUSED(document)
    for (size_t i = 0, cnt = size(); i < cnt; ++i) {
        at(i)->execute(document);
    }
}


/// Reverts the command gorup
void ChangeGroup::revert(TextDocument* document)
{
    Q_UNUSED(document)
    for (size_t i = size() - 1; i != std::string::npos; --i ) {
        at(i)->revert(document);
    }
}


/// When another group is given, a merge is performed
bool ChangeGroup::giveAndMerge(TextDocument* document, Change* textChange)
{
    Q_UNUSED(document);
    if( textChange->isGroup()) {
        // move all changes from the given change-group to the new one
        ChangeGroup* inGroup = dynamic_cast<ChangeGroup*>(textChange);
        for (size_t i = 0, cnt = inGroup->size(); i < cnt; ++i) {
            giveChange(document, inGroup->at(i));
        }
        while (inGroup->takeLast());
        delete textChange;

        return true;
    }
    return false;
}


/// This method flattens the undo-group by expanding all subgroups to local groups
void ChangeGroup::flatten()
{
    for (size_t idx = 0; idx < size(); ++idx ) {
        ChangeGroup* group = dynamic_cast<ChangeGroup*>(at(idx));
        if (group) {
            take(idx);
            for (size_t i = 0; i<group->size(); ++i ) {
                ChangeGroup* subGroup = dynamic_cast<ChangeGroup*>(group->at(i));
                if (subGroup) {
                    subGroup->flatten();   // flatten the group
                    delete subGroup;
                    if (i >= group->size()) break; // just make sure empty groups go right (in theory empty groups aren't possible)
                }
                changeList_.insert(static_cast<qsizetype>(idx + i), group->at(i));
            }
            group->changeList_.clear();
            delete group;
        }
    }
}


void ChangeGroup::giveChange(TextDocument* doc, Change* change)
{
    Q_UNUSED(doc)
    changeList_.append(change);
}


Change* ChangeGroup::at(size_t idx)
{
    Q_ASSERT(idx < size());
    return changeList_.at(static_cast<qsizetype>(idx));
}


Change* ChangeGroup::take(size_t idx)
{
    Q_ASSERT(idx < size());

    Change* change = at(idx);
    changeList_.removeAt(static_cast<qsizetype>(idx));
    return change;
}


size_t ChangeGroup::size()
{
    return static_cast<size_t>(changeList_.size());
}


void ChangeGroup::clear(bool performDelete)
{
    if (performDelete) qDeleteAll(changeList_);
    changeList_.clear();
}


/// Returns the last change in the change group
/// @return the last textchange
Change* ChangeGroup::last()
{
    if (size() == 0) { return nullptr; }
    return at(size() - 1);
}


/// Takes the ownership of the last element and removes it from the stack
/// @return the last textchange
Change* ChangeGroup::takeLast()
{
    if (size() == 0) { return nullptr; }
    return take(size() - 1);
}


/// The total number of items in the list (excluding the group items)
/// @return the number of items recussive (iterating) all groups
size_t ChangeGroup::recursiveSize()
{
    size_t itemCount = 0;
    for (size_t i=0, cnt=size(); i < cnt; ++i) {
        ChangeGroup* group = dynamic_cast<ChangeGroup*>(at(i));
        if (group) {
            itemCount += group->size();
        } else {
            ++itemCount;
        }
    }
    return itemCount;
}


/// if this commandgroup only contains commands for a single controller context
/// Then this context is returned else 0 is returned
TextEditorController* ChangeGroup::controllerContext()
{
    TextEditorController* context = nullptr;
    for (size_t i = size() - 1; i != std::string::npos; --i) {
        TextEditorController* commandContext = at(i)->controllerContext();

        // multiple context in 1 group means it's a 'hard' undo
        if (commandContext == nullptr) return nullptr;  /// 0 is always 0!
        if (commandContext && context && commandContext != context) { return nullptr; }
        if (!context && commandContext) {
            context = commandContext;
        }
    }
    return context;
}


/// Converts this change group to a string
QString ChangeGroup::toString()
{
    QString s;
    s = QStringLiteral("%1/%2").arg(size()).arg(recursiveSize());

    QString extra;
    for (size_t i = 0,cnt = size(); i < cnt; ++i) {
        extra.append(QStringLiteral(" - %1: ").arg(i));
        extra.append(at(i)->toString());
        extra.append("\n");
    }
    return QStringLiteral("ChangeGroup(%1)\n%2").arg(s, extra);
}



} // edbee
