//-----------------------------------------------------------------------------
// Copyright (c) 2012 GarageGames, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------

#include "platform/platform.h"
#include "scene/sceneManager.h"

#include "scene/sceneObject.h"
#include "scene/zones/sceneTraversalState.h"
#include "scene/sceneRenderState.h"
#include "scene/zones/sceneRootZone.h"
#include "scene/zones/sceneZoneSpace.h"
#include "lighting/lightManager.h"
#include "renderInstance/renderPassManager.h"
#include "gfx/gfxDevice.h"
#include "gfx/gfxDrawUtil.h"
#include "gfx/gfxDebugEvent.h"
#include "console/engineAPI.h"
#include "sim/netConnection.h"
#include "T3D/gameBase/gameConnection.h"
#include "math/mathUtils.h"

// For player object bounds workaround.
#include "T3D/player.h"

#include "postFx/postEffectManager.h"

extern bool gEditingMission;


MODULE_BEGIN( Scene )

   MODULE_INIT_AFTER( Sim )   
   MODULE_SHUTDOWN_BEFORE( Sim )
   
   MODULE_INIT
   {
      // Client scene.
      gClientSceneGraph = new SceneManager( true );

      // Server scene.
      gServerSceneGraph = new SceneManager( false );

      Con::addVariable( "$Scene::lockCull", TypeBool, &SceneManager::smLockDiffuseFrustum,
         "Debug tool which locks the frustum culling to the current camera location.\n"
         "@ingroup Rendering\n" );

      Con::addVariable( "$Scene::disableTerrainOcclusion", TypeBool, &SceneCullingState::smDisableTerrainOcclusion,
         "Used to disable the somewhat expensive terrain occlusion testing.\n"
         "@ingroup Rendering\n" );

      Con::addVariable( "$Scene::disableZoneCulling", TypeBool, &SceneCullingState::smDisableZoneCulling,
         "If true, zone culling will be disabled and the scene contents will only be culled against the root frustum.\n\n"
         "@ingroup Rendering\n" );

      Con::addVariable( "$Scene::renderBoundingBoxes", TypeBool, &SceneManager::smRenderBoundingBoxes,
         "If true, the bounding boxes of objects will be displayed.\n\n"
         "@ingroup Rendering" );

      Con::addVariable( "$Scene::maxOccludersPerZone", TypeS32, &SceneCullingState::smMaxOccludersPerZone,
         "Maximum number of occluders that will be concurrently allowed into the scene culling state of any given zone.\n\n"
         "@ingroup Rendering" );

      Con::addVariable( "$Scene::occluderMinWidthPercentage", TypeF32, &SceneCullingState::smOccluderMinWidthPercentage,
         "TODO\n\n"
         "@ingroup Rendering" );

      Con::addVariable( "$Scene::occluderMinHeightPercentage", TypeF32, &SceneCullingState::smOccluderMinHeightPercentage,
         "TODO\n\n"
         "@ingroup Rendering" );
   }
   
   MODULE_SHUTDOWN
   {
      SAFE_DELETE( gClientSceneGraph );
      SAFE_DELETE( gServerSceneGraph );
   }

MODULE_END;


bool SceneManager::smRenderBoundingBoxes;
bool SceneManager::smLockDiffuseFrustum = false;
SceneCameraState SceneManager::smLockedDiffuseCamera = SceneCameraState( RectI(), Frustum(), MatrixF(), MatrixF() );

SceneManager* gClientSceneGraph = NULL;
SceneManager* gServerSceneGraph = NULL;


//-----------------------------------------------------------------------------

SceneManager::SceneManager( bool isClient )
   : mIsClient( isClient ),
     mZoneManager( NULL ),
     mUsePostEffectFog( true ),
     mDisplayTargetResolution( 0, 0 ),
     mCurrentRenderState( NULL ),
     mVisibleDistance( 500.f ),
     mVisibleGhostDistance( 0 ),
     mNearClip( 0.1f ),
     mLightManager( NULL ),
     mAmbientLightColor( LinearColorF( 0.1f, 0.1f, 0.1f, 1.0f ) ),
     mDefaultRenderPass( NULL )
{
   VECTOR_SET_ASSOCIATION( mBatchQueryList );

   // For the client, create a zone manager.

   if( isClient )
   {
      mZoneManager = new SceneZoneSpaceManager( getContainer() );

      // Add the root zone to the scene.

      addObjectToScene( mZoneManager->getRootZone() );
   }
}

//-----------------------------------------------------------------------------

SceneManager::~SceneManager()
{   
   SAFE_DELETE( mZoneManager );

   if( mLightManager )
      mLightManager->deactivate();   
}

//-----------------------------------------------------------------------------

void SceneManager::renderScene( ScenePassType passType, U32 objectMask )
{
   SceneCameraState cameraState = SceneCameraState::fromGFX();
   
   // Handle frustum locking.

   const bool lockedFrustum = ( smLockDiffuseFrustum && passType == SPT_Diffuse );
   if( lockedFrustum )
      cameraState = smLockedDiffuseCamera;
   else if( passType == SPT_Diffuse )
   {
      // Store the camera state so if we lock, this will become the
      // locked state.
      smLockedDiffuseCamera = cameraState;
   }
   
   // Create the render state.

   SceneRenderState renderState( this, passType, cameraState );

   // If we have locked the frustum, reset the view transform
   // on the render pass which the render state has just set
   // to the view matrix corresponding to the locked frustum.  For
   // rendering, however, we need the true view matrix from the
   // GFX state.

   if( lockedFrustum )
   {
      RenderPassManager* rpm = renderState.getRenderPass();
      rpm->assignSharedXform( RenderPassManager::View, GFX->getWorldMatrix() );
   }

   // Render.

   renderScene( &renderState, objectMask );
}

//-----------------------------------------------------------------------------

void SceneManager::renderScene( SceneRenderState* renderState, U32 objectMask, SceneZoneSpace* baseObject, U32 baseZone )
{
   PROFILE_SCOPE( SceneGraph_renderScene );

   // Get the lights for rendering the scene.

   PROFILE_START( SceneGraph_registerLights );
      LIGHTMGR->registerGlobalLights( &renderState->getCullingFrustum(), false, renderState->isDiffusePass());
   PROFILE_END();

   // If its a diffuse pass, update the current ambient light level.
   // To do that find the starting zone and determine whether it has a custom
   // ambient light color.  If so, pass it on to the ambient light manager.
   // If not, use the ambient light color of the sunlight.
   //
   // Note that we retain the starting zone information here and pass it
   // on to renderSceneNoLights so that we don't need to look it up twice.

   if( renderState->isDiffusePass() )
   {
      if( !baseObject && getZoneManager() )
      {
         getZoneManager()->findZone( renderState->getCameraPosition(), baseObject, baseZone );
         AssertFatal( baseObject != NULL, "SceneManager::renderScene - findZone() did not return an object" );
      }

      LinearColorF zoneAmbient;
      if( baseObject && baseObject->getZoneAmbientLightColor( baseZone, zoneAmbient ) )
         mAmbientLightColor.setTargetValue( zoneAmbient );
      else
      {
         const LightInfo* sunlight = LIGHTMGR->getSpecialLight( LightManager::slSunLightType );
         if( sunlight )
            mAmbientLightColor.setTargetValue( sunlight->getAmbient() );
      }

      renderState->setAmbientLightColor( mAmbientLightColor.getCurrentValue() );
   }

   // Trigger the pre-render signal.

   PROFILE_START( SceneGraph_preRenderSignal);
      mCurrentRenderState = renderState;
      getPreRenderSignal().trigger( this, renderState );
      mCurrentRenderState = NULL;
   PROFILE_END();

   // Render the scene.

   if(GFX->getCurrentRenderStyle() == GFXDevice::RS_StereoSideBySide)
   {
      // Store previous values
      RectI originalVP = GFX->getViewport();
      MatrixF originalWorld = GFX->getWorldMatrix();
      Frustum originalFrustum = GFX->getFrustum();

      // Save PFX & SceneManager projections
      MatrixF origNonClipProjection = renderState->getSceneManager()->getNonClipProjection();
      PFXFrameState origPFXState = PFXMGR->getFrameState();

      const FovPort *currentFovPort = GFX->getStereoFovPort();
      const MatrixF *worldEyeTransforms = GFX->getInverseStereoEyeTransforms();

      // Render left half of display
      GFX->activateStereoTarget(0);
      GFX->beginField();

      GFX->setWorldMatrix(worldEyeTransforms[0]);

      Frustum gfxFrustum = originalFrustum;
      MathUtils::makeFovPortFrustum(&gfxFrustum, gfxFrustum.isOrtho(), gfxFrustum.getNearDist(), gfxFrustum.getFarDist(), currentFovPort[0]);
      GFX->setFrustum(gfxFrustum);

      SceneCameraState cameraStateLeft = SceneCameraState::fromGFX();
      SceneRenderState renderStateLeft( this, renderState->getScenePassType(), cameraStateLeft );
      renderStateLeft.getSceneManager()->setNonClipProjection(GFX->getProjectionMatrix());
      renderStateLeft.setSceneRenderStyle(SRS_SideBySide);
      PFXMGR->setFrameMatrices(GFX->getWorldMatrix(), GFX->getProjectionMatrix());

      renderSceneNoLights( &renderStateLeft, objectMask, baseObject, baseZone ); // left

      // Indicate that we've just finished a field
      //GFX->clear(GFXClearTarget | GFXClearZBuffer | GFXClearStencil, ColorI(255,0,0), 1.0f, 0);
      GFX->endField();
      
      // Render right half of display
      GFX->activateStereoTarget(1);
      GFX->beginField();
      GFX->setWorldMatrix(worldEyeTransforms[1]);

      gfxFrustum = originalFrustum;
      MathUtils::makeFovPortFrustum(&gfxFrustum, gfxFrustum.isOrtho(), gfxFrustum.getNearDist(), gfxFrustum.getFarDist(), currentFovPort[1]);
      GFX->setFrustum(gfxFrustum);

      SceneCameraState cameraStateRight = SceneCameraState::fromGFX();
      SceneRenderState renderStateRight( this, renderState->getScenePassType(), cameraStateRight );
      renderStateRight.getSceneManager()->setNonClipProjection(GFX->getProjectionMatrix());
      renderStateRight.setSceneRenderStyle(SRS_SideBySide);
      PFXMGR->setFrameMatrices(GFX->getWorldMatrix(), GFX->getProjectionMatrix());

      renderSceneNoLights( &renderStateRight, objectMask, baseObject, baseZone ); // right

      // Indicate that we've just finished a field
      //GFX->clear(GFXClearTarget | GFXClearZBuffer | GFXClearStencil, ColorI(0,255,0), 1.0f, 0);
      GFX->endField();

      // Restore previous values
      renderState->getSceneManager()->setNonClipProjection(origNonClipProjection);
      PFXMGR->setFrameState(origPFXState);

      GFX->setWorldMatrix(originalWorld);
      GFX->setFrustum(originalFrustum);
      GFX->setViewport(originalVP);
   }
   else
   {
      renderSceneNoLights( renderState, objectMask, baseObject, baseZone );
   }

   // Trigger the post-render signal.

   PROFILE_START( SceneGraphRender_postRenderSignal );
      mCurrentRenderState = renderState;
      getPostRenderSignal().trigger( this, renderState );
      mCurrentRenderState = NULL;
   PROFILE_END();

   // Remove the previously registered lights.

   PROFILE_START( SceneGraph_unregisterLights);
      LIGHTMGR->unregisterAllLights();
   PROFILE_END();
}

//-----------------------------------------------------------------------------

void SceneManager::renderSceneNoLights( SceneRenderState* renderState, U32 objectMask, SceneZoneSpace* baseObject, U32 baseZone )
{
   // Set the current state.

   mCurrentRenderState = renderState;

   // Render.

   _renderScene( mCurrentRenderState, objectMask, baseObject, baseZone );

   #ifdef TORQUE_DEBUG

   // If frustum is locked and this is a diffuse pass, render the culling volumes of
   // zones that are selected (or the volumes of the outdoor zone if no zone is
   // selected).

   if( gEditingMission && renderState->isDiffusePass() && smLockDiffuseFrustum )
      renderState->getCullingState().debugRenderCullingVolumes();
   
   #endif

   mCurrentRenderState = NULL;
}

//-----------------------------------------------------------------------------

void SceneManager::_renderScene( SceneRenderState* state, U32 objectMask, SceneZoneSpace* baseObject, U32 baseZone )
{
   AssertFatal( this == gClientSceneGraph, "SceneManager::_buildSceneGraph - Only the client scenegraph can support this call!" );

   PROFILE_SCOPE( SceneGraph_batchRenderImages );

   // In the editor, override the type mask for diffuse passes.

   if( gEditingMission && state->isDiffusePass() )
      objectMask = EDITOR_RENDER_TYPEMASK;

   // Update the zoning state and traverse zones.

   if( getZoneManager() )
   {
      // Update.

      getZoneManager()->updateZoningState();

      // If zone culling isn't disabled, traverse the
      // zones now.

      if( !state->getCullingState().disableZoneCulling() )
      {
         // Find the start zone if we haven't already.

         if( !baseObject )
         {
            getZoneManager()->findZone( state->getCameraPosition(), baseObject, baseZone );
            AssertFatal( baseObject != NULL, "SceneManager::_renderScene - findZone() did not return an object" );
         }

         // Traverse zones starting in base object.

         SceneTraversalState traversalState( &state->getCullingState() );
         PROFILE_START( Scene_traverseZones );
         baseObject->traverseZones( &traversalState, baseZone );
         PROFILE_END();

         // Set the scene render box to the area we have traversed.

         state->setRenderArea( traversalState.getTraversedArea() );
      }
   }

   // Set the query box for the container query.  Never
   // make it larger than the frustum's AABB.  In the editor,
   // always query the full frustum as that gives objects
   // the opportunity to render editor visualizations even if
   // they are otherwise not in view.

   if( !state->getCullingFrustum().getBounds().isOverlapped( state->getRenderArea() ) )
   {
      // This handles fringe cases like flying backwards into a zone where you
      // end up pretty much standing on a zone border and looking directly into
      // its "walls".  In that case the traversal area will be behind the frustum
      // (remember that the camera isn't where visibility starts, it's the near
      // distance).

      return;
   }

   Box3F queryBox = state->getCullingFrustum().getBounds();
   if( !gEditingMission )
   {
      queryBox.minExtents.setMax( state->getRenderArea().minExtents );
      queryBox.maxExtents.setMin( state->getRenderArea().maxExtents );
   }

   PROFILE_START( Scene_cullObjects );

   //TODO: We should split the codepaths here based on whether the outdoor zone has visible space.
   //    If it has, we should use the container query-based path.
   //    If it hasn't, we should fill the object list directly from the zone lists which will usually
   //       include way fewer objects.
   
   // Gather all objects that intersect the scene render box.

   mBatchQueryList.clear();
   getContainer()->findObjectList( queryBox, objectMask, &mBatchQueryList );

   // Cull the list.

   U32 numRenderObjects = state->getCullingState().cullObjects(
      mBatchQueryList.address(),
      mBatchQueryList.size(),
      !state->isDiffusePass() ? SceneCullingState::CullEditorOverrides : 0 // Keep forced editor stuff out of non-diffuse passes.
   );

   //HACK: If the control object is a Player and it is not in the render list, force
   // it into it.  This really should be solved by collision bounds being separate from
   // object bounds; only because the Player class is using bounds not encompassing
   // the actual player object is it that we have this problem in the first place.
   // Note that we are forcing the player object into ALL passes here but such
   // is the power of proliferation of things done wrong.

   GameConnection* connection = GameConnection::getConnectionToServer();
   if( connection )
   {
      Player* player = dynamic_cast< Player* >( connection->getControlObject() );
      if( player )
      {
         mBatchQueryList.setSize( numRenderObjects );
         if( !mBatchQueryList.contains( player ) )
         {
            mBatchQueryList.push_back( player );
            numRenderObjects ++;
         }
      }
   }

   PROFILE_END();

   //store our rendered objects into a list we can easily look up against later if required
   mRenderedObjectsList.clear();
   for (U32 i = 0; i < numRenderObjects; ++i)
   {
      mRenderedObjectsList.push_back(mBatchQueryList[i]);
   }

   // Render the remaining objects.

   PROFILE_START( Scene_renderObjects );
   state->renderObjects( mBatchQueryList.address(), numRenderObjects );
   PROFILE_END();

   // Render bounding boxes, if enabled.

   if( smRenderBoundingBoxes && state->isDiffusePass() )
   {
      GFXDEBUGEVENT_SCOPE( Scene_renderBoundingBoxes, ColorI::WHITE );

      GameBase* cameraObject = 0;
      if( connection )
         cameraObject = connection->getCameraObject();

      GFXStateBlockDesc desc;
      desc.setFillModeWireframe();
      desc.setZReadWrite( true, false );

      for( U32 i = 0; i < numRenderObjects; ++ i )
      {
         SceneObject* object = mBatchQueryList[ i ];

         // Skip global bounds object.
         if( object->isGlobalBounds() )
            continue;

         // Skip camera object as we're viewing the scene from it.
         if( object == cameraObject )
            continue;

         const Box3F& worldBox = object->getWorldBox();
         GFX->getDrawUtil()->drawObjectBox(
            desc,
            Point3F( worldBox.len_x(), worldBox.len_y(), worldBox.len_z() ),
            worldBox.getCenter(),
            MatrixF::Identity,
            ColorI::WHITE
         );
      }
   }
}

//-----------------------------------------------------------------------------

struct ScopingInfo
{
   Point3F        scopePoint;
   F32            scopeDist;
   F32            scopeDistSquared;
   NetConnection* connection;
};

static void _scopeCallback( SceneObject* object, void* data )
{
   if( !object->isScopeable() )
      return;

   ScopingInfo* info = reinterpret_cast< ScopingInfo* >( data );
   NetConnection* connection = info->connection;

   F32 difSq = ( object->getWorldSphere().center - info->scopePoint ).lenSquared();
   if( difSq < info->scopeDistSquared )
   {
      // Not even close, it's in...
      connection->objectInScope( object );
   }
   else
   {
      // Check a little more closely...
      F32 realDif = mSqrt( difSq );
      if( realDif - object->getWorldSphere().radius < info->scopeDist)
         connection->objectInScope( object );
   }
}

void SceneManager::scopeScene( CameraScopeQuery* query, NetConnection* netConnection )
{
   PROFILE_SCOPE( SceneGraph_scopeScene );

   // Note that this method does not use the zoning information in the scene
   // to scope objects.  The reason is that with the way that scoping is implemented
   // in the networking layer--i.e. by killing off ghosts of objects that are out
   // of scope--, it doesn't make sense to let, for example, all objects in the outdoor
   // zone go out of scope, just because there is no exterior portal that is visible from
   // the current camera viewpoint (in any direction).
   //
   // So, we perform a simple box query on the area covered by the camera query
   // and then scope in everything that is in range.
   
   // Set up scoping info.

   ScopingInfo info;

   info.scopePoint       = query->pos;
   info.scopeDist        = query->visibleDistance;
   info.scopeDistSquared = info.scopeDist * info.scopeDist;
   info.connection       = netConnection;

   // Scope all objects in the query area.

   Box3F area( query->visibleDistance );
   area.setCenter( query->pos );

   getContainer()->findObjects( area, 0xFFFFFFFF, _scopeCallback, &info );
}

//-----------------------------------------------------------------------------

bool SceneManager::addObjectToScene( SceneObject* object )
{
   AssertFatal( !object->mSceneManager, "SceneManager::addObjectToScene - Object already part of a scene" );

   // Mark the object as belonging to us.

   object->mSceneManager = this;

   // Register with managers except its the root zone.

   if( !dynamic_cast< SceneRootZone* >( object ) )
   {
      // Add to container.

      getContainer()->addObject( object );

      // Register the object with the zone manager.

      if( getZoneManager() )
         getZoneManager()->registerObject( object );
   }

   // Notify the object.

   return object->onSceneAdd();
}

//-----------------------------------------------------------------------------

void SceneManager::removeObjectFromScene( SceneObject* obj )
{
   AssertFatal( obj, "SceneManager::removeObjectFromScene - Object is not declared" );
   AssertFatal( obj->getSceneManager() == this, "SceneManager::removeObjectFromScene - Object not part of SceneManager" );

   // Notify the object.

   obj->onSceneRemove();

   // Remove the object from the container.

   if( getContainer() )
      getContainer()->removeObject( obj );

   // Remove the object from the zoning system.

   if( getZoneManager() )
      getZoneManager()->unregisterObject( obj );

   // Clear out the reference to us.

   obj->mSceneManager = NULL;
}

//-----------------------------------------------------------------------------

void SceneManager::notifyObjectDirty( SceneObject* object )
{
   // Update container state.

   if( object->mContainer )
      object->mContainer->checkBins( object );

   // Mark zoning state as dirty.

   if( getZoneManager() )
      getZoneManager()->notifyObjectChanged( object );
}

//-----------------------------------------------------------------------------

void SceneManager::setDisplayTargetResolution( const Point2I &size )
{
   mDisplayTargetResolution = size;
}

//-----------------------------------------------------------------------------

const Point2I & SceneManager::getDisplayTargetResolution() const
{
   return mDisplayTargetResolution;
}

//-----------------------------------------------------------------------------

bool SceneManager::setLightManager( const char* lmName )
{
   LightManager *lm = LightManager::findByName( lmName );
   if ( !lm )
      return false;

   return _setLightManager( lm );
}

//-----------------------------------------------------------------------------

bool SceneManager::_setLightManager( LightManager* lm )
{
   // Avoid unnecessary work reinitializing materials.
   if ( lm == mLightManager )
      return true;

   // Make sure its valid... else fail!
   if ( !lm->isCompatible() )
      return false;

   // We only deactivate it... all light managers are singletons
   // and will manager their own lifetime.
   if ( mLightManager )
      mLightManager->deactivate();

   mLightManager = lm;

   if ( mLightManager )
      mLightManager->activate( this );

   return true;
}

//-----------------------------------------------------------------------------

RenderPassManager* SceneManager::getDefaultRenderPass() const
{
   if( !mDefaultRenderPass )
   {
      Sim::findObject( "DiffuseRenderPassManager", mDefaultRenderPass );
      AssertISV( mDefaultRenderPass, "SceneManager::_setDefaultRenderPass - No DiffuseRenderPassManager defined!  Must be set up in script!" );
   }

   return mDefaultRenderPass;
}

//=============================================================================
//    Console API.
//=============================================================================
// MARK: ---- Console API ----

//-----------------------------------------------------------------------------

DefineEngineFunction( sceneDumpZoneStates, void, ( bool updateFirst ), ( true ),
   "Dump the current zoning states of all zone spaces in the scene to the console.\n\n"
   "@param updateFirst If true, zoning states are brought up to date first; if false, the zoning states "
   "are dumped as is.\n\n"
   "@note Only valid on the client.\n"
   "@ingroup Game" )
{
   if( !gClientSceneGraph )
   {
      Con::errorf( "sceneDumpZoneStates - Only valid on client!" );
      return;
   }

   SceneZoneSpaceManager* manager = gClientSceneGraph->getZoneManager();
   if( !manager )
   {
      Con::errorf( "sceneDumpZoneStates - Scene is not using zones!" );
      return;
   }

   manager->dumpZoneStates( updateFirst );
}

//-----------------------------------------------------------------------------

DefineEngineFunction( sceneGetZoneOwner, SceneObject*, ( U32 zoneId ),,
   "Return the SceneObject that contains the given zone.\n\n"
   "@param zoneId ID of zone.\n"
   "@return A SceneObject or NULL if the given @a zoneId is invalid.\n\n"
   "@note Only valid on the client.\n"
   "@ingroup Game" )
{
   if( !gClientSceneGraph )
   {
      Con::errorf( "sceneGetZoneOwner - Only valid on client!" );
      return NULL;
   }

   SceneZoneSpaceManager* manager = gClientSceneGraph->getZoneManager();
   if( !manager )
   {
      Con::errorf( "sceneGetZoneOwner - Scene is not using zones!" );
      return NULL;
   }

   if( !manager->isValidZoneId( zoneId ) )
   {
      Con::errorf( "sceneGetZoneOwner - Invalid zone ID: %i", zoneId );
      return NULL;
   }

   return manager->getZoneOwner( zoneId );
}
