// Viewer.hxx -- alternative flightgear viewer application
//
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2009 Mathias Froehlich

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "SlaveCamera.hxx"

#include <simgear/scene/util/OsgMath.hxx>

#include "Viewer.hxx"

namespace fgviewer  {

class NoUpdateCallback : public osg::NodeCallback {
public:
    virtual ~NoUpdateCallback()
    { }
    virtual void operator()(osg::Node* node, osg::NodeVisitor* nodeVisitor)
    { }
};

SlaveCamera::SlaveCamera(const std::string& name) :
    _name(name),
    _viewport(new osg::Viewport())
{
    _referencePointMap["lowerLeft"] = osg::Vec2(-1, -1);
    _referencePointMap["lowerRight"] = osg::Vec2(1, -1);
    _referencePointMap["upperRight"] = osg::Vec2(1, 1);
    _referencePointMap["upperLeft"] = osg::Vec2(-1, 1);
}

SlaveCamera::~SlaveCamera()
{
}

bool
SlaveCamera::setDrawableName(const std::string& drawableName)
{
    if (_camera.valid())
        return false;
    _drawableName = drawableName;
    return true;
}

bool
SlaveCamera::setViewport(const SGVec4i& viewport)
{
    _viewport->setViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
    _frustum.setAspectRatio(getAspectRatio());
    return true;
}

bool
SlaveCamera::setViewOffset(const osg::Matrix& viewOffset)
{
    _viewOffset = viewOffset;
    return true;
}

bool
SlaveCamera::setViewOffsetDeg(double headingDeg, double pitchDeg, double rollDeg)
{
    osg::Matrix viewOffset = osg::Matrix::identity();
    viewOffset.postMultRotate(osg::Quat(SGMiscd::deg2rad(headingDeg), osg::Vec3(0, 1, 0)));
    viewOffset.postMultRotate(osg::Quat(SGMiscd::deg2rad(pitchDeg), osg::Vec3(-1, 0, 0)));
    viewOffset.postMultRotate(osg::Quat(SGMiscd::deg2rad(rollDeg), osg::Vec3(0, 0, 1)));
    return setViewOffset(viewOffset);
}

bool
SlaveCamera::setFrustum(const Frustum& frustum)
{
    _frustum = frustum;
    return true;
}

void
SlaveCamera::setFustumByFieldOfViewDeg(double fieldOfViewDeg)
{
    Frustum frustum(getAspectRatio());
    frustum.setFieldOfViewDeg(fieldOfViewDeg);
    setFrustum(frustum);
}

bool
SlaveCamera::setRelativeFrustum(const std::string names[2], const SlaveCamera& referenceCameraData,
                                const std::string referenceNames[2])
{
    // Track the way from one projection space to the other:
    // We want
    //  P = T2*S*T*P0
    // where P0 is the projection template sensible for the given window size,
    // S a scale matrix and T is a translation matrix.
    // We need to determine T and S so that the reference points in the parents
    // projection space match the two reference points in this cameras projection space.

    // Starting from the parents camera projection space, we get into this cameras
    // projection space by the transform matrix:
    //  P*R*inv(pP*pR) = T2*S*T*P0*R*inv(pP*pR)
    // So, at first compute that matrix without T2*S*T and determine S and T* from that

    // The initial projection matrix to build upon
    osg::Matrix P = Frustum(getAspectRatio()).getMatrix();

    osg::Matrix R = getViewOffset();
    osg::Matrix pP = referenceCameraData.getFrustum().getMatrix();
    osg::Matrix pR = referenceCameraData.getViewOffset();

    // Transform from the reference cameras projection space into this cameras eye space.
    osg::Matrix pPtoEye = osg::Matrix::inverse(pR*pP)*R;

    osg::Vec2 pRef[2] = {
        referenceCameraData.getProjectionReferencePoint(referenceNames[0]),
        referenceCameraData.getProjectionReferencePoint(referenceNames[1])
    };

    // The first reference point transformed to this cameras projection space
    osg::Vec3d pRefInThis0 = P.preMult(pPtoEye.preMult(osg::Vec3d(pRef[0], 1)));
    // Translate this projection matrix so that the first reference point is at the origin
    P.postMultTranslate(-pRefInThis0);

    // Transform the second reference point and get the scaling correct.
    osg::Vec3d pRefInThis1 = P.preMult(pPtoEye.preMult(osg::Vec3d(pRef[1], 1)));
    double s = osg::Vec2d(pRefInThis1[0], pRefInThis1[1]).length();
    if (s <= std::numeric_limits<double>::min())
        return false;
    osg::Vec2 ref[2] = {
        getProjectionReferencePoint(names[0]),
        getProjectionReferencePoint(names[1])
    };
    s = (ref[0] - ref[1]).length()/s;
    P.postMultScale(osg::Vec3d(s, s, 1));

    // The first reference point still maps to the origin in this projection space.
    // Translate the origin to the desired first reference point.
    P.postMultTranslate(osg::Vec3d(ref[0], 1));

    // Now osg::Matrix::inverse(pR*pP)*R*P should map pRef[i] exactly onto ref[i] for i = 0, 1.
    // Note that osg::Matrix::inverse(pR*pP)*R*P should exactly map pRef[0] at the near plane
    // to ref[0] at the near plane. The far plane is not taken care of.

    Frustum frustum;
    if (!frustum.setMatrix(P))
        return false;

    return setFrustum(frustum);
}

void
SlaveCamera::setProjectionReferencePoint(const std::string& name, const osg::Vec2& point)
{
    _referencePointMap[name] = point;
}

osg::Vec2
SlaveCamera::getProjectionReferencePoint(const std::string& name) const
{
    NameReferencePointMap::const_iterator i = _referencePointMap.find(name);
    if (i != _referencePointMap.end())
        return i->second;
    return osg::Vec2(0, 0);
}

void
SlaveCamera::setMonitorProjectionReferences(double width, double height,
                                            double bezelTop, double bezelBottom,
                                            double bezelLeft, double bezelRight)
{
    double left = 1 + 2*bezelLeft/width;
    double right = 1 + 2*bezelRight/width;

    double bottom = 1 + 2*bezelBottom/height;
    double top = 1 + 2*bezelTop/height;

    setProjectionReferencePoint("lowerLeft", osg::Vec2(-left, -bottom));
    setProjectionReferencePoint("lowerRight", osg::Vec2(right, -bottom));
    setProjectionReferencePoint("upperRight", osg::Vec2(right, top));
    setProjectionReferencePoint("upperLeft", osg::Vec2(-left, top));
}

osg::Vec3
SlaveCamera::getLeftEyeOffset(const Viewer& viewer) const
{
    return osg::Vec3(0, 0, 0);
}

osg::Vec3
SlaveCamera::getRightEyeOffset(const Viewer& viewer) const
{
    return osg::Vec3(0, 0, 0);
}

double
SlaveCamera::getZoomFactor(const Viewer& viewer) const
{
    return 1;
}

osg::Matrix
SlaveCamera::getEffectiveViewOffset(const Viewer& viewer) const
{
    // The eye offset in the master cameras coordinates.
    osg::Vec3 eyeOffset = getLeftEyeOffset(viewer);

    // Transform the eye offset into this slaves coordinates
    eyeOffset = eyeOffset*getViewOffset();

    // The slaves view matrix is composed of the master matrix
    osg::Matrix viewOffset = viewer.getCamera()->getViewMatrix();
    // ... its view offset ...
    viewOffset.postMult(getViewOffset());
    // ... and the inverse of the eye offset that is required
    // to keep the world at the same position wrt the projection walls
    viewOffset.postMultTranslate(-eyeOffset);
    return viewOffset;
}

Frustum
SlaveCamera::getEffectiveFrustum(const Viewer& viewer) const
{
    // The eye offset in the master cameras coordinates.
    osg::Vec3 eyeOffset = getLeftEyeOffset(viewer);

    // Transform the eye offset into this slaves coordinates
    eyeOffset = eyeOffset*getViewOffset();

    /// FIXME read that from external
    osg::Vec3 zoomScaleCenter(0, 0, -1);
    double zoomFactor = getZoomFactor(viewer);

    /// Transform into the local cameras orientation.
    zoomScaleCenter = getViewOffset().preMult(zoomScaleCenter);

    // The unmodified frustum
    Frustum frustum = getFrustum();

    // For unresized views this is a noop
    frustum.setAspectRatio(getAspectRatio());

    // need to correct this for the eye position within the projection system
    frustum.translate(-eyeOffset);

    // Scale the whole geometric extent of the projection surfaces by the zoom factor
    frustum.scale(1/zoomFactor, zoomScaleCenter);

    return frustum;
}

bool
SlaveCamera::realize(Viewer& viewer)
{
    if (_camera.valid())
        return false;
    _camera = _realizeImplementation(viewer);
    return _camera.valid();
}

bool
SlaveCamera::update(Viewer& viewer)
{
    return _updateImplementation(viewer);
}

osg::Camera*
SlaveCamera::_realizeImplementation(Viewer& viewer)
{
    Drawable* drawable = viewer.getDrawable(_drawableName);
    if (!drawable)
        return 0;
    osg::GraphicsContext* graphicsContext = drawable->getGraphicsContext();
    if (!graphicsContext)
        return 0;

    osg::Camera* camera = new osg::Camera;
    camera->setName(getName());
    camera->setGraphicsContext(graphicsContext);
    camera->setViewport(_viewport.get());
    camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF);

    // Not seriously consider something different
    camera->setDrawBuffer(GL_BACK);
    camera->setReadBuffer(GL_BACK);

    camera->setUpdateCallback(new NoUpdateCallback);

    return camera;
}

bool
SlaveCamera::_updateImplementation(Viewer& viewer)
{
    if (!_camera.valid())
        return false;
    return true;
}

} // namespace fgviewer
