#include "commands.hpp"

#include <algorithm>
#include <cmath>
#include <sstream>
#include <unordered_set>

#include <apps/opencs/model/world/columns.hpp>
#include <apps/opencs/model/world/land.hpp>
#include <apps/opencs/model/world/record.hpp>
#include <apps/opencs/model/world/universalid.hpp>

#include <components/esm3/loadland.hpp>
#include <components/esm3/loadpgrd.hpp>
#include <components/misc/constants.hpp>

#include <QAbstractItemModel>
#include <QAbstractProxyModel>

#include "cellcoordinates.hpp"
#include "idtable.hpp"
#include "idtree.hpp"
#include "nestedtablewrapper.hpp"
#include "pathgrid.hpp"

CSMWorld::TouchCommand::TouchCommand(IdTable& table, const std::string& id, QUndoCommand* parent)
    : QUndoCommand(parent)
    , mTable(table)
    , mId(id)
    , mOld(nullptr)
    , mChanged(false)
{
    setText(("Touch " + mId).c_str());
}

void CSMWorld::TouchCommand::redo()
{
    mOld = mTable.getRecord(mId).clone();
    mChanged = mTable.touchRecord(mId);
}

void CSMWorld::TouchCommand::undo()
{
    if (mChanged)
    {
        mTable.setRecord(mId, std::move(mOld));
        mChanged = false;
    }
}

CSMWorld::ImportLandTexturesCommand::ImportLandTexturesCommand(
    IdTable& landTable, IdTable& ltexTable, QUndoCommand* parent)
    : QUndoCommand(parent)
    , mLands(landTable)
    , mLtexs(ltexTable)
    , mOldState(0)
{
    setText("Copy land textures to current plugin");
}

void CSMWorld::ImportLandTexturesCommand::redo()
{
    const int pluginColumn = mLands.findColumnIndex(Columns::ColumnId_PluginIndex);
    const int oldPlugin = mLands.data(mLands.getModelIndex(getOriginId(), pluginColumn)).toInt();

    // Original data
    const int textureColumn = mLands.findColumnIndex(Columns::ColumnId_LandTexturesIndex);
    mOld = mLands.data(mLands.getModelIndex(getOriginId(), textureColumn)).value<DataType>();

    // Need to make a copy so the old values can be looked up
    DataType copy(mOld);

    // Perform touch/copy/etc...
    onRedo();

    std::unordered_map<uint16_t, uint16_t> indexMapping;
    for (uint16_t index : mOld)
    {
        // All indices are offset by 1 for a default texture
        if (index == 0)
            continue;
        if (indexMapping.contains(index))
            continue;
        const CSMWorld::Record<ESM::LandTexture>* record
            = static_cast<LandTextureIdTable&>(mLtexs).searchRecord(index - 1, oldPlugin);
        if (!record || record->isDeleted())
        {
            indexMapping.emplace(index, 0);
            continue;
        }
        if (!record->isModified())
        {
            mTouchedTextures.emplace_back(record->clone());
            mLtexs.touchRecord(record->get().mId.getRefIdString());
        }
        indexMapping.emplace(index, record->get().mIndex + 1);
    }
    for (int i = 0; i < Land::LAND_NUM_TEXTURES; ++i)
    {
        uint16_t oldIndex = mOld[i];
        uint16_t newIndex = indexMapping[oldIndex];
        copy[i] = newIndex;
    }

    // Apply modification
    const int stateColumn = mLands.findColumnIndex(Columns::ColumnId_Modification);
    mOldState = mLands.data(mLands.getModelIndex(getDestinationId(), stateColumn)).toInt();

    QVariant variant;
    variant.setValue(copy);
    mLands.setData(mLands.getModelIndex(getDestinationId(), textureColumn), variant);
}

void CSMWorld::ImportLandTexturesCommand::undo()
{
    // Restore to previous
    int textureColumn = mLands.findColumnIndex(Columns::ColumnId_LandTexturesIndex);
    QVariant variant;
    variant.setValue(mOld);
    mLands.setData(mLands.getModelIndex(getDestinationId(), textureColumn), variant);

    int stateColumn = mLands.findColumnIndex(Columns::ColumnId_Modification);
    mLands.setData(mLands.getModelIndex(getDestinationId(), stateColumn), mOldState);

    // Undo copy/touch/etc...
    onUndo();

    for (auto& ltex : mTouchedTextures)
    {
        ESM::RefId id = static_cast<Record<ESM::LandTexture>*>(ltex.get())->get().mId;
        mLtexs.setRecord(id.getRefIdString(), std::move(ltex));
    }
    mTouchedTextures.clear();
}

CSMWorld::CopyLandTexturesCommand::CopyLandTexturesCommand(
    IdTable& landTable, IdTable& ltexTable, const std::string& origin, const std::string& dest, QUndoCommand* parent)
    : ImportLandTexturesCommand(landTable, ltexTable, parent)
    , mOriginId(origin)
    , mDestId(dest)
{
}

const std::string& CSMWorld::CopyLandTexturesCommand::getOriginId() const
{
    return mOriginId;
}

const std::string& CSMWorld::CopyLandTexturesCommand::getDestinationId() const
{
    return mDestId;
}

CSMWorld::TouchLandCommand::TouchLandCommand(
    IdTable& landTable, IdTable& ltexTable, const std::string& id, QUndoCommand* parent)
    : ImportLandTexturesCommand(landTable, ltexTable, parent)
    , mId(id)
    , mOld(nullptr)
    , mChanged(false)
{
    setText(("Touch " + mId).c_str());
}

const std::string& CSMWorld::TouchLandCommand::getOriginId() const
{
    return mId;
}

const std::string& CSMWorld::TouchLandCommand::getDestinationId() const
{
    return mId;
}

void CSMWorld::TouchLandCommand::onRedo()
{
    mOld = mLands.getRecord(mId).clone();
    mChanged = mLands.touchRecord(mId);
}

void CSMWorld::TouchLandCommand::onUndo()
{
    if (mChanged)
    {
        mLands.setRecord(mId, std::move(mOld));
        mChanged = false;
    }
}

CSMWorld::ModifyCommand::ModifyCommand(
    QAbstractItemModel& model, const QModelIndex& index, const QVariant& newValue, QUndoCommand* parent)
    : QUndoCommand(parent)
    , mModel(&model)
    , mIndex(index)
    , mNew(newValue)
    , mHasRecordState(false)
    , mOldRecordState(CSMWorld::RecordBase::State_BaseOnly)
{
    if (QAbstractProxyModel* proxy = dynamic_cast<QAbstractProxyModel*>(mModel))
    {
        // Replace proxy with actual model
        mIndex = proxy->mapToSource(mIndex);
        mModel = proxy->sourceModel();
    }
}

void CSMWorld::ModifyCommand::redo()
{
    if (mIndex.parent().isValid())
    {
        CSMWorld::IdTree* tree = &dynamic_cast<CSMWorld::IdTree&>(*mModel);
        setText("Modify "
            + tree->nestedHeaderData(mIndex.parent().column(), mIndex.column(), Qt::Horizontal, Qt::DisplayRole)
                  .toString());
    }
    else
    {
        setText("Modify " + mModel->headerData(mIndex.column(), Qt::Horizontal, Qt::DisplayRole).toString());
    }

    // Remember record state before the modification
    if (CSMWorld::IdTable* table = dynamic_cast<IdTable*>(mModel))
    {
        mHasRecordState = true;
        int stateColumnIndex = table->findColumnIndex(Columns::ColumnId_Modification);

        int rowIndex = mIndex.row();
        if (mIndex.parent().isValid())
        {
            rowIndex = mIndex.parent().row();
        }

        mRecordStateIndex = table->index(rowIndex, stateColumnIndex);
        mOldRecordState = static_cast<CSMWorld::RecordBase::State>(table->data(mRecordStateIndex).toInt());
    }

    mOld = mModel->data(mIndex, Qt::EditRole);
    mModel->setData(mIndex, mNew);
}

void CSMWorld::ModifyCommand::undo()
{
    mModel->setData(mIndex, mOld);
    if (mHasRecordState)
    {
        mModel->setData(mRecordStateIndex, mOldRecordState);
    }
}

void CSMWorld::CreateCommand::applyModifications()
{
    if (!mNestedValues.empty())
    {
        CSMWorld::IdTree* tree = &dynamic_cast<CSMWorld::IdTree&>(mModel);
        std::map<int, std::pair<int, QVariant>>::const_iterator current = mNestedValues.begin();
        std::map<int, std::pair<int, QVariant>>::const_iterator end = mNestedValues.end();
        for (; current != end; ++current)
        {
            QModelIndex index = tree->index(0, current->second.first, tree->getNestedModelIndex(mId, current->first));
            tree->setData(index, current->second.second);
        }
    }
}

CSMWorld::CreateCommand::CreateCommand(IdTable& model, const std::string& id, QUndoCommand* parent)
    : QUndoCommand(parent)
    , mModel(model)
    , mId(id)
    , mType(UniversalId::Type_None)
{
    setText(("Create record " + id).c_str());
}

void CSMWorld::CreateCommand::addValue(int column, const QVariant& value)
{
    mValues[column] = value;
}

void CSMWorld::CreateCommand::addNestedValue(int parentColumn, int nestedColumn, const QVariant& value)
{
    mNestedValues[parentColumn] = std::make_pair(nestedColumn, value);
}

void CSMWorld::CreateCommand::setType(UniversalId::Type type)
{
    mType = type;
}

void CSMWorld::CreateCommand::redo()
{
    mModel.addRecordWithData(mId, mValues, mType);
    applyModifications();
}

void CSMWorld::CreateCommand::undo()
{
    mModel.removeRow(mModel.getModelIndex(mId, 0).row());
}

CSMWorld::RevertCommand::RevertCommand(IdTable& model, const std::string& id, QUndoCommand* parent)
    : QUndoCommand(parent)
    , mModel(model)
    , mId(id)
    , mOld(nullptr)
{
    setText(("Revert record " + id).c_str());
}

void CSMWorld::RevertCommand::redo()
{
    mOld = mModel.getRecord(mId).clone();

    int column = mModel.findColumnIndex(Columns::ColumnId_Modification);

    QModelIndex index = mModel.getModelIndex(mId, column);
    RecordBase::State state = static_cast<RecordBase::State>(mModel.data(index).toInt());

    if (state == RecordBase::State_ModifiedOnly)
    {
        mModel.removeRows(index.row(), 1);
    }
    else
    {
        mModel.setData(index, static_cast<int>(RecordBase::State_BaseOnly));
    }
}

void CSMWorld::RevertCommand::undo()
{
    mModel.setRecord(mId, std::move(mOld));
}

CSMWorld::DeleteCommand::DeleteCommand(
    IdTable& model, const std::string& id, CSMWorld::UniversalId::Type type, QUndoCommand* parent)
    : QUndoCommand(parent)
    , mModel(model)
    , mId(id)
    , mOld(nullptr)
    , mType(type)
{
    setText(("Delete record " + id).c_str());
}

void CSMWorld::DeleteCommand::redo()
{
    mOld = mModel.getRecord(mId).clone();

    int column = mModel.findColumnIndex(Columns::ColumnId_Modification);

    QModelIndex index = mModel.getModelIndex(mId, column);
    RecordBase::State state = static_cast<RecordBase::State>(mModel.data(index).toInt());

    if (state == RecordBase::State_ModifiedOnly)
    {
        mModel.removeRows(index.row(), 1);
    }
    else
    {
        mModel.setData(index, static_cast<int>(RecordBase::State_Deleted));
    }
}

void CSMWorld::DeleteCommand::undo()
{
    mModel.setRecord(mId, std::move(mOld), mType);
}

CSMWorld::ReorderRowsCommand::ReorderRowsCommand(IdTable& model, int baseIndex, const std::vector<int>& newOrder)
    : mModel(model)
    , mBaseIndex(baseIndex)
    , mNewOrder(newOrder)
{
}

void CSMWorld::ReorderRowsCommand::redo()
{
    mModel.reorderRows(mBaseIndex, mNewOrder);
}

void CSMWorld::ReorderRowsCommand::undo()
{
    int size = static_cast<int>(mNewOrder.size());
    std::vector<int> reverse(size);

    for (int i = 0; i < size; ++i)
        reverse.at(mNewOrder[i]) = i;

    mModel.reorderRows(mBaseIndex, reverse);
}

CSMWorld::CloneCommand::CloneCommand(CSMWorld::IdTable& model, const std::string& idOrigin,
    const std::string& idDestination, const CSMWorld::UniversalId::Type type, QUndoCommand* parent)
    : CreateCommand(model, idDestination, parent)
    , mIdOrigin(idOrigin)
{
    setType(type);
    setText(("Clone record " + idOrigin + " to the " + idDestination).c_str());
}

void CSMWorld::CloneCommand::redo()
{
    mModel.cloneRecord(ESM::RefId::stringRefId(mIdOrigin), ESM::RefId::stringRefId(mId), mType);
    applyModifications();
    for (auto& value : mOverrideValues)
    {
        mModel.setData(mModel.getModelIndex(mId, value.first), value.second);
    }
}

void CSMWorld::CloneCommand::undo()
{
    mModel.removeRow(mModel.getModelIndex(mId, 0).row());
}

void CSMWorld::CloneCommand::setOverrideValue(int column, QVariant value)
{
    mOverrideValues.emplace_back(std::make_pair(column, value));
}

CSMWorld::CreatePathgridCommand::CreatePathgridCommand(IdTable& model, const std::string& id, QUndoCommand* parent)
    : CreateCommand(model, id, parent)
{
    setType(UniversalId::Type_Pathgrid);
}

void CSMWorld::CreatePathgridCommand::redo()
{
    CreateCommand::redo();

    std::unique_ptr<Record<Pathgrid>> record
        = std::make_unique<Record<Pathgrid>>(static_cast<const Record<Pathgrid>&>(mModel.getRecord(mId)));
    record->get().blank();
    record->get().mCell = ESM::RefId::stringRefId(mId);

    std::pair<CellCoordinates, bool> coords = CellCoordinates::fromId(mId);
    if (coords.second)
    {
        record->get().mData.mX = coords.first.getX();
        record->get().mData.mY = coords.first.getY();
    }

    mModel.setRecord(mId, std::move(record), mType);
}

CSMWorld::UpdateCellCommand::UpdateCellCommand(IdTable& model, int row, QUndoCommand* parent)
    : QUndoCommand(parent)
    , mModel(model)
    , mRow(row)
{
    setText("Update cell ID");
}

void CSMWorld::UpdateCellCommand::redo()
{
    if (!mNew.isValid())
    {
        int cellColumn = mModel.searchColumnIndex(Columns::ColumnId_Cell);
        mIndex = mModel.index(mRow, cellColumn);

        QModelIndex xIndex = mModel.index(mRow, mModel.findColumnIndex(Columns::ColumnId_PositionXPos));

        QModelIndex yIndex = mModel.index(mRow, mModel.findColumnIndex(Columns::ColumnId_PositionYPos));

        int x = std::floor(mModel.data(xIndex).toFloat() / Constants::CellSizeInUnits);
        int y = std::floor(mModel.data(yIndex).toFloat() / Constants::CellSizeInUnits);

        std::ostringstream stream;

        stream << "#" << x << " " << y;

        mNew = QString::fromUtf8(stream.str().c_str());
    }

    mModel.setData(mIndex, mNew);
}

void CSMWorld::UpdateCellCommand::undo()
{
    mModel.setData(mIndex, mOld);
}

CSMWorld::DeleteNestedCommand::DeleteNestedCommand(
    IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent)
    : QUndoCommand(parent)
    , NestedTableStoring(model, id, parentColumn)
    , mModel(model)
    , mId(id)
    , mParentColumn(parentColumn)
    , mNestedRow(nestedRow)
{
    std::string title = model.headerData(parentColumn, Qt::Horizontal, Qt::DisplayRole).toString().toUtf8().constData();
    setText(("Delete row in " + title + " sub-table of " + mId).c_str());

    QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn);
    mModifyParentCommand = new ModifyCommand(mModel, parentIndex, parentIndex.data(Qt::EditRole), this);
}

void CSMWorld::DeleteNestedCommand::redo()
{
    QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn);
    mModifyParentCommand->redo();
    mModel.removeRows(mNestedRow, 1, parentIndex);
}

void CSMWorld::DeleteNestedCommand::undo()
{
    QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn);
    mModel.setNestedTable(parentIndex, getOld());
    mModifyParentCommand->undo();
}

CSMWorld::AddNestedCommand::AddNestedCommand(
    IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent)
    : QUndoCommand(parent)
    , NestedTableStoring(model, id, parentColumn)
    , mModel(model)
    , mId(id)
    , mNewRow(nestedRow)
    , mParentColumn(parentColumn)
{
    std::string title = model.headerData(parentColumn, Qt::Horizontal, Qt::DisplayRole).toString().toUtf8().constData();
    setText(("Add row in " + title + " sub-table of " + mId).c_str());

    QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn);
    mModifyParentCommand = new ModifyCommand(mModel, parentIndex, parentIndex.data(Qt::EditRole), this);
}

void CSMWorld::AddNestedCommand::redo()
{
    QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn);
    mModifyParentCommand->redo();
    mModel.addNestedRow(parentIndex, mNewRow);
}

void CSMWorld::AddNestedCommand::undo()
{
    QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn);
    mModel.setNestedTable(parentIndex, getOld());
    mModifyParentCommand->undo();
}

CSMWorld::NestedTableStoring::NestedTableStoring(const IdTree& model, const std::string& id, int parentColumn)
    : mOld(model.nestedTable(model.getModelIndex(id, parentColumn)))
{
}

CSMWorld::NestedTableStoring::~NestedTableStoring()
{
    delete mOld;
}

const CSMWorld::NestedTableWrapperBase& CSMWorld::NestedTableStoring::getOld() const
{
    return *mOld;
}
