/**
 * Small OpenGL tagging demo
 * (c) Daniel Hiepler, 2007-2008
 */

#include <stdio.h>
#include <getopt.h>
#include <math.h>
#include <SDL/SDL.h>
#include <SDL/SDL_opengl.h>
#include <SDL/SDL_image.h>

#include "include/main.h"


#define FRAMERATE_MAX 100
#define DEFAULT_SCREEN_W 800
#define DEFAULT_SCREEN_H 600
#define BRUSH_WIDTH 32
#define BRUSH_HEIGHT 32

#ifndef TRUE
#define TRUE 1
#endif

#ifndef FALSE
#define FALSE 0
#endif





/* global info structure */
static struct MainLocal
{
	int width, height, depth;		/* screen dimensions */
	int old_x, old_y;
	int mouse_x;
	int mouse_y;
	int mouse_clicked;
	int mouse_dragged;
	int mouse_show;
	SDL_Surface *display;
	int sdl_flags;
	GLuint text_brush;
	GLuint text_drip;
	struct Texture *first_texture;
	struct Drip *first_drip;
	int drip_threshold;
	int drip_smeg_min,drip_smeg_max;
	int drip_life_min,drip_life_max;
}local;


void m_setScreenWidth(int w)
{
	local.width = w;
}
void m_setScreenHeight(int h)
{
	local.height = h;
}
void m_setScreenDepth(int d)
{
	local.depth = d;
}
int m_getScreenWidth()
{
	return local.width;
}
int m_getScreenHeight()
{
	return local.height;
}
int m_getScreenDepth()
{
	return local.depth;
}



int m_getMouseClicked()
{
	return local.mouse_clicked;
}

int m_getMouseDragged()
{
	return local.mouse_dragged;
}

int m_getMouseX()
{
	return local.mouse_x;
}
int m_getMouseY()
{
	return local.mouse_y;
}

/**
 * print help-text
 */
static void m_usage(char *program_name)
{
 printf("Usage: %s [OPTION] [TEXTURES]\n" \
	"\t-r <wdith>x<height>\t--resolution\tResolution\n" \
	"\t-f\t\t\t--fullscreen\tRun fullscreen\n" \
	"\t-s\t\t\t--show\t\tShow mouse-pointer\n" \
	"\t-p\t\t\t--pyramid\tDraw pyramid instead of cube\n" \
	"\t-l\t\t\t--light\t\tEnable lighting\n" \
	"\t-a\t\t\t--alpha\t\tEnable alpha-blending \"glenz\" effect\n" \
	"\t-b\t\t\t--blitz\t\tFlash on click\n" \
	"\t-e\t\t\t--explode\tDraw \"exploding\" effect on click\n" \
	"\t-m\t\t\t--move\t\tMove explosion with rotating object\n" \
	"\t-h\t\t\t--help\t\tThis help\n\n" \
	"You can give up to 6 texture filenames.\n" \
	"While active press key to clear screen, ESC to exit.\n", program_name);
}
/**
 * initialize OpenGL
 */
int gl_init(int screen_width, int screen_height)
{
	/* protect against division by zero */
	if ( screen_height == 0 )
	screen_height = 1;
	
		
	/* Setup our viewport. */
	glViewport(0,0, (GLsizei) screen_width, (GLsizei) screen_height);
	GL_CHECKERROR;
	
	/* change to the projection matrix and set our viewing volume. */
	glMatrixMode( GL_PROJECTION );
	GL_CHECKERROR;
	glLoadIdentity( );

	/* Set our perspective */
	gluPerspective( 45.0f, 
				((GLfloat) screen_width / (GLfloat) screen_height),
				0.1f, 100.0f );
	GL_CHECKERROR;
	
	
	/* Alpha blending */
	glBlendFunc(GL_SRC_ALPHA, GL_ONE);
	GL_CHECKERROR;
	glEnable(GL_BLEND);
	GL_CHECKERROR;
	
	/* Enable smooth shading */
	glShadeModel( GL_SMOOTH );
	GL_CHECKERROR;
	
	/* init openGL */
	glClearColor(0,0,0,0);
	GL_CHECKERROR;
	
	/* Depth buffer setup */
	glClearDepth( 1.0f );
	GL_CHECKERROR;
	
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
	glHint(GL_POINT_SMOOTH_HINT,GL_NICEST);
	
	/* change projection */
	glMatrixMode(GL_PROJECTION);			// Select Projection
	glLoadIdentity();						// Reset The Matrix
	glOrtho(0, (GLdouble) screen_width, (GLdouble) screen_height, 0, -1, 1);
	glMatrixMode(GL_MODELVIEW);				// Select Modelview Matrix
	glLoadIdentity();						// Reset The Matrix
	
	
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, local.text_brush);
	glDisable(GL_LIGHTING);
	
	
	local.width = screen_width;
	local.height = screen_height;
	
	if(glGetError() != GL_NO_ERROR)
		return -1;
	else
		return 0;
}

/**
 * random function
 */
int myrand(int min, int max)
{
 int val;
	
 val = 1 + (int) ((float) max * ((float) rand() / (RAND_MAX + (float) min)));
 
 return val;
}

void drip_free(struct Drip *my_drip)
{
 if(my_drip)
 {	 
  if(my_drip->previous_drip)
   my_drip->previous_drip->next_drip = my_drip->next_drip;
  else
   local.first_drip = my_drip->next_drip;
  
  if(my_drip->next_drip)
   my_drip->next_drip->previous_drip = my_drip->previous_drip;
  else
   if(my_drip->previous_drip) 
	 my_drip->previous_drip->next_drip = NULL;
  
  free(my_drip);
 }
}
/**
 * create new drip
 */
void drip_new(struct Drip **first, int x, int y, int smegginess, int lifetime)
{
	struct Drip *current_drip = NULL, *new_drip;
	
	if((new_drip = calloc(1, sizeof(struct Drip))))
	{
		/* seek to last drip */
		if(*first) 
		{
		for(current_drip = *first;
			current_drip->next_drip;
			current_drip = current_drip->next_drip);
		
			current_drip->next_drip = new_drip;
		}
		else *first = new_drip;
		
		/* init drip */
		new_drip->x = x;
		new_drip->y = y;
		new_drip->smegginess = smegginess;
		new_drip->lifetime = lifetime;
		new_drip->previous_drip = current_drip;
	}
}

/**
 * draw pixel
 */
void drawPixel(int x, int y)
{
	glBindTexture( GL_TEXTURE_2D, local.text_drip );
	glBegin(GL_QUADS);
		
		glColor4ub(10,22,40,128);
		
		glTexCoord2f(0,0);
		glVertex2i(-(BRUSH_WIDTH/2)+x,-(BRUSH_HEIGHT/2)+y);
	
		glTexCoord2f(1,0);
		glVertex2i((BRUSH_WIDTH/2)+x, -(BRUSH_HEIGHT/2)+y);
	
		glTexCoord2f(1,1);
		glVertex2i((BRUSH_WIDTH/2)+x,(BRUSH_HEIGHT/2)+y);
	
		glTexCoord2f(0,1);
		glVertex2i(-(BRUSH_WIDTH/2)+x,(BRUSH_HEIGHT/2)+y);
		
	glEnd();
}
   
/**
 * set brush
 */
void drawBrush(int x, int y)
{
	glBindTexture( GL_TEXTURE_2D, local.text_brush );

	
	glBegin(GL_QUADS);
		
		glColor4ub(10,22,40,200);
		
		glTexCoord2f(0,0);
		glVertex2i(-(BRUSH_WIDTH/2)+x,-(BRUSH_HEIGHT/2)+y);
	
		glTexCoord2f(1,0);
		glVertex2i((BRUSH_WIDTH/2)+x, -(BRUSH_HEIGHT/2)+y);
	
		glTexCoord2f(1,1);
		glVertex2i((BRUSH_WIDTH/2)+x,(BRUSH_HEIGHT/2)+y);
	
		glTexCoord2f(0,1);
		glVertex2i(-(BRUSH_WIDTH/2)+x,(BRUSH_HEIGHT/2)+y);
		
	glEnd();

	
	GL_CHECKERROR;
	
	/* make randomness for drips */
	if(rand() % local.drip_threshold == local.drip_threshold-1)
	{
		/* generate drip */
		drip_new(&local.first_drip, x, y, 
			myrand(local.drip_smeg_min,local.drip_smeg_max),
			myrand(local.drip_life_min,local.drip_life_max));
	}
}

/**
 * small bresenham to draw lines
 */
void drawLine(register int x1, register int y1, register int x2, register int y2, int use_brush) {
 register int	deltaX, deltaY;	
 register int	addX, addY;
 register int	counter, error;
 	
 // default step right	
 addX		= 1;
 // ...and down
 addY	 	= 1;
 // first calculate delta values
 deltaX 	= x2 - x1;
 deltaY 	= y2 - y1;
 
 // check for negative delta-value
 if(deltaX < 0) {
  addX = -addX;
  deltaX = -deltaX;
 }	 
 if(deltaY < 0) {
  addY = -addY;
  deltaY = -deltaY;
 }
 
 
 // special case for horizontal and vertical lines...
 // this will speed up things a bit in these cases
 if(deltaX == 0) {  
  for(counter = deltaY; counter > 0; counter--) {
   if(y1 >= 0 && y1 <= local.height) {
	   if(use_brush) 
		   drawBrush(x1, y1);
	   else
		   drawPixel(x1, y1);
   }
   y1 += addY;
  }
  return;
 }
 // horizontal line
 if(deltaY == 0) {  
  for(counter = deltaX; counter > 0; counter--) {
   if(x1 <= local.width && x1 >= 0) 
   {
	   if(use_brush) 
		   drawBrush(x1, y1);
	   else
		   drawPixel(x1, y1);
   }
   x1 += addX;
  }
  return;
 } 
 if(deltaX > deltaY) {
  // calc initial error-value
  error = deltaX>>1;
  for(counter = deltaX; counter > 0; counter--) {	 
   // adjust error-value
   error += deltaY;
   // set pixel with clipping
   if(x1 <= local.width && x1 >= 0 && y1 <= local.height && y1 >= 0) 
   {
	   if(use_brush) 
		   drawBrush(x1, y1);
	   else
		   drawBrush(x1, y1);
   }
   // if error is out of range, go one line further up or down
   if(error >= deltaX) {
	error -= deltaX;
	y1 += addY;	   
   }
   x1 += addX;
  }
 }
 else {
  // calc initial error-value
  error = deltaY>>1;
  for(counter = deltaY; counter >= 0; counter--) {
   // adjust error-value
   error += deltaX;
   // set pixel with clipping
   if(x1 <= local.width && x1 >= 0 && y1 <= local.height && y1 >= 0)
   {
	   if(use_brush) 
		   drawBrush(x1, y1);
	   else
		   drawPixel(x1, y1);
   }
   // if error is out of range, go one line further left or right
   if(error >= deltaY) {
	error -= deltaY;
    x1 += addX;	   
   }
   y1 += addY;
  }	  
 }
}


/**
 * draw drips
 */
void drip_do()
{
	struct Drip *next, *current_drip;
	int xold, yold;
	
	if(local.first_drip) 
		current_drip = local.first_drip;
	else 
		return;
	
	while(current_drip)
	{
		/* save next drip */
		next = current_drip->next_drip;
		
		/* drip still alive? */
		if(current_drip->lifetime > 0 &&
			current_drip->y < local.height)
		{
			/* move drip */
			xold = current_drip->x;
			yold = current_drip->y;
			
			/* let drip-age */
			current_drip->lifetime--;
			current_drip->y += current_drip->smegginess/8;
			
			current_drip->smegginess = myrand(0,50);
			
			if(current_drip->smegginess % 50 == 0)
				current_drip->x++;
			
			if(current_drip->smegginess % 50 == 1)
				current_drip->x--;
			
			/* draw drip */
			drawLine(xold, yold, current_drip->x, current_drip->y, FALSE);
		}
		else
		{
			drip_free(current_drip);
		}
		
		/* next one */
		current_drip = next;  
	}	 
}

/**
 * count how much bits are set in integer
 */
static unsigned int countBits(int v)
{
	unsigned int c; // c accumulates the total bits set in v
	
	for (c = 0; v; c++)
	{
	  v &= v - 1; // clear the least significant bit set
	}
	
	return c;
}


/**
 * load image as GL texture
 */
int textureLoad(const char *filename, GLuint *id)
{
    int result = -1;				/* Status indicator */
    SDL_Surface *t = NULL; 	/* Create storage space for the texture */
	GLenum format;

	
	printf("Loading: \"%s\"\n", filename);
	
	IMG_InvertAlpha(TRUE);
	
    /* Load The Bitmap, Check For Errors, If Bitmap's Not Found Quit */
    if((id) && (t = IMG_Load(filename)))
	{

	    /* Set the status to true */
	    result = 0;

	    /* Create The Texture */
	    glGenTextures( 1, id );
		GL_CHECKERROR;
		
	    /* Typical Texture Generation Using Data From The Bitmap */
	    glBindTexture( GL_TEXTURE_2D, *id );
		GL_CHECKERROR;
		
		/* Pixel-format */
		if(t->format->Rmask == 255)
		{
				format = GL_RGBA;
		}
		else
		{
				format = GL_BGRA;
		}
		
		/* width power of 2? */
		if(countBits(t->w) != 1)
		{
			printf("ERROR: Texture width must be a power of 2!\n");
			result = -1;						
		}
		
		/* height power of 2? */
		if(countBits(t->h) != 1)
		{
			printf("ERROR: Texture height must be a power of 2!\n");
			result = -1;			
		}
		
		if(result >= 0)
		{			
			/* Linear Filtering */
			glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
			glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
			GL_CHECKERROR;
			
			/* Generate The Texture */
			glTexImage2D( GL_TEXTURE_2D, 0, 3, t->w, t->h, 0, GL_RGB,
				  GL_UNSIGNED_BYTE, t->pixels );
			GL_CHECKERROR;
		}
		
		/* Free up any memory we may have used */		
		SDL_FreeSurface( t );
    }
	else
	{
		id = NULL;
	}

    return result;
}

/**
 * parse commandline arguments
 */
static int m_parseArguments(int argc, char **argv) {
	int result = 0;
	int argument_index = 0;
	int argument;
	int width, height;
	struct Texture *new, *tmp, *s = NULL;
	static struct option loptions[] = 
	{
		{"help",0,0,'h'},
		{"resolution",1,0,'r'},
		{"fullscreen",0,0,'f'},
		{"show",0,0,'s'},
		{0,0,0,0}
	};
	
	while(TRUE) 
	{
		argument = getopt_long(argc, argv, "-hr:fs", loptions, &argument_index);
		if (argument == -1) break;
		
		switch(argument) 
		{	 
						
			/* show mouse-pointer */
			case 's':
				local.mouse_show = TRUE;
				break;
			
						
			/* display help text */
			case 'h':
				m_usage(argv[0]);   
				result = -1;
				break;
			
			
			/* fullscreen */
			case 'f':
				local.sdl_flags |= SDL_FULLSCREEN;
				break;
			
			case 'r':
				if (!optarg) 
				{ 
					printf("** Please provide resolution (<WIDTH>x<HEIGHT>)\n");
					result = -1;
				}
				else {
					sscanf(optarg, "%dx%d", &width, &height);
					if(width <= 0 || height <= 0)
					{
						printf("Invalid resolution.\n");
						result = -1;		 
					}
					
					m_setScreenWidth(width);
					m_setScreenHeight(height);
				}
				break;
			
			/* treat unknown parameters as texture-filenames */
			case '?':
				fprintf(stderr,"**FATAL ERROR\n getopt returned character code 0%o\n",argument);
				result = -1;
				break;
			
			/* eek! this may never happen */
			default:
				/* init new texture definition */
				new = calloc(1, sizeof(struct Texture));
				new->filename = strdup(optarg);
				/* append to list */
				for(tmp = local.first_texture; tmp; tmp = tmp->next)
				{
					s = tmp;
				}
				
				if(s)
					s->next = new;
				else
					local.first_texture = new;
				
				break;
		}
	}
	return result;
}


/**
 * cleanup drips
 */
void drips_cleanup()
{
	struct Drip *tmpDrip,*tmp2Drip;
		
	/* free drips */
	for(tmpDrip = local.first_drip; tmpDrip; tmpDrip = tmp2Drip)
	{
		tmp2Drip = tmpDrip->next_drip;
		drip_free(tmpDrip); 
	}
		
	local.first_drip = NULL;
}

/**
 * cleanup everything
 */
void m_cleanup()
{
	struct Texture *tmp, *tmp_f;
	
	
	printf("Cleaning up...\n");
	
	drips_cleanup();
	
	/* free textures */
	for(tmp = local.first_texture; tmp; tmp = tmp_f)
	{
		free(tmp->filename);
		tmp_f = tmp->next;
		free(tmp);
	}	
	local.first_texture = NULL;
		
	
	SDL_ShowCursor(SDL_ENABLE);
}


/**
 * draw scene
 */
void m_drawScene()
{

	
	if(local.mouse_clicked)
	{
		drawLine(local.old_x, local.old_y, local.mouse_x, local.mouse_y, TRUE);
	}
		
	drip_do();
	
	/* display frame */
	SDL_GL_SwapBuffers();
	
	glFlush();

	
}

/**
 * handle different keypresses
 */
static void m_handleKeypress(SDL_Event *event)
{
	switch(event->key.keysym.sym)
	{
		
		/* quit */
		case SDLK_q:
		case SDLK_ESCAPE:
			m_cleanup();
			exit(0);
			break;
		
		/* fullscreen */
		case SDLK_f:
			SDL_WM_ToggleFullScreen(local.display);
			break;
		
		/* clear */
		case SDLK_SPACE:
			/* Clear The Screen And The Depth Buffer */
			glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
			/* delete drips */
			drips_cleanup();
			break;
		
		default:
			break;
	}
}

/**
 * SDL Event handler
 */
static void m_handleSDLEvents()
{
	SDL_Event event;
	
	
	/* check for SDL events */
	while(SDL_PollEvent(&event))
	{
		switch(event.type) 
		{
					
			/* window has been resized */
			case SDL_VIDEORESIZE:
				local.display = SDL_SetVideoMode(event.resize.w, event.resize.h,
									local.depth, local.sdl_flags);
				if(!local.display)
				{
					printf("Window resize error! (%d/%d)\n",
								event.resize.w, event.resize.h);
					m_cleanup();
					exit(-1);
				}
				
				if(gl_init(event.resize.w, event.resize.h) < 0)
				{
					printf("Window resize error (OpenGL)!\n");
					m_cleanup();
					exit(-1);
				}
				
				local.width = event.resize.w;
				local.height = event.resize.h;
				
				break;
			
			
			/* quit */
			case SDL_QUIT:
				m_cleanup();
				exit(0);
				break;
			
			
			/* keypress */
			case SDL_KEYDOWN:
				m_handleKeypress(&event);
				break; 
			
			
			/* mouse-motion */
			case SDL_MOUSEMOTION:
				/* save previous coordinates */
				local.old_x = local.mouse_x;
				local.old_y = local.mouse_y;
				local.mouse_x = (int) event.motion.x;
				local.mouse_y = (int) event.motion.y;
				
			
				if(local.mouse_clicked)
				{
					local.mouse_dragged = 1;
				}		 
				else
				{
					local.mouse_dragged = 0;
				}
				break;
			
				
			/* mouse-up */
			case SDL_MOUSEBUTTONUP:							
				local.mouse_clicked = 0;
				break;
			
			
			/* mouse-down */
			case SDL_MOUSEBUTTONDOWN:
				local.mouse_clicked = 1;
				break;
			
				
			/* oOops! */
			default:
				break;
		}
	}
}

/**
 * delay to respect framerate
 */
void m_fpsDelay()
{
	static GLint t, t0;
	
	/* get current time */
	t = SDL_GetTicks();
	
	/* sleep if we are beyond the framerate */
	if((t-t0) < (1000/FRAMERATE_MAX))
	{
		SDL_Delay((1000/FRAMERATE_MAX)-(t-t0));
	}
	
	t0 = t+(1000/FRAMERATE_MAX)-(t-t0);
}

/***
 * main()
 */
int main(int argc, char *argv[])
{
	const SDL_VideoInfo *videoInfo;
		
	
	/* initialize local structure */
	memset(&local, 0, sizeof(struct MainLocal));

	local.drip_smeg_max = 1;
	local.drip_smeg_min = 1;
	local.drip_life_max = 200;
	local.drip_life_min = 10;
	local.drip_threshold = 450;
	m_setScreenWidth(DEFAULT_SCREEN_W);
	m_setScreenHeight(DEFAULT_SCREEN_H);
	
	local.depth = 8;
	local.sdl_flags = SDL_OPENGL | SDL_GL_DOUBLEBUFFER |
						SDL_HWPALETTE |SDL_RESIZABLE ;
	
	
	printf("laserTraq tagging-demo - (c) D.Hiepler 2007-2008\n\n");
	
	
	/* parse arguments */
	if(m_parseArguments(argc, argv) < 0)
	{
		m_cleanup();
		exit(-1);
	}
	
	
	/* init SDL */
	if ( SDL_Init(SDL_INIT_VIDEO) < 0 ) 
	{
		fprintf(stderr, "SDL-init error: \"%s\"\n", SDL_GetError());
		m_cleanup();
		exit(-1);
	}
		
	/* Quit SDL when we exit */
	atexit(SDL_Quit);

	videoInfo = SDL_GetVideoInfo();
	
	/* Fast? */
	if(videoInfo->hw_available)
	{
		local.sdl_flags |= SDL_HWSURFACE;
	}
	else
	{
		local.sdl_flags |= SDL_SWSURFACE;
	}
	
	/* Accelerated? */
	if(videoInfo->blit_hw)
		local.sdl_flags |= SDL_HWACCEL;
	
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
	
	/* Set video mode */
	if(!(local.display = SDL_SetVideoMode( local.width, local.height, local.depth, local.sdl_flags)))
	{
		fprintf(stderr, "sdl-display open error: \"%s\"\n", SDL_GetError());
		m_cleanup();
		exit(-1);
	}
	
	/* initialize OpenGL */
	gl_init(local.width, local.height);
	
	/* load textures */
	/*for(tmp = obj_getFirstTexture(); tmp; tmp = tmp->next)
	{
		if(obj_textureLoad(tmp->filename, &(tmp->id)) < 0)
		{
			fprintf(stderr, "Failed to load \"%s\".\n",
						tmp->filename);
			m_cleanup();
			exit(-1);
		}
	}*/
	
	if(textureLoad("pixmaps/brush2.png", &local.text_brush) < 0)
	{
		printf("Failed to load brush...\n");
		m_cleanup();
		exit(-1);
	}
	
	if(textureLoad("pixmaps/drip.png", &local.text_drip) < 0)
	{
		printf("Failed to load drip...\n");
		m_cleanup();
		exit(-1);
	}
	
	/* hide mouse-pointer if necessary */
	if(!local.mouse_show)
		SDL_ShowCursor(SDL_DISABLE);
	
	
	SDL_WM_SetCaption("tagging demo (laserTraq demo) - D.Hiepler", NULL);
		
	
	/* main loop */
	while(1)
	{
		
		m_drawScene();

		
		m_handleSDLEvents();
		
		
		m_fpsDelay();
		
	}
	
	

	m_cleanup();

	return (0);
}
