/*
   render/particles.cc
   This file is part of the Osirion project and is distributed under
   the terms of the GNU General Public License version 2
*/

#include "auxiliary/functions.h"
#include "render/particles.h"
#include "render/camera.h"
#include "render/gl.h"
#include "render/draw.h"
#include "render/textures.h"
#include "core/application.h"

namespace render {

/* ---- class Particle --------------------------------------------- */

Particle::Particle(const math::Vector3f &location, float time) : particle_location(location)
{
	particle_time = time;
}

Particle::Particle(const math::Vector3f &location, const math::Axis &axis, float time) : particle_location(location), particle_axis(axis)
{
	particle_time = time;
}


/* ---- static ParticleScript registry ---------------------------------- */

ParticleScript::Registry ParticleScript::particles_registry;

void ParticleScript::list()
{
	std::string str;
	for (Registry::iterator it = particles_registry.begin(); it != particles_registry.end(); it++) {
		
		ParticleScript *script = (*it).second;
		switch (script->type()) {
			case Flame:
				str.assign("flame");
				break;
			case Jet:
				str.assign("jet");
				break;
			case Spray:
				str.assign("spray");
				break;
			case Trail:
				str.assign("trail");
				break;
		}
		con_print << "  " << script->label() << " " << str << std::endl;
		con_print << particles_registry.size() << " particle scripts" << std::endl;
	}
}

void ParticleScript::clear()
{
	for (Registry::iterator it = particles_registry.begin(); it != particles_registry.end(); it++) {
		delete (*it).second;
		(*it).second = 0;
	}
 	particles_registry.clear();
}

ParticleScript *ParticleScript::find(const std::string &label)
{
	Registry::iterator it = particles_registry.find(label);
	
	if (it != particles_registry.end()) {
		return (*it).second;
	} else {
		return 0;
	}
}

ParticleScript *ParticleScript::load(const std::string &label)
{
	ParticleScript *script = find(label);
	if (script)
		return script;

	filesystem::IniFile inifile;

	inifile.open("particles/" + label);

	if (!inifile.is_open()) {
		con_warn << "Could not open " << inifile.name() << std::endl;
		return 0;
	}

	script = new ParticleScript(label);
	con_debug << "  " << inifile.name() << std::endl;

	std::string strval;

	while (inifile.getline()) {

		if (inifile.got_section()) {
			if (inifile.got_section("particles")) {
				continue;
			} else {
				inifile.unknown_section();
			}

		} else if (inifile.got_key()) {

			if (inifile.in_section("particles")) {

				if (inifile.got_key_string("type", strval)) {
					aux::to_label(strval);
					if (strval.compare("flame") == 0) {
						script->particles_type = ParticleScript::Flame;
					} else if (strval.compare("jet") == 0) {
						script->particles_type = ParticleScript::Jet;
					} else if (strval.compare("trail") == 0) {
						script->particles_type = ParticleScript::Trail;
					} else {
						inifile.unknown_value();
					}
					continue;
				} else if (inifile.got_key_string("texture", script->particles_texture)) {
					Textures::load("textures/" + script->texture());
					continue;
				} else if (inifile.got_key_float("speed", script->particles_speed)) {
					continue;
				} else if (inifile.got_key_float("offset", script->particles_offset)) {
					math::clamp(script->particles_offset, 0.0f, 1.0f);
					continue;
				} else if (inifile.got_key_float("alpha", script->particles_alpha)) {
					continue;
				} else if (inifile.got_key_float("radius", script->particles_radius)) {
					script->particles_radius *= model::SCALE;
					continue;
				} else if (inifile.got_key_float("eject", script->particles_eject)) {
					continue;
				} else if (inifile.got_key_float("timeout", script->particles_timeout)) {
					continue;
				} else if (inifile.got_key_color("color", script->particles_color)) {
					continue;
				} else {
					inifile.unkown_key();
				}
			}
		}
	}

	inifile.close();

	particles_registry[script->label()] = script;

	return script;
}

/* ---- class ParticleScript --------------------------------------- */

ParticleScript::ParticleScript(const std::string label) : particles_label(label)
{
	particles_radius = 1.0f;
	particles_alpha = 1.0f;

	particles_speed = 0.0f;
	particles_timeout = 2.0f;
	particles_eject = 0.25f;

	particles_offset = 0.1f;

	particles_color.assign(1.0f, 1.0f);
}

ParticleScript::~ParticleScript()
{
}

/* ---- class ParticleSystem --------------------------------------- */

ParticleSystem::ParticleSystem(ParticleScript *script, core::Entity *entity, model::Particles *modelclass)
{
	particlesystem_entity = entity;
	particlesystem_last_eject = 0; // timestamp of the last eject

	particlesystem_script = script;
	particlesystem_texture = 0;
	particlesystem_cull = model::CullNone;

	particlesystem_modelclass = modelclass;

	if (particlesystem_script) {
		particlesystem_texture = Textures::load("textures/" + particlesystem_script->texture());

		// map entity radius overrules script value
		if (modelclass->radius())
			particlesystem_radius = modelclass->radius();
		else
			particlesystem_radius = particlesystem_script->radius();
		
		color.assign(particlesystem_script->color());
	}

	if (particlesystem_modelclass) {
		particlesystem_location.assign(modelclass->location());
		if (modelclass->entity()) {
			color.assign(entity->color());
		}
		if (modelclass->engine()) {
			color.assign(entity->model()->enginecolor());
		}
		particlesystem_axis.assign(modelclass->axis());
// 		particlesystem_cull = particlesystem_modelclass->cull();
	}
}

ParticleSystem::~ParticleSystem()
{
	clear();
}

void ParticleSystem::clear()
{
	for (Stream::iterator it = particlesystem_stream.begin(); it != particlesystem_stream.end(); it++) {
		delete (*it);
	}
	particlesystem_stream.clear();
}

void ParticleSystem::draw(float elapsed)
{
	if (particlesystem_entity->type() == core::Entity::Controlable) {
		core::EntityControlable *ec = static_cast<core::EntityControlable *>(particlesystem_entity);
		if (ec->eventstate() == core::Entity::Docked) {
			if (particlesystem_stream.size())
				clear();
			return;
		}
	}

	now = core::application()->time();
	ejector_location.assign(particlesystem_entity->location() + (particlesystem_entity->axis() * location()));

	// remove dead particles
	Stream::reverse_iterator it = particlesystem_stream.rbegin();
	while ((it  != particlesystem_stream.rend()) && ((*it)->time() + particlesystem_script->timeout() <= now)) {
		delete (*particlesystem_stream.rbegin());
		particlesystem_stream.pop_back();
		it = particlesystem_stream.rbegin();
	}

	// apply speed
	bool ejector_active = false;
	if (particlesystem_script->speed()) {
		for (Stream::iterator it = particlesystem_stream.begin(); it != particlesystem_stream.end(); it++) {
			Particle *particle = (*it);
			particle->location() += particle->axis().forward() * particlesystem_script->speed() * elapsed;
		}
		ejector_active = true;
	} else {
		if (particlesystem_entity->type() == core::Entity::Dynamic) {
			core::EntityDynamic *ed = static_cast<core::EntityDynamic *>(particlesystem_entity);
			if (ed->speed()) {
				ejector_active = true;
			}
		} else if (particlesystem_entity->type() == core::Entity::Controlable) {
			core::EntityControlable *ec = static_cast<core::EntityControlable *>(particlesystem_entity);
			if ((ec->thrust() > 0.0f) || (ec->eventstate() == core::Entity::ImpulseInitiate) || (ec->eventstate() == core::Entity::Impulse)) {
				ejector_active = true;
			}
		}
	}

	// add new particles
	if (ejector_active && (particlesystem_last_eject + particlesystem_script->eject() <= now)) {
		particlesystem_stream.push_front(new Particle(ejector_location, particlesystem_entity->axis() * particlesystem_axis, now));
		particlesystem_last_eject = now;
	}
}

/* ---- class Jet -------------------------------------------------- */

Jet::Jet(ParticleScript *script, core::Entity *entity, model::Particles *modelclass) :
		ParticleSystem(script, entity, modelclass) {
}

Jet::~Jet()
{}

void Jet::draw(float elapsed) {
	if (!particlesystem_script)
		return;

	ParticleSystem::draw(elapsed);

	math::Vector3f quad[4];

	if (particlesystem_stream.size()) {
		Textures::bind(particlesystem_texture);
		gl::begin(gl::Quads);

		for (Stream::iterator it = particlesystem_stream.begin(); it != particlesystem_stream.end(); it++) {
			Particle *particle = (*it);
			
			quad[0].assign(particle->axis().up() - particle->axis().left());
			quad[1].assign(particle->axis().up() + particle->axis().left());
			quad[2].assign(particle->axis().up() * -1 + particle->axis().left());
			quad[3].assign(particle->axis().up() * -1 - particle->axis().left());
			
			float t = now - particle->time();
			float f = 0;

			if (t < particlesystem_script->timeout() * 0.1f) {
				f = t / (0.1f * particlesystem_script->timeout());
			} else {
				t = t - particlesystem_script->timeout() * 0.1f;
				f = t / (0.9f * particlesystem_script->timeout());
				f = 1.0 - f;
			}

			f *= f;
			float radius = particlesystem_radius * f;
			color.a = f * particlesystem_script->alpha();
			gl::color(color);
	
			glTexCoord2f(0,1);
			gl::vertex(particle->location() + quad[0] * radius);
			glTexCoord2f(0,0);
			gl::vertex(particle->location() + quad[1] * radius);
			glTexCoord2f(1,0);
			gl::vertex(particle->location() + quad[2] * radius);
			glTexCoord2f(1,1);
			gl::vertex(particle->location() + quad[3] * radius);
			Stats::quads++;
		}

		gl::end();
	}
}
/* ---- class Spray ------------------------------------------------ */

Spray::Spray(ParticleScript *script, core::Entity *entity, model::Particles *modelclass) :
		ParticleSystem(script, entity, modelclass) {
}

Spray::~Spray()
{}

void Spray::draw(float elapsed) {
	if (!particlesystem_script)
		return;

	ParticleSystem::draw(elapsed);

	math::Vector3f quad[4];

	if (particlesystem_stream.size()) {
		Textures::bind(particlesystem_texture);
		gl::begin(gl::Quads);

		for (Stream::iterator it = particlesystem_stream.begin(); it != particlesystem_stream.end(); it++) {
			Particle *particle = (*it);
			
			quad[0].assign(Camera::axis().up() - Camera::axis().left());
			quad[1].assign(Camera::axis().up() + Camera::axis().left());
			quad[2].assign(Camera::axis().up() * -1 + Camera::axis().left());
			quad[3].assign(Camera::axis().up() * -1 - Camera::axis().left());
			
			float t = now - particle->time();
			float f = 0;

			if (t < particlesystem_script->timeout() * 0.1f) {
				f = t / (0.1f * particlesystem_script->timeout());
			} else {
				t = t - particlesystem_script->timeout() * 0.1f;
				f = t / (0.9f * particlesystem_script->timeout());
				f = 1.0 - f;
			}

			f *= f;
			float radius = particlesystem_radius * f;
			color.a = f * particlesystem_script->alpha();
			gl::color(color);
	
			glTexCoord2f(0,1);
			gl::vertex(particle->location() + quad[0] * radius);
			glTexCoord2f(0,0);
			gl::vertex(particle->location() + quad[1] * radius);
			glTexCoord2f(1,0);
			gl::vertex(particle->location() + quad[2] * radius);
			glTexCoord2f(1,1);
			gl::vertex(particle->location() + quad[3] * radius);
			Stats::quads++;
		}

		gl::end();
	}
}


/* ---- class Trail ------------------------------------------------ */

Trail::Trail(ParticleScript *script, core::Entity *entity, model::Particles *modelclass) :
		ParticleSystem(script, entity, modelclass) {
}

Trail::~Trail()
{}

void Trail::draw(float elapsed) {

	if (!particlesystem_script)
		return;

	ParticleSystem::draw(elapsed);	

	if (particlesystem_stream.size()) {
		Textures::bind(particlesystem_texture);
		
		gl::begin(gl::Quads);

		Stream::iterator first = particlesystem_stream.begin();

		float tp = now - (*first)->time();
		float fp = 0;

		if (tp < particlesystem_script->timeout() * 0.1f) {
			fp = tp / (0.1f * particlesystem_script->timeout());
		} else {
			tp = tp - particlesystem_script->timeout() * 0.1f;
			fp = tp / (0.9f * particlesystem_script->timeout());
			fp = 1.0 - fp;
		}
		tp = (now - (*first)->time()) / particlesystem_script->timeout();

		if (tp > 0) {
			color.a = 0.0f;
			gl::color(color);
	
			glTexCoord2f(1,0);
			gl::vertex(ejector_location);
			glTexCoord2f(0,0);
			gl::vertex(ejector_location);
	
			color.a = fp;
			gl::color(color);
	
			glTexCoord2f(0,1);
			gl::vertex((*first)->location() + (*first)->axis().left() * particlesystem_radius * fp);
			glTexCoord2f(1,1);
			gl::vertex((*first)->location() - (*first)->axis().left() * particlesystem_radius * fp);
			Stats::quads++;
		}

		Stream::iterator next = first;
		for (next++; next != particlesystem_stream.end(); next++) {

			float t = now - (*next)->time();
			float f = 0;

			if (t < particlesystem_script->timeout() * 0.1f) {
				f = t / (0.1f * particlesystem_script->timeout());
			} else {
				t = t - particlesystem_script->timeout() * 0.1f;
				f = t / (0.9f * particlesystem_script->timeout());
				f = 1.0 - f;
			}
			t = (now - (*next)->time()) / particlesystem_script->timeout();

			color.a = fp * particlesystem_script->alpha();
			gl::color(color);

			glTexCoord2f(1,0);
			gl::vertex((*first)->location() - (*first)->axis().left() * particlesystem_radius * fp);
			glTexCoord2f(0,0);
			gl::vertex((*first)->location() + (*first)->axis().left() * particlesystem_radius * fp);

			color.a = f * particlesystem_script->alpha();
			gl::color(color);

			glTexCoord2f(0,1);
			gl::vertex((*next)->location() + (*next)->axis().left() * particlesystem_radius * f);
			glTexCoord2f(1,1);
			gl::vertex((*next)->location() - (*next)->axis().left() * particlesystem_radius * f);

			Stats::quads++;

			first = next;
			fp = f;
			tp = t;
		}


		gl::end();
	}
}

/* ---- class Flame ------------------------------------------------ */

Flame::Flame(ParticleScript *script, core::Entity *entity, model::Particles *modelclass) :
		ParticleSystem(script, entity, modelclass) {
}

Flame::~Flame()
{}

void Flame::draw(float elapsed) {

	if (!particlesystem_script)
		return;

	ParticleSystem::draw(elapsed);	

	if (particlesystem_stream.size() > 1) {
		
		Textures::bind(particlesystem_texture);
		gl::begin(gl::Quads);

		Stream::iterator first = particlesystem_stream.begin();

		float tp = now - (*first)->time();
		float fp = 0;

		if (tp < particlesystem_script->timeout() * particlesystem_script->offset()) {
			fp = tp / (particlesystem_script->offset() * particlesystem_script->timeout());
		} else {
			tp = tp - particlesystem_script->timeout() * particlesystem_script->offset();
			fp = tp / ((1.0f - particlesystem_script->offset()) * particlesystem_script->timeout());
			fp = 1.0 - fp;
		}
		tp = (now - (*first)->time()) / particlesystem_script->timeout();

		Stream::iterator next = first;
		if (tp > 0) {
			color.a = 0;
			gl::color(color);
			glTexCoord2f(1,0);
			gl::vertex(ejector_location);
			glTexCoord2f(0,0);
			gl::vertex(ejector_location);

			color.a = fp * particlesystem_script->alpha();
			gl::color(color);
			glTexCoord2f(0,tp);
			gl::vertex((*next)->location() + (*next)->axis().left() * particlesystem_radius * fp);
			glTexCoord2f(1,tp);
			gl::vertex((*next)->location() + (*next)->axis().up() * particlesystem_radius * fp);
			Stats::quads++;

			color.a = 0;
			gl::color(color);
			glTexCoord2f(1,0);
			gl::vertex(ejector_location);
			glTexCoord2f(0,0);
			gl::vertex(ejector_location);

			color.a = fp * particlesystem_script->alpha();
			gl::color(color);
			glTexCoord2f(0,tp);
			gl::vertex((*next)->location() + (*next)->axis().up() * particlesystem_radius * fp);
			glTexCoord2f(1,tp);
			gl::vertex((*next)->location() - (*next)->axis().left() * particlesystem_radius * fp);
			Stats::quads++;

			color.a = 0;
			gl::color(color);
			glTexCoord2f(1,0);
			gl::vertex(ejector_location);
			glTexCoord2f(0,0);
			gl::vertex(ejector_location);

			color.a = fp * particlesystem_script->alpha();
			gl::color(color);
			glTexCoord2f(0,tp);
			gl::vertex((*next)->location() + (*next)->axis().left() * particlesystem_radius * fp);
			glTexCoord2f(1,tp);
			gl::vertex((*next)->location() - (*next)->axis().up() * particlesystem_radius * fp);
			Stats::quads++;

			color.a = 0;
			gl::color(color);
			glTexCoord2f(1,0);
			gl::vertex(ejector_location);
			glTexCoord2f(0,0);
			gl::vertex(ejector_location);

			color.a = fp * particlesystem_script->alpha();
			gl::color(color);
			glTexCoord2f(0,tp);
			gl::vertex((*next)->location() - (*next)->axis().up() * particlesystem_radius * fp);
			glTexCoord2f(1,tp);
			gl::vertex((*next)->location() - (*next)->axis().left() * particlesystem_radius * fp);
			Stats::quads++;
		}

		for (next++; next != particlesystem_stream.end(); next++) {

			float t = now - (*next)->time();
			float f = 0;

			if (t < particlesystem_script->timeout() * particlesystem_script->offset()) {
				f = t / (particlesystem_script->offset() * particlesystem_script->timeout());
			} else {
				t = t - particlesystem_script->timeout() * particlesystem_script->offset();
				f = t / ((1.0f - particlesystem_script->offset()) * particlesystem_script->timeout());
				f = 1.0 - f;
			}
			t = (now - (*next)->time()) / particlesystem_script->timeout();

			color.a = fp * particlesystem_script->alpha();
			gl::color(color);
			glTexCoord2f(1,tp);
			gl::vertex((*first)->location() + (*first)->axis().up() * particlesystem_radius * fp);
			glTexCoord2f(0,tp);
			gl::vertex((*first)->location() + (*first)->axis().left() * particlesystem_radius * fp);

			color.a = f * particlesystem_script->alpha();
			gl::color(color);
			glTexCoord2f(0,t);
			gl::vertex((*next)->location() + (*next)->axis().left() * particlesystem_radius * f);
			glTexCoord2f(1,t);
			gl::vertex((*next)->location() + (*next)->axis().up() * particlesystem_radius * f);
			Stats::quads++;

			color.a = fp * particlesystem_script->alpha();
			gl::color(color);
			glTexCoord2f(1,tp);
			gl::vertex((*first)->location() - (*first)->axis().left() * particlesystem_radius * fp);
			glTexCoord2f(0,tp);
			gl::vertex((*first)->location() + (*first)->axis().up() * particlesystem_radius * fp);

			color.a = f * particlesystem_script->alpha();
			gl::color(color);
			glTexCoord2f(0,t);
			gl::vertex((*next)->location() + (*next)->axis().up() * particlesystem_radius * f);
			glTexCoord2f(1,t);
			gl::vertex((*next)->location() - (*next)->axis().left() * particlesystem_radius * f);
			Stats::quads++;

			color.a = fp * particlesystem_script->alpha();
			gl::color(color);
			glTexCoord2f(1,tp);
			gl::vertex((*first)->location() - (*first)->axis().up() * particlesystem_radius * fp);
			glTexCoord2f(0,tp);
			gl::vertex((*first)->location() + (*first)->axis().left() * particlesystem_radius * fp);

			color.a = f * particlesystem_script->alpha();
			gl::color(color);
			glTexCoord2f(0,t);
			gl::vertex((*next)->location() + (*next)->axis().left() * particlesystem_radius * f);
			glTexCoord2f(1,t);
			gl::vertex((*next)->location() - (*next)->axis().up() * particlesystem_radius * f);
			Stats::quads++;

			color.a = fp * particlesystem_script->alpha();
			gl::color(color);
			glTexCoord2f(1,tp);
			gl::vertex((*first)->location() - (*first)->axis().left() * particlesystem_radius * fp);
			glTexCoord2f(0,tp);
			gl::vertex((*first)->location() - (*first)->axis().up() * particlesystem_radius * fp);

			color.a = f * particlesystem_script->alpha();
			gl::color(color);
			glTexCoord2f(0,t);
			gl::vertex((*next)->location() - (*next)->axis().up() * particlesystem_radius * f);
			glTexCoord2f(1,t);
			gl::vertex((*next)->location() - (*next)->axis().left() * particlesystem_radius * f);
			Stats::quads++;

			first = next;
			fp = f;
			tp = t;
		}


		gl::end();
	}
}

} // namespace render