// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include "qquick3dcamera_p.h"

#include <QtQuick3DRuntimeRender/private/qssgrendercamera_p.h>

#include "qquick3dquaternionutils_p.h"
#include "qquick3dnode_p_p.h"

#include <QtMath>
#include <QtQuick3DUtils/private/qssgutils_p.h>

#include "qquick3dutils_p.h"

QT_BEGIN_NAMESPACE

/*!
    \qmltype Camera
    \inherits Node
    \inqmlmodule QtQuick3D
    \brief Defines an abstract base for Cameras.

    A Camera defines how the content of the 3D scene is projected onto a 2D surface,
    such as a View3D. A scene needs at least one Camera in order to visualize its
    contents.

    It is possible to position and rotate the Camera like any other spatial \l{QtQuick3D::Node}{Node} in
    the scene. The \l{QtQuick3D::Node}{Node}'s location and orientation determines where the Camera is in
    the scene, and what direction it is facing. The default orientation of the camera
    has its forward vector pointing along the negative Z axis and its up vector along
    the positive Y axis.

    Together with the position and orientation, the frustum defines which parts of
    a scene are visible to the Camera and how they are projected onto the 2D surface.
    The different Camera subtypes provide multiple options to determine the shape of the
    Camera's frustum.

    \list
    \li PerspectiveCamera provides a camera with a pyramid-shaped frustum, where objects are
    projected so that those further away from the camera appear to be smaller. This is the
    most commonly used camera type, and corresponds to how most real world cameras work.
    \li OrthographicCamera provides a camera where the lines of the frustum are parallel,
    making the perceived scale of an object unaffected by its distance to the camera. Typical
    use cases for this type of camera are CAD (Computer-Assisted Design) applications and
    cartography.
    \li FrustumCamera is a perspective camera type where the frustum can be freely customized
    by the coordinates of its intersection with the near plane. It can be useful if an
    asymmetrical camera frustum is needed.
    \li CustomCamera is a camera type where the projection matrix can be freely customized,
    and can be useful for advanced users who wish to calculate their own projection matrix.
    \endlist

    To illustrate the difference, these screenshots show the same scene as projected by a
    PerspectiveCamera and an OrthographicCamera. Notice how the red box is smaller than the
    green box in the image rendered using the perspective projection.
    \table
    \header
    \li Perspective camera
    \li Orthographic camera
    \row
    \li \image perspectivecamera.png
    \li \image orthographiccamera.png
    \endtable

    \sa {Qt Quick 3D - View3D Example}
*/

/*!
    \internal
*/
QQuick3DCamera::QQuick3DCamera(QQuick3DNodePrivate &dd, QQuick3DNode *parent)
    : QQuick3DNode(dd, parent) {}

/*!
    \qmlproperty bool Camera::frustumCullingEnabled

    When this property is \c true, objects outside the camera frustum will be culled, meaning they will
    not be passed to the renderer. By default this property is set to \c false. For scenes where all or
    most objects are inside the camera frustum, frustum culling is an unnecessary performance overhead.
    But for complex scenes where large parts are located outside the camera's view, enabling frustum
    culling may improve performance.
*/
bool QQuick3DCamera::frustumCullingEnabled() const
{
    return m_frustumCullingEnabled;
}

void QQuick3DCamera::setFrustumCullingEnabled(bool frustumCullingEnabled)
{
    if (m_frustumCullingEnabled == frustumCullingEnabled)
        return;

    m_frustumCullingEnabled = frustumCullingEnabled;
    emit frustumCullingEnabledChanged();
    update();
}

/*!
    \qmlproperty Node Camera::lookAtNode

    If this property is set to a \c non-null value, the rotation of this camera is automatically
    updated so that this camera keeps looking at the specified node whenever the scene position of
    this camera or the specified node changes.
    By default this property is set to \c{null}.

    \sa lookAt
*/
QQuick3DNode *QQuick3DCamera::lookAtNode() const
{
    return m_lookAtNode;
}

void QQuick3DCamera::setLookAtNode(QQuick3DNode *node)
{
    if (m_lookAtNode == node)
        return;

    if (m_lookAtNode) {
        disconnect(m_lookAtNode, &QQuick3DNode::scenePositionChanged, this, &QQuick3DCamera::updateLookAt);
        disconnect(this, &QQuick3DNode::scenePositionChanged, this, &QQuick3DCamera::updateLookAt);
    }

    m_lookAtNode = node;

    if (m_lookAtNode) {
        connect(m_lookAtNode, &QQuick3DNode::scenePositionChanged, this, &QQuick3DCamera::updateLookAt);
        connect(this, &QQuick3DNode::scenePositionChanged, this, &QQuick3DCamera::updateLookAt);
    }

    emit lookAtNodeChanged();
    updateLookAt();
}

/*!
    \qmlmethod vector3d Camera::mapToViewport(vector3d scenePos)

    Transforms \a scenePos from global scene space (3D) into viewport space (2D).

    The returned position is normalized, with the top-left of the viewport
    at [0, 0] and the bottom-right at [1, 1]. The returned z-value will contain
    the distance from the near clip plane of the frustum (clipNear) to \a scenePos in
    scene coordinates. If the distance is negative, the point is behind camera.

    If \a scenePos cannot successfully be mapped to a position in the viewport, a
    position of [0, 0, 0] is returned.

    \sa mapFromViewport(), {View3D::mapFrom3DScene()}{View3D.mapFrom3DScene()}
*/
QVector3D QQuick3DCamera::mapToViewport(const QVector3D &scenePos) const
{
    QSSGRenderCamera *cameraNode = static_cast<QSSGRenderCamera *>(QQuick3DObjectPrivate::get(this)->spatialNode);
    if (!cameraNode)
        return QVector3D(0, 0, 0);

    QVector4D scenePosRightHand(scenePos, 1);

    // Transform position
    const QMatrix4x4 sceneToCamera = sceneTransform().inverted();
    const QMatrix4x4 projectionViewMatrix = cameraNode->projection * sceneToCamera;
    const QVector4D transformedScenePos = QSSGUtils::mat44::transform(projectionViewMatrix, scenePosRightHand);

    if (qFuzzyIsNull(transformedScenePos.w()) || qIsNaN(transformedScenePos.w()))
        return QVector3D(0, 0, 0);

    // Normalize scenePosView between [-1, 1]
    QVector3D scenePosView = transformedScenePos.toVector3D() / transformedScenePos.w();

    // Set z to be the scene distance from clipNear so that the return value
    // can be used as argument to viewportToscene() to reverse the call.
    const QVector4D clipNearPos(scenePosView.x(), scenePosView.y(), -1, 1);
    auto invProj = projectionViewMatrix.inverted();
    const QVector4D clipNearPosTransformed = QSSGUtils::mat44::transform(invProj, clipNearPos);
    if (qFuzzyIsNull(clipNearPosTransformed.w()) || qIsNaN(clipNearPosTransformed.w()))
        return QVector3D(0, 0, 0);
    const QVector4D clipNearPosScene = clipNearPosTransformed / clipNearPosTransformed.w();
    QVector4D clipFarPos = clipNearPos;
    clipFarPos.setZ(0);
    const QVector4D clipFarPosTransformed = QSSGUtils::mat44::transform(invProj, clipFarPos);
    if (qFuzzyIsNull(clipFarPosTransformed.w()) || qIsNaN(clipFarPosTransformed.w()))
        return QVector3D(0, 0, 0);
    const QVector4D clipFarPosScene = clipFarPosTransformed / clipFarPosTransformed.w();
    const QVector3D direction = (clipFarPosScene - clipNearPosScene).toVector3D();
    const QVector3D scenePosVec = (scenePosRightHand - clipNearPosScene).toVector3D();

    const float distanceToScenePos = scenePosVec.length()
            * (QVector3D::dotProduct(direction, scenePosVec) > 0.f ? 1 : -1);

    scenePosView.setZ(distanceToScenePos);

    // Convert x and y to be between [0, 1]
    scenePosView.setX((scenePosView.x() / 2) + 0.5f);
    scenePosView.setY((scenePosView.y() / 2) + 0.5f);
    // And convert origin from bottom-left to top-left
    scenePosView.setY(1 - scenePosView.y());
    return scenePosView;
}

/*!
    \qmlmethod vector3d Camera::mapFromViewport(vector3d viewportPos)

    Transforms \a viewportPos from viewport space (2D) into global scene space (3D).

    The x- and y-values of \a viewportPos must be normalized, with the top-left
    of the viewport at [0, 0] and the bottom-right at [1, 1]. The z-value is interpreted
    as the distance from the near clip plane of the frustum (clipNear).

    If \a viewportPos cannot successfully be mapped to a position in the scene, a position
    of [0, 0, 0] is returned.

    \sa mapToViewport, {View3D::mapTo3DScene()}{View3D.mapTo3DScene()}
*/
QVector3D QQuick3DCamera::mapFromViewport(const QVector3D &viewportPos) const
{
    QSSGRenderCamera *cameraNode = static_cast<QSSGRenderCamera *>(QQuick3DObjectPrivate::get(this)->spatialNode);
    if (!cameraNode)
        return QVector3D(0, 0, 0);

    // Pick two positions in the frustum
    QVector4D clipNearPos(viewportPos, 1);
    // Convert origin from top-left to bottom-left
    clipNearPos.setY(1 - clipNearPos.y());
    // Convert to homogenous position between [-1, 1]
    clipNearPos.setX((clipNearPos.x() * 2.0f) - 1.0f);
    clipNearPos.setY((clipNearPos.y() * 2.0f) - 1.0f);
    QVector4D clipFarPos = clipNearPos;
    // clipNear: z = -1, clipFar: z = 1. It's recommended to use 0 as
    // far pos instead of clipFar because of infinite projection issues.
    clipNearPos.setZ(-1);
    clipFarPos.setZ(0);

    // Transform position to scene
    const QMatrix4x4 sceneToCamera = sceneTransform().inverted();
    const QMatrix4x4 projectionViewMatrixInv = (cameraNode->projection * sceneToCamera).inverted();
    const QVector4D transformedClipNearPos = QSSGUtils::mat44::transform(projectionViewMatrixInv, clipNearPos);
    const QVector4D transformedClipFarPos = QSSGUtils::mat44::transform(projectionViewMatrixInv, clipFarPos);

    if (qFuzzyIsNull(transformedClipNearPos.w()) || qIsNaN(transformedClipNearPos.w()) ||
        qFuzzyIsNull(transformedClipFarPos.w()) || qIsNaN(transformedClipFarPos.w()))
        return QVector3D(0, 0, 0);

    // Reverse the projection
    const QVector3D clipNearPosScene = transformedClipNearPos.toVector3D()
            / transformedClipNearPos.w();
    const QVector3D clipFarPosScene = transformedClipFarPos.toVector3D()
            / transformedClipFarPos.w();

    // Calculate the position in the scene
    const QVector3D direction = (clipFarPosScene - clipNearPosScene).normalized();
    const float distanceFromClipNear = viewportPos.z();
    QVector3D scenePos = clipNearPosScene + (direction * distanceFromClipNear);

    return scenePos;
}

/*!
 * \internal
 */
QVector3D QQuick3DCamera::mapToViewport(const QVector3D &scenePos,
                                        qreal width,
                                        qreal height)
{
    // We can be called before a spatial node is created, if that is the case, create the node now.
    if (QSSGRenderCamera *cameraNode = static_cast<QSSGRenderCamera *>(updateSpatialNode(QQuick3DObjectPrivate::get(this)->spatialNode))) {
        QQuick3DObjectPrivate::get(this)->spatialNode = cameraNode;
        QSSGRenderCamera::Configuration camConfig { cameraNode->getDpr(), 1.0f /* ssaa multiplier */ };
        QSSGRenderCamera::calculateProjectionInternal(*cameraNode, QRect(0, 0, width * camConfig.dpr, height * camConfig.dpr), camConfig);
    }

    return QQuick3DCamera::mapToViewport(scenePos);
}

/*!
 * \internal
 */
QVector3D QQuick3DCamera::mapFromViewport(const QVector3D &viewportPos,
                                          qreal width,
                                          qreal height)
{
    // We can be called before a spatial node is created, if that is the case, create the node now.
    if (QSSGRenderCamera *cameraNode = static_cast<QSSGRenderCamera *>(updateSpatialNode(QQuick3DObjectPrivate::get(this)->spatialNode))) {
        QQuick3DObjectPrivate::get(this)->spatialNode = cameraNode;
        QSSGRenderCamera::Configuration camConfig { cameraNode->getDpr(), 1.0f /* ssaa multiplier */ };
        QSSGRenderCamera::calculateProjectionInternal(*cameraNode, QRect(0, 0, width * camConfig.dpr, height * camConfig.dpr));
    }

    return QQuick3DCamera::mapFromViewport(viewportPos);
}

/*!
    \qmlmethod vector3d Camera::lookAt(vector3d scenePos)
    \since 5.15

    Sets the rotation value of the Camera so that it is pointing at \a scenePos.
*/

void QQuick3DCamera::lookAt(const QVector3D &scenePos)
{
    // Assumption: we never want the camera to roll.
    // We use Euler angles here to avoid roll to sneak in through numerical instability.

    const auto &targetPosition = scenePos;
    auto sourcePosition = scenePosition();

    QVector3D targetVector = sourcePosition - targetPosition;

    float yaw = qRadiansToDegrees(atan2(targetVector.x(), targetVector.z()));

    QVector2D p(targetVector.x(), targetVector.z()); // yaw vector projected to horizontal plane
    float pitch = qRadiansToDegrees(atan2(p.length(), targetVector.y())) - 90;

    const float previousRoll = eulerRotation().z();
    setEulerRotation(QVector3D(pitch, yaw, previousRoll));
}

/*!
    \qmlmethod vector3d Camera::lookAt(QtQuick3D::Node node)
    \since 5.15

    Sets the rotation value of the Camera so that it is pointing at \a node.
*/

void QQuick3DCamera::lookAt(QQuick3DNode *node)
{
    if (!node)
        return;
    lookAt(node->scenePosition());
}

void QQuick3DCamera::updateGlobalVariables(const QRectF &inViewport)
{
    QSSGRenderCamera *node = static_cast<QSSGRenderCamera *>(QQuick3DObjectPrivate::get(this)->spatialNode);
    if (node)
        QSSGRenderCamera::calculateProjectionInternal(*node, inViewport);
}

/*!
 * \internal
 */
QSSGRenderGraphObject *QQuick3DCamera::updateSpatialNode(QSSGRenderGraphObject *node)
{
    if (!node) {
        markAllDirty();
        node = new QSSGRenderCamera(QQuick3DObjectPrivate::get(this)->type);
    }

    QQuick3DNode::updateSpatialNode(node);

    QSSGRenderCamera *camera = static_cast<QSSGRenderCamera *>(node);
    if (qUpdateIfNeeded(camera->enableFrustumClipping, m_frustumCullingEnabled))
        camera->markDirty(QSSGRenderCamera::DirtyFlag::CameraDirty);
    if (qUpdateIfNeeded(camera->levelOfDetailPixelThreshold, m_levelOfDetailBias))
        camera->markDirty(QSSGRenderCamera::DirtyFlag::CameraDirty);

    return node;
}

void QQuick3DCamera::updateLookAt()
{
    if (m_lookAtNode)
        lookAt(m_lookAtNode);
}

/*!
    \qmlproperty real Camera::levelOfDetailBias
    \since 6.5

    This property changes the size a model needs to be when rendered before the
    automatic level of detail meshes are used. Each generated level of detail
    mesh contains an ideal size value that each level should be shown, which is
    a ratio of how much of the rendered scene will be that mesh. A model that
    represents only a few pixels on screen will not require the full geometry
    to look correct, so a lower level of detail mesh will be used instead in
    this case. This value is a bias to the ideal value such that a value smaller
    than \c 1.0 will require an even smaller rendered size before switching to
    a lesser level of detail. Values above \c 1.0 will lead to lower levels of detail
    being used sooner.  A value of \c 0.0 will disable the usage of levels of detail
    completely.

    The default value is \c 1.0

    \note This property will only have an effect on Models with geomtry containing
    levels of detail.

    \sa Model::levelOfDetailBias
*/

float QQuick3DCamera::levelOfDetailBias() const
{
    return m_levelOfDetailBias;
}

void QQuick3DCamera::setLevelOfDetailBias(float newLevelOfDetailBias)
{
    if (qFuzzyCompare(m_levelOfDetailBias, newLevelOfDetailBias))
        return;
    m_levelOfDetailBias = newLevelOfDetailBias;
    emit levelOfDetailBiasChanged();
    update();
}

QT_END_NAMESPACE

