#include "pathgrid.hpp"

#include <algorithm>
#include <functional>
#include <memory>
#include <set>
#include <stddef.h>

#include <osg/Array>
#include <osg/Callback>
#include <osg/GL>
#include <osg/Geometry>
#include <osg/Group>
#include <osg/MixinVector>
#include <osg/Node>
#include <osg/PositionAttitudeTransform>
#include <osg/PrimitiveSet>
#include <osg/StateAttribute>
#include <osg/StateSet>
#include <osg/Vec3f>
#include <osg/Vec4f>

#include <apps/opencs/model/world/cell.hpp>
#include <apps/opencs/model/world/cellcoordinates.hpp>
#include <apps/opencs/model/world/columns.hpp>
#include <apps/opencs/model/world/idcollection.hpp>
#include <apps/opencs/model/world/pathgrid.hpp>
#include <apps/opencs/model/world/record.hpp>
#include <apps/opencs/model/world/subcellcollection.hpp>
#include <apps/opencs/model/world/universalid.hpp>
#include <apps/opencs/view/render/mask.hpp>
#include <apps/opencs/view/render/tagbase.hpp>

#include <components/esm3/loadcell.hpp>
#include <components/esm3/loadland.hpp>
#include <components/esm3/loadpgrd.hpp>
#include <components/sceneutil/pathgridutil.hpp>

#include "../../model/world/commandmacro.hpp"
#include "../../model/world/commands.hpp"
#include "../../model/world/data.hpp"
#include "../../model/world/idtree.hpp"
#include "worldspacewidget.hpp"

namespace osg
{
    class NodeVisitor;
}

namespace CSVRender
{
    class PathgridNodeCallback : public osg::NodeCallback
    {
    public:
        void operator()(osg::Node* node, osg::NodeVisitor* nv) override
        {
            PathgridTag* tag = static_cast<PathgridTag*>(node->getUserData());
            tag->getPathgrid()->update();
        }
    };

    PathgridTag::PathgridTag(Pathgrid* pathgrid)
        : TagBase(Mask_Pathgrid)
        , mPathgrid(pathgrid)
    {
    }

    Pathgrid* PathgridTag::getPathgrid() const
    {
        return mPathgrid;
    }

    QString PathgridTag::getToolTip(bool /*hideBasics*/, const WorldspaceHitResult& hit) const
    {
        QString text("Pathgrid: ");
        text += mPathgrid->getId().c_str();
        text += " (";
        text += QString::number(SceneUtil::getPathgridNode(hit.index0));
        text += ")";

        return text;
    }

    Pathgrid::Pathgrid(CSMWorld::Data& data, osg::Group* parent, const std::string& pathgridId,
        const CSMWorld::CellCoordinates& coordinates)
        : mData(data)
        , mPathgridCollection(mData.getPathgrids())
        , mId(ESM::RefId::stringRefId(pathgridId))
        , mCoords(coordinates)
        , mInterior(false)
        , mDragOrigin(0)
        , mChangeGeometry(true)
        , mRemoveGeometry(false)
        , mUseOffset(true)
        , mParent(parent)
        , mPathgridGeometry(nullptr)
        , mDragGeometry(nullptr)
        , mTag(new PathgridTag(this))
    {
        const float CoordScalar = ESM::Land::REAL_SIZE;

        mBaseNode = new osg::PositionAttitudeTransform();
        mBaseNode->setPosition(osg::Vec3f(mCoords.getX() * CoordScalar, mCoords.getY() * CoordScalar, 0.f));
        mBaseNode->setUserData(mTag);
        mBaseNode->setUpdateCallback(new PathgridNodeCallback());
        mBaseNode->setNodeMask(Mask_Pathgrid);
        mParent->addChild(mBaseNode);

        mPathgridGroup = new osg::Group();
        mBaseNode->addChild(mPathgridGroup);

        recreateGeometry();

        int index = mData.getCells().searchId(mId);
        if (index != -1)
        {
            const CSMWorld::Cell& cell = mData.getCells().getRecord(index).get();
            mInterior = cell.mData.mFlags & ESM::Cell::Interior;
        }
    }

    Pathgrid::~Pathgrid()
    {
        mParent->removeChild(mBaseNode);
    }

    const CSMWorld::CellCoordinates& Pathgrid::getCoordinates() const
    {
        return mCoords;
    }

    const std::string& Pathgrid::getId() const
    {
        return mId.getRefIdString();
    }

    bool Pathgrid::isSelected() const
    {
        return !mSelected.empty();
    }

    const Pathgrid::NodeList& Pathgrid::getSelected() const
    {
        return mSelected;
    }

    void Pathgrid::selectAll()
    {
        mSelected.clear();

        const CSMWorld::Pathgrid* source = getPathgridSource();
        if (source)
        {
            for (unsigned short i = 0; i < static_cast<unsigned short>(source->mPoints.size()); ++i)
                mSelected.push_back(i);

            createSelectedGeometry(*source);
        }
        else
        {
            removeSelectedGeometry();
        }
    }

    void Pathgrid::toggleSelected(unsigned short node)
    {
        NodeList::iterator searchResult = std::find(mSelected.begin(), mSelected.end(), node);
        if (searchResult != mSelected.end())
        {
            mSelected.erase(searchResult);
        }
        else
        {
            mSelected.push_back(node);
        }

        createSelectedGeometry();
    }

    void Pathgrid::invertSelected()
    {
        NodeList temp = NodeList(mSelected);
        mSelected.clear();

        const CSMWorld::Pathgrid* source = getPathgridSource();
        if (source)
        {
            for (unsigned short i = 0; i < static_cast<unsigned short>(source->mPoints.size()); ++i)
            {
                if (std::find(temp.begin(), temp.end(), i) == temp.end())
                    mSelected.push_back(i);
            }

            createSelectedGeometry(*source);
        }
        else
        {
            removeSelectedGeometry();
        }
    }

    void Pathgrid::clearSelected()
    {
        mSelected.clear();
        removeSelectedGeometry();
    }

    void Pathgrid::moveSelected(const osg::Vec3d& offset)
    {
        mUseOffset = true;
        mMoveOffset += offset;

        recreateGeometry();
    }

    void Pathgrid::setDragOrigin(unsigned short node)
    {
        mDragOrigin = node;
    }

    void Pathgrid::setDragEndpoint(unsigned short node)
    {
        const CSMWorld::Pathgrid* source = getPathgridSource();
        if (source)
        {
            const CSMWorld::Pathgrid::Point& pointA = source->mPoints[mDragOrigin];
            const CSMWorld::Pathgrid::Point& pointB = source->mPoints[node];

            osg::Vec3f start = osg::Vec3f(pointA.mX, pointA.mY, pointA.mZ + SceneUtil::DiamondHalfHeight);
            osg::Vec3f end = osg::Vec3f(pointB.mX, pointB.mY, pointB.mZ + SceneUtil::DiamondHalfHeight);

            createDragGeometry(start, end, true);
        }
    }

    void Pathgrid::setDragEndpoint(const osg::Vec3d& pos)
    {
        const CSMWorld::Pathgrid* source = getPathgridSource();
        if (source)
        {
            const CSMWorld::Pathgrid::Point& point = source->mPoints[mDragOrigin];

            osg::Vec3f start = osg::Vec3f(point.mX, point.mY, point.mZ + SceneUtil::DiamondHalfHeight);
            osg::Vec3f end = pos - mBaseNode->getPosition();
            createDragGeometry(start, end, false);
        }
    }

    void Pathgrid::resetIndicators()
    {
        mUseOffset = false;
        mMoveOffset.set(0, 0, 0);

        mPathgridGroup->removeChild(mDragGeometry);
        mDragGeometry = nullptr;
    }

    void Pathgrid::applyPoint(CSMWorld::CommandMacro& commands, const osg::Vec3d& worldPos)
    {
        CSMWorld::IdTree* model
            = &dynamic_cast<CSMWorld::IdTree&>(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids));
        const std::string& idString = mId.getRefIdString();
        const CSMWorld::Pathgrid* source = getPathgridSource();
        if (source)
        {
            osg::Vec3d localCoords = worldPos - mBaseNode->getPosition();

            int posX = clampToCell(static_cast<int>(localCoords.x()));
            int posY = clampToCell(static_cast<int>(localCoords.y()));
            int posZ = clampToCell(static_cast<int>(localCoords.z()));

            int recordIndex = mPathgridCollection.getIndex(mId);
            int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints);

            int posXColumn
                = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosX);

            int posYColumn
                = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosY);

            int posZColumn
                = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosZ);

            QModelIndex parent = model->index(recordIndex, parentColumn);
            int row = static_cast<int>(source->mPoints.size());

            // Add node to end of list
            commands.push(new CSMWorld::AddNestedCommand(*model, idString, row, parentColumn));
            commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posXColumn, parent), posX));
            commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posYColumn, parent), posY));
            commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, posZColumn, parent), posZ));
        }
        else
        {
            int index = mPathgridCollection.searchId(mId);
            if (index == -1)
            {
                // Does not exist
                commands.push(new CSMWorld::CreatePathgridCommand(*model, idString));
            }
            else
            {
                source = &mPathgridCollection.getRecord(index).get();

                // Deleted, so revert and remove all data
                commands.push(new CSMWorld::RevertCommand(*model, idString));

                int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints);
                for (int row = source->mPoints.size() - 1; row >= 0; --row)
                {
                    commands.push(new CSMWorld::DeleteNestedCommand(*model, idString, row, parentColumn));
                }

                parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges);
                for (int row = source->mEdges.size() - 1; row >= 0; --row)
                {
                    commands.push(new CSMWorld::DeleteNestedCommand(*model, idString, row, parentColumn));
                }
            }
        }
    }

    void Pathgrid::applyPosition(CSMWorld::CommandMacro& commands)
    {
        const CSMWorld::Pathgrid* source = getPathgridSource();
        if (source)
        {
            osg::Vec3d localCoords = mMoveOffset;

            int offsetX = static_cast<int>(localCoords.x());
            int offsetY = static_cast<int>(localCoords.y());
            int offsetZ = static_cast<int>(localCoords.z());

            QAbstractItemModel* model = mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids);

            int recordIndex = mPathgridCollection.getIndex(mId);
            int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints);

            int posXColumn
                = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosX);

            int posYColumn
                = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosY);

            int posZColumn
                = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosZ);

            QModelIndex parent = model->index(recordIndex, parentColumn);

            for (const auto& selected : mSelected)
            {
                const CSMWorld::Pathgrid::Point& point = source->mPoints[selected];
                int row = static_cast<int>(selected);

                commands.push(new CSMWorld::ModifyCommand(
                    *model, model->index(row, posXColumn, parent), clampToCell(point.mX + offsetX)));

                commands.push(new CSMWorld::ModifyCommand(
                    *model, model->index(row, posYColumn, parent), clampToCell(point.mY + offsetY)));

                commands.push(new CSMWorld::ModifyCommand(
                    *model, model->index(row, posZColumn, parent), clampToCell(point.mZ + offsetZ)));
            }
        }
    }

    void Pathgrid::applyEdge(CSMWorld::CommandMacro& commands, unsigned short node1, unsigned short node2)
    {
        const CSMWorld::Pathgrid* source = getPathgridSource();
        if (source)
        {
            addEdge(commands, *source, node1, node2);
        }
    }

    void Pathgrid::applyEdges(CSMWorld::CommandMacro& commands, unsigned short node)
    {
        const CSMWorld::Pathgrid* source = getPathgridSource();
        if (source)
        {
            for (const auto& selected : mSelected)
            {
                addEdge(commands, *source, node, selected);
            }
        }
    }

    void Pathgrid::applyRemoveNodes(CSMWorld::CommandMacro& commands)
    {
        const CSMWorld::Pathgrid* source = getPathgridSource();
        if (source)
        {
            CSMWorld::IdTree* model
                = &dynamic_cast<CSMWorld::IdTree&>(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids));

            // Want to remove nodes from end of list first
            std::sort(mSelected.begin(), mSelected.end(), std::greater<int>());

            int recordIndex = mPathgridCollection.getIndex(mId);
            int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints);

            for (std::vector<unsigned short>::iterator row = mSelected.begin(); row != mSelected.end(); ++row)
            {
                commands.push(new CSMWorld::DeleteNestedCommand(
                    *model, mId.getRefIdString(), static_cast<int>(*row), parentColumn));
            }

            // Fix/remove edges
            std::set<int, std::greater<int>> edgeRowsToRemove;

            parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges);

            int edge0Column
                = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge0);

            int edge1Column
                = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge1);

            QModelIndex parent = model->index(recordIndex, parentColumn);

            for (size_t edge = 0; edge < source->mEdges.size(); ++edge)
            {
                int adjustment0 = 0;
                int adjustment1 = 0;

                // Determine necessary adjustment
                for (const auto point : mSelected)
                {
                    if (source->mEdges[edge].mV0 == point || source->mEdges[edge].mV1 == point)
                    {
                        edgeRowsToRemove.insert(static_cast<int>(edge));

                        adjustment0 = 0; // No need to adjust, its getting removed
                        adjustment1 = 0;
                        break;
                    }

                    if (source->mEdges[edge].mV0 > point)
                        --adjustment0;

                    if (source->mEdges[edge].mV1 > point)
                        --adjustment1;
                }

                if (adjustment0 != 0)
                {
                    int adjustedEdge = source->mEdges[edge].mV0 + adjustment0;
                    commands.push(
                        new CSMWorld::ModifyCommand(*model, model->index(edge, edge0Column, parent), adjustedEdge));
                }

                if (adjustment1 != 0)
                {
                    int adjustedEdge = source->mEdges[edge].mV1 + adjustment1;
                    commands.push(
                        new CSMWorld::ModifyCommand(*model, model->index(edge, edge1Column, parent), adjustedEdge));
                }
            }

            for (const auto row : edgeRowsToRemove)
            {
                commands.push(new CSMWorld::DeleteNestedCommand(*model, mId.getRefIdString(), row, parentColumn));
            }
        }

        clearSelected();
    }

    void Pathgrid::applyRemoveEdges(CSMWorld::CommandMacro& commands)
    {
        const CSMWorld::Pathgrid* source = getPathgridSource();
        if (source)
        {
            // Want to remove from end of row first
            std::set<int, std::greater<int>> rowsToRemove;
            for (size_t i = 0; i <= mSelected.size(); ++i)
            {
                for (size_t j = i + 1; j < mSelected.size(); ++j)
                {
                    int row = edgeExists(*source, mSelected[i], mSelected[j]);
                    if (row != -1)
                    {
                        rowsToRemove.insert(row);
                    }

                    row = edgeExists(*source, mSelected[j], mSelected[i]);
                    if (row != -1)
                    {
                        rowsToRemove.insert(row);
                    }
                }
            }

            CSMWorld::IdTree* model
                = &dynamic_cast<CSMWorld::IdTree&>(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids));
            int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges);

            std::set<int, std::greater<int>>::iterator row;
            for (row = rowsToRemove.begin(); row != rowsToRemove.end(); ++row)
            {
                commands.push(new CSMWorld::DeleteNestedCommand(*model, mId.getRefIdString(), *row, parentColumn));
            }
        }
    }

    osg::ref_ptr<PathgridTag> Pathgrid::getTag() const
    {
        return mTag;
    }

    void Pathgrid::recreateGeometry()
    {
        mChangeGeometry = true;
    }

    void Pathgrid::removeGeometry()
    {
        mRemoveGeometry = true;
    }

    void Pathgrid::update()
    {
        if (mRemoveGeometry)
        {
            removePathgridGeometry();
            removeSelectedGeometry();
        }
        else if (mChangeGeometry)
        {
            createGeometry();
        }

        mChangeGeometry = false;
        mRemoveGeometry = false;
    }

    void Pathgrid::createGeometry()
    {
        const CSMWorld::Pathgrid* source = getPathgridSource();
        if (source)
        {
            CSMWorld::Pathgrid temp;
            if (mUseOffset)
            {
                temp = *source;

                for (NodeList::iterator it = mSelected.begin(); it != mSelected.end(); ++it)
                {
                    temp.mPoints[*it].mX += mMoveOffset.x();
                    temp.mPoints[*it].mY += mMoveOffset.y();
                    temp.mPoints[*it].mZ += mMoveOffset.z();
                }

                source = &temp;
            }

            removePathgridGeometry();
            mPathgridGeometry = SceneUtil::createPathgridGeometry(*source);
            mPathgridGroup->addChild(mPathgridGeometry);

            createSelectedGeometry(*source);
        }
        else
        {
            removePathgridGeometry();
            removeSelectedGeometry();
        }
    }

    void Pathgrid::createSelectedGeometry()
    {
        const CSMWorld::Pathgrid* source = getPathgridSource();
        if (source)
        {
            createSelectedGeometry(*source);
        }
        else
        {
            removeSelectedGeometry();
        }
    }

    void Pathgrid::createSelectedGeometry(const CSMWorld::Pathgrid& source)
    {
        removeSelectedGeometry();

        mSelectedGeometry = SceneUtil::createPathgridSelectedWireframe(source, mSelected);
        mPathgridGroup->addChild(mSelectedGeometry);
    }

    void Pathgrid::removePathgridGeometry()
    {
        if (mPathgridGeometry)
        {
            mPathgridGroup->removeChild(mPathgridGeometry);
            mPathgridGeometry = nullptr;
        }
    }

    void Pathgrid::removeSelectedGeometry()
    {
        if (mSelectedGeometry)
        {
            mPathgridGroup->removeChild(mSelectedGeometry);
            mSelectedGeometry = nullptr;
        }
    }

    void Pathgrid::createDragGeometry(const osg::Vec3f& start, const osg::Vec3f& end, bool valid)
    {
        if (mDragGeometry)
            mPathgridGroup->removeChild(mDragGeometry);

        mDragGeometry = new osg::Geometry();

        osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array(2);
        osg::ref_ptr<osg::Vec4Array> colors = new osg::Vec4Array(1);
        osg::ref_ptr<osg::DrawElementsUShort> indices = new osg::DrawElementsUShort(osg::PrimitiveSet::LINES, 2);

        (*vertices)[0] = start;
        (*vertices)[1] = end;

        if (valid)
        {
            (*colors)[0] = osg::Vec4f(0.91f, 0.66f, 1.f, 1.f);
        }
        else
        {
            (*colors)[0] = osg::Vec4f(1.f, 0.f, 0.f, 1.f);
        }

        indices->setElement(0, 0);
        indices->setElement(1, 1);

        mDragGeometry->setVertexArray(vertices);
        mDragGeometry->setColorArray(colors, osg::Array::BIND_OVERALL);
        mDragGeometry->addPrimitiveSet(indices);
        mDragGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);

        mPathgridGroup->addChild(mDragGeometry);
    }

    const CSMWorld::Pathgrid* Pathgrid::getPathgridSource()
    {
        int index = mPathgridCollection.searchId(mId);
        if (index != -1 && !mPathgridCollection.getRecord(index).isDeleted())
        {
            return &mPathgridCollection.getRecord(index).get();
        }

        return nullptr;
    }

    int Pathgrid::edgeExists(const CSMWorld::Pathgrid& source, unsigned short node1, unsigned short node2)
    {
        for (size_t i = 0; i < source.mEdges.size(); ++i)
        {
            if (source.mEdges[i].mV0 == node1 && source.mEdges[i].mV1 == node2)
                return static_cast<int>(i);
        }

        return -1;
    }

    void Pathgrid::addEdge(
        CSMWorld::CommandMacro& commands, const CSMWorld::Pathgrid& source, unsigned short node1, unsigned short node2)
    {
        CSMWorld::IdTree* model
            = &dynamic_cast<CSMWorld::IdTree&>(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids));

        int recordIndex = mPathgridCollection.getIndex(mId);
        int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges);

        int edge0Column
            = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge0);

        int edge1Column
            = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge1);

        QModelIndex parent = model->index(recordIndex, parentColumn);
        int row = static_cast<int>(source.mEdges.size());

        if (edgeExists(source, node1, node2) == -1)
        {
            commands.push(new CSMWorld::AddNestedCommand(*model, mId.getRefIdString(), row, parentColumn));
            commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, edge0Column, parent), node1));
            commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, edge1Column, parent), node2));
            ++row;
        }

        if (edgeExists(source, node2, node1) == -1)
        {
            commands.push(new CSMWorld::AddNestedCommand(*model, mId.getRefIdString(), row, parentColumn));
            commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, edge0Column, parent), node2));
            commands.push(new CSMWorld::ModifyCommand(*model, model->index(row, edge1Column, parent), node1));
        }
    }

    int Pathgrid::clampToCell(int v)
    {
        const int CellExtent = ESM::Land::REAL_SIZE;

        if (mInterior)
            return v;
        else if (v > CellExtent)
            return CellExtent;
        else if (v < 0)
            return 0;
        else
            return v;
    }
}
