#include "l4particles.h"
#include "../munga/time.h"

LPDIRECT3DDEVICE9 ParticleEngine::mDevice = NULL;
PARTICLE_EFFECT ParticleEngine::mInstalledEffects[MAX_PARTICLE_EFFECTS];
LPDIRECT3DTEXTURE9 ParticleEngine::mParticleTexture = NULL;

bool ParticleEngine::mActiveParticles = false;
LPDIRECT3DVERTEXBUFFER9 ParticleEngine::mVertBuffer = NULL;

Particle *ParticleEngine::mParticlesHead = NULL;
Particle *ParticleEngine::mParticlesTail = NULL;
long ParticleEngine::mTotalParticleCount = 0;
long ParticleEngine::mMaxParticleCount = 8192;

int compare(const Particle *a, const Particle *b, const D3DXMATRIX *view_matrix)
{
	D3DXVECTOR3 aVec(a->mX, a->mY, a->mZ);
	D3DXVECTOR3 bVec(b->mX, b->mY, b->mZ);

	D3DXVec3TransformCoord(&aVec, &aVec, view_matrix);
	D3DXVec3TransformCoord(&bVec, &bVec, view_matrix);

	float aDist = aVec.x * aVec.x + aVec.y * aVec.y + aVec.z * aVec.z;
	float bDist = bVec.x * bVec.x + bVec.y * bVec.y + bVec.z * bVec.z;

	// if both particles are within a certain distance from
	// each other then we'll sort them by effect instead of
	// distance so that they will be grouped together when
	// rendering
	//if (abs(bDist - aDist) < EFFECT_GROUPING_EPSILON)
	//	return b->mEffect->id - a->mEffect->id;
	//else
		return (int)(bDist - aDist);
}

void mergesort(Particle **headPtr, Particle **tailPtr, const D3DXMATRIX *view_matrix)
{
	Particle *list = *headPtr;

	if (!list)
	{
		(*tailPtr) = NULL;
		return;
	}

    Particle *p, *q, *e, *tail, *oldhead;
    int insize, nmerges, psize, qsize, i;

	insize = 1;

	while (1)
	{
		p = list;
		oldhead = list;
		list = NULL;
		tail = NULL;

		nmerges = 0;	// count number of merges we do in this pass

		while (p)
		{
			nmerges++;	// there exists a merge to be done
			// step 'insize' places along from p
			q = p;
			psize = 0;
			for (i=0; i<insize; i++)
			{
				psize++;
				q = q->Next();
				if (!q)
					break;
			}

			// if q hasn't fallen off end, we have two lists to merge
			qsize = insize;

			// now we have two lists; merge them
			while (psize > 0 || (qsize > 0 && q))
			{
				// decide whether next element of merge comes from p or q
				if (psize == 0)
				{
					// p is empty; e must come from q
					e = q;
					q = q->Next();
					qsize--;
				}
				else if (qsize == 0 || !q)
				{
					// q is empty; e must come from p
					e = p;
					p = p->Next();
					psize--;
				}
				else if (compare(p, q, view_matrix) <= 0)
				{
					// first elements of p is lower (or same);
					// e must come from p
					e = p;
					p = p->Next();
					psize--;
				}
				else
				{
					// first element of q is lower; e must come from q
					e = q;
					q = q->Next();
					qsize--;
				}

				// add the next element to the merged list
				if (tail)
					tail->mNextParticle = e;
				else
					list = e;
				e->mPrevParticle = tail;
				tail = e;
			}

			// now p has stepped 'insize' places along, and q has too
			p = q;
		}

		tail->mNextParticle = NULL;

		// if we have done only one merge, we're finished
		if (nmerges <= 1)	// allow for nmerges == 0, the empty list case
		{
			(*headPtr) = list;
			(*tailPtr) = tail;
			return;
		}

		// otherwise repeat, merging lists twice the size
		insize *= 2;
	}
}

Particle::Particle(PARTICLE_EFFECT *effect)
{
	mEffect = effect;
	mAge = 0.0f;
	mX = 0.0f;
	mY = 0.0f;
	mZ = 0.0f;
	mColor.argb = D3DCOLOR_ARGB(0xFF, 0xFF, 0x00, 0x00);
	memset(&mVelocity, 0, sizeof(mVelocity));
	memset(&mAcceleration, 0, sizeof(mAcceleration));
}

void Particle::Execute(Scalar dT)
{
	if (!mEffect)
		return;

	if (IsAlive())
    {
		mX += mVelocity.x * dT;
		mY += mVelocity.y * dT;
		mZ += mVelocity.z * dT;
		
		mVelocity.x += mAcceleration.x * dT;
		mVelocity.y += mAcceleration.y * dT;
		mVelocity.z += mAcceleration.z * dT;

		mAge += dT;
        Scalar percentComplete = mAge / mEffect->fragLifetime;
		COLOR_POINT *cp0 = NULL;
		COLOR_POINT *cp1 = NULL;
		for (COLOR_POINT *cp = mEffect->colors + COLOR_POINT_COUNT - 2; cp >= mEffect->colors; cp--)
        {
            if (cp->active && percentComplete >= cp->time)
            {
                cp0 = cp;
                cp1 = cp + 1;
                break;
            }
        }

		if (!cp0 || !cp1)
		{
			cp0 = mEffect->colors;

			if (COLOR_POINT_COUNT > 1)
			{
				cp1 = mEffect->colors + 1;
			} else
			{
				cp1 = cp0;
			}
		}

        percentComplete = (percentComplete - cp0->time) / (cp1->time - cp0->time);
        mColor.a = (unsigned char)(cp0->color.a + (cp1->color.a - cp0->color.a) * percentComplete);
        mColor.r = (unsigned char)(cp0->color.r + (cp1->color.r - cp0->color.r) * percentComplete);
        mColor.g = (unsigned char)(cp0->color.g + (cp1->color.g - cp0->color.g) * percentComplete);
        mColor.b = (unsigned char)(cp0->color.b + (cp1->color.b - cp0->color.b) * percentComplete);
    }
}

ParticleEmitter::ParticleEmitter()
:	mEffect(NULL),
	mActive(false),
	mPosition(0.0f, 0.0f, 0.0f)
{
}

void ParticleEmitter::SetEffect(int effect)
{
	mEffect = &ParticleEngine::mInstalledEffects[effect];
}

void ParticleEmitter::Execute()
{
	if (mEffect == NULL)
		return;

	// calculate the number of particles we're going to need
	if (mActive)
	{
		bool fire = false;
		if (mEffect->id >= 1000)
		{
			// this is an independant effect
			static Scalar lastEmitted = (Scalar)Now();
			Scalar now = (Scalar)Now();

			INDIE_EFFECT *indieEffect = (INDIE_EFFECT*)mEffect;
			if ((now - lastEmitted) >= indieEffect->releasePeriod)
			{
				lastEmitted = now;
				fire = true;
			}
			if ((now - mActivated) >= indieEffect->duration)
				mActive = false;
		}
		else
		{
			fire = true;
			mActive = false;
		}

		if (fire)
		{
			for (int i = 0; i < mEffect->fragCount; ++i)
				ParticleEngine::CreateParticle(mPosition, mEffect);
		}
	}
}

void ParticleEngine::Destroy()
{
	mVertBuffer->Release();
	mParticleTexture->Release();
}

void ParticleEngine::Initialize(LPDIRECT3DDEVICE9 device)
{
	mDevice = device;
	memset(mInstalledEffects, 0, sizeof(mInstalledEffects));

	ParticleEngine::mMaxParticleCount = atoi(getenv("MAXPARTICLES"));
	
	// create the vertex buffer that will store the four vertices we need for the billboards
	mDevice->CreateVertexBuffer(ParticleEngine::mMaxParticleCount * 6 * sizeof(L4BASICVERTEX),
		D3DUSAGE_DYNAMIC,
		L4BASICVERTEX_FVF,
		D3DPOOL_DEFAULT,
		&mVertBuffer,
		NULL);

	D3DXCreateTextureFromFile(mDevice, L"VIDEO\\particles.png", &mParticleTexture);
}

void ParticleEngine::InstallEffect(int effectNumber, PARTICLE_EFFECT effect)
{
	mInstalledEffects[effectNumber] = effect;
	mInstalledEffects[effectNumber].id = effectNumber;
}

void ParticleEngine::CreateParticle(D3DXVECTOR3 position, PARTICLE_EFFECT *effect)
{
	if (effect == NULL || effect->fragCount <= 0)
		return;

	Particle *p = new Particle(effect);

	// check to make sure we have room for this new particle
	if (mTotalParticleCount + 1 >= ParticleEngine::mMaxParticleCount)
	{
		mTotalParticleCount--;

		// there isn't room for this particle so we'll delete the oldest particle
		if (mParticlesHead == mParticlesTail)
		{
			delete mParticlesHead;
			mParticlesHead = NULL;
			mParticlesTail = NULL;
		}
		else
		{
			Particle *temp = mParticlesTail;
			mParticlesTail = mParticlesTail->mPrevParticle;
			mParticlesTail->mNextParticle = NULL;
			delete temp;
		}
	}

	// insert the new particle into the linked list
	if (mParticlesHead)
		mParticlesHead->mPrevParticle = p;
	p->mPrevParticle = NULL;
	p->mNextParticle = mParticlesHead;
	mParticlesHead = p;
	if (!mParticlesTail)
		mParticlesTail = p;

	mTotalParticleCount++;

	// now initialize all the particle data based on the effect
	p->mX = position.x + ((float)rand() * 2.0f / RAND_MAX - 1.0f) * effect->varianceX;
	p->mY = position.y + ((float)rand() * 2.0f / RAND_MAX - 1.0f) * effect->varianceY;
	p->mZ = position.z + ((float)rand() * 2.0f / RAND_MAX - 1.0f) * effect->varianceZ;

	D3DXVECTOR3 vec(
		(float)rand() * 2.0f / RAND_MAX - 1.0f,
		(float)rand() * 2.0f / RAND_MAX - 1.0f,
		(float)rand() * 2.0f / RAND_MAX - 1.0f);

	D3DXVec3Normalize(&vec, &vec);
	
	p->mVelocity.x = vec.x * effect->velocity;
	p->mVelocity.y = vec.y * effect->velocity;
	p->mVelocity.z = vec.z * effect->velocity;

	p->mAcceleration.x = 0.0f;
	p->mAcceleration.y = effect->gravity;
	p->mAcceleration.z = 0.0f;

	// generate a random rotation amount in radians
	float centerX = effect->textureBounds.left + (effect->textureBounds.right - effect->textureBounds.left) / 2.0f;
	float centerY = effect->textureBounds.top + (effect->textureBounds.bottom - effect->textureBounds.top) / 2.0f;
	D3DXVECTOR2 rotCenter(centerX, centerY);
	D3DXMatrixTransformation2D(&p->mTextureTransform, NULL, 1, NULL, &rotCenter, ((float)rand() / RAND_MAX) * 2.0f * D3DX_PI, NULL);
	
	p->mColor.argb = (effect->colors[0].active ? effect->colors[0].color.argb : 0);
}

int ParticleEngine::BuildParticleVertices(const Particle *p, L4BASICVERTEX *verts, D3DXMATRIX *inverse_view_matrix)
{
	D3DXMATRIX rotation;
	PARTICLE_EFFECT *effect = p->mEffect;
	
	inverse_view_matrix->_41 = p->mX;
	inverse_view_matrix->_42 = p->mY;
	inverse_view_matrix->_43 = p->mZ;

	float fragSize = effect->fragSize;
	D3DXVECTOR3 vector3s[] =
	{
		D3DXVECTOR3(-fragSize,  fragSize, 0.0f),	// upper left
		D3DXVECTOR3( fragSize, -fragSize, 0.0f),	// lower right
		D3DXVECTOR3(-fragSize, -fragSize, 0.0f),	// lower left
		
		D3DXVECTOR3(-fragSize,  fragSize, 0.0f),	// upper left
		D3DXVECTOR3( fragSize,  fragSize, 0.0f),	// upper right
		D3DXVECTOR3( fragSize, -fragSize, 0.0f) 	// lower right
	};

	D3DXVECTOR2 vector2s[] = 
	{
		D3DXVECTOR2(effect->textureBounds.left, effect->textureBounds.top),		// upper left
		D3DXVECTOR2(effect->textureBounds.right, effect->textureBounds.bottom),	// lower right
		D3DXVECTOR2(effect->textureBounds.left, effect->textureBounds.bottom),	// lower left
		
		D3DXVECTOR2(effect->textureBounds.left, effect->textureBounds.top),		// upper left
		D3DXVECTOR2(effect->textureBounds.right, effect->textureBounds.top),	// upper right
		D3DXVECTOR2(effect->textureBounds.right, effect->textureBounds.bottom) 	// lower right
	};

	D3DXVECTOR3 normal, inNormal(0.0f, 0.0f, 1.0f);
	D3DXVec3TransformCoord(&normal, &inNormal, inverse_view_matrix);
	
	int count = (sizeof(vector3s) / sizeof(D3DXVECTOR3));
	for (int i=0; i<count; i++)
	{
		D3DXVECTOR3 outVec3;
		D3DXVECTOR2 outVec2;

		D3DXVec3TransformCoord(&outVec3, &vector3s[i], inverse_view_matrix);
		
		verts[i].x = outVec3.x;
		verts[i].y = outVec3.y;
		verts[i].z = outVec3.z;

		verts[i].nx = normal.x;
		verts[i].ny = normal.y;
		verts[i].nz = normal.z;

		if (effect->rotate)
			D3DXVec2TransformCoord(&outVec2, &vector2s[i], &p->mTextureTransform);
		else
			outVec2 = vector2s[i];

		verts[i].u = outVec2.x;
		verts[i].v = outVec2.y;

		verts[i].color = p->mColor.argb;
	}

	return count;
}

void ParticleEngine::ExecuteParticles(const D3DXMATRIX *view_matrix, Scalar timeSlice)
{
	// if we don't have a device or we have no particles, early exit
	if (!mDevice || mTotalParticleCount <= 0)
		return;	// we can't run without having been initialized with a device

	// first we need to execute all the particles to update their positions, color, etc.
	for (Particle *p = mParticlesHead; p;)
	{
		if (p->IsAlive())
		{
			p->Execute(timeSlice);

			p = p->mNextParticle;
		}
		else
		{
			// remove from the list
			if (p->mPrevParticle)
				p->mPrevParticle->mNextParticle = p->mNextParticle;
			if (p->mNextParticle)
				p->mNextParticle->mPrevParticle = p->mPrevParticle;

			// time to kill the particle
			if (p == mParticlesHead)
			{
				mParticlesHead = p->mNextParticle;
			}
			if (p == mParticlesTail)
			{
				mParticlesTail = p->mPrevParticle;
			}

			Particle *temp = p;
			p = p->mNextParticle;

			mTotalParticleCount--;
			delete temp;
		}
	}

	// now we're going to sort the particle list based on distance to the camera
	mergesort(&mParticlesHead, &mParticlesTail, view_matrix);

	//Scalar beforeBuild = (Scalar)Now();
	D3DXMATRIX inverse_view_matrix;
	D3DXMatrixInverse(&inverse_view_matrix, NULL, view_matrix);

	// and finally, we'll build the vertex list to send to Direct3D
	L4BASICVERTEX *vertBuffer = NULL;
	mVertBuffer->Lock(0, mTotalParticleCount * sizeof(L4BASICVERTEX), (void**)&vertBuffer, D3DLOCK_DISCARD);

	int actualParticles = 0;
	for (Particle *p = mParticlesHead; p; p = p->mNextParticle)
	{
		vertBuffer += BuildParticleVertices(p, vertBuffer, &inverse_view_matrix);
		actualParticles++;
	}

	mVertBuffer->Unlock();

	//Scalar afterBuild = (Scalar)Now();

	//afterBuild = afterBuild;
}

void ParticleEngine::RenderParticles(const D3DXMATRIX *view_matrix, Scalar timeSlice)
{
	// we always have to execute otherwise our timing
	// variables get all screwed up when we don't run for a while
	ExecuteParticles(view_matrix, timeSlice);

	if (!mDevice || mTotalParticleCount <= 0)
		return;

	// setup stages for particle's texture
	mDevice->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
	mDevice->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
	mDevice->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);

	mDevice->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE);
	mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE);
	mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);

	mDevice->SetRenderState(D3DRS_DIFFUSEMATERIALSOURCE, D3DMCS_COLOR1);
	mDevice->SetRenderState(D3DRS_EMISSIVEMATERIALSOURCE, D3DMCS_COLOR1);

	mDevice->SetTexture(0, mParticleTexture);

	mDevice->SetTextureStageState(0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE);
			
	mDevice->SetFVF(L4BASICVERTEX_FVF);
	mDevice->SetStreamSource(0, mVertBuffer, 0, sizeof(L4BASICVERTEX));

	//DWORD wrap_u, wrap_v;
	//mDevice->GetSamplerState(0, D3DSAMP_ADDRESSU, &wrap_u);
	//mDevice->GetSamplerState(0, D3DSAMP_ADDRESSV, &wrap_v);

	//mDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, D3DTADDRESS_WRAP);
	//mDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, D3DTADDRESS_WRAP);
	
	D3DXMATRIX world_matrix;
	D3DXMatrixIdentity(&world_matrix);
	mDevice->SetTransform(D3DTS_WORLD, &world_matrix);
	
	mDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, mTotalParticleCount * 2);

	mDevice->SetRenderState(D3DRS_EMISSIVEMATERIALSOURCE, D3DMCS_MATERIAL);

	mDevice->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_TFACTOR);

	//mDevice->SetSamplerState(0, D3DSAMP_ADDRESSU, wrap_u);
	//mDevice->SetSamplerState(0, D3DSAMP_ADDRESSV, wrap_v);
}