/*!\file window.c
 * \brief Utilisation du raster "maison" pour finaliser le pipeline de
 * rendu 3D. Ici on peut voir les géométries disponibles.
 * \author Farès BELHADJ, amsi@up8.edu
 * \date December 4, 2020.
 * \todo pour les étudiant(e)s : changer la variation de l'angle de
 * rotation pour qu'il soit dépendant du temps et non du framerate
 */
#include <assert.h>
/* inclusion des entêtes de fonctions de gestion de primitives simples
 * de dessin. La lettre p signifie aussi bien primitive que
 * pédagogique. */
#include <GL4D/gl4dp.h>
/* inclure notre bibliothèque "maison" de rendu */
#include "moteur.h"

/* inclusion des entêtes de fonctions de création et de gestion de
 * fenêtres système ouvrant un contexte favorable à GL4dummies. Cette
 * partie est dépendante de la bibliothèque SDL2 */
#include <GL4D/gl4duw_SDL2.h>
#include <SDL_mixer.h>

/* protos de fonctions locales (static) */
static void init(void);
static void draw(void);
static void key(int keycode);
static void sortie(void);
static void pmotion(int x, int y);
static void mouse(int button, int state, int x, int y);
static void goto_obj(float * mvMat);
static void move_to(float posx, float posy , float posz);
static void draw_object(float * nmv, float * projMat, surface_t * obj,
						float ma, float mx, float my,
						float tx, float ty, float tz, float s,
						float ra, float rx, float ry);
static void get_params(void);
static int get_sign(float x, float y);

/*!\brief un identifiant pour l'écran (de dessin) */
static uint _screenId = 0;

// obj contains all planets, the Sun, Pluto, all the moons and their parameters.
static object_t _obj[28];

/* des variable d'états pour activer/désactiver des options de rendu */
static int _use_tex = 1, _use_color = 0, _use_lighting = 1;

typedef struct cam_t cam_t;
// camera structure, borrowed from sample3d_01-1.6 of GL4Dummies samples.
struct cam_t {
	float x, y, z;
	float theta;
};

static cam_t _cam = {0, 1.0f, 10, 0};

// movement speed.
static float _v = 0.1f;

// window size.
static int _wW = 1200, _wH = 900;

// middle of the window.
static int _xm = 600, _ym = 450;

static int _pause = 0; // boolean value for the pause.
static int _movement = 1; // boolean to allow movement.
static int _overview = 0; // boolean to toggle overview (view from the top).
static int _p = -1; // the object (sun, planets, pluto) number to move.
static float _s = 1.0f; // multiplier for angle (for speeding planets movement and rotation).
static float _a = 0.0f; // rotation angle.
static float _r = 0.0f; // -//-

static Mix_Chunk * bsound; // background sound.

/*!\brief paramètre l'application et lance la boucle infinie. */
int main(int argc, char ** argv) {
	if (SDL_Init(SDL_INIT_AUDIO) == -1) {
		fprintf(stderr, "SDL_Init: %s\n,", Mix_GetError());
		exit(3);
	}
	/* tentative de création d'une fenêtre pour GL4Dummies */
	if (!gl4duwCreateWindow(argc, argv, /* args du programme */
				"Solar System", /* titre */
				10, 10, _wW, _wH, /* x, y, largeur, heuteur */
				GL4DW_SHOWN) /* état visible */) {
		/* ici si échec de la création souvent lié à un problème d'absence
		 * de contexte graphique ou d'impossibilité d'ouverture d'un
		 * contexte OpenGL (au moins 3.2) */
		return 1;
	}
	/* Pour forcer la désactivation de la synchronisation verticale */
	SDL_GL_SetSwapInterval(0);
	init();
	/* création d'un screen GL4Dummies (texture dans laquelle nous
	 * pouvons dessiner) aux dimensions de la fenêtre */
	_screenId = gl4dpInitScreen();
	/* mettre en place la fonction d'interception clavier */
	gl4duwMouseFunc(mouse);
	gl4duwKeyDownFunc(key);
	gl4duwPassiveMotionFunc(pmotion);
	/* mettre en place la fonction de display */
	gl4duwDisplayFunc(draw);
	/* boucle infinie pour éviter que le programme ne s'arrête et ferme
	 * la fenêtre immédiatement */

	gl4duwMainLoop();
	return 0;
}

/*!\brief init de nos données, spécialement les trois surfaces
 * utilisées dans ce code */
void init(void) {
	uint id[28], i;
	int flags, initted;

	flags 	= MIX_INIT_MP3;
	initted = Mix_Init(flags);

	if ((initted & flags) != flags) {
		fprintf(stderr, "Mix_Init: Failed to init required mp3 support!\n");
		fprintf(stderr, "Mix_Init: %s\n,", Mix_GetError());
		exit(2);
	}
	if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024) == -1) {
		fprintf(stderr, "Mix_OpenAudio: %s\n,", Mix_GetError());
		exit(4);
	}
	if (bsound == NULL)
		bsound = Mix_LoadWAV("./space.wav");
	if (Mix_PlayChannel(-1, bsound, 0) < 0)
		fprintf(stderr, "Mix_PlayChannel: %s\n", Mix_GetError());

	// create all spheres.
	for (i = 0; i < 28; ++i) {
		_obj[i].s = mkSphere(12, 12);
	}

	// get all textures.
	id[0] 	= getTexFromBMP("images/2k-sun.bmp");
	id[1] 	= getTexFromBMP("images/2k-mercury.bmp");
	id[2] 	= getTexFromBMP("images/2k-venus-surface.bmp");
	id[3] 	= getTexFromBMP("images/2k-earth-daymap.bmp");
	id[4] 	= getTexFromBMP("images/2k-moon.bmp");
	id[5] 	= getTexFromBMP("images/2k-mars.bmp");
	id[6] 	= getTexFromBMP("images/moons/phobos.bmp");
	id[7] 	= getTexFromBMP("images/moons/deimos.bmp");
	id[8] 	= getTexFromBMP("images/2k-jupiter.bmp");
	id[9] 	= getTexFromBMP("images/moons/io.bmp");
	id[10] 	= getTexFromBMP("images/moons/europa.bmp");
	id[11] 	= getTexFromBMP("images/moons/ganymede.bmp");
	id[12] 	= getTexFromBMP("images/moons/callisto.bmp");
	id[13] 	= getTexFromBMP("images/2k-saturn.bmp");
	id[14] 	= getTexFromBMP("images/moons/enceladus.bmp");
	id[15] 	= getTexFromBMP("images/moons/dione.bmp");
	id[16] 	= getTexFromBMP("images/moons/rhea.bmp");
	id[17] 	= getTexFromBMP("images/moons/titan.bmp");
	id[18] 	= getTexFromBMP("images/moons/iapetus.bmp");
	id[19] 	= getTexFromBMP("images/2k-uranus.bmp");
	id[20] 	= getTexFromBMP("images/moons/ariel.bmp");
	id[21] 	= getTexFromBMP("images/moons/umbriel.bmp");
	id[22] 	= getTexFromBMP("images/moons/titania.bmp");
	id[23] 	= getTexFromBMP("images/moons/oberon.bmp");
	id[24] 	= getTexFromBMP("images/2k-neptune.bmp");
	id[25] 	= getTexFromBMP("images/moons/triton.bmp");
	id[26] 	= getTexFromBMP("images/pluto.bmp");
	id[27] 	= getTexFromBMP("images/moons/charon.bmp");
	// https://upload.wikimedia.org/wikipedia/commons/4/4f/Moons_of_solar_system_v7.jpg
	// most of the biggest moons are here.

	// set texture to an object id and set all options.
	for (i = 0; i < 28; ++i) {
		setTexId(_obj[i].s, id[i]);
		disableSurfaceOption(_obj[i].s, SO_USE_COLOR);
		if (_use_tex)
			enableSurfaceOption(_obj[i].s, SO_USE_TEXTURE);
		if (_use_lighting)
			enableSurfaceOption(_obj[i].s, SO_USE_LIGHTING);
	}
	get_params();
	atexit(sortie);
}

static void get_params(void) {
	int i;
	FILE * f;
	f = fopen("vars.txt", "r");
	for (i = 0; i < 28; ++i) {
		fscanf(f, "%d %f %f %f %f %f %f %f %f %f %f", &_obj[i].id,
				&_obj[i].vars[0], &_obj[i].vars[1], &_obj[i].vars[2], &_obj[i].vars[3], &_obj[i].vars[4],
				&_obj[i].vars[5], &_obj[i].vars[6], &_obj[i].vars[7], &_obj[i].vars[8], &_obj[i].vars[9]);
		// debug
		/*fprintf(stderr, "%d %f %f %f %f %f %f %f %f %f %f\n", _obj[i].id,
				_obj[i].vars[0], _obj[i].vars[1], _obj[i].vars[2], _obj[i].vars[3], _obj[i].vars[4],
				_obj[i].vars[5], _obj[i].vars[6], _obj[i].vars[7], _obj[i].vars[8], _obj[i].vars[9]);*/
	}
	fclose(f);
}

// get sign for further move calculation.
static int get_sign(float x, float y) {
	if (x > y)
		return 1;
	return -1;
}

// move to the coordinate.
static void move_to(float posx, float posy , float posz){
	float epsilon = 0.1f; // precision for comparing
	if (fabsf(_cam.x - posx) > epsilon) {
		_cam.x = _cam.x - (get_sign(_cam.x, posx) * 0.09f);
	}
	if (fabsf(_cam.y - posy) > epsilon) {
		_cam.y = _cam.y - (get_sign(_cam.y, posy) * 0.09f);
	}
	if (fabsf(_cam.z - posz) > epsilon) {
		_cam.z = _cam.z - (get_sign(_cam.z, posz) * 0.09f);
	}
}

// go to a choosen object (planet, star, pluto(dwarf))
static void goto_obj(float * mvMat) {
	switch (_p) {
		case 1: // MERCURY
			_a = 0; // reset angle to 0.
			lookAt(mvMat, _cam.x, _cam.y, _cam.z, _obj[1].vars[3], _obj[1].vars[4], _obj[1].vars[5], 0.0f, 1.0f, 0.0f);
			move_to(_obj[1].vars[3], _obj[1].vars[4], 0.5f);
			break;
		case 2: // VENUS
			_a = 0;
			lookAt(mvMat, _cam.x, _cam.y, _cam.z, _obj[2].vars[3], _obj[2].vars[4], _obj[2].vars[5], 0.0f, 1.0f, 0.0f);
			move_to(_obj[2].vars[3], _obj[2].vars[4], 1.0f);
			break;
		case 3: // EARTH
			_a = 0;
			lookAt(mvMat, _cam.x, _cam.y, _cam.z, _obj[3].vars[3], _obj[3].vars[4], _obj[3].vars[5], 0.0f, 1.0f, 0.0f);
			move_to(_obj[3].vars[3], _obj[3].vars[4], 1.0f);
			break;
		case 4: // MARS
			_a = 0;
			lookAt(mvMat, _cam.x, _cam.y, _cam.z, _obj[5].vars[3], _obj[5].vars[4], _obj[5].vars[5], 0.0f, 1.0f, 0.0f);
			move_to(_obj[5].vars[3], _obj[5].vars[4], 0.5f);
			break;
		case 5: // JUPITER
			_a = 0;
			lookAt(mvMat, _cam.x, _cam.y, _cam.z, _obj[8].vars[3], _obj[8].vars[4], _obj[8].vars[5], 0.0f, 1.0f, 0.0f);
			move_to(_obj[8].vars[3], _obj[8].vars[4], 8.0f);
			break;
		case 6: // SATURN
			_a = 0;
			lookAt(mvMat, _cam.x, _cam.y, _cam.z, _obj[13].vars[3], _obj[13].vars[4], _obj[13].vars[5], 0.0f, 1.0f, 0.0f);
			move_to(_obj[13].vars[3], _obj[13].vars[4], 8.0f);
			break;
		case 7: // URANUS
			_a = 0;
			lookAt(mvMat, _cam.x, _cam.y, _cam.z, _obj[19].vars[3], _obj[19].vars[4], _obj[19].vars[5], 0.0f, 1.0f, 0.0f);
			move_to(_obj[19].vars[3], _obj[19].vars[4], 5.0f);
			break;
		case 8: // NEPTUNE
			_a = 0;
			lookAt(mvMat, _cam.x, _cam.y, _cam.z, _obj[24].vars[3], _obj[24].vars[4], _obj[24].vars[5], 0.0f, 1.0f, 0.0f);
			move_to(_obj[24].vars[3], _obj[24].vars[4], 4.0f);
			break;
		case 9: // PLUTO
			_a = 0;
			lookAt(mvMat, _cam.x, _cam.y, _cam.z, _obj[26].vars[3], _obj[26].vars[4], _obj[26].vars[5], 0.0f, 1.0f, 0.0f);
			move_to(_obj[26].vars[3], _obj[26].vars[4], 0.4f);
			break;
		case 0: // SUN
			_a = 0;
			lookAt(mvMat, _cam.x, _cam.y, _cam.z, _obj[0].vars[3], _obj[0].vars[4], _obj[0].vars[5], 0.0f, 1.0f, 0.0f);
			move_to(_obj[0].vars[3], _obj[0].vars[4], 10.0f);
			break;
	}
}

static void draw_object(float * nmv, float * projMat, surface_t * obj,
		float ma, float mx, float my,
		float tx, float ty, float tz, float s,
		float ra, float rx, float ry) {
	rotate(nmv, ma, mx, my, 0); // orbit movement
	translate(nmv, tx, ty, tz);
	scale(nmv, s, s, s);
	rotate(nmv, ra, rx, ry, 0); // rotation
	transform_n_raster(obj, nmv, projMat);
}

/*!\brief la fonction appelée à chaque display. */
void draw(void) {
	static double t0 = 0, t, dt;
	int i;
	float mvMat[16], projMat[16], nmv[16], cpy[16];

	t 	= gl4dGetElapsedTime();
	dt 	= (t - t0) / 1000.0;
	t0 	= t;
	/* effacer l'écran et le buffer de profondeur */
	gl4dpClearScreen();
	clearDepth();
	/* des macros facilitant le travail avec des matrices et des
	 * vecteurs se trouvent dans la bibliothèque GL4Dummies, dans le
	 * fichier gl4dm.h */
	/* charger un frustum dans projMat */
	MFRUSTUM(projMat, -0.005f, 0.005f, -0.005f, 0.005f, 0.01f, 1000.0f);
	/* charger la matrice identité dans model-view */
	MIDENTITY(mvMat);

	// Y ----> 1.0 - (_ym - (_wH >> 1)) / (float)_wH * 5
	if (_movement)
		lookAt(mvMat, _cam.x, _cam.y, _cam.z, _cam.x - sin(_cam.theta), 1.0, _cam.z - cos(_cam.theta), 0.0, 1.0, 0.0);
	else
		goto_obj(mvMat);

	// SUN
	memcpy(nmv, mvMat, sizeof nmv); /* copie mvMat dans nmv */
	draw_object(nmv, projMat, _obj[0].s, _r/_obj[0].vars[0], _obj[0].vars[1], _obj[0].vars[2], _obj[0].vars[3],
			_obj[0].vars[4], _obj[0].vars[5], _obj[0].vars[6], _obj[0].vars[7], _obj[0].vars[8], _obj[0].vars[9]);

	for (i = 1; i < 28; ++i) {
		if(_obj[i].id == 1) { // if object is a planet
			memcpy(nmv, mvMat, sizeof nmv);
			draw_object(nmv, projMat, _obj[i].s, _a/_obj[i].vars[0], _obj[i].vars[1], _obj[i].vars[2], _obj[i].vars[3],
					_obj[i].vars[4], _obj[i].vars[5], _obj[i].vars[6], _r/_obj[i].vars[7], _obj[i].vars[8], _obj[i].vars[9]);
			if (i <= 27) {
				if (_obj[i+1].id == 2)
					memcpy(cpy, nmv, sizeof cpy);
			}
		} else if (_obj[i].id == 2) { // if object is a moon
			memcpy(nmv, cpy, sizeof nmv);
			draw_object(nmv, projMat, _obj[i].s, _r/_obj[i].vars[0], _obj[i].vars[1], _obj[i].vars[2], _obj[i].vars[3],
					_obj[i].vars[4], _obj[i].vars[5], _obj[i].vars[6], _obj[i].vars[7], _obj[i].vars[8], _obj[i].vars[9]);
		}
	}

	/* déclarer qu'on a changé (en bas niveau) des pixels du screen  */
	gl4dpScreenHasChanged();
	/* fonction permettant de raffraîchir l'ensemble de la fenêtre*/
	gl4dpUpdateScreen(NULL);

	if (!_pause){
		_a += ((360.0 * dt) / 60) * _s; // 360 in 1 minute so 1 day = 1 min
		_r += ((360.0 * dt) / 60) * _s; // 360 in 1 minute so 1 day = 1 min
	}
}

/*!\brief intercepte l'événement clavier pour modifier les options. */
void key(int keycode) {
	double step = 5.0;
	int i;
	switch(keycode) {
		case GL4DK_UP:
			if (_movement)
				_cam.y += step;
			break;
		case GL4DK_DOWN:
			if (_movement)
				_cam.y -= step;
			break;
		case GL4DK_RIGHT:
			if (!_pause)
				_s += 30.0f;
			break;
		case GL4DK_LEFT:
			if (!_pause)
				_s -= 30.0f;
			break;
		case GL4DK_w:
			if (_movement) {
				_cam.x += -_v * step * sin(_cam.theta);
				_cam.z += -_v * step * cos(_cam.theta);
			}
			break;
		case GL4DK_s:
			if (_movement) {
				_cam.x += _v * step * sin(_cam.theta);
				_cam.z += _v * step * cos(_cam.theta);
			}
			break;
		case GL4DK_a:
			if (_movement)
				_cam.theta += step * 0.01;
			break;
		case GL4DK_d:
			if (_movement)
				_cam.theta -= step * 0.01;
			break;
		case GL4DK_p:
			if (_pause == 0)
				_pause = 1;
			else
				_pause = 0;
			break;
		case GL4DK_MINUS:
			_v -= 0.01f;
			if (_v < 0.01f){
				_v = 0.01f;
			}
			break;
		case GL4DK_EQUALS:
			_v += 0.01f;
			if (_v > 0.5f){
				_v = 0.5f;
			}
			break;
		case GL4DK_o:
			if (_movement) {
				if (!_overview) {
					_overview = !_overview;
					_cam.x = 0.0f;
					_cam.y = 200.0f;
					_cam.z = 0.0f;
					_cam.theta = 0.0f;
				} else {
					_overview = !_overview;
					_cam.x = 0.0f;
					_cam.y = 1.0f;
					_cam.z = 10.0f;
					_cam.theta = 0.0f;
				}
			}
			break;
		case GL4DK_r:
			_a = _r;
			_p = -1;
			_pause = 0;
			_movement = 1;
			_overview = 0;
			break;
		case GL4DK_m:
			_s = 1;
			break;
		case GL4DK_q:
			exit(0);
			break;
		case GL4DK_0:
			_p = 0;
			/*_pause = 1;*/
			_movement = 0;
			_overview = 0;
			break;
		case GL4DK_1:
			_p = 1;
			/*_pause = 1;*/
			_movement = 0;
			_overview = 0;
			break;
		case GL4DK_2:
			_p = 2;
			/*_pause = 1;*/
			_movement = 0;
			_overview = 0;
			break;
		case GL4DK_3:
			_p = 3;
			/*_pause = 1;*/
			_movement = 0;
			_overview = 0;
			break;
		case GL4DK_4:
			_p = 4;
			/*_pause = 1;*/
			_movement = 0;
			_overview = 0;
			break;
		case GL4DK_5:
			_p = 5;
			/*_pause = 1;*/
			_movement = 0;
			_overview = 0;
			break;
		case GL4DK_6:
			_p = 6;
			/*_pause = 1;*/
			_movement = 0;
			_overview = 0;
			break;
		case GL4DK_7:
			_p = 7;
			/*_pause = 1;*/
			_movement = 0;
			_overview = 0;
			break;
		case GL4DK_8:
			_p = 8;
			/*_pause = 1;*/
			_movement = 0;
			_overview = 0;
			break;
		case GL4DK_9:
			_p = 9;
			/*_pause = 1;*/
			_movement = 0;
			_overview = 0;
			break;
		case GL4DK_t: /* 't' la texture */
			_use_tex = !_use_tex;
			if(_use_tex) {
				for (i = 0; i < 28; ++i) {
					enableSurfaceOption(_obj[i].s, SO_USE_TEXTURE);
				}
			} else {
				for (i = 0; i < 28; ++i) {
					disableSurfaceOption(_obj[i].s, SO_USE_TEXTURE);
				}
			}
			break;
		case GL4DK_c: /* 'c' utiliser la couleur */
			_use_color = !_use_color;
			if(_use_color) {
				for (i = 0; i < 28; ++i) {
					enableSurfaceOption(_obj[i].s, SO_USE_COLOR);
				}
			} else {
				for (i = 0; i < 28; ++i) {
					disableSurfaceOption(_obj[i].s, SO_USE_COLOR);
				}
			}
			break;
		case GL4DK_l: /* 'l' utiliser l'ombrage par la méthode Gouraud */
			_use_lighting = !_use_lighting;
			if(_use_lighting) {
				for (i = 0; i < 28; ++i) {
					enableSurfaceOption(_obj[i].s, SO_USE_LIGHTING);
				}
			} else {
				for (i = 0; i < 28; ++i) {
					disableSurfaceOption(_obj[i].s, SO_USE_LIGHTING);
				}
			}
			break;
		default: break;
	}
}

// handler of mouse motion.
static void pmotion(int x, int y) {
	if (_movement){
		_xm = x;
		_ym = y;
		_cam.theta = -(_xm - (_wW >> 1)) / (float)_wW*10;
	}
}

// handler of mouse buttons.
static void mouse(int button, int state, int x, int y) {
	if (_movement){
		double dtheta = M_PI;
		if (button == GL4D_BUTTON_LEFT)
			_cam.y += dtheta;
		if (button == GL4D_BUTTON_RIGHT)
			_cam.y -= dtheta;
		if (button == GL4D_BUTTON_MIDDLE)
			_cam.y = 1.0f;
	}
}

/*!\brief à appeler à la sortie du programme. */
void sortie(void) {
	int i;
	Mix_CloseAudio();
	Mix_Quit();
	for (i = 0; i < 28; ++i) {
		if(_obj[i].s){
			freeSurface(_obj[i].s);
			_obj[i].s = NULL;
		}
	}
	if (bsound)
		Mix_FreeChunk(bsound);
	bsound = NULL;
	/* libère tous les objets produits par GL4Dummies, ici
	 * principalement les screen */
	gl4duClean(GL4DU_ALL);
}
