//-----------------------------------------------------------------------------
// 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 "T3D/portal.h"

#include "core/stream/bitStream.h"
#include "console/consoleTypes.h"
#include "console/engineAPI.h"
#include "scene/sceneManager.h"
#include "scene/sceneRenderState.h"
#include "scene/zones/sceneRootZone.h"
#include "scene/culling/sceneCullingState.h"
#include "scene/zones/sceneTraversalState.h"
#include "math/mPlaneSet.h"
#include "gfx/gfxDrawUtil.h"
#include "gfx/gfxTransformSaver.h"

#include "scene/mixin/sceneAmbientSoundObject.impl.h"
#include "scene/mixin/scenePolyhedralObject.impl.h"
#include "math/mPolyhedron.impl.h"


IMPLEMENT_CO_NETOBJECT_V1( Portal );

ConsoleDocClass( Portal,
   "@brief An object that provides a \"window\" into a zone, allowing a viewer "
      "to see what's rendered in the zone.\n\n"

   "A portal is an object that connects zones such that the content of one zone becomes "
   "visible in the other when looking through the portal.\n\n"

   "Each portal is a full zone which is divided into two sides by the portal plane that "
   "intersects it.  This intersection polygon is shown in red in the editor.  Either of the "
   "sides of a portal can be connected to one or more zones.\n\n"

   "A connection from a specific portal side to a zone is made in either of two ways:\n\n"

   "<ol>\n"
   "<li>By moving a Zone object to intersect with the portal at the respective side.  While usually it makes "
   "sense for this overlap to be small, the connection is established correctly as long as the center of the Zone "
   "object that should connect is on the correct side of the portal plane.</li>\n"
   "<li>By the respective side of the portal free of Zone objects that would connect to it.  In this case, given "
   "that the other side is connected to one or more Zones, the portal will automatically connect itself to the "
   "outdoor \"zone\" which implicitly is present in any level.</li>\n"
   "</ol>\n\n"

   "From this, it follows that there are two types of portals:\n\n"

   "<dl>\n"
   "<dt>Exterior Portals</dt>"
   "<dd>An exterior portal is one that is connected to one or more Zone objects on one side and to the outdoor "
   "zone at the other side.  This kind of portal is most useful for covering transitions from outdoor spaces to "
   "indoor spaces.</dd>"
   "<dt>Interior Portals</dt>"
   "<dd>An interior portal is one that is connected to one or more Zone objects on both sides.  This kind of portal "
   "is most useful for covering transitions between indoor spaces./dd>"
   "</dl>\n\n"

   "Strictly speaking, there is a third type of portal called an \"invalid portal\".  This is a portal that is not "
   "connected to a Zone object on either side in which case the portal serves no use.\n\n"

   "Portals in Torque are bidirectional meaning that they connect zones both ways and "
   "you can look through the portal's front side as well as through its back-side.\n\n"

   "Like Zones, Portals can either be box-shaped or use custom convex polyhedral shapes.\n\n"

   "Portals will usually be created in the editor but can, of course, also be created "
   "in script code as such:\n\n"

   "@tsexample\n"
      "// Example declaration of a Portal.  This will create a box-shaped portal.\n"
      "new Portal( PortalToTestZone )\n"
      "{\n"
      "   position = \"12.8467 -4.02246 14.8017\";\n"
      "	 rotation = \"0 0 -1 97.5085\";\n"
      "	 scale = \"1 0.25 1\";\n"
      "	 canSave = \"1\";\n"
      "	 canSaveDynamicFields = \"1\";\n"
      "};\n"
   "@endtsexample\n\n"

   "@note Keep in mind that zones and portals are more or less strictly a scene optimization mechanism meant to "
      "improve render times.\n\n"

   "@see Zone\n"
   "@ingroup enviroMisc\n"
);


// Notes:
// - This class implements portal spaces as single zones.  A different, interesting take
//   on this is to turn portal spaces into two zones with the portal plane acting as
//   the separator.
// - One downside to our treatment of portals as full zones in their own right is that
//   in certain cases we end up including space in the traversal that is clearly not visible.
//   Take a situation where you are in the outside zone and you are looking straight into
//   the wall of a house.  On the other side of that house is a portal leading into the house.
//   While the traversal will not step through that portal into the house since that would
//   be leading towards the camera rather than away from it, it will still add the frustum
//   to the visible space of the portal zone and thus make everything in the portal zone
//   visible.  It has to do this since, while it can easily tell whether to step through the
//   portal or not, it cannot easily tell whether the whole portal zone is visible or not as
//   that depends on the occlusion situation in the outdoor zone.


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

Portal::Portal()
   : mClassification( InvalidPortal ),
     mIsGeometryDirty( true )
{
   VECTOR_SET_ASSOCIATION( mPortalPolygonWS );

   mNetFlags.set( Ghostable | ScopeAlways );
   mTypeMask |= StaticObjectType;

   mPassableSides[ FrontSide ] = true;
   mPassableSides[ BackSide ] = true;

   mObjScale.set( 1.0f, 0.25f, 1.0f );

   // We're not closed off.
   mZoneFlags.clear( ZoneFlag_IsClosedOffSpace );
}

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

void Portal::initPersistFields()
{
   addGroup( "Zoning" );

      addProtectedField( "frontSidePassable", TypeBool, Offset( mPassableSides[ FrontSide ], Portal ),
         &_setFrontSidePassable, &defaultProtectedGetFn,
         "Whether one can view through the front-side of the portal." );
      addProtectedField( "backSidePassable", TypeBool, Offset( mPassableSides[ BackSide ], Portal ),
         &_setBackSidePassable, &defaultProtectedGetFn,
         "Whether one can view through the back-side of the portal." );

   endGroup( "Zoning" );

   Parent::initPersistFields();
}

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

void Portal::consoleInit()
{
   // Disable rendering of portals by default.
   getStaticClassRep()->mIsRenderEnabled = false;
}

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

String Portal::describeSelf() const
{
   String str = Parent::describeSelf();
   
   switch( getClassification() )
   {
      case InvalidPortal:     str += "|InvalidPortal"; break;
      case ExteriorPortal:    str += "|ExteriorPortal"; break;
      case InteriorPortal:    str += "|InteriorPortal"; break;
   }

   return str;
}

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

bool Portal::writeField( StringTableEntry fieldName, const char* value )
{
   static StringTableEntry sFrontSidePassable = StringTable->insert( "frontSidePassable" );
   static StringTableEntry sBackSidePassable = StringTable->insert( "backSidePassable" );

   // Don't write passable flags if at default.
   if( ( fieldName == sFrontSidePassable || fieldName == sBackSidePassable ) &&
       dAtob( value ) )
      return false;

   return Parent::writeField( fieldName, value );
}

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

void Portal::onSceneRemove()
{
   // Disconnect from root zone, if it's an exterior portal.

   if( mClassification == ExteriorPortal )
   {
      AssertFatal( getSceneManager()->getZoneManager(), "Portal::onSceneRemove - Portal classified as exterior without having a zone manager!" );
      getSceneManager()->getZoneManager()->getRootZone()->disconnectZoneSpace( this );
   }

   Parent::onSceneRemove();
}

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

void Portal::setTransform( const MatrixF& mat )
{
   // Portal geometry needs updating.  Set this before calling
   // parent because the transform change will cause an immediate
   // update of the portal's zoning state.
   mIsGeometryDirty = true;

   Parent::setTransform( mat );
}

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

void Portal::setSidePassable( Side side, bool value )
{
   if( mPassableSides[ side ] == value )
      return;

   mPassableSides[ side ] = value;

   if( isServerObject() )
      setMaskBits( PassableMask );
}

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

U32 Portal::packUpdate( NetConnection *con, U32 mask, BitStream *stream )
{
   U32 retMask = Parent::packUpdate( con, mask, stream );

   stream->writeFlag( mPassableSides[ FrontSide ] );
   stream->writeFlag( mPassableSides[ BackSide ] );

   return retMask;
}

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

void Portal::unpackUpdate( NetConnection *con, BitStream *stream )
{
   Parent::unpackUpdate( con, stream );

   mPassableSides[ FrontSide ] = stream->readFlag();
   mPassableSides[ BackSide ] = stream->readFlag();
}

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

void Portal::_renderObject( ObjectRenderInst *ri, SceneRenderState *state, BaseMatInstance* overrideMat )
{
   if( overrideMat )
      return;

   // Update geometry if necessary.

   if( mIsGeometryDirty )
      _updateGeometry();

   // Render portal polygon.

   GFXStateBlockDesc desc;
   desc.setBlend( true );
   desc.setZReadWrite( true, false );
   desc.setCullMode( GFXCullNone );

   PlaneF::Side viewSide = mPortalPlane.whichSide( state->getCameraPosition() );

   ColorI color;
   switch( mClassification )
   {
      case InvalidPortal:  color = ColorI( 255, 255, 255, 45 ); break;
      case ExteriorPortal: color = viewSide == PlaneF::Front ? ColorI( 0, 128, 128, 45 ) : ColorI( 0, 255, 255, 45 ); break;
      case InteriorPortal: color = viewSide == PlaneF::Front ? ColorI( 128, 128, 0, 45 ) : ColorI( 255, 255, 0, 45 ); break;
   }

   GFX->getDrawUtil()->drawPolygon( desc, mPortalPolygonWS.address(), mPortalPolygonWS.size(), color );

   desc.setFillModeWireframe();
   GFX->getDrawUtil()->drawPolygon( desc, mPortalPolygonWS.address(), mPortalPolygonWS.size(), ColorI::RED );

   // Render rest.

   Parent::_renderObject( ri, state, overrideMat );
}

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

void Portal::traverseZones( SceneTraversalState* state, U32 startZoneId )
{
   // Check whether the portal is occluded.

   if( state->getTraversalDepth() > 0 &&
       state->getCullingState()->isOccluded( this ) )
      return;

   Parent::traverseZones( state, startZoneId );
}

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

void Portal::_traverseConnectedZoneSpaces( SceneTraversalState* state )
{
   PROFILE_SCOPE( Portal_traverseConnectedZoneSpaces );

   // Don't traverse out from the portal if it is invalid.

   if( mClassification == InvalidPortal )
      return;

   AssertFatal( !mIsGeometryDirty, "Portal::_traverseConnectedZoneSpaces - Geometry not up-to-date!" );

   // When starting traversal within a portal zone, we cannot really use the portal
   // plane itself to direct our visibility queries.  For example, the camera might
   // actually be located in front of the portal plane and thus cannot actually look
   // through the portal, though it will still see what lies on front of where the
   // portal leads.
   //
   // So if we're the start of the traversal chain, i.e. the traversal has started
   // out in the portal zone, then just put the traversal through to SceneZoneSpace
   // so it can hand it over to all connected zone managers.
   //
   // Otherwise, just do a normal traversal by stepping through the portal.

   if( state->getTraversalDepth() == 1 )
   {
      Parent::_traverseConnectedZoneSpaces( state );
      return;
   }
   
   SceneCullingState* cullingState = state->getCullingState();
   const SceneCameraState& cameraState = cullingState->getCameraState();

   // Get the data of the zone we're coming from.  Note that at this point
   // the portal zone itself is already on top of the traversal stack, so
   // we skip over the bottom-most entry.

   const U32 sourceZoneId = state->getZoneIdFromStack( 1 );
   const SceneZoneSpace* sourceZoneSpace = state->getZoneFromStack( 1 );

   // Find out which side of the portal we are on given the
   // source zone.

   const Portal::Side currentZoneSide =
      sourceZoneId == SceneZoneSpaceManager::RootZoneId
      ? ( getInteriorSideOfExteriorPortal() == FrontSide ? BackSide : FrontSide )
      : getSideRelativeToPortalPlane( sourceZoneSpace->getPosition() );

   // Don't step through portal if the side we're interested in isn't passable.

   if( !isSidePassable( currentZoneSide ) )
      return;

   // If the viewpoint isn't on the same side of the portal as the source zone,
   // then stepping through the portal would mean we are stepping back towards
   // the viewpoint which doesn't make sense; so, skip the portal.

   const Point3F& viewPos = cameraState.getViewPosition();
   const F32 viewPosDistToPortal = mFabs( getPortalPlane().distToPlane( viewPos ) );
   if( !mIsZero( viewPosDistToPortal ) && getSideRelativeToPortalPlane( viewPos ) != currentZoneSide )
      return;

   // Before we go ahead and do the real work, try to find out whether
   // the portal is at a perpendicular or near-perpendicular angle to the view
   // direction.  If so, there's no point in going further since we can't really
   // see much through the portal anyway.  It also prevents us from stepping
   // over front/back side ambiguities.

   Point3F viewDirection = cameraState.getViewDirection();
   const F32 dotProduct = mDot( viewDirection, getPortalPlane() );
   if( mIsZero( dotProduct ) )
      return;

   // Finally, if we have come through a portal to the current zone, check if the target
   // portal we are trying to step through now completely lies on the "backside"--i.e.
   // the side of portal on which our current zone lies--of the source portal.  If so,
   // we can be sure this portal leads us in the wrong direction.  This prevents the
   // outdoor zone from having just arrived through a window on one side of a house just
   // to go round the house and re-enter it from the other side.

   Portal* sourcePortal = state->getTraversalDepth() > 2 ? dynamic_cast< Portal* >( state->getZoneFromStack( 2 ) ) : NULL;
   if( sourcePortal != NULL )
   {
      const Side sourcePortalFrontSide =
         sourceZoneId == SceneZoneSpaceManager::RootZoneId
         ? sourcePortal->getInteriorSideOfExteriorPortal()
         : sourcePortal->getSideRelativeToPortalPlane( sourceZoneSpace->getPosition() );
      const PlaneF::Side sourcePortalPlaneFrontSide =
         sourcePortalFrontSide == FrontSide ? PlaneF::Front : PlaneF::Back;

      bool allPortalVerticesOnBackside = true;
      const U32 numVertices = mPortalPolygonWS.size();
      for( U32 i = 0; i < numVertices; ++ i )
      {
         // Not using getSideRelativeToPortalPlane here since we want PlaneF::On to be
         // counted as backside here.
         if( sourcePortal->mPortalPlane.whichSide( mPortalPolygonWS[ i ] ) == sourcePortalPlaneFrontSide )
         {
            allPortalVerticesOnBackside = false;
            break;
         }
      }

      if( allPortalVerticesOnBackside )
         return;
   }

   // If we come from the outdoor zone, then we don't want to step through any portal
   // where the interior zones are actually on the same side as our camera since that
   // would mean we are stepping into an interior through the backside of a portal.

   if( sourceZoneId == SceneZoneSpaceManager::RootZoneId )
   {
      const Portal::Side cameraSide = getSideRelativeToPortalPlane( viewPos );
      if( cameraSide == getInteriorSideOfExteriorPortal() )
         return;
   }

   // Clip the current culling volume against the portal's polygon.  If the polygon
   // lies completely outside the volume or for some other reason there's no good resulting
   // volume, _generateCullingVolume() will return false and we terminate this portal sequence
   // here.
   //
   // However, don't attempt to clip the portal if we are standing really close to or
   // even below the near distance away from the portal plane.  In that case, trying to
   // clip the portal will only result in trouble so we stick to the original culling volume
   // in that case.

   bool haveClipped = false;
   if( viewPosDistToPortal > ( cameraState.getFrustum().getNearDist() + 0.1f ) )
   {
      SceneCullingVolume volume;
      if( !_generateCullingVolume( state, volume ) )
         return;

      state->pushCullingVolume( volume );
      haveClipped = true;
   }

   // Short-circuit things if we are stepping from an interior zone outside.  In this
   // case we know that the only zone we care about is the outdoor zone so head straight
   // into it.

   if( isExteriorPortal() && sourceZoneId != SceneZoneSpaceManager::RootZoneId )
      getSceneManager()->getZoneManager()->getRootZone()->traverseZones( state );
   else
   {
      // Go through the zones that the portal connects to and
      // traverse into them.

      for( ZoneSpaceRef* ref = mConnectedZoneSpaces; ref != NULL; ref = ref->mNext )
      {
         SceneZoneSpace* targetSpace = ref->mZoneSpace;
         if( targetSpace == sourceZoneSpace )
            continue; // Skip space we originated from.

         // We have handled the case of stepping into the outdoor zone above and
         // by skipping the zone we originated from, we have implicitly handled the
         // case of stepping out of the outdoor zone.  Thus, we should not see the
         // outdoor zone here.  Important as getPosition() is meaningless for it.
         AssertFatal( targetSpace->getZoneRangeStart() != SceneZoneSpaceManager::RootZoneId,
            "Portal::_traverseConnectedZoneSpaces - Outdoor zone must have been handled already" );

         // Skip zones that lie on the same side as the zone
         // we originated from.

         if( getSideRelativeToPortalPlane( targetSpace->getPosition() ) == currentZoneSide )
            continue;

         // Traverse into the space.

         targetSpace->traverseZones( state );
      }
   }

   // If we have pushed our own clipping volume,
   // remove that from the stack now.

   if( haveClipped )
      state->popCullingVolume();
}

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

bool Portal::_generateCullingVolume( SceneTraversalState* state, SceneCullingVolume& outVolume ) const
{
   PROFILE_SCOPE( Portal_generateCullingVolume );

   SceneCullingState* cullingState = state->getCullingState();
   const SceneCullingVolume& currentVolume = state->getCurrentCullingVolume();

   // Clip the portal polygon against the current culling volume.

   Point3F vertices[ 64 ];
   U32 numVertices = 0;

   numVertices = currentVolume.getPlanes().clipPolygon(
      mPortalPolygonWS.address(),
      mPortalPolygonWS.size(),
      vertices,
      sizeof( vertices ) /sizeof( vertices[ 0 ] )
   );

   AssertFatal( numVertices == 0 || numVertices >= 3,
      "Portal::_generateCullingVolume - Clipping produced degenerate polygon" );

   if( !numVertices )
      return false;

   // Create a culling volume.

   return cullingState->createCullingVolume(
      vertices, numVertices,
      SceneCullingVolume::Includer,
      outVolume
   );
}

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

void Portal::connectZoneSpace( SceneZoneSpace* zoneSpace )
{
   Parent::connectZoneSpace( zoneSpace );

   // Update portal state.  Unfortunately, we can't do that on demand
   // easily since everything must be in place before a traversal starts.

   _update();
}

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

void Portal::disconnectZoneSpace( SceneZoneSpace* zoneSpace )
{
   Parent::disconnectZoneSpace( zoneSpace );

   // Update portal state.

   _update();
}

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

void Portal::_disconnectAllZoneSpaces()
{
   Parent::_disconnectAllZoneSpaces();

   // Update portal state.

   _update();
}

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

void Portal::_update()
{
   if( mIsGeometryDirty )
      _updateGeometry();

   _updateConnectivity();
}

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

void Portal::_updateGeometry()
{
   const F32 boxHalfWidth = getScale().x * 0.5f;
   const F32 boxHalfHeight = getScale().z * 0.5f;

   const Point3F center = getTransform().getPosition();
   const Point3F up = getTransform().getUpVector() * boxHalfHeight;
   const Point3F right = getTransform().getRightVector() * boxHalfWidth;

   // Update the portal polygon and plane.

   if( mIsBox )
   {
      // It's a box so the portal polygon is a rectangle.
      // Simply compute the corner points by stepping from the
      // center to the corners using the up and right vector.

      mPortalPolygonWS.setSize( 4 );

      mPortalPolygonWS[ 0 ] = center + right - up; // bottom right
      mPortalPolygonWS[ 1 ] = center - right - up; // bottom left
      mPortalPolygonWS[ 2 ] = center - right + up; // top left
      mPortalPolygonWS[ 3 ] = center + right + up; // top right

      // Update the plane by going through three of the points.

      mPortalPlane = PlaneF(
         mPortalPolygonWS[ 0 ],
         mPortalPolygonWS[ 1 ],
         mPortalPolygonWS[ 2 ]
      );
   }
   else
   {
      // It's not necessarily a box so we must use the general
      // routine.

      // Update the portal plane by building a plane that cuts the
      // OBB in half vertically along its Y axis.

      mPortalPlane = PlaneF(
         center + right - up,
         center - right - up,
         center - right + up
      );

      // Slice the polyhedron along the same plane in object space.

      const PlaneF slicePlane = PlaneF( Point3F::Zero, Point3F( 0.f, 1.f, 0.f ) );

      mPortalPolygonWS.setSize( mPolyhedron.getNumEdges() );
      U32 numPoints = mPolyhedron.constructIntersection( slicePlane, mPortalPolygonWS.address(), mPortalPolygonWS.size() );
      mPortalPolygonWS.setSize( numPoints );

      // Transform the polygon to world space.

      for( U32 i = 0; i < numPoints; ++ i )
      {
         mPortalPolygonWS[ i ].convolve( getScale() );
         mObjToWorld.mulP( mPortalPolygonWS[ i ] );
      }
   }

   mIsGeometryDirty = false;
}

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

void Portal::_updateConnectivity()
{
   SceneZoneSpaceManager* zoneManager = getSceneManager()->getZoneManager();
   if( !zoneManager )
      return;

   // Find out where our connected zones are in respect to the portal
   // plane.

   bool haveInteriorZonesOnFrontSide = false;
   bool haveInteriorZonesOnBackSide = false;
   bool isConnectedToRootZone = ( mClassification == ExteriorPortal );

   for(  ZoneSpaceRef* ref = mConnectedZoneSpaces;
         ref != NULL; ref = ref->mNext )
   {
      SceneZoneSpace* zone = dynamic_cast< SceneZoneSpace* >( ref->mZoneSpace );
      if( !zone || zone->isRootZone() )
         continue;

      if( getSideRelativeToPortalPlane( zone->getPosition() ) == FrontSide )
         haveInteriorZonesOnFrontSide = true;
      else
         haveInteriorZonesOnBackSide = true;
   }

   // If we have zones connected to us on only one side, we are an exterior
   // portal.  Otherwise, we're an interior portal.

   SceneRootZone* rootZone = zoneManager->getRootZone();
   if( haveInteriorZonesOnFrontSide && haveInteriorZonesOnBackSide )
   {
      mClassification = InteriorPortal;
   }
   else if( haveInteriorZonesOnFrontSide || haveInteriorZonesOnBackSide )
   {
      mClassification = ExteriorPortal;

      // Remember where our interior zones are.

      if( haveInteriorZonesOnBackSide )
         mInteriorSide = BackSide;
      else
         mInteriorSide = FrontSide;

      // If we aren't currently connected to the root zone,
      // establish the connection now.

      if( !isConnectedToRootZone )
      {
         Parent::connectZoneSpace( rootZone );
         rootZone->connectZoneSpace( this );
      }
   }
   else
      mClassification = InvalidPortal;

   // If we have been connected to the outdoor zone already but the
   // portal got classified as invalid or interior now, break the
   // connection to the outdoor zone.

   if( isConnectedToRootZone &&
       ( mClassification == InvalidPortal || mClassification == InteriorPortal ) )
   {
      Parent::disconnectZoneSpace( rootZone );
      rootZone->disconnectZoneSpace( this );
   }
}

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

Portal::Side Portal::getSideRelativeToPortalPlane( const Point3F& point ) const
{
   // For our purposes, we consider PlaneF::Front and PlaneF::On
   // placement as FrontSide.

   PlaneF::Side planeSide = getPortalPlane().whichSide( point );
   if( planeSide == PlaneF::Front || planeSide == PlaneF::On )
      return FrontSide;
   else
      return BackSide;
}

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

bool Portal::_setFrontSidePassable( void* object, const char* index, const char* data )
{
   Portal* portal = reinterpret_cast< Portal* >( object );
   portal->setSidePassable( Portal::FrontSide, EngineUnmarshallData< bool >()( data ) );
   return false;
}

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

bool Portal::_setBackSidePassable( void* object, const char* index, const char* data )
{
   Portal* portal = reinterpret_cast< Portal* >( object );
   portal->setSidePassable( Portal::BackSide, EngineUnmarshallData< bool >()( data ) );
   return false;
}

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

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

DefineEngineMethod( Portal, isInteriorPortal, bool, (),,
   "Test whether the portal connects interior zones only.\n\n"
   "@return True if the portal is an interior portal." )
{
   return object->isInteriorPortal();
}

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

DefineEngineMethod( Portal, isExteriorPortal, bool, (),,
   "Test whether the portal connects interior zones to the outdoor zone.\n\n"
   "@return True if the portal is an exterior portal." )
{
   return object->isExteriorPortal();
}
