/*
   Copyright (C) 2001-2006, William Joseph.
   All Rights Reserved.

   This file is part of GtkRadiant.

   GtkRadiant 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.

   GtkRadiant 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 GtkRadiant; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "referencecache.h"
#include "globaldefs.h"

#include "debugging/debugging.h"

#include "iscenegraph.h"
#include "iselection.h"
#include "iundo.h"
#include "imap.h"

MapModules& ReferenceAPI_getMapModules();

#include "imodel.h"

ModelModules& ReferenceAPI_getModelModules();

#include "ifilesystem.h"
#include "iarchive.h"
#include "ifiletypes.h"
#include "ireference.h"
#include "ientity.h"
#include "qerplugin.h"

#include <list>

#include "container/cache.h"
#include "container/hashfunc.h"
#include "os/path.h"
#include "stream/textfilestream.h"
#include "nullmodel.h"
#include "maplib.h"
#include "stream/stringstream.h"
#include "os/file.h"
#include "moduleobserver.h"
#include "moduleobservers.h"

#include "mainframe.h"
#include "map.h"
#include "filetypes.h"

extern bool g_writeMapComments;

bool References_Saved();

void MapChanged(){
	Map_SetModified( g_map, !References_Saved() );
}


EntityCreator* g_entityCreator = 0;

bool MapResource_loadFile( const MapFormat& format, scene::Node& root, const char* filename ){
	globalOutputStream() << "Open file " << filename << " for read...";
	TextFileInputStream file( filename );
	if ( !file.failed() ) {
		globalOutputStream() << "success\n";
		ScopeDisableScreenUpdates disableScreenUpdates( path_get_filename_start( filename ), "Loading Map" );
		ASSERT_NOTNULL( g_entityCreator );
		format.readGraph( root, file, *g_entityCreator );
		return true;
	}
	else
	{
		globalErrorStream() << "failure\n";
		return false;
	}
}

NodeSmartReference MapResource_load( const MapFormat& format, const char* path, const char* name ){
	NodeSmartReference root( NewMapRoot( name ) );

	StringOutputStream fullpath( 256 );
	fullpath << path << name;

	if ( path_is_absolute( fullpath.c_str() ) ) {
		MapResource_loadFile( format, root, fullpath.c_str() );
	}
	else
	{
		globalErrorStream() << "map path is not fully qualified: " << makeQuoted( fullpath.c_str() ) << "\n";
	}

	return root;
}

bool MapResource_saveFile( const MapFormat& format, scene::Node& root, GraphTraversalFunc traverse, const char* filename ){
	//ASSERT_MESSAGE(path_is_absolute(filename), "MapResource_saveFile: path is not absolute: " << makeQuoted(filename));
	globalOutputStream() << "Open file " << filename << " for write...";
	TextFileOutputStream file( filename );
	if ( !file.failed() ) {
		globalOutputStream() << "success\n";
		ScopeDisableScreenUpdates disableScreenUpdates( path_get_filename_start( filename ), "Saving Map" );
		format.writeGraph( root, traverse, file, g_writeMapComments );
		return true;
	}

	globalErrorStream() << "failure\n";
	return false;
}

bool file_saveBackup( const char* path ){
	if ( file_writeable( path ) ) {
		StringOutputStream backup( 256 );
		backup << StringRange( path, path_get_extension( path ) ) << "bak";

#if GDEF_OS_WINDOWS
		// NT symlinks are not supported yet.
		return ( !file_exists( backup.c_str() ) || file_remove( backup.c_str() ) ) // remove backup
			   && file_move( path, backup.c_str() ); // rename current to backup
#else
		// POSIX symlinks are supported.
		return file_move( path, backup.c_str() ); // rename current to backup
#endif
	}

	globalErrorStream() << "map path is not writeable: " << makeQuoted( path ) << "\n";
	return false;
}

bool MapResource_save( const MapFormat& format, scene::Node& root, const char* path, const char* name ){
	StringOutputStream fullpath( 256 );
	fullpath << path << name;

	if ( path_is_absolute( fullpath.c_str() ) ) {
#if GDEF_OS_WINDOWS
		// NT symlinks are not supported yet.
		if ( !file_exists( fullpath.c_str() ) || file_saveBackup( fullpath.c_str() ) ) {
#else
		// POSIX symlinks are supported.
		/* We don't want a backup + rename operation if the .map file is
		 * a symlink. Otherwise we'll break the user's careful symlink setup.
		 * Just overwrite the original file. Assume the user has versioning. */
		bool make_backup;
		struct stat st;
		if ( lstat(fullpath.c_str(), &st) == 0 ) {
			make_backup = true;		// file exists
			if ( (st.st_mode & S_IFMT) == S_IFLNK ) {
				make_backup = false;  	// .. but it is a symlink
			}
		} else {
			make_backup = false;		// nothing to move
		}

		if ( !make_backup || file_saveBackup( fullpath.c_str() ) ) {
#endif
			return MapResource_saveFile( format, root, Map_Traverse, fullpath.c_str() );
		}

		globalErrorStream() << "failed to save a backup map file: " << makeQuoted( fullpath.c_str() ) << "\n";
		return false;
	}

	globalErrorStream() << "map path is not fully qualified: " << makeQuoted( fullpath.c_str() ) << "\n";
	return false;
}

namespace
{
NodeSmartReference g_nullNode( NewNullNode() );
NodeSmartReference g_nullModel( g_nullNode );
}

class NullModelLoader : public ModelLoader
{
public:
scene::Node& loadModel( ArchiveFile& file ){
	return g_nullModel;
}
};

namespace
{
NullModelLoader g_NullModelLoader;
}


/// \brief Returns the model loader for the model \p type or 0 if the model \p type has no loader module
ModelLoader* ModelLoader_forType( const char* type ){
	const char* moduleName = findModuleName( &GlobalFiletypes(), ModelLoader::Name(), type );
	if ( string_not_empty( moduleName ) ) {
		ModelLoader* table = ReferenceAPI_getModelModules().findModule( moduleName );
		if ( table != 0 ) {
			return table;
		}
		else
		{
			globalErrorStream() << "ERROR: Model type incorrectly registered: \"" << moduleName << "\"\n";
			return &g_NullModelLoader;
		}
	}
	return 0;
}

NodeSmartReference ModelResource_load( ModelLoader* loader, const char* name ){
	ScopeDisableScreenUpdates disableScreenUpdates( path_get_filename_start( name ), "Loading Model" );

	NodeSmartReference model( g_nullModel );

	{
		ArchiveFile* file = GlobalFileSystem().openFile( name );

		if ( file != 0 ) {
			globalOutputStream() << "Loaded Model: \"" << name << "\"\n";
			model = loader->loadModel( *file );
			file->release();
		}
		else
		{
			globalErrorStream() << "Model load failed: \"" << name << "\"\n";
		}
	}

	model.get().m_isRoot = true;

	return model;
}


inline hash_t path_hash( const char* path, hash_t previous = 0 ){
#if GDEF_OS_WINDOWS
	return string_hash_nocase( path, previous );
#else // UNIX
	return string_hash( path, previous );
#endif
}

struct PathEqual
{
	bool operator()( const CopiedString& path, const CopiedString& other ) const {
		return path_equal( path.c_str(), other.c_str() );
	}
};

struct PathHash
{
	typedef hash_t hash_type;

	hash_type operator()( const CopiedString& path ) const {
		return path_hash( path.c_str() );
	}
};

typedef std::pair<CopiedString, CopiedString> ModelKey;

struct ModelKeyEqual
{
	bool operator()( const ModelKey& key, const ModelKey& other ) const {
		return path_equal( key.first.c_str(), other.first.c_str() ) && path_equal( key.second.c_str(), other.second.c_str() );
	}
};

struct ModelKeyHash
{
	typedef hash_t hash_type;

	hash_type operator()( const ModelKey& key ) const {
		return hash_combine( path_hash( key.first.c_str() ), path_hash( key.second.c_str() ) );
	}
};

typedef HashTable<ModelKey, NodeSmartReference, ModelKeyHash, ModelKeyEqual> ModelCache;
ModelCache g_modelCache;
bool g_modelCache_enabled = true;

ModelCache::iterator ModelCache_find( const char* path, const char* name ){
	if ( g_modelCache_enabled ) {
		return g_modelCache.find( ModelKey( path, name ) );
	}
	return g_modelCache.end();
}

ModelCache::iterator ModelCache_insert( const char* path, const char* name, scene::Node& node ){
	if ( g_modelCache_enabled ) {
		return g_modelCache.insert( ModelKey( path, name ), NodeSmartReference( node ) );
	}
	return g_modelCache.insert( ModelKey( "", "" ), g_nullModel );
}

void ModelCache_flush( const char* path, const char* name ){
	ModelCache::iterator i = g_modelCache.find( ModelKey( path, name ) );
	if ( i != g_modelCache.end() ) {
		//ASSERT_MESSAGE((*i).value.getCount() == 0, "resource flushed while still in use: " << (*i).key.first.c_str() << (*i).key.second.c_str());
		g_modelCache.erase( i );
	}
}

void ModelCache_clear(){
	g_modelCache_enabled = false;
	g_modelCache.clear();
	g_modelCache_enabled = true;
}

NodeSmartReference Model_load( ModelLoader* loader, const char* path, const char* name, const char* type ){
	if ( loader != 0 ) {
		return ModelResource_load( loader, name );
	}
	else
	{
		const char* moduleName = findModuleName( &GlobalFiletypes(), MapFormat::Name(), type );
		if ( string_not_empty( moduleName ) ) {
			const MapFormat* format = ReferenceAPI_getMapModules().findModule( moduleName );
			if ( format != 0 ) {
				return MapResource_load( *format, path, name );
			}
			else
			{
				globalErrorStream() << "ERROR: Map type incorrectly registered: \"" << moduleName << "\"\n";
				return g_nullModel;
			}
		}
		else
		{
			if ( string_not_empty( type ) ) {
				globalErrorStream() << "Model type not supported: \"" << name << "\"\n";
			}
			return g_nullModel;
		}
	}
}

namespace
{
bool g_realised = false;

// name may be absolute or relative
const char* rootPath( const char* name ){
	return GlobalFileSystem().findRoot(
			   path_is_absolute( name )
			   ? name
			   : GlobalFileSystem().findFile( name )
			   );
}
}

struct ModelResource : public Resource
{
	NodeSmartReference m_model;
	const CopiedString m_originalName;
	CopiedString m_path;
	CopiedString m_name;
	CopiedString m_type;
	ModelLoader* m_loader;
	ModuleObservers m_observers;
	std::time_t m_modified;
	std::size_t m_unrealised;

	ModelResource( const CopiedString& name ) :
		m_model( g_nullModel ),
		m_originalName( name ),
		m_type( path_get_extension( name.c_str() ) ),
		m_loader( 0 ),
		m_modified( 0 ),
		m_unrealised( 1 ){
		m_loader = ModelLoader_forType( m_type.c_str() );

		if ( g_realised ) {
			realise();
		}
	}

	~ModelResource(){
		if ( realised() ) {
			unrealise();
		}
		ASSERT_MESSAGE( !realised(), "ModelResource::~ModelResource: resource reference still realised: " << makeQuoted( m_name.c_str() ) );
	}

	// NOT COPYABLE
	ModelResource( const ModelResource& );

	// NOT ASSIGNABLE
	ModelResource& operator=( const ModelResource& );

	void setModel( const NodeSmartReference& model ){
		m_model = model;
	}

	void clearModel(){
		m_model = g_nullModel;
	}

	void loadCached(){
		if ( g_modelCache_enabled ) {
			// cache lookup
			ModelCache::iterator i = ModelCache_find( m_path.c_str(), m_name.c_str() );
			if ( i == g_modelCache.end() ) {
				i = ModelCache_insert(
					m_path.c_str(),
					m_name.c_str(),
					Model_load( m_loader, m_path.c_str(), m_name.c_str(), m_type.c_str() )
					);
			}

			setModel( ( *i ).value );
		}
		else
		{
			setModel( Model_load( m_loader, m_path.c_str(), m_name.c_str(), m_type.c_str() ) );
		}
	}

	void loadModel(){
		loadCached();
		connectMap();
		mapSave();
	}

	bool load(){
		ASSERT_MESSAGE( realised(), "resource not realised" );
		if ( m_model == g_nullModel ) {
			loadModel();
		}

		return m_model != g_nullModel;
	}

	bool save(){
		if ( !mapSaved() ) {
			const char* moduleName = findModuleName( GetFileTypeRegistry(), MapFormat::Name(), m_type.c_str() );
			if ( string_not_empty( moduleName ) ) {
				const MapFormat* format = ReferenceAPI_getMapModules().findModule( moduleName );
				if ( format != 0 && MapResource_save( *format, m_model.get(), m_path.c_str(), m_name.c_str() ) ) {
					mapSave();
					return true;
				}
			}
		}
		return false;
	}

	void flush(){
		if ( realised() ) {
			ModelCache_flush( m_path.c_str(), m_name.c_str() );
		}
	}

	scene::Node* getNode(){
		//if(m_model != g_nullModel)
		{
			return m_model.get_pointer();
		}
		//return 0;
	}

	void setNode( scene::Node* node ){
		ModelCache::iterator i = ModelCache_find( m_path.c_str(), m_name.c_str() );
		if ( i != g_modelCache.end() ) {
			( *i ).value = NodeSmartReference( *node );
		}
		setModel( NodeSmartReference( *node ) );

		connectMap();
	}

	void attach( ModuleObserver& observer ){
		if ( realised() ) {
			observer.realise();
		}
		m_observers.attach( observer );
	}

	void detach( ModuleObserver& observer ){
		if ( realised() ) {
			observer.unrealise();
		}
		m_observers.detach( observer );
	}

	bool realised(){
		return m_unrealised == 0;
	}

	void realise(){
		ASSERT_MESSAGE( m_unrealised != 0, "ModelResource::realise: already realised" );
		if ( --m_unrealised == 0 ) {
			m_path = rootPath( m_originalName.c_str() );
			m_name = path_make_relative( m_originalName.c_str(), m_path.c_str() );

			//globalOutputStream() << "ModelResource::realise: " << m_path.c_str() << m_name.c_str() << "\n";

			m_observers.realise();
		}
	}

	void unrealise(){
		if ( ++m_unrealised == 1 ) {
			m_observers.unrealise();

			//globalOutputStream() << "ModelResource::unrealise: " << m_path.c_str() << m_name.c_str() << "\n";
			clearModel();
		}
	}

	bool isMap() const {
		return Node_getMapFile( m_model ) != 0;
	}

	void connectMap(){
		MapFile* map = Node_getMapFile( m_model );
		if ( map != 0 ) {
			map->setChangedCallback( makeCallbackF(MapChanged) );
		}
	}

	std::time_t modified() const {
		StringOutputStream fullpath( 256 );
		fullpath << m_path.c_str() << m_name.c_str();
		return file_modified( fullpath.c_str() );
	}

	void mapSave(){
		m_modified = modified();
		MapFile* map = Node_getMapFile( m_model );
		if ( map != 0 ) {
			map->save();
		}
	}

	bool mapSaved() const {
		MapFile* map = Node_getMapFile( m_model );
		if ( map != 0 ) {
			return m_modified == modified() && map->saved();
		}
		return true;
	}

	bool isModified() const {
		return ( ( !string_empty( m_path.c_str() ) // had or has an absolute path
				   && m_modified != modified() ) // AND disk timestamp changed
				 || !path_equal( rootPath( m_originalName.c_str() ), m_path.c_str() ) ); // OR absolute vfs-root changed
	}

	void refresh(){
		if ( isModified() ) {
			flush();
			unrealise();
			realise();
		}
	}
};

class HashtableReferenceCache : public ReferenceCache, public ModuleObserver
{
typedef HashedCache<CopiedString, ModelResource, PathHash, PathEqual> ModelReferences;
ModelReferences m_references;
std::size_t m_unrealised;

class ModelReferencesSnapshot
{
ModelReferences& m_references;
typedef std::list<ModelReferences::iterator> Iterators;
Iterators m_iterators;
public:
typedef Iterators::iterator iterator;

ModelReferencesSnapshot( ModelReferences& references ) : m_references( references ){
	for ( ModelReferences::iterator i = m_references.begin(); i != m_references.end(); ++i )
	{
		m_references.capture( i );
		m_iterators.push_back( i );
	}
}

~ModelReferencesSnapshot(){
	for ( Iterators::iterator i = m_iterators.begin(); i != m_iterators.end(); ++i )
	{
		m_references.release( *i );
	}
}

iterator begin(){
	return m_iterators.begin();
}

iterator end(){
	return m_iterators.end();
}
};

public:

typedef ModelReferences::iterator iterator;

HashtableReferenceCache() : m_unrealised( 1 ){
}

iterator begin(){
	return m_references.begin();
}

iterator end(){
	return m_references.end();
}

void clear(){
	m_references.clear();
}

Resource* capture( const char* path ){
	//globalOutputStream() << "capture: \"" << path << "\"\n";
	return m_references.capture( CopiedString( path ) ).get();
}

void release( const char* path ){
	m_references.release( CopiedString( path ) );
	//globalOutputStream() << "release: \"" << path << "\"\n";
}

void setEntityCreator( EntityCreator& entityCreator ){
	g_entityCreator = &entityCreator;
}

bool realised() const {
	return m_unrealised == 0;
}

void realise(){
	ASSERT_MESSAGE( m_unrealised != 0, "HashtableReferenceCache::realise: already realised" );
	if ( --m_unrealised == 0 ) {
		g_realised = true;

		{
			ModelReferencesSnapshot snapshot( m_references );
			for ( ModelReferencesSnapshot::iterator i = snapshot.begin(); i != snapshot.end(); ++i )
			{
				ModelReferences::value_type& value = *( *i );
				if ( value.value.count() != 1 ) {
					value.value.get()->realise();
				}
			}
		}
	}
}

void unrealise(){
	if ( ++m_unrealised == 1 ) {
		g_realised = false;

		{
			ModelReferencesSnapshot snapshot( m_references );
			for ( ModelReferencesSnapshot::iterator i = snapshot.begin(); i != snapshot.end(); ++i )
			{
				ModelReferences::value_type& value = *( *i );
				if ( value.value.count() != 1 ) {
					value.value.get()->unrealise();
				}
			}
		}

		ModelCache_clear();
	}
}

void refresh(){
	ModelReferencesSnapshot snapshot( m_references );
	for ( ModelReferencesSnapshot::iterator i = snapshot.begin(); i != snapshot.end(); ++i )
	{
		ModelResource* resource = ( *( *i ) ).value.get();
		if ( !resource->isMap() ) {
			resource->refresh();
		}
	}
}
};

namespace
{
HashtableReferenceCache g_referenceCache;
}

#if 0
class ResourceVisitor
{
public:
virtual void visit( const char* name, const char* path, const
					};
#endif

void SaveReferences(){
	ScopeDisableScreenUpdates disableScreenUpdates( "Processing...", "Saving Map" );
	for ( HashtableReferenceCache::iterator i = g_referenceCache.begin(); i != g_referenceCache.end(); ++i )
	{
		( *i ).value->save();
	}
	MapChanged();
}

bool References_Saved(){
	for ( HashtableReferenceCache::iterator i = g_referenceCache.begin(); i != g_referenceCache.end(); ++i )
	{
		scene::Node* node = ( *i ).value->getNode();
		if ( node != 0 ) {
			MapFile* map = Node_getMapFile( *node );
			if ( map != 0 && !map->saved() ) {
				return false;
			}
		}
	}
	return true;
}

void RefreshReferences(){
	ScopeDisableScreenUpdates disableScreenUpdates( "Processing...", "Refreshing Models" );
	g_referenceCache.refresh();
}


void FlushReferences(){
	ModelCache_clear();

	g_referenceCache.clear();
}

ReferenceCache& GetReferenceCache(){
	return g_referenceCache;
}


#include "modulesystem/modulesmap.h"
#include "modulesystem/singletonmodule.h"
#include "modulesystem/moduleregistry.h"

class ReferenceDependencies :
	public GlobalRadiantModuleRef,
	public GlobalFileSystemModuleRef,
	public GlobalFiletypesModuleRef
{
ModelModulesRef m_model_modules;
MapModulesRef m_map_modules;
public:
ReferenceDependencies() :
	m_model_modules( GlobalRadiant().getRequiredGameDescriptionKeyValue( "modeltypes" ) ),
	m_map_modules( GlobalRadiant().getRequiredGameDescriptionKeyValue( "maptypes" ) )
{
}

ModelModules& getModelModules(){
	return m_model_modules.get();
}

MapModules& getMapModules(){
	return m_map_modules.get();
}
};

class ReferenceAPI : public TypeSystemRef
{
ReferenceCache* m_reference;
public:
typedef ReferenceCache Type;

STRING_CONSTANT( Name, "*" );

ReferenceAPI(){
	g_nullModel = NewNullModel();

	GlobalFileSystem().attach( g_referenceCache );

	m_reference = &GetReferenceCache();
}

~ReferenceAPI(){
	GlobalFileSystem().detach( g_referenceCache );

	g_nullModel = g_nullNode;
}

ReferenceCache* getTable(){
	return m_reference;
}
};

typedef SingletonModule<ReferenceAPI, ReferenceDependencies> ReferenceModule;
typedef Static<ReferenceModule> StaticReferenceModule;
StaticRegisterModule staticRegisterReference( StaticReferenceModule::instance() );

ModelModules& ReferenceAPI_getModelModules(){
	return StaticReferenceModule::instance().getDependencies().getModelModules();
}

MapModules& ReferenceAPI_getMapModules(){
	return StaticReferenceModule::instance().getDependencies().getMapModules( );
}
