/*****************************************************************************
 * $LastChangedDate: 2011-04-23 21:07:07 -0400 (Sat, 23 Apr 2011) $
 * @file
 * @author  Jim E. Brooks  http://www.palomino3d.org
 * @brief   Window class.
 * @remarks - Window resize problems:
 *            OSG supportsResize is set to false since multiple problems
 *            occur if window is resized (some problems are external).
 *            The worst external problem is severe artifacts
 *            if motion-blur (accumulation buffer) is enabled (Nvidia 7600).
 *          - Manipulator mode:
 *            The simulator runs in a reduced mode if manipulator is enabled
 *            as it is intended for debugging or creating a scene.
 *          - Pre/post draw events:
 *            Some clients register listeners to directly execute OpenGL functions.
 *            Broadcasting these events are initiated by osg::Camera callbacks
 *            or osg::Operation which is when OpenGL functions can execute correctly.
 *//*
 * LEGAL:   COPYRIGHT (C) 2007 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE VERSION 2 (GPL 2).
 *****************************************************************************/

#define VIEW_WINDOW_CC 1
#include "base/module.hh"
#include "base/timer.hh"
#include "base/stream.hh"
using namespace base;
#include "glue/module.hh"
#include "glue/conf_bin_ext.hh"
using namespace glue;
#include "gfx/module.hh"
#include "gfx/gfx.hh"
using namespace gfx;
#include "input/module.hh"
#include "input/device_keyboard_osg.hh"
using namespace input;
#include "graph/module.hh"
#include "graph/scene_graph.hh"
using namespace graph;
#include "view/module.hh"
#include "view/window.hh"
#include "view/conf.hh"
#include "view/node_masks.hh"
#include "view/events.hh"
#include "view/viewpoint_chase.hh"
#include "view/manipulator_keyboard.hh"
#include "view/viewpoint_manipulator.hh"

#include <osg/PolygonMode>
#include <osg/StateAttribute>
#include <osgViewer/Viewer>
#include <osgViewer/GraphicsWindow>
#include <osgViewer/ViewerEventHandlers>  // StatsHandler

namespace view {

#undef KEY_F12  // header conflict
const int DEFAULT_STATS_HANDLER_KEY = osgGA::GUIEventAdapter::KEY_F12;

////////////////////////////////////////////////////////////////////////////////
//////////////////////////   CameraDrawCallback  ///////////////////////////////
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
/// @brief For broadcasting pre-draw and post-draw events for a View.
///
/// OSG calls this before and after rendering an osg::Camera.
/// This is reflected to any listeners in the app registered by base::Event.
///
class CameraDrawCallback : public osg::Camera::DrawCallback
{
public:
    enum { PRE_DRAW = true, POST_DRAW = false };

    CameraDrawCallback( shptr<View> view, const bool preDraw )
    :   mView(view), mPreDraw(preDraw)
    {
    }

    virtual void operator()( const osg::Camera& ) const /*suppress checks.sh*/
    {
        CHECK_TYPESIG(mView,TYPESIG_VIEW);

        // Reflect from OSG as broadcasted event.
        if ( mPreDraw )
        {
            // Pre-draw-ALL views.
            if ( mView == GET_MAIN_VIEW() )
            {
                EVENT_VIEW_PRE_DRAW_ALL_VIEWS.Broadcast( Void() );
            }

            // Pre-draw-ONE view.
            EVENT_VIEW_PRE_DRAW_ONE_VIEW.Broadcast( mView );
        }
        else
        {
            // Post-draw-ALL views.
            // (done at Window::DoOperation())

            // Post-draw-ONE view.
            EVENT_VIEW_POST_DRAW_ONE_VIEW.Broadcast( mView );
        }
    }

private:
    shptr<View>     mView;
    const bool      mPreDraw;   ///< pre-draw or post-draw
};

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////  Window  //////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

Window* Window::msInstance;  // there can be only one

/*****************************************************************************
 * ctor/dtor.
 *****************************************************************************/
Window::Window( const Vector2& pos, const WidthHeight<int>& wh, const string& title )
:   mViewer(new osgViewer::Viewer),
    mViews(),
    mTitle(title),
    mFPS(),
    mTraits(NULL),
    mGraphicsContext(NULL),
    mGraphicsWindow(NULL),
    mOperation(new Operation),
    mStatsHandler(NULL),  // assume stats are disabled
    mManipulator(new KeyboardManipulator)
{
    // Catch user passing bad args with -wh.
    if ( (wh.w <= 0) or (wh.h <= 0) or (wh.w > 10000) or (wh.h > 10000) )
    {
        CERROR << "ERROR: invalid window width/height" << std::endl;
        EXIT( 1 );
    }

    // Create window.
    mTraits = new osg::GraphicsContext::Traits;
    mTraits->x = pos[XX];  // position can be set if window manager allows
    mTraits->y = pos[YY];
    mTraits->width  = wh.w;
    mTraits->height = wh.h;
    mTraits->windowDecoration = true;
    mTraits->doubleBuffer = true;
    mTraits->sharedContext = NULL;
    mTraits->windowName = title;
    mTraits->supportsResize = false;
    mGraphicsContext = osg::GraphicsContext::createGraphicsContext( mTraits.get() );
    mGraphicsWindow = dynamic_cast<osgViewer::GraphicsWindow*>( mGraphicsContext.get() );
    if ( UX( mGraphicsWindow == NULL ) )
    {
        throw Exception( "ERROR: unable to create OSG GraphicsWindow." );
    }

    // [2007/12] Use the single-thread model.
    // Multiple threads would cause instability
    // because this program modifies the scene graph
    // while OSG is still visiting it.
    mViewer->setThreadingModel( osgViewer::Viewer::SingleThreaded );

    // Widen field-of-view.
    // OSG default values were:
    // projMatrix fov=29.1484 aspectRatio=1.25 zNear=1 zFar=10000
    double fov = 0.0, aspectRatio = 0.0, zNear = 0.0, zFar = 0.0;
    mViewer->getCamera()->getProjectionMatrixAsPerspective( fov, aspectRatio, zNear, zFar );
    //COUT << "projMatrix fov=" << fov << " aspectRatio=" << aspectRatio << " zNear=" << zNear << " zFar=" << zFar << std::endl;
    aspectRatio = VIEW_CONF.mViewProjectionAspectRatio;
    fov         = VIEW_CONF.mViewProjectionFOV;
    mViewer->getCamera()->setProjectionMatrixAsPerspective( fov, aspectRatio, zNear, zFar );

    // Create the main view.
    shptr<Viewpoint> mainViewpoint = new ChaseViewpoint;
    if ( VIEW_CONF.mManipulatorEnabled )
         mainViewpoint = new ManipulatorViewpoint( mManipulator );
    AddView( new View( ENABLE,
                       Rect<int>( 0, 0, wh.w, wh.h ),
                       mainViewpoint,
                       mViewer->getCamera() ),
             MAIN_VIEW );

    // Set the absolute root node of everything.
    RefPtr<Node> rootNode = GET_SCENE_GRAPH().GetRootNode().get();
    mViewer->setSceneData( rootNode.get() );

    // To let app handle ESC key.
    mViewer->setKeyEventSetsDone( 0 );

    // Enable statistics display when a key is pressed.
    if ( VIEW_CONF.mStatsEnabled )
    {
        mStatsHandler = new osgViewer::StatsHandler;  // ctor does NOT instantiate
        OsgRegisterEventHandler( mStatsHandler.get() );
        OsgSetStatsKey( DEFAULT_STATS_HANDLER_KEY );
    }

    // To do low-level graphics operations.
    mGraphicsWindow->add( mOperation.get() );
}

Window::~Window()
{
    // NOP
}

/*****************************************************************************
 * Singleton method.
 * Parameters for creating a Window (size,title) come from the view module's conf file.
 *****************************************************************************/
Window*
Window::CreateInstance( void )
{
ASSERT_RUN_ONCE;
    return new Window( VIEW_CONF.mWindowPosition,
                       VIEW_CONF.mWindowWidthHeight,
                       VIEW_CONF.mWindowTitle );
}

/*****************************************************************************
 * Make the Window.
 *****************************************************************************/
void
Window::Make( void )
{
    // NOP because GetInstance() creates the Window.
}

/*****************************************************************************
 * Reset window.
 *****************************************************************************/
void
Window::Reset( void )
{
    // Disable OSG stats display.
    // Statistics might be entirely disabled.
    if ( VIEW_CONF.mStatsEnabled and (mStatsHandler != NULL) )
        mStatsHandler->reset();
}

/*****************************************************************************
 * Enter the main graphics loop.
 *****************************************************************************/
void
Window::EnterMainLoop( void )
{
    // Bypass processing other views if manipulator is enabled.
    if ( VIEW_CONF.mManipulatorEnabled )
    {
        // SUBTLE ordering of OSG code:
        // RestoreViewpoint() alters the manipulator AFTER
        // it is attached to Viewer (else restoring will have no effect).
        mViewer->setCameraManipulator( mManipulator.get() );
      //RestoreViewpoint();  // don't auto-restore viewpoint
    }

    while ( true )
    {
        // Pulse the Timer singleton.
        GET_TIMER().Tick();

        // Broadcast event.
        // (Broadcast is done at camera callback when OpenGL functions can be executed).
        //EVENT_VIEW_PRE_DRAW_ALL_VIEWS.Broadcast( Void() );

        // For each View, re-compute its view matrix.
        for ( Views::iterator iter = mViews.begin(); iter != mViews.end(); ++iter )
        {
            shptr<View> view = *iter;
            CHECK_TYPESIG(view,TYPESIG_VIEW);
            if ( view->IfEnabled() )
            {
                // Recompute the view matrix (involves the Viewpoint functor).
                view->ComputeViewMatrix();
            }
        }

        // Render a frame.  This renders all views.
        mViewer->frame();

        // Compute FPS.
        mFPS.Tick( GET_TIMER().GetElapsedTime() );

        // Broadcast event.
        //EVENT_VIEW_POST_DRAW_ALL_VIEWS.Broadcast( Void() );  // done at Window::DoOperation()

        // Exit program?
        if ( mViewer->done() )
            break;
    }

    EXIT( 0 );
}

/*****************************************************************************
 * Attach osg::Camera to this Window.
 *****************************************************************************/
void
Window::OsgAddCamera( RefPtr<osg::Camera> camera, const eCamera cameraType, const bool mainView )
{
    // Only the main view needs to generate events.
    // And prevents OSG confusings events in complex arrangements of views.
    camera->setAllowEventFocus( mainView );

    // Bypass processing other views if manipulator is enabled.
    if ( not VIEW_CONF.mManipulatorEnabled )
    {
        // Add osg::Camera to osg::Viewer.
        // But don't add master as a slave of itself (prevent cycle).
        if ( not mViewer->containsCamera( camera.get() ) )
        {
            // HUD/2D camera shouldn't use 3D scene data.
            const bool useMastersSceneData = (cameraType == eCamera_3D);
            mViewer->addSlave( camera.get(), useMastersSceneData );
        }
    }

    // Set where camera renders to.
    camera->setGraphicsContext( mGraphicsContext.get() );
}

/*****************************************************************************
 * Add View to this Window.
 *****************************************************************************/
void
Window::AddView( shptr<View> view, const bool mainView )
{
CHECK_TYPESIG(view,TYPESIG_VIEW);

    // Add View object to container.
    mViews.push_back( view );

    // Add underlying osg::Camera to osg::Viewer.
    RefPtr<osg::Camera> camera = view->OsgGetCamera();
    OsgAddCamera( camera, eCamera_3D, mainView );

    // Set the camera's cull mask (node mask).
    // This is for the ability to render 3D models in one view but not others.
    // A specific case is disabling the player's model in OTW view.
#if 1
    // Disregard previous cull mask.
    camera->setCullMask( mainView ? defs::NODE_MASK_MAIN_VIEW_ALL
                                  : defs::NODE_MASK_AUX_VIEW_ALL );
#else
    // Regard previous cull mask.
    osg::Node::NodeMask mask = camera->getCullMask();
    mask &= ~defs::NODE_MASK_ALL;
    mask |= (mainView ? defs::NODE_MASK_MAIN_VIEW_ALL
                      : defs::NODE_MASK_AUX_VIEW_ALL);
    camera->setCullMask( mask );
#endif

    // Register a callback with OSG to broadcast events.
    camera->setPreDrawCallback(  new CameraDrawCallback( view, CameraDrawCallback::PRE_DRAW ) );
    camera->setPostDrawCallback( new CameraDrawCallback( view, CameraDrawCallback::POST_DRAW ) );

    // RELATIVE_RF isn't appropritate (but is for manipulators).
    if ( not VIEW_CONF.mManipulatorEnabled )
        camera->setReferenceFrame( osg::Transform::ABSOLUTE_RF );

    // Try to suppress culling of Craft (doesn't seem to help much).
    camera->setCullingMode( camera->getCullingMode() & ~osg::CullStack::SMALL_FEATURE_CULLING );
}

/*****************************************************************************
 * Save/restore viewpoint to binary configuration file.
 *****************************************************************************/
bool
Window::SaveViewpoint( void )
{
    // Save viewpoint to binary config file.
    if ( VIEW_CONF.mManipulatorEnabled )
    {
        RefPtr<osgGA::MatrixManipulator> manipulator = mViewer->getCameraManipulator();
        const Matrix m = manipulator->getMatrix();
        if ( WriteConfBin( glue::GetConfBin(), "manipulatorMatrix", m ) )
        {
            CLOG << "Saved viewpoint (manipulator-mode)." << std::endl;
            return true;
        }
        else
        {
            CERROR << "ERROR: Failed to save viewpoint (manipulator)" << std::endl;
            return false;
        }
    }
    else
    {
        CERROR << "[Window::SaveViewpoint] UNIMPLEMENTED case" << std::endl;
        return false;
    }
}

bool
Window::RestoreViewpoint( void )
{
    // Restore viewpoint from binary config file.
    if ( VIEW_CONF.mManipulatorEnabled )
    {
        Matrix m;
        if ( ReadConfBin( glue::GetConfBin(), "manipulatorMatrix", m ) )
        {
            CLOG << "Restored viewpoint (manipulator-mode)." << std::endl;
            mManipulator->setByMatrix( m );
            return true;
        }
        else
        {
            // Be quiet because restoring is automatically tried.
          //CERROR << "WARNING: Failed to restore viewpoint (manipluator-mode).  Was it saved before?" << std::endl;
            return false;
        }
    }
    else
    {
        CERROR << "[Window::RestoreViewpoint] UNIMPLEMENTED case" << std::endl;
        return false;
    }
}

/*****************************************************************************
 * Get size of window as a (width,height) pair.
 *****************************************************************************/
WidthHeight<int>
Window::GetWindowSize( void )
{
    // Get screen coordinates of both corners, subtract to get width,height.
    const Rect<int> rect = GetWindowPosition();
    const int w = rect.x2 - rect.x1;
    const int h = rect.y2 - rect.y1;
    ASSERT( (w >= 0) and (h >= 0) );
    return WidthHeight<int>( w, h );
}

/*****************************************************************************
 * Get size of window as a rectangle.
 * @return Rect(0,0,w,h).
 * Return value is similar to View::GetRect() and is independent of window's position.
 *****************************************************************************/
Rect<int>
Window::GetWindowRect( void )
{
    const WidthHeight<int> wh = GetWindowSize();
    return Rect<int>( 0, 0, wh.w, wh.h );
}

/*****************************************************************************
 * Get the position of window as screen coordinates of bottom-left and top-right corners.
 * x1,y1,x2,y2 will all change as window is moved.
 *****************************************************************************/
Rect<int>
Window::GetWindowPosition( void )
{
    // Get the current size of OSG GraphicsWindow (window may be resized externally).
    int x = 0, y = 0, w = 0, h = 0;
    mGraphicsWindow->getWindowRectangle( x, y, w, h );
    ASSERT( BOOL_CAST( w|h ) );
    return Rect<int>( x, y, x+w, y+h );  // convert w,h to x2,y2
}

/*****************************************************************************
 * OSG-specific.
 *****************************************************************************/
RefPtr<osgViewer::GraphicsWindow>
Window::OsgGetGraphicsWindow( void )
{
    return mGraphicsWindow;
}

/*****************************************************************************
 * Register handler for input events (keyboard, mouse).
 *****************************************************************************/
void
Window::OsgRegisterEventHandler( RefPtr<osgGA::GUIEventHandler> eventHandler )
{
    mViewer->addEventHandler( eventHandler.get() );
}

/*****************************************************************************
 * Set which key toggles the statistics display.
 *****************************************************************************/
void
Window::OsgSetStatsKey( const int key )
{
    // Statistics might be entirely disabled.
    if ( VIEW_CONF.mStatsEnabled and (mStatsHandler != NULL) )
        mStatsHandler->setKeyEventTogglesOnScreenStats( key );
}

/*****************************************************************************
 * Called by OSG to let app do low-level graphics operations.
 *****************************************************************************/
void
Window::DoOperation( void )
{
    // Call post-draw listeners.
    EVENT_VIEW_POST_DRAW_ALL_VIEWS.Broadcast( Void() );

    // Draw outline around Views (unless disabled).
    Views::iterator viewIter = mViews.begin();
    ASSERT( viewIter != mViews.end() );
#if COMPILE_PALOMINO == 3
    ++viewIter;  // skip main view
#endif
    for ( ; viewIter != mViews.end(); ++viewIter )
    {
        shptr<View> view = *viewIter;
        if ( view->IfEnabled() )
            DrawViewOutline( view, view->GetOutlineColor() );
    }
}

/*****************************************************************************
 * Draw outline around 2D viewpoint.
 *****************************************************************************/
void
Window::DrawViewOutline( shptr<View> view, const RGBA outlineColor )
{
CHECK_TYPESIG(view,TYPESIG_VIEW);

    GFX::DrawBox( view->GetRect(), outlineColor );
}

////////////////////////////////////////////////////////////////////////////////
///////////////////////////  Window::Operation  ////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * ctor/dtor.
 *****************************************************************************/
Window::Operation::Operation( void )
:   osg::Operation("WindowOperation",true)
{
    // NOP
}

Window::Operation::~Operation()
{
    // NOP
}

/*****************************************************************************
 * Functor.
 *****************************************************************************/
void
Window::Operation::operator()( osg::Object* )
{
    // Reflect to Window.
    GET_WINDOW().DoOperation();
}

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////   Window::FPS  ///////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * ctor.
 ****************************************************************************/
Window::FPS::FPS( void )
:   mFPS(0),
    mFrameCount(0),
    mFrameStart(0)
{
    // NOP
}

/*****************************************************************************
 * To be called after rendering every View.
 *
 * This computes momentary FPS (FPS within the last second)
 * which reflects rapid changes in frame-rate.
 * Another way is average FPS but that hides frame-rate drop.
 *****************************************************************************/
void
Window::FPS::Tick( const Milliseconds elapsedMilsec )
{
    // Count this frame.
    ++mFrameCount;

    // Is this second finished?
    if ( elapsedMilsec - mFrameStart >= Milliseconds(1000) )
    {
        // mFrameCount directly is frames/second.
        // Save value in mFPS which ComputeFPS() will return later.
        mFPS = mFrameCount;

        // Reset for the next second.
        mFrameStart = elapsedMilsec;
        mFrameCount = 0;
    }
}

} // namespace view
