/*
  File name: CSDLClass.cpp
  Copyright: (C) 2005 - 2013 Tomasz Kazmierczak

  This file is part of Cytadela

 *   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 3 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, see <http://www.gnu.org/licenses/>.  *

*/

#include <memory.h>
#include "CSDLClass.h"
#include "videoplayer.h"

CSDLClass *CSDLClass::instance()
{
	static CSDLClass inst;
	return &inst;
}

//init/free functions
CSDLClass::CSDLClass() : mScreen(0), mJoystick(0), mMusic(0), mAudioAvailable(false),
	mAudioEnabled(true), mLoadedSoundsCount(0), mMenuMouseXPos(0), mMenuMouseYPos(0),
	mLastMouseInputTime(0), mLastJoyInputTime(0), mScreenSizesCount(0),
	mScreenSizes(0), mScreenMode(FullScreen)
{
	memset(mSounds, 0, sizeof(mSounds));
}

bool CSDLClass::init()
{
	uint32_t flags = SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_NOPARACHUTE;
#if !defined(__MINGW32__) && !defined(__APPLE__)
	flags |= SDL_INIT_EVENTTHREAD; /* Not supported on Windows or Mac OS X */
#endif
	if(-1 == SDL_Init(flags)) {
		fprintf(stderr, "!SDL_Init(): %s\n", SDL_GetError());
		return false;
	}

	//check for needed screen sizes
	//size for 2D video mode
	flags = SDL_HWSURFACE | SDL_FULLSCREEN | SDL_DOUBLEBUF;
	if(not SDL_VideoModeOK(CSDL_DEFAULT_SCREEN_W, CSDL_DEFAULT_SCREEN_H, 24, flags)) {
		fprintf(stderr, "Hardware surface in %sx%s mode not available!\n",
		        CSDL_DEFAULT_SCREEN_W_TXT, CSDL_DEFAULT_SCREEN_H_TXT);
		SDL_Quit();
		return false;
	}
	//sizes for OpenGL (we need at least one of them)
	flags &= ~SDL_DOUBLEBUF;
	flags |= SDL_OPENGL;
	static ScreenSize sizes[] = {{{CSDL_DEFAULT_SCREEN_W_TXT, CSDL_DEFAULT_SCREEN_H_TXT},
	                               {CSDL_DEFAULT_SCREEN_W, CSDL_DEFAULT_SCREEN_H}, true},
	                              {{"800", "600"}, {800, 600}, true},
	                              {{"1024", "768"}, {1024, 768}, true},
	                              {{"1152", "864"}, {1152, 864}, true},
	                              {{"1280", "720"}, {1280, 720}, true},
	                              {{"1280", "800"}, {1280, 800}, true},
	                              {{"1280", "960"}, {1280, 960}, true},
	                              {{"1280", "1024"}, {1280, 1024}, true},
	                              {{"1366", "768"}, {1366, 768}, true},
	                              {{"1600", "1024"}, {1600, 1024}, true},
	                              {{"1600", "1200"}, {1600, 1200}, true},
	                              {{"1920", "1080"}, {1920, 1080}, true},
	                              {{"1920", "1200"}, {1920, 1200}, true},
	                              {{"2048", "1536"}, {2048, 1536}, true}};
	bool anySizeAvailable = false;
	//how many sizes the ScreenSizes array consists of?
	mScreenSizesCount = sizeof(sizes) / sizeof(ScreenSize);
	for(int32_t i = 0; i < mScreenSizesCount; i++) {
		if(SDL_VideoModeOK(sizes[i].resolution[0], sizes[i].resolution[1], 24, flags) < 16)
			sizes[i].available = false;
		else anySizeAvailable = true;
	}
	if(not anySizeAvailable) {
		fprintf(stderr, "No required screen resolutions in OpenGL mode available!\n"
		                "At least one of the following screen resolutions in OpenGL\n"
		                "mode is needed to run the game (in at least 16 bit color\n"
		                "mode):\n");
		for(int32_t i = 0; i < mScreenSizesCount; i++)
			fprintf(stderr, "%dx%d ", sizes[i].resolution[0], sizes[i].resolution[1]);
		fprintf(stderr, "\n");
		SDL_Quit();
		return false;
	}
	mScreenSizes = sizes;

 	SDL_ShowCursor(SDL_DISABLE);

	//if there is a joystick then we enable it's events and open it
	mJoystick = 0;
	if(SDL_NumJoysticks() > 0) {
		SDL_JoystickEventState(SDL_ENABLE);
		mJoystick = SDL_JoystickOpen(0);
	}

	//audio initialization
	if(0 == Mix_OpenAudio(44100, AUDIO_S16, 2, 4096))
		mAudioAvailable = true;
	else
		fprintf(stderr, "!Mix_OpenAudio(44100, AUDIO_S16, 2, 4096): %s\n", Mix_GetError());

	mMenuMouseXPos = 0;
	mMenuMouseYPos = 0;
	return true;
}

bool CSDLClass::switchVideoMode(int32_t width, int32_t height, VideoMode vmode, ScreenMode smode)
{
	if(smode != NoChange)
		mScreenMode = smode;

	uint32_t flags = SDL_HWSURFACE;
	if(mScreenMode == FullScreen)
		flags |= SDL_FULLSCREEN;

	switch(vmode) {
		case Mode2D: {
			flags |= SDL_DOUBLEBUF;
			break;
		}
		case Mode3D: {
			//attribs for OGL
			SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
			SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
			SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 1);
			SDL_GL_SetAttribute(SDL_GL_ACCUM_RED_SIZE, 8);
			SDL_GL_SetAttribute(SDL_GL_ACCUM_GREEN_SIZE, 8);
			SDL_GL_SetAttribute(SDL_GL_ACCUM_BLUE_SIZE, 8);
			flags |= SDL_OPENGL;
			break;
		}
	}

	mScreen = SDL_SetVideoMode(width, height, 24, flags);

	if(mScreen == 0) {
		fprintf(stderr, "!SDL_SetVideoMode(%d, %d, %s): %s\n", width, height,
		        (flags & SDL_OPENGL) ? "3D" : "2D", SDL_GetError());
		return false;
	}
	return true;
}

void CSDLClass::free()
{
	freeSounds();
	Mix_CloseAudio();
	if(mJoystick != 0)
		SDL_JoystickClose(mJoystick);
	SDL_WM_GrabInput(SDL_GRAB_OFF);
	SDL_ShowCursor(SDL_ENABLE);
	SDL_Quit();
}

//input functions
void CSDLClass::delayInput()
{
	uint32_t time = SDL_GetTicks();
	mLastMouseInputTime = time;
	mLastJoyInputTime = time;
}

void CSDLClass::clearEventQueue()
{
	SDL_Event event;
	while(SDL_PollEvent(&event));
}

SDLKey CSDLClass::readInputMenu()
{
	uint32_t time = SDL_GetTicks();
	SDL_Event event;
	//check if there are any events
	while(SDL_PollEvent(&event)) {
		switch(event.type) {
			case SDL_KEYDOWN: {
				switch(event.key.keysym.sym) {
				case SDLK_KP_ENTER:
					return SDLK_RETURN;
				case SDLK_ESCAPE:
				case SDLK_RETURN:
				case SDLK_SPACE:
				case SDLK_UP:
				case SDLK_DOWN:
				case SDLK_LEFT:
				case SDLK_RIGHT:
					return event.key.keysym.sym;
				default:
					break;
				}
				break;
			}

			case SDL_MOUSEMOTION: {
				mMenuMouseYPos += event.motion.yrel;
				if(mMenuMouseYPos > 30) {
					mMenuMouseYPos = 0;
					return SDLK_DOWN;
				}
				if(mMenuMouseYPos < -30) {
					mMenuMouseYPos = 0;
					return SDLK_UP;
				}
				mMenuMouseXPos += event.motion.xrel;
				if(mMenuMouseXPos > 30) {
					mMenuMouseXPos = 0;
					return SDLK_RIGHT;
				}
				if(mMenuMouseXPos < -30) {
					mMenuMouseXPos = 0;
					return SDLK_LEFT;
				}
				break;
			}

			default:
				break;
		}
	}
	//get mouse input
	if(SDL_GetMouseState(0, 0) & SDL_BUTTON(SDL_BUTTON_LEFT) or
	   SDL_GetMouseState(0, 0) & SDL_BUTTON(SDL_BUTTON_RIGHT)) {
		if(time - mLastMouseInputTime < 250)
			return SDLK_UNKNOWN;
		mLastMouseInputTime = time;
		return SDLK_RETURN;
	} else //if mouse buttons are released we don't delay their next input
	    mLastMouseInputTime = time - 250;
	//get joystick input
	if(mJoystick != 0) {
		int16_t y = SDL_JoystickGetAxis(mJoystick, 1);
		bool joyBtnPressed = (bool)SDL_JoystickGetButton(mJoystick, 0);
		//if there is no input from joy we don't delay its next input
		if(y < 3200 and y > -3200 and not joyBtnPressed) {
			mLastJoyInputTime = time - 250;
			return SDLK_UNKNOWN;
		}

		//delay of movement and 'fire' repetition
		if(time - mLastJoyInputTime < 250)
			return SDLK_UNKNOWN;
		mLastJoyInputTime = time;
		//button
		if(joyBtnPressed)
			return SDLK_RETURN;
		//axes
		if(y > 3200)
			return SDLK_DOWN;
		if(y < -3200)
			return SDLK_UP;
	}
	return SDLK_UNKNOWN;
}

void CSDLClass::readInputGame(InputArray *inputArray)
{
	SDL_Event event;
	//check if there are any events
	uint32_t readCounter = 0;
	while(SDL_PollEvent(&event)) {
		switch(event.type) {
			case SDL_KEYDOWN: {
				if(readCounter == 5)
					break;
				inputArray->key[readCounter] = event.key.keysym.sym;
				inputArray->status[readCounter] = true;
				++readCounter;
				break;
			}
			case SDL_KEYUP: {
				if(readCounter == 5)
					break;
				inputArray->key[readCounter] = event.key.keysym.sym;
				inputArray->status[readCounter] = false;
				++readCounter;
				break;
			}
			case SDL_MOUSEMOTION: {
				inputArray->mouseX = event.motion.xrel;
				inputArray->mouseY = event.motion.yrel;
				break;
			}
			default:
				break;
		}
	}
	inputArray->keyMap = SDL_GetKeyState(0);
	//if left mouse button is pressed
	if(SDL_GetMouseState(0, 0) & SDL_BUTTON(SDL_BUTTON_LEFT))
		inputArray->lmb = true;
	else
		inputArray->lmb = false;
	if(SDL_GetMouseState(0, 0) & SDL_BUTTON(SDL_BUTTON_RIGHT))
		inputArray->rmb = true;
	else
		inputArray->rmb = false;
	//get joystick input
	if(mJoystick != 0) {
		//axes
		int16_t x = SDL_JoystickGetAxis(mJoystick, 0);
		int16_t y = SDL_JoystickGetAxis(mJoystick, 1);
		if(y < -3200)
			inputArray->joyUp = true;
		else inputArray->joyUp = false;
		if(y > 3200)
			inputArray->joyDown = true;
		else inputArray->joyDown = false;
		if(x > 3200)
			inputArray->joyRight = true;
		else inputArray->joyRight = false;
		if(x < -3200)
			inputArray->joyLeft = true;
		else inputArray->joyLeft = false;
		//button
		inputArray->joyButtonPressed = (bool)SDL_JoystickGetButton(mJoystick, 0);
	}
}

//display functions
void CSDLClass::displayImage(uint32_t imgNum, SDL_Rect *srcRect, int16_t x, int16_t y)
{
	if(imgNum >= mImgArray.mImagesCount)
		return;
	SDL_Rect destRect = {x, y, mImgArray.mImageDims[imgNum].width,
	                     mImgArray.mImageDims[imgNum].height};
	SDL_BlitSurface(mImgArray.mImageSurface[imgNum], srcRect, mScreen, &destRect);
}

void CSDLClass::displayText(uint32_t imgNum, const char *text, int16_t x, int16_t y)
{
	if(imgNum >= mImgArray.mImagesCount)
		return;
	uint16_t width = mImgArray.mImageDims[imgNum].width / 16;
	uint16_t height = mImgArray.mImageDims[imgNum].height / 16;
	SDL_Rect srcRect = {0, 0, width, height};
	SDL_Rect destRect = {x, y, width, height};
	uint32_t textLen = strlen(text);
	int32_t newLinesCount = 0;
	for(uint32_t i = 0; i < textLen; ++i) {
		uint8_t c = text[i];
		//if new line
		if(c == '\n') {
			destRect.x = x;
			newLinesCount++;
			continue;
		}
		//find an apropirate character in the font image
		uint16_t xOffs = c % 16;
		uint16_t yOffs = c / 16;
		srcRect.x = xOffs * width;
		srcRect.y = yOffs * height;
		//we must modify the destRect.y every time before blitting because
		//SDL_BlitSurface() saves the final blit rectangle (after clipping)
		//in destRect, so if the Y coord was, for example, negative (text was
		//scrolled above the upper boundary of the screen) it has been changed
		//to 0 and the next drawn characters (in current line and in next lines
		//also) would not have been scrolled up
		//if we wanted to scroll the text horizontally we would do the same
		//with the X coord, of course
		destRect.y = y + newLinesCount * (height * 3 / 2);
		//blit the character image to the screen
		SDL_BlitSurface(mImgArray.mImageSurface[imgNum], &srcRect, mScreen, &destRect);
		destRect.x += width;
	}
}

void CSDLClass::blackScreen()
{
	SDL_FillRect(mScreen, 0, 0);
}

int32_t CSDLClass::findFirstAvailableScreenSize(int32_t start)
{
	//NOTE: if the CSDLClass was initialized in spite of
	//a failure to find at least one valid screen mode
	//this function will return 'start' and will _not_
	//report the lack of screen modes
	int32_t i = start + 1;
	for(; i < mScreenSizesCount; ++i) {
		if(mScreenSizes[i].available)
			break;
	}
	if(i < mScreenSizesCount)
		return i;
	for(i = 0; i < start; ++i) {
		if(mScreenSizes[i].available)
			break;
	}
	return i;
}

//audio functions
bool CSDLClass::loadMusic(const char *fileName)
{
	if(not mAudioAvailable)
		return true;

	if(mMusic != 0)
		Mix_FreeMusic(mMusic);

	mMusic = Mix_LoadMUS(fileName);
	if(mMusic == 0) {
		fprintf(stderr, "!Mix_LoadMUS(\"%s\"): %s\n", fileName, Mix_GetError());
		return false;
	}
	return true;
}

void CSDLClass::freeMusic()
{
	if(not mAudioAvailable)
		return;
	if(mMusic != 0) {
		Mix_FreeMusic(mMusic);
		mMusic = 0;
	}
}

void CSDLClass::playMusic(int32_t loops)
{
	if(mAudioEnabled and mAudioAvailable and -1 == Mix_PlayMusic(mMusic, loops))
		fprintf(stderr, "!Mix_PlayMusic(): %s\n", Mix_GetError());
}

void CSDLClass::stopMusic()
{
	Mix_HaltMusic();
}

int32_t CSDLClass::loadSound(const char *fileName)
{
	if(not mAudioAvailable)
		return CSDL_AUDIO_NOT_INITIALIZED;

	if(mLoadedSoundsCount == MAX_SOUNDS_COUNT) {
		fprintf(stderr, "Can't load more sounds than %d!\n", MAX_SOUNDS_COUNT);
		return CSDL_LOAD_SOUND_FAILED;
	}
	mSounds[mLoadedSoundsCount] = Mix_LoadWAV(fileName);
	if(not mSounds[mLoadedSoundsCount]) {
		fprintf(stderr, "!Mix_LoadWAV(\"%s\"): %s\n", fileName, Mix_GetError());
		return CSDL_LOAD_SOUND_FAILED;
	}
	mLoadedSoundsCount++;
	return (mLoadedSoundsCount-1);
}

void CSDLClass::freeSounds()
{
	//stop playback on all channels
	Mix_HaltChannel(-1);
	//and stop the music playback
	Mix_HaltMusic();

	//free sounds
	for(int32_t i = 0; i < mLoadedSoundsCount; ++i) {
		Mix_FreeChunk(mSounds[i]);
		mSounds[i] = 0;
	}
	mLoadedSoundsCount = 0;
}

void CSDLClass::playSoundFadeIn(int32_t sound, int32_t channel, int32_t loops, int32_t fadeInTime)
{
	if(sound < 0 or not mAudioAvailable or not mAudioEnabled)
		return;
	Mix_Volume(sound, 128);
	if(-1 == Mix_FadeInChannel(channel, mSounds[sound], loops, fadeInTime))
		fprintf(stderr, "!Mix_FadeInChannel(): %s\n", Mix_GetError());
}

void CSDLClass::playSound(int32_t sound, int32_t channel, int32_t volume, int32_t loops)
{
	if(sound < 0 or not mAudioAvailable or not mAudioEnabled)
		return;
	Mix_Volume(channel, volume);
	if(-1 == Mix_PlayChannel(channel, mSounds[sound], loops))
		fprintf(stderr, "!Mix_PlayChannel(): %s\n", Mix_GetError());
}

void CSDLClass::setSoundVolume(int32_t channel, int32_t volume)
{
	if(mAudioAvailable)
		Mix_Volume(channel, volume);
}

void CSDLClass::stopSound(int32_t channel, int32_t fadeOutTime)
{
	if(mAudioAvailable)
		Mix_FadeOutChannel(channel, fadeOutTime);
}

bool CSDLClass::isAudioEnabled() {
	return mAudioEnabled;
}

void CSDLClass::setAudioEnabled(bool enabled) {
	//if we are disabling the audio, then we have to stop all sounds and the music
	if(not enabled) {
		Mix_HaltChannel(-1);
		Mix_HaltMusic();
	}
	mAudioEnabled = enabled;
}

bool CSDLClass::playVideo(const char *fileName, bool offOnFinish, bool audio)
{
	return play(fileName, mScreen, offOnFinish, audio);
}

void CImagesArray::free()
{
	if(mImageDims) {
		delete[] mImageDims;
		mImageDims = 0;
	}

	if(mImageSurface) {
		for(uint32_t i = 0; i < mImagesCount; i++) {
			if(mImageSurface[i])
				SDL_FreeSurface(mImageSurface[i]);
		}
		delete[] mImageSurface;
		mImageSurface = 0;
	}
	mImagesCount = 0;
}

bool CImagesArray::loadImg(const char* farFileName, const char *imgFileName,  uint32_t &imgNum)
{
	//create new arrays
	SDL_Surface **tmpSurf = new SDL_Surface*[mImagesCount + 1];
	if(tmpSurf == 0) {
		fprintf(stderr, "CImagesArray::loadImg(%s, %s): !pTmpSurf\n", farFileName, imgFileName);
		return false;
	}
	ImgDims *tmpDims = new ImgDims[mImagesCount + 1];
	if(tmpDims == 0) {
		delete[] tmpSurf;
		fprintf(stderr, "CImagesArray::loadImg(%s, %s): !pTmpDims\n", farFileName, imgFileName);
		return false;
	}
	//copy elements (pointers to SDL_Surfaces and image dimms structres)
	//from old arrays to new ones
	for(uint32_t i = 0; i < mImagesCount; i++) {
		tmpSurf[i] = mImageSurface[i];
		tmpDims[i] = mImageDims[i];
	}

	//read image data
	uint8_t *data = farReadImgData(farFileName, imgFileName, tmpDims[mImagesCount]);
	if(data == 0) {
		delete[] tmpDims;
		delete[] tmpSurf;
		fprintf(stderr,"CImagesArray::loadImg(): !farReadImgData(%s, %s)\n",
		        farFileName, imgFileName);
		return false;
	}
	uint32_t w = tmpDims[mImagesCount].width;
	uint32_t h = tmpDims[mImagesCount].height;
	uint32_t bpp = tmpDims[mImagesCount].bitsPerPixel;
	uint32_t size = tmpDims[mImagesCount].size;

	uint32_t aMask = 0;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
	uint32_t bMask = 0xFF0000;
	uint32_t gMask = 0x00FF00;
	uint32_t rMask = 0x0000FF;
	if(bpp == 32) {
		bMask = 0xFF000000;
		gMask = 0x00FF0000;
		rMask = 0x0000FF00;
		aMask = 0x000000FF;
	}
#else
	uint32_t bMask = 0x000000FF;
	uint32_t gMask = 0x0000FF00;
	uint32_t rMask = 0x00FF0000;
	if(bpp == 32)
		aMask = 0xFF000000;
#endif

	//create the surface (in video memory - SDL_HWSURFACE)
	tmpSurf[mImagesCount] = SDL_CreateRGBSurface(SDL_HWSURFACE, w, h, bpp, rMask, gMask, bMask, aMask);
	if(tmpSurf[mImagesCount] == 0) {
		delete[] tmpDims;
		delete[] tmpSurf;
		delete[] data;
		fprintf(stderr,"CImagesArray::loadImg(%s, %s): !pTmpSurf[mImagesCount]\n",
		        farFileName, imgFileName);
		return false;
	}
	//copy pixels
	uint32_t linewidth = w * (bpp / 8);  //bytes per line
	//swap vertically while copying
	uint8_t *dest = (uint8_t *)(tmpSurf[mImagesCount]->pixels);
	for(uint32_t i = 0; i < h; ++i)
		memcpy(&dest[i * linewidth], &data[size - ((i + 1) * linewidth)], linewidth);
	//we don't need the data any more
	delete[] data;

	//delete old arrays and copy pointers to new arrays
	delete[] mImageDims;
	delete[] mImageSurface;
	mImageDims = tmpDims;
	mImageSurface = tmpSurf;
	imgNum = mImagesCount;
	mImagesCount++;
	return true;
}

const ImgDims *CImagesArray::getImageDimensions(uint32_t imageNum)
{
	if(imageNum < mImagesCount)
		return &mImageDims[imageNum];
	return 0;
}
