// 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 A tool to connect a property of one object to that of another
		\author Bart Janssens (bart.janssens@polytechnic.be)
		\author Tim Shead (tshead@k-3d.com)
*/

#include "button.h"
#include "k3ddialog.h"
#include "object_chooser.h"

#include <k3dsdk/application.h>
#include <k3dsdk/idocument.h>
#include <k3dsdk/ioptions.h>
#include <k3dsdk/iuser_interface.h>
#include <k3dsdk/object.h>
#include <k3dsdk/property_collection.h>
#include <k3dsdk/result.h>
#include <k3dsdk/state_change_set.h>

#include <sdpgtk/sdpgtkevents.h>

namespace k3d
{

/// Returns true iff adding a dependency from the source property to the destination property would cause a cyclic dependency
bool is_cyclic(idocument& Document, iproperty& Destination, iproperty* const Source)
{
	// We're clearing a dependency, so no problemo ...
	if(!Source)
		return false;

	// Avoid really obvious stuff ...
	if(Source == &Destination)
		return true;

	// If the source has a dependency all its own, follow it ...
	iproperty* const source = Document.dag().dependency(*Source);
	if(source)
		return is_cyclic(Document, Destination, source);

	iobject* const object = find_object(Document.objects(), *Source);
	if(!object)
		return false;

	iproperty_collection* const property_collection = dynamic_cast<iproperty_collection*>(object);
	return_val_if_fail(property_collection, false);

	for(iproperty_collection::properties_t::const_iterator property = property_collection->properties().begin(); property != property_collection->properties().end(); ++property)
		{
			if(*property == &Destination)
				return true;

			if(*property == Source)
				continue;

			iproperty* const source = Document.dag().dependency(**property);
			if(source)
				return is_cyclic(Document, Destination, source);
		}

	return false;
}

} // namespace k3d

namespace
{

/////////////////////////////////////////////////////////////////////////////////////////////
// object_container

typedef k3d_object_property(k3d::iproperty_collection, k3d::immutable_name, k3d::no_undo, k3d::local_storage) object_container;

/////////////////////////////////////////////////////////////////////////////////////////////
// property_container

class property_container
{
public:
	property_container(k3d::iproperty* const Property) :
		m_property(Property)
	{
	}

	void set(k3d::iproperty* const Property)
	{
		m_property = Property;
		m_changed_signal.emit();
	}

	k3d::iproperty* get()
	{
		return m_property;
	}
	
	typedef SigC::Signal0<void> property_changed_signal_t;
	property_changed_signal_t& changed_signal()
	{
		return m_changed_signal;
	}
	
private:
	k3d::iproperty* m_property;
	property_changed_signal_t m_changed_signal;
};

/////////////////////////////////////////////////////////////////////////////////////////////
// property_chooser_gtkml

sdpxml::Document& property_chooser_gtkml()
{
	static sdpxml::Document gtkml("empty");
	if(gtkml.Name() == "empty")
		{
			std::istringstream uitemplate(
				"<gtkml>"
					"<eventbox>"
						"<hbox homogeneous=\"false\">"
							"<event signal=\"destroy\" name=\"destroy\"/>"
							"<button name=\"choose\" expand=\"true\" fill=\"true\">"
								"<event signal=\"clicked\" name=\"choose\"/>"
								"<hbox homogeneous=\"false\">"
									"<label name=\"label\" alignment=\"0 0.5\" labelpadding=\"3 0\" expand=\"true\" fill=\"true\"/>"
									"<arrow direction=\"down\" shadowtype=\"in\"/>"
								"</hbox>"
							"</button>"
						"</hbox>"
					"</eventbox>"
				"</gtkml>\n");

			assert(gtkml.Load(uitemplate, "property_chooser builtin template"));
		}

	return gtkml;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////
// property_chooser

const std::string control_label = "label";
const std::string control_choose = "choose";
const std::string control_selectnone = "selectnone";
const std::string control_selectproperty = "selectproperty";
const std::string event_close("event_close");
const std::string event_connect("event_connect");
const std::string event_configure("event_configure");
const std::string event_delete("event_delete");

/// Menu to choose properties, adapted from k3d::object_chooser::control. It uses an object property as data source.
class property_chooser :
	public k3dControl
{
	typedef k3dControl base;
	
public:
	property_chooser(k3d::iunknown* const CommandNodeParent, const std::string CommandNodeName) :
		base(CommandNodeParent, CommandNodeName),
		m_object(0),
		m_property(0),
		m_filter_property(0)
	{
		// Load the template:
		return_if_fail(Load(property_chooser_gtkml()));
		RootWidget().Show();
	}
	
	~property_chooser()
	{
		// No more events from this point forward ...
		DisconnectAllEvents();

		// Clean-up the GTK+ tree ...
		if(Root())
			RootWidget().Destroy();
	}

	bool attach(object_container* Object, property_container* Property, property_container* FilterProperty, k3d::istate_recorder* const StateRecorder, const std::string StateChangeName)
	{
		// Sanity checks ...
		return_val_if_fail(Object, false);
		return_val_if_fail(Property, false);

		return_val_if_fail(!m_object, false);
		return_val_if_fail(!m_property, false);
		return_val_if_fail(!m_filter_property, false);

		// Complete initialization:
		return_val_if_fail(base::Attach(StateRecorder, StateChangeName), false);

		// Set the data members
		m_object = Object;
		m_property = Property;
		m_filter_property = FilterProperty;

		// Update the display ...
		update();

		// We want to be notified if the data source changes ...
		m_object->changed_signal().connect(SigC::slot(*this, &property_chooser::on_object_changed));
		m_property->changed_signal().connect(SigC::slot(*this, &property_chooser::on_property_changed));

		if(m_filter_property)
			m_filter_property->changed_signal().connect(SigC::slot(*this, &property_chooser::on_filter_property_changed));

		return true;
	}

	bool execute_command(const std::string& Command, const std::string& Arguments)
	{
		if(Command == control_selectproperty)
			{
				// Find a property with the correct name ...
				k3d::iproperty* property = 0;

				if(m_object->interface())
					{
						const k3d::iproperty_collection::properties_t properties(m_object->interface()->properties());
					
						for(unsigned int i = 0; i != properties.size(); ++i)
							{
								if(Arguments == properties[i]->name())
									{
										property = properties[i];
										break;
									}
							}
					}
				return_val_if_fail(property, false);

				InteractiveActivateButton(control_choose, k3d::application().options().tutorial_speed(), true);

				for(menu_items::const_iterator menu_item = m_menu.begin(); menu_item != m_menu.end(); ++menu_item)
					{
						if(menu_item->property != property)
							continue;

						sdpGtkMenuItem m(GTK_MENU_ITEM(static_cast<GtkWidget*>(*menu_item)));
						m.InteractiveWarpPointer(k3d::application().options().tutorial_speed(), true, false);
						m.InteractiveActivate();

						gtk_menu_popdown(m_menu);

						return true;
					}

				return false;
			}
		else if(Command == control_selectnone)
			{
				InteractiveActivateButton(control_choose, k3d::application().options().tutorial_speed(), true);

				for(menu_items::const_iterator menu_item = m_menu.begin(); menu_item != m_menu.end(); ++menu_item)
					{
						if(menu_item->property)
							continue;

						sdpGtkMenuItem m(GTK_MENU_ITEM(static_cast<GtkWidget*>(*menu_item)));
						m.InteractiveWarpPointer(k3d::application().options().tutorial_speed(), true, false);
						m.InteractiveActivate();

						gtk_menu_popdown(m_menu);

						return true;
					}

				return false;
			}

		return base::execute_command(Command, Arguments);
	}

private:
	void on_object_changed()
	{
		return_if_fail(m_property);
		m_property->set(0);
	}

	void on_property_changed()
	{
		m_menu.clear();
		update();
	}

	void on_filter_property_changed()
	{
		return_if_fail(m_property);
		return_if_fail(m_filter_property);

		if(m_property->get() && m_filter_property->get() && m_property->get()->type() != m_filter_property->get()->type())
			m_property->set(0);

		m_menu.clear();
		update();
	}
	
	void update()
	{
		// Sanity checks ...
		return_if_fail(m_property);

		//check if there is a selection:
		std::string selection("--None--");
		if(m_property->get() != 0)
			selection = m_property->get()->name();

		// Display the current selection ...
		Label(control_label).SetText(selection.c_str());
	}
	
	void OnEvent(sdpGtkEvent* Event)
	{
		// Sanity checks ...
		assert_warning(Event);

		if(Event->Name() == "destroy")
			on_destroy();
		else if(Event->Name() == control_choose)
			on_choose();
		else
			base::OnEvent(Event);
	}

	void on_destroy()
	{
		DisconnectAllEvents();
		Clear();
	}

	void set_property(k3d::iproperty* const Property, const std::string UndoName)
	{
		// Sanity checks ...
		return_if_fail(m_property);

		// If nothing changed, we're done ...
		if(Property == m_property->get())
			return;
	
		// Turn this into an undo/redo -able event ...
		if(m_StateRecorder)
			m_StateRecorder->start_recording(k3d::create_state_change_set());
	
		// Set the new value ...
		m_property->set(Property);
	
		// Turn this into an undo/redo -able event ...
		if(m_StateRecorder)
			m_StateRecorder->commit_change_set(m_StateRecorder->stop_recording(), UndoName);
	}

	void on_choose()
	{
		// Sanity checks ...
		return_if_fail(m_object);
		return_if_fail(m_property);
		
		// Build the menu on-demand ...
		if(m_menu.empty())
			{
				// Keep track of the menu item index so we can select one ...
				unsigned int index = 0;
				unsigned int selected_item = 0;
	
				// Insert a "none" choice ...
				m_menu.push_back(menu_item("--None--", 0, SigC::slot(*this, &property_chooser::on_select_none)));
				++index;
	
				
				// Get the list of properties:
				if(m_object->interface())
					{	
						// Insert choices for selecting existing properties ...
						const k3d::iproperty_collection::properties_t properties(m_object->interface()->properties());
					
						for(unsigned int i = 0; i != properties.size(); ++i)
							{
								k3d::iproperty* property = properties[i];
		
								// Create a menu item for each property ...
								if(!match_filter(property))
									continue;

								m_menu.push_back(menu_item(property->name(), property, SigC::bind(SigC::slot(*this, &property_chooser::on_select_property), property)));
		
								// See if this is the currently selected object ...
								if(property == m_property->get())
									selected_item = index;
								++index;
							}
					}
				m_menu.build();
			}
	
		m_menu.popup();
	}

	// Checks if the type of Property is the same as the one in the filter
	bool match_filter(k3d::iproperty* Property)
	{
		if(m_filter_property && m_filter_property->get())
			return m_filter_property->get()->type() == Property->type();

		return true;
	}

	void on_select_none()
	{
		// Make sure we've got some storage, first!
		return_if_fail(m_property);
	
		// Record the command for posterity (tutorials) ...
		k3d::record_command(*this, k3d::icommand_node::command_t::USER_INTERFACE, control_selectnone);
	
		// Update the data object ...
		set_property(0, "Select None");
		update();
	}

	void on_select_property(k3d::iproperty* const Property)
	{
		// Sanity checks ...
		return_if_fail(Property);
		
		// Record things for posterity ...
		k3d::record_command(*this, k3d::icommand_node::command_t::USER_INTERFACE, control_selectproperty, Property->name());
	
		// Update underlying data ...
		set_property(Property, "Select " + Property->name());
	}

	class menu_item :
		public k3d::dynamic_menu::item
	{
	public:
		menu_item(const std::string& Name, k3d::iproperty* const Property, const slot_t& Slot = slot_t()) : k3d::dynamic_menu::item(Name, Slot), property(Property) { }
		
		k3d::iproperty* property;
	};

	typedef std::vector<menu_item> menu_items;
	k3d::dynamic_menu::control<menu_items> m_menu;
	
	object_container* m_object;
	property_container* m_property;
	property_container* m_filter_property;
};

/////////////////////////////////////////////////////////////////////////////
// connect_properties_dialog

class connect_properties_dialog :
	public k3dDialog
{
	typedef k3dDialog base;
	
public:
	connect_properties_dialog(k3d::idocument& Document, k3d::iobject* const SourceObject, k3d::iproperty* const SourceProperty, k3d::iobject* const DestinationObject, k3d::iproperty* const DestinationProperty) :
		base(&Document, "connect_properties", new k3d::options_window_geometry_store()),
		m_document(Document),
		m_source_object(k3d::init_name("source_object") + k3d::init_description("Source object [object]") + k3d::init_object_value(SourceObject) + k3d::init_document(Document)),
		m_destination_object(k3d::init_name("destination_object") + k3d::init_description("Destination object [object]") + k3d::init_object_value(DestinationObject) + k3d::init_document(Document)),
		m_source_property(SourceProperty),
		m_destination_property(DestinationProperty),
		m_source_property_chooser(0),
		m_destination_property_chooser(0)
	{
		return_if_fail(LoadGTKMLTemplate("connect_properties.gtkml"));

		if(get_object_chooser("source_object"))
			get_object_chooser("source_object")->attach(k3d::object_chooser::filter<k3d::iproperty_collection>(true), k3d::object_chooser::proxy(m_source_object, Document), 0, "");
		if(get_object_chooser("destination_object"))
			get_object_chooser("destination_object")->attach(k3d::object_chooser::filter<k3d::iproperty_collection>(true), k3d::object_chooser::proxy(m_destination_object, Document), 0, "");

		if(get_button("connect"))
			get_button("connect")->signal_activate().connect(SigC::slot(*this, &connect_properties_dialog::on_connect));
		if(get_button("close"))
			get_button("close")->signal_activate().connect(SigC::slot(*this, &connect_properties_dialog::on_close));

		m_source_property_chooser = new property_chooser(this, "source_property_chooser");
		m_destination_property_chooser = new property_chooser(this, "destination_property_chooser");

		m_source_property_chooser->attach(&m_source_object, &m_source_property, 0,  0, "Source property");
		m_destination_property_chooser->attach(&m_destination_object, &m_destination_property, &m_source_property, 0, "Destination property");

		Container("source_properties").Attach(m_source_property_chooser->RootWidget());
		Container("destination_properties").Attach(m_destination_property_chooser->RootWidget());

		// Close ourselves if the document is closed ...
		Document.close_signal().connect(SigC::slot(*this, &connect_properties_dialog::on_close));

		Show();
	}

	~connect_properties_dialog()
	{
		// No more events from this point forward ...
		DisconnectAllEvents();

		// Clean-up the GTK+ tree ...
		if(Root())
			RootWidget().Destroy();

		// Get rid of our widget pointers ...
		Clear();

		delete m_destination_property_chooser;
		delete m_source_property_chooser;
	}

private:	
	void OnDelete(sdpGtkEvent* Event)
	{
		// Sanity checks ...
		assert_warning(Event);

		// Don't let it happen ...
		((sdpGtkEventWidgetDeleteEvent*)Event)->SetResult(true);

		// Turn it into a "close", instead ...
		on_close();
	}
	
	void on_connect()
	{
		// Sanity checks ...
		return_if_fail(m_source_object.object());
		return_if_fail(m_source_property.get());
//		return_if_fail(m_destination_object.object());
//		return_if_fail(m_destination_property.get());
		if(m_destination_property.get())
			return_if_fail(m_source_property.get()->type() == m_destination_property.get()->type());

		// Prevent circular dependencies!
		if(k3d::is_cyclic(m_document, *m_destination_property.get(), m_source_property.get()))
			{
				if(k3d::application().user_interface())
					k3d::application().user_interface()->error_message("Cannot create circular DAG dependency", "Connect Properties:");

				std::cerr << error << "Cannot create circular DAG dependency" << std::endl;
				return;
			}

		k3d::record_state_change_set change(m_document, "Connect " + m_source_object.object()->name() + "." + m_source_property.get()->name() + " -> " + m_destination_object.object()->name() + "." + m_destination_property.get()->name());

		k3d::idag::dependencies_t dependencies;
		dependencies.insert(std::make_pair(m_destination_property.get(), m_source_property.get()));
		m_document.dag().set_dependencies(dependencies);
	}
	
	void on_close()
	{
		delete this;
	}

	k3d::idocument& m_document;
	k3d_object_property(k3d::iproperty_collection, k3d::immutable_name, k3d::no_undo, k3d::local_storage) m_source_object;
	k3d_object_property(k3d::iproperty_collection, k3d::immutable_name, k3d::no_undo, k3d::local_storage) m_destination_object;
	property_container m_source_property;
	property_container m_destination_property;
	property_chooser* m_source_property_chooser;
	property_chooser* m_destination_property_chooser;
};

} // namespace

namespace k3d
{

void create_connect_properties_dialog(idocument& Document, iobject* const SourceObject, iproperty* const SourceProperty, iobject* const DestinationObject, iproperty* const DestinationProperty)
{
	new connect_properties_dialog(Document, SourceObject, SourceProperty, DestinationObject, DestinationProperty);
}
			
} //namespace k3d


