/*****************************************************************************
 * $Id: mod_pony_cloud_cumulus.cc,v 1.4 2006/11/25 22:34:18 devel Exp $
 * @file
 * @author	Jim E. Brooks  http://www.jimbrooks.org
 * @brief	CumulusCloud class.
 *//*
 * LEGAL:	COPYRIGHT (C) 2006 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE (GPL).
 *****************************************************************************/

#define MOD_PONY_CLOUD_CUMULUS_CC 1
#include "mod_pony_common.hh"
#include "gfx_random.hh"
#include "eng_math.hh"
#include "libnoise.hh"
#include "mod_pony_cloud.hh"
#include "mod_pony_cloud_cumulus.hh"
#include "mod_pony_event.hh"

////////////////////////////////////////////////////////////////////////////////
///////////////////////////////  CumulusCloud  /////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

SafePtr<const CumulusCloud::Shape>      CumulusCloud::msShape;
CloudBillboardMap<CumulusCloud::Shape>  CumulusCloud::msCloudBillboardMap;

// For switching clouds for day or sunset times.
static QuadShade sCumulusCloudShadeQuad;

/*****************************************************************************
 * Ctor/dtor.
 * @param	q
 * @param	pos
 * @param	radius
 * @param	shape
 *			Defines cloud's shape.
 *****************************************************************************/
CumulusCloud::CumulusCloud( QuadrantPtrConst q, const Position& pos, const fp radius, const Shape& shape )
:   Parent(q, pos)
{
	SetRadius( radius );

    // Get Billboard object associated with this cloud.
	msShape = &shape;  // implied arg for Paint(), MakeInstance() will invoke it
	SafePtr<Billboard> billboard = \
     msCloudBillboardMap.GetBillboard( shape,
                                       *this,
                                       Billboard::Parm(SLICES, SLICE_TEXTURE_RATIO, TEXTURE_WIDTH) );

    // Make an instance of billboard.  Billboard is an object factory.
    // It creates a set of BillboardSlice objects for a particular BillboardOwner (this object).
    billboard->MakeInstance( *this, *this, &sCumulusCloudShadeQuad );  // *this aren't redundant, they're passed as different types

	FinalConstruction();
	DEBUG_CODE( CheckObjectCtor(); )
}
CumulusCloud::~CumulusCloud()
{
	DEBUG_CODE( CheckObjectDtor(); )
}

/*****************************************************************************
 * Billboard call Paint() method to generate texture image for this specific object.
 *
 * @remarks
 * Historical note: This algorithm was originally written to generate
 * texture images of a cloud that can properly face any viewpoint.
 * Current CPUs (in 2006) are not fast enough to frequently recompute
 * texture images that stay synchronized with a changing viewpoint.
 *****************************************************************************/
void
CumulusCloud::Paint( Billboard& billboard, const uint sliceIdx,
                     Billboard::Texel* const texels, const uint texelCnt )
{
    const Shape& shape = *msShape;  // implied arg passed by ctor

#if 0
    // (Keep this)
	// Compute matrix in noise/local space that is oriented towards the viewpoint.
    Matrix m;
    IdentityMatrix( m );
    OrientMatrix( m, gEng.mEye.GetPosition() );
    TransposeMatrix( m, gEng.mEye.mCm );
#else
    Matrix m;
    IdentityMatrix( m );
#endif

    // Visual quality is tuned to an exact texture width.
    // If texture width is reduced for speed, this algorithm will adjust.

    const fp texWidthTuned      = CumulusCloud::TEXTURE_WIDTH_TUNED;
    const fp texWidthHalfTuned  = CumulusCloud::TEXTURE_WIDTH_TUNED * 0.5;
    const fp radius             = sqrtf( SQUARE(texWidthHalfTuned) + SQUARE(texWidthHalfTuned) );  // not Object radius
    const fp recip_radius       = 1.0 / radius;
    const uint sliceCnt         = billboard.GetSliceCnt();
	const uint texWidth         = billboard.GetTextureWidth();
ASSERT( sliceIdx >= 0  && sliceCnt <= 128 );  // idx can be 0, cnt should be > 0
ASSERT( sliceCnt  > 0  && sliceCnt <= 128 );
ASSERT( texWidth > 0 && texWidth <= 16384 );

    // Prepare to iterate thru a 3D cube of noise.
    // For speed, before the loop, pre-transform the initial noise coords
    // and increment by fractions per the matrix's rotation
    // (as opposed to transforming the noise coords every iteration).
    //
    // Slice is a 2D quad (-thw,-thw),(thw,thw) at Z in noise space.
    // thw is half the texture width.
    // Z is in range {0,...,tw}.
    //
    // Visualize this as a 2D rectangle (slice) inside a 3D cube of noise.
    // The 2D slice is cut before rotation
    // (if the 2D slices were connected, they fill the cube).
    // This rotates the slice around the cube's center.
    // An offset is added to ensure all noise coords are positive (required by Perlin function).

#define TRANSFORM_CORNER( IDX, X, Y, Z ) \
{ \
    sliceCorners[IDX].x = (X) * m[Xx] + (Y) * m[Xy] + (Z) * m[Xz] + NOISE_OFFSET; \
    sliceCorners[IDX].y = (X) * m[Yx] + (Y) * m[Yy] + (Z) * m[Yz] + NOISE_OFFSET; \
    sliceCorners[IDX].z = (X) * m[Zx] + (Y) * m[Zy] + (Z) * m[Zz] + NOISE_OFFSET; \
}
    const fp NOISE_OFFSET = 10000;  // to prevent negative noise coords
    Vector3 sliceCorners[4];  // transformed corners of cloud slice (in noise space)
    const fp nx = texWidthHalfTuned;
    const fp ny = texWidthHalfTuned;
    const fp nz = texWidthTuned * (fp(sliceIdx) / fp(sliceCnt)) - texWidthTuned/2;  // midpoint
    TRANSFORM_CORNER( NW, -nx, -ny, nz )
    TRANSFORM_CORNER( SW, -nx,  ny, nz )
    TRANSFORM_CORNER( SE,  nx,  ny, nz )
    TRANSFORM_CORNER( NE,  nx, -ny, nz )

    // Compute increments by interpolation.
    // Compute a 3D increment for the noise coords
    // that maps (0,...,textureWidth) to (0,...,1.0).
    // That is, map a 2D texture image coords to a 2D slice in 3D noise space.
    // Interpolate an in-between point, then subtract starting point to turn it into an increment.
    Vector3 noiseVec = sliceCorners[NW];  // start at NW corner
    const fp interpFrac = (1.0 / texWidthTuned) * (texWidthTuned / texWidth);  // interpolation fraction, adjust for actual width
    const Vector3 noiseVecIncX = Interpolate( sliceCorners[NW], sliceCorners[NE], interpFrac) - sliceCorners[NW];
    const Vector3 noiseVecIncY = Interpolate( sliceCorners[NW], sliceCorners[SW], interpFrac) - sliceCorners[NW];
    uint texelIdx = 0;
    DEBUG_CODE( Vector3 noiseVecDebug(0,0,0); )
    for ( uint tx = 0; tx < texWidth; ++tx )  // (tx,ty) are coords in 2D texture image (RGBA array)
    {
        // Increment along the rotated X axis.
        noiseVec += noiseVecIncX;
        Vector3 noiseVecSaved = noiseVec;  // save before inner loop modifies it

        for ( uint ty = 0; ty < texWidth; ++ty )
        {
            ASSERT( texelIdx < texelCnt );

            // Increment along the rotated Y axis.
            noiseVec += noiseVecIncY;

            // Approximate the shape of a cumulus (puffy) cloud as a sphere.
            // Diminish color from center (to make a circular shape).
            // Along the way, apply filter values to add texture to the cloud.
            const fp dist = Distance( noiseVec - NOISE_OFFSET );  // 3D distance in 3D noise (not 2D texture)
            const fp distNorm = MIN( 1.0, dist * recip_radius );

// Check 3D distance calculation.
#if DEBUG
if ( UX(tx == texWidth/2 && ty == texWidth/2) )
{
 if ( sliceIdx == 0 )					// sliceIdx==0 is an edge slice.
// { ASSERT(int(distNorm*100) == 70); }	// ASSERT( FEQ(distNorm,0.707) )
 { ASSERT(int(distNorm*10) == 7); }	// ASSERT( FEQ(distNorm,0.707) )
 else if ( sliceIdx == sliceCnt/2 )		// sliceIdx==sliceCnt/2 is center slice.
 { ASSERT( distNorm < 0.15 ); }
}
#endif
            // Skip 2D points outside cloud's radius.
            if ( EX( distNorm < 0.7 ) )
            {
                // Accumulate color from noise at scaled frequencies.
                // Scaling noise coords is how noise frequency is scaled.
                fp color = noise::PerlinNoise( noiseVec.x * shape.mNoiseFreq,
                                               noiseVec.y * shape.mNoiseFreq,
                                               noiseVec.z * shape.mNoiseFreq );
                for ( uint freqScale = 2; freqScale <= shape.mNoiseFreqScaleLimit; freqScale <<= 1 )
                {
                    const fp sc = freqScale * shape.mNoiseFreq;
                    color += noise::PerlinNoise( noiseVec.x * sc,
                                                 noiseVec.y * sc,
                                                 noiseVec.z * sc ) / freqScale;
                }
                color = (color + 1.0) * 0.5;  // remap {-1,..,+1} to {0,..,+1}

                // Approximate the shape of a cumulous (puffy) cloud as a sphere.
                // Diminish color from center (to make circular).
                const fp oneMinusDistNorm = 1.0 - distNorm * shape.mDistNormFactor;
                color += oneMinusDistNorm;
                color *= oneMinusDistNorm;
              //color = pow(color,4);  // higher powers increase detail/contrast
			    fp color2 = color * color; color *= color2;  // faster than pow(4)
				color *= 4.0;  // expands and brightens
              //const fp alpha = color * 5;  // scaling alpha makes cloud have contrasting dark details
                fp alpha = MIN( 0.3, color );  // limit alpha
                color += shape.mColor;  // or cloud will be too dark
                // Reject dim colors at edge of cloud (refines excise cumulus cloud shape).
                if ( distNorm > 0.6 && alpha < 0.2 ) goto Reject;
                alpha *= shape.mAlpha;
                const uint8 colorByte = ClampFF(color);  // float --> uint8
                texels[texelIdx] = \
                Texture::MakeTexel32( ClampFF(colorByte+20), // B
                                      colorByte,             // G
                                      colorByte,             // R
                                      ClampFF(alpha) );      // A
            }
            else  // 2D point outside cloud's radius
            {
            Reject:
                texels[texelIdx] = (Texture::Texel32)0;
            }

            ++texelIdx;  // inc by texel (not byte)
        }
        DEBUG_CODE( noiseVecDebug = noiseVec; )
        noiseVec = noiseVecSaved;  // undo inner loop
    }

    ASSERT( texelIdx == texelCnt );  // ensure exactly every texel was written
#if 0
    DEBUG_CODE( ASSERT( (int(noiseVecDebug.x)>>1) == (int(sliceCorners[SE].x)>>1) ); )  // loop should have incremented
    DEBUG_CODE( ASSERT( (int(noiseVecDebug.y)>>1) == (int(sliceCorners[SE].y)>>1) ); )  // noiseVec from NW to SE corner
    DEBUG_CODE( ASSERT( (int(noiseVecDebug.z)>>1) == (int(sliceCorners[SE].z)>>1) ); )
#endif
} 

/*****************************************************************************
 * Listen for weather events.
 *****************************************************************************/
void
Listener_CumulusCloud_Weather( void )
{
	// Change color of cumulus clouds if day or sunset.
	if ( gModule.mWeather.IfSunsetOrSunrise() )
	{
		// This affects the BillboardSlice quad.
		// It will be drawn smooth-shaded in red/yellow colors.
		sCumulusCloudShadeQuad.Enable( true );
		sCumulusCloudShadeQuad.SetRgba( 0, RGBA(1.00, 1.00, 0.50, 0.40) );
		sCumulusCloudShadeQuad.SetRgba( 1, RGBA(1.00, 0.00, 0.00, 0.50) );
		sCumulusCloudShadeQuad.SetRgba( 2, RGBA(1.00, 0.00, 0.10, 0.50) );
		sCumulusCloudShadeQuad.SetRgba( 3, RGBA(1.00, 1.00, 0.00, 0.40) );
	}
	else
	{
		// Draw the usual white cloud (noon).
		sCumulusCloudShadeQuad.Enable( false );
	}
}
