#include "instanceselectionmode.hpp"

#include <cmath>
#include <string>
#include <vector>

#include <apps/opencs/model/doc/document.hpp>
#include <apps/opencs/model/world/data.hpp>
#include <apps/opencs/model/world/universalid.hpp>
#include <apps/opencs/view/render/mask.hpp>
#include <apps/opencs/view/render/selectionmode.hpp>

#include <osg/Array>
#include <osg/GL>
#include <osg/Geometry>
#include <osg/Group>
#include <osg/Math>
#include <osg/PositionAttitudeTransform>
#include <osg/PrimitiveSet>
#include <osg/StateAttribute>
#include <osg/StateSet>
#include <osg/Vec3d>
#include <osg/Vec3f>
#include <osg/Vec4f>
#include <osg/ref_ptr>

#include <QAction>
#include <QMenu>
#include <QPoint>

#include "../../model/world/commands.hpp"
#include "../../model/world/idtable.hpp"

#include "instancedragmodes.hpp"
#include "object.hpp"
#include "worldspacewidget.hpp"

namespace CSVWidget
{
    class SceneToolbar;
}

namespace CSVRender
{
    class TagBase;

    InstanceSelectionMode::InstanceSelectionMode(
        CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, osg::Group* cellNode)
        : SelectionMode(parent, worldspaceWidget, Mask_Reference)
        , mParentNode(cellNode)
    {
        mSelectSame = new QAction("Extend selection to instances with same object ID", this);
        mDeleteSelection = new QAction("Delete selected instances", this);

        connect(mSelectSame, &QAction::triggered, this, &InstanceSelectionMode::selectSame);
        connect(mDeleteSelection, &QAction::triggered, this, &InstanceSelectionMode::deleteSelection);
    }

    InstanceSelectionMode::~InstanceSelectionMode()
    {
        if (mBaseNode)
            mParentNode->removeChild(mBaseNode);
    }

    void InstanceSelectionMode::setDragStart(const osg::Vec3d& dragStart)
    {
        mDragStart = dragStart;
    }

    const osg::Vec3d& InstanceSelectionMode::getDragStart()
    {
        return mDragStart;
    }

    void InstanceSelectionMode::dragEnded(const osg::Vec3d& dragEndPoint, DragMode dragMode)
    {
        float dragDistance = (mDragStart - dragEndPoint).length();
        if (mBaseNode)
            mParentNode->removeChild(mBaseNode);
        if (getCurrentId() == "cube-centre")
        {
            osg::Vec3d pointA(mDragStart[0] - dragDistance, mDragStart[1] - dragDistance, mDragStart[2] - dragDistance);
            osg::Vec3d pointB(mDragStart[0] + dragDistance, mDragStart[1] + dragDistance, mDragStart[2] + dragDistance);
            getWorldspaceWidget().selectInsideCube(pointA, pointB, dragMode);
        }
        else if (getCurrentId() == "cube-corner")
        {
            getWorldspaceWidget().selectInsideCube(mDragStart, dragEndPoint, dragMode);
        }
        else if (getCurrentId() == "sphere")
        {
            getWorldspaceWidget().selectWithinDistance(mDragStart, dragDistance, dragMode);
        }
    }

    void InstanceSelectionMode::drawSelectionCubeCentre(const osg::Vec3f& mousePlanePoint)
    {
        float dragDistance = (mDragStart - mousePlanePoint).length();
        drawSelectionCube(mDragStart, dragDistance);
    }

    void InstanceSelectionMode::drawSelectionCubeCorner(const osg::Vec3f& mousePlanePoint)
    {
        drawSelectionBox(mDragStart, mousePlanePoint);
    }

    void InstanceSelectionMode::drawSelectionBox(const osg::Vec3d& pointA, const osg::Vec3d& pointB)
    {
        if (mBaseNode)
            mParentNode->removeChild(mBaseNode);
        mBaseNode = new osg::PositionAttitudeTransform;
        mBaseNode->setPosition(pointA);

        osg::ref_ptr<osg::Geometry> geometry(new osg::Geometry);

        osg::Vec3Array* vertices = new osg::Vec3Array;
        vertices->push_back(osg::Vec3f(0.0f, 0.0f, 0.0f));
        vertices->push_back(osg::Vec3f(0.0f, 0.0f, pointB[2] - pointA[2]));
        vertices->push_back(osg::Vec3f(0.0f, pointB[1] - pointA[1], 0.0f));
        vertices->push_back(osg::Vec3f(0.0f, pointB[1] - pointA[1], pointB[2] - pointA[2]));

        vertices->push_back(osg::Vec3f(pointB[0] - pointA[0], 0.0f, 0.0f));
        vertices->push_back(osg::Vec3f(pointB[0] - pointA[0], 0.0f, pointB[2] - pointA[2]));
        vertices->push_back(osg::Vec3f(pointB[0] - pointA[0], pointB[1] - pointA[1], 0.0f));
        vertices->push_back(osg::Vec3f(pointB[0] - pointA[0], pointB[1] - pointA[1], pointB[2] - pointA[2]));

        geometry->setVertexArray(vertices);

        osg::DrawElementsUShort* primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, 0);

        // top
        primitives->push_back(2);
        primitives->push_back(1);
        primitives->push_back(0);

        primitives->push_back(3);
        primitives->push_back(1);
        primitives->push_back(2);

        // bottom
        primitives->push_back(4);
        primitives->push_back(5);
        primitives->push_back(6);

        primitives->push_back(6);
        primitives->push_back(5);
        primitives->push_back(7);

        // sides
        primitives->push_back(1);
        primitives->push_back(4);
        primitives->push_back(0);

        primitives->push_back(4);
        primitives->push_back(1);
        primitives->push_back(5);

        primitives->push_back(4);
        primitives->push_back(2);
        primitives->push_back(0);

        primitives->push_back(6);
        primitives->push_back(2);
        primitives->push_back(4);

        primitives->push_back(6);
        primitives->push_back(3);
        primitives->push_back(2);

        primitives->push_back(7);
        primitives->push_back(3);
        primitives->push_back(6);

        primitives->push_back(1);
        primitives->push_back(3);
        primitives->push_back(5);

        primitives->push_back(5);
        primitives->push_back(3);
        primitives->push_back(7);

        geometry->addPrimitiveSet(primitives);

        osg::Vec4Array* colours = new osg::Vec4Array;

        colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.5f, 0.2f));
        colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f));
        colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.4f, 0.2f));
        colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f));
        colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.4f, 0.2f));
        colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f));
        colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.4f, 0.2f));
        colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f));

        geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX);

        geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
        geometry->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON);
        geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
        geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);

        mBaseNode->addChild(geometry);
        mParentNode->addChild(mBaseNode);
    }

    void InstanceSelectionMode::drawSelectionCube(const osg::Vec3d& point, float radius)
    {
        if (mBaseNode)
            mParentNode->removeChild(mBaseNode);
        mBaseNode = new osg::PositionAttitudeTransform;
        mBaseNode->setPosition(point);

        osg::ref_ptr<osg::Geometry> geometry(new osg::Geometry);

        osg::Vec3Array* vertices = new osg::Vec3Array;
        for (int i = 0; i < 2; ++i)
        {
            float height = i ? -radius : radius;
            vertices->push_back(osg::Vec3f(height, -radius, -radius));
            vertices->push_back(osg::Vec3f(height, -radius, radius));
            vertices->push_back(osg::Vec3f(height, radius, -radius));
            vertices->push_back(osg::Vec3f(height, radius, radius));
        }

        geometry->setVertexArray(vertices);

        osg::DrawElementsUShort* primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, 0);

        // top
        primitives->push_back(2);
        primitives->push_back(1);
        primitives->push_back(0);

        primitives->push_back(3);
        primitives->push_back(1);
        primitives->push_back(2);

        // bottom
        primitives->push_back(4);
        primitives->push_back(5);
        primitives->push_back(6);

        primitives->push_back(6);
        primitives->push_back(5);
        primitives->push_back(7);

        // sides
        primitives->push_back(1);
        primitives->push_back(4);
        primitives->push_back(0);

        primitives->push_back(4);
        primitives->push_back(1);
        primitives->push_back(5);

        primitives->push_back(4);
        primitives->push_back(2);
        primitives->push_back(0);

        primitives->push_back(6);
        primitives->push_back(2);
        primitives->push_back(4);

        primitives->push_back(6);
        primitives->push_back(3);
        primitives->push_back(2);

        primitives->push_back(7);
        primitives->push_back(3);
        primitives->push_back(6);

        primitives->push_back(1);
        primitives->push_back(3);
        primitives->push_back(5);

        primitives->push_back(5);
        primitives->push_back(3);
        primitives->push_back(7);

        geometry->addPrimitiveSet(primitives);

        osg::Vec4Array* colours = new osg::Vec4Array;

        colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.5f, 0.2f));
        colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f));
        colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.4f, 0.2f));
        colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f));
        colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.4f, 0.2f));
        colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f));
        colours->push_back(osg::Vec4f(0.3f, 0.3f, 0.4f, 0.2f));
        colours->push_back(osg::Vec4f(0.9f, 0.9f, 1.0f, 0.2f));

        geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX);

        geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
        geometry->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON);
        geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
        geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);

        mBaseNode->addChild(geometry);
        mParentNode->addChild(mBaseNode);
    }

    void InstanceSelectionMode::drawSelectionSphere(const osg::Vec3f& mousePlanePoint)
    {
        float dragDistance = (mDragStart - mousePlanePoint).length();
        drawSelectionSphere(mDragStart, dragDistance);
    }

    void InstanceSelectionMode::drawSelectionSphere(const osg::Vec3d& point, float radius)
    {
        if (mBaseNode)
            mParentNode->removeChild(mBaseNode);
        mBaseNode = new osg::PositionAttitudeTransform;
        mBaseNode->setPosition(point);

        osg::ref_ptr<osg::Geometry> geometry(new osg::Geometry);

        osg::Vec3Array* vertices = new osg::Vec3Array;
        constexpr int resolution = 32;
        float radiusPerResolution = radius / resolution;
        float reciprocalResolution = 1.0f / resolution;
        float doubleReciprocalRes = reciprocalResolution * 2;

        osg::Vec4Array* colours = new osg::Vec4Array;

        for (int i = 0; i <= resolution; i += 2)
        {
            float iShifted = (static_cast<float>(i) - resolution / 2.0f); // i - 16 = -16 ... 16
            float xPercentile = iShifted * doubleReciprocalRes;
            float x = xPercentile * radius;
            float thisRadius = sqrt(radius * radius - x * x);

            // the next row
            float iShifted2 = (static_cast<float>(i + 1) - resolution / 2.0f);
            float xPercentile2 = iShifted2 * doubleReciprocalRes;
            float x2 = xPercentile2 * radius;
            float thisRadius2 = sqrt(radius * radius - x2 * x2);

            for (int j = 0; j < resolution; ++j)
            {
                float vertexX = thisRadius * sin(j * reciprocalResolution * osg::PI * 2);
                float vertexY = i * radiusPerResolution * 2 - radius;
                float vertexZ = thisRadius * cos(j * reciprocalResolution * osg::PI * 2);
                float heightPercentage = (vertexZ + radius) / (radius * 2);
                vertices->push_back(osg::Vec3f(vertexX, vertexY, vertexZ));
                colours->push_back(osg::Vec4f(heightPercentage, heightPercentage, heightPercentage, 0.3f));

                float vertexNextRowX = thisRadius2 * sin(j * reciprocalResolution * osg::PI * 2);
                float vertexNextRowY = (i + 1) * radiusPerResolution * 2 - radius;
                float vertexNextRowZ = thisRadius2 * cos(j * reciprocalResolution * osg::PI * 2);
                float heightPercentageNextRow = (vertexZ + radius) / (radius * 2);
                vertices->push_back(osg::Vec3f(vertexNextRowX, vertexNextRowY, vertexNextRowZ));
                colours->push_back(
                    osg::Vec4f(heightPercentageNextRow, heightPercentageNextRow, heightPercentageNextRow, 0.3f));
            }
        }

        geometry->setVertexArray(vertices);

        osg::DrawElementsUShort* primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLE_STRIP, 0);

        for (int i = 0; i < resolution; ++i)
        {
            // Even
            for (int j = 0; j < resolution * 2; ++j)
            {
                if (i * resolution * 2 + j > static_cast<int>(vertices->size()) - 1)
                    continue;
                primitives->push_back(i * resolution * 2 + j);
            }
            if (i * resolution * 2 > static_cast<int>(vertices->size()) - 1)
                continue;
            primitives->push_back(i * resolution * 2);
            primitives->push_back(i * resolution * 2 + 1);

            // Odd
            for (int j = 1; j < resolution * 2 - 2; j += 2)
            {
                if ((i + 1) * resolution * 2 + j - 1 > static_cast<int>(vertices->size()) - 1)
                    continue;
                primitives->push_back((i + 1) * resolution * 2 + j - 1);
                primitives->push_back(i * resolution * 2 + j + 2);
            }
            if ((i + 2) * resolution * 2 - 2 > static_cast<int>(vertices->size()) - 1)
                continue;
            primitives->push_back((i + 2) * resolution * 2 - 2);
            primitives->push_back(i * resolution * 2 + 1);
            primitives->push_back((i + 1) * resolution * 2);
        }

        geometry->addPrimitiveSet(primitives);

        geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX);

        geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
        geometry->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON);
        geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
        geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);

        mBaseNode->addChild(geometry);
        mParentNode->addChild(mBaseNode);
    }

    bool InstanceSelectionMode::createContextMenu(QMenu* menu)
    {
        if (menu)
        {
            SelectionMode::createContextMenu(menu);

            menu->addAction(mSelectSame);
            menu->addAction(mDeleteSelection);
        }

        return true;
    }

    void InstanceSelectionMode::selectSame()
    {
        getWorldspaceWidget().selectAllWithSameParentId(Mask_Reference);
    }

    void InstanceSelectionMode::deleteSelection()
    {
        std::vector<osg::ref_ptr<TagBase>> selection = getWorldspaceWidget().getSelection(Mask_Reference);

        CSMWorld::IdTable& referencesTable = dynamic_cast<CSMWorld::IdTable&>(
            *getWorldspaceWidget().getDocument().getData().getTableModel(CSMWorld::UniversalId::Type_References));

        for (std::vector<osg::ref_ptr<TagBase>>::iterator iter = selection.begin(); iter != selection.end(); ++iter)
        {
            CSMWorld::DeleteCommand* command = new CSMWorld::DeleteCommand(
                referencesTable, static_cast<ObjectTag*>(iter->get())->mObject->getReferenceId());

            getWorldspaceWidget().getDocument().getUndoStack().push(command);
        }
    }
}