#include "precompiled.h"

#include "MapReader.h"
#include "lib/types.h"
#include "UnitManager.h"
#include "Unit.h"
#include "ps/Game.h"
#include "ObjectManager.h"
#include "simulation/Entity.h"
#include "simulation/BaseEntity.h"
#include "simulation/BaseEntityCollection.h"
#include "simulation/EntityManager.h"
#include "ps/CLogger.h"
#include "maths/MathUtil.h"
#include "Camera.h"
#include "graphics/Patch.h"
#include "renderer/WaterManager.h"
#include "renderer/SkyManager.h"

#include "Model.h"
#include "Terrain.h"
#include "TextureManager.h"
#include "TextureEntry.h"

#include "lib/timer.h"
#include "ps/Loader.h"
#include "ps/LoaderThunks.h"

#define LOG_CATEGORY "graphics"

CMapReader::CMapReader()
	: xml_reader(0)
{
	cur_terrain_tex = 0;	// important - resets generator state
}


// LoadMap: try to load the map from given file; reinitialise the scene to new data if successful
void CMapReader::LoadMap(const char* filename, CTerrain *pTerrain_,
						 CUnitManager *pUnitMan_, WaterManager* pWaterMan_, SkyManager* pSkyMan_,
						 CLightEnv *pLightEnv_, CCamera *pCamera_)
{
	// latch parameters (held until DelayedLoadFinished)
	pTerrain = pTerrain_;
	pUnitMan = pUnitMan_;
	pLightEnv = pLightEnv_;
	pCamera = pCamera_;
	pWaterMan = pWaterMan_;
	pSkyMan = pSkyMan_;

	// [25ms]
	unpacker.Read(filename, "PSMP");

	// check version
	if (unpacker.GetVersion() < FILE_READ_VERSION) {
		throw PSERROR_File_InvalidVersion();
	}

	// delete all existing entities
	g_EntityManager.deleteAll();
	// delete all remaining non-entity units
	pUnitMan->DeleteAll();
	g_UnitMan.SetNextID(0);

	// unpack the data
	RegMemFun(this, &CMapReader::UnpackMap, L"CMapReader::UnpackMap", 1200);

	if (unpacker.GetVersion() >= 3) {
		// read the corresponding XML file
		filename_xml = filename;
		filename_xml = filename_xml.Left(filename_xml.Length()-4) + ".xml";
		RegMemFun(this, &CMapReader::ReadXML, L"CMapReader::ReadXML", 5800);
	}

	// apply data to the world
	RegMemFun(this, &CMapReader::ApplyData, L"CMapReader::ApplyData", 5);

	RegMemFun(this, &CMapReader::DelayLoadFinished, L"CMapReader::DelayLoadFinished", 5);
}

// UnpackMap: unpack the given data from the raw data stream into local variables
int CMapReader::UnpackMap()
{
	// now unpack everything into local data
	int ret = UnpackTerrain();
	if(ret != 0)	// failed or timed out
		return ret;

	if (unpacker.GetVersion() < 4)
		UnpackObjects();

	if (unpacker.GetVersion() >= 2 && unpacker.GetVersion() < 4)
		UnpackLightEnv();

	return 0;
}

// UnpackLightEnv: unpack lighting parameters from input stream
void CMapReader::UnpackLightEnv()
{
	unpacker.UnpackRaw(&m_LightEnv.m_SunColor, sizeof(m_LightEnv.m_SunColor));
	unpacker.UnpackRaw(&m_LightEnv.m_Elevation, sizeof(m_LightEnv.m_Elevation));
	unpacker.UnpackRaw(&m_LightEnv.m_Rotation, sizeof(m_LightEnv.m_Rotation));
	unpacker.UnpackRaw(&m_LightEnv.m_TerrainAmbientColor, sizeof(m_LightEnv.m_TerrainAmbientColor));
	unpacker.UnpackRaw(&m_LightEnv.m_UnitsAmbientColor, sizeof(m_LightEnv.m_UnitsAmbientColor));
	m_LightEnv.CalculateSunDirection();
}

// UnpackObjects: unpack world objects from input stream
void CMapReader::UnpackObjects()
{
	// unpack object types
	u32 numObjTypes;
	unpacker.UnpackRaw(&numObjTypes, sizeof(numObjTypes));
	m_ObjectTypes.resize(numObjTypes);
	for (u32 i=0; i<numObjTypes; i++) {
		unpacker.UnpackString(m_ObjectTypes[i]);
	}

	// unpack object data
	u32 numObjects;
	unpacker.UnpackRaw(&numObjects, sizeof(numObjects));
	m_Objects.resize(numObjects);
	if (numObjects)
		unpacker.UnpackRaw(&m_Objects[0], sizeof(SObjectDesc)*numObjects);
}

// UnpackTerrain: unpack the terrain from the end of the input data stream
//		- data: map size, heightmap, list of textures used by map, texture tile assignments
int CMapReader::UnpackTerrain()
{
	// yield after this time is reached. balances increased progress bar
	// smoothness vs. slowing down loading.
	const double end_time = get_time() + 200e-3;

	// first call to generator (this is skipped after first call,
	// i.e. when the loop below was interrupted)
	if (cur_terrain_tex == 0)
	{
		// unpack map size
		unpacker.UnpackRaw(&m_MapSize, sizeof(m_MapSize));

		// unpack heightmap [600us]
		u32 verticesPerSide = m_MapSize*PATCH_SIZE+1;
		m_Heightmap.resize(SQR(verticesPerSide));
		unpacker.UnpackRaw(&m_Heightmap[0], SQR(verticesPerSide)*sizeof(u16));

		// unpack # textures
		unpacker.UnpackRaw(&num_terrain_tex, sizeof(num_terrain_tex));
		m_TerrainTextures.reserve(num_terrain_tex);
	}

	// unpack texture names; find handle for each texture.
	// interruptible.
	while (cur_terrain_tex < num_terrain_tex)
	{
		CStr texturename;
		unpacker.UnpackString(texturename);

		Handle handle;
		CTextureEntry* texentry = g_TexMan.FindTexture(texturename);
		// mismatch between texture datasets?
		if (!texentry)
			handle = 0;
		else
			handle = texentry->GetHandle();
		m_TerrainTextures.push_back(handle);

		cur_terrain_tex++;
		LDR_CHECK_TIMEOUT(cur_terrain_tex, num_terrain_tex);
	}

	// unpack tile data [3ms]
	u32 tilesPerSide = m_MapSize*PATCH_SIZE;
	m_Tiles.resize(SQR(tilesPerSide));
	unpacker.UnpackRaw(&m_Tiles[0], (u32)(sizeof(STileDesc)*m_Tiles.size()));

	// reset generator state.
	cur_terrain_tex = 0;

	return 0;
}

// ApplyData: take all the input data, and rebuild the scene from it
int CMapReader::ApplyData()
{
	// initialise the terrain
	pTerrain->Initialize(m_MapSize, &m_Heightmap[0]);
	
	// setup the textures on the minipatches
	STileDesc* tileptr = &m_Tiles[0];
	for (u32 j=0; j<m_MapSize; j++) {
		for (u32 i=0; i<m_MapSize; i++) {
			for (u32 m=0; m<(u32)PATCH_SIZE; m++) {
				for (u32 k=0; k<(u32)PATCH_SIZE; k++) {
					CMiniPatch& mp = pTerrain->GetPatch(i,j)->m_MiniPatches[m][k];

					mp.Tex1 = m_TerrainTextures[tileptr->m_Tex1Index];
					mp.Tex1Priority = tileptr->m_Priority;

					tileptr++;
				}
			}
		}
	}

	// add new objects
	for (size_t i = 0; i < m_Objects.size(); ++i)
	{

		if (unpacker.GetVersion() < 3)
		{
			debug_warn("Old unsupported map version - objects will be missing");
			// (getTemplateByActor doesn't work, since entity templates are now
			// loaded on demand)
		}

		std::set<CStrW> selections; // TODO: read from file
		CUnit* unit = g_UnitMan.CreateUnit(m_ObjectTypes.at(m_Objects[i].m_ObjectIndex), NULL, selections);

		if (unit)
		{
			CMatrix3D transform;
			memcpy2(&transform._11, m_Objects[i].m_Transform, sizeof(float)*16);
			unit->GetModel()->SetTransform(transform);
		}
	}
	//Make units start out conforming correctly
	g_EntityManager.conformAll();

	if (unpacker.GetVersion() >= 2)
	{
		// copy over the lighting parameters
		*pLightEnv = m_LightEnv;
	}
	return 0;
}



// Holds various state data while reading maps, so that loading can be
// interrupted (e.g. to update the progress display) then later resumed.
class CXMLReader
{
public:
	CXMLReader(const CStr& xml_filename, CMapReader& mapReader)
		: m_MapReader(mapReader)
	{
		Init(xml_filename);
	}

	// return semantics: see Loader.cpp!LoadFunc.
	int ProgressiveRead();

private:
	CXeromyces xmb_file;

	CMapReader& m_MapReader;

	int el_entity;
	int el_template, el_player;
	int el_position, el_orientation;
	int el_nonentity;
	int el_actor;
	int at_x, at_y, at_z;
	int at_angle;

	XMBElementList nodes; // children of root

	// loop counters
	int node_idx;
	int entity_idx, nonentity_idx;

	// # entities+nonentities processed and total (for progress calc)
	int completed_jobs, total_jobs;


	void Init(const CStr& xml_filename);

	void ReadEnvironment(XMBElement parent);
	void ReadCamera(XMBElement parent);
	int ReadEntities(XMBElement parent, double end_time);
	int ReadNonEntities(XMBElement parent, double end_time);

	// squelch "unable to generate" warnings
	CXMLReader(const CXMLReader& rhs);
	const CXMLReader& operator=(const CXMLReader& rhs);
};


void CXMLReader::Init(const CStr& xml_filename)
{
	// must only assign once, so do it here
	node_idx = entity_idx = nonentity_idx = 0;

	if (xmb_file.Load(xml_filename) != PSRETURN_OK)
		throw PSERROR_File_ReadFailed();

	// define the elements and attributes that are frequently used in the XML file,
	// so we don't need to do lots of string construction and comparison when
	// reading the data.
	// (Needs to be synchronised with the list in CXMLReader - ugh)
#define EL(x) el_##x = xmb_file.getElementID(#x)
#define AT(x) at_##x = xmb_file.getAttributeID(#x)
	EL(entity);
	EL(template);
	EL(player);
	EL(position);
	EL(orientation);
	EL(nonentity);
	EL(actor);
	AT(x); AT(y); AT(z);
	AT(angle);
#undef AT
#undef EL

	XMBElement root = xmb_file.getRoot();
	debug_assert(xmb_file.getElementString(root.getNodeName()) == "scenario");
	nodes = root.getChildNodes();

	// find out total number of entities+nonentities
	// (used when calculating progress)
	completed_jobs = 0;
	total_jobs = 0;
	for (int i = 0; i < nodes.Count; i++)
		total_jobs += nodes.item(i).getChildNodes().Count;
}


void CXMLReader::ReadEnvironment(XMBElement parent)
{
#define EL(x) int el_##x = xmb_file.getElementID(#x)
#define AT(x) int at_##x = xmb_file.getAttributeID(#x)
	EL(skyset);
	EL(suncolour);
	EL(sunelevation);
	EL(sunrotation);
	EL(terrainambientcolour);
	EL(unitsambientcolour);
	EL(terrainshadowtransparency);
	EL(water);
	EL(waterbody);
	EL(type);
	EL(colour);
	EL(height);
	EL(shininess);
	EL(waviness);
	AT(r); AT(g); AT(b);
#undef AT
#undef EL

	XERO_ITER_EL(parent, element)
	{
		int element_name = element.getNodeName();

		XMBAttributeList attrs = element.getAttributes();

		if (element_name == el_skyset)
		{
			m_MapReader.pSkyMan->SetSkySet(element.getText());
		}
		else if (element_name == el_suncolour)
		{
			m_MapReader.m_LightEnv.m_SunColor = RGBColor(
				CStr(attrs.getNamedItem(at_r)).ToFloat(),
				CStr(attrs.getNamedItem(at_g)).ToFloat(),
				CStr(attrs.getNamedItem(at_b)).ToFloat());
		}
		else if (element_name == el_sunelevation)
		{
			m_MapReader.m_LightEnv.m_Elevation = CStr(attrs.getNamedItem(at_angle)).ToFloat();
		}
		else if (element_name == el_sunrotation)
		{
			m_MapReader.m_LightEnv.m_Rotation = CStr(attrs.getNamedItem(at_angle)).ToFloat();
		}
		else if (element_name == el_terrainambientcolour)
		{
			m_MapReader.m_LightEnv.m_TerrainAmbientColor = RGBColor(
				CStr(attrs.getNamedItem(at_r)).ToFloat(),
				CStr(attrs.getNamedItem(at_g)).ToFloat(),
				CStr(attrs.getNamedItem(at_b)).ToFloat());
		}
		else if (element_name == el_unitsambientcolour)
		{
			m_MapReader.m_LightEnv.m_UnitsAmbientColor = RGBColor(
				CStr(attrs.getNamedItem(at_r)).ToFloat(),
				CStr(attrs.getNamedItem(at_g)).ToFloat(),
				CStr(attrs.getNamedItem(at_b)).ToFloat());
		}
		else if (element_name == el_terrainshadowtransparency)
		{
			m_MapReader.m_LightEnv.SetTerrainShadowTransparency(CStr(element.getText()).ToFloat());
		}
		else if (element_name == el_water)
		{
			XERO_ITER_EL(element, waterbody)
			{
				debug_assert(waterbody.getNodeName() == el_waterbody);
				XERO_ITER_EL(waterbody, waterelement)
				{
					int element_name = waterelement.getNodeName();
					if (element_name == el_type)
					{
						// TODO: implement this, when WaterManager supports it
					}
					else if (element_name == el_colour)
					{
						XMBAttributeList attrs = waterelement.getAttributes();
						m_MapReader.pWaterMan->m_WaterColor = CColor(
							CStr(attrs.getNamedItem(at_r)).ToFloat(),
							CStr(attrs.getNamedItem(at_g)).ToFloat(),
							CStr(attrs.getNamedItem(at_b)).ToFloat(),
							1.f);
					}
					else if (element_name == el_height)
					{
						m_MapReader.pWaterMan->m_WaterHeight = CStr(waterelement.getText()).ToFloat();
					}
					else if (element_name == el_shininess)
					{
						m_MapReader.pWaterMan->m_Shininess = CStr(waterelement.getText()).ToFloat();
					}
					else if (element_name == el_waviness)
					{
						m_MapReader.pWaterMan->m_Waviness = CStr(waterelement.getText()).ToFloat();
					}
					else
						debug_warn("Invalid map XML data");
				}

			}
		}
		else
			debug_warn("Invalid map XML data");
	}

	m_MapReader.m_LightEnv.CalculateSunDirection();
}

void CXMLReader::ReadCamera(XMBElement parent)
{
#define EL(x) int el_##x = xmb_file.getElementID(#x)
#define AT(x) int at_##x = xmb_file.getAttributeID(#x)
	EL(declination);
	EL(rotation);
	EL(position);
	AT(angle);
	AT(x); AT(y); AT(z);
#undef AT
#undef EL

	float declination = DEGTORAD(30.f), rotation = DEGTORAD(-45.f);
	CVector3D translation = CVector3D(100, 150, -100);

	XERO_ITER_EL(parent, element)
	{
		int element_name = element.getNodeName();

		XMBAttributeList attrs = element.getAttributes();
		if (element_name == el_declination)
		{
			declination = CStr(attrs.getNamedItem(at_angle)).ToFloat();
		}
		else if (element_name == el_rotation)
		{
			rotation = CStr(attrs.getNamedItem(at_angle)).ToFloat();
		}
		else if (element_name == el_position)
		{
			translation = CVector3D(
				CStr(attrs.getNamedItem(at_x)).ToFloat(),
				CStr(attrs.getNamedItem(at_y)).ToFloat(),
				CStr(attrs.getNamedItem(at_z)).ToFloat());
		}
		else
			debug_warn("Invalid map XML data");
	}

	m_MapReader.pCamera->m_Orientation.SetXRotation(declination);
	m_MapReader.pCamera->m_Orientation.RotateY(rotation);
	m_MapReader.pCamera->m_Orientation.Translate(translation);
	m_MapReader.pCamera->UpdateFrustum();
}


int CXMLReader::ReadEntities(XMBElement parent, double end_time)
{
	XMBElementList entities = parent.getChildNodes();
	while (entity_idx < entities.Count)
	{
		// all new state at this scope and below doesn't need to be
		// wrapped, since we only yield after a complete iteration.

		XMBElement entity = entities.item(entity_idx++);
		debug_assert(entity.getNodeName() == el_entity);

		CStrW TemplateName;
		int PlayerID = 0;
		CVector3D Position;
		float Orientation = 0.f;

		XERO_ITER_EL(entity, setting)
		{
			int element_name = setting.getNodeName();

			// <template>
			if (element_name == el_template)
			{
				TemplateName = setting.getText();
			}
			// <player>
			else if (element_name == el_player)
			{
				PlayerID = CStr(setting.getText()).ToInt();
			}
			// <position>
			else if (element_name == el_position)
			{
				XMBAttributeList attrs = setting.getAttributes();
				Position = CVector3D(
					CStr(attrs.getNamedItem(at_x)).ToFloat(),
					CStr(attrs.getNamedItem(at_y)).ToFloat(),
					CStr(attrs.getNamedItem(at_z)).ToFloat());
			}
			// <orientation>
			else if (element_name == el_orientation)
			{
				XMBAttributeList attrs = setting.getAttributes();
				Orientation = CStr(attrs.getNamedItem(at_angle)).ToFloat();
			}
			else
				debug_warn("Invalid map XML data");
		}

		CBaseEntity* base = g_EntityTemplateCollection.getTemplate( TemplateName, g_Game->GetPlayer(PlayerID) );
		if (! base)
			LOG(ERROR, LOG_CATEGORY, "Failed to load entity template '%ls'", TemplateName.c_str());
		else
		{
			std::set<CStrW> selections; // TODO: read from file

			HEntity ent = g_EntityManager.create(base, Position, Orientation, selections);

			if (! ent)
				LOG(ERROR, LOG_CATEGORY, "Failed to create entity of type '%ls'", TemplateName.c_str());
			else
			{
				ent->SetPlayer(g_Game->GetPlayer(PlayerID));

				// TODO: save object IDs in the map file, and load them again,
				// so that triggers have a persistent identifier for objects
				ent->m_actor->SetID(g_UnitMan.GetNewID());
			}
		}

		completed_jobs++;
		LDR_CHECK_TIMEOUT(completed_jobs, total_jobs);
	}

	return 0;
}


int CXMLReader::ReadNonEntities(XMBElement parent, double end_time)
{
	XMBElementList nonentities = parent.getChildNodes();
	while (nonentity_idx < nonentities.Count)
	{
		// all new state at this scope and below doesn't need to be
		// wrapped, since we only yield after a complete iteration.

		XMBElement nonentity = nonentities.item(nonentity_idx++);
		debug_assert(nonentity.getNodeName() == el_nonentity);

		CStr ActorName;
		CVector3D Position;
		float Orientation = 0.f;

		XERO_ITER_EL(nonentity, setting)
		{
			int element_name = setting.getNodeName();

			// <actor>
			if (element_name == el_actor)
			{
				ActorName = setting.getText();
			}
			// <position>
			else if (element_name == el_position)
			{
				XMBAttributeList attrs = setting.getAttributes();
				Position = CVector3D(CStr(attrs.getNamedItem(at_x)).ToFloat(),
					CStr(attrs.getNamedItem(at_y)).ToFloat(),
					CStr(attrs.getNamedItem(at_z)).ToFloat());
			}
			// <orientation>
			else if (element_name == el_orientation)
			{
				XMBAttributeList attrs = setting.getAttributes();
				Orientation = CStr(attrs.getNamedItem(at_angle)).ToFloat();
			}
			else
				debug_warn("Invalid map XML data");
		}

		std::set<CStrW> selections; // TODO: read from file

		CUnit* unit = g_UnitMan.CreateUnit(ActorName, NULL, selections);

		if (unit)
		{
			// Copied from CEntity::updateActorTransforms():
			float s = sin(Orientation);
			float c = cos(Orientation);
			CMatrix3D m;
			m._11 = -c;     m._12 = 0.0f;   m._13 = -s;     m._14 = Position.X;
			m._21 = 0.0f;   m._22 = 1.0f;   m._23 = 0.0f;   m._24 = Position.Y;
			m._31 = s;      m._32 = 0.0f;   m._33 = -c;     m._34 = Position.Z;
			m._41 = 0.0f;   m._42 = 0.0f;   m._43 = 0.0f;   m._44 = 1.0f;
			unit->GetModel()->SetTransform(m);

			// TODO: save object IDs in the map file, and load them again,
			// so that triggers have a persistent identifier for objects
			unit->SetID(g_UnitMan.GetNewID());
		}

		completed_jobs++;
		LDR_CHECK_TIMEOUT(completed_jobs, total_jobs);
	}

	return 0;
}


int CXMLReader::ProgressiveRead()
{
	// yield after this time is reached. balances increased progress bar
	// smoothness vs. slowing down loading.
	const double end_time = get_time() + 200e-3;

	int ret;

	while (node_idx < nodes.Count)
	{
		XMBElement node = nodes.item(node_idx);
		CStr name = xmb_file.getElementString(node.getNodeName());
		if (name == "environment")
		{
			ReadEnvironment(node);
		}
		else if (name == "camera")
		{
			ReadCamera(node);
		}
		else if (name == "entities")
		{
			ret = ReadEntities(node, end_time);
			if (ret != 0)	// error or timed out
				return ret;
		}
		else if (name == "nonentities")
		{
			ret = ReadNonEntities(node, end_time);
			if (ret != 0)	// error or timed out
				return ret;
		}
		else
			debug_warn("Invalid map XML data");

		node_idx++;
	}

	return 0;
}


// progressive
int CMapReader::ReadXML()
{
	if (!xml_reader)
		xml_reader = new CXMLReader(filename_xml, *this);

	int ret = xml_reader->ProgressiveRead();
	// finished or failed
	if (ret <= 0)
	{
		delete xml_reader;
		xml_reader = 0;
	}

	return ret;
}


int CMapReader::DelayLoadFinished()
{
	// we were dynamically allocated by CWorld::Initialize
	delete this;

	return 0;
}
