// 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

#include "auto_dialog.h"
#include "connect_properties.h"
#include "context_menu.h"
#include "dynamic_menu.h"
#include "rename_object_dialog.h"

#include <k3dsdk/application.h>
#include <k3dsdk/classes.h>
#include <k3dsdk/icommand_node.h>
#include <k3dsdk/idag.h>
#include <k3dsdk/idocument.h>
#include <k3dsdk/idocument_plugin_factory.h>
#include <k3dsdk/imesh_sink.h>
#include <k3dsdk/imesh_source.h>
#include <k3dsdk/iobject_collection.h>
#include <k3dsdk/itime_sink.h>
#include <k3dsdk/itransform_sink.h>
#include <k3dsdk/itransform_source.h>
#include <k3dsdk/iuser_interface.h>
#include <k3dsdk/iviewport.h>
#include <k3dsdk/iviewport_host.h>
#include <k3dsdk/objects.h>
#include <k3dsdk/plugins.h>
#include <k3dsdk/property.h>
#include <k3dsdk/selection.h>
#include <k3dsdk/state_change_set.h>
#include <k3dsdk/time_source.h>
#include <k3dsdk/utility.h>
#include <k3dsdk/viewport.h>

//#include <sdpgtk/sdpgtkevents.h>

namespace k3d
{

namespace context_menu
{

////////////////////////////////////////////////////////////////////
// object::implementation

class object::implementation :
	public SigC::Object
{
public:
	implementation(idocument& Document) :
		m_document(Document)
	{
	}

	~implementation()
	{
	}
			
	void show(icommand_node* const ParentCommandNode, iobject& Object)
	{
		m_view_filter_by_type_menu = dynamic_menu::control<>();
		m_alter_mesh_menu = dynamic_menu::control<>();
		m_filter_transform_menu = dynamic_menu::control<>();
		m_filter_mesh_menu = dynamic_menu::control<>();
		m_context_menu = dynamic_menu::control<>();

		// Basic functionality common to all objects ...
		iobject* const object = &Object;
		if(object)
			{
				m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_edit_object", "Edit " + object->name() + " Properties", SigC::bind(SigC::slot(*this, &implementation::on_edit_object), object)));
				m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_rename_object", "Rename " + object->name(), SigC::bind(SigC::slot(*this, &implementation::on_rename_object), object)));
				m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_delete_object", "Delete " + object->name(), SigC::bind(SigC::slot(*this, &implementation::on_delete_object), object)));

				// If the object can be hidden ...
				iproperty* const viewport_visible = get_typed_property<bool>(*object, "viewport_visible");
				if(viewport_visible)
					{
						m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_hide", "Hide", SigC::bind(SigC::slot(*this, &implementation::on_hide), object)));
						m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_hide_others", "Hide Others", SigC::bind(SigC::slot(*this, &implementation::on_hide_others), object)));
					}
			}
		
		// If this is a viewport, provide operations for the viewport host (if any)
		iviewport* const viewport = dynamic_cast<iviewport*>(object);
		iobject* const viewport_host_object = dynamic_cast<iobject*>(viewport ? viewport->host() : static_cast<iviewport_host*>(0));
		if(viewport_host_object)
			{
				m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_edit_host", "Edit " + viewport_host_object->name() + " Properties", SigC::bind(SigC::slot(*this, &implementation::on_edit_object), viewport_host_object)));
				m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_rename_host", "Rename " + viewport_host_object->name(), SigC::bind(SigC::slot(*this, &implementation::on_rename_object), viewport_host_object)));
			}

		// If this is a property collection, add an option to connect properties ...
		iproperty_collection* const property_collection = dynamic_cast<iproperty_collection*>(object);
		if(property_collection)
			{
				m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_set_connections", "Set Connections", SigC::bind(SigC::slot(*this, &implementation::on_set_connections), object)));
			}

		// If the object is a mesh data sink ...
		imesh_sink* const mesh_sink = dynamic_cast<imesh_sink*>(object);
		if(mesh_sink)
			{
				factories_t filters;
				const factories_t data_source_filters = plugins<imesh_source >();
				const factories_t data_sink_filters = plugins<imesh_sink >();
				std::set_intersection(data_source_filters.begin(), data_source_filters.end(), data_sink_filters.begin(), data_sink_filters.end(), std::inserter(filters, filters.end()));
				
				// Volunteer to insert a filter between it and its source (but only if there isn't a partial selection) ...
				k3d::mesh* const mesh = boost::any_cast<k3d::mesh*>(k3d::get_property_value(m_document.dag(), mesh_sink->mesh_sink_input()));
				if(!mesh || !k3d::contains_selection(*mesh))
					{
						if(!filters.empty())
							{
								for(factories_t::const_iterator filter = filters.begin(); filter != filters.end(); ++filter)
									m_filter_mesh_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_filter_mesh_" + (*filter)->name(), (*filter)->name(), SigC::bind(SigC::bind(SigC::slot(*this, &implementation::on_filter_mesh), *filter), object)));
								m_filter_mesh_menu.build();

								m_context_menu.push_back(dynamic_menu::item("Filter Mesh ...", m_filter_mesh_menu));
							}
					}

				// Volunteer make permanent changes to the source ...
				if(!filters.empty())
					{
						for(factories_t::const_iterator filter = filters.begin(); filter != filters.end(); ++filter)
							m_alter_mesh_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_alter_mesh_" + (*filter)->name(), (*filter)->name(), SigC::bind(SigC::bind(SigC::slot(*this, &implementation::on_alter_mesh), *filter), object)));
						m_alter_mesh_menu.build();

						m_context_menu.push_back(dynamic_menu::item("Alter Mesh ...", m_alter_mesh_menu));
					}
			}

		// If the object is a transform data sink ...
		if(dynamic_cast<itransform_sink*>(object))
			{
				// Volunteer to add a new transformation filter ...
				const factories_t data_source_filters = plugins<itransform_source >();
				const factories_t data_sink_filters = plugins<itransform_sink >();
				
				factories_t filters;
				std::set_intersection(data_source_filters.begin(), data_source_filters.end(), data_sink_filters.begin(), data_sink_filters.end(), std::inserter(filters, filters.end()));
			
				if(!data_source_filters.empty())
					{
						for(factories_t::const_iterator filter = data_source_filters.begin(); filter != data_source_filters.end(); ++filter)
							m_filter_transform_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_filter_transform_" + (*filter)->name(), (*filter)->name(), SigC::bind(SigC::bind(SigC::slot(*this, &implementation::on_add_parent), *filter), object)));
						m_filter_transform_menu.build();

						m_context_menu.push_back(dynamic_menu::item("Add Parent ...", m_filter_transform_menu));
					}

/*
				if(!filters.empty())
					{
						for(factories_t::const_iterator filter = filters.begin(); filter != filters.end(); ++filter)
							m_filter_transform_menu.push_back(dynamic_menu::item((*filter)->name(), SigC::bind(SigC::bind(SigC::slot(*this, &implementation::on_filter_transform), *filter), object)));
						m_filter_transform_menu.build();

						m_context_menu.push_back(dynamic_menu::item());
						m_context_menu.push_back(dynamic_menu::item("Filter Transform ...", m_filter_transform_menu));
					}
*/
			}

		// Add some handy transformation-related stuff ...
		if(object)
			{
				if(get_typed_property<vector3>(*object, "position"))
					m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_reset_position", "Reset " + object->name() + " Position", SigC::bind(SigC::slot(*this, &implementation::on_reset_position), object)));
				
				if(get_typed_property<angle_axis>(*object, "orientation"))
					m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_reset_orientation", "Reset " + object->name() + " Orientation", SigC::bind(SigC::slot(*this, &implementation::on_reset_orientation), object)));
				
				if(get_typed_property<vector3>(*object, "scale"))
					m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_reset_scale", "Reset " + object->name() + " Scale", SigC::bind(SigC::slot(*this, &implementation::on_reset_scale), object)));
			}

		m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_show_all", "Show All", SigC::slot(*this, &implementation::on_show_all)));
		m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_deselect_all", "Deselect All", SigC::slot(*this, &implementation::on_deselect_all)));

/*
		// Sorting / layout options ...
		m_context_menu.push_back(dynamic_menu::item());
		m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_sort_by_label", "Sort by Label", SigC::slot(*this, &implementation::on_sort_by_label)));
		m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_sort_by_type", "Sort by Type", SigC::slot(*this, &implementation::on_sort_by_type)));

		// Filtering options ...
		m_context_menu.push_back(dynamic_menu::item());
		m_context_menu.push_back(dynamic_menu::item(ParentCommandNode, "context_show_all_objects", "Show All Objects", SigC::slot(*this, &implementation::on_show_all_objects)));

		static dynamic_menu::object<>* filter_by_type_menu = 0;
		if(!filter_by_type_menu)
			{
				filter_by_type_menu = new dynamic_menu::object<>();
				std::vector<iplugin_factory*> factories(application().plugins().begin(), application().plugins().end());
				std::sort(factories.begin(), factories.end(), detail::sort_by_name());
				
				for(std::vector<iplugin_factory*>::const_iterator factory = factories.begin(); factory != factories.end(); ++factory)
					{
						if(!dynamic_cast<idocument_plugin_factory*>(*factory))
							continue;

						filter_by_type_menu->push_back(dynamic_menu::item(ParentCommandNode, "context_show_" + (*factory)->name(), (*factory)->name(), SigC::bind(SigC::slot(*this, &implementation::on_filter_by_type), (*factory)->class_id())));
					}
			}

		m_view_filter_by_type_menu = *filter_by_type_menu;
		m_view_filter_by_type_menu.build();
		m_context_menu.push_back(dynamic_menu::item("Filter by Type ...", m_view_filter_by_type_menu));
*/

		// Make the menu visible ...
		m_context_menu.build();
		m_context_menu.popup();
	}
	
private:
	void on_edit_object(iobject* const Object)
	{
		return_if_fail(Object);

		if(application().user_interface())
			application().user_interface()->show(*Object);
	}

	void on_rename_object(iobject* const Object)
	{
		return_if_fail(Object);
		show_rename_object_dialog(*Object);
	}

	void on_delete_object(iobject* const Object)
	{
		return_if_fail(Object);
		
		record_state_change_set changeset(m_document, "Delete " + Object->name());

		objects_t objects;
		objects.insert(Object);
		
		delete_objects(m_document, objects);
		
		viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
	}
	
	void on_set_connections(iobject* const Object)
	{
		return_if_fail(Object);
		
		create_connect_properties_dialog(m_document, Object, 0, 0, 0);
	}

	void on_hide(iobject* const Object)
	{
		return_if_fail(Object);
		set_property_value(*Object, "viewport_visible", false);
		set_property_value(*Object, "render_final", false);
	}

	void on_hide_others(iobject* const Object)
	{
		return_if_fail(Object);

		iobject_collection::objects_t objects = m_document.objects().collection();
		objects.erase(Object);

		for(iobject_collection::objects_t::iterator object = objects.begin(); object != objects.end(); ++object)
			{
				set_property_value(**object, "viewport_visible", false);
				set_property_value(**object, "render_final", false);
			}
	}

	void on_deselect_all()
	{
		deselect_all(m_document);
		m_document.set_mouse_focus(0);

		viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
	}

	void on_show_all()
	{
		iobject_collection::objects_t objects = m_document.objects().collection();

		for(iobject_collection::objects_t::iterator object = objects.begin(); object != objects.end(); ++object)
			{
				set_property_value(**object, "viewport_visible", true);
				set_property_value(**object, "render_final", true);
			}
	}

	void on_reset_position(iobject* const Transformable)
	{
		assert(Transformable);

		set_property_value(*Transformable, "position", vector3(0, 0, 0));
		viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
	}

	void on_reset_orientation(iobject* const Transformable)
	{
		assert(Transformable);

		set_property_value(*Transformable, "orientation", angle_axis());
		viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
	}

	void on_reset_scale(iobject* const Transformable)
	{
		assert(Transformable);

		set_property_value(*Transformable, "scale", vector3(1, 1, 1));
		viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
	}

	void on_filter_mesh(iobject* const Object, iplugin_factory* const Filter)
	{
		assert(Object);
		assert(Filter);

		// Get the upstream and downstream properties ...
		imesh_sink* const downstream_sink = dynamic_cast<imesh_sink*>(Object);
		return_if_fail(downstream_sink);

		iproperty& downstream_input = downstream_sink->mesh_sink_input();

		iproperty* const upstream_output = m_document.dag().dependency(downstream_input);
		return_if_fail(upstream_output);

		// This block is recorded for undo purposes ...
		{
			record_state_change_set changeset(m_document, "Add Filter " + Filter->name());

			// Create our filter object ...
			iobject* const filter = create_document_plugin(*Filter, m_document, unique_name(m_document.objects(), Filter->name()));
			return_if_fail(filter);

			// Get its input and output properties ...
			imesh_sink* const filter_sink = dynamic_cast<imesh_sink*>(filter);
			return_if_fail(filter_sink);
			imesh_source* const filter_source = dynamic_cast<imesh_source*>(filter);
			return_if_fail(filter_source);
			
			// Insert the filter into the DAG ...
			idag::dependencies_t dependencies;
			dependencies.insert(std::make_pair(&filter_sink->mesh_sink_input(), upstream_output));
			dependencies.insert(std::make_pair(&downstream_input, &filter_source->mesh_source_output()));
			m_document.dag().set_dependencies(dependencies);
	
			// Display the filter properties ...
			create_auto_object_dialog(*filter);
		}

		viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
	}

	void on_alter_mesh(iobject* const Object, iplugin_factory* const Tool)
	{
		assert(Object);
		assert(Tool);

		// Get the upstream and downstream properties for this object ...
		imesh_sink* const downstream_sink = dynamic_cast<imesh_sink*>(Object);
		return_if_fail(downstream_sink);
		
		iproperty& downstream_input = downstream_sink->mesh_sink_input();
		iproperty* const upstream_output = m_document.dag().dependency(downstream_input);
		return_if_fail(upstream_output);		

		// Create our tool object ...
		iobject* const tool = create_document_plugin(*Tool, m_document, Tool->name());
		return_if_fail(tool);

		// Get its input and output properties ...
		imesh_sink* const tool_sink = dynamic_cast<imesh_sink*>(tool);
		return_if_fail(tool_sink);
		imesh_source* const tool_source = dynamic_cast<imesh_source*>(tool);
		return_if_fail(tool_source);

		// Display the filter properties ...
		create_auto_tool_dialog(*tool, *upstream_output, tool_sink->mesh_sink_input(), tool_source->mesh_source_output(), downstream_input);

		viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
	}

	void on_add_parent(iobject* const Object, iplugin_factory* const Filter)
	{
		assert(Object);
		assert(Filter);

		// Get the upstream and downstream properties ...
		itransform_sink* const downstream_sink = dynamic_cast<itransform_sink*>(Object);
		return_if_fail(downstream_sink);

		iproperty& downstream_input = downstream_sink->transform_sink_input();

/*
		iproperty* const upstream_output = m_document.dag().dependency(downstream_input);
		return_if_fail(upstream_output);
*/
		// This block is recorded for undo purposes ...
		{
			record_state_change_set changeset(m_document, "Add Transform " + Filter->name());

			// Create our filter object ...
			iobject* const filter = create_document_plugin(*Filter, m_document, Filter->name());
			return_if_fail(filter);

			// Get its input and output properties ...
/*
			imesh_sink* const filter_sink = dynamic_cast<imesh_sink*>(filter);
			return_if_fail(filter_sink);
*/
			itransform_source* const filter_source = dynamic_cast<itransform_source*>(filter);
			return_if_fail(filter_source);
			
			// Insert the filter into the DAG ...
			idag::dependencies_t dependencies;
/*
			dependencies.insert(std::make_pair(&filter_sink->transform_sink_input(), upstream_output));
*/
			dependencies.insert(std::make_pair(&downstream_input, &filter_source->transform_source_output()));
			
			itime_sink* const time_sink = dynamic_cast<itime_sink*>(filter);
			if(time_sink)
				dependencies.insert(std::make_pair(&time_sink->time_sink_input(), get_time(m_document)));
			
			m_document.dag().set_dependencies(dependencies);
	
			// Display the filter properties ...
			create_auto_object_dialog(*filter);
		}

		viewport::redraw_all(m_document, iviewport::ASYNCHRONOUS);
	}

	idocument& m_document;

	// Popup context menus
	dynamic_menu::control<> m_context_menu;
	dynamic_menu::control<> m_filter_mesh_menu;
	dynamic_menu::control<> m_alter_mesh_menu;
	dynamic_menu::control<> m_filter_transform_menu;
	dynamic_menu::control<> m_view_filter_by_type_menu;
};

////////////////////////////////////////////////////////////////////
// object

object::object(idocument& Document) :
	m_implementation(new implementation(Document))
{
}

object::~object()
{
	delete m_implementation;
}

void object::show(icommand_node* const ParentCommandNode, iobject& Object)
{
	m_implementation->show(ParentCommandNode, Object);
}

} // namespace context_menu

} // namespace k3d

