/* The Solar System, simulated on the web. */

#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include "cgic.h"
#include "gd.h"
#include "gdfontt.h"

#define OBJECTS_FILE "/CHANGE/THIS/FILE/sss/objects.dat"
#define PROGRAM_URL "/CHANGE/THIS/URL/cgi-bin/nph-sss"
#define PROGRAM_DATA_PATH "/CHANGE/THIS/PATH/sss/"

int windowSize = 200;

typedef struct {
  double x,y;
  double vx,vy;
  double mass;
  double radius;
  char name[20];
} object;

int objectsTotal = 0;

double gConstant;

double maxX;
double maxY;
double minX;
double minY;
double width;
double magnification = 1.0;

#define OBJECTS_MAX 100

object objects[OBJECTS_MAX];


#define SCALEX(x) (((x-minX)/(maxX-minX))*windowSize) 
#define UNSCALEX(x) ((x*(maxX-minX)/windowSize)+minX)
#define SCALEY(y) (((y-minY)/(maxY-minY))*windowSize)
#define UNSCALEY(y) ((y*(maxY-minY)/windowSize)+minY)

#ifndef PI
#define PI 3.141592653
#endif /* PI */

int Page(int firstTime);
int Canvas();
int Select();
int ZoomIn();
int ZoomOut();
int TimePlus();
int TimeMinus();
int Resume();
int Nemesis();
int UnCenter();
int SizePlus();
int SizeMinus();
int Form();
void AccountForCenter();
void ObjectsSetup();

int sessionId;

void SelectSessionId();

void WriteSessionState();
void ReadSessionState();

void ParseSessionId();

char *pathInfoP;

int cgiMain() {
	int i;
	pathInfoP = cgiPathInfo;
	ParseSessionId();
	if (!strcmp(pathInfoP, "/canvas.gif")) {
		return Canvas();
	} else if (!strcmp(pathInfoP, "/form")) {
		return Form();
	} else if (!strcmp(pathInfoP, "/zoomin")) {
		return ZoomIn();	
	} else if (!strcmp(pathInfoP, "/zoomout")) {
		return ZoomOut();	
	} else if (!strcmp(pathInfoP, "/timeplus")) {
		return TimePlus();	
	} else if (!strcmp(pathInfoP, "/timeminus")) {
		return TimeMinus();	
	} else if (!strcmp(pathInfoP, "/sizeplus")) {
		return SizePlus();	
	} else if (!strcmp(pathInfoP, "/sizeminus")) {
		return SizeMinus();	
	} else if (!strcmp(pathInfoP, "/uncenter")) {
		return UnCenter();	
	} else if (!strcmp(pathInfoP, "/nemesis")) {
		return Nemesis();	
	} else if (!strcmp(pathInfoP, "/select")) {
		return Select();	
	} else if (!strcmp(pathInfoP, "/resume")) {
		return Resume();	
	} else {
		return Page(1);
	}
}

int centerFloating = 0;
int centerFloatingObject;
double timeStep=0.5;

int Page(int firstTime) {
	int i;
	if (firstTime) {
		SelectSessionId();
		ObjectsSetup();
		WriteSessionState();
	}
	fprintf(cgiOut,
		"HTTP/1.0 200 OK");
	fprintf(cgiOut,
		"Pragma: no-cache\n");
	cgiHeaderContentType("text/html");
	fprintf(cgiOut, 
		"<html><head><title>Solar System Simulator</title></head>\n");
	fprintf(cgiOut, 
		"<body><h1>Solar System Simulator</h1>\n");
	fprintf(cgiOut,
		"Click on any planet to center the display\n");
	fprintf(cgiOut,
		"on that object, or select one of the options below.\n<p>");
	fprintf(cgiOut,
		"<em>Notes:</em> <strong>zoom in to watch the inner solar system</strong>,\n");
	fprintf(cgiOut,
		"or increase the time step many times to make the outer planets\n");
	fprintf(cgiOut, 
		"more interesting to watch.\n");
	fprintf(cgiOut, "<strong>Large time steps will destabilize the moon.</strong>\n");
	fprintf(cgiOut,
		"Even larger time steps may confuse the planets as well.<p>\n");
	fprintf(cgiOut,
		"The stop button can be used to pause the simulation. Select\n");
	fprintf(cgiOut,
		"Resume to resume the simulation after a stop.<p>");

	/* Imagemap link */
	fprintf(cgiOut, 
		"<br><a href=\"%s/ID:%d/select\">\n",
		PROGRAM_URL,
		sessionId);

	/* Image */
	fprintf(cgiOut,
		"<img src=\"%s/ID:%d/canvas.gif\" ISMAP></a>\n",
		PROGRAM_URL, 
		sessionId);
	fprintf(cgiOut, "<p>\n");

	/* Status display */
	if (centerFloating) {
		fprintf(cgiOut,
			"Centered on: %s ", 
			objects[centerFloatingObject].name);
	}
	fprintf(cgiOut,
		"Magnification: %fx ", magnification);
	fprintf(cgiOut,
		"Time Step: %f days<br>", timeStep);

	/* Various controls */
	fprintf(cgiOut,
		"<a href=\"%s/ID:%d/zoomin\">Zoom In x2</a> ",
		PROGRAM_URL,
		sessionId);
	fprintf(cgiOut,
		"<a href=\"%s/ID:%d/zoomout\">Zoom Out x2</a> ",
		PROGRAM_URL,
		sessionId);
	fprintf(cgiOut,
		"<a href=\"%s/ID:%d/timeplus\">Time Step x2</a> ",
		PROGRAM_URL,
		sessionId);
	fprintf(cgiOut,
		"<a href=\"%s/ID:%d/timeminus\">Time Step /2</a> ",
		PROGRAM_URL,
		sessionId);
	/* This code works, but Netscape is very difficult to use
		if the window is any larger than 200x200, at least on
		my system. Feel free to remove the #if/#endif pair
		and see what happens. */
#if 0
	if (windowSize < 1600) {
		fprintf(cgiOut,
			"<a href=\"%s/ID:%d/sizeplus\">Window Size x2</a> ",
			PROGRAM_URL,
			sessionId);
	}
	if (windowSize > 25) {
		fprintf(cgiOut,
			"<a href=\"%s/ID:%d/sizeminus\">Window Size /2</a> ",
			PROGRAM_URL,
			sessionId);
	}
#endif
	fprintf(cgiOut,
		"<a href=\"%s/ID:%d/nemesis\">NEMESIS</a> ",
		PROGRAM_URL,
		sessionId);
	fprintf(cgiOut,
		"<a href=\"%s/\">Restart</a> ",
		PROGRAM_URL);
	fprintf(cgiOut,
		"<a href=\"%s/ID:%d/resume\">Resume</a> ",
		PROGRAM_URL,
		sessionId);
	if (centerFloating) {
		fprintf(cgiOut,
			"<a href=\"%s/ID:%d/uncenter\">Uncenter</a>",
			PROGRAM_URL,
			sessionId);
	}
	fprintf(cgiOut, "<p>\n");

	/* A form, for skilled users */
	fprintf(cgiOut, "<hr>Use this form if you wish to make several changes quickly.<p>\n");
	fprintf(cgiOut, "<form action=\"%s/ID:%d/form\">\n",
		PROGRAM_URL,
		sessionId);
	fprintf(cgiOut, "<input type=submit value=\"Submit Changes\"> <input type=reset value=\"Clear Form\"><br>\n");
	fprintf(cgiOut, "<input type=text name=magnification value=\"%f\"> Magnification<br>\n", magnification);
	fprintf(cgiOut, "<input type=text name=timestep value=\"%f\"> Time Step (days)<br>\n", timeStep);
	fprintf(cgiOut, "Center Display On:<br>");
	fprintf(cgiOut, "<select name=center>\n");
	if (centerFloating) {
		fprintf(cgiOut, "<option value=\"-1\"> Nothing<br>\n");
	} else {	
		fprintf(cgiOut, "<option value=\"-1\" selected> Nothing<br>\n");
	}
	for (i=0; (i < objectsTotal); i++) {
		fprintf(cgiOut, "<option value=\"%d\" ", i);
		if (centerFloating && (centerFloatingObject == i)) {
			fprintf(cgiOut, "selected");
		}
		fprintf(cgiOut, "> %s<br>\n", objects[i].name);
	}
	fprintf(cgiOut, "</select>\n<br>");
	fprintf(cgiOut, "</form>\n");	
	fprintf(cgiOut, "</body></html>\n");
	return 0;
}	

int Canvas() {
	int i, j;
	int x,y;
	int done = 0;
	int labels = 0;
	int trails = 0;
	gdImagePtr im;
	int black;
	int white;
	int blue;
	fprintf(cgiOut,
		"HTTP/1.0 200 OK\n");

	cgiHeaderContentType(
		"multipart/x-mixed-replace;boundary=goober");
	while (!done) {
		/* Re-read the session state, do the computations
			as quickly as possible, and write the state
			again. */
		ReadSessionState();
		for (i=0; (i < objectsTotal); i++) {
			double x,y;
			for (j=0; (j < objectsTotal); j++) {
				if (i != j) {
					double dist;
					double pull;
					dist = hypot(
						objects[i].x - objects[j].x,
						objects[i].y - objects[j].y);
					if (dist != 0.0) {
						pull=(gConstant * 
							objects[j].mass /
							(dist * dist));
						objects[i].vx += 
							(objects[j].x - 
								objects[i].x) /
							dist * pull * timeStep;
						objects[i].vy += 
							(objects[j].y - 
								objects[i].y) /
							dist * pull * timeStep; 
					}
				}
			}
		}
		for (i=0; (i < objectsTotal); i++) {
			objects[i].x += (objects[i].vx * timeStep);
			objects[i].y += (objects[i].vy * timeStep);
		} 
		WriteSessionState();
		AccountForCenter();
		/* Now build a GIF */
		im = gdImageCreate(windowSize, windowSize);
		black = gdImageColorAllocate(im, 0, 0, 0);	
		white = gdImageColorAllocate(im, 255, 255, 255);	
		blue = gdImageColorAllocate(im, 192, 192, 255);	
		for (i=0; (i<objectsTotal); i++) {
			object *o = &objects[i];
			double val = log10(o->radius);
			int x = SCALEX(o->x);
			int y = SCALEY(o->y);
			gdImageArc(im, x, y,
				val, val, 0, 360, white);
			gdImageString(im, gdFontTiny,
				x - (gdFontTiny->w *
				strlen(o->name) / 2), 
				y + val,
				o->name, blue);
		}
		sleep(1);
		fprintf(cgiOut, "\n--goober\n");
		fprintf(cgiOut, "Content-type: image/gif\n\n");
		gdImageGif(im, cgiOut);
		gdImageDestroy(im);	
		fflush(cgiOut);
	}
	fprintf(cgiOut, "\n--goober--\n");
	return 0;
}

void CenterClosest(int x, int y);

int Select() {
	int x, y;
	ReadSessionState();
	if (sscanf(cgiQueryString, "%d,%d", &x, &y) != 2) {
		cgiHeaderContentType("text/html");
		fprintf(cgiOut,
			"HTTP/1.0 200 OK\n");
		fprintf(cgiOut, "<h1>Bad Click: %s</h1>\n", cgiQueryString);
		return 0;		
	}	
	CenterClosest(x, y);
	WriteSessionState();
	return Page(0);
}

int UnCenter() {
	ReadSessionState();
	centerFloating = 0;
	WriteSessionState();
	return Page(0);
}

int ZoomIn() {
	ReadSessionState();
	magnification *= 2.0;
	WriteSessionState();
	return Page(0);
}

int ZoomOut() {
	ReadSessionState();
	magnification /= 2.0;
	WriteSessionState();
	return Page(0);
}

int TimePlus() {
	ReadSessionState();
	timeStep *= 2.0;
	WriteSessionState();
	return Page(0);
}

int TimeMinus() {
	ReadSessionState();
	timeStep /= 2.0;
	WriteSessionState();
	return Page(0);
}

int SizePlus() {
	ReadSessionState();
	if (windowSize < 200) {
		windowSize *= 2;
	}
	WriteSessionState();
	return Page(0);
}

int SizeMinus() {
	ReadSessionState();
	if (windowSize) {
		windowSize /= 2;
	}
	WriteSessionState();
	return Page(0);
}

int Resume() {
	ReadSessionState();
	return Page(0);
}

int Nemesis() {
	double largestMass = 1.0, largestRadius = 1.0;
	int i;
	object *o;
	ReadSessionState();
	if (objectsTotal == OBJECTS_MAX) {
		/* Don't break the simulation */
		return 0;
	}
	/* Wreak havoc: introduce an object as
		large as the largest object in the
		system and send it whizzing through. */
	for (i=0; (i < objectsTotal); i++) {
		if ((!i) || (objects[i].mass > largestMass)) {
			largestMass = objects[i].mass;
		}
		if ((!i) || (objects[i].radius > largestRadius)) {
			largestRadius = objects[i].radius;
		}
	}
	o = &objects[objectsTotal];
	/* Start in the upper left corner of the simulation */
	o->x = minX;
	o->y = minY;
	/* Fast enough to cross in 365 days */
	o->vx = (maxX - minX) / 365.0;
	o->vy = (maxY - minY) / 365.0;
	/* Very big, very nasty */
	o->mass = largestMass * 2;
	o->radius = largestRadius * 2;
	strcpy(o->name, "NEMESIS");	
	objectsTotal++;
	WriteSessionState();
	return Page(0);
}

void SelectSessionId() {
	FILE *in;
	FILE *out;	
	char s[256];
	/* Come up with a new, never-before-used session id. */
	sprintf(s, "%s/id", PROGRAM_DATA_PATH);

	in = fopen(s, "r");
	
	if (!fscanf(in, "%d\n", &sessionId)) {
		sessionId = 0;
	}	
	fclose(in);

	out = fopen(s, "w");
	fprintf(out, "%d\n", sessionId+1);
	fclose(out);
}

void ParseSessionId() {
	if (!strncmp(pathInfoP, "/ID:", 4)) {
		char *next;
		sessionId = atoi(pathInfoP + 4);
		next = strchr(pathInfoP + 4, '/');
		if (next) {
			pathInfoP = next;
		} else {
			pathInfoP = "";
		}
	}
}

void CenterClosest(int x, int y) {
	double smallestDist;
	int i;
	double centerX, centerY;
	centerFloating = 1;
	AccountForCenter();
	centerX = UNSCALEX(x);
	centerY = UNSCALEY(y);
	for (i=0; (i<objectsTotal); i++) {
		double dist;
		dist = hypot(objects[i].x - 
			centerX,objects[i].y - centerY);
		if (dist < smallestDist || (!i)) {
			smallestDist = dist;
			centerFloatingObject = i;
		}
	}
}

void AccountForCenter() {
	if (centerFloating) {
		minX = objects[centerFloatingObject].x -
			width / 2.0 / magnification;
		maxX = objects[centerFloatingObject].x +
			width / 2.0 / magnification;
		minY = objects[centerFloatingObject].y -
			width / 2.0 / magnification;
		maxY = objects[centerFloatingObject].y +
			width / 2.0 / magnification;
	} else {
		double cX, cY;
		cX = (minX + maxX) / 2.0;
		cY = (minY + maxY) / 2.0;
		minX = cX - width / 2.0 / magnification;
		maxX = cX + width / 2.0 / magnification;
		minY = cY - width / 2.0 / magnification;
		maxY = cY + width / 2.0 / magnification;
	}
}
			
int Form() {
	int center;
	ReadSessionState();

	/* cgic makes this very easy. Use the current values
		as the defaults. */
	cgiFormDouble("magnification", &magnification, magnification);	
	cgiFormDouble("timestep", &timeStep, timeStep);

	/* Find out which object was centered on, if any. */
	cgiFormInteger("center", &center, -1);
	if (center == -1) {
		centerFloating = 0; 
	} else if ((center >= 0) && (center < objectsTotal)) {
		centerFloating = 1;
		centerFloatingObject = center;
	}
	WriteSessionState();
	return Page(0);	
}

void ObjectsSetup(void) {
	FILE* in;
	object *o;
	char line[256];
	objectsTotal = 0;
	if (!(in = fopen(OBJECTS_FILE, "r"))) {
		exit(1);
	}
	do {
		if (!fgets(line, 256, in)) {
			return;
		}
	} while ((line[0] == '#') || (line[0] == '\n'));
	gConstant = atof(line);
	while (!feof(in)) {
		int ch;
		o = &objects[objectsTotal];
		if (!fgets(line, 256, in)) {
			break;
		}
		if ((line[0] == '#') || (line[0] == '\n')) {
			continue;
		}
		if (sscanf(line,"%20s %lf %lf %lf %lf %lf %lf",
			o->name,
			&(o->x), &(o->y),
			&(o->vx), &(o->vy),
			&(o->mass), &(o->radius)) < 7)
		{
			continue;
		}
		objectsTotal++;
		if (objectsTotal == OBJECTS_MAX) {
			break;
		}
	}
	fclose(in);
}

void WriteSessionState() {
	FILE *out;	
	char sold[256], snew[256];
	int i;
	/* Write to a different filename initially, then
		delete the old and rename the new at the
		end. This reduces the probability that an
		untimely kill signal will cause problems. */
	sprintf(sold, "%s/%d.sav", PROGRAM_DATA_PATH, sessionId);
	sprintf(snew, "%s/%d.dtn", PROGRAM_DATA_PATH, sessionId);
	out = fopen(snew, "w");
	if (!out) {
		/* Can't access memo file */
		return;
	}
	fprintf(out, "%d\n", objectsTotal);
	fprintf(out, "%e\n", gConstant);
	fprintf(out, "%d\n", windowSize);
	fprintf(out, "%d\n", centerFloating);
	fprintf(out, "%d\n", centerFloatingObject);
	fprintf(out, "%e\n", timeStep);
	fprintf(out, "%e\n", magnification);
	for (i=0; (i < objectsTotal); i++) {
		object *o = &objects[i];
		fprintf(out, "%e %e %e %e %e %e %20s\n",
			o->x, o->y, o->vx, o->vy,
			o->mass, o->radius, o->name);
	}
	fclose(out);
	/* OK, swap the files quickly. */
	unlink(sold);
	rename(snew, sold);
}
		
void ReadSessionState() {
	FILE *in;	
	char s[256];
	int i;
	maxX=0;
	minX=0;
	maxY=0;
	minY=0;
	sprintf(s, "%s/%d.sav", PROGRAM_DATA_PATH, sessionId);
	in = fopen(s, "r");
	if (!in) {
		/* Can't access memo file */
		return;
	}
	fscanf(in, "%d\n", &objectsTotal);
	fscanf(in, "%lf\n", &gConstant);
	fscanf(in, "%d\n", &windowSize);
	fscanf(in, "%d\n", &centerFloating);
	fscanf(in, "%d\n", &centerFloatingObject);
	fscanf(in, "%lf\n", &timeStep);
	fscanf(in, "%lf\n", &magnification);
	for (i=0; (i < objectsTotal); i++) {
		object *o = &objects[i];
		fscanf(in, "%lf %lf %lf %lf %lf %lf %s\n",
			&o->x, &o->y, &o->vx, &o->vy,
			&o->mass, &o->radius, &o->name);
		if (o->x < minX || !objectsTotal) {
			minX = o->x;
		}
		if (o->x > maxX || !objectsTotal) {
			maxX = o->x;
		}
		if (o->y < minY || !objectsTotal) {
			minY = o->y;
		}
		if (o->y > maxY || !objectsTotal) {
			maxY = o->y;
		}
	}
	fclose(in);
	if ((-maxX) < minX) {
		minX = -maxX;
	} else {
		maxX = -minX;
	}
	if ((-maxY) < minY) {
		minY = - maxY;
	} else {
		maxY = - minY;
	}
	if (minX < minY) {
		minY = minX;
	} else {
		minX = minY;
	}
	if (maxX > maxY) {
		 maxY = maxX;
	} else {
		maxX = maxY;
	}
	width = (maxX - minX);
}
