/*
===========================================================================

Doom 3 GPL Source Code
Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company.

This file is part of the Doom 3 GPL Source Code ("Doom 3 Source Code").

Doom 3 Source Code is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Doom 3 Source Code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Doom 3 Source Code.  If not, see <http://www.gnu.org/licenses/>.

In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code.  If not, please request a copy in writing from id Software at the address below.

If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA.

===========================================================================
*/

#include "sys/platform.h"
#include "renderer/ModelManager.h"
#include "Game_local.h"

#include "SmokeParticles.h"

static const char *smokeParticle_SnapshotName = "_SmokeParticle_Snapshot_";

/*
================
idSmokeParticles::idSmokeParticles
================
*/
idSmokeParticles::idSmokeParticles( void ) {
	initialized = false;
	memset( &renderEntity, 0, sizeof( renderEntity ) );
	renderEntityHandle = -1;
	memset( smokes, 0, sizeof( smokes ) );
	freeSmokes = NULL;
	numActiveSmokes = 0;
	currentParticleTime = -1;
}

/*
================
idSmokeParticles::Init
================
*/
void idSmokeParticles::Init( void ) {
	if ( initialized ) {
		Shutdown();
	}

	// set up the free list
	for ( int i = 0; i < MAX_SMOKE_PARTICLES-1; i++ ) {
		smokes[i].next = &smokes[i+1];
	}
	smokes[MAX_SMOKE_PARTICLES-1].next = NULL;
	freeSmokes = &smokes[0];
	numActiveSmokes = 0;

	activeStages.Clear();

	memset( &renderEntity, 0, sizeof( renderEntity ) );

	renderEntity.bounds.Clear();
	renderEntity.axis = mat3_identity;
	renderEntity.shaderParms[ SHADERPARM_RED ]		= 1;
	renderEntity.shaderParms[ SHADERPARM_GREEN ]	= 1;
	renderEntity.shaderParms[ SHADERPARM_BLUE ]		= 1;
	renderEntity.shaderParms[3] = 1;

	renderEntity.hModel = renderModelManager->AllocModel();
	renderEntity.hModel->InitEmpty( smokeParticle_SnapshotName );

	// we certainly don't want particle shadows
	renderEntity.noShadow = 1;

	// huge bounds, so it will be present in every world area
	renderEntity.bounds.AddPoint( idVec3(-100000, -100000, -100000) );
	renderEntity.bounds.AddPoint( idVec3( 100000,  100000,  100000) );

	renderEntity.callback = idSmokeParticles::ModelCallback;
	// add to renderer list
	renderEntityHandle = gameRenderWorld->AddEntityDef( &renderEntity );

	currentParticleTime = -1;

	initialized = true;
}

/*
================
idSmokeParticles::Shutdown
================
*/
void idSmokeParticles::Shutdown( void ) {
	// make sure the render entity is freed before the model is freed
	if ( renderEntityHandle != -1 ) {
		gameRenderWorld->FreeEntityDef( renderEntityHandle );
		renderEntityHandle = -1;
	}
	if ( renderEntity.hModel != NULL ) {
		renderModelManager->FreeModel( renderEntity.hModel );
		renderEntity.hModel = NULL;
	}
	initialized = false;
}

/*
================
idSmokeParticles::FreeSmokes
================
*/
void idSmokeParticles::FreeSmokes( void ) {
	for ( int activeStageNum = 0; activeStageNum < activeStages.Num(); activeStageNum++ ) {
		singleSmoke_t *smoke, *next, *last;

		activeSmokeStage_t *active = &activeStages[activeStageNum];
		const idParticleStage *stage = active->stage;

		for ( last = NULL, smoke = active->smokes; smoke; smoke = next ) {
			next = smoke->next;

			float frac = (float)( gameLocal.time - smoke->privateStartTime ) / ( stage->particleLife * 1000 );
			if ( frac >= 1.0f ) {
				// remove the particle from the stage list
				if ( last != NULL ) {
					last->next = smoke->next;
				} else {
					active->smokes = smoke->next;
				}
				// put the particle on the free list
				smoke->next = freeSmokes;
				freeSmokes = smoke;
				numActiveSmokes--;
				continue;
			}

			last = smoke;
		}

		if ( !active->smokes ) {
			// remove this from the activeStages list
			activeStages.RemoveIndex( activeStageNum );
			activeStageNum--;
		}
	}
}

/*
================
idSmokeParticles::EmitSmoke

Called by game code to drop another particle into the list
================
*/
bool idSmokeParticles::EmitSmoke( const idDeclParticle *smoke, const int systemStartTime, const float diversity, const idVec3 &origin, const idMat3 &axis ) {
	bool	continues = false;

	if ( !smoke ) {
		return false;
	}

	if ( !gameLocal.isNewFrame ) {
		return false;
	}

	// dedicated doesn't smoke. No UpdateRenderEntity, so they would not be freed
	if ( gameLocal.localClientNum < 0 ) {
		return false;
	}

	assert( gameLocal.time == 0 || systemStartTime <= gameLocal.time );
	if ( systemStartTime > gameLocal.time ) {
		return false;
	}

	idRandom steppingRandom( 0xffff * diversity );

	// for each stage in the smoke that is still emitting particles, emit a new singleSmoke_t
	for ( int stageNum = 0; stageNum < smoke->stages.Num(); stageNum++ ) {
		const idParticleStage *stage = smoke->stages[stageNum];

		if ( !stage->cycleMsec ) {
			continue;
		}

		if ( !stage->material ) {
			continue;
		}

		if ( stage->particleLife <= 0 ) {
			continue;
		}

		// see how many particles we should emit this tic
		// FIXME:			smoke.privateStartTime += stage->timeOffset;
		int		finalParticleTime = stage->cycleMsec * stage->spawnBunching;
		int		deltaMsec = gameLocal.time - systemStartTime;

		int		nowCount, prevCount;
		if ( finalParticleTime == 0 ) {
			// if spawnBunching is 0, they will all come out at once
			if ( gameLocal.time == systemStartTime ) {
				prevCount = -1;
				nowCount = stage->totalParticles-1;
			} else {
				prevCount = stage->totalParticles;
			}
		} else {
			nowCount = floor( ( (float)deltaMsec / finalParticleTime ) * stage->totalParticles );
			if ( nowCount >= stage->totalParticles ) {
				nowCount = stage->totalParticles-1;
			}
			prevCount = floor( ((float)( deltaMsec - USERCMD_MSEC ) / finalParticleTime) * stage->totalParticles );
			if ( prevCount < -1 ) {
				prevCount = -1;
			}
		}

		if ( prevCount >= stage->totalParticles ) {
			// no more particles from this stage
			continue;
		}

		if ( nowCount < stage->totalParticles-1 ) {
			// the system will need to emit particles next frame as well
			continues = true;
		}

		// find an activeSmokeStage that matches this
		activeSmokeStage_t	*active = NULL;
		int i;
		for ( i = 0 ; i < activeStages.Num() ; i++ ) {
			active = &activeStages[i];
			if ( active->stage == stage ) {
				break;
			}
		}
		if ( i == activeStages.Num() ) {
			// add a new one
			activeSmokeStage_t	newActive;

			newActive.smokes = NULL;
			newActive.stage = stage;
			i = activeStages.Append( newActive );
			active = &activeStages[i];
		}

		// add all the required particles
		for ( prevCount++ ; prevCount <= nowCount ; prevCount++ ) {
			if ( !freeSmokes ) {
				gameLocal.Printf( "idSmokeParticles::EmitSmoke: no free smokes with %d active stages\n", activeStages.Num() );
				return true;
			}
			singleSmoke_t	*newSmoke = freeSmokes;
			freeSmokes = freeSmokes->next;
			numActiveSmokes++;

			newSmoke->index = prevCount;
			newSmoke->axis = axis;
			newSmoke->origin = origin;
			newSmoke->random = steppingRandom;
			newSmoke->privateStartTime = systemStartTime + prevCount * finalParticleTime / stage->totalParticles;
			newSmoke->next = active->smokes;
			active->smokes = newSmoke;

			steppingRandom.RandomInt();	// advance the random
		}
	}

	return continues;
}

/*
================
idSmokeParticles::UpdateRenderEntity
================
*/
bool idSmokeParticles::UpdateRenderEntity( renderEntity_s *renderEntity, const renderView_t *renderView ) {

	// FIXME: re-use model surfaces
	renderEntity->hModel->InitEmpty( smokeParticle_SnapshotName );

	// this may be triggered by a model trace or other non-view related source,
	// to which we should look like an empty model
	if ( !renderView ) {
		return false;
	}

	// don't regenerate it if it is current
	if ( renderView->time == currentParticleTime && !renderView->forceUpdate ) {
		return false;
	}
	currentParticleTime = renderView->time;

	particleGen_t g;

	g.renderEnt = renderEntity;
	g.renderView = renderView;

	for ( int activeStageNum = 0; activeStageNum < activeStages.Num(); activeStageNum++ ) {
		singleSmoke_t *smoke, *next, *last;

		activeSmokeStage_t *active = &activeStages[activeStageNum];
		const idParticleStage *stage = active->stage;

		if ( !stage->material ) {
			continue;
		}

		// allocate a srfTriangles that can hold all the particles
		int count = 0;
		for ( smoke = active->smokes; smoke; smoke = smoke->next ) {
			count++;
		}
		int	quads = count * stage->NumQuadsPerParticle();
		srfTriangles_t *tri = renderEntity->hModel->AllocSurfaceTriangles( quads * 4, quads * 6 );
		tri->numIndexes = quads * 6;
		tri->numVerts = quads * 4;

		// just always draw the particles
		tri->bounds[0][0] =
		tri->bounds[0][1] =
		tri->bounds[0][2] = -99999;
		tri->bounds[1][0] =
		tri->bounds[1][1] =
		tri->bounds[1][2] = 99999;

		tri->numVerts = 0;
		for ( last = NULL, smoke = active->smokes; smoke; smoke = next ) {
			next = smoke->next;

			g.frac = (float)( gameLocal.time - smoke->privateStartTime ) / ( stage->particleLife * 1000 );
			if ( g.frac >= 1.0f ) {
				// remove the particle from the stage list
				if ( last != NULL ) {
					last->next = smoke->next;
				} else {
					active->smokes = smoke->next;
				}
				// put the particle on the free list
				smoke->next = freeSmokes;
				freeSmokes = smoke;
				numActiveSmokes--;
				continue;
			}

			g.index = smoke->index;
			g.random = smoke->random;

			g.origin = smoke->origin;
			g.axis = smoke->axis;

			g.originalRandom = g.random;
			g.age = g.frac * stage->particleLife;

			tri->numVerts += stage->CreateParticle( &g, tri->verts + tri->numVerts );

			last = smoke;
		}
		if ( tri->numVerts > quads * 4 ) {
			gameLocal.Error( "idSmokeParticles::UpdateRenderEntity: miscounted verts" );
		}

		if ( tri->numVerts == 0 ) {

			// they were all removed
			renderEntity->hModel->FreeSurfaceTriangles( tri );

			if ( !active->smokes ) {
				// remove this from the activeStages list
				activeStages.RemoveIndex( activeStageNum );
				activeStageNum--;
			}
		} else {
			// build the index list
			int	indexes = 0;
			for ( int i = 0 ; i < tri->numVerts ; i += 4 ) {
				tri->indexes[indexes+0] = i;
				tri->indexes[indexes+1] = i+2;
				tri->indexes[indexes+2] = i+3;
				tri->indexes[indexes+3] = i;
				tri->indexes[indexes+4] = i+3;
				tri->indexes[indexes+5] = i+1;
				indexes += 6;
			}
			tri->numIndexes = indexes;

			modelSurface_t	surf;
			surf.geometry = tri;
			surf.shader = stage->material;
			surf.id = 0;

			renderEntity->hModel->AddSurface( surf );
		}
	}
	return true;
}

/*
================
idSmokeParticles::ModelCallback
================
*/
bool idSmokeParticles::ModelCallback( renderEntity_s *renderEntity, const renderView_t *renderView ) {
	// update the particles
	if ( gameLocal.smokeParticles ) {
		return gameLocal.smokeParticles->UpdateRenderEntity( renderEntity, renderView );
	}

	return true;
}
