/*****************************************************************************
 * $Id: eng_billboard.cc,v 1.18 2006/11/25 17:21:53 devel Exp $
 * @file
 * @author	Jim E. Brooks  http://www.jimbrooks.org
 * @brief	Billboard class.
 * @verbatim
 * -----------------------------------------------------------------------------
 * QuadShade:
 * This struct provides info for smooth-shading a quad.
 * It is passed a pointer to Billboard.  BillboardOwner owns the QuadShade
 * and is allowed to modify it at any time.  An example is changing clouds
 * for day or sunset: red/yellow colors are smooth-shaded for sunset.
 * -----------------------------------------------------------------------------
 * @endverbatim
 *//*
 * LEGAL:	COPYRIGHT (C) 2006 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE (GPL).
 *****************************************************************************/

// If true, billboards are rendered opaque to ensure geometry is facing correctly.
//#define DEBUG_BILLBOARD 1  // requires compiling DEBUG=2

#define ENG_BILLBOARD_CC 1
#include "eng.hh"
#include "eng_object.hh"
#include "eng_billboard.hh"
#include "eng_math.hh"
#include "gfx_stream.hh"
#include "gfx_random.hh"

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////  Billboard  //////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

// Default is no smooth-shading (textured quad using replace function, vertex color N/A).
QuadShade Billboard::msQuadShadeNone;

/*****************************************************************************
 * Construct Billboard from new texture images generated (painted) by BillboardOwner.
 *
 * @pre requestedSliceCnt % requestedSliceTextureRatio must be 0 (integral).
 * @param   billboardOwner
 *          This parameter allows a Billboard (esp. its textures)
 *          to be shared by multiple owner Objects.
 * @param   parm
 *
 *          To construct billboard by procedural textures:
 *          ----------------------------------------------
 *          Parm::mSliceCnt:
 *          Requested amount of texture images to be generated.
 *          Not the same as textures objects.
 *
 *          Parm::mSliceTextureRatio:
 *          If requestedSliceCnt=64 and requestedSliceTextureRatio=4, then
 *          every 16 slice images are composited as 1 actual texture object.
 *
 *          Parm::mTextureWidth:
 *          Requested texture width.
 *
 *          To construct billboard by pre-loaded textures:
 *          ----------------------------------------------
 *          Parm::mTextureNames:
 *          Container of names of pre-loaded textures (from .tga files).
 *
 *****************************************************************************/
Billboard::Billboard( BillboardOwner& billboardOwner, const Parm& parm )
{
    // billboardOwner is ephemeral so don't store it (many owners share a billboard).
    // Parm struct may ease future changes to billboard parameters.

    const uint sliceCnt          = parm.GetSliceCnt();
    const uint sliceTextureRatio = parm.GetSliceTextureRatio();
    const uint textureWidth      = parm.GetTextureWidth();

    ASSERT( sliceTextureRatio > 0 );
    ASSERT( sliceTextureRatio <= sliceCnt );  // allow ratio=cnt=1 (single slice)
    ASSERT( (sliceCnt % sliceTextureRatio) == 0 );  // must be integral
    ASSERT( (parm.IfProceduralTextures() && !parm.IfPreloadedTextures())
         || (!parm.IfProceduralTextures() && parm.IfPreloadedTextures()) );

    CLEAR_BIT_FIELD( &mBits );
    mBits.mSliceCnt	    = sliceCnt;
    mBits.mTextureCnt   = sliceCnt / sliceTextureRatio;
    mBits.mTextureWidth = textureWidth;
    mBits.mSingleSlice  = (sliceCnt == 1);

    if ( parm.IfProceduralTextures() )
    {
    // Procedural textures.

        // Create texture objects.
    	for ( uint i = 0; i < mBits.mTextureCnt; ++i )
    	    mTextures.push_back( NewTexture( mBits.mTextureWidth ) );  // idx,cnt,w,texTyp

        // Paint slices.
        // Slices images will be composited, loaded into a less number of textures.
        Paint( billboardOwner );
    }
    else if ( parm.IfPreloadedTextures() )
    {
    // Pre-loaded textures.

        // Convert container of texture names into container of Texture2 objects.
        for ( uint i = 0; i < parm.GetTextureNameCnt(); ++i )
        {
            mTextures.push_back( gEng.GetTexture( parm.GetTextureNameIdx(i) ) );
        }
    }
    else { ASSERT(0); }

ENGINE_STATS_CODE( ++gEng.mStats.mBillboards; )

ASSERT( mBits.mSliceCnt >= MIN_SLICES );
ASSERT( mBits.mSliceCnt <= MAX_SLICES );
ASSERT( mBits.mTextureCnt >= MIN_TEXTURES );
ASSERT( mBits.mTextureCnt <= MAX_TEXTURES );
ASSERT( mBits.mTextureWidth >= MIN_TEXTURE_WIDTH );
ASSERT( mBits.mTextureWidth <= MAX_TEXTURE_WIDTH );
ASSERT( mTextures.size() >= MIN_TEXTURES );
ASSERT( mBits.mTextureWidth == textureWidth );
ASSERT( mBits.mTextureCnt == (sliceCnt / sliceTextureRatio) );
ASSERT( mBits.mSliceCnt == sliceCnt );
ASSERT( (1<<TEXTURE_WIDTH_BITS) > MAX_TEXTURE_WIDTH );
}

/*****************************************************************************
 * Dtor.
 *****************************************************************************/
Billboard::~Billboard()
{
ASSERT( mTextures.size() <= MAX_TEXTURES );  // leaking?

    // mTextures[]: its ownership is complicated, for now, leave allocated.
    // Either Billboard or Engine owns it (if latter, this dtor cannot delete).

    ENGINE_STATS_CODE( --gEng.mStats.mBillboards; )
}

/*****************************************************************************
 * Create new 2D texture object (image not loaded yet).
 * Caller will own it.
 *****************************************************************************/
Texture2*
Billboard::NewTexture( const uint textureWidth )
{
ASSERT( textureWidth >= MIN_TEXTURE_WIDTH );
ASSERT( textureWidth <= MAX_TEXTURE_WIDTH );

    Texture2* texture = new Texture2( TEXEL_TYPE, textureWidth, textureWidth );
	texture->SetMinFilter( Texture::eMinFilter_LINEAR );
    texture->SetMagFilter( Texture::eMagFilter_LINEAR );
    texture->SetFunc( Texture::eFunc_REPLACE );
    return texture;
}

/*****************************************************************************
 * Paint means to compute texture images.
 * A billboard has a greater number of slices than texture objects.
 * The images of multiple slices are composited into one actual texture,
 * for rendering speed and conserving texture resources.
 *****************************************************************************/
void
Billboard::Paint( BillboardOwner& billboardOwner )
{
    const uint textureCnt = GetTextureCnt();
ASSERT( textureCnt >= MIN_TEXTURES );
ASSERT( textureCnt <= MAX_TEXTURES );

    DEBUG_CODE( const uint sliceCnt = GetSliceCnt(); )
ASSERT( sliceCnt >= MIN_SLICES );
ASSERT( sliceCnt <= MAX_SLICES );

    const uint texelCnt = GetTexelCnt();
ASSERT( texelCnt >= MIN_TEXELS );
ASSERT( texelCnt <= MAX_TEXELS );

	const uint imgLen = texelCnt * TEXEL_LEN;
ASSERT( sliceCnt >= MIN_SLICES );
ASSERT( sliceCnt <= MAX_SLICES );

    // Eg if sliceCnt=64 and sliceTextureRatio=4, then textureCnt=16.
    // More slices should exist than textures, since the goal is to
    // composite multiple slices into one texture.
    const uint sliceTextureRatio = GetSliceTextureRatio();
ASSERT( sliceTextureRatio > 0 );
ASSERT( sliceTextureRatio <= sliceCnt );  // allow ratio=cnt=1 (single slice)

    uint textureIdx, sliceIdx;
    for ( textureIdx = sliceIdx = 0;
          textureIdx < textureCnt;
          ++textureIdx, sliceIdx += sliceTextureRatio )  // sliceIdx increments faster than textureIdx
    {
    ASSERT( sliceIdx <= MAX_SLICES );
    ASSERT( textureIdx <= MAX_TEXTURES );

        Paint( billboardOwner,
               textureIdx,
               sliceIdx, sliceIdx + sliceTextureRatio,
               texelCnt, imgLen );
    }
    ASSERT( sliceIdx == sliceCnt );
    ASSERT( textureIdx == textureCnt );
}
// subroutine
void
Billboard::Paint( BillboardOwner& billboardOwner,
                  const uint textureIdx,
                  const uint sliceBegin, const uint sliceEnd, // end as in STL
                  const uint texelCnt, const uint imgLen )
{
ASSERT( textureIdx <= MAX_TEXTURES );
ASSERT( sliceBegin <= MAX_TEXTURES );
ASSERT( sliceEnd <= (MAX_TEXTURES+1) );  // end as in STL

    // Holds temporary texture images.
    const uint TEMP_TEXEL_BUF_LEN = MAX_TEXELS * 2;
    static Billboard::Texel sTempTexelBuf[TEMP_TEXEL_BUF_LEN];
ASSERT( texelCnt < TEMP_TEXEL_BUF_LEN );

DEBUG_CODE( sTempTexelBuf[texelCnt] = 0xdeadbeef; )  // catch buffer overrun

	//--------------------------------------------------------------------------
	GFX_BEGIN_BLEND_IMAGE( GetTextureWidth() );
	{
	    // Start by painting first slice.
        uint sliceIdx = sliceBegin;
		billboardOwner.Paint( *this, sliceIdx, sTempTexelBuf, texelCnt );
		GFX_BLEND_IMAGE( sTempTexelBuf, imgLen );

	    // Paint the rest of the temp slices
        // Blend them into one compositie image.
	    // Blending must be done in back-to-front order.
		for ( sliceIdx = sliceBegin+1; sliceIdx < sliceEnd; ++sliceIdx )  // skip the first painted one
		{
			billboardOwner.Paint( *this, sliceIdx, sTempTexelBuf, texelCnt );
			GFX_BLEND_IMAGE( sTempTexelBuf, imgLen );
		}

		// Get composite image.
		GFX_GET_BLEND_IMAGE( sTempTexelBuf, imgLen );

		// Load composite image into actual texture object.
		mTextures[textureIdx]->SetTextureImage( sTempTexelBuf, texelCnt );
	}
	GFX_END_BLEND_IMAGE();
	//--------------------------------------------------------------------------

DEBUG_CODE( ASSERT( sTempTexelBuf[texelCnt] == 0xdeadbeef ) );
}

/*****************************************************************************
 * Set the threshold when the angle between the viewpoint and billboard
 * changes enough so that the billboard needs to repaint itself.
 *****************************************************************************/
void
SetViewAngleThreshold( Radian rad )
{
UNIMPLEMENTED();
}

/*****************************************************************************
 * In a graphics sense rather C++ sense, make a new instance of a Billboard.
 * MakeInstance() creates a set of BillboardSlice objects relative to the owner.
 * This supports sharing a Billboard (its texture images are expensive to generate).
 *
 * @param   billboardOwner, object
 *          In practice, both args will be passed (seemingly redundantly)
 *          via MakeInstance(*this,*this) for the same C++ object that
 *          has multiple inheritance from Billboard and Object.
 * @param	shade
 *          (see BillboardSlice ctor)
 *****************************************************************************/
void
Billboard::MakeInstance( BillboardOwner& billboardOwner, Object& object, SafePtr<QuadShade> shade )
{
    const uint textureCnt = mTextures.size();
ASSERT( textureCnt >= MIN_TEXTURES );
ASSERT( textureCnt <= MAX_TEXTURES );
    for ( uint textureIdx = 0; textureIdx < textureCnt; ++textureIdx )
	{
        new BillboardSlice( object, *mTextures[textureIdx], GetTextureZ(textureIdx), shade );
	}
}

////////////////////////////////////////////////////////////////////////////////
//////////////////////////////  BillboardSlice  ////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * Ctor.
 * @param   object
 *          BillboardOwner.
 * @param	texture
 * @param	z
 *			In local space.
 * @param	shade
 *          If QuadShade is enabled, implies using texture Modulate function.
 *          Else, implies using texture Replace function.
 *****************************************************************************/
BillboardSlice::BillboardSlice( Object& object, Texture2& texture, const fp z, SafePtr<QuadShade> shade )
:   ObjectData(object.GetQuadrant()),
    mObject(object),
    mTexture(texture),
    mZ(z),
	mShade(shade)
{
    SetRadius( object.GetRadius() );
ASSERT( object.GetRadius() < MAX_OBJECT_RADIUS );
ASSERT( GetRadius() == object.GetRadius() );

ASSERT( z >= BILLBOARD_SLICE_MIN_Z );
ASSERT( z <= BILLBOARD_SLICE_MAX_Z );
}

/*****************************************************************************
 * @return Distance from this slice to viewplane.
 *****************************************************************************/
fp
BillboardSlice::GetDistance( void )
{
	// The usual Object::GetDistance() (it adds radius to distance) is inappropriate.
	// Compute the distance directly from the position [GetDistanceFromPosition()].
	// Then adjust the distance by where the slice is in local space.
    fp distance = Parent::GetDistanceFromPosition();
    distance -= GetZ() * GetRadius();  // add slice's Z offset where Z = {-0.5,..,+0.5}
ASSERT( distance <= MAX_OBJECT_DISTANCE );
    return distance;
}

/*****************************************************************************
 * See long description in eng_billboard.hh.
 *****************************************************************************/
const EyeVertex*
BillboardSlice::GetEyeVertexs( void )
{
	// The billboard is a quad that is exactly parallel to the viewplane.
	// Form the quad's geometry in eye space by adding (+-radius,+-radius)
	// to the Billboard's transformed center, then rotate the 4 vertexs
	// by the same degree the Eye has rolled.

    // Transform Billboard's position (center) from world space to eye space.
	ASSERT2( gWorld.IfInside( GetPosition() ) );
    EyeVertex epos;
    gEng.TransformVertex( epos, GetPosition() );

	// COMPUTE_BILLBOARD_EYE_VERTEX():
    // EV       : EyeVertex result
    // LX,LY    : (x,y) in local space
    // EZ       : z in eye space
    // SIN,COS  : from eye's roll angle
#define COMPUTE_BILLBOARD_EYE_VERTEX( EV, LX, LY, EZ, SIN, COS )    \
{                                                                   \
	fp x2 = (LX)*(COS) - (LY)*(SIN);                                \
	fp y2 = (LX)*(SIN) + (LY)*(COS);                                \
    (EV).Set( epos.x + x2, epos.y + y2, (EZ) );                     \
}
    const fp radius = GetRadius();  // radius of billboard
	ASSERT2( radius > 0  &&  radius < MAX_OBJECT_RADIUS );
    const fp ez = epos.z + GetZ() * radius;  // Z coord in eye space
    fp sine, cosine; SinCos( deg2rad(gEng.mEye.GetRollDeg()), sine, cosine );
    static EyeVertex ev[4];
    COMPUTE_BILLBOARD_EYE_VERTEX( ev[NE], +radius, -radius, ez, sine, cosine );
    COMPUTE_BILLBOARD_EYE_VERTEX( ev[SE], +radius, +radius, ez, sine, cosine );
    COMPUTE_BILLBOARD_EYE_VERTEX( ev[SW], -radius, +radius, ez, sine, cosine );
    COMPUTE_BILLBOARD_EYE_VERTEX( ev[NW], -radius, -radius, ez, sine, cosine );
    return ev;  // return static array (ok)
#undef COMPUTE_BILLBOARD_EYE_VERTEX
}

/*****************************************************************************
 * Draw object.
 *****************************************************************************/
void
BillboardSlice::Draw( void )
{
DEBUG_CODE( OPENGL_CLEAR_ERROR(); )

    // If low LOD, draw only the middle slice.
    const fp z = GetZ();
    if ( ABS(z) > 0.0 && gEng.IfLessLodUser(eLodUser_MID) )
        return;

#if DEBUG_BILLBOARD
GFX_DEBUG_ENABLE_TEXTURE(false);  // render billboards opaque
GFX_RGBA( RGBA(1.0, 1.0, 1.0, 0.75) );
#endif

	// Changing texture state must be by Draw(), not once at ctor,
	// because BillboardOwner may change QuadShade at any time.

	// For smooth-shading, texture color is modulated with colors of the quad's 4 vertexs.
	mTexture.SetFunc( mShade->IfEnabled() ? Texture2::eFunc_MODULATE : Texture2::eFunc_REPLACE );

	// The billboard is either draw as textured quad (replace function)
	// or with smooth shading n which every vertex of the quad has its own color.
	if ( mShade->IfEnabled() ) { GFX_SHADE( eShade_SMOOTH ); }

    const EyeVertex* ev = GetEyeVertexs();
    GFX_BEGIN_BLEND( eDepthTest_DISABLE );
      	GFX_BEGIN( GP_QUADS, mTexture );
		if ( EX( ! mShade->IfEnabled() ) )
		{
								           GFX_MAP_TEXTURE_2D( 0.0,  0.0 ); GFX_VERTEXV( &ev[NE] );
								           GFX_MAP_TEXTURE_2D( 0.0,  1.0 ); GFX_VERTEXV( &ev[SE] );
								           GFX_MAP_TEXTURE_2D( 1.0,  1.0 ); GFX_VERTEXV( &ev[SW] );
								           GFX_MAP_TEXTURE_2D( 1.0,  0.0 ); GFX_VERTEXV( &ev[NW] );
		}
		else
		{
			GFX_RGBA( mShade->GetRgba(0) ); GFX_MAP_TEXTURE_2D( 0.0, 0.0 ); GFX_VERTEXV( &ev[NE] );
            GFX_RGBA( mShade->GetRgba(1) ); GFX_MAP_TEXTURE_2D( 0.0, 1.0 ); GFX_VERTEXV( &ev[SE] );
            GFX_RGBA( mShade->GetRgba(2) ); GFX_MAP_TEXTURE_2D( 1.0, 1.0 ); GFX_VERTEXV( &ev[SW] );
            GFX_RGBA( mShade->GetRgba(3) ); GFX_MAP_TEXTURE_2D( 1.0, 0.0 ); GFX_VERTEXV( &ev[NW] );
		}
      	GFX_END( mTexture );
   	GFX_END_BLEND();

	if ( mShade->IfEnabled() ) { GFX_SHADE( eShade_NORMAL ); }

#if DEBUG_BILLBOARD
GFX_DEBUG_ENABLE_TEXTURE();
#endif

ENGINE_STATS_CODE( ++gEng.mStats.mBillboardSlicesDrawn; )
DEBUG_CODE( OPENGL_CHECK_ERROR(); )
}

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////  Billboard::Parm  ////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * Ctor for procedular textures.
 *****************************************************************************/
Billboard::Parm::Parm( uint sliceCnt, uint sliceTextureRatio, uint textureWidth )
:   mSliceCnt(sliceCnt),
    mSliceTextureRatio(sliceTextureRatio),
    mTextureWidth(textureWidth)
{
    // mTextureNames empty.
    DEBUG_CODE( CheckCtor() );
}

/*****************************************************************************
 * Ctor for pre-loaded textures.
 *****************************************************************************/
Billboard::Parm::Parm( const Parm::TextureNameContainer& textureNames )
{
    // Assumes all texture widths are the same (CheckCtor() will check).
    ASSERT( ! textureNames.empty() );
    mSliceCnt          = textureNames.size();
    mSliceTextureRatio = 1;
    mTextureWidth      = gEng.GetTexture(textureNames[0])->GetWidth();
    mTextureNames      = textureNames;
    DEBUG_CODE( CheckCtor() );
}

/*****************************************************************************
 * Check ctor.
 *****************************************************************************/
#if DEBUG
void
Billboard::Parm::CheckCtor( void )
{
    ASSERT( mSliceCnt >= MIN_SLICES );
    ASSERT( mSliceCnt <= MAX_SLICES );
    ASSERT( mSliceTextureRatio > 0 );
    ASSERT( mSliceTextureRatio < 100 );
    ASSERT( mSliceTextureRatio <= mSliceCnt );  // allow ratio=cnt=1 (single slice)
    ASSERT( (mSliceCnt % mSliceTextureRatio) == 0 );  // must be integral
    ASSERT( (IfProceduralTextures() && !IfPreloadedTextures())
         || (!IfProceduralTextures() && IfPreloadedTextures()) );

    // Ensure all textures have the same width (limitation of Billboard class for simplicity).
    if ( ! mTextureNames.empty() )
    {
        const uint width0 = gEng.GetTexture(mTextureNames[0])->GetWidth();
        for ( uint i = 1; i < mTextureNames.size(); ++i )
        {
            uint width = gEng.GetTexture(mTextureNames[i])->GetWidth();
            ASSERT( width0 == width );
        }
    }
}
#endif // DEBUG
