/**********************************************************************
  Sphere - Class for drawing spheres in OpenGL

  Copyright (C) 2007-2009 Jean Brefort <jean.brefort@normalesup.org>
  Copyright (C) 2006,2007 Benoit Jacob <jacob@math.jussieu.fr>

  This file is part of the Avogadro molecular editor project.
  For more information, see <http://avogadro.sourceforge.net/>

  Avogadro is free software; you can redistribute it and/or modify 
  it under the terms of the GNU General Public License as published by 
  the Free Software Foundation; either version 2 of the License, or 
  (at your option) any later version.

  Avogadro is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  02110-1301, USA.
 **********************************************************************/

#include "config.h"
#include "sphere.h"
#include "vector.h"
#include <GL/gl.h>
#include <cmath>

namespace gcu {

class SpherePrivate
{
public:
	SpherePrivate (): vertexBuffer(0), indexBuffer(0), displayList(0), isValid(false) {}

	/** Pointer to the buffer storing the vertex array */
	Vector3f *vertexBuffer;
	/** Pointer to the buffer storing the indices */
	unsigned short *indexBuffer;
	/** The id of the OpenGL display list */
	GLuint displayList;
	/** the detail-level of the sphere. Must be at least 0.
	* If 0, the sphere is an octahedron. If >=1, this number is
	* interpreted as the number of sub-edges into which
	* each edge of the icosahedron must be split. So the
	* number of faces of the sphere is simply:
	* 20 * detail^2. When detail==1, the sphere is just the
	* icosahedron */
	int detail;

	bool isValid;
};

Sphere::Sphere (int detail): d (new SpherePrivate)
{
	setup (detail);
}

Sphere::~Sphere ()
{
	freeBuffers ();
	if (d->displayList)
		glDeleteLists (d->displayList, 1);
	delete d;
}

void Sphere::freeBuffers ()
{
	if (d->indexBuffer) {
		delete[] d->indexBuffer;
		d->indexBuffer = 0;
	}
	if( d->vertexBuffer ) {
		delete[] d->vertexBuffer;
		d->vertexBuffer = 0;
	}
}

void Sphere::draw (Vector const &center, double radius) const
{
	glPushMatrix ();
	glTranslated (center.GetX (), center.GetY (), center.GetZ ());
	glScaled (radius, radius, radius);
	glCallList (d->displayList);
	glPopMatrix ();
}

#define USE_OCTAHEDRON_VERTEX(i) glNormal3fv(octahedronVertices[i]); \
                                 glVertex3fv(octahedronVertices[i]);

void Sphere::initialize ()
{
	if (d->detail < 0)
		return;

	// deallocate any previously allocated buffer
	freeBuffers ();
	d->isValid = false;
	int vertexCount = 0, indexCount = 0;

	if (d->detail == 0) {
		if (!d->displayList)
				d->displayList = glGenLists (1);
		if (!d->displayList)
				return;
		float octahedronVertices[6][3] = {{1, 0, 0},
										 {0, 1, 0},
										 {0, 0, 1},
										 {0, -1, 0},
										 {0, 0, -1},
										 {-1, 0, 0}};
		glNewList( d->displayList, GL_COMPILE );
		glBegin(GL_TRIANGLE_FAN);
		USE_OCTAHEDRON_VERTEX(0);
		USE_OCTAHEDRON_VERTEX(1);
		USE_OCTAHEDRON_VERTEX(2);
		USE_OCTAHEDRON_VERTEX(3);
		USE_OCTAHEDRON_VERTEX(4);
		USE_OCTAHEDRON_VERTEX(1);
		glEnd();
		glBegin(GL_TRIANGLE_FAN);
		USE_OCTAHEDRON_VERTEX(5);
		USE_OCTAHEDRON_VERTEX(1);
		USE_OCTAHEDRON_VERTEX(4);
		USE_OCTAHEDRON_VERTEX(3);
		USE_OCTAHEDRON_VERTEX(2);
		USE_OCTAHEDRON_VERTEX(1);
		glEnd();
		glEndList();
		d->isValid = true;
		return;
	}

	// compute number of vertices and indices
	vertexCount = (3 * d->detail + 1) * (5 * d->detail + 1);
	indexCount = (2 * (2 * d->detail + 1) + 2) * 5 * d->detail;

	// allocate memory for buffers
	d->vertexBuffer = new Vector3f[vertexCount];
	if (!d->vertexBuffer)
		return;
	d->indexBuffer = new unsigned short[indexCount];
	if (!d->indexBuffer)  {
		delete[] d->vertexBuffer;
		d->vertexBuffer = 0;
		return;
	}

	// build vertex buffer
	for (int strip = 0; strip < 5; strip++)
		for (int column = 1; column < d->detail; column++)
			for (int row = column; row <= 2 * d->detail + column; row++)
				computeVertex (strip, column, row);

	for (int strip = 1; strip < 5; strip++)
		for (int row = 0; row <= 3 * d->detail; row++)
			computeVertex (strip, 0, row);
		  

	for( int row = 0; row <= 2 * d->detail; row++ )
		computeVertex (0, 0, row);

	for( int row = d->detail; row <= 3 * d->detail; row++ )
		computeVertex (4, d->detail, row);

	// build index buffer
	unsigned int i = 0;
	for (int strip = 0; strip < 5; strip++)
		for (int column = 0; column < d->detail; column++) {
			int row = column;
			d->indexBuffer[i++] = indexOfVertex (strip, column, row);
			for (; row <= 2 * d->detail + column; row++) {
				d->indexBuffer[i++] = indexOfVertex (strip, column, row);
				d->indexBuffer[i++] = indexOfVertex (strip, column + 1, row + 1);
			}
			d->indexBuffer[i++] = indexOfVertex (strip, column + 1,
												2 * d->detail + column + 1);
		}

	// compile display list and free buffers
	if (!d->displayList)
		d->displayList = glGenLists( 1 );
	if (!d->displayList)
		return;
	glEnableClientState (GL_VERTEX_ARRAY);
	glEnableClientState (GL_NORMAL_ARRAY);
	glNewList (d->displayList, GL_COMPILE);
	glVertexPointer (3, GL_FLOAT, 0, d->vertexBuffer);
	glNormalPointer (GL_FLOAT, 0, d->vertexBuffer);
	glDrawElements (GL_TRIANGLE_STRIP, indexCount, GL_UNSIGNED_SHORT, d->indexBuffer);
	glEndList ();
	glDisableClientState (GL_VERTEX_ARRAY);
	glDisableClientState (GL_NORMAL_ARRAY);
	freeBuffers ();
	d->isValid = true;
}

unsigned short Sphere::indexOfVertex (int strip, int column, int row)
{
	return row + (3 * d->detail + 1) * (column + d->detail * strip);
}

void Sphere::computeVertex (int strip, int column, int row)
{
	strip %= 5;
	int next_strip = (strip + 1) % 5;

	// the index of the vertex we want to store the result in
	unsigned short index = indexOfVertex (strip, column, row);

	// reference to the vertex we want to store the result in
	Vector3f &vertex = d->vertexBuffer[index];

	// the "golden ratio", useful to construct an icosahedron
	const float phi = (1 + sqrtf(5.0f)) / 2;

	// the 12 vertices of the icosahedron
	const Vector3f northPole (0, 1, phi);
	const Vector3f northVertices[5] = {
		Vector3f (0, -1, phi),
		Vector3f (phi, 0, 1),
		Vector3f (1, phi, 0),
		Vector3f (-1, phi, 0),
		Vector3f (-phi, 0, 1) };
	const Vector3f southVertices[5] = {
		Vector3f (-1, -phi, 0),
		Vector3f (1, -phi, 0),
		Vector3f (phi, 0, -1),
		Vector3f (0, 1, -phi),
		Vector3f (-phi, 0, -1)
	};
	const Vector3f southPole (0, -1, -phi);

	// pointers to the 3 vertices of the face of the icosahedron
	// in which we are
	const Vector3f *v0, *v1, *v2;

	// coordinates of our position inside this face.
	// range from 0 to d->detail.
	int  c1, c2;

	// first, normalize the global coords row, column
	if (row >= 2 * d->detail && column == 0) {
		strip--;
		if (strip < 0)
			strip += 5;
		next_strip--;
		if (next_strip < 0)
			next_strip += 5;
		column = d->detail;
	}

	// next, determine in which face we are, and determine the coords
	// of our position inside this face
	if (row  <= d->detail) {
		v0 = &northVertices[strip];
		v1 = &northPole;
		v2 = &northVertices[next_strip];
		c1 = d->detail - row;
		c2 = column;
	} else if (row >= 2 * d->detail) {
		v0 = &southVertices[next_strip];
		v1 = &southPole;
		v2 = &southVertices[strip];
		c1 = row - 2 * d->detail;
		c2 = d->detail - column;
	} else if (row <= d->detail + column) {
		v0 = &northVertices[next_strip];
		v1 = &southVertices[next_strip];
		v2 = &northVertices[strip];
		c1 = row - d->detail;
		c2 = d->detail - column;
	} else {
		v0 = &southVertices[strip];
		v1 = &southVertices[next_strip];
		v2 = &northVertices[strip];
		c1 = column;
		c2 = 2 * d->detail - row;
	}

	// now, compute the actual coords of the vertex
	float u1 = static_cast<float> (c1) / d->detail;
	float u2 = static_cast<float> (c2) / d->detail;
	vertex = *v0 + u1 * (*v1 - *v0) + u2 * (*v2 - *v0);

	// project the vertex onto the sphere
	vertex.normalize ();
}

void Sphere::setup (int detail)
{
	if (d->isValid && detail == d->detail)
		return;
	d->detail = detail;
	initialize ();
}

}
