#include "./undocommands.h"
#include "./stacksupport.h"

#include "../model/entrymodel.h"
#include "../model/fieldmodel.h"

#include <passwordfile/io/entry.h>

#include <QApplication>

using namespace std;
using namespace Io;

namespace QtGui {

/*!
 * \class CustomUndoCommand
 * \brief The CustomUndoCommand class acts as base class for undo commands used within the
 *        models of the application.
 * \sa http://qt-project.org/doc/qt-5/qundocommand.html
 */

/*!
 * \brief Constructs a new custom undo command with the specified \a stackSupport.
 */
CustomUndoCommand::CustomUndoCommand(StackSupport *stackSupport)
    : m_stackSupport(stackSupport)
    , m_redoResult(false)
    , m_undoResult(true)
    , m_noop(false)
{
}

void CustomUndoCommand::redo()
{
    if (m_undoResult) {
        StackAbsorper stackAbsorper(m_stackSupport);
        m_redoResult = internalRedo();
    }
}

void CustomUndoCommand::undo()
{
    if (m_redoResult) {
        StackAbsorper stackAbsorper(m_stackSupport);
        m_undoResult = internalUndo();
    }
}

/*!
 * \fn CustomUndoCommand::internalRedo()
 * \brief This method is internally called to perform the redo action.
 */

/*!
 * \fn CustomUndoCommand::internalUndo()
 * \brief This method is internally called to perform the undo action.
 */

/*!
 * \class FieldModelSetValueCommand
 * \brief Sets the value for the specified index and role in the specified field model.
 */

/// \cond

static QString getFieldName(FieldModel *model, int row, const QModelIndex &index)
{
    return model->index(row, 0, index.parent()).data().toString();
}

/// \endcond

/*!
 * \brief Constructs a new command.
 */
FieldModelSetValueCommand::FieldModelSetValueCommand(FieldModel *model, const QModelIndex &index, const QVariant &value, int role)
    : CustomUndoCommand(model)
    , m_account(model->accountEntry())
    , m_model(model)
    , m_row(index.row())
    , m_col(index.column())
    , m_newValue(value)
    , m_oldValue(model->data(index, role))
    , m_role(role)
{
    switch (role) {
    case Qt::DisplayRole:
    case Qt::EditRole:
        switch (m_col) {
        case 0:
            if (m_oldValue.toString().isEmpty()) {
                setText(QApplication::translate("undocommands", "setting field name to »%1«").arg(m_newValue.toString()));
            } else {
                setText(QApplication::translate("undocommands", "setting field name »%1« to »%2«").arg(m_oldValue.toString(), m_newValue.toString()));
            }
            break;
        case 1:
            if (const auto fieldName = getFieldName(model, m_row, index); !fieldName.isEmpty()) {
                setText(QApplication::translate("undocommands", "setting value of »%1« field").arg(fieldName));
            } else {
                setText(QApplication::translate("undocommands", "setting value of empty field"));
            }
            break;
        }
        break;
    case FieldTypeRole:
        setText(QApplication::translate("undocommands", "setting type of »%1« field").arg(getFieldName(model, m_row, index)));
        break;
    default:
        setText(QApplication::translate("undocommands", "setting field property in row »%1«").arg(m_row + 1));
    }
    setNoop(m_oldValue == m_newValue);
}

bool FieldModelSetValueCommand::internalRedo()
{
    m_model->setAccountEntry(m_account);
    return m_model->setData(m_model->index(m_row, m_col), m_newValue, m_role);
}

bool FieldModelSetValueCommand::internalUndo()
{
    m_model->setAccountEntry(m_account);
    return m_model->setData(m_model->index(m_row, m_col), m_oldValue, m_role);
}

/*!
 * \class FieldModelInsertRowsCommand
 * \brief Inserts the specified number of rows before the specified row in the specified field model.
 */

/*!
 * \brief Constructs a new command.
 */
FieldModelInsertRowsCommand::FieldModelInsertRowsCommand(FieldModel *model, int row, int count)
    : CustomUndoCommand(model)
    , m_account(model->accountEntry())
    , m_model(model)
    , m_row(row)
    , m_count(count)
{
    setText(QApplication::translate("undocommands", "insertion of %1 row(s) before row %2", nullptr, count).arg(count).arg(row + 1));
}

bool FieldModelInsertRowsCommand::internalRedo()
{
    m_model->setAccountEntry(m_account);
    return m_model->insertRows(m_row, m_count, QModelIndex());
}

bool FieldModelInsertRowsCommand::internalUndo()
{
    m_model->setAccountEntry(m_account);
    return m_model->removeRows(m_row, m_count, QModelIndex());
}

/*!
 * \class FieldModelRemoveRowsCommand
 * \brief Removes the specified number of rows at the specified row in the specified field model.
 */

/*!
 * \brief Constructs a new command.
 */
FieldModelRemoveRowsCommand::FieldModelRemoveRowsCommand(FieldModel *model, int row, int count)
    : CustomUndoCommand(model)
    , m_account(model->accountEntry())
    , m_model(model)
    , m_row(row)
    , m_count(count)
{
    if (count == 1) {
        setText(QApplication::translate("undocommands", "removal of row %1", nullptr, count).arg(row + 1));
    } else {
        setText(QApplication::translate("undocommands", "removal of the rows %1 to %2", nullptr, count).arg(row + 1).arg(row + count));
    }
}

bool FieldModelRemoveRowsCommand::internalRedo()
{
    m_model->setAccountEntry(m_account);
    if (m_values.isEmpty()) {
        for (int row = m_row, end = m_row + m_count; row < end; ++row) {
            if (const Field *const field = m_model->field(static_cast<std::size_t>(row))) {
                m_values << Field(*field);
            }
        }
    }
    return m_model->removeRows(m_row, m_count, QModelIndex());
}

bool FieldModelRemoveRowsCommand::internalUndo()
{
    m_model->setAccountEntry(m_account);
    bool res = m_model->insertRows(m_row, m_count, QModelIndex());
    for (int row = m_row, end = m_row + m_count, value = 0, values = static_cast<int>(m_values.size()); row < end && value < values; ++row, ++value) {
        m_model->setData(m_model->index(row, 0), QString::fromStdString(m_values.at(value).name()), Qt::EditRole);
        m_model->setData(m_model->index(row, 1), QString::fromStdString(m_values.at(value).value()), Qt::EditRole);
        m_model->setData(m_model->index(row, 0), static_cast<int>(m_values.at(value).type()), FieldTypeRole);
    }
    return res;
}

/*!
 * \brief Stores the entry path for the specified \a model and \a index in \a res.
 */
static void indexToPath(EntryModel *model, const QModelIndex &index, list<string> &res)
{
    res.clear();
    if (Entry *entry = model->entry(index)) {
        entry->path(res);
    }
}

/*!
 * \brief Fetches the entry for the specified \a model and \a path.
 * \remarks The \a path will be modified. To prevent this use entryFromPathCpy().
 */
static Entry *entryFromPath(EntryModel *model, list<string> &path)
{
    if (NodeEntry *rootEntry = model->rootEntry()) {
        return rootEntry->entryByPath(path);
    }
    return nullptr;
}

/*!
 * \brief Fetches the entry for the specified \a model and \a path.
 */
static Entry *entryFromPathCpy(EntryModel *model, list<string> path)
{
    return entryFromPath(model, path);
}

/*!
 * \class EntryModelSetValueCommand
 * \brief Sets the value for the specified index and role in the specified entry model.
 */

/*!
 * \brief Constructs a new command.
 */
EntryModelSetValueCommand::EntryModelSetValueCommand(EntryModel *model, const QModelIndex &index, const QVariant &value, int role)
    : CustomUndoCommand(model)
    , m_model(model)
    , m_newValue(value)
    , m_oldValue(model->data(index, role))
    , m_role(role)
{
    indexToPath(model, index, m_path);
    switch (role) {
    case Qt::DisplayRole:
    case Qt::EditRole:
        if (m_oldValue.toString().isEmpty()) {
            setText(QApplication::translate("undocommands", "setting entry name to »%1«").arg(m_newValue.toString()));
        } else {
            setText(
                QApplication::translate("undocommands", "setting entry name from »%1« to »%2«").arg(m_oldValue.toString(), m_newValue.toString()));
        }
        break;
    default:
        QString name = model->data(model->index(index.row(), 0, index.parent()), Qt::DisplayRole).toString();
        if (name.isEmpty()) {
            setText(QApplication::translate("undocommands", "setting property of an entry"));
        } else {
            setText(QApplication::translate("undocommands", "setting property of entry »%1«").arg(name));
        }
    }
    setNoop(m_oldValue == m_newValue);
}

bool EntryModelSetValueCommand::internalRedo()
{
    if (Entry *entry = entryFromPath(m_model, m_path)) {
        bool res = m_model->setData(m_model->index(entry), m_newValue, m_role);
        m_path.clear();
        entry->path(m_path);
        return res;
    }
    return false;
}

bool EntryModelSetValueCommand::internalUndo()
{
    if (Entry *entry = entryFromPath(m_model, m_path)) {
        bool res = m_model->setData(m_model->index(entry), m_oldValue, m_role);
        m_path.clear();
        entry->path(m_path);
        return res;
    }
    return false;
}

/*!
 * \class EntryModelInsertRowsCommand
 * \brief Modifies the specified number of rows before the specified row in the specified entry model under the specified parent.
 */

/*!
 * \brief Constructs a new command.
 */
EntryModelModifyRowsCommand::EntryModelModifyRowsCommand(EntryModel *model, int row, int count, const QModelIndex &parent)
    : CustomUndoCommand(model)
    , m_model(model)
    , m_row(row)
    , m_count(count)
{
    indexToPath(model, parent, m_parentPath);
}

/*!
 * \brief Destroys the command.
 *
 * Removed entries will be deleted finally.
 */
EntryModelModifyRowsCommand::~EntryModelModifyRowsCommand()
{
    qDeleteAll(m_values);
}

/*!
 * \brief Inserts the buffered entries to the model.
 */
bool EntryModelModifyRowsCommand::insert()
{
    if (Entry *const parentEntry = entryFromPathCpy(m_model, m_parentPath)) {
        if (m_model->insertEntries(m_row, m_model->index(parentEntry), m_values)) {
            m_values.clear();
            return true;
        }
    }
    return false;
}

/*!
 * \brief Removes the entries from the model.
 *
 * The entries and the model have been specified when constructing the class.
 *
 * The removed entries are buffered.
 */
bool EntryModelModifyRowsCommand::remove()
{
    if (Entry *const parentEntry = entryFromPathCpy(m_model, m_parentPath)) {
        m_values = m_model->takeEntries(m_row, m_count, m_model->index(parentEntry));
        return !m_values.isEmpty();
    }
    return false;
}

/*!
 * \class EntryModelInsertRowsCommand
 * \brief Inserts the specified number of rows before the specified row in the specified entry model under the specified parent.
 */

/*!
 * \brief Constructs a new command.
 */
EntryModelInsertRowsCommand::EntryModelInsertRowsCommand(EntryModel *model, int row, int count, const QModelIndex &parent)
    : EntryModelModifyRowsCommand(model, row, count, parent)
{
    setText(QApplication::translate("undocommands", "insertion of %1 entry/entries", 0, count).arg(count));
    switch (m_model->insertType()) {
    case EntryType::Account:
        m_values << new AccountEntry;
        break;
    case EntryType::Node:
        m_values << new NodeEntry;
        break;
    }
}

bool EntryModelInsertRowsCommand::internalRedo()
{
    return insert();
}

bool EntryModelInsertRowsCommand::internalUndo()
{
    return remove();
}

/*!
 * \class EntryModelRemoveRowsCommand
 * \brief Removes the specified number of rows at the specified row in the specified entry model under the specified parent.
 */

/*!
 * \brief Constructs a new command.
 */
EntryModelRemoveRowsCommand::EntryModelRemoveRowsCommand(EntryModel *model, int row, int count, const QModelIndex &parent)
    : EntryModelModifyRowsCommand(model, row, count, parent)
{
    setText(QApplication::translate("undocommands", "removal of %1 entry/entries", 0, count).arg(count));
}

bool EntryModelRemoveRowsCommand::internalRedo()
{
    return remove();
}

bool EntryModelRemoveRowsCommand::internalUndo()
{
    return insert();
}

/*!
 * \class EntryModelMoveRowsCommand
 * \brief Moves the specified rows to the specified destination within the specified entry model.
 */

/*!
 * \brief Constructs a new command.
 */
EntryModelMoveRowsCommand::EntryModelMoveRowsCommand(
    EntryModel *model, const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild)
    : CustomUndoCommand(model)
    , m_model(model)
    , m_sourceRow(sourceRow)
    , m_count(count)
    , m_destChild(destinationChild)
{
    indexToPath(model, sourceParent, m_sourceParentPath);
    indexToPath(model, destinationParent, m_destParentPath);
    setText(QApplication::translate("undocommands", "move of %1 entry/entries", 0, count).arg(count));
}

bool EntryModelMoveRowsCommand::internalRedo()
{
    if (m_count) {
        Entry *sourceParentEntry = entryFromPathCpy(m_model, m_sourceParentPath);
        Entry *destParentEntry = entryFromPathCpy(m_model, m_destParentPath);
        if (sourceParentEntry && destParentEntry) {
            return m_model->moveRows(m_model->index(sourceParentEntry), m_sourceRow, m_count, m_model->index(destParentEntry), m_destChild);
        }
        return false;
    }
    return true;
}

bool EntryModelMoveRowsCommand::internalUndo()
{
    if (!m_count) {
        return true;
    }
    Entry *const sourceParentEntry = entryFromPathCpy(m_model, m_sourceParentPath);
    Entry *const destParentEntry = entryFromPathCpy(m_model, m_destParentPath);
    if (sourceParentEntry && destParentEntry) {
        auto sourceRow = m_destChild, destChild = m_sourceRow;
        // moves within the same parent needs special consideration
        if (sourceParentEntry == destParentEntry) {
            // move entry down
            if (m_sourceRow < m_destChild) {
                sourceRow -= m_count;
                // move entry up
            } else if (m_sourceRow > m_destChild) {
                destChild += m_count;
                // keep entry were it is
            } else {
                return true;
            }
        }
        return m_model->moveRows(m_model->index(destParentEntry), sourceRow, m_count, m_model->index(sourceParentEntry), destChild);
    }
    return false;
}
} // namespace QtGui
