/* Copyright (C) 2015 Wildfire Games.
 * This file is part of 0 A.D.
 *
 * 0 A.D. 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 2 of the License, or
 * (at your option) any later version.
 *
 * 0 A.D. 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 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "precompiled.h"

#include "ObjectManager.h"

#include "graphics/ObjectBase.h"
#include "graphics/ObjectEntry.h"
#include "ps/CLogger.h"
#include "ps/Game.h"
#include "ps/Profile.h"
#include "ps/Filesystem.h"
#include "ps/XML/Xeromyces.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpTerrain.h"
#include "simulation2/components/ICmpVisual.h"


bool CObjectManager::ObjectKey::operator< (const CObjectManager::ObjectKey& a) const
{
	if (ActorName < a.ActorName)
		return true;
	else if (ActorName > a.ActorName)
		return false;
	else
		return ActorVariation < a.ActorVariation;
}

static Status ReloadChangedFileCB(void* param, const VfsPath& path)
{
	return static_cast<CObjectManager*>(param)->ReloadChangedFile(path);
}

CObjectManager::CObjectManager(CMeshManager& meshManager, CSkeletonAnimManager& skeletonAnimManager, CSimulation2& simulation)
: m_MeshManager(meshManager), m_SkeletonAnimManager(skeletonAnimManager), m_Simulation(simulation)
{
	RegisterFileReloadFunc(ReloadChangedFileCB, this);

	if (!CXeromyces::AddValidator(g_VFS, "actor", "art/actors/actor.rng"))
		LOGERROR("CObjectManager: failed to load actor grammar file 'art/actors/actor.rng'");
}

CObjectManager::~CObjectManager()
{
	UnloadObjects();

	UnregisterFileReloadFunc(ReloadChangedFileCB, this);
}


CObjectBase* CObjectManager::FindObjectBase(const CStrW& objectname)
{
	ENSURE(!objectname.empty());

	// See if the base type has been loaded yet:

	std::map<CStrW, CObjectBase*>::iterator it = m_ObjectBases.find(objectname);
	if (it != m_ObjectBases.end())
		return it->second;

	// Not already loaded, so try to load it:

	CObjectBase* obj = new CObjectBase(*this);

	VfsPath pathname = VfsPath("art/actors/") / objectname;

	if (obj->Load(pathname))
	{
		m_ObjectBases[objectname] = obj;
		return obj;
	}
	else
		delete obj;

	LOGERROR("CObjectManager::FindObjectBase(): Cannot find object '%s'", utf8_from_wstring(objectname));

	return 0;
}

CObjectEntry* CObjectManager::FindObject(const CStrW& objname)
{
	std::vector<std::set<CStr> > selections; // TODO - should this really be empty?
	return FindObjectVariation(objname, selections);
}

CObjectEntry* CObjectManager::FindObjectVariation(const CStrW& objname, const std::vector<std::set<CStr> >& selections)
{
	CObjectBase* base = FindObjectBase(objname);

	if (! base)
		return NULL;

	return FindObjectVariation(base, selections);
}

CObjectEntry* CObjectManager::FindObjectVariation(CObjectBase* base, const std::vector<std::set<CStr> >& selections)
{
	PROFILE("object variation loading");

	// Look to see whether this particular variation has already been loaded

	std::vector<u8> choices = base->CalculateVariationKey(selections);
	ObjectKey key (base->m_Pathname.string(), choices);

	std::map<ObjectKey, CObjectEntry*>::iterator it = m_Objects.find(key);
	if (it != m_Objects.end() && !it->second->m_Outdated)
		return it->second;

	// If it hasn't been loaded, load it now

	// TODO: If there was an existing ObjectEntry, but it's outdated (due to hotloading),
	// we'll get a memory leak when replacing its entry in m_Objects. The problem is
	// some CUnits might still have a pointer to the old ObjectEntry so we probably can't
	// just delete it now. Probably we need to redesign the caching/hotloading system so it
	// makes more sense (e.g. use shared_ptr); for now I'll just leak, to avoid making the logic
	// more complex than it is already is, since this only matters for the rare case of hotloading.

	CObjectEntry* obj = new CObjectEntry(base, m_Simulation); // TODO: type ?

	// TODO (for some efficiency): use the pre-calculated choices for this object,
	// which has already worked out what to do for props, instead of passing the
	// selections into BuildVariation and having it recalculate the props' choices.

	if (! obj->BuildVariation(selections, choices, *this))
	{
		DeleteObject(obj);
		return NULL;
	}

	m_Objects[key] = obj;

	return obj;
}

CTerrain* CObjectManager::GetTerrain()
{
	CmpPtr<ICmpTerrain> cmpTerrain(m_Simulation, SYSTEM_ENTITY);
	if (!cmpTerrain)
		return NULL;
	return cmpTerrain->GetCTerrain();
}

void CObjectManager::DeleteObject(CObjectEntry* entry)
{
	std::function<bool(const std::pair<ObjectKey, CObjectEntry*>&)> second_equals =
		[&entry](const std::pair<ObjectKey, CObjectEntry*>& a) { return a.second == entry; };

	std::map<ObjectKey, CObjectEntry*>::iterator it = m_Objects.begin();
	while (m_Objects.end() != (it = find_if(it, m_Objects.end(), second_equals)))
		it = m_Objects.erase(it);

	delete entry;
}


void CObjectManager::UnloadObjects()
{
	for (const std::pair<ObjectKey, CObjectEntry*>& p : m_Objects)
		delete p.second;
	m_Objects.clear();

	for (const std::pair<CStrW, CObjectBase*>& p : m_ObjectBases)
		delete p.second;
	m_ObjectBases.clear();
}

Status CObjectManager::ReloadChangedFile(const VfsPath& path)
{
	// Mark old entries as outdated so we don't reload them from the cache
	for (std::map<ObjectKey, CObjectEntry*>::iterator it = m_Objects.begin(); it != m_Objects.end(); ++it)
		if (it->second->m_Base->UsesFile(path))
			it->second->m_Outdated = true;

	// Reload actors that use a changed object
	for (std::map<CStrW, CObjectBase*>::iterator it = m_ObjectBases.begin(); it != m_ObjectBases.end(); ++it)
	{
		if (it->second->UsesFile(path))
		{
			it->second->Reload();

			// Slightly ugly hack: The graphics system doesn't preserve enough information to regenerate the
			// object with all correct variations, and we don't want to waste space storing it just for the
			// rare occurrence of hotloading, so we'll tell the component (which does preserve the information)
			// to do the reloading itself
			const CSimulation2::InterfaceListUnordered& cmps = m_Simulation.GetEntitiesWithInterfaceUnordered(IID_Visual);
			for (CSimulation2::InterfaceListUnordered::const_iterator eit = cmps.begin(); eit != cmps.end(); ++eit)
				static_cast<ICmpVisual*>(eit->second)->Hotload(it->first);
		}
	}

	return INFO::OK;
}
