//	GamePlayState.cpp - Basic GameState which does everything via Lua.
//  ----------------------------------------------------------------------------
//	This file is part of 'NiallsAVLib', base code for any kind of audiovisual
//	apps.
//	Copyright (C) 2013  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 "NiallsOSCLib/OSCBundle.h"
#include "drawing/Drawer.h"
#include "GamePlayState.h"
#include "EXCOSCSounder.h"
#include "Application.h"
#include "LuaClasses.h"
#include "LuaDrawer.h"
#include "FilePath.h"
#include "UTF8File.h"

#include "luabind/luabind.hpp"

#include <sstream>

//------------------------------------------------------------------------------
GameStateCreator<GamePlayState> GamePlayState::creator;

//------------------------------------------------------------------------------
GamePlayState::GamePlayState(Application& app, shared_ptr<Sounder>& sound):
GameState(app, sound),
Thread("EXCOSC OSC Listen Thread"),
luaState(0),
fade(0.0f),
fadeTween(fade, 0.0f, 0.01f),
logFile(L"NiallsAVLibLuaLog"),
luaError(false),
timeToStop(false),
threadHasFinished(false),
sounder(dynamic_pointer_cast<EXCOSCSounder>(sound))
{
	int16_t port;
	std::string narrowStr;
	std::stringstream tempstr;
	const AppSettings& settings(app.getAppSettings());

	std::string luaFilePath;
#ifdef OSX
	FilePath scriptsDir(FilePath::getCurrentExecutableDir().getChild(L"scripts"));
#else
	FilePath scriptsDir(FilePath::getDataFilesDir().getChild(L"scripts"));
#endif
	FilePath luaFile(scriptsDir);

	try
	{
		luaState = luaL_newstate();
		luaL_openlibs(luaState);
		luabind::open(luaState);

		LuaClasses::registerLuaClasses(luaState);
		LuaDrawer::registerLuaMethods(luaState);

		//Register this class' sound methods.
		luabind::module(luaState)
		[
			luabind::class_<GamePlayState>("GamePlayState")
				.def("playMusic", &GamePlayState::playMusic)
				.def("stopMusic", &GamePlayState::stopMusic)
				.def("triggerSound", &GamePlayState::triggerSound)
				.def("triggerLoop", &GamePlayState::triggerLoop)
				.def("stopLoop", &GamePlayState::stopLoop)
				.def("toggleGranulator", &GamePlayState::toggleGranulator)
				.def("setGranulatorParams", &GamePlayState::setGranulatorParams)
				.def("toggleDelay", &GamePlayState::toggleDelay)
				.def("setDelayParams", &GamePlayState::setDelayParams)
				.def("toggleDistortion", &GamePlayState::toggleDistortion)
				.def("setDistortionGain", &GamePlayState::setDistortionGain)
				.def("sendOscMessage", &GamePlayState::sendOscMessage)
				.def("getDrawingAreaSize", &GamePlayState::getDrawingAreaSize)
		];

		for(int i=0;i<luaFile.getNumChildren();++i)
		{
			if(luaFile.getChild(i).getExtension() == L"lua")
			{
				UTF8File::wstringToChar(luaFile.getChild(i).getPath(), luaFilePath);
				luaL_dofile(luaState, luaFilePath.c_str());
			}
		}

		luabind::call_function<void>(luaState, "initialise", this);
	}
	catch(luabind::error& e)
	{
		handleError(lua_tostring(luaState, -1));
	}
	catch(const std::exception &err)
	{
		handleError(err.what());
	}

	fadeTween.setIndex(1.0f);

	//Setup listen socket.
	listenSocket.setAddress("127.0.0.1");
	UTF8File::wstringToChar(settings.getArgumentValue(L"Listen Port"), narrowStr);
	if(narrowStr.length() > 0)
	{
		tempstr << narrowStr;
		tempstr >> port;
	}
	else
		port = 5678;
	listenSocket.setPort(port);
	listenSocket.bindSocket();

	//Setup send socket.
	UTF8File::wstringToChar(settings.getArgumentValue(L"Send Address"), narrowStr);
	if(narrowStr.length() > 0)
		sendSocket.setAddress(narrowStr);
	else
		sendSocket.setAddress("127.0.0.1");
	UTF8File::wstringToChar(settings.getArgumentValue(L"Send Port"), narrowStr);
	if(narrowStr.length() > 0)
	{
		tempstr.clear();
		tempstr.str("");
		tempstr << narrowStr;
		tempstr >> port;
	}
	else
		port = 5679;
	sendSocket.setPort(port);

	startThread(this);
}

//------------------------------------------------------------------------------
GamePlayState::~GamePlayState()
{
	timeToStop = true;

	if(!luaError)
		lua_close(luaState);

	sounder->stopAll();

	while(!threadHasFinished)
	{
		Thread::wait(30);
	}
}

//------------------------------------------------------------------------------
void GamePlayState::update()
{
	//Handle any queued-up messages.
	//(this is necessary because all our lua functions have to be called from
	// the same thread)
	if(messageQueue.size() > 0)
	{
		ScopedLock lock(critSec);

		while(messageQueue.size() > 0)
		{
			const OSC::Message& message(messageQueue.front());

			if(message.getNumFloats() > 0.0f)
			{
				if(!luaError)
				{
					try
					{
						luabind::call_function<void>(luaState,
													 "oscMessageReceived",
													 this,
													 message.getAddress(),
													 message.getFloat(0));
					}
					catch(luabind::error& e)
					{
						handleError(lua_tostring(luaState, -1));
					}
					catch(const std::exception &err)
					{
						handleError(err.what());
					}
				}
			}

			messageQueue.pop();
		}
	}

	if(!luaError)
	{
		try
		{
			luabind::call_function<void>(luaState, "update", this);
		}
		catch(luabind::error& e)
		{
			handleError(lua_tostring(luaState, -1));
		}
		catch(const std::exception &err)
		{
			handleError(err.what());
		}
	}

	if(!fadeTween.getFinished())
	{
		fadeTween.tick();
		if(fade >= 1.0f)
			quit();
	}
}

//------------------------------------------------------------------------------
void GamePlayState::draw(shared_ptr<Drawer>& d, float timestepInterp)
{
	if(!luaDrawer.getDrawer())
		luaDrawer.setDrawer(d);

	if(!luaError)
	{
		try
		{
			luabind::call_function<void>(luaState, "draw", luaDrawer);

			if(!LuaClasses::getDebugOutput().empty())
			{
				luaDrawer.drawText(LuaClasses::getDebugOutput(),
								   "DebugFont",
								   TwoFloats(32.0f, d->getDrawingAreaSize().y*0.5f),
								   d->getDrawingAreaSize().x-64.0f,
								   0,
								   1.0f,
								   ThreeFloats(1.0f, 1.0f, 1.0f),
								   1.0f);
			}
		}
		catch(luabind::error& e)
		{
			handleError(lua_tostring(luaState, -1));
		}
		catch(const std::exception &err)
		{
			handleError(err.what());
		}
	}
	else
	{
		luaDrawer.drawText(lastError,
						   "DebugFont",
						   32.0f,
						   d->getDrawingAreaSize().x-64.0f,
						   0,
						   1.0f,
						   1.0f,
						   1.0f);
	}

	if(fade > 0.0f)
		d->fillRect(0.0f, d->getDrawingAreaSize(), 1.0f, fade);
}

//------------------------------------------------------------------------------
void GamePlayState::keyDown(const std::wstring& action,
						   const std::wstring& keyPressed,
						   int keyModifier)
{
	if(action == L"Escape")
	{
		fadeTween.reset(1.0f);
		sounder->stopAll();
	}
	else if(keyPressed == L"F5")
	{
		LuaClasses::getDebugOutput().clear();
		nextState(L"GamePlayState");
	}
	else if(!luaError)
	{
		try
		{
			luabind::call_function<void>(luaState,
										 "keyDown",
										 this,
										 getNarrowAction(action),
										 getNarrowAction(keyPressed),
										 keyModifier);
		}
		catch(luabind::error& e)
		{
			handleError(lua_tostring(luaState, -1));
		}
		catch(const std::exception &err)
		{
			handleError(err.what());
		}
	}
}

//------------------------------------------------------------------------------
void GamePlayState::keyUp(const std::wstring& action,
					     const std::wstring& keyPressed,
					     int keyModifier)
{
	if(!luaError)
	{
		try
		{
			luabind::call_function<void>(luaState,
										 "keyUp",
										 this,
										 getNarrowAction(action),
										 getNarrowAction(keyPressed),
										 keyModifier);
		}
		catch(luabind::error& e)
		{
			handleError(lua_tostring(luaState, -1));
		}
		catch(const std::exception &err)
		{
			handleError(err.what());
		}
	}
}

//------------------------------------------------------------------------------
void GamePlayState::mouseDown(int button, const TwoFloats& pos)
{
	if(!luaError)
	{
		try
		{
			luabind::call_function<void>(luaState,
										 "mouseDown",
										 this,
										 button,
										 pos);
		}
		catch(luabind::error& e)
		{
			handleError(lua_tostring(luaState, -1));
		}
		catch(const std::exception &err)
		{
			handleError(err.what());
		}
	}
}

//------------------------------------------------------------------------------
void GamePlayState::mouseUp(int button, const TwoFloats& pos)
{
	if(!luaError)
	{
		try
		{
			luabind::call_function<void>(luaState,
										 "mouseUp",
										 this,
										 button,
										 pos);
		}
		catch(luabind::error& e)
		{
			handleError(lua_tostring(luaState, -1));
		}
		catch(const std::exception &err)
		{
			handleError(err.what());
		}
	}
}

//------------------------------------------------------------------------------
void GamePlayState::mouseMove(const TwoFloats& pos)
{
	if(!luaError)
	{
		try
		{
			luabind::call_function<void>(luaState,
										 "mouseMove",
										 this,
										 pos);
		}
		catch(luabind::error& e)
		{
			handleError(lua_tostring(luaState, -1));
		}
		catch(const std::exception &err)
		{
			handleError(err.what());
		}
	}
}

//------------------------------------------------------------------------------
void GamePlayState::mouseWheel(bool dir)
{
	if(!luaError)
	{
		try
		{
			luabind::call_function<void>(luaState,
										 "mouseWheel",
										 this,
										 dir);
		}
		catch(luabind::error& e)
		{
			handleError(lua_tostring(luaState, -1));
		}
		catch(const std::exception &err)
		{
			handleError(err.what());
		}
	}
}

//------------------------------------------------------------------------------
void GamePlayState::joyMove(const std::wstring& action,
						   float value,
						   int joystick,
						   int axis)
{
	if(!luaError)
	{
		try
		{
			luabind::call_function<void>(luaState,
										 "joyMove",
										 this,
										 getNarrowAction(action),
										 value,
										 joystick,
										 axis);
		}
		catch(luabind::error& e)
		{
			handleError(lua_tostring(luaState, -1));
		}
		catch(const std::exception &err)
		{
			handleError(err.what());
		}
	}
}

//------------------------------------------------------------------------------
bool GamePlayState::run()
{
	int i;
	char *data;
	int32_t dataSize;

	while(!timeToStop)
	{
		data = listenSocket.getData(dataSize);

		if(dataSize > 0)
		{
			if(OSC::Message::isMessage(data, dataSize))
			{
				OSC::Message message(data, dataSize);

				handleOscMessage(message);
			}
			else if(OSC::Bundle::isBundle(data, dataSize))
			{
				OSC::Bundle bundle(data, dataSize);

				for(i=0;i<bundle.getNumMessages();++i)
					handleOscMessage(OSC::Message(*(bundle.getMessage(i))));
			}
		}
	}

	threadHasFinished = true;

	return true;
}

//------------------------------------------------------------------------------
void GamePlayState::playMusic(const std::string& sound)
{
	std::wstring wideSound;

	UTF8File::charToWstring(sound, wideSound);

	sounder->playMusic(wideSound);
}

//------------------------------------------------------------------------------
void GamePlayState::stopMusic()
{
	sounder->stopMusic();
}

//------------------------------------------------------------------------------
void GamePlayState::triggerSound(const std::string& sound,
								float level,
								float pan)
{
	std::wstring wideSound;

	UTF8File::charToWstring(sound, wideSound);

	sounder->triggerSound(wideSound, level, pan);
}

//------------------------------------------------------------------------------
int GamePlayState::triggerLoop(const std::string& sound,
							  float level,
							  float pan,
							  bool fade)
{
	std::wstring wideSound;

	UTF8File::charToWstring(sound, wideSound);

	return sounder->triggerLoop(wideSound, level, pan, fade);
}

//------------------------------------------------------------------------------
void GamePlayState::stopLoop(int index)
{
	sounder->stopLoop(index);
}

//------------------------------------------------------------------------------
void GamePlayState::toggleGranulator(bool val)
{
	sounder->toggleGranulator(val);
}

//------------------------------------------------------------------------------
void GamePlayState::setGranulatorParams(float density,
										float duration,
										float pitch,
										float feedback,
										float mix,
										float level)
{
	sounder->setGranulatorParams(density,
								 duration,
								 pitch,
								 feedback,
								 mix,
								 level);
}

//------------------------------------------------------------------------------
void GamePlayState::toggleDelay(bool val)
{
	sounder->toggleDelay(val);
}

//------------------------------------------------------------------------------
void GamePlayState::setDelayParams(float delayTime, float feedback, float mix)
{
	sounder->setDelayParams(delayTime, feedback, mix);
}

//------------------------------------------------------------------------------
void GamePlayState::toggleDistortion(bool val)
{
	sounder->toggleDistortion(val);
}

//------------------------------------------------------------------------------
void GamePlayState::setDistortionGain(float val)
{
	sounder->setDistortionGain(val);
}

//------------------------------------------------------------------------------
void GamePlayState::sendOscMessage(const std::string& address, float value)
{
	char *data;
	int32_t dataSize;
	OSC::Message message;

	message.setAddress(address);
	message.addFloat32(value);

	dataSize = message.getSize();
	data = message.getData();
	sendSocket.sendData(data, dataSize);
}

//------------------------------------------------------------------------------
const TwoFloats GamePlayState::getDrawingAreaSize() const
{
	return getApplication().getDrawingAreaSize();
}

//------------------------------------------------------------------------------
void GamePlayState::handleError(const std::string& err)
{
	lastError = err;
	logFile.write(lastError);
	luaError = true;

	if(luaState)
		lua_close(luaState);
}

//------------------------------------------------------------------------------
void GamePlayState::handleOscMessage(const OSC::Message& message)
{
	ScopedLock lock(critSec);

	messageQueue.push(message);
}
