// K-3D
// Copyright (c) 1995-2004, Timothy M. Shead
//
// Contact: tshead@k-3d.com
//
// 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 2 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, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

/** \file
		\brief Provides a concrete implementation of k3d::idocument, which encapsulates an open K-3D document
		\author Tim Shead (tshead@k-3d.com)
		\author Dan Erikson (derikson@montana.com)
*/

#include "document.h"

#include <k3dsdk/application.h>
#include <k3dsdk/classes.h>
#include <k3dsdk/command_node.h>
#include <k3dsdk/data.h>
#include <k3dsdk/dependencies.h>
#include <k3dsdk/file_filter.h>
#include <k3dsdk/icommand_tree.h>
#include <k3dsdk/idag.h>
#include <k3dsdk/ideletable.h>
#include <k3dsdk/idocument.h>
#include <k3dsdk/idocument_plugin_factory.h>
#include <k3dsdk/igeometry_read_format.h>
#include <k3dsdk/igeometry_write_format.h>
#include <k3dsdk/imaterial.h>
#include <k3dsdk/imouse_event_observer.h>
#include <k3dsdk/iobject_collection.h>
#include <k3dsdk/iplugin_factory.h>
#include <k3dsdk/iselectable.h>
#include <k3dsdk/iselection.h>
#include <k3dsdk/iobject.h>
#include <k3dsdk/path_data.h>
#include <k3dsdk/persistence.h>
#include <k3dsdk/plugins.h>
#include <k3dsdk/property_collection.h>
#include <k3dsdk/serialization.h>
#include <k3dsdk/string_cast.h>
#include <k3dsdk/string_modifiers.h>
#include <k3dsdk/user_interface.h>
#include <k3dsdk/utility.h>
#include <k3dsdk/viewport.h>
#include <k3dsdk/xml_utility.h>

#include <sdpxml/sdpxml.h>
#include <sigc++/bind.h>

#include <boost/filesystem/fstream.hpp>
#include <boost/filesystem/path.hpp>

#include <fstream>
#include <iterator>
#include <memory>

namespace
{

/////////////////////////////////////////////////////////////////////////////
// next_document_number

/// Returns unique document numbers for the duration of the current session
unsigned long next_document_number()
{
	static unsigned long document_number = 0;
	return ++document_number;
}

/////////////////////////////////////////////////////////////////////////////
// save_object

/// Serializes K-3D objects
class save_object
{
public:
	save_object(sdpxml::Element& Element, k3d::idependencies& Dependencies) :
		m_Element(Element),
		m_Dependencies(Dependencies),
		m_Count(0)
	{
	}

	void operator()(k3d::iobject* const Object)
	{
		// Save the object if it supports persistence ...
		k3d::ipersistent* const persist = dynamic_cast<k3d::ipersistent*>(Object);
		if(!persist)
			return;

		// Create an XML element for the object ...
		sdpxml::Element& objectelement = m_Element.Append(sdpxml::Element("object", "",
			sdpxml::Attribute("name", Object->name()),
			sdpxml::Attribute("class", sdpToString(Object->factory().class_id())),
			sdpxml::Attribute("id", sdpToString(Object->id()))));
		persist->save(objectelement, m_Dependencies);

		m_Count++;
	}

private:
	sdpxml::Element& m_Element;
	k3d::idependencies& m_Dependencies;
	unsigned long m_Count;
};

/////////////////////////////////////////////////////////////////////////////
// state_recorder_implementation

/// Provides a concrete implementation of k3d::istate_recorder
class state_recorder_implementation :
	public k3d::istate_recorder
{
public:
	state_recorder_implementation() :
		m_redo_position(m_change_sets.end()),
		m_saved_position(m_change_sets.end())
	{
	}

	~state_recorder_implementation()
	{
		// Delete all our change sets ...
		std::for_each(m_change_sets.begin(), m_change_sets.end(), k3d::delete_object());

		// Sanity checks ...
		return_if_fail(!m_current_recording.get());
	}

	void start_recording(std::auto_ptr<k3d::istate_change_set> ChangeSet)
	{
		// Sanity checks ...
		return_if_fail(ChangeSet.get());
		return_if_fail(!m_current_recording.get());
		m_current_recording = ChangeSet;
	}

	std::auto_ptr<k3d::istate_change_set> stop_recording()
	{
		// Sanity checks ...
		return_val_if_fail(m_current_recording.get(), m_current_recording);

		// Let the world know that recording is stopping ...
		m_current_recording->recording_done_signal().emit();

		return m_current_recording;
	}

	void commit_change_set(std::auto_ptr<k3d::istate_change_set> ChangeSet, const std::string& Label)
	{
		// Sanity checks ...
		return_if_fail(ChangeSet.get());
		return_if_fail(Label.size());

		// Recording a state change creates a "branch" in the state change tree, so trim any data in the old branch ...
		for(change_set_iterator victim = m_redo_position; victim != m_change_sets.end(); victim++)
			{
				if(m_saved_position == victim)
					{
						m_saved_position = m_change_sets.end();
						break;
					}
			}

		std::for_each(m_redo_position, m_change_sets.end(), k3d::delete_object());
		m_change_sets.erase(m_redo_position, m_change_sets.end());

		// Store the new change set ...
		m_change_sets.push_back(new change_set_record(ChangeSet.release(), Label));
		m_redo_position = m_change_sets.end();

		// Notify observers that our contents have been modified ...
		m_stack_changed_signal.emit();
	}

	k3d::istate_change_set* current_change_set()
	{
		return m_current_recording.get();
	}

	void mark_saved()
	{
		if(m_redo_position != m_change_sets.begin())
			m_saved_position = --change_set_iterator(m_redo_position);

		m_mark_saved_signal.emit();
	}

	bool unsaved_changes()
	{
		// Either we haven't changed anything or we undid all our changes
		if(m_redo_position == m_change_sets.begin())
			return m_saved_position != m_change_sets.end();

		// See if the current state and the saved state match ...
		return m_saved_position != --change_set_iterator(m_redo_position);
	}

	unsigned long undo_count()
	{
		return std::distance(m_change_sets.begin(), m_redo_position);
	}

	unsigned long redo_count()
	{
		return std::distance(m_redo_position, m_change_sets.end());
	}

	const std::string next_undo_label()
	{
		return m_redo_position != m_change_sets.begin() ? (*(--change_set_iterator(m_redo_position)))->label : std::string();
	}

	const std::string next_redo_label()
	{
		return m_redo_position != m_change_sets.end() ? (*m_redo_position)->label : std::string();
	}

	void undo()
	{
		// Should never happen in the middle of recording a state change set ...
		return_if_fail(!m_current_recording.get());

		// Make sure we have actions to undo ...
		return_if_fail(m_redo_position != m_change_sets.begin());

		// Undo the most recent change set
		m_redo_position--;
		(*m_redo_position)->change_set->undo();

		// Let the gentry know ...
		m_stack_changed_signal.emit();
	}

	void redo()
	{
		// Should never happen in the middle of recording a state change set ...
		return_if_fail(!m_current_recording.get());

		// Make sure we have actions to redo ...
		return_if_fail(m_redo_position != m_change_sets.end());

		// Redo the most recently undone change set ...
		(*m_redo_position)->change_set->redo();
		m_redo_position++;

		// Let the gentry know ...
		m_stack_changed_signal.emit();
	}

	void visit_change_sets(ichange_set_visitor& Visitor)
	{
		for(change_set_iterator undoset = m_change_sets.begin(); undoset != m_redo_position; undoset++)
			Visitor.visit_undo_change_set((*undoset)->label, undoset == m_saved_position);

		for(change_set_iterator redoset = m_redo_position; redoset != m_change_sets.end(); redoset++)
			Visitor.visit_redo_change_set((*redoset)->label, redoset == m_saved_position);
	}

	k3d::istate_recorder::stack_changed_signal_t& stack_changed_signal()
	{
		return m_stack_changed_signal;
	}

	k3d::istate_recorder::mark_saved_signal_t& mark_saved_signal()
	{
		return m_mark_saved_signal;
	}

private:
	/// Associates a name with a change set
	class change_set_record
	{
	public:
		change_set_record(k3d::istate_change_set* const ChangeSet, const std::string& Label) :
			change_set(ChangeSet),
			label(Label)
		{
			// Sanity checks ...
			assert_warning(change_set);
			assert_warning(label.size());
		}
		
		~change_set_record()
		{
			delete change_set;
		}

		k3d::istate_change_set* const change_set;
		const std::string label;
	};

	/// Stores a collection of change sets
	typedef std::list<change_set_record*> change_sets_t;
	typedef change_sets_t::iterator change_set_iterator;
	/// Stores change sets
	change_sets_t m_change_sets;

	/// Stores the current change set
	std::auto_ptr<k3d::istate_change_set> m_current_recording;
	/// Stores an iterator that partitions the m_change_sets collection into undo-able and redo-able change sets
	change_set_iterator m_redo_position;
	/// Stores an iterator that points to the last saved change set
	change_set_iterator m_saved_position;

	stack_changed_signal_t m_stack_changed_signal;
	mark_saved_signal_t m_mark_saved_signal;
};

/////////////////////////////////////////////////////////////////////////////
// object_collection_implementation

class object_collection_implementation :
	public k3d::iobject_collection
{
public:
	object_collection_implementation(k3d::istate_recorder& StateRecorder) :
		m_state_recorder(StateRecorder),
		m_largest_id(0)
	{
	}

	~object_collection_implementation()
	{
	}

	void add_objects(const objects_t& Objects)
	{
		// Ensure no NULLs creep in ...
		objects_t objects(Objects);
		if(objects.erase(static_cast<k3d::iobject*>(0)))
			std::cerr << warning << "NULL object cannot be inserted into object collection and will be ignored" << std::endl;

		// Keep track of the largest ID seen so far ...
		for(objects_t::const_iterator object = objects.begin(); object != objects.end(); ++object)
			m_largest_id = std::max(m_largest_id, (*object)->id());

		// We want to emit a signal whenever an object's name changes ...
		for(objects_t::const_iterator object = objects.begin(); object != objects.end(); ++object)
			(*object)->name_changed_signal().connect(SigC::bind(m_rename_object_signal.slot(), *object));

		// If we're recording undo/redo data, record the new state ...
		if(m_state_recorder.current_change_set())
			{
				m_state_recorder.current_change_set()->record_old_state(new remove_objects_container(*this, objects));
				m_state_recorder.current_change_set()->record_new_state(new add_objects_container(*this, objects));
			}

		// Make the change and notify observers ...
		m_objects.insert(objects.begin(), objects.end());
		m_add_objects_signal.emit(objects);
	}

	const iobject_collection::objects_t& collection()
	{
		return m_objects;
	}

	void remove_objects(const objects_t& Objects)
	{
		// Ensure no NULLs creep in ...
		objects_t objects(Objects);
		if(objects.erase(static_cast<k3d::iobject*>(0)))
			std::cerr << warning << "NULL object will be ignored" << std::endl;

		// If we're recording undo/redo data, record the new state ...
		if(m_state_recorder.current_change_set())
			{
				m_state_recorder.current_change_set()->record_old_state(new add_objects_container(*this, objects));
				m_state_recorder.current_change_set()->record_new_state(new remove_objects_container(*this, objects));
			}

		// Make the change and notify observers ...
		for(objects_t::const_iterator object = objects.begin(); object != objects.end(); ++object)
			{
				(*object)->deleted_signal().emit();
				m_objects.erase(*object);
			}
		m_remove_objects_signal.emit(objects);

	}

	k3d::iobject::id_type next_available_id()
	{
		return ++m_largest_id;
	}

	add_objects_signal_t& add_objects_signal()
	{
		return m_add_objects_signal;
	}

	remove_objects_signal_t& remove_objects_signal()
	{
		return m_remove_objects_signal;
	}

	rename_object_signal_t& rename_object_signal()
	{
		return m_rename_object_signal;
	}

	void on_close_document()
	{
		// Give objects a chance to shut down ...
		for(k3d::iobject_collection::objects_t::iterator object = m_objects.begin(); object != m_objects.end(); ++object)
			{
				(*object)->deleted_signal().emit();
			}

		// Zap objects ...
		for(k3d::iobject_collection::objects_t::iterator object = m_objects.begin(); object != m_objects.end(); ++object)
			delete dynamic_cast<k3d::ideletable*>(*object);
	}

private:
	class add_objects_container :
		public k3d::istate_container
	{
	public:
		add_objects_container(k3d::iobject_collection& Collection, const k3d::iobject_collection::objects_t& Objects) :
			m_collection(Collection),
			m_objects(Objects)
		{
		}

		~add_objects_container()
		{
		}

		void restore_state()
		{
			m_collection.add_objects(m_objects);
		}

	private:
		k3d::iobject_collection& m_collection;
		const k3d::iobject_collection::objects_t m_objects;
	};

	class remove_objects_container :
		public k3d::istate_container
	{
	public:
		remove_objects_container(k3d::iobject_collection& Collection, const k3d::iobject_collection::objects_t& Objects) :
			m_collection(Collection),
			m_objects(Objects)
		{
		}

		~remove_objects_container()
		{
		}

		void restore_state()
		{
			m_collection.remove_objects(m_objects);
		}

	private:
		k3d::iobject_collection& m_collection;
		const k3d::iobject_collection::objects_t m_objects;
	};

	/// Provides storage for undo/redo information
	k3d::istate_recorder& m_state_recorder;
	/// Provides undo-able storage for a collection of objects
	k3d::iobject_collection::objects_t m_objects;
	/// Signal for notifying observers when objects are added to the collection
	add_objects_signal_t m_add_objects_signal;
	/// Signal for notifying observers when objects are removed from the collection
	remove_objects_signal_t m_remove_objects_signal;
	/// Signal for notifying observers when an object's name is changed
	rename_object_signal_t m_rename_object_signal;
	/// Caches the largest object ID encountered so-far, so we can generate unique IDs quickly
	k3d::iobject::id_type m_largest_id;
};

/////////////////////////////////////////////////////////////////////////////
// dag_implementation

class dag_implementation :
	public k3d::idag,
	public SigC::Object
{
public:
	dag_implementation(k3d::istate_recorder& StateRecorder) :
		m_state_recorder(StateRecorder)
	{
	}

	~dag_implementation()
	{
	}

	void set_dependencies(dependencies_t& Dependencies)
	{
		// Don't let any NULLs creep in ...
		if(Dependencies.erase(static_cast<k3d::iproperty*>(0)))
			std::cerr << warning << "Cannot assign a dependency to a NULL property" << std::endl;

		// Ensure there aren't any circular referances
		/** \todo Make this more robust */
		for(dependencies_t::iterator dependency = Dependencies.begin(); dependency != Dependencies.end(); ++dependency)
			{
				if(dependency->first == dependency->second)
					dependency->second = 0;
			}

		// If we're recording undo/redo data, record the new state ...
		if(m_state_recorder.current_change_set())
			m_state_recorder.current_change_set()->record_new_state(new set_dependencies_container(*this, Dependencies));

		// Update our internal graph, keep track of the original state as we go ...
		dependencies_t old_dependencies;
		for(dependencies_t::iterator dependency = Dependencies.begin(); dependency != Dependencies.end(); ++dependency)
			{
				dependencies_t::iterator old_dependency = get_dependency(dependency->first);
				old_dependencies.insert(*old_dependency);
					
				old_dependency->second = dependency->second;
				
				m_change_connections[dependency->first].disconnect();
				if(dependency->second)
					m_change_connections[dependency->first] = dependency->second->changed_signal().connect(dependency->first->changed_signal().slot());
			}

		// If we're recording undo/redo data, keep track of the original state ...
		if(m_state_recorder.current_change_set())
			m_state_recorder.current_change_set()->record_old_state(new set_dependencies_container(*this, old_dependencies));

		// Notify observers that the DAG as a whole has changed ...
		m_dependency_signal.emit(Dependencies);

		// Synthesize change notifications for every property whose parent was set ...
		for(dependencies_t::iterator dependency = Dependencies.begin(); dependency != Dependencies.end(); ++dependency)
			dependency->first->changed_signal().emit();
	}

	k3d::iproperty* dependency(k3d::iproperty& Target)
	{
		return get_dependency(&Target)->second;
	}

	const dependencies_t& dependencies()
	{
		return m_dependencies;
	}

	dependency_signal_t& dependency_signal()
	{
		return m_dependency_signal;
	}

	void on_close_document()
	{
		// Since the document is closing anyway, close all our connections to avoid pointless thrashing-around ...
		for(connections_t::iterator connection = m_change_connections.begin(); connection != m_change_connections.end(); ++connection)
			connection->second.disconnect();
			
		for(connections_t::iterator connection = m_delete_connections.begin(); connection != m_delete_connections.end(); ++connection)
			connection->second.disconnect();
	}

	void on_property_deleted(k3d::iproperty* Property)
	{
		dependencies_t::iterator dependency = m_dependencies.find(Property);
		return_if_fail(dependency != m_dependencies.end());
		
		if(m_state_recorder.current_change_set())
			{
				dependencies_t old_dependencies;
				old_dependencies.insert(*dependency);
				m_state_recorder.current_change_set()->record_old_state(new set_dependencies_container(*this, old_dependencies));
				m_state_recorder.current_change_set()->record_new_state(new delete_property_container(*this, Property));
			}

		m_dependencies.erase(dependency);
		
		m_delete_connections[Property].disconnect();
		m_delete_connections.erase(Property);

		dependencies_t new_dependencies;		
		for(dependencies_t::iterator dependency = m_dependencies.begin(); dependency != m_dependencies.end(); ++dependency)
			{
				if(dependency->second == Property)
					new_dependencies.insert(std::make_pair(dependency->first, static_cast<k3d::iproperty*>(0)));
			}

		if(new_dependencies.size())
			set_dependencies(new_dependencies);			
	}

private:
	dependencies_t::iterator get_dependency(k3d::iproperty* Property)
	{
		assert(Property);

		dependencies_t::iterator result = m_dependencies.find(Property);
		if(result == m_dependencies.end())
			{
				result = m_dependencies.insert(std::make_pair(Property, static_cast<k3d::iproperty*>(0))).first;
				m_delete_connections[Property] = Property->deleted_signal().connect(SigC::bind(SigC::slot(*this, &dag_implementation::on_property_deleted), Property));
			}

		return result;
	}

	class set_dependencies_container :
		public k3d::istate_container
	{
	public:
		set_dependencies_container(k3d::idag& Dag, const k3d::idag::dependencies_t& Dependencies) :
			m_dag(Dag),
			m_dependencies(Dependencies)
		{
		}
		
		~set_dependencies_container()
		{
		}
		
		void restore_state()
		{
			m_dag.set_dependencies(m_dependencies);
		}
	
	private:
		k3d::idag& m_dag;
		k3d::idag::dependencies_t m_dependencies;
	};
	
	class delete_property_container :
		public k3d::istate_container
	{
	public:
		delete_property_container(dag_implementation& Dag, k3d::iproperty* const Property) :
			m_dag(Dag),
			m_property(Property)
		{
		}
		
		~delete_property_container()
		{
		}
		
		void restore_state()
		{
			m_dag.on_property_deleted(m_property);
		}
	
	private:
		dag_implementation& m_dag;
		k3d::iproperty* const m_property;
	};

	k3d::istate_recorder& m_state_recorder;
	
	/// Stores inter-property dependencies
	dependencies_t m_dependencies;

	/// Defines storage for per-property signal connections	
	typedef std::map<k3d::iproperty*, SigC::Connection> connections_t;
	/// Stores connections between property change signals (so a change to a source property is automatically passed-along to its dependent properties)
	connections_t m_change_connections;
	/// Stores connections to property delete signals (so we can clean-up when a property goes away)
	connections_t m_delete_connections;
	/// Signal that is emitted anytime the dependency graph is modified
	dependency_signal_t m_dependency_signal;
};

/////////////////////////////////////////////////////////////////////////////
// selection_implementation

/// Provides an implementation of k3d::iselection
class selection_implementation :
	public k3d::iselection
{
public:
	selected_signal_t& selected_signal()
	{
		return m_selected_signal;
	}

	deselected_signal_t& deselected_signal()
	{
		return m_deselected_signal;
	}

	deselect_all_signal_t& deselect_all_signal()
	{
		return m_deselect_all_signal;
	}

private:
	selected_signal_t m_selected_signal;
	deselected_signal_t m_deselected_signal;
	deselect_all_signal_t m_deselect_all_signal;
};

/////////////////////////////////////////////////////////////////////////////
// public_document_implementation

/// Encapsulates an open K-3D document
class public_document_implementation :
	public k3d::idocument,
	public k3d::command_node,
	public k3d::property_collection,
	public SigC::Object
{
public:
	public_document_implementation(k3d::istate_recorder& StateRecorder, k3d::iobject_collection& Objects, k3d::idag& Dag) :
		k3d::command_node("document"),
		property_collection(Dag),
		m_state_recorder(StateRecorder),
		m_objects(Objects),
		m_dag(Dag),
		m_path(k3d::init_name("path") + k3d::init_description("Document path [string]") + k3d::init_value(boost::filesystem::path()) + k3d::init_document(*this)),
		m_title(k3d::init_name("title") + k3d::init_description("Document title [string]") + k3d::init_value<std::string>("") + k3d::init_document(*this)),
		m_mouse_focus(k3d::init_value<k3d::imouse_event_observer*>(0))
	{
		// We want to be asked before closing the application
		k3d::application().safe_to_close_signal().connect(SigC::slot(*this, &public_document_implementation::safe_to_close_application));

		// We want to be notified as other documents come-and-go ...
		k3d::application().pre_create_document_signal().connect(SigC::slot(*this, &public_document_implementation::on_pre_create_document));
		k3d::application().close_document_signal().connect(SigC::slot(*this, &public_document_implementation::on_close_document));

		// Add ourselves to the command tree ...
		k3d::icommand_node* const parent = dynamic_cast<k3d::icommand_node*>(&k3d::application());
		return_if_fail(parent);
		k3d::application().command_tree().add_node(*this, *parent);

		// Register some properties ...
		register_property(m_path);
		register_property(m_title);

		k3d::application().new_document_signal().emit(*this);
	}

	~public_document_implementation()
	{
		// Note: our close signal gets called by our owner - yes, it's a hack

		// Remove ourselves from the command tree ...
		k3d::application().command_tree().remove_node(*this);
	}

	// Create a new, default document from scratch ...
	bool create()
	{
		// Set our default title ...
		m_title.set_value("Untitled Document " + sdpToString(next_document_number()));

		k3d::viewport::redraw_all(*this, k3d::iviewport::ASYNCHRONOUS);

		return true;
	}

	// Open a document from disk ...
	bool open(const boost::filesystem::path& DocumentFile)
	{
		// Record our path & title ...
		m_path.set_value(DocumentFile);
		m_title.set_value(DocumentFile.leaf());

		// Load the document ...
		return_val_if_fail(load(DocumentFile), false);

		// Force a display update ...
		k3d::viewport::redraw_all(*this, k3d::iviewport::ASYNCHRONOUS);

		return true;
	}

	/////////////////////////////////////////////////////////////////////////////
	// k3d::icommand_node implementation

	bool execute_command(const std::string& Command, const std::string& Arguments)
	{
		return k3d::command_node::execute_command(Command, Arguments);
	}

	bool safe_to_close_application()
	{
		return m_safe_to_close_signal.emit();
	}

	void on_pre_create_document()
	{
		// This little bit of trickery ensures that documents always have a unique command node name ...
		set_command_node_name(command_node_name() + "*");
	}

	void on_close_document(k3d::idocument& Document)
	{
		// Don't do anything if it's us ...
		if(this == &Document)
			return;

		// This little bit of trickery ensures that documents always have a unique command node name ...
		std::string name = command_node_name();
		if(name[name.size()-1] == '*')
			set_command_node_name(name.substr(0, name.size() - 1));
	}


	k3d::iobject_collection& objects()
	{
		return m_objects;
	}

	k3d::idag& dag()
	{
		return m_dag;
	}

	k3d::iselection& selection()
	{
		return m_selection;
	}

	k3d::istate_recorder& state_recorder()
	{
		return m_state_recorder;
	}

	const boost::filesystem::path path()
	{
		return m_path.value();
	}

	const std::string title()
	{
		return m_title.value();
	}

	struct sort_by_id
	{
		bool operator()(k3d::iobject* LHS, k3d::iobject* RHS)
		{
			return LHS->id() < RHS->id();
		}
	};

	bool save(const boost::filesystem::path& File)
	{
		// Try to open the file ...
		boost::filesystem::ofstream filestream(File);
		return_val_if_fail(filestream.good(), false);

		// Record our new path & title ...
		m_path.set_value(File);
		m_title.set_value(File.leaf());

		// Create our output document and dependencies objects ...
		sdpxml::Document xml("k3dml");
		sdpxml::SetAttribute(xml, sdpxml::Attribute("version", VERSION));
		k3d::dependencies dependencies;

		// The document itself ...
		sdpxml::Element& xml_document = xml.Append(sdpxml::Element("document"));

		// Save objects ...
		sdpxml::Element& xml_objects = xml_document.Append(sdpxml::Element("objects"));

		// Sort objects by ID before we save ...
		std::vector<k3d::iobject*> objects(m_objects.collection().begin(), m_objects.collection().end());
		std::sort(objects.begin(), objects.end(), sort_by_id());
		
		for(std::vector<k3d::iobject*>::iterator object = objects.begin(); object != objects.end(); ++object)
			{
				// Save the object if it supports persistence ...
				k3d::ipersistent* const persist = dynamic_cast<k3d::ipersistent*>(*object);
				if(!persist)
					continue;

				// Create an XML element for the object ...
				sdpxml::Element& xml_object = xml_objects.Append(sdpxml::Element("object", "",
					sdpxml::Attribute("name", (*object)->name()),
					sdpxml::Attribute("class", sdpToString((*object)->factory().class_id())),
					sdpxml::Attribute("id", sdpToString((*object)->id()))));
				persist->save(xml_object, dependencies);
			}

		// Save the DAG ...
		k3d::save_dag(*this, xml_document);

		// Save the XML ...
		filestream << xml << std::endl;
		return_val_if_fail(filestream.good(), false);

		// Record save position in the undo/redo stack
		state_recorder().mark_saved();

		return true;
	}

	void set_mouse_focus(k3d::imouse_event_observer* const Observer)
	{
		// Note - a NULL focus is allowed ...
		m_mouse_focus.set_value(Observer);
	}

	k3d::imouse_event_observer* mouse_focus()
	{
		return m_mouse_focus.value();
	}

	k3d::idocument::safe_to_close_signal_t& safe_to_close_signal()
	{
		return m_safe_to_close_signal;
	}

	k3d::idocument::close_signal_t& close_signal()
	{
		return m_close_signal;
	}

	k3d::idocument::title_signal_t& title_signal()
	{
		return m_title.changed_signal();
	}

	k3d::idocument::mouse_focus_signal_t& mouse_focus_signal()
	{
		return m_mouse_focus.changed_signal();
	}

private:
	/** \todo Get rid of this in 0.5 / 0.6 */
	typedef std::map<k3d::iobject::id_type, k3d::iobject::id_type> object_id_map_t;

	/** \brief Helper function for retrieving <variable> tags as a part of converting legacy documents
		\todo Get rid of this in 0.5 / 0.6
	*/
	
	sdpxml::Element& get_variable(sdpxml::Element& XMLObject, const std::string Name)
	{
		return k3d::xml::safe_element(k3d::xml::safe_element(XMLObject, "variables"), sdpxml::Element("variable", "", sdpxml::Attribute("name", Name)));
	}

	/** \brief Helper function for renaming <variable> tags as a part of converting legacy documents
		\todo Get rid of this in 0.5 / 0.6
	*/
	void rename_variable(sdpxml::Element& XMLObject, const std::string& OldName, const std::string& NewName)
	{
		sdpxml::Element* const xml_variables = sdpxml::FindElement(XMLObject, sdpxml::SameName("variables"));
		if(!xml_variables)
			return;
			
		for(sdpxml::ElementCollection::iterator xml_variable = xml_variables->Children().begin(); xml_variable != xml_variables->Children().end(); ++xml_variable)
			{
				if(xml_variable->Name() != "variable")
					continue;
					
				if(OldName == sdpxml::GetAttribute(*xml_variable, "name", std::string()))
					{
						sdpxml::SetAttribute(*xml_variable, sdpxml::Attribute("name", NewName));
						return;
					}
			}
	}

	void reconnect_channel(sdpxml::Element& XMLObject, const std::string& Property, sdpxml::ElementCollection& NewXMLDependencies, const std::string& Value)
	{
		NewXMLDependencies.push_back(
			sdpxml::Element("dependency", "",
				sdpxml::Attribute("from_object", sdpxml::GetAttribute(get_variable(XMLObject, Property), "value", std::string())),
				sdpxml::Attribute("from_property", "output"),
				sdpxml::Attribute("to_object", sdpxml::GetAttribute(XMLObject, "id", std::string())),
				sdpxml::Attribute("to_property", Property)));

		sdpxml::SetAttribute(get_variable(XMLObject, Property), sdpxml::Attribute("value", Value));
	}

	/** \brief Helper functor for converting legacy hierarchy data into DAG dependencies
		\todo Get rid of this in 0.5 / 0.6
	*/
	class add_legacy_dependencies
	{
	public:
		add_legacy_dependencies(sdpxml::Element& XMLParent, object_id_map_t& SourceObjectIDMap, object_id_map_t& TargetObjectIDMap, sdpxml::ElementCollection& NewXMLDependencies) :
			parent_id(sdpxml::GetAttribute(XMLParent, "id", 0)),
			source_object_id_map(SourceObjectIDMap),
			target_object_id_map(TargetObjectIDMap),
			new_xml_dependencies(NewXMLDependencies)
		{
		}
		
		void operator()(sdpxml::Element& XMLObject)
		{
			if(XMLObject.Name() != "object")
				return;

			new_xml_dependencies.push_back(
				sdpxml::Element("dependency", "",
					sdpxml::Attribute("from_object", k3d::string_cast(source_object_id_map[parent_id])),
					sdpxml::Attribute("from_property", "output_matrix"),
					sdpxml::Attribute("to_object", k3d::string_cast(target_object_id_map[sdpxml::GetAttribute(XMLObject, "id", 0)])),
					sdpxml::Attribute("to_property", "input_matrix")));

			std::for_each(XMLObject.Children().begin(), XMLObject.Children().end(), add_legacy_dependencies(XMLObject, source_object_id_map, target_object_id_map, new_xml_dependencies));
		}
		
	private:
		const k3d::iobject::id_type parent_id;
		object_id_map_t& source_object_id_map;
		object_id_map_t& target_object_id_map;
		sdpxml::ElementCollection& new_xml_dependencies;
	};

	/** \brief Modifies XML as-needed so that both legacy and recent documents can be loaded with the same code
		\todo Get rid of this in 0.5 / 0.6
	*/
	void upgrade_document(sdpxml::Element& XMLDocument)
	{
		// If this document doesn't contain the version of the program that wrote it, we consider it "ancient", which has an impact on how we map certain values to channels
//		const bool ancient_document = sdpxml::GetAttribute(XMLDocument, "version", std::string()).empty();
	
		sdpxml::Element& xml_objects = k3d::xml::safe_element(XMLDocument, "objects");
		sdpxml::Element& xml_dag = k3d::xml::safe_element(XMLDocument, "dag");

		// Keep track of any additional objects or dependencies we decide to add to the document ...
		sdpxml::ElementCollection new_xml_objects;
		sdpxml::ElementCollection new_xml_dependencies;

		// Store a mapping of original object IDs to (possibly) new object IDs for converting from the hierarchy to the DAG ...
		object_id_map_t source_object_id_map;
		object_id_map_t target_object_id_map;

		// Get the largest ID in the existing document, in case we need to generate any new unique IDs ...
		k3d::iobject::id_type largest_id = 0;
		for(sdpxml::ElementCollection::iterator xml_object = xml_objects.Children().begin(); xml_object != xml_objects.Children().end(); ++xml_object)
			{
				if(xml_object->Name() == "object")
					largest_id = std::max(largest_id, sdpxml::GetAttribute(*xml_object, "id", k3d::iobject::id_type(0)));
			}

		// Convert legacy document-time data into a TimeSource object ...
		std::string xml_time_source_id;
		sdpxml::ElementPointer const xml_time = sdpxml::FindElement(XMLDocument, sdpxml::SameName("time"));
		if(xml_time)
			{
				sdpxml::Element xml_time_source("object");
				sdpxml::SetAttribute(xml_time_source, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::TimeSource())));
				sdpxml::SetAttribute(xml_time_source, sdpxml::Attribute("id", k3d::string_cast(++largest_id)));
				sdpxml::SetAttribute(xml_time_source, sdpxml::Attribute("name", "TimeSource"));
				xml_time_source.Children().insert(xml_time_source.Children().end(), xml_time->Children().begin(), xml_time->Children().end());
				
				rename_variable(xml_time_source, "starttime", "start_time");
				rename_variable(xml_time_source, "endtime", "end_time");
				rename_variable(xml_time_source, "framerate", "frame_rate");
				rename_variable(xml_time_source, "viewtime", "time");
				
				new_xml_objects.push_back(xml_time_source);

				xml_time_source_id = sdpxml::GetAttribute(xml_time_source, "id", std::string());
			}

		// Look for specific older objects, and replace them with their newer counterparts ...
		for(sdpxml::ElementCollection::iterator xml_object = xml_objects.Children().begin(); xml_object != xml_objects.Children().end(); ++xml_object)
			{
				if(xml_object->Name() != "object")
					continue;

				// The <state> tag is a reliable way to identify legacy versions of objects
				const bool legacy_object = sdpxml::FindElement(*xml_object, sdpxml::SameName("state"));
				const k3d::uuid class_id = sdpxml::GetAttribute(*xml_object, "class", k3d::uuid::null());
				const k3d::iobject::id_type object_id = sdpxml::GetAttribute(*xml_object, "id", 0);
				const std::string name = sdpxml::GetAttribute(*xml_object, "name", std::string());

				// By default, hierarchical relationships apply to the original object
				source_object_id_map[object_id] = object_id;
				target_object_id_map[object_id] = object_id;
				
				// Convert legacy mesh objects into FrozenMesh / MeshInstance pairs ...
				if(class_id == k3d::uuid(0x00000001, 0x00000000, 0x00000000, 0x00000018))
					{
						sdpxml::SetAttribute(*xml_object, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::FrozenMesh())));

						sdpxml::Element xml_mesh_instance(*xml_object);
						sdpxml::SetAttribute(xml_mesh_instance, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::MeshInstance())));
						sdpxml::SetAttribute(xml_mesh_instance, sdpxml::Attribute("id", k3d::string_cast(++largest_id)));
						sdpxml::SetAttribute(xml_mesh_instance, sdpxml::Attribute("name", name + " Instance"));
						
						new_xml_objects.push_back(xml_mesh_instance);
						new_xml_dependencies.push_back(sdpxml::Element("dependency", "", sdpxml::Attribute("from_object", k3d::string_cast(object_id)), sdpxml::Attribute("from_property", "output_mesh"), sdpxml::Attribute("to_object", k3d::string_cast(largest_id)), sdpxml::Attribute("to_property", "input_mesh")));

						// Any hierarchical relationships should be applied to the mesh instance, not the mesh ...
						source_object_id_map[object_id] = largest_id;
						target_object_id_map[object_id] = largest_id;
						
						std::cerr << warning << "Converting legacy Mesh object into a FrozenMesh / MeshInstance pair" << std::endl;
					}
				// Convert legacy camera objects into RenderManEngine / Viewport pairs ...
				else if(class_id == k3d::uuid(0x00000001, 0x00000000, 0x00000000, 0x0000000d))
					{
						sdpxml::SetAttribute(*xml_object, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::RenderManEngine())));

						sdpxml::Element xml_viewport(*xml_object);
						sdpxml::SetAttribute(xml_viewport, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::Viewport())));
						sdpxml::SetAttribute(xml_viewport, sdpxml::Attribute("id", k3d::string_cast(++largest_id)));
						sdpxml::SetAttribute(xml_viewport, sdpxml::Attribute("name", name + " Viewport"));
						k3d::xml::safe_element(xml_viewport, "variables").Append(sdpxml::Element("variable", "", sdpxml::Attribute("name", "viewport_host"), sdpxml::Attribute("value", k3d::string_cast(object_id))));

						rename_variable(xml_viewport, "editorfog", "fog");
						rename_variable(xml_viewport, "editorfognear", "fog_near");
						rename_variable(xml_viewport, "editorfogfar", "fog_far");
						rename_variable(xml_viewport, "editortwosided", "draw_two_sided");
						rename_variable(xml_viewport, "editororientation", "draw_face_orientations");
						rename_variable(xml_viewport, "editorfogfar", "fog_far");
						
						rename_variable(*xml_object, "riengine", "render_engine");
						rename_variable(*xml_object, "outputpixelwidth", "pixel_width");
						rename_variable(*xml_object, "outputpixelheight", "pixel_height");
						rename_variable(*xml_object, "outputpixelaspectratio", "pixel_aspect_ratio");
						rename_variable(*xml_object, "nearplane", "near");
						rename_variable(*xml_object, "farplane", "far");
						rename_variable(*xml_object, "rixbucketsize", "bucket_width");
						rename_variable(*xml_object, "riybucketsize", "bucket_height");
						rename_variable(*xml_object, "rigridsize", "grid_size");
						rename_variable(*xml_object, "rieyesplits", "eye_splits");
						rename_variable(*xml_object, "ritexturememory", "texture_memory");
						rename_variable(*xml_object, "transparentbackground", "render_alpha");
						rename_variable(*xml_object, "rixsamples", "pixel_xsamples");
						rename_variable(*xml_object, "riysamples", "pixel_ysamples");
						rename_variable(*xml_object, "ripixelfiltername", "pixel_filter");
						rename_variable(*xml_object, "ripixelfilterwidth", "pixel_filter_width");
						rename_variable(*xml_object, "ripixelfilterheight", "pixel_filter_height");
						rename_variable(*xml_object, "ridepthoffield", "render_dof");
						rename_variable(*xml_object, "focallength", "focal_length");
						rename_variable(*xml_object, "focalplane", "focus_plane");
						rename_variable(*xml_object, "rishadingrate", "shading_rate");
						rename_variable(*xml_object, "rishadinginterpolation", "shading_interpolation");
						rename_variable(*xml_object, "ritwosided", "two_sided");
						rename_variable(*xml_object, "ricropwindowleft", "crop_window_left");
						rename_variable(*xml_object, "ricropwindowright", "crop_window_right");
						rename_variable(*xml_object, "ricropwindowtop", "crop_window_top");
						rename_variable(*xml_object, "ricropwindowbottom", "crop_window_bottom");
						rename_variable(*xml_object, "rimotionblur", "render_motion_blur");

						reconnect_channel(*xml_object, "focal_length", new_xml_dependencies, "35");
						reconnect_channel(*xml_object, "focal_plane", new_xml_dependencies, "35");
						reconnect_channel(*xml_object, "fstop", new_xml_dependencies, "1.8");
						reconnect_channel(*xml_object, "exposure", new_xml_dependencies, "1");
						reconnect_channel(*xml_object, "gamma", new_xml_dependencies, "1");
						reconnect_channel(*xml_object, "far", new_xml_dependencies, "1");
						reconnect_channel(*xml_object, "near", new_xml_dependencies, "10000");

						// Get rid of any viewport transformations
						xml_viewport.Children().erase(std::remove_if(xml_viewport.Children().begin(), xml_viewport.Children().end(), sdpxml::SameName("transformation")), xml_viewport.Children().end());

						// Set the render engine clipping frustum explicitly (used to be implicit)
						const double pixel_width = sdpxml::GetAttribute(get_variable(*xml_object, "pixel_width"), "value", 0.0);
						const double pixel_height = sdpxml::GetAttribute(get_variable(*xml_object, "pixel_height"), "value", 0.0);
						const double pixel_aspect_ratio = sdpxml::GetAttribute(get_variable(*xml_object, "pixel_aspect_ratio"), "value", 0.0);
						if(pixel_height)
							{
								const double world_aspect_ratio = (pixel_width / pixel_height) * pixel_aspect_ratio;
								const double perspective_focal_length = sdpxml::GetAttribute(get_variable(*xml_object, "focal_length"), "value", 0.0);
								if(perspective_focal_length)
									{
										sdpxml::SetAttribute(get_variable(*xml_object, "left"), sdpxml::Attribute("value", k3d::string_cast(-0.5 * world_aspect_ratio / perspective_focal_length)));
										sdpxml::SetAttribute(get_variable(*xml_object, "right"), sdpxml::Attribute("value", k3d::string_cast(0.5 * world_aspect_ratio / perspective_focal_length)));
										sdpxml::SetAttribute(get_variable(*xml_object, "top"), sdpxml::Attribute("value", k3d::string_cast(0.5 / perspective_focal_length)));
										sdpxml::SetAttribute(get_variable(*xml_object, "bottom"), sdpxml::Attribute("value", k3d::string_cast(-0.5 / perspective_focal_length)));
										sdpxml::SetAttribute(get_variable(*xml_object, "near"), sdpxml::Attribute("value", "1.0"));
									}
							}
																		
						new_xml_objects.push_back(xml_viewport);
						new_xml_dependencies.push_back(sdpxml::Element("dependency", "", sdpxml::Attribute("from_object", k3d::string_cast(object_id)), sdpxml::Attribute("from_property", "output_matrix"), sdpxml::Attribute("to_object", k3d::string_cast(largest_id)), sdpxml::Attribute("to_property", "input_matrix")));
												
						std::cerr << warning << "Converting legacy Camera object into a RenderManEngine / Viewport pair" << std::endl;
					}
				// Convert legacy transformer objects into Transform objects ...
				else if(class_id == k3d::uuid(0x00000001, 0x00000000, 0x00000000, 0x00000040))
					{
						sdpxml::Element xml_scale(*xml_object);
						sdpxml::SetAttribute(xml_scale, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::Scale())));
						sdpxml::SetAttribute(xml_scale, sdpxml::Attribute("id", k3d::string_cast(++largest_id)));
						sdpxml::SetAttribute(xml_scale, sdpxml::Attribute("name", name + " Scale"));
						rename_variable(xml_scale, "xscale", "x");
						rename_variable(xml_scale, "yscale", "y");
						rename_variable(xml_scale, "zscale", "z");
						reconnect_channel(xml_scale, "x", new_xml_dependencies, "1");
						reconnect_channel(xml_scale, "y", new_xml_dependencies, "1");
						reconnect_channel(xml_scale, "z", new_xml_dependencies, "1");

						target_object_id_map[object_id] = largest_id;

						sdpxml::Element xml_orientation(*xml_object);
						sdpxml::SetAttribute(xml_orientation, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::Orientation())));
						sdpxml::SetAttribute(xml_orientation, sdpxml::Attribute("id", k3d::string_cast(++largest_id)));
						sdpxml::SetAttribute(xml_orientation, sdpxml::Attribute("name", name + " Orientation"));
						rename_variable(xml_orientation, "xrotation", "x");
						rename_variable(xml_orientation, "yrotation", "y");
						rename_variable(xml_orientation, "zrotation", "z");
						reconnect_channel(xml_orientation, "x", new_xml_dependencies, "0");
						reconnect_channel(xml_orientation, "y", new_xml_dependencies, "0");
						reconnect_channel(xml_orientation, "z", new_xml_dependencies, "0");

						sdpxml::SetAttribute(*xml_object, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::Position())));
						sdpxml::SetAttribute(*xml_object, sdpxml::Attribute("name", name + " Position"));
						rename_variable(*xml_object, "xoffset", "x");
						rename_variable(*xml_object, "yoffset", "y");
						rename_variable(*xml_object, "zoffset", "z");
						reconnect_channel(*xml_object, "x", new_xml_dependencies, "0");
						reconnect_channel(*xml_object, "y", new_xml_dependencies, "0");
						reconnect_channel(*xml_object, "z", new_xml_dependencies, "0");
					
						new_xml_objects.push_back(xml_orientation);
						new_xml_objects.push_back(xml_scale);

						new_xml_dependencies.push_back(
							sdpxml::Element("dependency", "",
							sdpxml::Attribute("from_object", sdpxml::GetAttribute(xml_scale, "id", std::string())),
							sdpxml::Attribute("from_property", "output_matrix"),
							sdpxml::Attribute("to_object", sdpxml::GetAttribute(xml_orientation, "id", std::string())),
							sdpxml::Attribute("to_property", "input_matrix")));

						new_xml_dependencies.push_back(
							sdpxml::Element("dependency", "",
							sdpxml::Attribute("from_object", sdpxml::GetAttribute(xml_orientation, "id", std::string())),
							sdpxml::Attribute("from_property", "output_matrix"),
							sdpxml::Attribute("to_object", sdpxml::GetAttribute(*xml_object, "id", std::string())),
							sdpxml::Attribute("to_property", "input_matrix")));

						std::cerr << warning << "Converting legacy Transformer object into a Position / Orientation / Scale triplet" << std::endl;
					}
				// Convert legacy channel objects into ScalarBezierChannel and ColorBezierChannel objects ...
				else if(class_id == k3d::uuid(0x00000001, 0x00000000, 0x00000000, 0x0000000e))
					{
						const std::string datatype = sdpxml::GetAttribute(get_variable(*xml_object, "datatype"), "value", std::string());

						if(datatype == "color")
							{
								sdpxml::SetAttribute(*xml_object, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::ColorBezierChannel())));
								std::cerr << warning << "Converting legacy Channel object into a ColorBezierChannel object" << std::endl;
							}
						else
							{
								sdpxml::SetAttribute(*xml_object, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::ScalarBezierChannel())));
								std::cerr << warning << "Converting legacy Channel object into a ScalarBezierChannel object" << std::endl;
							}
						
						new_xml_dependencies.push_back(
							sdpxml::Element("dependency", "",
							sdpxml::Attribute("from_object", xml_time_source_id),
							sdpxml::Attribute("from_property", "time"),
							sdpxml::Attribute("to_object", sdpxml::GetAttribute(*xml_object, "id", std::string())),
							sdpxml::Attribute("to_property", "input")));
					}
				// Update legacy FileBitmap objects ...
				else if(class_id == k3d::classes::FileBitmap())
					{
						rename_variable(*xml_object, "imagepath", "file");
					}
				// Update RenderManMaterial objects ...
				else if(class_id == k3d::classes::RenderManMaterial() && legacy_object)
					{
						reconnect_channel(*xml_object, "color", new_xml_dependencies, "1 1 1");
						reconnect_channel(*xml_object, "opacity", new_xml_dependencies, "1 1 1");
					}
				// Ignore old-fashioned FractalTerrain objects ...
				else if(class_id == k3d::uuid(0x00000001, 0x00000000, 0x00000000, 0x00000051))
					{
						sdpxml::SetAttribute(*xml_object, sdpxml::Attribute("do_not_load", "true"));
						std::cerr << warning << "Legacy FractalTerrain object will not be loaded" << std::endl;
					}
				// Convert old-fashioned Fog objects into RenderManVolumeShader objects ...
				else if(class_id == k3d::uuid(0xeefa8085, 0xac3146a0, 0xaff19aac, 0x4a2b5a5d))
					{
						sdpxml::SetAttribute(*xml_object, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::RenderManVolumeShader())));
						
						const std::string shader_name = sdpxml::GetAttribute(get_variable(*xml_object, "shader"), "value", std::string());
						xml_object->Children() = get_variable(*xml_object, "shader").Children();
						sdpxml::SetAttribute(get_variable(*xml_object, "shader_name"), sdpxml::Attribute("value", shader_name));

						std::cerr << warning << "Converting legacy Fog object into a RenderManVolumeShader - manually reconnect it" << std::endl;
					}
				// Handle updates to quadric primitivies ...
				else if(
						class_id == k3d::classes::Cone() ||
						class_id == k3d::classes::Cylinder() ||
						class_id == k3d::classes::Disk() ||
						class_id == k3d::classes::Hyperboloid() ||
						class_id == k3d::classes::Paraboloid() ||
						class_id == k3d::classes::Sphere() ||
						class_id == k3d::classes::Torus())
					{
						rename_variable(*xml_object, "sweepangle", "thetamax");
						
						if(class_id == k3d::classes::Paraboloid())
							{
								rename_variable(*xml_object, "height", "zmax");
							}
					}
							
				// Create separate shader objects when we run across embedded shader information ...
				else if(class_id == k3d::classes::RenderManLight())
					{
						sdpxml::Element& shader = get_variable(*xml_object, "shader");
						if(shader.Children().size())
							{
								const std::string shader_name = sdpxml::GetAttribute(get_variable(*xml_object, "shader"), "value", std::string());
							
								sdpxml::Element xml_shader("object");
								sdpxml::SetAttribute(xml_shader, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::RenderManLightShader())));
								sdpxml::SetAttribute(xml_shader, sdpxml::Attribute("id", k3d::string_cast(++largest_id)));
								sdpxml::SetAttribute(xml_shader, sdpxml::Attribute("name", name + " Shader"));
								
								shader.Children().swap(xml_shader.Children());
								sdpxml::SetAttribute(get_variable(xml_shader, "shader_name"), sdpxml::Attribute("value", shader_name));
								
								new_xml_objects.push_back(xml_shader);

								sdpxml::SetAttribute(get_variable(*xml_object, "shader"), sdpxml::Attribute("value", k3d::string_cast(largest_id)));
																								
								std::cerr << warning << "Creating a RenderManLightShader object from a legacy RenderManLight" << std::endl;
							}
					}
				else if(class_id == k3d::classes::RenderManMaterial())
					{
						sdpxml::Element& surface_shader = get_variable(*xml_object, "surface");
						if(surface_shader.Children().size())
							{
								const std::string shader_name = sdpxml::GetAttribute(get_variable(*xml_object, "surface"), "value", std::string());
							
								sdpxml::Element xml_shader("object");
								sdpxml::SetAttribute(xml_shader, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::RenderManSurfaceShader())));
								sdpxml::SetAttribute(xml_shader, sdpxml::Attribute("id", k3d::string_cast(++largest_id)));
								sdpxml::SetAttribute(xml_shader, sdpxml::Attribute("name", name + " Shader"));
								
								surface_shader.Children().swap(xml_shader.Children());
								sdpxml::SetAttribute(get_variable(xml_shader, "shader_name"), sdpxml::Attribute("value", shader_name));
								
								new_xml_objects.push_back(xml_shader);

								sdpxml::SetAttribute(get_variable(*xml_object, "surface_shader"), sdpxml::Attribute("value", k3d::string_cast(largest_id)));
																								
								std::cerr << warning << "Creating a RenderManSurfaceShader object from a legacy RenderManMaterial" << std::endl;
							}
							
						sdpxml::Element& displacement_shader = get_variable(*xml_object, "displacement");
						if(displacement_shader.Children().size())
							{
								const std::string shader_name = sdpxml::GetAttribute(get_variable(*xml_object, "displacement"), "value", std::string());
							
								sdpxml::Element xml_shader("object");
								sdpxml::SetAttribute(xml_shader, sdpxml::Attribute("class", k3d::string_cast(k3d::classes::RenderManDisplacementShader())));
								sdpxml::SetAttribute(xml_shader, sdpxml::Attribute("id", k3d::string_cast(++largest_id)));
								sdpxml::SetAttribute(xml_shader, sdpxml::Attribute("name", name + " Displacement Shader"));
								
								displacement_shader.Children().swap(xml_shader.Children());
								sdpxml::SetAttribute(get_variable(xml_shader, "shader_name"), sdpxml::Attribute("value", shader_name));
								
								new_xml_objects.push_back(xml_shader);

								sdpxml::SetAttribute(get_variable(*xml_object, "displacement_shader"), sdpxml::Attribute("value", k3d::string_cast(largest_id)));
																								
								std::cerr << warning << "Creating a RenderManDisplacementShader object from a legacy RenderManMaterial" << std::endl;
							}
					}
			}

		// Convert the hierarchy into DAG dependencies ...
		sdpxml::Element* const xml_hierarchy = sdpxml::FindElement(XMLDocument, sdpxml::SameName("hierarchy"));
		if(xml_hierarchy)
			{
				std::cerr << warning << "Converting legacy Hierarchy into DAG dependencies ... may generate harmless downstream errors" << std::endl;
			
				for(sdpxml::ElementCollection::iterator xml_object = xml_hierarchy->Children().begin(); xml_object != xml_hierarchy->Children().end(); ++xml_object)
					{
						if(xml_object->Name() != "object")
							continue;
							
						std::for_each(xml_object->Children().begin(), xml_object->Children().end(), add_legacy_dependencies(*xml_object, source_object_id_map, target_object_id_map, new_xml_dependencies));
					}
			}

		// Add new objects to the document ...
		xml_objects.Children().insert(xml_objects.Children().end(), new_xml_objects.begin(), new_xml_objects.end());
		xml_dag.Children().insert(xml_dag.Children().end(), new_xml_dependencies.begin(), new_xml_dependencies.end());
	}

	bool load(const boost::filesystem::path DocumentPath)
	{
		// Sanity checks ...
		return_val_if_fail(!DocumentPath.empty(), false);

		// Try loading the file as XML ...
		sdpxml::Document xml("k3dml");
		boost::filesystem::ifstream stream(DocumentPath);
		if(!xml.Load(stream, DocumentPath.native_file_string()))
			return false;

		// Make sure it's a K3D document ...
		if(xml.Name() != "k3dml")
			return false;

		// Look for a document version ...
		std::stringstream version(sdpxml::GetAttribute(xml, "version", std::string()));
		unsigned long major_version = 0;
		unsigned long minor_version = 0;
		unsigned long revision = 0;
		unsigned long build = 0;
		char point;
		version >> major_version >> point >> minor_version >> point >> revision >> point >> build;

		// Look for the document element ...
		sdpxml::ElementPointer const xml_document = sdpxml::FindElement(xml, sdpxml::SameName("document"));
		if(xml_document)
			{
				// Handle documents from older versions of the software by modifying the document ...
				upgrade_document(*xml_document);

std::ofstream temp("/tmp/converted.xml");
temp << *xml_document << std::endl;

				// Load objects
				k3d::iobject_collection::objects_t objects;
				sdpxml::ElementPointer const xml_objects = sdpxml::FindElement(*xml_document, sdpxml::SameName("objects"));
				if(xml_objects)
					{
						for(sdpxml::ElementCollection::iterator xml_object = xml_objects->Children().begin(); xml_object != xml_objects->Children().end(); ++xml_object)
							{
								if(xml_object->Name() != "object")
									continue;

								if(sdpxml::GetAttribute(*xml_object, "do_not_load", false))
									continue;

								const std::string name = sdpxml::GetAttribute(*xml_object, "name", std::string());
								const k3d::uuid class_id = sdpxml::GetAttribute(*xml_object, "class", k3d::uuid::null());
								if(class_id == k3d::uuid::null())
									{
										std::cerr << error << "Object [" << name << "] with unspecified class ID will not be loaded" << std::endl;
										continue;
									}

								const k3d::iobject::id_type object_id = sdpxml::GetAttribute(*xml_object, "id", 0);
								if(object_id == 0)
									{
										std::cerr << error << "Object [" << name << "] with unspecified ID will not be loaded" << std::endl;
										continue;
									}

								k3d::iplugin_factory* const plugin_factory = k3d::plugin(class_id);
								if(!plugin_factory)
									{
										std::cerr << error << "Object [" << name << "] with unknown class ID [" << class_id << "] will not be loaded" << std::endl;
										continue;
									}

								k3d::idocument_plugin_factory* const document_plugin_factory = dynamic_cast<k3d::idocument_plugin_factory*>(plugin_factory);
								if(!document_plugin_factory)
									{
										std::cerr << error << "Non-document object [" << name << "] will not be loaded" << std::endl;
										continue;
									}
									
								k3d::iobject* const object = document_plugin_factory->create_plugin(*this);
								if(!object)
									{
										std::cerr << error << "Error creating object [" << name << "] instance" << std::endl;
										continue;
									}

								k3d::ipersistent* const persist = dynamic_cast<k3d::ipersistent*>(object);
								if(!persist)
									{
										std::cerr << error << "Error loading object [" << name << "]" << std::endl;

										delete dynamic_cast<k3d::ideletable*>(object);
										continue;
									}

								k3d::undoable_new(dynamic_cast<k3d::ideletable*>(object), *this);

								persist->load(xml, *xml_object);
								object->set_id(object_id);
								objects.insert(object);
							}

						m_objects.add_objects(objects);
					}

				// Load the DAG ...
				k3d::load_dag(*this, *xml_document);

				// Let all our objects know we're done loading ...
				for(k3d::iobject_collection::objects_t::const_iterator object = objects.begin(); object != objects.end(); ++object)
					{
						k3d::ipersistent* const persist = dynamic_cast<k3d::ipersistent*>(*object);
						if(persist)
							persist->load_complete();
					}
			}

		return true;
	}

	/// Used to find-out whether it's safe to close the document
	safe_to_close_signal_t m_safe_to_close_signal;
	/// Notifies observers that the document is being closed
	close_signal_t m_close_signal;

	/// Records changes made by the user for Undo / Redo purposes
	k3d::istate_recorder& m_state_recorder;
	/// Stores document objects ...
	k3d::iobject_collection& m_objects;
	/// Stores a reference to an implementation of idag
	k3d::idag& m_dag;
	/// Stores the set of selected objects
	selection_implementation m_selection;

	/// Stores the full document filepath
	k3d_data_property(boost::filesystem::path, k3d::immutable_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_path;
	/// Stores the document title
	k3d_data_property(std::string, k3d::immutable_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_title;
	/// Keeps track of which object (if any) has grabbed the mouse focus
	k3d_data(k3d::imouse_event_observer*, k3d::no_name, k3d::change_signal, k3d::no_undo, k3d::local_storage, k3d::no_constraint) m_mouse_focus;
};

/// This is a real abortion, but it solves our interdependency problems among state recorder, dag, and property collection implementations
class document_implementation
{
public:
	document_implementation() :
		m_state_recorder(new state_recorder_implementation()),
		m_objects(new object_collection_implementation(*m_state_recorder)),
		m_dag(new dag_implementation(*m_state_recorder)),
		m_document(new public_document_implementation(*m_state_recorder, *m_objects, *m_dag))
	{
	}

	~document_implementation()
	{
		m_document->close_signal().emit();
		
		m_dag->on_close_document();
		m_objects->on_close_document();

		delete m_document;
		delete m_dag;
		delete m_objects;
		delete m_state_recorder;
	}

	state_recorder_implementation* const m_state_recorder;
	object_collection_implementation* const m_objects;
	dag_implementation* const m_dag;
	public_document_implementation* const m_document;

private:
	document_implementation(const document_implementation&);
	document_implementation& operator=(const document_implementation&);
};

typedef std::vector<document_implementation*> documents_t;

documents_t& documents()
{
	static documents_t g_documents;
	return g_documents;
}

} // namespace

namespace k3d
{

idocument* create_document()
{
	std::auto_ptr<document_implementation> document(new document_implementation());
	if(!document->m_document->create())
		return 0;

	documents().push_back(document.get());
	return document.release()->m_document;
}

idocument* open_document(const boost::filesystem::path& DocumentFile)
{
	std::auto_ptr<document_implementation> document(new document_implementation());
	if(!document->m_document->open(DocumentFile))
		return 0;

	documents().push_back(document.get());
	return document.release()->m_document;
}

void close_document(idocument& Document)
{
	for(documents_t::iterator document = documents().begin(); document != documents().end(); ++document)
		{
			if((*document)->m_document == &Document)
				{
					delete *document;
					documents().erase(document);

					return;
				}
		}

	std::cerr << error << "close_document(): could not find document to destroy" << std::endl;
}

} // namespace k3d

