/*****************************************************************************
The Dark Mod GPL Source Code

This file is part of the The Dark Mod Source Code, originally based
on the Doom 3 GPL Source Code as published in 2011.

The Dark Mod 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. For details, see LICENSE.TXT.

Project: The Dark Mod (http://www.thedarkmod.com/)

******************************************************************************/

#include "precompiled.h"
#pragma hdrstop



#include "Game_local.h"

idCVar g_newSmokeParticles(
	"g_newSmokeParticles", "1", CVAR_BOOL | CVAR_GAME,
	"Which implementation of smoke particles to use:\n"
	"  0 --- old implementation before 2.13\n"
	"  1 --- new implementation in 2.14 and later"
);

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 startTime, const float diversity, const idVec3 &origin, const idMat3 &axis, bool allowCycling ) {
	if ( g_newSmokeParticles.GetBool() ) {
		return EmitSmokeNew( smoke, startTime, diversity, origin, axis, allowCycling );
	} else {
		return EmitSmokeOld( smoke, startTime, diversity, origin, axis );
	}
}

bool idSmokeParticles::EmitSmokeOld( 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( static_cast<int>(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 = static_cast<int>(stage->spawnBunching * stage->cycleMsec);
		int		deltaMsec = gameLocal.time - systemStartTime;

		int		nowCount = 0, 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 = static_cast<int>(floor( ( (float)deltaMsec / finalParticleTime ) * stage->totalParticles ));
			if ( nowCount >= stage->totalParticles ) {
				nowCount = stage->totalParticles-1;
			}
			prevCount = static_cast<int>(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->randomSeed = steppingRandom.GetSeed();
			newSmoke->privateStartTime = systemStartTime + prevCount * finalParticleTime / stage->totalParticles;
			newSmoke->next = active->smokes;
			active->smokes = newSmoke;

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

	return continues;
}

bool idSmokeParticles::EmitSmokeNew( const idDeclParticle *smoke, const int systemStartTime, const float diversity, const idVec3 &origin, const idMat3 &axis, bool allowCycling ) {
	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;
	}

	int numStagesStillActive = 0;

	// 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;
		}

		idPartSysEmit psEmit;
		psEmit.entityParmsTimeOffset = -systemStartTime * 1e-3f;
		psEmit.entityParmsStopTime = 0;	// don't use
		psEmit.totalParticles = stage->totalParticles;
		psEmit.randomizer = stageNum + diversity;

		psEmit.viewTimeMs = gameLocal.previousTime;
		bool isFullyOver;
		int64 prevCount = idParticle_CoundEmitted( *stage, psEmit, isFullyOver );

		psEmit.viewTimeMs = gameLocal.time;
		int64 nowCount = idParticle_CoundEmitted( *stage, psEmit, isFullyOver );

		if ( !allowCycling ) {
			// the old behavior is that particle stages never loop, even if they have cycles = 0
			prevCount = idMath::Imin( prevCount, stage->totalParticles );
			nowCount = idMath::Imin( nowCount, stage->totalParticles );
			if ( nowCount == stage->totalParticles )
				isFullyOver = true;
		}

		if ( prevCount < nowCount ) {

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

			// add all the required particles
			for ( int64 globalIndex = prevCount; globalIndex < nowCount; globalIndex++ ) {
				int index = globalIndex % stage->totalParticles;
				idParticleData part;
				int cycleNumber;
				if ( !idParticle_EmitParticle( *stage, psEmit, index, part, cycleNumber ) ) {
					// sometimes happens when CoundEmitted returns one patricle a bit ahead of time 
					continue;
				}
				// per-particle coordinate system is only filled for deform particles
				// imbue emitters coordinate system into particle
				assert( part.origin == vec3_zero );
				assert( part.axis == mat3_identity );

				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 = index;
				newSmoke->axis = axis;
				newSmoke->origin = origin;
				newSmoke->randomSeed = part.randomSeed;
				newSmoke->privateStartTime = psEmit.viewTimeMs - part.frac * stage->particleLife * 1000;

				newSmoke->next = active->smokes;
				active->smokes = newSmoke;
			}
		}

		if ( !isFullyOver )
			numStagesStillActive++;
	}

	return numStagesStillActive > 0;
}

/*
================
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;

	idPartSysData psys;
	psys.entityAxis = renderEntity->axis;
	memcpy(&psys.entityParmsColor, renderEntity->shaderParms, sizeof(psys.entityParmsColor));
	psys.viewAxis = renderView->viewaxis;

	for ( int activeStageNum = 0; activeStageNum < activeStages.Num(); activeStageNum++ ) {
		activeSmokeStage_t *active = &activeStages[activeStageNum];
		const idParticleStage *stage = active->stage;

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

		psys.totalParticles = stage->totalParticles;

		// allocate a srfTriangles that can hold all the particles
		int count = 0;
		for ( singleSmoke_t *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 ( singleSmoke_t *last = NULL, *smoke = active->smokes, *next; smoke; smoke = next ) {
			next = smoke->next;

			idParticleData part;
			part.frac = (float)( gameLocal.time - smoke->privateStartTime ) / ( stage->particleLife * 1000 );
			if ( part.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;
			}

			part.index = smoke->index;
			part.randomSeed = smoke->randomSeed;

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

			idDrawVert *ptr = tri->verts + tri->numVerts;
			idParticle_CreateParticle(*stage, psys, part, ptr);
			tri->numVerts = ptr - tri->verts;

			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.material = 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;
}
