#include "ifilesystem.h"
#include "core.h"
#include "log.h"
#include "engine.h"
#include "game.h"
#include "game_lua_help.h"
#include "game_ui.h"
#include "inputmanager.h"
#include "ientity.h"
#include "entitymanager.h"
#include "world.h"
#include "game_object.h"
#include "soundsystem.h"
#include "animation.h"
#include "scenemanager.h"
#include "game_world_objects.h"
#include "actor_base.h"
#include "physics/physicsworld.h"

#include <pugixml.hpp>

#include <map>
#include <string>

static Game s_game;
Game* g_game = &s_game;

class GameSoundSystem
{
public:
	static GameSoundSystem& GetInstance();

public:
	void PlaySound2D(const std::string& filename);
	void PlaySound3D(const std::string& filename, const glm::vec3& pos);
	void CacheSound(const std::string& filename);

private:
	std::map<std::string, SoundHandle> m_sounds;
};

GameSoundSystem& GameSoundSystem::GetInstance()
{
	static GameSoundSystem inst;
	return inst;
}

void GameSoundSystem::PlaySound2D(const std::string& filename)
{
	auto it = m_sounds.find(filename);
	if (it == m_sounds.end())
	{
		CacheSound(filename);
		it = m_sounds.find(filename);
	}

	SDL_assert(it != m_sounds.end());

	// .hack due unimplemented sound sources^
	if (g_soundSystem.IsPlaying(it->second))
		g_soundSystem.Stop(it->second);

	g_soundSystem.Play(it->second, false);
}

void GameSoundSystem::PlaySound3D(const std::string& filename, const glm::vec3& pos)
{
	auto it = m_sounds.find(filename);
	if (it == m_sounds.end())
	{
		CacheSound(filename);
		it = m_sounds.find(filename);
	}

	SDL_assert(it != m_sounds.end());

	// .hack due unimplemented sound sources^
	if (g_soundSystem.IsPlaying(it->second))
		g_soundSystem.Stop(it->second);

	g_soundSystem.Play3D(it->second, pos.x, pos.y, pos.z, true);
}

void GameSoundSystem::CacheSound(const std::string& filename)
{
	m_sounds.emplace(filename, g_soundSystem.LoadSound(filename.c_str()));
}

void consoleMsg(const char* msg)
{
	Logger::Msg("%s", msg);
}

void engineError(const char* msg)
{
	Core::Error("%s", msg);
}

void engineWarning(const char* msg)
{
	Core::Warning("%s", msg);
}

void enginePlaySound(const char* filename)
{
	GameSoundSystem::GetInstance().PlaySound2D(filename);
}

void enginePlaySound3D(const char* filename, float x, float y, float z)
{
	GameSoundSystem::GetInstance().PlaySound3D(filename, glm::vec3(x, y, z));
}

LuaPlus::LuaObject engineCreateEntity(const char* classname)
{
	Entity* entity = static_cast<Entity*>(g_game->Lua_CreateEntity(classname));
	SDL_assert_always(entity);
	
	entity->Init();

	return entity->GetLuaObject();
}

LuaPlus::LuaObject engineGetEntityFromID(lua_Integer id)
{
	LuaPlus::LuaObject lookup = GetLuaState().GetGlobal("g_entities");
	LuaPlus::LuaObject entityTable = lookup.GetByIndex(id);
	return entityTable;
}

LuaPlus::LuaObject engineFindEntityByName(const char* name)
{
	Entity* entity = (Entity*)g_game->FindEntityByName(name);
	if (!entity)
	{
		LuaPlus::LuaObject table;
		table.AssignNil(GetLuaState());
		return table;
	}

	return entity->GetLuaObject();
}

void engineAddEntityToWorld(LuaPlus::LuaObject& object)
{
	LuaPlus::LuaObject cppclass = object["__object"];
	SDL_assert_always(cppclass.IsLightUserdata());

	Entity* entity = static_cast<Entity*>(cppclass.GetLightUserdata());
	g_world->AddEntity(entity);
}

LuaPlus::LuaObject engineTraceRay(float rayBeginX, float rayBeginY, float rayBeginZ,
	float rayEndX, float rayEndY, float rayEndZ, const LuaPlus::LuaObject& ignoreTable)
{
	IEntityBase* pIgnoreEntity = nullptr;
	if (ignoreTable.IsTable())
	{
		LuaPlus::LuaObject ignoreEntityTable = ignoreTable.GetByName("__object");
		if (ignoreEntityTable.IsLightUserdata())
			pIgnoreEntity = (IEntityBase*)ignoreEntityTable.GetLightUserdata();
	}

	TraceRayResult result = {};
	glm::vec3 rayBegin = glm::vec3(rayBeginX, rayBeginY, rayBeginZ);
	glm::vec3 rayEnd = glm::vec3(rayEndX, rayEndY, rayEndZ);

	if (g_PhysicsWorld)
		g_PhysicsWorld->TraceRay(result, rayBegin, rayEnd, pIgnoreEntity);
	else
		Logger::Msg("engine.trace_ray(): no server started or game loaded!");

	LuaPlus::LuaObject resultTable = GetLuaState().CreateTable();
	resultTable.SetNumber("pos_x", result.position.x);
	resultTable.SetNumber("pos_y", result.position.y);
	resultTable.SetNumber("pos_z", result.position.z);
	resultTable.SetNumber("normal_x", result.normal.x);
	resultTable.SetNumber("normal_y", result.normal.y);
	resultTable.SetNumber("normal_z", result.normal.z);
	resultTable.SetInteger("entity_id", result.pEntity ? result.pEntity->GetID() : -1);
	resultTable.SetInteger("hit", result.hit);
	return resultTable;
}

float engineGetDelta()
{
	return GetEngine()->GetDelta();
}

void registerEngine()
{
	using namespace LuaPlus;

	// register engine functions
	LuaObject engineTable = GetLuaState().GetGlobals().CreateTable("engine");
	engineTable.RegisterDirect("error", &engineError);
	engineTable.RegisterDirect("warning", &engineWarning);
	engineTable.RegisterDirect("create_entity", &engineCreateEntity);
	engineTable.RegisterDirect("add_entity_to_world", &engineAddEntityToWorld);
	engineTable.RegisterDirect("get_entity_from_id", &engineGetEntityFromID);
	engineTable.RegisterDirect("find_entity_by_name", &engineFindEntityByName);
	engineTable.RegisterDirect("play_sound", &enginePlaySound);
	engineTable.RegisterDirect("play_sound_3d", &enginePlaySound3D);
	engineTable.RegisterDirect("get_delta", &engineGetDelta);
	engineTable.RegisterDirect("trace_ray", &engineTraceRay);

	LuaObject consoleTable = GetLuaState().GetGlobals().CreateTable("console");
	consoleTable.RegisterDirect("print", &consoleMsg);

	registerCamera();

	registerInput();

	registerEngineUI();

	// action globals
	GetLuaState().DoString("ACTION_FIRE = 0");
	GetLuaState().DoString("ACTION_ALT_FIRE = 1");
	GetLuaState().DoString("ACTION_RELOAD = 2");
	GetLuaState().DoString("ACTION_USE = 3");

	// animations globals
	GetLuaState().DoString("ANIM_PLAYBACK_NONE = 0");
	GetLuaState().DoString("ANIM_PLAYBACK_REPEAT = 1");


	char buffer[64];

#define REGISTER_CONSTANT(constant) \
	snprintf(buffer, sizeof(buffer), #constant" = %d", constant); \
	GetLuaState().DoString(buffer)

	REGISTER_CONSTANT(EMovementDir_None);
	REGISTER_CONSTANT(EMovementDir_Forward);
	REGISTER_CONSTANT(EMovementDir_Backward);
	REGISTER_CONSTANT(EMovementDir_Left);
	REGISTER_CONSTANT(EMovementDir_Right);
	REGISTER_CONSTANT(EMovementDir_Jump);

	// Physics
	REGISTER_CONSTANT(PhysicsFilter_None);
	REGISTER_CONSTANT(PhysicsFilter_Player);
	REGISTER_CONSTANT(PhysicsFilter_Triggers);
	REGISTER_CONSTANT(PhysicsFilter_Usable);
	REGISTER_CONSTANT(PhysicsFilter_NPC);
	REGISTER_CONSTANT(PhysicsFilter_Obstacle);
	REGISTER_CONSTANT(kPhysicsFilter_AllAux);

#undef REGISTER_CONSTANT
}

int inputGetMousePos(LuaPlus::LuaState* state)
{
	const glm::vec2& pos = g_inputManager.GetMousePos();
	state->PushNumber(pos.x);
	state->PushNumber(pos.y);
	return 2;
}

int inputGetDeltaMousePos(LuaPlus::LuaState* state)
{
	const glm::vec2& pos = g_inputManager.GetMouseDeltaPos();
	state->PushNumber(pos.x);
	state->PushNumber(pos.y);
	return 2;
}

void inputLock(bool value)
{
	g_inputManager.SetRelativeMouseMode(value);
}

bool inputGetLock()
{
	return g_inputManager.GetRelativeMouseMode();
}

void registerInput()
{
	LuaPlus::LuaObject inputTable = GetLuaState().GetGlobals().CreateTable("input");
	inputTable.Register("get_mouse_pos", &inputGetMousePos);
	inputTable.Register("get_delta_mouse_pos", &inputGetDeltaMousePos);
	inputTable.RegisterDirect("lock_mouse", &inputLock);
	inputTable.RegisterDirect("get_lock_mouse", &inputGetLock);
}

void toggleNoclip()
{
	static bool s_noclip = false;

	s_noclip = !s_noclip;

	LuaPlus::LuaObject player = GetLuaState().GetGlobal("g_player");
	if (player.IsTable())
	{
		ActorBase* actor = (ActorBase*)player["__object"].GetLightUserdata();
		actor->SetNoclip(s_noclip);
	}
}

int cameraGetPos(LuaPlus::LuaState* state)
{
	glm::vec3 v = glm::vec3(0.0f);

	Camera* camera = g_cameraManager.GetActiveCamera();
	if (camera)
		v = camera->GetPosition();

	state->PushNumber(v.x);
	state->PushNumber(v.y);
	state->PushNumber(v.z);

	return 3;
}

int cameraGetFront(LuaPlus::LuaState* state)
{
	glm::vec3 v = glm::vec3(0.0f);

	Camera* camera = g_cameraManager.GetActiveCamera();
	if (camera)
		v = camera->GetFront();

	state->PushNumber(v.x);
	state->PushNumber(v.y);
	state->PushNumber(v.z);

	return 3;
}

int cameraGetRight(LuaPlus::LuaState* state)
{
	glm::vec3 v = glm::vec3(0.0f);

	Camera* camera = g_cameraManager.GetActiveCamera();
	if (camera)
		v = camera->GetRight();

	state->PushNumber(v.x);
	state->PushNumber(v.y);
	state->PushNumber(v.z);

	return 3;
}

int cameraGetUp(LuaPlus::LuaState* state)
{
	glm::vec3 v = glm::vec3(0.0f);

	Camera* camera = g_cameraManager.GetActiveCamera();
	if (camera)
		v = camera->GetUp();

	state->PushNumber(v.x);
	state->PushNumber(v.y);
	state->PushNumber(v.z);

	return 3;
}

int cameraGetYaw(LuaPlus::LuaState* state)
{
	float v = 0.0f;

	Camera* camera = g_cameraManager.GetActiveCamera();
	if (camera)
		v = camera->GetYaw();

	state->PushNumber(v);

	return 1;
}

int cameraGetPitch(LuaPlus::LuaState* state)
{
	float v = 0.0f;

	Camera* camera = g_cameraManager.GetActiveCamera();
	if (camera)
		v = camera->GetPitch();

	state->PushNumber(v);

	return 1;
}

void cameraSetYawPitch(float yaw, float pitch)
{
	Camera* camera = g_cameraManager.GetActiveCamera();
	if (camera)
		camera->SetYawPitch(yaw, pitch);
}

void registerCamera()
{
	LuaPlus::LuaObject cameraTable = GetLuaState().GetGlobals().CreateTable("camera");
	cameraTable.Register("get_position", &cameraGetPos);
	cameraTable.Register("get_front", &cameraGetFront);
	cameraTable.Register("get_right", &cameraGetRight);
	cameraTable.Register("get_up", &cameraGetUp);
	cameraTable.Register("get_yaw", &cameraGetYaw);
	cameraTable.Register("get_pitch", &cameraGetPitch);
	cameraTable.RegisterDirect("set_yaw_pitch", &cameraSetYawPitch);
}

void registerClasses()
{
	using namespace LuaPlus;

	// base thing
	GetLuaState().GetGlobals().RegisterDirect("load_script", &luaLoadScript);
	GetLuaState().DoString("g_factory = {}");
	GetLuaState().DoString("g_entities = {}");

	registerEngine();
}

struct LuaPrototype
{
	std::string m_luaname;
	std::string m_enginename;
	std::string m_description;
};

std::vector<LuaPrototype> g_prototypes;

void initializeEntityPrototypesFromLua()
{
	using namespace LuaPlus;
	LuaObject entitiesTable = GetLuaState().GetGlobal("g_entity_table");

	for (LuaTableIterator it(entitiesTable); it; it.Next()) {
		LuaObject entityTable = it.GetValue();
		assert(entityTable.IsTable());

		if (entityTable.GetTableCount() < 3)
		{
			Core::Error("Entry in entity prototype table is bad, missing argument");
		}

		LuaObject luaname = entityTable[1];
		LuaObject classname = entityTable[2];
		LuaObject description = entityTable[3];
		
		Logger::Msg("lua ent: name: %s classname: %s description: %s", luaname.ToString(), classname.ToString(), description.ToString());

		LuaPrototype prototype;
		prototype.m_luaname = luaname.ToString();
		prototype.m_enginename = classname.ToString();
		prototype.m_description = description.ToString();
		g_prototypes.push_back(prototype);

		// get a prototype
		LuaObject prototypeTable = GetLuaState().GetGlobal(luaname.ToString());
		SDL_assert_always(!prototypeTable.IsNil());
		prototypeTable.SetObject("__index", prototypeTable);

		
		/*for (LuaTableIterator it2(entityTable); it2; it2.Next()) {
			Logger::Msg("%s", it2.GetValue().ToString());
		}*/
	}
}

void initializeEntitiesTable()
{
	using namespace LuaPlus;

	LuaObject entitiesTable = GetLuaState().GetGlobal("g_entity_table");
	if (entitiesTable.IsNil()) {
		Core::Error("initializeEntitiesTable: failed initialize the entity table! g_entity_table is nil");
	}

	if (!entitiesTable.IsTable()) {
		Core::Error("initializeEntitiesTable: failed initialize the entity table! g_entity_table is not a table");
	}

	initializeEntityPrototypesFromLua();
}

void initializeLua()
{
	using namespace LuaPlus;

	// register base classes
	registerClasses();

	// load game base script
	luaLoadScript("game_init.lua");
}

void Game::Init()
{
	initializeLua();
	initializeEntitiesTable();

	luaCallFunction("sv_game_init");
}

void Game::InitForNewMap(const char* mapname)
{
	LoadLevelXML(mapname);

	luaCallFunction("sv_on_game_start");
}

void Game::LoadLevelXML(const char* mapname)
{
	// Load XML file of the level
	char buffer[kMaxPathLength];
	snprintf(buffer, kMaxPathLength, "data/levels/%s/%s.xml", mapname, mapname);

	FileHandle_t file = GetFileSystem()->OpenFile(buffer, "rb");
	SDL_assert_always(file != kInvalidFileHandleValue);

	size_t length = GetFileSystem()->GetFileLength(file);

	char* filedata = new char[length + 1];
	GetFileSystem()->ReadFile(file, filedata, length);
	filedata[length] = '\0';

	GetFileSystem()->CloseFile(file);

	pugi::xml_document doc;
	pugi::xml_parse_result result = doc.load_buffer(filedata, length);
	delete[] filedata;
	if (!result) {
		Core::Error("Game::LoadLevelXML: Error while reading level description file '%s'\nError: %s:%i",
		buffer, result.description(), result.offset);
	}

//	std::map<std::string, std::string> properties;

	for (pugi::xml_node entitynode : doc.child("Level").child("Entities").children("Entity")) {
		pugi::xml_attribute entityname = entitynode.attribute("name");
		pugi::xml_attribute classname = entitynode.attribute("classname");

		if (classname.empty()) {
			if (!entityname.empty()) {
				Core::Error(" Game::LoadLevelXML: Classname is not specified for entity '%s'", entityname.as_string());
			}
			else {
				Core::Error(" Game::LoadLevelXML: Classname is not specified for entity");
			}
		}

		// Create entity and expose it to the engine
		Entity* entity = static_cast<Entity*>(Lua_CreateEntity(classname.as_string()));

		if (!entityname.empty())
			entity->SetName(entityname.as_string());

		pugi::xml_node position = entitynode.child("Position");
		if (position)
		{
			float x = position.attribute("x").as_float();
			float y = position.attribute("y").as_float();
			float z = position.attribute("z").as_float();

			entity->SetPosition(glm::vec3(x, y, z));
		}

		pugi::xml_node model = entitynode.child("Model");
		if (model)
		{
			const char* filename = model.attribute("filename").as_string();
			if (filename)
				entity->LoadModel(filename);
		}

		pugi::xml_node animation = entitynode.child("Animation");
		if (animation)
		{
			const char* name = animation.attribute("name").as_string();
			int mode = animation.attribute("mode").as_int();
			SkeletonInstance* skeleton = entity->GetSkeletonInstance();
			if (skeleton)
				skeleton->PlayAnimation(skeleton->FindAnimation(name), mode);
		}

		pugi::xml_node physics = entitynode.child("Physics");
		if (physics)
		{
			bool value = physics.attribute("value").as_bool();
			if (value)
				entity->CreateTestBody();
		}

		Light* light = dynamic_cast<Light*>(entity); // BAD!!!
		if (light)
			light->InitFromXML(entitynode);

		entity->Init();

		//IEntityBase* entity = g_entityManager->CreateEntity(classname.as_string());
		g_world->AddEntity(entity);
	}
}

IEntityBase* Game::Lua_CreateEntity(const char* classname)
{
	using namespace LuaPlus;

	SDL_assert_always(classname);

	// find prototype
	LuaPrototype* luaprototype = Lua_FindPrototype(classname);

	// create an entity
	Entity* entity = nullptr;
	if (luaprototype)
	{
		entity = static_cast<Entity*>(g_entityManager->CreateEntity(luaprototype->m_enginename.c_str()));

		// override classname because of entity system is so dumb
		entity->SetClassname(luaprototype->m_luaname.c_str());
	}
	else
	{
		entity = static_cast<Entity*>(g_entityManager->CreateEntity(classname));
	}

	SDL_assert_always(entity);

	// get the factrory
	LuaObject factory = GetLuaState().GetGlobal("g_factory");

	// and the lookup table
	LuaObject lookup = GetLuaState().GetGlobal("g_entities");

	// generate name
	char entityname[256];
	snprintf(entityname, sizeof(entityname), "%s_%d", classname, factory.GetTableCount());

	// create an table
	LuaObject entityTable = GetLuaState().CreateTable();
	entityTable.SetString("__private_name", entityname);
	entityTable.SetInteger("m_id", entity->GetID());

	// assign prototype
	if (luaprototype)
	{
		LuaObject prototype = GetLuaState().GetGlobal(luaprototype->m_luaname.c_str());
		SDL_assert_always(!prototype.IsNil());
		entityTable.SetMetatable(prototype);
	}
	
	// link to the entity
	entity->InitFromTable(entityTable);

	// push in to the factory
	factory.SetObject(entityname, entityTable);
	lookup.SetObject(entity->GetID(), entityTable);

	return entity;
}

LuaPrototype* Game::Lua_FindPrototype(const char* classname)
{
	// find a prototype
	for (std::vector<LuaPrototype>::iterator it = g_prototypes.begin();
		it != g_prototypes.end();
		++it)
	{
		if (strcmp((*it).m_luaname.c_str(), classname) == 0)
		{
			return &(*it);
		}
	}

	return nullptr;
}

IEntityBase* Game::FindEntityByName(const char* name)
{
	if (!g_world)
		return nullptr;

	int numEntities = g_world->GetNumEntities();
	for (int i = 0; i < numEntities; i++)
	{
		Entity* entity = dynamic_cast<Entity*>(g_world->GetEntity(i));
		if (entity && entity->GetName() == name)
			return entity;
	}

	return nullptr;
}

void Game::Shutdown()
{
}

void Game::Render2D()
{
	gameRenderUI();
}

//LuaPrototype* pluaprototype = Lua_FindPrototype(classname);
//
//if (pluaprototype)
//{
//	Entity* entity = static_cast<Entity*>(g_entityManager->CreateEntity(pluaprototype->m_enginename.c_str()));
//	SDL_assert_always(entity);

//	// get a prototype
//	LuaObject prototype = GetLuaState().GetGlobal(pluaprototype->m_luaname.c_str());
//	SDL_assert_always(!prototype.IsNil());
//	//prototype.SetObject("__index", prototype);

//	// generate table
//	LuaObject factory = GetLuaState().GetGlobal("g_factory");

//	// generate name
//	std::string entityname = pluaprototype->m_luaname + "_" + std::to_string(factory.GetTableCount());
//	
//	// create an table
//	LuaObject entityTable = GetLuaState().GetGlobals().CreateTable(entityname.c_str());
//	entityTable.SetMetatable(prototype);
//	entityTable.SetString("m_name", entityname.c_str());
//	
//	// push in to the factory
//	factory.SetObject(entityname.c_str(), entityTable);

//	// link to the entity
//	entity->InitFromTable(entityTable);

//	return entity;
//}

//IEntityBase* entity = g_entityManager->CreateEntity(classname);
