/*****************************************************************************
 * $LastChangedDate: 2011-04-23 21:07:07 -0400 (Sat, 23 Apr 2011) $
 * @file
 * @author  Jim E. Brooks  http://www.palomino3d.org
 * @brief   Planet class is a Facade over ossimplanet or one GIS image.
 * @verbatim
 *
 * Degrees vs. radians:
 * --------------------
 * osg::Ellipsoid is based on RADIANS.
 * SphereVertex is based on Degrees.
 * Relevant OSSIM methods are based on DEGREES.
 *
 * Latitude and longitude:
 * -----------------------
 * Latitude is in range {-90,..+90} degrees.
 * Longitude is in range {-180,..,+180} degrees.
 *
 * @endverbatim
 *//*
 * LEGAL:   COPYRIGHT (C) 2008 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE VERSION 2 (GPL 2).
 *****************************************************************************/

#if COMPILE_OSSIM

#define WORLD_PLANET_OSSIM_CC 1
#define NEED_PLANET_CLASS 1
#define WORLD_OSSIM_VERBOSE 1
#define COMPILE_ALTITUDE_MSL 1

#ifndef OSGPLANET_WITHOUT_WMS
#include <wms/wms.h>
#endif
#include <ossim/ossimVersion.h>
#define PAL_OSSIM_VERSION (OSSIM_MAJOR_VERSION_NUMBER * 10000 + OSSIM_MINOR_VERSION_NUMBER * 100 + OSSIM_RELEASE_NUMBER)
#if PAL_OSSIM_VERSION < (1*10000 + 7*100 + 16)
// OSSIM 1.7.16 was the current version in 2009/06.
#error "Requires OSSIM 1.7.16 or newer"
#endif
#include <ossim/base/ossimArgumentParser.h>
#include <ossim/init/ossimInit.h>
#include <ossimPlanet/ossimPlanet.h>
#include <ossimPlanet/ossimPlanetLand.h>
#include <ossimPlanet/ossimPlanetTextureLayerRegistry.h>

#include "base/module.hh"
#include "base/conf.hh"
#include "base/file.hh"
#include "base/stream.hh"
using namespace base;
#include "glue/module.hh"
using namespace glue;
#include "math/module.hh"
#include "math/funcs.hh"
#include "math/funcs_vector.hh"
using namespace math;
#include "object/module.hh"
using namespace object;
#include "gfx/module.hh"
#include "gfx/rgb_colors.hh"
using namespace gfx;
#include "graph/module.hh"
#include "graph/scene_graph.hh"
using namespace graph;
#include "world/module.hh"
#include "world/defs.hh"
#include "world/conf.hh"
#include "world/planet.hh"
#include "world/planet_ossim.hh"

#include <osgDB/ReadFile>

namespace world {

// -----------------------------------------------------------------------------
// Definitions:
namespace defs {

const string        OSSIM_CONF_FILENAME                 = "ossim.kwl";
const fp            RECOMPUTE_TANGENT_MATRIX_THRESHOLD  = 0.01f;  // (lat,lon)
const ossim_uint32  DEFAULT_OSSIM_LEVEL_DETAIL          = 16;
const bool          DEFAULT_OSSIM_ELEV_ENABLED          = true;
const fpx           DEFAULT_OSSIM_ELEV_SCALE            = 1.0;
const bool          DEFAULT_OSSIM_MIPMAPPING_FLAG       = true;

} // namespace defs
// -----------------------------------------------------------------------------

////////////////////////////////////////////////////////////////////////////////
///////////////////////////////  functions  ////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * Assign a numeric var from kwl definition.
 * Var left unchanged if keyword is undefined.
 *****************************************************************************/
template<typename NUMBER_TYPE>
void ASSIGN_KWL_NUMBER( ossimKeywordlist& kwl, const char* keyword, NUMBER_TYPE& var )
{
    const char* str = kwl.find( keyword );
    if ( EX( str != NULL ) )
    {
        var = NUMBER_TYPE(atof(str));
#if WORLD_OSSIM_VERBOSE
        CLOG << "OSSIM kwl[" << keyword << "] = " << var << std::endl;
#endif
    }
    // else unchanged
}

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////  OssimPlanet  ////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * ctor/dtor.
 *****************************************************************************/
OssimPlanet::OssimPlanet( void )
:   mRootObject(NULL),
    mOssimPlanet(new ossimPlanet),
    mOssimModel(mOssimPlanet->model()),  // ORDER-DEPENDENT
    mTangentMatrix(),
    mMeterSimRatio(0.0),
    mElevationScale(world::defs::DEFAULT_OSSIM_ELEV_SCALE)
{
#if WORLD_OSSIM_VERBOSE
    CLOG << "OSSIM " << OSSIM_VERSION << std::endl;
#endif

#ifndef OSGPLANET_WITHOUT_WMS
    wmsInitialize();
#endif

    // Initialize OSSIM.
    int argc = 1; PERSISTENT char dummyArgv0[64]; char* argv[] = { dummyArgv0 };  // nil
    ossimArgumentParser argumentParser( &argc, argv );
    osg::ArgumentParser arguments( &argumentParser.argc(), argumentParser.argv() );
    ossimInit::instance()->initialize( argumentParser );

    // .........................................................................
    // Load OSSIM configuration from kwl (Key Word List) file.

    ossim_uint32  confLevelDetail      = world::defs::DEFAULT_OSSIM_LEVEL_DETAIL;  // LOD
    bool          confElevEnabled      = world::defs::DEFAULT_OSSIM_ELEV_ENABLED;
    bool          confMipMappingFlag   = world::defs::DEFAULT_OSSIM_MIPMAPPING_FLAG;

    ossimKeywordlist kwl;
    const string kwlPathname = string(base::GetDataDir())
                             + string("/")
                             + string(world::defs::OSSIM_CONF_FILENAME);
    kwl.addFile( kwlPathname.c_str() );

    // Keyword "elevation_scale":
    ASSIGN_KWL_NUMBER( kwl, "elevation_scale", mElevationScale );

    // Keyword "level_detail" (LOD):
    ASSIGN_KWL_NUMBER( kwl, "level_detail", confLevelDetail );

    // Change paths.
    const char* archive0_type = kwl.find( "archive0.type" );  // archive #0
    if ( archive0_type != NULL )
    {
        ChangeKeywordRelativePath( kwl, "archive0.file0" );
        ChangeKeywordRelativePath( kwl, "archive0.file1" );
    }
    const char* archive1_type = kwl.find( "archive1.type" );  // archive #1
    if ( archive1_type != NULL )
    {
        ChangeKeywordRelativePath( kwl, "archive1.file0" );
        ChangeKeywordRelativePath( kwl, "archive1.file1" );
    }

    // .........................................................................
    // Apply configuration to OssimPlanet object.

    mOssimPlanet->setupDefaults();
    mOssimPlanet->land()->setHeightExag( mElevationScale );  // "exag"
    mOssimPlanet->land()->setMaxLevelDetail( confLevelDetail );
    mOssimPlanet->land()->setElevationEnabledFlag( confElevEnabled );
    mOssimPlanet->land()->setMipMappingFlag( confMipMappingFlag );

    // Test directory containing OSSIM configuration.
    // Also set cache dir in keyword list.
    const string ossimDataDir = FindOssimDataDir();
#if WORLD_OSSIM_VERBOSE
    CLOG << "OSSIM data dir: " << ossimDataDir << std::endl;
#endif
    const string ossimCacheDir = FindOrMakeCacheDir();
    kwl.add( "archive0.cache_dir", ossimCacheDir.c_str(), true );  // true:overwrite

    // Texture layer.
    RefPtr<ossimPlanetTextureLayer> layer = ossimPlanetTextureLayerRegistry::instance()->createLayer( kwl.toString() );
    if ( layer.valid() )
        mOssimPlanet->land()->referenceLayer()->addTop( layer.get() );

    //--------------------------------------------------------------------------
    // Construct 3D object.

    // Place planet under a transform node.
    Matrix matrix = mOssimPlanet->getMatrix();
    RefPtr<osg::MatrixTransform> transformNode = new TransformNode;
    transformNode->setMatrix( matrix );
    transformNode->addChild( mOssimPlanet.get() );

    // Create Object to encapsulate ossimplanet.
    mRootObject = new Object( new Graph( transformNode.get(), "ossim_planet" ),
                              WorldVertex(0,0,0) );

    // Planet is a collidable object.
    mRootObject->SetCollidable( true );

    //--------------------------------------------------------------------------
    // Compute spatial info.

    // Pre-compute meter/sim ratio.
    RecomputeMeterSimRatio();

#if DEBUG
    // Test conversions.
    DebugSelfChecks();
#endif

    // Set background color.
    GET_SCENE_GRAPH().SetBackgroundColor( RGBA( RGB_COSMOS_BLUE, 1.0f ) );
}

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

/*****************************************************************************
 * [private] Re-compute the meter/sim ratio.
 *****************************************************************************/
void
OssimPlanet::RecomputeMeterSimRatio( void )
{
    // Pre-compute meter/sim ratio.
    const fpx EARTH_AVG_RADIUS_METERS = 6378100.0L;
    mMeterSimRatio = fpx(EARTH_AVG_RADIUS_METERS) / fpx(RadiusGraphical());
}

/*****************************************************************************
 * Debug.
 *****************************************************************************/
#if DEBUG
void
OssimPlanet::DebugSelfChecks( void )
{
    // Test roundtrip coordinate conversion (input == output == input).
    const fp altMargin = 50.0;  // margin in meters to tolerate inaccuracy in conversion
    #define TEST_CONVERSION( LAT, LON, ALT )                                        \
    {                                                                               \
        const SphereVertex debugSV( Degree(LAT), Degree(LON), Meter(ALT) );         \
        const WorldVertex  debugWV = SphereVertex2WorldVertex( debugSV );           \
        const SphereVertex debugSV2 = WorldVertex2SphereVertex( debugWV );          \
      /*CERROR << debugSV << std::endl;*/                                           \
      /*CERROR << debugWV << std::endl;*/                                           \
      /*CERROR << debugSV2 << std::endl;*/                                          \
        ASSERT( FP_EQ<fp>( debugSV.mLat, debugSV2.mLat ) );                         \
        ASSERT( FP_EQ<fp>( debugSV.mLon, debugSV2.mLon ) );                         \
        ASSERT( ABS(debugSV.mAlt.FP() - debugSV2.mAlt.FP()) < altMargin );          \
    }
    // Note:
    // Latitude is in range {-90,..+90} degrees.
    // Longitude is in range {-180,..,+180} degrees.
    TEST_CONVERSION( 30.0, 55.0, 0.0 );           TEST_CONVERSION( 60.0, 55.0, 0.0 );
    TEST_CONVERSION( 30.0, -55.0, 1000.0 );       TEST_CONVERSION( -60.0, 55.0, 1000.0 );
    TEST_CONVERSION( -60.0, 179.0, 2000.0 );      TEST_CONVERSION( 60.0, 1.0, 2000.0 );
    TEST_CONVERSION( 0.0, 0.0, 0.0 );             TEST_CONVERSION( 0.0, 180.0, 0.0 );
//  TEST_CONVERSION( 0.0, 270.0, 0.0 );           TEST_CONVERSION( 0.0, 359.0, 0.0 );  // lon is wrong (bad test)
    TEST_CONVERSION( 0.0, -90.0, 0.0 );           TEST_CONVERSION( 0.0, -179.0, 0.0 );
    TEST_CONVERSION( 45.3383, -121.9845, 500.0 ); TEST_CONVERSION( 45.3383, 121.9845, 500.0 );
}
#endif // DEBUG

/*****************************************************************************
 * @return The planet's node.
 *****************************************************************************/
shptr<Object>
OssimPlanet::GetRootObject( void )
{
ASSERT( mRootObject != NULL );  // ctor makes it
    return mRootObject.PTR();
}

/*****************************************************************************
 * Compute a matrix that is tangent to a point on the world.
 *****************************************************************************/
const Matrix&
OssimPlanet::GetTangentMatrix( const SphereVertex& sv )
{
    // Cache the previously computed matrix.
    PERSISTENT Radian sPrevLat = Radian(999.0f);
    PERSISTENT Radian sPrevLon = Radian(999.0f);

    if ( (ABS(sPrevLat - sv.mLat) > RECOMPUTE_TANGENT_MATRIX_THRESHOLD)
      or (ABS(sPrevLon - sv.mLon) > RECOMPUTE_TANGENT_MATRIX_THRESHOLD) )
    {
        sPrevLat = sv.mLat;
        sPrevLon = sv.mLon;
        osg::Matrixd lsrMatrix;
        mOssimModel->lsrMatrix( sv.ToVec3d(), lsrMatrix );
        mTangentMatrix = lsrMatrix;
        mTangentMatrix.setTrans( 0.0f, 0.0f, 0.0f );
    }

    return mTangentMatrix;
}

/*****************************************************************************
 * Compute a normalized "up vector" extending from a point on the world.
 *****************************************************************************/
WorldVertex
OssimPlanet::ComputeUpVector( const SphereVertex& sv )
{
    // Here are 4 ways to compute it.
    // The one that seems the wrongest is the right one ;-)
#if 1
    // Normalize matrix axis since matrix may be scaled.
    const Matrix& m = GetTangentMatrix(sv);
//  return Normalize<WorldVertex,fp>( m[Zx], m[Zy], m[Zz] );
    return Normalize<WorldVertex,fp>( m[Xz], m[Yz], m[Zz] );  // bingo
#else
    // Transform a normalized "up vector" thru the tangent matrix.
//  return WorldVertex( 0.0f, 0.0f, 1.0f ) * GetTangentMatrix(sv);
    return GetTangentMatrix(sv) * WorldVertex( 0.0f, 0.0f, 1.0f );
#endif
}

/*****************************************************************************
 * Convert spherical coordinates to a 3D vertex.
 *****************************************************************************/
WorldVertex
OssimPlanet::SphereVertex2WorldVertex( const SphereVertex& sv )
{
    // Convert to 3D vertex.
    // Compensate for elevation scale.
    osg::Vec3d svRad( sv.mLat,
                      sv.mLon,
                      sv.mAlt.FP() * mElevationScale );
    osg::Vec3d v;
    mOssimModel->forward( svRad, v );  // in,out

#if COMPILE_ALTITUDE_MSL
    // Compute altitude at MSL.
    mOssimModel->mslToEllipsoidal( v );  // out
#endif

    return WorldVertex(v);
}

/*****************************************************************************
 * Convert graphical coordinates to spherical coordinates.
 *****************************************************************************/
SphereVertex
OssimPlanet::WorldVertex2SphereVertex( const WorldVertex& wv )
{
    // Convert 3D vector to raw (lat,lon,alt).
    osg::Vec3d sv;
    mOssimModel->inverse( wv, sv );  // in,out

#if COMPILE_ALTITUDE_MSL
    // Compute altitude at MSL.
    mOssimModel->ellipsoidalToMsl( sv );  // out
#endif

    // Compensate for elevation scale.
    return SphereVertex( Degree(sv[LAT]),
                         Degree(sv[LON]),
                         Meter( sv[ALT] / mElevationScale) );
}

/*****************************************************************************
 * @return meter / graphicCoord
 *****************************************************************************/
fpx
OssimPlanet::MeterSimRatio( void )
{
ASSERT( mMeterSimRatio > 0.000001 );  // ctor should precompute it

    return mMeterSimRatio;
}

/*****************************************************************************
 * Compute the radius of the planet in graphical coordinates.
 *****************************************************************************/
fp
OssimPlanet::RadiusGraphical( void )
{
#if 1
    // In fact.
    return 1.0f;
#else
    // Compute radius from distance of opposite spherical coordinates
    // translated to graphical coordinates.
    const osg::Vec3d sv0(   0.0f, 0.0f, 0.0f );
    const osg::Vec3d sv1( 180.0f, 0.0f, 0.0f );
    osg::Vec3d wv0;
    osg::Vec3d wv1;
    mOssimModel->forward( sv0, wv0 );  // in,out
    mOssimModel->forward( sv1, wv1 );  // in,out
    const fp diameter = Distance( WorldVertex(wv0[XX],wv0[YY],wv0[ZZ]),
                                  WorldVertex(wv1[XX],wv1[YY],wv1[ZZ]) );
    return diameter * 0.5;
#endif
}

/*****************************************************************************
 * @return Volume of world in 3D space.
 *****************************************************************************/
BoxVolume
OssimPlanet::GetVolume( void )
{
    // Radius of OSSIM planet is 1.0.
    return BoxVolume( WorldVertex( -1.0f, -1.0f, -1.0f ),
                      WorldVertex(  1.0f,  1.0f,  1.0f ) );
}

/*****************************************************************************
 * In an OSSIM keyword list (loaded from data/ossim.kwl),
 * if a path is relative (no leading '/' char), prepend the data dir.
 *****************************************************************************/
void
OssimPlanet::ChangeKeywordRelativePath( ossimKeywordlist& kwl, const char key[] )
{
    const char* val = kwl.find( key );
    if ( (val != NULL) and (val[0] != '/' ) )
    {
        const string val2 = string(base::GetDataDir()) + string("/") + string(val);
        kwl.add( key, val2.c_str(), true );  // true:overwrite
        ASSERT( STREQ( kwl.find(key), val2.c_str() ) );  // check new value
    }
}

/*****************************************************************************
 * Find the directory containing OSSIM configuration.  Usually ~/.ossim.
 * @return Directory or THROWS C++ EXCEPTION!
 *****************************************************************************/
string
OssimPlanet::FindOssimDataDir( void )
{
    // Do once.
    PERSISTENT string sDataDir;
    if ( UX( sDataDir.empty() ) )
    {
        // Try OSSIM_HOME env var.
        const char* env_OSSIM_HOME = getenv("OSSIM_HOME");
        if ( env_OSSIM_HOME != NULL )
        {
            if ( IfDirExists( env_OSSIM_HOME ) )
                return (sDataDir = env_OSSIM_HOME);
            else
                throw Exception( "ERROR: OSSIM_HOME env defined but dir doesn't exist" );
        }

        // Try ~/.ossim
        const char* env_HOME = getenv("HOME");
        if ( env_HOME != NULL )
        {
            const string dot_ossim_dir = string(env_HOME) + string("/.ossim");
            if ( IfDirExists( dot_ossim_dir.c_str() ) )
                return (sDataDir = dot_ossim_dir);
        }
        else
        {
            CERROR << "ERROR: HOME env var undefined" << endl;
        }

        // Can't find it!
        throw Exception( "ERROR: cannot find OSSIM data dir (usually ~/.ossim)" );
    }

    return sDataDir;
}

/*****************************************************************************
 * Find or create the cache dir.
 * @return Directory or THROWS EXCEPTION!
 *****************************************************************************/
string
OssimPlanet::FindOrMakeCacheDir( void )
{
    const string ossimDir = FindOssimDataDir();
    const string cacheDir = string(ossimDir) + string("/cache");
    if ( IfDirExists( cacheDir ) )
    {
        return cacheDir;
    }
    else
    {
        if ( not MakeDir( cacheDir ) )
            throw Exception( "ERROR: cannot make OSSIM cache dir" );
        return cacheDir;
    }
}

} // namespace world

#endif // COMPILE_OSSIM
