/*  xio - I/O 
 *  Copyright (C) Joakim Kolsjö and Anders Asplund 2003
 *	This program 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.
 *
 *  This program 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307 
 */


#include "xio.h"
#include <iostream>
#include <cstdio>
#include <cmath>

using namespace std;

#ifdef WIN32
	#include <windows.h>
	#include "SDL/SDL_image.h"
#else
	#include <dirent.h>
	#include <sys/time.h>
	#include <sys/types.h>
	#include "SDL_image.h"
#endif

#include <png.h>

namespace xio
{
#ifdef DEBUG
	void sdlq() {SDL_Quit(); cout << "DEBUG | SDL_Quit has been run" << endl;}
#endif
	
	// Random __________________________________________________________________	
	Random::Random()
	{
		srand(0);
	}
	
	float Random::get()
	{
		return (((float)rand()) / RAND_MAX);
	}
	
	// Video ___________________________________________________________________
	Video::Video(int w, int h, int bpp, bool fs, bool opengl, const char *window_title)
	{
		width = w; height = h; b = bpp; fullscreen = fs; ogl = opengl; wtitle = window_title;
		sdl_back = createSurface(width, height);

		if (sdl_back == NULL) {
			string temp = "Video backbuffer not created: ";
			temp +=	SDL_GetError();
			xs::error.add(temp.c_str());
		}	
	}
	
	void Video::init()
	{
		// Init SDL
		if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER) < 0) {
			string temp = "Unable to init SDL: ";
			temp +=	SDL_GetError();
			xs::error.add(temp.c_str());
		}
		else
#ifdef DEBUG
			xs::shutdown.add(sdlq);
#else
			xs::shutdown.add(SDL_Quit);
#endif
			
		// Fetch the video info
		const SDL_VideoInfo *video_info;
		if(!(video_info = SDL_GetVideoInfo())) {
			string temp = "Video query failed: ";
			temp +=	SDL_GetError();
			xs::error.add(temp.c_str());
		}
				
#ifdef DEBUG
		cout << "DEBUG | Activating HWPALETTE" << endl;
#endif
		int flags = SDL_HWPALETTE;
	
		if(ogl)
		{
#ifdef DEBUG
			cout << "DEBUG | Activating OPENGL" << endl;
#endif
			flags |= SDL_OPENGL;
			flags |= SDL_GL_DOUBLEBUFFER;
		}
		else
		{
#ifdef DEBUG
			cout << "DEBUG | Activating DOUBLEBUF" << endl;
#endif
			flags |= SDL_DOUBLEBUF;
		}
		
		int hw_accel = 0;
		if(video_info->hw_available)
		{
#ifdef DEBUG
			cout << "DEBUG | Activating HWSURFACE" << endl;
#endif
			hw_accel |= SDL_HWSURFACE;
			flags |= SDL_HWSURFACE;
		}
		else
		{
#ifdef DEBUG
			cout << "DEBUG | Activating SWSURFACE" << endl;
#endif
			hw_accel |= SDL_SWSURFACE;
			flags |= SDL_SWSURFACE;
		}
		
		// Check if hardware blits can be done
		if(video_info->blit_hw)
		{
#ifdef DEBUG
			cout << "DEBUG | Activating HWACCEL" << endl;
#endif
			flags |= SDL_HWACCEL;
		}
		
		if(fullscreen) {
			flags |= SDL_FULLSCREEN;
			SDL_ShowCursor(0);
		}
				
		// Create the screen
		if(!(sdl_screen = SDL_SetVideoMode(width, height, b, flags))) {
			string temp = "Video mode set failed: ";
			temp +=	SDL_GetError();
			xs::error.add(temp.c_str());
		}
		SDL_WM_SetCaption(wtitle.c_str(), NULL);
#ifdef DEBUG
		cout << "DEBUG | BitsPerPixel: " << (int)(sdl_screen->format->BitsPerPixel) << endl;
#endif
	}
	
	void Video::update() 
	{
		SDL_BlitSurface(sdl_back, NULL, sdl_screen, NULL);
		SDL_Flip(sdl_screen);
	}
	
	void Video::getPixel(xio::surface *surface, rgb *pixel, int x, int y)
	{
		pixel->r = ((u32*)surface->pixels)[x + y*surface->w]>>16;
		pixel->g = ((u32*)surface->pixels)[x + y*surface->w]>>8;
		pixel->b = ((u32*)surface->pixels)[x + y*surface->w];
	}

	void Video::setPixel(xio::surface *surface, int x, int y, u8 r, u8 g, u8 b)
	{
		((u32*)surface->pixels)[x + y*surface->w] = ((u32)(r<<16)) + ((u32)(g<<8)) + (u32)b;
	}

	xio::surface *Video::createSurface(int w, int h)
	{
		return	SDL_CreateRGBSurface(SDL_SWSURFACE, w, h, b, 0, 0, 0, 0);
	}
	
	void Video::freeSurface(xio::surface *surface)
	{
		SDL_FreeSurface(surface);
	}
	
	surface *Video::resizeSurface(surface *src, int width, int height)
	{
		float hScale = (float)src->h / (float)height;
		float wScale = (float)src->w / (float)width;
		xio::surface *outsurface = createSurface(width, height);
		xio::rgb srcc;
		
		for(int y = 0; y < height; y++)
		{
			for(int x = 0; x < width; x++)
			{
				getPixel(src, &srcc, (int)(x*wScale), (int)(y*hScale));
				setPixel(outsurface, x, y, srcc.r, srcc.g, srcc.b);			
			}
		}	
		return outsurface;
	}
	
	void Video::warpSurface(surface *src, surface *dst, int dx, int dy)
	{
		rgb inpixel;
		for(int y = 0; y < src->h; y++)
		{
			for(int x = 0; x < src->w; x++)
			{
				getPixel(src, &inpixel, x, y);
				if((dx + x) < 0) {
					setPixel(dst, dst->w + dx + x, dy+y, inpixel.r, inpixel.g, inpixel.b);
				}
				else {
					setPixel(dst, dx+x, dy+y, inpixel.r, inpixel.g, inpixel.b);
				}
			}
		}
	}
	
	/* Doesn't work as it should really...
	void Video::rotateSurface(surface *src, surface *dst, float angle)
	{
		float cos_val = cos(angle);
		float sin_val = sin(angle);
		int half_w = src->w>>1;
		int half_h = src->h>>1;

		int bs, cs;
		int srcx, srcy;
		
		for(int y = 0; y < src->h; y++)
		{
  			for(int x = 0; x < src->w; x++)
			{
    			bs = x - half_w;
    			cs = y - half_h;

    			srcx = (int)(bs * sin_val) + (int)(cs * cos_val) + half_w;
    			srcy = (int)(bs * cos_val) - (int)(cs * sin_val) + half_h;
				rgb srcpixel;
				getPixel(src, &srcpixel, srcx, srcy);
				setPixel(dst, x, y, srcpixel.r, srcpixel.g, srcpixel.b);
			}
  		}
	}*/
	
	void Video::blit(surface *src, surface *dst, int dx, int dy, int width, int height, float alpha, bool centralize)
	{
		rgb ipixel, spixel;
		
		bool usealpha = false, useresize = false;
		
		if(alpha != -1)
			usealpha = true;
		
		if(!(width <= 0 && height <= 0))
			useresize = true;
		
		int w, h;
		
		xio::surface *insurface;
		
		// Check if we need to use resize
		if(useresize) {
			insurface = resizeSurface(src, width, height);
			w = width, h = height;
		} else {
			insurface = src;
			w = src->w;
			h = src->h;
		}
				
		// Calculate the area of the insurface that is used (to be able to draw
		// outside of dst)
		int x1, x2;
		int y1, y2;

		// Check if dx and dy are a central point
		if(centralize) {
			dx -= w/2;
			dy -= h/2;
		}
		
#if 1
		// X
		// If the insurface are less wide than dst
		if(insurface->w < dst->w)
		{
			// insurface is outside of dst on the left
			if(dx < 0) {
				x1 = -dx;
				x2 = insurface->w;
			}
			// insurface is outside of dst on the right
			else if((dx + insurface->w) > dst->w)
			{
				x1 = 0;
				x2 = insurface->w + (dst->w - (insurface->w + dx));
			}
			// insurface is at dx=0
			else if(dx >= 0) {
				x1 = 0;
				x2 = insurface->w;
			}
		}
		// If the insurface is more wide than dst
		else
		{
			// insurface is outside dst on the right
			if(dx >= 0)
			{	
				x1 = 0;
				x2 = insurface->w + (dst->w - (insurface->w + dx));
			}
			// insurface is outside dst on the left
			else if(dx < 0 && ((insurface->w + dx) <= dst->w))
			{
				x1 = -dx;
				x2 = insurface->w;
			}
			// insurface is outside dst on the left and the right
			else if(dx < 0 && ((insurface->w + dx) >= dst->w))
			{
				x1 = -dx;
				x2 = insurface->w + (dst->w - (insurface->w + dx));
			}
		}
		
		// Y
		// If the insurface are less high than dst
		if(insurface->h < dst->h)
		{
			// insurface is outside of dst on the top
			if(dy < 0) {
				y1 = -dy;
				y2 = insurface->h;
			}
			// insurface is outside of dst on the bottom
			else if((dy + insurface->h) > dst->h)
			{
				y1 = 0;
				y2 = insurface->h + (dst->h - (insurface->h + dy));
			}
			// insurface is inside dst
			else if(dy >= 0) {
				y1 = 0;
				y2 = insurface->h;
			}
		}
		// If the insurface is more high than dst
		else
		{
			// insurface is outside dst on the bottom
			if(dy >= 0)
			{	
				y1 = 0;
				y2 = insurface->h + (dst->h - (insurface->h + dy));
			}
			// insurface is outside dst on the top
			else if(dy < 0 && ((insurface->h + dy) <= dst->h))
			{
				y1 = -dy;
				y2 = insurface->h;
			}
			// insurface is outside dst on the top and the bottom
			else if(dy < 0 && ((insurface->h + dy) >= dst->h))
			{
				y1 = -dy;
				y2 = insurface->h + (dst->h - (insurface->h + dy));
			}
		}
#endif
#if 0   // Just some code to compare fps
		x1 = 0;
		x2 = insurface->w;		
		y1 = 0;
		y2 = insurface->h;	
#endif
				
		// We can't use negative destinations for anything but calculating
		// the correct area of insurface to use.
		if(dx < 0)
			dx = 0;
		if(dy < 0)
			dy = 0;

		// Draw the selected area to dst
		for(int y = y1; y < y2; y++)
		{
			for(int x = x1; x < x2; x++)
			{
				// Get image pixel	
				getPixel(insurface, &ipixel, x, y);
				
				// Only draw if the color isn't R255, G0, B255 (transparent)
				if(!(ipixel.r == 255 && ipixel.g == 0 && ipixel.b == 255))
				{					
					// Do we want to use alpha?
					if(usealpha)
					{
						// Get surface pixel
						getPixel(dst, &spixel, (x-x1)+dx, (y-y1)+dy);
								
						// Calculate and draw new pixel from alpha
						setPixel(dst, (x-x1)+dx, (y-y1)+dy,
							(u8)(ipixel.r*(1-alpha) + spixel.r*alpha),
							(u8)(ipixel.g*(1-alpha) + spixel.g*alpha),
							(u8)(ipixel.b*(1-alpha) + spixel.b*alpha));
					}
					else
						setPixel(dst, (x-x1)+dx, (y-y1)+dy, ipixel.r, ipixel.g, ipixel.b);
				}
			}
		}
		if(useresize) // If we have used resize, this surface must be deleted
			freeSurface(insurface);
	}
	
	void Video::blitToScreen(surface *src, int dx, int dy, int width, int height, float alpha, bool centralize)
	{
		blit(src, sdl_back, dx, dy, width, height, alpha, centralize);
	}
	
	void Video::fillSurface(xio::surface *s, u32 color, int sx, int sy, int dx, int dy)
	{
		// Check if paramerers are invalid before we do anything
		if(sx < 0)
			xs::error.add("xio::Video::fillSurface - x is less than zero.");
		else if(sy < 0)
			xs::error.add("xio::Video::fillSurface - y is less than zero.");
		else if(dx > s->w)
			xs::error.add("xio::Video::fillSurface - x is more than screen width.");
		else if(dy > s->h)
			xs::error.add("xio::Video::fillSurface - y is more than screen height.");
		
		filldest.x = sx;
		filldest.y = sy;
		filldest.w = dx-sx;
		filldest.h = dy-sy;
		SDL_FillRect(s, &filldest, color);	
	}

	void Video::fillScreen(u32 color, int sx, int sy, int dx, int dy)
	{
		fillSurface(sdl_screen, color, sx, sy, dx, dy);		
	}
	
	void Video::fillSurfaceAll(xio::surface *s, u32 color)
	{
		fillSurface(s, color, 0, 0, s->w, s->h);
	}
	
	void Video::fillScreenAll(u32 color)
	{
		fillSurface(sdl_back, color, 0, 0, sdl_back->w, sdl_back->h);
	}
	
	// Keyboard ________________________________________________________________
	void Event::update()
	{
		SDL_Event event;
		SDL_PollEvent(&event);
		if(event.type == SDL_KEYDOWN)
			if(event.key.keysym.sym == SDLK_ESCAPE)
				xs::shutdown.run(0);
			else
				keyboard_input = event.key.keysym.sym;
#ifdef DEBUG
		else if(event.type == SDL_MOUSEBUTTONDOWN) {
				cout << "DEBUG | Mouse pressed at " << event.motion.x << ", "
						<< event.motion.y << "." << endl;
		}
#endif
		else if(event.type == SDL_QUIT)
			xs::shutdown.run(0);
	}
	
	// Sound ___________________________________________________________________
	Sound::Sound(bool use, int samplerate)
	{	
		use_sound = use;
#ifdef DEBUG
		cout << "DEBUG | Samplerate: " << samplerate << endl;
#endif
		
		if(use_sound)
		{
			// Open 22050 Hz, signed 16bit, system byte order, stereo audio,
			// using 1024 byte chunks
			if(Mix_OpenAudio(samplerate, MIX_DEFAULT_FORMAT, 2,
				1024) == -1)
			{	
				string warn = "Mix_OpenAudio: ";
				warn += Mix_GetError();
				xs::warning.add(warn.c_str());
			}
			else
				xs::shutdown.add(SDL_CloseAudio); 
		}
		else
			xs::warning.add("Sound not used!");
	}
		
	void Sound::play(const char *file)
	{
		if(use_sound)
		{
			FILE *fp = fopen(file, "r");
			if(fp)
			{
				Mix_VolumeMusic(MIX_MAX_VOLUME);
						
				snd = Mix_LoadMUS(file);
				
				if(!snd)
				{
					// This might be a critical error...
					string warn = "Mix_LoadMUS(";
					warn += file;
					warn += "): ";
					warn += Mix_GetError();
					xs::warning.add(warn.c_str());
				}
				
				// Start playing in an infinite loop
				if(-1 == Mix_PlayMusic(snd, -1))
				{
					string warn = "Mix_PlayMusic: ";
					warn += Mix_GetError();
					xs::warning.add(warn.c_str());
				}
			}
			else
			{
				string warn = "Could not open ";
				warn += file;
				warn += ", sound disabled.";
				xs::warning.add(warn.c_str());
			}
		}
	}
		
	void Sound::restart()
	{
		if(use_sound)
			Mix_SetMusicPosition(0);
	}
	
	void Sound::setVol(int vol)
	{
		if(use_sound)
			Mix_VolumeMusic(vol);
	}
	
// Timer _______________________________________________________________________
	Timer::Timer()
	{
#ifdef unix
		// Set start time to timer init
		gettimeofday(&tm,NULL);
		starttime = tm.tv_sec;
#endif
#ifdef WIN32
        timeBeginPeriod(1);	// 1 ms timer precision
	    starttime = timeGetTime();
#endif
	}
	
	long Timer::getTime()
	{
	#ifdef unix
    	gettimeofday(&tm,NULL);

		// Return in form of milliseconds
		return (long)(((double)tm.tv_sec - starttime
				+ ((double)tm.tv_usec)/1000000.0f)*1000);
	#endif
	#ifdef WIN32
        return (long)(timeGetTime() - starttime);
	#endif
	}

	void Timer::delay(long delaytime)
	{
		long time = getTime();
		while(getTime() - time < delaytime) {}
	}
	
    // Texture _________________________________________________________________
	xio::Texture::Texture()
	{	
#ifdef DEBUG
		kbytes = 0;
		using_loadall = false;
#endif
	}
	
	xio::surface *Texture::loadPNG(const char *file)
	{
		int x, y;
		int width, height;
		int num_pass;
		png_byte bit_depth;
		png_byte color_type;
		png_structp png_ptr;
		png_infop info_ptr;
		png_bytep *row_pointers;
		png_byte header[8];
		
		// Open file and check that it is a png
		FILE *fp = fopen(file, "rb");
		if (!fp) {
			string err = "[xio::texture::load_png] File ";
			err += file;
			err += " could not be opened for reading.";
			xs::error.add(err.c_str());
		}
		fread(header, 1, 8, fp);
		
		if(png_sig_cmp(header, 0, 8)) {
			string err = (string) "[xio::texture::load_png] File ";
			err += file;
			err	+= " is not recognized as a PNG file.";
			xs::error.add(err.c_str());
		}
	
		// Initialize
		png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
		
		if(!png_ptr)
			xs::error.add("[xio::texture::load_png] png_create_read_struct failed.");
	
		info_ptr = png_create_info_struct(png_ptr);
		if (!info_ptr)
			xs::error.add("[xio::texture::load_png] png_create_info_struct failed.");
	
		if (setjmp(png_jmpbuf(png_ptr)))
			xs::error.add("[xio::texture::load_png] Error during init_io.");
	
		png_init_io(png_ptr, fp);
		png_set_sig_bytes(png_ptr, 8);
	
		png_read_info(png_ptr, info_ptr);
	
		width = info_ptr->width;
		height = info_ptr->height;
		color_type = info_ptr->color_type;
		bit_depth = info_ptr->bit_depth;
	
		num_pass = png_set_interlace_handling(png_ptr);
		png_read_update_info(png_ptr, info_ptr);
	
	
		// Read file
		if(setjmp(png_jmpbuf(png_ptr)))
			xs::error.add("[xio::texture::loadPNG] Error during read_image.");
		
		// Allocate temporary memory
		row_pointers = (png_bytep*) malloc(sizeof(png_bytep) * height);
		for (y=0; y<height; y++)
			row_pointers[y] = (png_byte*) malloc(info_ptr->rowbytes);
	
		// Read image
		png_read_image(png_ptr, row_pointers);

#ifdef DEBUG
		kbytes += (width * height * 4)/1000;
#endif		
		// Get surface
		SDL_Surface *ts = SDL_CreateRGBSurface(SDL_SWSURFACE, width,
			height, 32, 0, 0, 0, 0);
		
		// Copy the color info into the texture 
		for (y=0; y<height; y++) {
			png_byte* row = row_pointers[y];
			for (x=0; x<width; x++) {
				png_byte* ptr = &(row[x*3]);
				((u32*)ts->pixels)[x + y*ts->w] =
					((int)(ptr[0]<<16)) + ((int)(ptr[1]<<8)) + (int)ptr[2];
			}
		}
		
		// Free temporary memory
		delete [] row_pointers;
		return ts;
	}

	
	void Texture::loadOne(const char *file)
	{
		string filename = file;
		string fe = filename.substr(filename.find_last_of(".")+1);
		string name = filename.substr(filename.find_last_of("/")+1);
		
		// hm.. well it works
		char na[255];
		for(int i = 0; i < 255; i++) na[i] = 0;
		for(unsigned int i = 0; i < name.length(); i++)
		{
			if(name[i] == '.')
				break;
			else
				na[i] = name[i];
		}
	
		if(fe == "png")
		{
			list[na] = loadPNG(file);
			string tmp = na;
#ifdef DEBUG
			if(using_loadall) {
				cout << na;
				cout.flush();
			}
			else
				cout << "DEBUG | Loading: " << na << " ("
					<< file << ")" << endl;
#endif
		}
	}
	
	surface *Texture::get(const char *idstr)
	{
		if(!list[idstr])
		{
			string out = "texture \"";
			out += idstr;
			out += "\" does not exist!";
			xs::error.add(out.c_str());
		}
		return list[idstr];
	}


#ifdef WIN32

	void Texture::loadAll(const char *dir)
	{
#ifdef DEBUG
		using_loadall = true; // Set loadOne's debug output behavior
#endif
		WIN32_FIND_DATA fileData;
		bool fFinished = FALSE;
		string str_dir = dir;


#ifdef DEBUG		
		cout << "DEBUG | Loading from " << dir << ": ";
		cout.flush();
#endif
		// Open the given directory.
		HANDLE directory = FindFirstFile((str_dir+(string)"/*").c_str(), &fileData);

		// Check if the given directory exists
		if(directory == INVALID_HANDLE_VALUE) 
		{ 
			string temp = "Cant find any files in the directory: " + (string)(dir);
			xs::error.add(temp.c_str());
		} 

		// print the names of the .png files found
		while(!fFinished) 
		{ 
			string str = fileData.cFileName;
			if(str != "." && str != "..")
			{
				string fe = str.substr(str.find_last_of(".")+1);
				if(fe == "png")
				{
					string fullname = (string) str_dir + "/" + str; 
					loadOne(fullname.c_str());
#ifdef DEBUG		
					cout << " ";
#endif
				}
			}			
			if (!FindNextFile(directory, &fileData)) 
			{
				if (GetLastError() == ERROR_NO_MORE_FILES) 
				{
#ifdef DEBUG
					cout << " (Successfully loaded files)" << endl;
#endif
					fFinished = TRUE; 
				} 
				else 
				{ 
					string temp = "Couldn't find next file.";
					xs::error.add(temp.c_str());
				} 
			}
		} 
#ifdef DEBUG
		using_loadall = false;
#endif
		// Close the search handle. 
		FindClose(directory);
	}

#else


	void Texture::loadAll(const char *dir)
	{
#ifdef DEBUG
		using_loadall = true; // Set loadOne's debug output behavior
#endif
		DIR *directory;
		directory = opendir(dir);
		
		rewinddir(directory);
		dirent *dnt;

#ifdef DEBUG		
		cout << "DEBUG | Loading from " << dir << ":";
		cout.flush();
#endif
		while((dnt = readdir(directory)))
		{
			string str = dnt->d_name;
			if(str != "." && str != "..")
			{
				string fe = str.substr(str.find_last_of(".")+1);
				if(fe == "png")
				{
					string fullname = (string) dir + "/" + str; 
#ifdef DEBUG		
					cout << " ";
#endif
					loadOne(fullname.c_str());
				}
			}			
		}
#ifdef DEBUG
		cout << "." << endl;
		using_loadall = false;
#endif
		closedir(directory);
	}
#endif
	
	void Texture::add(xio::surface *src, const char *name)
	{
		list[name] = src;
#ifdef DEBUG
		cout << "DEBUG | Adding texture \"" << name << "\" to list." << endl;
#endif		
	}
}
