/* Copyright (C) 2007  Mikko Sysikaski <mikko.sysikaski@gmail.com>
 * This program is free software distributed under GNU General Public License
 * version 3 or (at your option) any later version.
 * No warranty of any kind is provided. See the accompanying file COPYING for details.
 */

#include "renderer.h"
#include "physics.h"
#include "board.h"
#include "drawutils.h"
#include "glscreen.h"
#include "noisegen.h"
#include "SDL.h"
#include <GL/gl.h>
#include <GL/glu.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

static const int BOARD_LIST = 1;
static const int HOLE_LIST = 3;

static const int BASE_TEX = 0, BLOCK_TEX = 1;
static GLuint texNames[2] = {0, 0};
static const float HEIGHT = 5.0f;
static const float BHEIGHT = 0.5f;

#ifdef M_SQRT1_2
#define SQRT1_2 0.70710678118654752440
#else
#define SQRT1_2 M_SQRT1_2
#endif

Renderer::Renderer():
	topView(0), turnBoard(1), m_height(5.0f)
{
	genTextures();
}

Renderer::~Renderer()
{
}

void Renderer::render()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	
	const Coord2& place = m_state->getPlace();
	float rotX = m_state->getRotX();
	float rotY = m_state->getRotY();

	if (topView && !m_board->noTopView()) {
		float ratio = float(GLScreen::getResX()) / float(GLScreen::getResY());
		glTranslatef(0, 0, -std::max(float(m_board->getX()), m_board->getY()*ratio));
	}
	else
		glTranslatef(0, 0, -m_height);
	if (turnBoard) {
		glRotatef(rotX*0.1f, 0, 1, 0);
		glRotatef(rotY*0.1f, 1, 0, 0);
	}
	glPushMatrix(); {
		if (topView && !m_board->noTopView())
			glTranslatef(place.x-m_board->getX()*0.5, -place.y+m_board->getY()*0.5f, 0);
		glTranslatef(0, 0, m_state->getHeight());
		glScalef(BALL_SIZE, BALL_SIZE, BALL_SIZE);
		glTranslatef(0, 0, 1);
		glCallList(BALL_LIST);
	} glPopMatrix();
	if (topView && !m_board->noTopView())
		glTranslatef(-m_board->getX()*0.5f, m_board->getY()*0.5f, 0);
	else
		glTranslatef(-place.x, place.y, 0);
	glRotatef(90, 1, 0, 0);
	
	glCallList(BOARD_LIST);
//	setBoard(m_board, m_state);
	
//	SDL_GL_SwapBuffers();

	GLenum err = glGetError();
	if (err != GL_NO_ERROR) {
		fprintf(stderr, "Opengl error: %s\n", gluErrorString(err));
	}
}

static inline void horizBlock(int x, int y, float len=1.0f)
{
	using namespace drawutils;
	float fx = float(x);
	float fy = float(y+1);
	
	float y1 = fy-BLOCK_SIZE;
	float y2 = fy+BLOCK_SIZE;
	
//	printf("Drawing hblock: %i %i\n", y, x);
	
	hQuad(fx+len, fx, 0, BHEIGHT, y1);
	hQuad(fx, fx+len, 0, BHEIGHT, y2);
	vQuad(fx, 0, BHEIGHT, y2, y1);
	vQuad(fx+len, 0, BHEIGHT, y1, y2);
	hTopQuad(fx, fx+len, BHEIGHT, y2, y1);
}
static inline void vertBlock(int x, int y, float len=1.0f)
{
	using namespace drawutils;
	float fx = float(x+1);
	float fy = float(y);
	
	float x1 = fx-BLOCK_SIZE;
	float x2 = fx+BLOCK_SIZE;
	
//	printf("Drawing vblock: %i %i\n", y, x);
	
	vQuad(x1, 0, BHEIGHT, fy+len, fy);
	vQuad(x2, 0, BHEIGHT, fy, fy+len);
	hQuad(x2, x1, 0, BHEIGHT, fy);
	hQuad(x1, x2, 0, BHEIGHT, fy+len);
	vTopQuad(x2, x1, BHEIGHT, fy, fy+len);
}
static inline void bslashBlock(int x, int y, float len=1.0f)
{
	using namespace drawutils;
	const float d = BLOCK_SIZE*SQRT1_2;

	float x1 = x, y1 = y;
	float x2 = x1+len, y2 = y1+len;

//	float x1 = x+d;
//	float x2 = x+1-d;
	glNormal3f(-SQRT1_2, 0, SQRT1_2);
	diagQuad(x1-d, x2-d, 0, BHEIGHT, y1+d, y2+d);
	glNormal3f(SQRT1_2, 0, -SQRT1_2);
	diagQuad(x2+d, x1+d, 0, BHEIGHT, y2-d, y1-d);
	glNormal3f(-SQRT1_2, 0, -SQRT1_2);
	diagQuad(x1+d, x1-d, 0, BHEIGHT, y1-d, y1+d);
	glNormal3f(SQRT1_2, 0, SQRT1_2);
	diagQuad(x2-d, x2+d, 0, BHEIGHT, y2+d, y2-d);
	diagTopQuad(x1, x2, BHEIGHT, y1, y2, d, d);
}
static inline void slashBlock(int x, int y, float len=1.0f)
{
	using namespace drawutils;
	const float d = BLOCK_SIZE*SQRT1_2;

	float x1 = x, y2 = y;
	float x2 = x1+len, y1 = y2+len;

//	float x1 = x+d;
//	float x2 = x+1-d;
	glNormal3f(SQRT1_2, 0, SQRT1_2);
	diagQuad(x1+d, x2+d, 0, BHEIGHT, y1+d, y2+d);
	glNormal3f(-SQRT1_2, 0, -SQRT1_2);
	diagQuad(x2-d, x1-d, 0, BHEIGHT, y2-d, y1-d);
	glNormal3f(-SQRT1_2, 0, SQRT1_2);
	diagQuad(x1-d, x1+d, 0, BHEIGHT, y1-d, y1+d);
	glNormal3f(SQRT1_2, 0, -SQRT1_2);
	diagQuad(x2+d, x2-d, 0, BHEIGHT, y2+d, y2-d);
	diagTopQuad(x1, x2, BHEIGHT, y1, y2, -d, d);
}

void Renderer::setBoard(Board* b, Physics* p)
{
	m_board = b;
	m_state = p;

	int w = b->getX();
	int h = b->getY();
	
//	printf("Creating display list; %i %i\n", w, h);
	
	glNewList(BOARD_LIST, GL_COMPILE);
//	glDisable(GL_CULL_FACE);
	// draw blocks
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, texNames[BLOCK_TEX]);
	glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
	glColor3f(0.85, 0.55, 0.2);
	
	glEnable(GL_POLYGON_OFFSET_FILL);
	glPolygonOffset(1.0f, 1.0f);
	glBegin(GL_QUADS);
	for(int y=0; y<h; ++y) {
		for(int x=0; x<w; ++x) {
			if ((*m_board)[y][x] & 1)
				horizBlock(x, y);
		}
	}
	glEnd();
	glPolygonOffset(2.0f, 2.0f);
	glBegin(GL_QUADS);
	for(int y=0; y<h; ++y) {
		for(int x=0; x<w; ++x) {
			if ((*m_board)[y][x] & 2)
				vertBlock(x, y);
		}
	}
	glEnd();
	glPolygonOffset(3.0f, 3.0f);
	glBegin(GL_QUADS);
	for(int y=0; y<h; ++y) {
		for(int x=0; x<w; ++x) {
			if ((*m_board)[y][x] & 4)
				bslashBlock(x, y);
		}
	}
	glEnd();
	glPolygonOffset(4.0f, 4.0f);
	glBegin(GL_QUADS);
	for(int y=0; y<h; ++y) {
		for(int x=0; x<w; ++x) {
			if ((*m_board)[y][x] & 8)
				slashBlock(x, y);
		}
	}
	glEnd();
	// draw borders around the board
	glPolygonOffset(0, 0);
	glBegin(GL_QUADS);
	horizBlock(0, -1, w);
	horizBlock(0, h-1, w);
	glEnd();
	glPolygonOffset(0.5f, 0.5f);
	glBegin(GL_QUADS);
	vertBlock(-1, 0, h);
	vertBlock(w-1, 0, h);
	glEnd();
	
		// draw holes
	// draw the the win holes with alpha
	glPolygonOffset(-1.0f, -1.0f);
//	glDepthFunc(GL_ALWAYS);
	glEnable(GL_BLEND);
	glDisable(GL_TEXTURE_2D);
	glDisable(GL_LIGHTING);
	glColor4f(0.2, 1, 0.2, 0.7);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	for(int y=0; y<m_board->getY(); ++y) {
		for(int x=0; x<m_board->getX(); ++x) {
			if (m_board->getBlock(y,x) == END) {
				glPushMatrix();
				glTranslatef(x+0.5, 0, y+0.5);
				glCallList(HOLE_LIST);
				glPopMatrix();
			}
		}
	}
	glDisable(GL_BLEND);
//	glDepthFunc(GL_LESS);
//	glDisable(GL_DEPTH_TEST);
//	glDepthMask(0);
//	glDrawBuffer(GL_NONE);
	
	// should not be needed but mesa seems to ignore glDrawBuffer
	glColorMask(0,0,0,0);
	
	for(int y=0; y<m_board->getY(); ++y) {
		for(int x=0; x<m_board->getX(); ++x) {
			if (m_board->getBlock(y,x) == HOLE) {
				glPushMatrix();
				glTranslatef(x+0.5, 0, y+0.5);
				glCallList(HOLE_LIST);
				glPopMatrix();
			}
		}
	}
	glBegin(GL_QUADS);
	for(int y=0; y<m_board->getY(); ++y) {
		for(int x=0; x<m_board->getX(); ++x) {
			if (m_board->getBlock(y,x) == SHOLE) {
				glVertex3f(x, 0, y);
				glVertex3f(x, 0, y+1);
				glVertex3f(x+1, 0, y+1);
				glVertex3f(x+1, 0, y);
			}
		}
	}
	glEnd();
	
	glDisable(GL_POLYGON_OFFSET_FILL);
	glDrawBuffer(GL_BACK);
	glColorMask(1,1,1,1);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LESS);
	glDepthMask(1);

	glEnable(GL_LIGHTING);
	glDepthFunc(GL_LESS);
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, texNames[BASE_TEX]);
	glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
	glColor3f(0.85, 0.45, 0.15);
	glBegin(GL_QUADS);
	drawutils::hTopQuad(0, w, 0, h, 0);
/*	glTexCoord2f(0, 0); glVertex3f(0, 0, h);
	glTexCoord2f(w, 0); glVertex3f(w, 0, h);
	glTexCoord2f(w, h); glVertex3f(w, 0, 0);
	glTexCoord2f(0, h); glVertex3f(0, 0, 0);*/
	glEnd();
	
	glEndList();
}

void Renderer::genTextures()
{	
	const int TSIZE_BASE = 128;
	const int TSIZE_BLOCK = 128;
	
	// TODO: generate leet wood textures using turbulance etc.
	
// 	GLubyte baseT[TSIZE_BASE][TSIZE_BASE];
// 	GLubyte blockT[TSIZE_BLOCK][TSIZE_BLOCK];

	m_baseTex = new GLubyte[TSIZE_BASE*TSIZE_BASE];
	m_blockTex = new GLubyte[TSIZE_BLOCK*TSIZE_BLOCK];

	NoiseGen noise(TSIZE_BASE, TSIZE_BASE);
	
	GLubyte* texPtr = m_baseTex;
	for(int y=0; y<TSIZE_BASE; ++y) {
		for(int x=0; x<TSIZE_BASE; ++x) {
		//	*texPtr++ = (y^x)*256/TSIZE_BASE;
			*texPtr++ = 64+GLubyte(noise.tile(x, y, 32.0f)*100.0f);
		}
	}
	texPtr = m_blockTex;
	for(int y=0; y<TSIZE_BLOCK; ++y) {
		for(int x=0; x<TSIZE_BLOCK; ++x) {
		//	blockT[y][x] = (y^x)*256/TSIZE_BLOCK;
			float value = y*M_PI/16.0f + noise.turbulence(x, y, 16.0f);
			*texPtr++ = GLuint(128.0f + 64.0f*fabs(sin(value)));
		}
	}
	
//	glEnable(GL_TEXTURE_2D);
	if (!texNames[0])
		glGenTextures(2, texNames);
	
	glBindTexture(GL_TEXTURE_2D, texNames[BASE_TEX]);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE8, TSIZE_BASE,
	             TSIZE_BASE, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_baseTex);

	glBindTexture(GL_TEXTURE_2D, texNames[BLOCK_TEX]);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE8, TSIZE_BLOCK,
	             TSIZE_BLOCK, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, m_blockTex);
}

void Renderer::reloadTextures()
{
}

void Renderer::init()
{
	glEnable(GL_DEPTH_TEST);
//	glDisable(GL_CULL_FACE);
	glEnable(GL_CULL_FACE);
	glEnable(GL_LIGHTING);

	GLScreen::set3D();
	glEnable(GL_TEXTURE_2D);
}

void Renderer::resetGL()
{
//	printf("resetting...\n");
	reloadTextures();

	using namespace drawutils;

	glEnable(GL_COLOR_MATERIAL);
	glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE);
	glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 0);
	
	glNewList(BALL_LIST, GL_COMPILE);
	const float spec[4] = {0.2, 0.2, 0.2, 1};
	glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, spec);
	glDisable(GL_TEXTURE_2D);
	glEnable(GL_LIGHTING);
//	glColor3f(0.8, 0.8, 0.8);
	glColor3f(0.3, 0.3, 0.3);
	renderBall(4);
	const float black[4] = {0,0,0,1};
	glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, black);
	glEndList();

	glNewList(HOLE_LIST, GL_COMPILE);
	renderSphere(32, HOLE_SIZE);
	glEndList();

	glEnable(GL_LIGHT0);
	float pos[] = {1, -1, 1, 0};
	glLightfv(GL_LIGHT0, GL_POSITION, pos);
}
