// -*- C++ -*-
//    Title: particle.cpp
//  Created: Sun Mar 14 12:00:27 2010
//   Author: Tim Weyrich <t.weyrich@cs.ucl.ac.uk>
//      $Id: $
//
// copyright (c) 2010, University College London
//

// Code template (to help the class as a startup for the coursework) from Tim
// Additional code (for main particle engine, particle graphics generated, keyboard control, etc from Michael Kargas)

#ifdef _WIN32
#  include <windows.h>
#endif
#include <stdio.h>
#include <math.h>

#include <iostream>
#include <deque>

#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glut.h>

#include "myclock.h"
#include "amra_vector.h"
#include "random.h"

struct Color
{
	Color(float _r, float _g, float _b, float _a)
		:	r(_r),
			g(_g),
			b(_b),
			a(_a)
	{}

	float r;
	float g;
	float b;
	float a;
};

struct Particle
{
    Particle(const Point &_pos, const Vector &_vel, const Color &_col)
        : pos(_pos),
          vel(_vel),
          creationTimeStamp(0),
		  col(_col)
    { }

    Point  pos;
    Vector  vel;
    // Add any additional attributes here
    // ...
	Color col;

    double  creationTimeStamp;
};

class ParticleSystem
{
  public:
    ParticleSystem()
        : gravity(0,-9.81f,0),
		  direction(0,1.0f,0),
		  disp_angle(0.0f),
          timeStamp(myclock())
    { }
    
    std::deque<Particle>  particles;

    Vector  gravity;
	Vector direction;
	float disp_angle;
    double  timeStamp;
};

ParticleSystem ps;

// Window size
//
const int  WIDTH = 500;
const int  HEIGHT = 500;

// Maximum particle age in seconds
//
const double  MAXAGE = 4;
const int MAXPARTICLES = 4096;
const int NUMBOIDS = 64;
const float DISTANCE = 8;
const float FLOOR_ELEVATION = -4;

bool boids = 0;


// ==== Code for generation of particle gfx ====

enum {PARTICLE_STAR, FLOOR, NUM_TEXTURES};
int twidth[] = {128, 256, 0};
int theight[] = {128, 256, 0};

GLuint textures[NUM_TEXTURES];
unsigned int *bitmaps[NUM_TEXTURES];

void generateParticleStar()
{
    int i = 0;
	for (int y = -theight[PARTICLE_STAR]/2; y<theight[PARTICLE_STAR]/2; y++)
    {
        for (int x = -twidth[PARTICLE_STAR]/2; x<twidth[PARTICLE_STAR]/2; x++)
        {
            int c = 255;
            if (x!=0 || y!=0)
            {
                c = 32768 / (x*x + y*y);
                if (c>255) c = 255;
			}

			const float l = 2.0f;
			if ((x>=-l && x<=l) || (y>=-l && y<=l))
			{
				c = (int)((float)c * 1.5f);
				if (c>255) c = 255;
			}

			bitmaps[PARTICLE_STAR][i++] = (c<<24) | (c<<16) | (c<<8) | c;
        }
    }
}

void generateFloor()
{
    int i = 0;
	for (int y = 0; y<theight[FLOOR]; y++)
    {
        for (int x = 0; x<twidth[FLOOR]; x++)
        {
			int c = x ^ y;
			bitmaps[FLOOR][i++] = (255<<24) | (c<<16) | (c<<8) | c;
		}
	}
}

void generateTextures()
{
	generateParticleStar();
	generateFloor();
}

void loadTextures()
{
	for (int i=0; i<NUM_TEXTURES; i++)
	{
		bitmaps[i] = new unsigned int[twidth[i] * theight[i]];
	}

	generateTextures();

	glGenTextures(NUM_TEXTURES, textures);
	for (int i=0; i<NUM_TEXTURES; i++)
	{
		glBindTexture(GL_TEXTURE_2D, textures[i]);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
		gluBuild2DMipmaps(GL_TEXTURE_2D, 3, twidth[i], theight[i], GL_RGBA, GL_UNSIGNED_BYTE, bitmaps[i]);
	}
}

void drawParticle(Particle &p)
{
	Color c = p.col;
	glColor4f(c.r, c.g, c.b, c.a);
    glLoadIdentity();

	float center_x = p.pos.x();
	float center_y = p.pos.y();
	float center_z = p.pos.z();

	float size = 0.25f;
	float x0 = center_x - size;
	float x1 = center_x + size;
	float y0 = center_y - size;
	float y1 = center_y + size;
	float z = center_z;

	glBegin(GL_QUADS);
		glTexCoord2f(0,0);
		glVertex3f(x0, y0, z);
		glTexCoord2f(1,0);
		glVertex3f(x1, y0, z);
		glTexCoord2f(1,1);
		glVertex3f(x1, y1, z);
		glTexCoord2f(0,1);
		glVertex3f(x0, y1, z);
	glEnd();
}

void drawVector(Vector &v, Color c, float scale)
{
	glColor3f(c.r, c.g, c.b);

	glLoadIdentity();

	float vec_dist = 32 * scale;

	glBegin(GL_LINES);
		glVertex3f(0,0,-vec_dist);
		glVertex3f(v.x() * scale, v.y() * scale,-vec_dist);
	glEnd();

	Vector v1 = v.normalised();
	v1.rotate2D_Z(-135);
	glBegin(GL_LINES);
		glVertex3f(v.x() * scale, v.y() * scale,-vec_dist);
		glVertex3f(v.x() * scale + v1.x(), v.y() * scale + v1.y(),-vec_dist);
	glEnd();

	Vector v2 = v.normalised();
	v2.rotate2D_Z(135);
	glBegin(GL_LINES);
		glVertex3f(v.x() * scale, v.y() * scale,-vec_dist);
		glVertex3f(v.x() * scale + v2.x(), v.y() * scale + v2.y(),-vec_dist);
	glEnd();
}

void displayDispersion()
{
	if (boids==1) return;

	glLoadIdentity();

	glDisable(GL_TEXTURE_2D);
	glColor3f(1.0f, 1.0f, 0.0f);

	float z = 32;
	float px = (WIDTH / 2) / (512.0f / z);
	float py = -(HEIGHT / 2) / (448.0f / z);
	float disp = ps.disp_angle / 45.0f;

	glBegin(GL_LINES);
		glVertex3f(px, py, -z);
		glVertex3f(px - disp, py + 2, -z);
		glVertex3f(px, py, -z);
		glVertex3f(px + disp, py + 2, -z);
	glEnd();
}

void displayParticles()
{
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, textures[PARTICLE_STAR]);

	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_COLOR);

	int size = ps.particles.size();
	for (int i=0; i<size; i++)
	{
		drawParticle(ps.particles.at(i));
	}

	glDisable(GL_BLEND);
	glDisable(GL_TEXTURE_2D);
}

void displayVectors()
{
	drawVector(ps.direction, Color(1.0f, 1.0f, 1.0f, 1.0f), 1.0f);
	if (boids==0)
		drawVector(ps.gravity, Color(1.0f, 0.0f, 0.0f, 1.0f), 1.0f);
}

void displayFloor(int grid_width, int grid_height, float elevation, float size)
{
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, textures[FLOOR]);

	glLoadIdentity();
	glTranslatef((-grid_width/2) * size, 0, (-grid_height/2) * size);

	for (int z=0; z<grid_height; z++)
	{
		float shade = 1.0f - ((float)grid_height - (float)z) / (float)grid_height;
		for (int x=0; x<grid_width; x++)
		{
			glBegin(GL_QUADS);
				glColor3f(0.25f * shade, 0.25f * shade, 0.75f * shade);
				glTexCoord2f(0,0);
				glVertex3f(x * size, elevation, z * size);
				glTexCoord2f(1,0);
				glVertex3f((x+1) * size, elevation, z * size);
				glTexCoord2f(1,1);
				glVertex3f((x+1) * size, elevation, (z+1) * size);
				glTexCoord2f(0,1);
				glVertex3f(x * size, elevation, (z+1) * size);
			glEnd();
		}
	}

	glDisable(GL_TEXTURE_2D);
}

void display(void)
{
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glLoadIdentity();
	/*if (boids==1)
	{
		Point pos = ps.particles.at(0).pos;
		glTranslatef(-pos.x(), pos.y(), -pos.z());
	}*/

	displayFloor(64, 64, FLOOR_ELEVATION, 4.0f);
	displayParticles();
	displayVectors();
	displayDispersion();

	glutSwapBuffers();
}

void simulate(void)
{
    double  now = myclock();

    // Here: simulate particles between the times "ps.timeStamp" and "now"
    // ...


	// Checking if we want to kill a particle
	for (int i=0; i<ps.particles.size(); i++)
	{
		Particle &p = ps.particles.at(i);
		p.col.a = 1.0f - (now - p.creationTimeStamp) / MAXAGE;
		if (now - p.creationTimeStamp > MAXAGE)
		{
			ps.particles.pop_front();
			i--;
		}
	}

	// Moving the particles
	float time_dif = (now - ps.timeStamp);
	int size = ps.particles.size();
	for (int i=0; i<size; i++)
	{
		Particle &p = ps.particles.at(i);
		p.pos = p.pos + p.vel * time_dif;
		if (p.pos.y() < FLOOR_ELEVATION)
		{
			p.pos = p.pos + p.vel * (-time_dif) * (1.0f - (p.pos.y() - FLOOR_ELEVATION));
			p.vel.y() = - p.vel.y();
		}
		else
		{
			p.vel = p.vel + ps.gravity * time_dif;
		}
	}
    ps.timeStamp = now;
}

float boids_speed = 2.0f;
float boids_dist_view = 1.0f;
float boids_dist_near = 0.25f;
float step_cohesion = 1.0f / 256.0f;
float step_alignment = 1.0f / 64.0f;
float step_move_to_center = 1.0f / 128.0f;

Point the_mass_center;

Vector boids_cohesion(int b_num)
{
	Point p(0,0,0);

	int size = ps.particles.size();
	for (int i=0; i < size; i++)
	{
		if (i!=b_num)
		{
			p = p + ps.particles.at(i).pos;
		}
	}

	p = p / (float)(size - 1);
	Vector v = p - ps.particles.at(b_num).pos;

	the_mass_center = p;

	for (int i=0; i<size; i++)
	{
		ps.particles.at(i).pos.x() = ps.particles.at(i).pos.x() - the_mass_center.x();
		ps.particles.at(i).pos.y() = ps.particles.at(i).pos.y() - the_mass_center.y();
		ps.particles.at(i).pos.z() = ps.particles.at(i).pos.z() - the_mass_center.z();
	}

	return v * step_cohesion;
}

Vector boids_separation(int b_num)
{
	Vector v(0,0,0);
	Point p0 = ps.particles.at(b_num).pos;

	int size = ps.particles.size();
	for (int i=0; i < size; i++)
	{
		if (i!=b_num)
		{
			Point p1 = ps.particles.at(i).pos;
			float dist = p0.dist(p1);
			if (dist < boids_dist_near)
			{
				v = v + (p0 - p1);
			}
		}
	}

	return v;
}

Vector boids_alignment(int b_num)
{
	Vector v(0,0,0);
	Point p0 = ps.particles.at(b_num).pos;

	int size = ps.particles.size();
	int counter = 0;
	for (int i=0; i < size; i++)
	{
		if (i!=b_num)
		{
			Point p1 = ps.particles.at(i).pos;
			float dist = p0.dist(p1);
			if (dist < boids_dist_view);
			{
				v = v + ps.particles.at(i).vel;
				counter++;
			}
		}
	}

	v = v * (1.0f / (float)counter);

	return v * step_alignment;
}

Vector boids_move_to_point(int i, Point p)
{
	Vector v = (p - ps.particles.at(i).pos);
	v.normalise();
	float dist = p.dist(ps.particles.at(i).pos);
	return v * (dist * step_move_to_center);
}

void simulateBoids()
{
    double  now = myclock();
	float time_dif = (now - ps.timeStamp);

	Vector vdir(ps.direction.x(), 0.0f, -ps.direction.y());
	vdir = vdir.normalised() * 0.25;
	Point pcenter(0.0f, 0.0f, -DISTANCE);

	int size = ps.particles.size();
	for (int i=0; i<size; i++)
	{
		Vector v1 = boids_cohesion(i);
		Vector v2 = boids_separation(i);
		Vector v3 = boids_alignment(i);
		Vector v4 = boids_move_to_point(i, pcenter);

		ps.particles.at(i).vel = ps.particles.at(i).vel + v1 + v2 + v3 + v4;// + vdir;

		ps.particles.at(i).pos = ps.particles.at(i).pos + ps.particles.at(i).vel * time_dif;
	}

	ps.timeStamp = now;
}

Particle breedParticle(Point pos, Vector dir, float dispersion)
{
	float angle1 = dispersion * (frand() - 0.5);
	dir.rotate2D_Z(angle1);
	float angle2 = dispersion * (frand() - 0.5);
	dir.rotate2D_X(angle2);

	Color col = Color(frand(), frand(), frand(), 1.0f);

	Particle p = Particle(pos, dir, col);
	p.creationTimeStamp = myclock();
	return p;
}

void idle(void)
{
    // Add a particle if we're not at the limit

	if (boids==0)
	{
		Particle p = breedParticle(Point(0,0,-DISTANCE),ps.direction * frand(0.75,1.0), ps.disp_angle);
		if (ps.particles.size() < MAXPARTICLES)
			ps.particles.push_back(p);
	}
	
	if( boids )
        simulateBoids();
    else
        simulate();
    
    glutPostRedisplay();
}

int m_button;
int m_state;

void changeVector(int xscreen, int yscreen, float scale, Vector &v)
{
	v.x() = ((float)xscreen - WIDTH / 2.0f) * scale;
	v.y() = -((float)yscreen - HEIGHT / 2.0f) * scale;
}

void changeDispersion(int xscreen)
{
	int xcenter = WIDTH / 2;
	int xdist = abs(xcenter - xscreen);
	float fdist = (float)xdist / (float)xcenter;	// 0 to 1

	ps.disp_angle = 90 * fdist;
}

void initParticles()
{
	ps.particles.clear();
	printf("%d\n",ps.particles.size());
	ps.direction = Vector(0.0f, 12.0f, 0.0f);
	ps.gravity = Vector(0.0f, -9.81f, 0.0f);
	ps.disp_angle = 30;
}

void initBoids()
{
	ps.particles.clear();
	ps.direction = Vector(0.0f, 0.0f, 0.0f);
	ps.gravity = Vector(0.0f, 0.0f, 0.0f);

	float scale = 0.25;

	for (int i=0; i<NUMBOIDS; i++)
	{
		float xp = (sin(i / 11.0f) * 16.0f + cos(i / 7.0f) * 12.0f) * scale;
		float zp = (sin(i / 17.0f) * 24.0f + cos(i / 3.0f) * 17.0f) * scale  - DISTANCE / 2;
		Vector pdir = Vector(0,FLOOR_ELEVATION / 2.0,-DISTANCE / 2) + Vector(xp, FLOOR_ELEVATION / 2.0, zp).invert();
		pdir = pdir.normalised() * frand(0.75, 1.0);
		Particle p = breedParticle(Point(xp, FLOOR_ELEVATION / 2.0, zp), pdir, 15.0f);
		ps.particles.push_back(p);
	}
}

void mouseAction(int xscreen, int yscreen)
{
	float scale = 0.08;
	if (m_state==0)
	{
		switch(m_button)
		{
			case 0:
				changeVector(xscreen, yscreen, scale, ps.direction);
				if (boids==1)
				{
					ps.direction.normalise();
					ps.direction = ps.direction * boids_speed;
				}
			break;

			case 1:
				if (boids==0)
					changeDispersion(xscreen);
			break;

			case 2:
				if (boids==0)
					changeVector(xscreen, yscreen, scale, ps.gravity);
			break;
		}
	}
}

void mouse( int button, int state, int x, int y )
{
	m_button = button;
	m_state = state;

	mouseAction(x,y);
}

void mouseActiveMotion( int x, int y )
{
	mouseAction(x,y);
}

void keyboard(unsigned char key,int x,int y)
{
	if (key==32)
	{
		boids = (boids + 1) & 1;

		if (boids==0)
			initParticles();
		else
			initBoids();
	}
}

void glutResize(int w, int h)
{
    if (w!=WIDTH || h!=HEIGHT) 
        glutReshapeWindow(WIDTH,HEIGHT);    // if resized, snap back to original size/shape
    glutPostRedisplay();
}

void create_window()
{
    // create a window with the desired size and title
    glutInitWindowSize(WIDTH,HEIGHT);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutCreateWindow("Particle System");

    // specify the function to be used for rendering in this window
    glutDisplayFunc( display );
    glutIdleFunc( idle );
    glutKeyboardFunc( keyboard );
    glutMouseFunc( mouse );
    glutMotionFunc( mouseActiveMotion );
    glutReshapeFunc( glutResize );

    glMatrixMode(GL_PROJECTION);
    gluPerspective(64.0, WIDTH/HEIGHT, 1.0, 4096.0);
    glMatrixMode(GL_MODELVIEW);
	//glLoadIdentity();

    glDisable(GL_DEPTH_TEST);
	glEnable(GL_TEXTURE_2D);
    glShadeModel(GL_SMOOTH);

	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_COLOR);
}

int main(int argc, char** argv)
{
#if 0
    // demonstrate myclock() function
    std::cout << myclock() << std::endl;
#ifdef _WIN32
    Sleep((DWORD)1.5e3);
#else
    usleep(1.5e6);
#endif
    std::cout << myclock() << std::endl;
    exit(0);
#endif

    glutInit(&argc, argv);
    create_window();

	if (boids==0)
		initParticles();
	else
		initBoids();

	loadTextures();

    glutMainLoop();
}
