// Copyright (C) 2015 Paul Lemire paul.lemire350@gmail.com
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "pickboundingvolumejob_p.h"
#include "qpicktriangleevent.h"
#include "qpicklineevent.h"
#include "qpickpointevent.h"
#include <Qt3DCore/private/qaspectmanager_p.h>
#include <Qt3DCore/private/vector_helper_p.h>
#include <Qt3DRender/qobjectpicker.h>
#include <Qt3DRender/qviewport.h>
#include <Qt3DRender/qgeometryrenderer.h>
#include <Qt3DRender/private/qobjectpicker_p.h>
#include <Qt3DRender/private/nodemanagers_p.h>
#include <Qt3DRender/private/entity_p.h>
#include <Qt3DRender/private/objectpicker_p.h>
#include <Qt3DRender/private/managers_p.h>
#include <Qt3DRender/private/geometryrenderer_p.h>
#include <Qt3DRender/private/rendersettings_p.h>
#include <Qt3DRender/private/trianglesvisitor_p.h>
#include <Qt3DRender/private/job_common_p.h>
#include <Qt3DRender/private/qpickevent_p.h>
#include <Qt3DRender/private/pickboundingvolumeutils_p.h>

#include <QSurface>
#include <QWindow>
#include <QOffscreenSurface>

QT_BEGIN_NAMESPACE

namespace Qt3DRender {
using namespace Qt3DRender::RayCasting;

namespace Render {

class PickBoundingVolumeJobPrivate : public Qt3DCore::QAspectJobPrivate
{
public:
    PickBoundingVolumeJobPrivate(PickBoundingVolumeJob *q) : q_ptr(q) { }
    ~PickBoundingVolumeJobPrivate() override = default;

    bool isRequired() const override;
    void postFrame(Qt3DCore::QAspectManager *manager) override;

    enum CustomEventType {
        MouseButtonClick = QEvent::User,
    };

    struct EventDetails {
        Qt3DCore::QNodeId pickerId;
        int sourceEventType;
        QPickEventPtr resultingEvent;
        Qt3DCore::QNodeId viewportNodeId;
    };

    QList<EventDetails> dispatches;
    PickBoundingVolumeJob *q_ptr;
    Q_DECLARE_PUBLIC(PickBoundingVolumeJob)
};


bool PickBoundingVolumeJobPrivate::isRequired() const
{
    Q_Q(const PickBoundingVolumeJob);
    return !q->m_pendingMouseEvents.empty() || q->m_pickersDirty || q->m_oneEnabledAtLeast;
}

void PickBoundingVolumeJobPrivate::postFrame(Qt3DCore::QAspectManager *manager)
{
    using namespace Qt3DCore;
    QNodeId previousId;
    QObjectPicker *node = nullptr;

    for (auto res: std::as_const(dispatches)) {
        if (previousId != res.pickerId) {
            node = qobject_cast<QObjectPicker *>(manager->lookupNode(res.pickerId));
            previousId = res.pickerId;
        }
        if (!node)
            continue;

        QObjectPickerPrivate *dnode = static_cast<QObjectPickerPrivate *>(QObjectPickerPrivate::get(node));

        // resolve front end details
        QPickEvent *pickEvent = res.resultingEvent.data();
        if (pickEvent) {
            QPickEventPrivate *dpickEvent = QPickEventPrivate::get(pickEvent);
            dpickEvent->m_viewport = static_cast<QViewport *>(manager->lookupNode(res.viewportNodeId));
            dpickEvent->m_entityPtr = static_cast<QEntity *>(manager->lookupNode(dpickEvent->m_entity));
        }

        // dispatch event
        switch (res.sourceEventType) {
        case QEvent::MouseButtonPress:
            dnode->pressedEvent(pickEvent);
            break;
        case QEvent::MouseButtonRelease:
            dnode->releasedEvent(pickEvent);
            break;
        case MouseButtonClick:
            dnode->clickedEvent(pickEvent);
            break;
        case QEvent::MouseMove:
            dnode->movedEvent(pickEvent);
            break;
        case QEvent::Enter:
            emit node->entered();
            dnode->setContainsMouse(true);
            break;
        case QEvent::Leave:
            dnode->setContainsMouse(false);
            emit node->exited();
            break;
        default: Q_UNREACHABLE();
        }
    }

    dispatches.clear();
}

namespace {

void setEventButtonAndModifiers(const QMouseEvent *event, QPickEvent::Buttons &eventButton, int &eventButtons, int &eventModifiers)
{
    switch (event->button()) {
    case Qt::LeftButton:
        eventButton = QPickEvent::LeftButton;
        break;
    case Qt::RightButton:
        eventButton = QPickEvent::RightButton;
        break;
    case Qt::MiddleButton:
        eventButton = QPickEvent::MiddleButton;
        break;
    case Qt::BackButton:
        eventButton = QPickEvent::BackButton;
        break;
    default:
        break;
    }

    if (event->buttons() & Qt::LeftButton)
        eventButtons |= QPickEvent::LeftButton;
    if (event->buttons() & Qt::RightButton)
        eventButtons |= QPickEvent::RightButton;
    if (event->buttons() & Qt::MiddleButton)
        eventButtons |= QPickEvent::MiddleButton;
    if (event->buttons() & Qt::BackButton)
        eventButtons |= QPickEvent::BackButton;
    if (event->modifiers() & Qt::ShiftModifier)
        eventModifiers |= QPickEvent::ShiftModifier;
    if (event->modifiers() & Qt::ControlModifier)
        eventModifiers |= QPickEvent::ControlModifier;
    if (event->modifiers() & Qt::AltModifier)
        eventModifiers |= QPickEvent::AltModifier;
    if (event->modifiers() & Qt::MetaModifier)
        eventModifiers |= QPickEvent::MetaModifier;
    if (event->modifiers() & Qt::KeypadModifier)
        eventModifiers |= QPickEvent::KeypadModifier;
}

} // anonymous

PickBoundingVolumeJob::PickBoundingVolumeJob()
    : AbstractPickingJob(*new PickBoundingVolumeJobPrivate(this))
    , m_pickersDirty(true)
{
    SET_JOB_RUN_STAT_TYPE(this, JobTypes::PickBoundingVolume, 0)
}

void PickBoundingVolumeJob::setRoot(Entity *root)
{
    m_node = root;
}

bool PickBoundingVolumeJob::processMouseEvent(QObject* object, QMouseEvent *event)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    m_pendingMouseEvents.emplace_back(object, std::unique_ptr<QMouseEvent>(static_cast<QMouseEvent *>(event->clone())));
#else
    m_pendingMouseEvents.emplace_back(object, new QMouseEvent(*event));
#endif
    return false;
}

void PickBoundingVolumeJob::markPickersDirty()
{
    m_pickersDirty = true;
}

bool PickBoundingVolumeJob::runHelper()
{
    // Move to clear the events so that we don't process them several times
    // if run is called several times
    const auto mouseEvents = Qt3DCore::moveAndClear(m_pendingMouseEvents);

    // If we have no events return early
    if (mouseEvents.empty())
        return false;

    // Quickly look which picker settings we've got
    if (m_pickersDirty) {
        m_pickersDirty = false;
        m_oneEnabledAtLeast = false;
        m_oneHoverAtLeast = false;

        const auto activeHandles = m_manager->objectPickerManager()->activeHandles();
        for (const auto &handle : activeHandles) {
            auto picker = m_manager->objectPickerManager()->data(handle);
            m_oneEnabledAtLeast |= picker->isEnabled();
            m_oneHoverAtLeast |= picker->isHoverEnabled();
            if (m_oneEnabledAtLeast && m_oneHoverAtLeast)
                break;
        }
    }

    // bail out early if no picker is enabled
    if (!m_oneEnabledAtLeast)
        return false;

    bool hasMoveEvent = false;
    bool hasOtherEvent = false;
    // Quickly look which types of events we've got
    for (const auto &event : mouseEvents) {
        const bool isMove = (event.second->type() == QEvent::MouseMove);
        hasMoveEvent |= isMove;
        hasOtherEvent |= !isMove;
    }

    // In the case we have a move event, find if we actually have
    // an object picker that cares about these
    if (!hasOtherEvent) {
        // Retrieve the last used object picker
        ObjectPicker *lastCurrentPicker = m_manager->objectPickerManager()->data(m_currentPicker);

        // The only way to set lastCurrentPicker is to click
        // so we can return since if we're there it means we
        // have only move events. But keep on if hover support
        // is needed
        if (lastCurrentPicker == nullptr && !m_oneHoverAtLeast)
            return false;

        const bool caresAboutMove = (hasMoveEvent &&
                                      (m_oneHoverAtLeast ||
                                        (lastCurrentPicker && lastCurrentPicker->isDragEnabled())));
        // Early return if the current object picker doesn't care about move events
        if (!caresAboutMove)
            return false;
    }

    const PickingUtils::PickConfiguration pickConfiguration(m_frameGraphRoot, m_renderSettings);
    if (pickConfiguration.vcaDetails.empty())
        return false;

    // TO DO:
    // If we have move or hover move events that someone cares about, we try to avoid expensive computations
    // by compressing them into a single one

    // For each mouse event
    for (const auto &event : mouseEvents)
        processPickEvent(pickConfiguration, event.first, event.second.get());

    // Clear Hovered elements that needs to be cleared
    // Send exit event to object pickers on which we
    // had set the hovered flag for a previous frame
    // and that aren't being hovered any longer
    clearPreviouslyHoveredPickers();
    return true;
}

void PickBoundingVolumeJob::processPickEvent(const PickingUtils::PickConfiguration &pickConfiguration, QObject *object, const QMouseEvent *event)
{
    m_hoveredPickersToClear = m_hoveredPickers;

    QPickEvent::Buttons eventButton = QPickEvent::NoButton;
    int eventButtons = 0;
    int eventModifiers = QPickEvent::NoModifier;

    setEventButtonAndModifiers(event, eventButton, eventButtons, eventModifiers);

    // For each Viewport / Camera and Area entry
    for (const PickingUtils::ViewportCameraAreaDetails &vca : pickConfiguration.vcaDetails) {
        PickingUtils::HitList sphereHits;
        QRay3D ray = rayForViewportAndCamera(vca, object, event->pos());
        if (!ray.isValid()) {
            // An invalid rays is when we've lost our surface or the mouse
            // has moved out of the viewport In case of a button released
            // outside of the viewport, we still want to notify the
            // lastCurrent entity about this.
            dispatchPickEvents(event, PickingUtils::HitList(), eventButton, eventButtons, eventModifiers, m_renderSettings->pickResultMode(),
                               vca.viewportNodeId);
            continue;
        }

        PickingUtils::HierarchicalEntityPicker entityPicker(ray);
        entityPicker.setLayerFilterIds(vca.layersFilters);

        if (entityPicker.collectHits(m_manager, m_node)) {
            if (pickConfiguration.trianglePickingRequested) {
                PickingUtils::TriangleCollisionGathererFunctor gathererFunctor;
                gathererFunctor.m_frontFaceRequested = pickConfiguration.frontFaceRequested;
                gathererFunctor.m_backFaceRequested = pickConfiguration.backFaceRequested;
                gathererFunctor.m_manager = m_manager;
                gathererFunctor.m_ray = ray;
                gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable();
                Qt3DCore::moveAtEnd(sphereHits, gathererFunctor.computeHits(entityPicker.entities(), m_renderSettings->pickResultMode()));
            }
            if (pickConfiguration.edgePickingRequested) {
                PickingUtils::LineCollisionGathererFunctor gathererFunctor;
                gathererFunctor.m_manager = m_manager;
                gathererFunctor.m_ray = ray;
                gathererFunctor.m_pickWorldSpaceTolerance = pickConfiguration.pickWorldSpaceTolerance;
                gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable();
                Qt3DCore::moveAtEnd(sphereHits, gathererFunctor.computeHits(entityPicker.entities(), m_renderSettings->pickResultMode()));
                PickingUtils::AbstractCollisionGathererFunctor::sortHits(sphereHits);
            }
            if (pickConfiguration.pointPickingRequested) {
                PickingUtils::PointCollisionGathererFunctor gathererFunctor;
                gathererFunctor.m_manager = m_manager;
                gathererFunctor.m_ray = ray;
                gathererFunctor.m_pickWorldSpaceTolerance = pickConfiguration.pickWorldSpaceTolerance;
                gathererFunctor.m_entityToPriorityTable = entityPicker.entityToPriorityTable();
                Qt3DCore::moveAtEnd(sphereHits, gathererFunctor.computeHits(entityPicker.entities(), m_renderSettings->pickResultMode()));
                PickingUtils::AbstractCollisionGathererFunctor::sortHits(sphereHits);
            }
            if (!pickConfiguration.primitivePickingRequested) {
                Qt3DCore::moveAtEnd(sphereHits, entityPicker.hits());
                PickingUtils::AbstractCollisionGathererFunctor::sortHits(sphereHits);
                if (m_renderSettings->pickResultMode() != QPickingSettings::AllPicks)
                    sphereHits = { sphereHits.front() };
            }
        }

        // Dispatch events based on hit results
        dispatchPickEvents(event, sphereHits, eventButton, eventButtons, eventModifiers, m_renderSettings->pickResultMode(),
                           vca.viewportNodeId);
    }
}

void PickBoundingVolumeJob::dispatchPickEvents(const QMouseEvent *event,
                                               const PickingUtils::HitList &sphereHits,
                                               QPickEvent::Buttons eventButton,
                                               int eventButtons,
                                               int eventModifiers,
                                               bool allHitsRequested,
                                               Qt3DCore::QNodeId viewportNodeId)
{
    Q_D(PickBoundingVolumeJob);

    ObjectPicker *lastCurrentPicker = m_manager->objectPickerManager()->data(m_currentPicker);
    // If we have hits
    if (!sphereHits.empty()) {
        // Note: how can we control that we want the first/last/all elements along the ray to be picked

        // How do we differentiate betwnee an Entity with no GeometryRenderer and one with one, both having
        // an ObjectPicker component when it comes

        for (const QCollisionQueryResult::Hit &hit : std::as_const(sphereHits)) {
            Entity *entity = m_manager->renderNodesManager()->lookupResource(hit.m_entityId);
            HObjectPicker objectPickerHandle = entity->componentHandle<ObjectPicker>();

            // If the Entity which actually received the hit doesn't have
            // an object picker component, we need to check the parent if it has one ...
            while (objectPickerHandle.isNull() && entity != nullptr) {
                entity = entity->parent();
                if (entity != nullptr)
                    objectPickerHandle = entity->componentHandle<ObjectPicker>();
            }

            ObjectPicker *objectPicker = m_manager->objectPickerManager()->data(objectPickerHandle);
            if (objectPicker != nullptr && objectPicker->isEnabled()) {

                if (lastCurrentPicker && !allHitsRequested) {
                    // if there is a current picker, it will "grab" all events until released.
                    // Clients should test that entity is what they expect (or only use
                    // world coordinates)
                    objectPicker = lastCurrentPicker;
                }

                // Send the corresponding event
                Vector3D localIntersection = hit.m_intersection;
                if (entity && entity->worldTransform())
                    localIntersection = entity->worldTransform()->inverted().map(hit.m_intersection);

                QPickEventPtr pickEvent;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
                const auto eventPos = event->position();
#else
                const auto eventPos = event->pos();
#endif
                switch (hit.m_type) {
                case QCollisionQueryResult::Hit::Triangle:
                    pickEvent.reset(new QPickTriangleEvent(eventPos,
                                                           convertToQVector3D(hit.m_intersection),
                                                           convertToQVector3D(localIntersection),
                                                           hit.m_distance,
                                                           hit.m_primitiveIndex,
                                                           hit.m_vertexIndex[0],
                                                           hit.m_vertexIndex[1],
                                                           hit.m_vertexIndex[2],
                                                           eventButton, eventButtons,
                                                           eventModifiers,
                                                           convertToQVector3D(hit.m_uvw)));
                    break;
                case QCollisionQueryResult::Hit::Edge:
                    pickEvent.reset(new QPickLineEvent(eventPos,
                                                       convertToQVector3D(hit.m_intersection),
                                                       convertToQVector3D(localIntersection),
                                                       hit.m_distance,
                                                       hit.m_primitiveIndex,
                                                       hit.m_vertexIndex[0], hit.m_vertexIndex[1],
                                                       eventButton, eventButtons, eventModifiers));
                    break;
                case QCollisionQueryResult::Hit::Point:
                    pickEvent.reset(new QPickPointEvent(eventPos,
                                                        convertToQVector3D(hit.m_intersection),
                                                        convertToQVector3D(localIntersection),
                                                        hit.m_distance,
                                                        hit.m_vertexIndex[0],
                                                        eventButton, eventButtons, eventModifiers));
                    break;
                case QCollisionQueryResult::Hit::Entity:
                    pickEvent.reset(new QPickEvent(eventPos,
                                                   convertToQVector3D(hit.m_intersection),
                                                   convertToQVector3D(localIntersection),
                                                   hit.m_distance,
                                                   eventButton, eventButtons, eventModifiers));
                    break;
                default:
                    Q_UNREACHABLE();
                }
                Qt3DRender::QPickEventPrivate::get(pickEvent.data())->m_entity = hit.m_entityId;
                switch (event->type()) {
                case QEvent::MouseButtonPress: {
                    // Store pressed object handle
                    m_currentPicker = objectPickerHandle;
                    m_currentViewport = viewportNodeId;
                    // Send pressed event to m_currentPicker
                    d->dispatches.push_back({objectPicker->peerId(), event->type(), pickEvent, viewportNodeId});
                    objectPicker->setPressed(true);
                    break;
                }

                case QEvent::MouseButtonRelease: {
                    // Only send the release event if it was pressed
                    if (objectPicker->isPressed()) {
                        d->dispatches.push_back({objectPicker->peerId(), event->type(), pickEvent, viewportNodeId});
                        objectPicker->setPressed(false);
                    }
                    if (lastCurrentPicker != nullptr && m_currentPicker == objectPickerHandle) {
                        d->dispatches.push_back({objectPicker->peerId(),
                                                 PickBoundingVolumeJobPrivate::MouseButtonClick,
                                                 pickEvent, viewportNodeId});
                        m_currentPicker = HObjectPicker();
                        m_currentViewport = {};
                    }
                    break;
                }
#if QT_CONFIG(gestures)
                case QEvent::Gesture: {
                    d->dispatches.push_back({objectPicker->peerId(),
                                             PickBoundingVolumeJobPrivate::MouseButtonClick,
                                             pickEvent, viewportNodeId});
                    break;
                }
#endif
                case QEvent::MouseMove: {
                    if ((objectPicker->isPressed() || objectPicker->isHoverEnabled()) && objectPicker->isDragEnabled())
                        d->dispatches.push_back({objectPicker->peerId(), event->type(), pickEvent, viewportNodeId});
                    Q_FALLTHROUGH(); // fallthrough
                }
                case QEvent::HoverMove: {
                    if (!m_hoveredPickers.contains(objectPickerHandle)) {
                        if (objectPicker->isHoverEnabled()) {
                            // Send entered event to objectPicker
                            d->dispatches.push_back({objectPicker->peerId(), QEvent::Enter, pickEvent, viewportNodeId});
                            // and save it in the hoveredPickers
                            m_hoveredPickers.push_back(objectPickerHandle);
                        }
                    }
                    break;
                }

                default:
                    break;
                }
            }

            // The ObjectPicker was hit -> it is still being hovered
            m_hoveredPickersToClear.removeAll(objectPickerHandle);

            lastCurrentPicker = m_manager->objectPickerManager()->data(m_currentPicker);
        }

        // Otherwise no hits
    } else {
        switch (event->type()) {
        case QEvent::MouseButtonRelease: {
            // Send release event to m_currentPicker
            if (lastCurrentPicker != nullptr && m_currentViewport == viewportNodeId) {
                m_currentPicker = HObjectPicker();
                m_currentViewport = {};
                QPickEventPtr pickEvent(new QPickEvent);
                lastCurrentPicker->setPressed(false);
                d->dispatches.push_back({lastCurrentPicker->peerId(), event->type(), pickEvent, viewportNodeId});
            }
            break;
        }
        default:
            break;
        }
    }
}

void PickBoundingVolumeJob::clearPreviouslyHoveredPickers()
{
    Q_D(PickBoundingVolumeJob);

    for (const HObjectPicker &pickHandle : std::as_const(m_hoveredPickersToClear)) {
        ObjectPicker *pick = m_manager->objectPickerManager()->data(pickHandle);
        if (pick)
            d->dispatches.push_back({pick->peerId(), QEvent::Leave, {}, {}});
        m_hoveredPickers.removeAll(pickHandle);
    }

    m_hoveredPickersToClear.clear();
}

} // namespace Render

} // namespace Qt3DRender

QT_END_NAMESPACE
