/* -*-c++-*- Producer - Copyright (C) 2001-2004  Don Burns
 *
 * This library is open source and may be redistributed and/or modified under
 * the terms of the OpenSceneGraph Public License (OSGPL) version 0.0 or
 * (at your option) any later version.  The full license is in LICENSE file
 * included with this distribution, and on the openscenegraph.org website.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * OpenSceneGraph Public License for more details.
 */

#ifdef _WIN32
#include <windows.h>
#endif

#include <string.h>
#include <algorithm>

#include "Camera.h"

using namespace osgProducer;

Camera::Camera( void )
{
    _index = 0;

    _projrectLeft   = 0.0;
    _projrectRight  = 1.0;
    _projrectBottom = 0.0;
    _projrectTop    = 1.0;

    osg::Matrix::value_type  id[] = {
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1
    };

    memcpy( _viewMatrix, id, sizeof(osg::Matrix::value_type[16]));
    _offset._xshear = _offset._yshear = 0.0f;
    memcpy( _offset._matrix, id, sizeof(osg::Matrix::value_type[16]));
    _offset._multiplyMethod = Offset::PreMultiply;

    _lens = new Lens;
    _lens->setAutoAspect(true);
    _rs = new RenderSurface;

    _clear_color[0] = 0.2f;
    _clear_color[1] = 0.2f;
    _clear_color[2] = 0.4f;
    _clear_color[3] = 1.0f;

    _focal_distance = 1.0;

    _shareLens = true;
    _shareView = true;

    _enabled = true;
    _initialized = false;

}

Camera::~Camera( void )
{
}


const osg::Matrix::value_type * Camera::getViewMatrix( void ) const
{
    return _viewMatrix;
}

void Camera::setViewByMatrix( const osg::Matrix &mat )
{
    osg::Matrix m;
    if ( _offset._multiplyMethod == Offset::PostMultiply )
      m = osg::Matrix( _offset._matrix ) * mat;
    else if( _offset._multiplyMethod == Offset::PreMultiply )
        m = mat * osg::Matrix( _offset._matrix );
    memcpy( _viewMatrix, m.ptr(), sizeof( osg::Matrix::value_type[16] ));
}

void Camera::setViewByLookat( const osg::Vec3 &eye, const osg::Vec3 &center, const osg::Vec3 &up )
{
    osg::Matrix m;
    m.makeLookAt(eye,center,up);
    setViewByMatrix( m );
}

void Camera::setViewByLookat( float eyeX, float eyeY, float eyeZ,
                              float centerX, float centerY, float centerZ,
                              float upX, float upY, float upZ )
{
    setViewByLookat( osg::Vec3(eyeX, eyeY, eyeZ),
                     osg::Vec3(centerX, centerY, centerZ ),
                     osg::Vec3(upX, upY, upZ) );
}


Camera::Lens::Lens( void )
{
// original defaults.
//     _left         = -0.5;
//     _right        =  0.5;
//     _bottom       = -0.5;
//     _top          =  0.5;

    // Setting of the frustum which are appropriate for
    // a monitor which is 26cm high, 50cm distant from the
    // viewer and an horzintal/vetical aspect ratio of 1.25.
    // This assumed to be a reasonable average setting for end users.
    _left         = -0.32;
    _right        =  0.32;
    _bottom       = -0.26;
    _top          =  0.26;
    _ortho_left   = -1.0;
    _ortho_right  =  1.0;
    _ortho_bottom = -1.0;
    _ortho_top    =  1.0;
    _nearClip     =  1.0;
    _farClip      =  1e6;
    _auto_aspect  = false;
    _updateFOV();
    _projection = Perspective;
}

void Camera::Lens::setAspectRatio( double aspectRatio )
{
    _aspect_ratio = aspectRatio;
    _left  = -0.5 * (_top - _bottom) * _aspect_ratio;
    _right =  0.5 * (_top - _bottom) * _aspect_ratio;
    _ortho_left  = -0.5 * (_ortho_top - _ortho_bottom) * _aspect_ratio;
    _ortho_right =  0.5 * (_ortho_top - _ortho_bottom) * _aspect_ratio;

    if( _projection == Perspective )
    _updateFOV();
}

void Camera::Lens::setPerspective( double hfov,   double vfov,
                       double nearClip,   double farClip )
{
    _hfov = osg::DegreesToRadians(hfov);
    _vfov = osg::DegreesToRadians(vfov);
    _aspect_ratio = tan(0.5*_hfov)/tan(0.5*_vfov);

    _nearClip = nearClip;
    _farClip  = farClip;

    _left   = -_nearClip * tan(_hfov/2.0);
    _right  =  _nearClip * tan(_hfov/2.0);
    _bottom = -_nearClip * tan(_vfov/2.0);
    _top    =  _nearClip * tan(_vfov/2.0);

    _projection = Perspective;
    setAutoAspect(false);
}

void Camera::Lens::setFrustum( double left,   double right,
                                  double bottom, double top,
                   double nearClip,   double farClip )
{
    _left = left;
    _right = right;
    _bottom = bottom;
    _top = top;
    _nearClip = nearClip;
    _farClip = farClip;
    _projection = Perspective;
    _updateFOV();
    setAutoAspect(false);
}

void Camera::Lens::setOrtho( double left, double right,
               double bottom, double top,
               double nearClip, double farClip )
{
    _ortho_left = left;
    _ortho_right = right;
    _ortho_bottom = bottom;
    _ortho_top = top;
    _nearClip = nearClip;
    _farClip = farClip;
    _projection = Orthographic;
    setAutoAspect(false);
}

void Camera::Lens::setMatrix( const osg::Matrix::value_type matrix[16] )
{
    memcpy( _matrix, matrix, sizeof(osg::Matrix::value_type[16]) );
    _projection = Manual;
    setAutoAspect(false);
}

bool Camera::Lens::getFrustum( double& left, double& right,
                double& bottom, double& top,
                double& zNear, double& zFar ) const
{
    //The following code was taken from osg's matrix implementation of getFrustum
    if (_matrix[3]!=0.0 || _matrix[7]!=0.0 || _matrix[11]!=-1.0 || _matrix[15]!=0.0) return false;

    zNear = _matrix[14] / (_matrix[10]-1.0);
    zFar = _matrix[14] / (1.0+_matrix[10]);

    left = zNear * (_matrix[8]-1.0) / _matrix[0];
    right = zNear * (1.0+_matrix[8]) / _matrix[0];

    top = zNear * (1.0+_matrix[9]) / _matrix[5];
    bottom = zNear * (_matrix[9]-1.0) / _matrix[5];

    return true;
}

bool Camera::Lens::getOrtho( double& left, double& right,
                double& bottom, double& top,
                double& zNear, double& zFar ) const
{
    //The following code was taken from osg's matrix implementation of getOrtho
    if (_matrix[3]!=0.0 || _matrix[7]!=0.0 || _matrix[11]!=0.0 || _matrix[15]!=1.0) return false;

    zNear = (_matrix[14]+1.0) / _matrix[10];
    zFar = (_matrix[14]-1.0) / _matrix[10];

    left = -(1.0+_matrix[12]) / _matrix[0];
    right = (1.0-_matrix[12]) / _matrix[0];

    bottom = -(1.0+_matrix[13]) / _matrix[5];
    top = (1.0-_matrix[13]) / _matrix[5];

    return true;
}

bool Camera::Lens::convertToOrtho( float d )
{

    if( _projection == Manual )
    {
        //Need to extract frustum values from manual matrix
        if( !getFrustum(_left,_right,_bottom,_top,_nearClip,_farClip) )
            return false;

        _updateFOV();
    }

    double s = d * tan(_vfov*0.5);
    _ortho_bottom = -s;
    _ortho_top = s;
    _ortho_left = -s*_aspect_ratio;
    _ortho_right = s*_aspect_ratio;
    _projection = Orthographic;
    return true;
}

bool Camera::Lens::convertToPerspective( float d )
{

    if( _projection == Manual )
    {
        //Need to extract ortho values from manual matrix
        if( !getOrtho(_ortho_left,_ortho_right,_ortho_bottom,_ortho_top,_nearClip,_farClip) )
            return false;
    }

    double hfov = 2 * atan( 0.5 * (_ortho_right - _ortho_left)/d);
    double vfov = 2 * atan( 0.5 * (_ortho_top - _ortho_bottom)/d);

    _left   = -_nearClip * tan(hfov*0.5);
    _right  =  _nearClip * tan(hfov*0.5);
    _bottom = -_nearClip * tan(vfov*0.5);
    _top    =  _nearClip * tan(vfov*0.5);

    _projection = Perspective;
    //_updateMatrix();

    return true;
}

void Camera::Lens::apply(float xshear, float yshear)
{
    generateMatrix(xshear,yshear,_matrix);
}

void Camera::Lens::getParams( double &left, double &right, double &bottom, double &top,
                double &nearClip, double &farClip )
{
    if( _projection == Perspective )
    {
        left   = _left;
        right  = _right;
        bottom = _bottom;
        top    = _top;
    }
    else if( _projection == Orthographic )
    {
        left   = _ortho_left;
        right  = _ortho_right;
        bottom = _ortho_bottom;
        top    = _ortho_top;
    }
    else if( _projection == Manual ) // could only be Manual, but best to make this clear
    {
        // Check if Manual matrix is either a valid perspective or orthographic matrix
        // If neither, then return bogus values -- nothing better we can do
        if(getFrustum(left,right,bottom,top,nearClip,farClip))
            return;

        if(getOrtho(left,right,bottom,top,nearClip,farClip))
            return;

        left   = _left;
        right  = _right;
        bottom = _bottom;
        top    = _top;
    }
    nearClip   = _nearClip;
    farClip    = _farClip;
}

void Camera::setProjectionRectangle( const float left, const float right,
                    const float bottom, const float top )
{
    _projrectLeft   = left;
    _projrectRight  = right;
    _projrectBottom = bottom;
    _projrectTop    = top;
}

void Camera::getProjectionRectangle( float &left, float &right,
                float &bottom, float &top ) const
{
    left   = _projrectLeft;
    right  = _projrectRight;
    bottom = _projrectBottom;
    top    = _projrectTop;
}

void Camera::setProjectionRectangle( int x, int y, unsigned int width, unsigned int height )
{
    int _x, _y;
    unsigned int _w, _h;

    _rs->getWindowRectangle( _x, _y, _w, _h );
#if 0
    if( _w == osgProducer::RenderSurface::UnknownDimension || _h == Producer::RenderSurface::UnknownDimension)
    {
        unsigned int ww;
        unsigned int hh;
        _rs->getScreenSize( ww, hh );
        if( _w == osgProducer::RenderSurface::UnknownDimension )
            _w = ww;
        if( _h == osgProducer::RenderSurface::UnknownDimension )
            _h = hh;
    }
#endif

    _projrectLeft  = float(x - _x)/float(_w);
    _projrectRight = float((x + width) - _x)/float(_w);
    _projrectBottom = float(y - _y)/float(_h);
    _projrectTop    = float((y+height) - _y)/float(_h);
}

void Camera::getProjectionRectangle( int &x, int &y, unsigned int &width, unsigned int &height ) const
{
    int _x, _y;
    unsigned int _w, _h;
    float fx, fy, fw, fh;

    _rs->getWindowRectangle( _x, _y, _w, _h );
#if 0
    if( _w == Producer::RenderSurface::UnknownDimension || _h == Producer::RenderSurface::UnknownDimension )
    {
        unsigned int ww;
        unsigned int hh;
        _rs->getScreenSize( ww, hh );
        if( _w == Producer::RenderSurface::UnknownDimension )
            _w = ww;
        if( _h == Producer::RenderSurface::UnknownDimension )
            _h = hh;
    }
#endif

    fx = _projrectLeft * _w;
    fy = _projrectBottom * _h;
    fw = _w * _projrectRight;
    fh = _h * _projrectTop;

    x = int(fx);
    y = int(fy);

    width = int(fw) - x;
    height = int(fh) - y;
}

void Camera::setClearColor( float r, float g, float b, float a )
{
    _clear_color[0] = r;
    _clear_color[1] = g;
    _clear_color[2] = b;
    _clear_color[3] = a;
}

void Camera::getClearColor( float& red, float& green, float& blue, float& alpha)
{
    red = _clear_color[0];
    green = _clear_color[1];
    blue = _clear_color[2];
    alpha = _clear_color[3];
}


void Camera::clear( void )
{
#if 0
    if( !_initialized ) _initialize();
    int x, y;
    unsigned int w, h;
    getProjectionRectangle( x, y, w, h );
    glViewport( x, y, w, h );
    glScissor( x, y, w, h );
    glClearColor( _clear_color[0], _clear_color[1], _clear_color[2], _clear_color[3] );
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT );
#endif
}


#if 0
void Camera::Lens::_updateMatrix( void )
{
    switch( _projection )
    {
        case Perspective :
            _matrix[ 0] = (2 * _nearClip)/(_right - _left);
            _matrix[ 1] = 0.0;
            _matrix[ 2] = 0.0;
            _matrix[ 3] = 0.0;

            _matrix[ 4] = 0.0;
            _matrix[ 5] = (2 * _nearClip)/(_top-_bottom);
            _matrix[ 6] = 0.0;
            _matrix[ 7] = 0.0;

            _matrix[ 8] = (_right + _left)/(_right-_left);
            _matrix[ 9] = (_top+_bottom)/(_top-_bottom);
            _matrix[10] = -(_farClip + _nearClip)/(_farClip-_nearClip);
            _matrix[11] = -1.0;

            _matrix[12] = 0.0;
            _matrix[13] = 0.0;
            _matrix[14] = -(2 * _farClip * _nearClip)/(_farClip-_nearClip);
            _matrix[15] = 0.0;

            _matrix[ 8] += -_xshear;
            _matrix[ 9] += -_yshear;

        _hfov = 2.0 * atan(((_right - _left) * 0.5)/_nearClip);
        _vfov = 2.0 * atan(((_top - _bottom) * 0.5)/_nearClip);

        break;

    case Orthographic :

            _matrix[ 0] = 2/(_ortho_right - _ortho_left);
            _matrix[ 1] = 0.0;
            _matrix[ 2] = 0.0;
            _matrix[ 3] = 0.0;

            _matrix[ 4] = 0.0;
            _matrix[ 5] = 2/(_ortho_top - _ortho_bottom);
            _matrix[ 6] = 0.0;
            _matrix[ 7] = 0.0;

            _matrix[ 8] = 0.0;
            _matrix[ 9] = 0.0;
            //_matrix[10] = -2.0/(_farClip - (-_farClip));
            _matrix[10] = -2.0/(_farClip - _nearClip);
            _matrix[11] = 0.0;

            _matrix[12] = -(_ortho_right+_ortho_left)/(_ortho_right-_ortho_left);
            _matrix[13] = -(_ortho_top+_ortho_bottom)/(_ortho_top-_ortho_bottom);
            //_matrix[14] = -(_farClip+(-_farClip))/(_farClip-(-_farClip));
            _matrix[14] = -(_farClip+_nearClip)/(_farClip-_nearClip);
            _matrix[15] = 1.0;

            _matrix[12] += _xshear;
            _matrix[13] += _yshear;

            //_hfov = 0.0;
            //_vfov = 0.0;

        break;
    }
}
#endif

void Camera::Lens::generateMatrix(float xshear, float yshear, osg::Matrix::value_type matrix[16] )
{
    switch( _projection )
    {
        case Perspective :
            matrix[ 0] = (2 * _nearClip)/(_right - _left);
            matrix[ 1] = 0.0;
            matrix[ 2] = 0.0;
            matrix[ 3] = 0.0;

            matrix[ 4] = 0.0;
            matrix[ 5] = (2 * _nearClip)/(_top-_bottom);
            matrix[ 6] = 0.0;
            matrix[ 7] = 0.0;

            matrix[ 8] = (_right + _left)/(_right-_left);
            matrix[ 9] = (_top+_bottom)/(_top-_bottom);
            matrix[10] = -(_farClip + _nearClip)/(_farClip-_nearClip);
            matrix[11] = -1.0;

            matrix[12] = 0.0;
            matrix[13] = 0.0;
            matrix[14] = -(2 * _farClip * _nearClip)/(_farClip-_nearClip);
            matrix[15] = 0.0;

            matrix[ 8] += -xshear;
            matrix[ 9] += -yshear;


        break;

    case Orthographic :

            matrix[ 0] = 2/(_ortho_right - _ortho_left);
            matrix[ 1] = 0.0;
            matrix[ 2] = 0.0;
            matrix[ 3] = 0.0;

            matrix[ 4] = 0.0;
            matrix[ 5] = 2/(_ortho_top - _ortho_bottom);
            matrix[ 6] = 0.0;
            matrix[ 7] = 0.0;

            matrix[ 8] = 0.0;
            matrix[ 9] = 0.0;
            //_matrix[10] = -2.0/(_farClip - (-_farClip));
            matrix[10] = -2.0/(_farClip - _nearClip);
            matrix[11] = 0.0;

            matrix[12] = -(_ortho_right+_ortho_left)/(_ortho_right-_ortho_left);
            matrix[13] = -(_ortho_top+_ortho_bottom)/(_ortho_top-_ortho_bottom);
            //_matrix[14] = -(_farClip+(-_farClip))/(_farClip-(-_farClip));
            matrix[14] = -(_farClip+_nearClip)/(_farClip-_nearClip);
            matrix[15] = 1.0;

            matrix[12] += xshear;
            matrix[13] += yshear;

        break;

    case Manual:

        memcpy( matrix, _matrix, sizeof(osg::Matrix::value_type[16]));

        if(xshear || yshear)
        {
            if (matrix[3]!=0.0 || matrix[7]!=0.0 || matrix[11]!=0.0 || matrix[15]!=1.0)
            {
                // It's not an orthographic matrix so just assume a perspective shear
                matrix[ 8] += -xshear;
                matrix[ 9] += -yshear;
            }
            else
            {
                 matrix[12] += xshear;
                 matrix[13] += yshear;
            }
        }
        break;
    }
}

void Camera::Lens::_updateFOV()
{
    _hfov = 2.0 * atan(((_right - _left) * 0.5)/_nearClip);
    _vfov = 2.0 * atan(((_top - _bottom) * 0.5)/_nearClip);
    _aspect_ratio = tan(0.5*_hfov)/tan(0.5*_vfov);
}


void Camera::setOffset( const osg::Matrix::value_type matrix[16],  const osg::Matrix::value_type xshear,  const osg::Matrix::value_type yshear )
{
    memcpy( _offset._matrix, matrix, sizeof(osg::Matrix::value_type[16]));
    _offset._xshear = xshear;
    _offset._yshear = yshear;
}

void Camera::setOffset(  double xshear, double yshear )
{
    _offset._xshear = xshear;
    _offset._yshear = yshear;
}

#if 0
void Camera::setSyncBarrier( RefBarrier *b )
{
    _syncBarrier = b;
}

void Camera::setFrameBarrier( RefBarrier *b )
{
    _frameBarrier = b;
}

int Camera::cancel()
{
#if 1
    _done = true;
#endif

    Thread::cancel();
    return 0;
}

void Camera::advance()
{
    _rs->makeCurrent();
    _rs->swapBuffers();
}

void Camera::run( void )
{
    if( !_syncBarrier.valid() || !_frameBarrier.valid() )
    {
        std::cerr << "Camera::run() : Threaded Camera requires a Barrier\n";
        return;
    }

    _done = false;

    _initialize();
    _syncBarrier->block();
    while( !_done )
    {
        // printf("   Camera::run before frame block\n");

        _frameBarrier->block();

        if (_done) break;

        // printf("   Camera::run after frame block\n");

        frame(false);

        if (_done) break;

        // printf("   Camera::run before sycn block\n");

        _syncBarrier->block();

        if (_done) break;

        // printf("   Camera::run after sycn block\n");

        advance();
    }

    // printf("Exiting Camera::run cleanly\n");
}



bool Camera::removePreCullCallback( Callback *cb )
{
    return _removeCallback( preCullCallbacks, cb );
}

bool Camera::removePostCullCallback( Callback *cb )
{
    return _removeCallback( postCullCallbacks, cb );
}

bool Camera::removePreDrawCallback( Callback *cb )
{
    return _removeCallback( preDrawCallbacks, cb );
}

bool Camera::removePostDrawCallback( Callback *cb )
{
    return _removeCallback( postDrawCallbacks, cb );
}

bool Camera::removePostSwapCallback( Callback *cb )
{
    return _removeCallback( postSwapCallbacks, cb );
}


bool Camera::_removeCallback( std::vector < ref_ptr<Callback> > &callbackList, Callback *callback )
{
    std::vector < Producer::ref_ptr< Producer::Camera::Callback> >::iterator p;
    p = std::find( callbackList.begin(), callbackList.end(), callback );
    if( p == callbackList.end() )
        return false;

    callbackList.erase( p );
    return true;
}

Camera::FrameTimeStampSet::FrameTimeStampSet():
    _pipeStatsDoubleBufferIndex(0),
    _pipeStatsFirstSync(true),
    _initialized(false)
{
    for( unsigned int i = 0; i < LastPipeStatsID; i++ )
        _pipeStats[i] = 0.0;
}

Camera::FrameTimeStampSet::~FrameTimeStampSet()
{
}

void Camera::FrameTimeStampSet::syncPipeStats()
{
    if( !_initialized )
        return;

    if( _pipeStatsFirstSync == true )
    {
        _pipeStatsFirstSync = false;
        return;
    }

    // We get the stats from the previous frame
    for( int i = 0; i < LastPipeStatsID; i++ )
    {
        if( _pipeStatsSetMask[1 - _pipeStatsDoubleBufferIndex]  & (1<<i) )
            _pipeStats[i] = PipeTimer::instance()->getElapsedTime( _pipeStatsNames[i][ 1 - _pipeStatsDoubleBufferIndex] );
    }

    _pipeStatsFrameNumber = _frameNumber - 1;
    _pipeStatsDoubleBufferIndex = 1 - _pipeStatsDoubleBufferIndex;
    _pipeStatsSetMask[_pipeStatsDoubleBufferIndex] = 0;
}

void Camera::FrameTimeStampSet::beginPipeTimer( PipeStatsID id)
{
    if( !_initialized )
        _init();

    PipeTimer::instance()->begin( _pipeStatsNames[id][_pipeStatsDoubleBufferIndex] );
    _pipeStatsSetMask[_pipeStatsDoubleBufferIndex] |= (1<<id);
}

void Camera::FrameTimeStampSet::endPipeTimer()
{
    if( !_initialized )
        return;

    PipeTimer::instance()->end() ;
}

void Camera::FrameTimeStampSet::_init()
{
    if( _initialized )
        return;

    for( unsigned int i = 0; i < (unsigned int)LastPipeStatsID; i++ )
        PipeTimer::instance()->genQueries( 2, _pipeStatsNames[i] );

    _pipeStatsSetMask[0] = 0;
    _pipeStatsSetMask[1] = 0;

    _initialized = true;
}

const Camera::FrameTimeStampSet &Camera::getFrameStats()
{
    return _frameStamps;
}
#endif
