
/*
   SPDX-FileCopyrightText: 2003-2007 Clarence Dang <dang@kde.org>

   SPDX-License-Identifier: BSD-2-Clause
*/

#define DEBUG_KP_COMMAND_HISTORY 0

#include "kpCommandHistoryBase.h"

#include <climits>

#include <QMenu>

#include <KActionCollection>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KSharedConfig>
#include <KStandardAction>
#include <KStandardShortcut>
#include <KToolBarPopupAction>

#include "document/kpDocument.h"
#include "environments/commands/kpCommandEnvironment.h"
#include "kpCommand.h"
#include "kpDefs.h"
#include "kpLogCategories.h"
#include "mainWindow/kpMainWindow.h"
#include "tools/kpTool.h"

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

static void ClearPointerList(QList<kpCommand *> &list)
{
    qDeleteAll(list);
    list.clear();
}

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

kpCommandHistoryBase::kpCommandHistoryBase(bool doReadConfig, KActionCollection *ac)
{
    m_actionUndo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-undo")), undoActionText(), this);
    ac->addAction(KStandardAction::name(KStandardAction::Undo), m_actionUndo);
    ac->setDefaultShortcuts(m_actionUndo, KStandardShortcut::shortcut(KStandardShortcut::Undo));
    connect(m_actionUndo, &KToolBarPopupAction::triggered, this, &kpCommandHistoryBase::undo);

    m_actionRedo = new KToolBarPopupAction(QIcon::fromTheme(QStringLiteral("edit-redo")), redoActionText(), this);
    ac->addAction(KStandardAction::name(KStandardAction::Redo), m_actionRedo);
    ac->setDefaultShortcuts(m_actionRedo, KStandardShortcut::shortcut(KStandardShortcut::Redo));
    connect(m_actionRedo, &KToolBarPopupAction::triggered, this, &kpCommandHistoryBase::redo);

    m_actionUndo->setEnabled(false);
    m_actionRedo->setEnabled(false);

    connect(m_actionUndo->popupMenu(), &QMenu::triggered, this, &kpCommandHistoryBase::undoUpToNumber);

    connect(m_actionRedo->popupMenu(), &QMenu::triggered, this, &kpCommandHistoryBase::redoUpToNumber);

    m_undoMinLimit = 10;
    m_undoMaxLimit = 500;
    m_undoMaxLimitSizeLimit = 16 * 1048576;

    m_documentRestoredPosition = 0;

    if (doReadConfig) {
        readConfig();
    }
}

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

kpCommandHistoryBase::~kpCommandHistoryBase()
{
    ::ClearPointerList(m_undoCommandList);
    ::ClearPointerList(m_redoCommandList);
}

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

// public
int kpCommandHistoryBase::undoLimit() const
{
    return undoMinLimit();
}

// public
void kpCommandHistoryBase::setUndoLimit(int limit)
{
    setUndoMinLimit(limit);
}

// public
int kpCommandHistoryBase::undoMinLimit() const
{
    return m_undoMinLimit;
}

// public
void kpCommandHistoryBase::setUndoMinLimit(int limit)
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::setUndoMinLimit(" << limit << ")";
#endif

    if (limit < 1 || limit > 5000 /*"ought to be enough for anybody"*/) {
        qCCritical(kpLogCommands) << "kpCommandHistoryBase::setUndoMinLimit(" << limit << ")";
        return;
    }

    if (limit == m_undoMinLimit) {
        return;
    }

    m_undoMinLimit = limit;
    trimCommandListsUpdateActions();
}

// public
int kpCommandHistoryBase::undoMaxLimit() const
{
    return m_undoMaxLimit;
}

// public
void kpCommandHistoryBase::setUndoMaxLimit(int limit)
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::setUndoMaxLimit(" << limit << ")";
#endif

    if (limit < 1 || limit > 5000 /*"ought to be enough for anybody"*/) {
        qCCritical(kpLogCommands) << "kpCommandHistoryBase::setUndoMaxLimit(" << limit << ")";
        return;
    }

    if (limit == m_undoMaxLimit) {
        return;
    }

    m_undoMaxLimit = limit;
    trimCommandListsUpdateActions();
}

// public
kpCommandSize::SizeType kpCommandHistoryBase::undoMaxLimitSizeLimit() const
{
    return m_undoMaxLimitSizeLimit;
}

// public
void kpCommandHistoryBase::setUndoMaxLimitSizeLimit(kpCommandSize::SizeType sizeLimit)
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::setUndoMaxLimitSizeLimit(" << sizeLimit << ")";
#endif

    if (sizeLimit < 0 || sizeLimit > (500 * 1048576) /*"ought to be enough for anybody"*/) {
        qCCritical(kpLogCommands) << "kpCommandHistoryBase::setUndoMaxLimitSizeLimit(" << sizeLimit << ")";
        return;
    }

    if (sizeLimit == m_undoMaxLimitSizeLimit) {
        return;
    }

    m_undoMaxLimitSizeLimit = sizeLimit;
    trimCommandListsUpdateActions();
}

// public
void kpCommandHistoryBase::readConfig()
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::readConfig()";
#endif
    KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral(kpSettingsGroupUndoRedo));

    setUndoMinLimit(cfg.readEntry(kpSettingUndoMinLimit, undoMinLimit()));
    setUndoMaxLimit(cfg.readEntry(kpSettingUndoMaxLimit, undoMaxLimit()));
    setUndoMaxLimitSizeLimit(cfg.readEntry<kpCommandSize::SizeType>(kpSettingUndoMaxLimitSizeLimit, undoMaxLimitSizeLimit()));

    trimCommandListsUpdateActions();
}

// public
void kpCommandHistoryBase::writeConfig()
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::writeConfig()";
#endif
    KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral(kpSettingsGroupUndoRedo));

    cfg.writeEntry(kpSettingUndoMinLimit, undoMinLimit());
    cfg.writeEntry(kpSettingUndoMaxLimit, undoMaxLimit());
    cfg.writeEntry<kpCommandSize::SizeType>(kpSettingUndoMaxLimitSizeLimit, undoMaxLimitSizeLimit());

    cfg.sync();
}

// public
void kpCommandHistoryBase::addCommand(kpCommand *command, bool execute)
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::addCommand(" << command << ",execute=" << execute
                           << ")"
#endif

        if (execute)
    {
        command->execute();
    }

    m_undoCommandList.push_front(command);
    ::ClearPointerList(m_redoCommandList);

#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "\tdocumentRestoredPosition=" << m_documentRestoredPosition;
#endif
    if (m_documentRestoredPosition != INT_MAX) {
        if (m_documentRestoredPosition > 0) {
            m_documentRestoredPosition = INT_MAX;
        } else {
            m_documentRestoredPosition--;
        }
#if DEBUG_KP_COMMAND_HISTORY
        qCDebug(kpLogCommands) << "\t\tdocumentRestoredPosition=" << m_documentRestoredPosition;
#endif
    }

    trimCommandListsUpdateActions();
}

// public
void kpCommandHistoryBase::clear()
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::clear()";
#endif

    ::ClearPointerList(m_undoCommandList);
    ::ClearPointerList(m_redoCommandList);

    m_documentRestoredPosition = 0;

    updateActions();
}

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

// protected slot
void kpCommandHistoryBase::undoInternal()
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::undoInternal()";
#endif

    kpCommand *undoCommand = nextUndoCommand();
    if (!undoCommand) {
        return;
    }

    undoCommand->unexecute();

    m_undoCommandList.erase(m_undoCommandList.begin());
    m_redoCommandList.push_front(undoCommand);

#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "\tdocumentRestoredPosition=" << m_documentRestoredPosition;
#endif
    if (m_documentRestoredPosition != INT_MAX) {
        m_documentRestoredPosition++;
        if (m_documentRestoredPosition == 0)
            Q_EMIT documentRestored();
#if DEBUG_KP_COMMAND_HISTORY
        qCDebug(kpLogCommands) << "\t\tdocumentRestoredPosition=" << m_documentRestoredPosition;
#endif
    }
}

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

// protected slot
void kpCommandHistoryBase::redoInternal()
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::redoInternal()";
#endif

    kpCommand *redoCommand = nextRedoCommand();
    if (!redoCommand) {
        return;
    }

    redoCommand->execute();

    m_redoCommandList.erase(m_redoCommandList.begin());
    m_undoCommandList.push_front(redoCommand);

#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "\tdocumentRestoredPosition=" << m_documentRestoredPosition;
#endif
    if (m_documentRestoredPosition != INT_MAX) {
        m_documentRestoredPosition--;
        if (m_documentRestoredPosition == 0) {
            Q_EMIT documentRestored();
        }
#if DEBUG_KP_COMMAND_HISTORY
        qCDebug(kpLogCommands) << "\t\tdocumentRestoredPosition=" << m_documentRestoredPosition;
#endif
    }
}

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

// public slot virtual
void kpCommandHistoryBase::undo()
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::undo()";
#endif

    undoInternal();
    trimCommandListsUpdateActions();
}

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

// public slot virtual
void kpCommandHistoryBase::redo()
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::redo()";
#endif

    redoInternal();
    trimCommandListsUpdateActions();
}

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

// public slot virtual
void kpCommandHistoryBase::undoUpToNumber(QAction *which)
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::undoUpToNumber(" << which << ")";
#endif

    for (int i = 0; i <= which->data().toInt() && !m_undoCommandList.isEmpty(); i++) {
        undoInternal();
    }

    trimCommandListsUpdateActions();
}

// public slot virtual
void kpCommandHistoryBase::redoUpToNumber(QAction *which)
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::redoUpToNumber(" << which << ")";
#endif

    for (int i = 0; i <= which->data().toInt() && !m_redoCommandList.isEmpty(); i++) {
        redoInternal();
    }

    trimCommandListsUpdateActions();
}

// protected
QString kpCommandHistoryBase::undoActionText() const
{
    kpCommand *undoCommand = nextUndoCommand();

    return (undoCommand) ? i18n("&Undo: %1", undoCommand->name()) : i18n("&Undo");
}

// protected
QString kpCommandHistoryBase::redoActionText() const
{
    kpCommand *redoCommand = nextRedoCommand();

    return (redoCommand) ? i18n("&Redo: %1", redoCommand->name()) : i18n("&Redo");
}

// protected
QString kpCommandHistoryBase::undoActionToolTip() const
{
    kpCommand *undoCommand = nextUndoCommand();

    return (undoCommand) ? i18n("Undo: %1", undoCommand->name()) : i18n("Undo");
}

// protected
QString kpCommandHistoryBase::redoActionToolTip() const
{
    kpCommand *redoCommand = nextRedoCommand();

    return (redoCommand) ? i18n("Redo: %1", redoCommand->name()) : i18n("Redo");
}

// protected
void kpCommandHistoryBase::trimCommandListsUpdateActions()
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::trimCommandListsUpdateActions()";
#endif

    trimCommandLists();
    updateActions();
}

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

// protected
void kpCommandHistoryBase::trimCommandList(QList<kpCommand *> &commandList)
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::trimCommandList()";
    QTime timer;
    timer.start();

    qCDebug(kpLogCommands) << "\tsize=" << commandList.size() << "    undoMinLimit=" << m_undoMinLimit << " undoMaxLimit=" << m_undoMaxLimit
                           << " undoMaxLimitSizeLimit=" << m_undoMaxLimitSizeLimit;
#endif
    if (commandList.size() <= m_undoMinLimit) {
#if DEBUG_KP_COMMAND_HISTORY
        qCDebug(kpLogCommands) << "\t\tsize under undoMinLimit - done";
#endif
        return;
    }

#if DEBUG_KP_COMMAND_HISTORY && 0
    qCDebug(kpLogCommands) << "\tsize over undoMinLimit - iterating thru cmds:";
#endif

    QList<kpCommand *>::iterator it = commandList.begin();
    int upto = 0;

    kpCommandSize::SizeType sizeSoFar = 0;

    while (it != commandList.end()) {
        bool advanceIt = true;

        if (sizeSoFar <= m_undoMaxLimitSizeLimit) {
            sizeSoFar += (*it)->size();
        }

#if DEBUG_KP_COMMAND_HISTORY && 0
        qCDebug(kpLogCommands) << "\t\t" << upto << ":"
                               << " name='" << (*it)->name() << "' size=" << (*it)->size() << "    sizeSoFar=" << sizeSoFar;
#endif

        if (upto >= m_undoMinLimit) {
            if (upto >= m_undoMaxLimit || sizeSoFar > m_undoMaxLimitSizeLimit) {
#if DEBUG_KP_COMMAND_HISTORY && 0
                qCDebug(kpLogCommands) << "\t\t\tkill";
#endif
                delete (*it);
                it = commandList.erase(it);
                advanceIt = false;
            }
        }

        if (advanceIt) {
            it++;
        }
        upto++;
    }

#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "\ttook " << timer.elapsed() << "ms";
#endif
}

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

// protected
void kpCommandHistoryBase::trimCommandLists()
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::trimCommandLists()";
#endif

    trimCommandList(m_undoCommandList);
    trimCommandList(m_redoCommandList);

#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "\tdocumentRestoredPosition="
                           << m_documentRestoredPosition
#endif
        if (m_documentRestoredPosition != INT_MAX)
    {
#if DEBUG_KP_COMMAND_HISTORY
        qCDebug(kpLogCommands) << "\t\tundoCmdList.size=" << m_undoCommandList.size() << " redoCmdList.size=" << m_redoCommandList.size();
#endif
        if (m_documentRestoredPosition > static_cast<int>(m_redoCommandList.size())
            || -m_documentRestoredPosition > static_cast<int>(m_undoCommandList.size())) {
#if DEBUG_KP_COMMAND_HISTORY
            qCDebug(kpLogCommands) << "\t\t\tinvalidate documentRestoredPosition";
#endif
            m_documentRestoredPosition = INT_MAX;
        }
    }
}

static void populatePopupMenu(QMenu *popupMenu, const QString &undoOrRedo, const QList<kpCommand *> &commandList)
{
    if (!popupMenu) {
        return;
    }

    popupMenu->clear();

    QList<kpCommand *>::const_iterator it = commandList.begin();
    int i = 0;
    while (i < 10 && it != commandList.end()) {
        QAction *action = new QAction(i18n("%1: %2", undoOrRedo, (*it)->name()), popupMenu);
        action->setData(i);
        popupMenu->addAction(action);
        i++;
        it++;
    }

    if (it != commandList.end()) {
        // TODO: maybe have a scrollview show all the items instead, like KOffice in KDE 3
        // LOCOMPAT: should be centered text.
        popupMenu->addSection(i18np("%1 more item", "%1 more items", commandList.size() - i));
    }
}

// protected
void kpCommandHistoryBase::updateActions()
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::updateActions()";
#endif

    m_actionUndo->setEnabled(static_cast<bool>(nextUndoCommand()));
    // Don't want to keep changing toolbar text.
    // TODO: As a bad side-effect, the menu doesn't have "Undo: <action>"
    //       anymore.  In any case, the KDE4 KToolBarPopupAction
    //       sucks in menus as it forces the clicking of a submenu.  IMO,
    //       there should be no submenu in the menu.
    // m_actionUndo->setText (undoActionText ());

    // But in icon mode, a tooltip with context is useful.
    m_actionUndo->setToolTip(undoActionToolTip());
#if DEBUG_KP_COMMAND_HISTORY
    QTime timer;
    timer.start();
#endif
    populatePopupMenu(m_actionUndo->popupMenu(), i18n("Undo"), m_undoCommandList);
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "\tpopuplatePopupMenu undo=" << timer.elapsed() << "ms";
#endif

    m_actionRedo->setEnabled(static_cast<bool>(nextRedoCommand()));
    // Don't want to keep changing toolbar text.
    // TODO: As a bad side-effect, the menu doesn't have "Undo: <action>"
    //       anymore.  In any case, the KDE4 KToolBarPopupAction
    //       sucks in menus as it forces the clicking of a submenu.  IMO,
    //       there should be no submenu in the menu.
    // m_actionRedo->setText (redoActionText ());

    // But in icon mode, a tooltip with context is useful.
    m_actionRedo->setToolTip(redoActionToolTip());
#if DEBUG_KP_COMMAND_HISTORY
    timer.restart();
#endif
    populatePopupMenu(m_actionRedo->popupMenu(), i18n("Redo"), m_redoCommandList);
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "\tpopuplatePopupMenu redo=" << timer.elapsed() << "ms";
#endif
}

// public
kpCommand *kpCommandHistoryBase::nextUndoCommand() const
{
    if (m_undoCommandList.isEmpty()) {
        return nullptr;
    }

    return m_undoCommandList.first();
}

// public
kpCommand *kpCommandHistoryBase::nextRedoCommand() const
{
    if (m_redoCommandList.isEmpty()) {
        return nullptr;
    }

    return m_redoCommandList.first();
}

// public
void kpCommandHistoryBase::setNextUndoCommand(kpCommand *command)
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::setNextUndoCommand(" << command << ")";
#endif

    if (m_undoCommandList.isEmpty()) {
        return;
    }

    delete *m_undoCommandList.begin();
    *m_undoCommandList.begin() = command;

    trimCommandListsUpdateActions();
}

// public slot virtual
void kpCommandHistoryBase::documentSaved()
{
#if DEBUG_KP_COMMAND_HISTORY
    qCDebug(kpLogCommands) << "kpCommandHistoryBase::documentSaved()";
#endif

    m_documentRestoredPosition = 0;
}

#include "moc_kpCommandHistoryBase.cpp"
