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

#ifndef QSSG_RENDER_CAMERA_H
#define QSSG_RENDER_CAMERA_H

//
//  W A R N I N G
//  -------------
//
// This file is not part of the Qt API.  It exists purely as an
// implementation detail.  This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//

#include <QtQuick3DRuntimeRender/private/qtquick3druntimerenderglobal_p.h>

#include <QtQuick3DRuntimeRender/private/qssgrendernode_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrenderray_p.h>

#include <QtQuick3DUtils/private/qssgrenderbasetypes_p.h>

QT_BEGIN_NAMESPACE

struct QSSGCameraGlobalCalculationResult
{
    bool m_wasDirty;
    bool m_computeFrustumSucceeded /* = true */;
};

struct QSSGRenderCameraConfiguration
{
    float dpr = 1.0f;
    float ssaaMultiplier = 1.0f;
};

class QSSGRenderCameraFieldOfView
{
public:
    enum class Orientation
    {
        Vertical,
        Horizontal,
    };

    explicit constexpr QSSGRenderCameraFieldOfView(Orientation orientation = Orientation::Vertical) : fovOrientation(orientation) {}
    QSSGRenderCameraFieldOfView(const QSSGRenderCameraFieldOfView &) = default;
    QSSGRenderCameraFieldOfView(QSSGRenderCameraFieldOfView &&) = default;

    QSSGRenderCameraFieldOfView &operator=(const QSSGRenderCameraFieldOfView &other) = default;

    template <Orientation Type = Orientation::Vertical>
    static QSSGRenderCameraFieldOfView fromDegrees(float fov)
    {
        return QSSGRenderCameraFieldOfView(fov, FovValue::Degrees, Type);
    }
    template <Orientation Type = Orientation::Vertical>
    static QSSGRenderCameraFieldOfView fromRadians(float fov)
    {
        return QSSGRenderCameraFieldOfView(fov, FovValue::Radians, Type);
    }

    constexpr Orientation orientation() const { return fovOrientation; }

    float degrees() const { return fovDegrees; }
    float radians() const { return fovRadians; }

    void setDegrees(float fov)
    {
        fovDegrees = fov;
        fovRadians = qDegreesToRadians(fov);
    }

    QSSGRenderCameraFieldOfView asVerticalFov(float aspectRatio) const
    {
        if (fovOrientation == Orientation::Vertical)
            return *this;

        const float fovV = float(2.0 * qAtan(qTan(qreal(fovRadians) / 2.0) / qreal(aspectRatio)));
        return fromRadians<Orientation::Vertical>(fovV);
    }

    void setRadians(float fov)
    {
        fovRadians = fov;
        fovDegrees = qRadiansToDegrees(fov);
    }
private:
    enum class FovValue
    {
        Degrees,
        Radians,
    };
    constexpr explicit QSSGRenderCameraFieldOfView(float fov, FovValue value, Orientation angleOfView)
        : fovDegrees(value == FovValue::Degrees ? fov : qRadiansToDegrees(fov))
        , fovRadians(value == FovValue::Radians ? fov : qDegreesToRadians(fov))
        , fovOrientation(angleOfView)
    {
    }

    float fovDegrees = 60.0f;
    float fovRadians = qDegreesToRadians(fovDegrees);
    Orientation fovOrientation;
};

class Q_QUICK3DRUNTIMERENDER_EXPORT QSSGRenderCamera : public QSSGRenderNode
{
public:
    using Configuration = QSSGRenderCameraConfiguration;
    using FieldOfView = QSSGRenderCameraFieldOfView;

    explicit QSSGRenderCamera(QSSGRenderGraphObject::Type type);

    enum class DirtyFlag : quint8
    {
        CameraDirty = 0x1
    };
    using FlagT = std::underlying_type_t<DirtyFlag>;

    static constexpr DirtyFlag DirtyMask { std::numeric_limits<FlagT>::max() };

    class ClipPlanes : public QVector2D
    {
    public:
        constexpr inline ClipPlanes() : QVector2D(0.0f, 10000.0f) {}
        constexpr inline ClipPlanes(float n, float f) : QVector2D(n, f) {}
        constexpr float clipNear() const noexcept { return x(); }
        constexpr float clipFar() const noexcept { return y(); }

        constexpr inline void setClipNear(float n) noexcept { setX(n); }
        constexpr inline void setClipFar(float f) noexcept { setY(f); }
    };

    class Magnification : public QVector2D
    {
    public:
        constexpr inline Magnification() : QVector2D(1.0f, 1.0f) {}
        constexpr inline Magnification(float h, float v) : QVector2D(h, v) {}
        constexpr float horizontal() const noexcept { return x(); }
        constexpr float vertical() const noexcept { return y(); }

        constexpr inline void setHorizontal(float h) noexcept { setX(h); }
        constexpr inline void setVertical(float v) noexcept { setY(v); }
    };

    // Used by the custom frustum camera
    struct Frustum
    {
        float top = 0.0f;
        float bottom = 0.0f;
        float left = 0.0f;
        float right = 0.0f;
    };

    // Set our position, rotation member variables based on the lookat target
    // Marks this object as dirty.
    // Need to test this when the camera's local transform is null.
    // Assumes parent's local transform is the identity, meaning our local transform is
    // our global transform.
    void lookAt(const QVector3D &inCameraPos, const QVector3D &inUpDir, const QVector3D &inTargetPos, const QVector3D &pivot);

    /*!
     * Convenience function intended for use with internal cameras that's not part of the scene graph!
     * This function will NOT ensure that the camera's global transform, projection matrix, or state
     * is updated!.
     */
    static bool calculateProjectionInternal(QSSGRenderCamera &camera,
                                            const QRectF &inViewport,
                                            Configuration config = {});

    /*!
     * \brief calculateProjection
     * \param inViewport
     * \param config
     * \return true if the projection was successfully calculated, false otherwise.
     *
     * This function will calculate the projection matrix for the camera.
     *
     * NOTE: This function will not update the camera's global transform. It is the caller's
     * responsibility to ensure that the camera's global transform has been calculated prior
     * to calling this function.
     */
    bool calculateProjection(const QRectF &inViewport, Configuration config = {});

    void setCustomProjection(const QMatrix4x4 &proj);

    static bool computeFrustumOrtho(QRectF inViewport,
                                    QSSGRenderCamera::ClipPlanes clipPlanes,
                                    QSSGRenderCamera::Magnification magnification,
                                    Configuration config,
                                    QMatrix4x4 &outProjection);
    static bool computeFrustumPerspective(QRectF inViewport,
                                          FieldOfView fov,
                                          QSSGRenderCamera::ClipPlanes clipPlanes,
                                          QMatrix4x4 &outProjection);
    static bool computeCustomFrustum(QSSGRenderCamera::Frustum frustum,
                                     QSSGRenderCamera::ClipPlanes clipPlanes,
                                     QMatrix4x4 &outProjection);

    static void calculateViewProjectionMatrix(const QMatrix4x4 &globalTransform,
                                              const QMatrix4x4 &projection,
                                              QMatrix4x4 &outMatrix);

    void calculateViewProjectionMatrix(const QMatrix4x4 &globalTransform, QMatrix4x4 &outMatrix) const;
    void calculateViewProjectionWithoutTranslation(const QMatrix4x4 &globalTransform, float near, float far, QMatrix4x4 &outMatrix) const;

    // Unproject a point (x,y) in viewport relative coordinates meaning
    // left, bottom is 0,0 and values are increasing right,up respectively.
    QSSGRenderRay unproject(const QMatrix4x4 &globalTransform, const QVector2D &inLayerRelativeMouseCoords, const QRectF &inViewport) const;

    [[nodiscard]] inline bool isDirty(DirtyFlag dirtyFlag = DirtyMask) const
    {
        return ((cameraDirtyFlags & FlagT(dirtyFlag)) != 0)
               || ((dirtyFlag == DirtyMask) && QSSGRenderNode::isDirty());
    }
    void markDirty(DirtyFlag dirtyFlag);
    void clearDirty(DirtyFlag dirtyFlag);

    float getLevelOfDetailMultiplier() const;

    float getDpr() const { return dpr; }

    // Setting these variables should be followed by marking the camera dirty!
    ClipPlanes clipPlanes;
    FieldOfView fov;
    Frustum customFrustum;
    Magnification magnification;
    QMatrix4x4 projection;
    float levelOfDetailPixelThreshold = 1.0;
    bool enableFrustumClipping = true;

private:
    // Record some values from creating the projection matrix
    // to use during mouse picking.
    QVector2D frustumScale;

    QRectF previousInViewport;
    float dpr = 1.0f;
    FlagT cameraDirtyFlags = 0;
};

[[nodiscard]] constexpr bool qFuzzyCompare(const QSSGRenderCamera::FieldOfView &fov1, const QSSGRenderCamera::FieldOfView &fov2) noexcept
{
    return fov1.orientation() == fov2.orientation() && qFuzzyCompare(fov1.degrees(), fov2.degrees());
}

[[nodiscard]] constexpr bool qFuzzyCompare(QSSGRenderCamera::Frustum f1, QSSGRenderCamera::Frustum f2) noexcept
{
    return qFuzzyCompare(f1.top, f2.top) && qFuzzyCompare(f1.bottom, f2.bottom) && qFuzzyCompare(f1.left, f2.left) && qFuzzyCompare(f1.right, f2.right);
}

QT_END_NAMESPACE

#endif
