//	SDL2.0Application.cpp - An implementation of Application for SDL 2.0.
//  ----------------------------------------------------------------------------
//	This file is part of 'NiallsAVLib', base code for any kind of audiovisual
//	apps.
//	Copyright (C) 2012  Niall Moody
//	
//	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 "SDL2.0Application.h"
#include "drawing/ScreenRes.h"
#include "drawing/Drawer.h"
#include "sound/Sounder.h"
#include "LoadingScreen.h"
#include "DebugHeaders.h"
#include "GlobalData.h"
#include "UTF8File.h"

#ifdef USE_PORTAUDIO
#include "sound/PortAudioDevice.h"
#endif
#include "SDL2.0AudioDevice.h"

#include "SDL_image.h"
#include "SDL.h"
#include <iostream>
#include <sstream>
#include <cassert>

#ifdef WIN32
#pragma warning(disable: 4996)
#endif

using std::wstringstream;
using std::wstring;
using std::string;
using std::vector;

//------------------------------------------------------------------------------
SDL2_0Application::SDL2_0Application():
timeToQuit(false),
lastRepaint(0),
timeAccumulator(0.0f),
lastFrameTime(1.0f/60.0f),
cmdWidth(-1),
cmdHeight(-1),
cmdWindow(false),
cmdMute(false),
cmdAudioAPI(L"SDL"),
cmdDebug(false),
cmdHelp(false),
vSync(true)
{
	
}

//------------------------------------------------------------------------------
SDL2_0Application::~SDL2_0Application()
{
	unsigned int i;

	if(audioDevice)
		audioDevice->stop();

	for(i=0;i<joysticks.size();++i)
		SDL_JoystickClose(joysticks[i]);

	if(!openglContext)
		SDL_GL_DeleteContext(openglContext);
	if(!window)
		SDL_DestroyWindow(window);

	IMG_Quit();
	SDL_Quit();
}

//------------------------------------------------------------------------------
void SDL2_0Application::initialise(CmdLineParams& cmdLine)
{
	int i;
	uint32_t sdlFlags = SDL_INIT_TIMER | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK;

	//Register our command line switch callbacks.
	cmdLine.registerCallback(L"width", L"--width", L"-w", true, L"-1", this);
	cmdLine.registerCallback(L"height", L"--height", L"-h", true, L"-1", this);
	cmdLine.registerCallback(L"window", L"--window", L"-win", false, L"", this);
	cmdLine.registerCallback(L"mute", L"--mute", L"-m", false, L"", this);
	cmdLine.registerCallback(L"drawer", L"--drawer", L"-d", true, L"", this);
	cmdLine.registerCallback(L"audioAPI", L"--audio-api", L"-a", true, L"SDL", this);
	cmdLine.registerCallback(L"debug", L"--debug", L"-debug", false, L"", this);
	cmdLine.registerCallback(L"help", L"--help", L"-help", false, L"", this);

	//Initialise our AppSettings.
	appSettings.initialise(cmdLine, this, keyManager, helpText);
	DebugStatement(L"SDL2.0Application: Initialised AppSettings.");

	//By this point we've registered all our GameStates, and one of them must
	//have been flagged as the start state.

	//If you hit this assert it means you haven't flagged any GameStates as
	//the start state.
	assert(startState != L"");

	//Handle any command line flags.
	cmdLine.handleParameters();
	//If the user passed in '--help' on the command line, we need to quit
	//immediately.
	if(cmdHelp)
		return;
	DebugStatement(L"SDL2.0Application: Handled all command line params.");

	//Construct the app's GlobalData.
	globalData = appSettings.getGlobalData();

	//If the user hasn't passed in the screen dimensions, set it to match
	//their desktop resolution.
	if(cmdWidth == -1)
		cmdWidth = ScreenResolution::getScreenWidth();
	if(cmdHeight == -1)
		cmdHeight = ScreenResolution::getScreenHeight();
	DebugStatement(L"SDL2.0Application: Screen width = " << cmdWidth << "; Screen height = " << cmdHeight);

	//Initialise SDL.
#ifdef USE_PORTAUDIO
	if(cmdAudioAPI == L"SDL")
#endif
		sdlFlags |= SDL_INIT_AUDIO;
	if(SDL_Init(sdlFlags) < 0)
	{
		DebugStatement(L"SDL2.0Application: Could not initialise SDL.");
		return;
	}
	DebugStatement("SDL2.0Application: SDL Initialised");

	//Handle any SDL-specific window creation stuff here.
	{
		int flags = SDL_WINDOW_OPENGL;

		//Try and accommodate 16-bit displays.
		if(is16Bit())
		{
			DebugStatement(L"SDL2.0Application: Display is 16bit.");
			if(SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5) < 0)
				DebugStatement(L"SDL2.0Application: Could not set red pixel size to 5.");
			if(SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 5) < 0)
				DebugStatement(L"SDL2.0Application: Could not set green pixel size to 5.");
			if(SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5) < 0)
				DebugStatement(L"SDL2.0Application: Could not set blue pixel size to 5.");
		}
		else
		{
			DebugStatement(L"SDL2.0Application: Display is 32bit.");
			if(SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8) < 0)
				DebugStatement(L"SDL2.0Application: Could not set red pixel size to 8.");
			if(SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8) < 0)
				DebugStatement(L"SDL2.0Application: Could not set green pixel size to 8.");
			if(SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8) < 0)
				DebugStatement(L"SDL2.0Application: Could not set blue pixel size to 8.");
			if(SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8) < 0)
				DebugStatement(L"SDL2.0Application: Could not set alpha pixel size to 8.");
		}
		//Set the depth buffer size.
		if(SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16) < 0)
			DebugStatement(L"SDL2.0Application: Could not set depth size to 16.");
		//Turn on double-buffering.
		if(SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1) < 0)
			DebugStatement(L"SDL2.0Application: Could not set double buffering.");

		//Make the window fullscreen, or centre it if not.
		if(!cmdWindow)
			flags |= SDL_WINDOW_FULLSCREEN;
		/*else
		{
			char tempstr[] = "SDL_VIDEO_CENTERED=1";
			putenv(tempstr); 
		}*/

		//Construct the actual window.
		{
			string tempstr;

			UTF8File::wstringToChar(AppSettings::applicationName, tempstr);
			window = SDL_CreateWindow(tempstr.c_str(),
									  SDL_WINDOWPOS_CENTERED,
									  SDL_WINDOWPOS_CENTERED,
									  cmdWidth,
									  cmdHeight,
									  flags);
		}
		if(!window)
		{
			DebugStatement(L"SDL2.0Application: Could not create window: " << SDL_GetError());

			quit();
			return;
		}
		DebugStatement(L"SDL2.0Application: Window constructed.");

		if(appSettings.getGrabMouse())
		{
			grabMouse(true);
			DebugStatement(L"SDL2.0Application: Grabbed mouse.");
		}

		//Construct the OpenGL context.
		openglContext = SDL_GL_CreateContext(window);
		if(!openglContext)
		{
			DebugStatement(L"SDL2.0Application: Could not create OpenGL context.");

			quit();
			return;
		}
		DebugStatement(L"SDL2.0Application: OpenGL context created.");

		//Turn on v-sync.
		if(vSync)
		{
			if(SDL_GL_SetSwapInterval(1) < 0)
			{
				DebugStatement(L"SDL2.0Application: Could not set vertical sync.");
			}
			else
				DebugStatement(L"SDL2.0Application: VSync is on.");
		}
	}

	//Construct and initialise the Drawer for this game.
	drawer = appSettings.getDrawer(cmdDrawer);
	drawer->initialise(cmdWidth, cmdHeight, appSettings);
	drawingSize = drawer->getDrawingAreaSize();
	DebugStatement(L"SDL2.0Application: Drawer initialised.");

	//Initialise any joysticks.
	SDL_JoystickEventState(SDL_ENABLE);

	for(i=0;i<SDL_NumJoysticks();++i)
		joysticks.push_back(SDL_JoystickOpen(i));
	DebugStatement(L"SDL2.0Application: Initialised " << joysticks.size() << L" joysticks.");

	//Initialise audio.
#ifdef USE_PORTAUDIO
	if(cmdAudioAPI == L"SDL")
#endif
	{
		audioDevice = AudioDevicePtr(new SDL2_0AudioDevice());
		DebugStatement(L"SDL2.0Application: Using SDL2.0AudioDevice for sound");
	}
#ifdef USE_PORTAUDIO
	else
	{
		audioDevice = AudioDevicePtr(new PortAudioDevice());
		DebugStatement(L"SDL2.0Application: Using PortAudioDevice for sound");
	}
#endif
	audioDevice->setAPI(cmdAudioAPI);
	audioDevice->setCallback(this);

	//Construct and initialise the Sounder for this game.
	sounder = appSettings.getSounder();
	sounder->setNumChannels(audioDevice->getNumInputs(),
							audioDevice->getNumOutputs());
	sounder->setSamplerate(audioDevice->getSamplerate());
	DebugStatement(L"SDL2.0Application: Sounder initialised.");

	//Start the audio.
	if(!audioDevice->start())
	{
		DebugStatement(L"SDL2.0Application: Could not start audio device.");
	}
	else
		DebugStatement(L"SDL2.0Application: Audio device started.");

	//Frustratingly, SDL may not give us the I/O and samplerate we requested, so
	//we need to update our Sounder accordingly.
	if(cmdAudioAPI == L"SDL")
	{
		sounder->setNumChannels(audioDevice->getNumInputs(),
								audioDevice->getNumOutputs());
		sounder->setSamplerate(audioDevice->getSamplerate());
	}
	DebugStatement(L"SDL2.0Application: Audio device num channels: " << audioDevice->getNumInputs() << L"(in) " << audioDevice->getNumOutputs() << L"(out)");
	DebugStatement(L"SDL2.0Application: Audio device samplerate: " << audioDevice->getSamplerate());

	//Hide the cursor.
	SDL_ShowCursor(SDL_DISABLE);

	//Initialise SDL_image.
	if(IMG_Init(IMG_INIT_JPG|IMG_INIT_PNG) == -1)
	{
		DebugStatement(L"SDL2.0Application: Could not initialise SDL_image. Error: " << IMG_GetError());
	}
	else
		DebugStatement(L"SDL2.0Application: SDL_image initialised.");

	//Create the LoadingScreen state.
	currentState = LoadingScreen::creator.createInstance(*this, sounder);
	//Set its colours.
	dynamic_pointer_cast<LoadingScreen>(currentState)->setColours(appSettings.getLoadingColours());

	DebugStatement(L"SDL2.0Application: Initialisation complete.");
}

//------------------------------------------------------------------------------
void SDL2_0Application::getAudio(float *input, float *output, int numSamples)
{
	if(sounder)
		sounder->getAudio(input, output, numSamples);
}

//------------------------------------------------------------------------------
void SDL2_0Application::eventLoop()
{
	const float dt = 1.0f/60.0f;
	uint32_t oldRepaint;
	uint32_t tempint;
	SDL_Event event;
	float frameTime;
	unsigned int i;

	DebugStatement(L"SDL2.0Application: Event loop started.");

	while(1)
	{
		if(timeToQuit)
			break;

		//Handle events.
		while(SDL_PollEvent(&event))
		{
			switch(event.type)
			{
				case SDL_KEYDOWN:
					if(!event.key.repeat)
					{
						keyManager.getActions(event.key.keysym, mappingsVector);

						for(i=0;i<mappingsVector.size();++i)
						{
							currentState->keyDown(mappingsVector[i],
												  keyManager.getKeyString(event.key.keysym.sym),
												  keyManager.getModVal((SDL_Keymod)event.key.keysym.mod));
						}
						if(!mappingsVector.size())
						{
							currentState->keyDown(L"",
												  keyManager.getKeyString(event.key.keysym.sym),
												  keyManager.getModVal((SDL_Keymod)event.key.keysym.mod));
						}
					}
					break;
				case SDL_KEYUP:
					keyManager.getActions(event.key.keysym, mappingsVector);

					for(i=0;i<mappingsVector.size();++i)
					{
						currentState->keyUp(mappingsVector[i],
											keyManager.getKeyString(event.key.keysym.sym),
											keyManager.getModVal((SDL_Keymod)event.key.keysym.mod));
					}
					if(!mappingsVector.size())
					{
						currentState->keyUp(L"",
											keyManager.getKeyString(event.key.keysym.sym),
											keyManager.getModVal((SDL_Keymod)event.key.keysym.mod));
					}
					break;
				case SDL_MOUSEBUTTONDOWN:
					currentState->mouseDown(event.button.button,
											mouseToDrawer(TwoFloats(event.motion.x,
																	event.motion.y)));
					break;
				case SDL_MOUSEBUTTONUP:
					currentState->mouseUp(event.button.button,
										  mouseToDrawer(TwoFloats(event.motion.x,
																  event.motion.y)));
					break;
				case SDL_MOUSEWHEEL:
					currentState->mouseWheel((event.wheel.y>0) ? true : false);
					break;
				case SDL_MOUSEMOTION:
					currentState->mouseMove(mouseToDrawer(TwoFloats(event.motion.x,
																	event.motion.y)));
					break;
				case SDL_QUIT:
					timeToQuit = true;
					break;
				case SDL_JOYAXISMOTION:
					keyManager.getActions(event.jaxis, mappingsVector);

					for(i=0;i<mappingsVector.size();++i)
					{
						currentState->joyMove(mappingsVector[i],
											  (float)(event.jaxis.value)/32768.0f,
											  event.jaxis.which,
											  event.jaxis.axis);
					}
					if(!mappingsVector.size())
					{
						currentState->joyMove(L"",
											  (float)(event.jaxis.value)/32768.0f,
											  event.jaxis.which,
											  event.jaxis.axis);
					}
					break;
				case SDL_JOYBUTTONDOWN:
					{
						wstring tempstr = keyManager.getJoyButtonText(event.jbutton);
						keyManager.getActions(event.jbutton, mappingsVector);

						for(i=0;i<mappingsVector.size();++i)
						{
							currentState->keyDown(mappingsVector[i],
												  tempstr,
												  0);
						}
						if(!mappingsVector.size())
							currentState->keyDown(L"", tempstr, 0);
					}
					break;
				case SDL_JOYBUTTONUP:
					{
						wstring tempstr = keyManager.getJoyButtonText(event.jbutton);
						keyManager.getActions(event.jbutton, mappingsVector);

						for(i=0;i<mappingsVector.size();++i)
						{
							currentState->keyUp(mappingsVector[i],
												tempstr,
												0);
						}
						if(!mappingsVector.size())
							currentState->keyUp(L"", tempstr, 0);
					}
					break;
				case SDL_JOYHATMOTION:
					{
						wstring tempstr = keyManager.getJoyHatText(event.jhat);
						keyManager.getActions(event.jhat, mappingsVector);

						for(i=0;i<mappingsVector.size();++i)
						{
							currentState->keyUp(mappingsVector[i],
												tempstr,
												0);
						}
						if(!mappingsVector.size())
							currentState->keyUp(L"", tempstr, 0);
					}
					break;
			}
		}

		//Work out delta value for this frame/update.
		oldRepaint = lastRepaint;
		lastRepaint = SDL_GetTicks();
		tempint = lastRepaint - oldRepaint;

		//int updates = 0;

		//Update the current state.
		frameTime = (float)tempint*0.001f;

		//We apply a low pass filter to frame time to smooth out any jitters...
		lastFrameTime = lastFrameTime + ((frameTime - lastFrameTime)/32.0f);
		timeAccumulator += lastFrameTime;
		//...and limit timeAccumulator for the same reason.
		if(timeAccumulator > (dt * 1.25f))
			timeAccumulator = dt * 1.25f;

		while(timeAccumulator >= dt)
		{
			currentState->update();
			//++updates;

			timeAccumulator -= dt;
		}
		/*if(!updates)
		{
			DebugStatement(lastRepaint << L": " << L"No update() in this frame!");
		}
		else if(updates > 1)
			DebugStatement(lastRepaint << L": " << updates << L" updates this frame!");
		if(!updates)
		{
			currentState->update();
			timeAccumulator += dt * 0.5f;
		}*/

		//Do the drawing.
		drawer->beginDrawing();
		currentState->draw(drawer, timeAccumulator/dt);
		drawer->endDrawing();
		SDL_GL_SwapWindow(window);
	}
	DebugStatement(L"SDL2.0Application: Event loop finished.");
}

//------------------------------------------------------------------------------
void SDL2_0Application::quit()
{
	timeToQuit = true;
	DebugStatement(L"SDL2.0Application: quit() called.");
}

//------------------------------------------------------------------------------
void SDL2_0Application::setMousePos(const TwoFloats& pos)
{
	TwoFloats tempPos;
	const TwoFloats& drawSize = drawer->getDrawingAreaSize();
	const TwoFloats& screenSize = drawer->getWindowSize();

	tempPos.x = pos.x/drawSize.x;
	tempPos.x *= screenSize.x;
	tempPos.y = pos.y/drawSize.y;
	tempPos.y *=  screenSize.y;

	SDL_WarpMouseInWindow(window, (int)tempPos.x, (int)tempPos.y);
}

//------------------------------------------------------------------------------
void SDL2_0Application::grabMouse(bool val)
{
	if(val)
		SDL_SetWindowGrab(window, SDL_TRUE);
	else
		SDL_SetWindowGrab(window, SDL_FALSE);
}

//------------------------------------------------------------------------------
void SDL2_0Application::nextState(const wstring newState, const wstring extra)
{
	wstring tempState;

	if(newState == L"-start state-")
		tempState = startState;
	else
		tempState = newState;

	//Check if the newState exists.
	if(stateCreators.find(tempState) != stateCreators.end())
	{
		//Delete the current state.
		currentState.reset();

		//Construct the new state.
		currentState = stateCreators[tempState]->createInstance(*this, sounder);
		DebugStatement(L"SDL2_0Application: New state created: " << tempState);

		//Reset lastFrameTime in case LoadingScreen messed with it.
		lastFrameTime = 1.0f/60.0f;
	}
	else
		DebugStatement(L"SDL2_0Application: Could not find a GameStateCreator for " << tempState);
}

//------------------------------------------------------------------------------
void SDL2_0Application::registerGameState(const wstring& stateName,
										  const GameStateCreatorBase *stateCreator,
										  bool isStartState)
{
	stateCreators.insert(make_pair(stateName, stateCreator));
	if(isStartState)
		startState = stateName;
}

//------------------------------------------------------------------------------
uint32_t SDL2_0Application::getTimeSinceStart() const
{
	return SDL_GetTicks();
}

//------------------------------------------------------------------------------
void SDL2_0Application::setAudioDevice(AudioDevice *newDevice)
{
	if(audioDevice)
		audioDevice->stop();

	audioDevice.reset(newDevice);
	audioDevice->setCallback(this);
}

//------------------------------------------------------------------------------
void SDL2_0Application::cmdLineParameterFound(const std::wstring& name,
										      const std::wstring& value)
{
	wstringstream tempstr;

	if(name == L"width")
	{
		tempstr << value;
		tempstr >> cmdWidth;
	}
	else if(name == L"height")
	{
		tempstr << value;
		tempstr >> cmdHeight;
	}
	else if(name == L"window")
		cmdWindow = true;
	else if(name == L"mute")
		cmdMute = true;
	else if(name == L"drawer")
		cmdDrawer = value;
	else if(name == L"audioAPI")
		cmdAudioAPI = value;
	else if(name == L"debug")
		DebugLogFile::getInstance().setLogging(true);
	else if(name == L"help")
	{
		//We write the help text to the command line in CmdLineParams.
		cmdHelp = true;
		quit();
	}
}

//------------------------------------------------------------------------------
TwoFloats SDL2_0Application::mouseToDrawer(const TwoFloats& pos)
{
	TwoFloats retval;
	const TwoFloats& drawSize = drawer->getDrawingAreaSize();
	const TwoFloats& screenSize = drawer->getWindowSize();
	//const float dAspect = drawSize.y/drawSize.x;
	//const float aspect = screenSize.y/screenSize.x;

	retval.x = pos.x/screenSize.x;
	retval.x *= drawSize.x;
	retval.y = pos.y/screenSize.y;
	retval.y *=  drawSize.y;

	return retval;
}

//------------------------------------------------------------------------------
bool SDL2_0Application::is16Bit()
{
	bool retval = false;
	SDL_DisplayMode mode;

	//This will not necessarily work on multi-monitor setups?
	SDL_GetDesktopDisplayMode(0, &mode);

	if(mode.format < SDL_PIXELFORMAT_RGB24)
		retval = true;

	return retval;
}

