// 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 Implements k3d::object_chooser, which provides a user interface for either creating new, or choosing existing, K-3D objects
		\author Tim Shead (tshead@k-3d.com)
*/

#include "button.h"
#include "gtkml.h"
#include "object_chooser.h"

#include <k3dsdk/application.h>
#include <k3dsdk/high_res_timer.h>
#include <k3dsdk/idocument.h>
#include <k3dsdk/idocument_plugin_factory.h>
#include <k3dsdk/iobject.h>
#include <k3dsdk/iobject_property.h>
#include <k3dsdk/istate_recorder.h>
#include <k3dsdk/iuser_interface.h>
#include <k3dsdk/objects.h>
#include <k3dsdk/ioptions.h>
#include <k3dsdk/plugins.h>
#include <k3dsdk/state_change_set.h>
#include <k3dsdk/utility.h>

#include <sdpgtk/sdpgtkevent.h>
#include <boost/lexical_cast.hpp>

namespace
{

const std::string control_label = "label";
const std::string control_choose = "choose";
const std::string control_selectnone = "selectnone";
const std::string control_selectobject = "selectobject";
const std::string control_newobject = "newobject";

/// Provides an implementation of k3d::object_chooser::iselection_filter that filters based on a k3d::iproperty object
class property_filter :
	public k3d::object_chooser::iselection_filter
{
public:
	property_filter(k3d::iproperty& Property) :
		m_object_property(dynamic_cast<k3d::iobject_property*>(&Property))
	{
	}

	bool allow_none()
	{
		return m_object_property && m_object_property->allow_none();
	}

	bool allow(k3d::iplugin_factory& Factory)
	{
		return m_object_property && m_object_property->allow(Factory);
	}

	bool allow(k3d::iobject& Object)
	{
		return m_object_property && m_object_property->allow(Object);
	}

private:
	k3d::iobject_property* const m_object_property;
};

} // namespace

namespace k3d
{

namespace object_chooser
{

/////////////////////////////////////////////////////////////////////////////
// control

control::control(k3d::iunknown* const CommandNodeParent, const std::string CommandNodeName) :
	base(CommandNodeParent, CommandNodeName)
{
	// Create our UI template ...
	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>"
					"<k3dbutton name=\"edit_object\">"
						"<arrow direction=\"right\" shadowtype=\"in\"/>"
					"</k3dbutton>"
				"</hbox>"
			"</eventbox>"
		"</gtkml>\n");

	return_if_fail(load_gtkml(uitemplate, "object chooser builtin template", *this));

	if(get_button("edit_object"))
		get_button("edit_object")->signal_activate().connect(SigC::slot(*this, &control::on_edit_object));

	RootWidget().Show();
}

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

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

const std::string control::CustomType() const
{
	return std::string("k3dobjectchooser");
}

bool control::Create(sdpGtkIObjectContainer* const ObjectContainer, sdpxml::Document& Document, sdpxml::Element& Element)
{
	// Sanity checks ...
	assert_warning(ObjectContainer);
	assert_warning(Element.Name() == "k3dobjectchooser");

	return true;
}

bool control::attach(std::auto_ptr<iselection_filter> SelectionFilter, std::auto_ptr<idata_proxy> Data, k3d::istate_recorder* const StateRecorder, const std::string StateChangeName)
{
	// Sanity checks ...
	return_val_if_fail(SelectionFilter.get(), false);
	return_val_if_fail(Data.get(), false);

	// Take ownership of the selection filter and data source ...
	m_selection_filter = SelectionFilter;
	m_data = Data;

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

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

	// We want to be notified if the data source changes ...
	m_data->changed_signal().connect(SigC::slot(*this, &control::update));

	// We want to be notified whenever the set of objects in the owning document change
	m_data->document().objects().add_objects_signal().connect(SigC::slot(*this, &control::on_objects_added));
	m_data->document().objects().remove_objects_signal().connect(SigC::slot(*this, &control::on_objects_removed));

	return true;
}

bool control::execute_command(const std::string& Command, const std::string& Arguments)
{
	if(Command == control_newobject)
		{
			// Look for an object info with the correct name ...
			const k3d::factories_t plugins(k3d::plugins(Arguments));
			return_val_if_fail(1 == plugins.size(), false);
			k3d::iplugin_factory* const factory = *plugins.begin();

			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->factory != factory)
						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_selectobject)
		{
			// Find an object with the correct name ...
			const objects_t objects = k3d::find_objects(m_data->document().objects(), Arguments);
			return_val_if_fail(1 == objects.size(), false);
			iobject* const object = *objects.begin();

			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->object != object)
						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->object || menu_item->factory)
						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);
}

void control::update()
{
	// Sanity checks ...
	return_if_fail(m_data.get() && m_selection_filter.get());

	// Start our menu over from scratch ...
	m_menu.clear();

	// Display the current selection ...
	Label(control_label).SetText((m_data->object() ? m_data->object()->name() : "--None--").c_str());
}

void control::on_objects_added(const iobject_collection::objects_t&)
{
	update();
}

void control::on_objects_removed(const iobject_collection::objects_t&)
{
	update();
}

void control::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 control::on_destroy()
{
	DisconnectAllEvents();
	Clear();
}

void control::set_object(iobject* const Object, const std::string UndoName)
{
	// If nothing changed, we're done ...
	if(Object == m_data->object())
		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_data->set_object(Object);

	// Turn this into an undo/redo -able event ...
	if(m_StateRecorder)
		m_StateRecorder->commit_change_set(m_StateRecorder->stop_recording(), UndoName);
}

void control::on_choose()
{
	// Sanity checks ...
	return_if_fail(m_selection_filter.get());
	return_if_fail(m_data.get());
	
	// 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 ...
			if(m_selection_filter->allow_none())
				{
					m_menu.push_back(menu_item("--None--", SigC::slot(*this, &control::on_none)));
					++index;
				}

			// Insert choices for creating new objects ...
			const k3d::factories_t plugins(k3d::application().plugins());
			for(k3d::factories_t::const_iterator factory = plugins.begin(); factory != plugins.end(); ++factory)
				{
					// Filter choices ...
					if(!m_selection_filter->allow(**factory))
						continue;

					// Create a "new" menu item for each factory ...
					m_menu.push_back(menu_item("--New " + (*factory)->name() + "--", *factory, SigC::bind(SigC::slot(*this, &control::on_create_new_object), *factory)));
					++index;
				}

			// Insert choices for selecting existing objects ...
			for(k3d::iobject_collection::objects_t::const_iterator handle = m_data->document().objects().collection().begin(); handle != m_data->document().objects().collection().end(); handle++)
				{
					iobject* const object = *handle;

					// Filter choices ...
					if(!m_selection_filter->allow(*object))
						continue;

					// Create a menu item for each object ...
					m_menu.push_back(menu_item(object->name(), object, SigC::bind(SigC::slot(*this, &control::on_select_existing_object), object)));

					// See if this is the currently selected object ...
					if(object == m_data->object())
						selected_item = index;

					++index;
				}

			m_menu.build();
		}

	m_menu.popup();
}

void control::on_edit_object()
{
	// Make sure we've got some storage, first!
	return_if_fail(m_data.get());

	// Make it happen ...
	if(m_data->object() && k3d::application().user_interface())
		k3d::application().user_interface()->show(*m_data->object());
}

void control::on_none()
{
	// Make sure we've got some storage, first!
	return_if_fail(m_data.get());

	// Record the command for posterity (tutorials) ...
	k3d::record_command(*this, k3d::icommand_node::command_t::USER_INTERFACE, control_selectnone);

	// Update the data object ...
	set_object(0, "Select None");
}

void control::on_select_existing_object(iobject* const Object)
{
	// Sanity checks ...
	return_if_fail(Object);
	// Make sure we've got some storage, first!
	return_if_fail(m_data.get());

	// Record things for posterity ...
	k3d::record_command(*this, k3d::icommand_node::command_t::USER_INTERFACE, control_selectobject, Object->name());

	// Update underlying data ...
	set_object(Object, "Select " + Object->name());
}

void control::on_create_new_object(k3d::iplugin_factory* const Factory)
{
	// Make sure we've got some storage, first!
	return_if_fail(m_data.get());

	// Sanity checks ...
	return_if_fail(Factory);

	// Record things for posterity ...
	k3d::record_command(*this, k3d::icommand_node::command_t::USER_INTERFACE, control_newobject, Factory->name());

	// Turn this into an undo/redo -able event ...
	if(m_StateRecorder)
		m_StateRecorder->start_recording(k3d::create_state_change_set());

	// Create the new object ...
	iobject* const object = k3d::create_document_plugin(*Factory, m_data->document(), k3d::unique_name(m_data->document().objects(), Factory->name()));
	return_if_fail(object);

	// Set the new value ...
	m_data->set_object(object);

	// Turn this into an undo/redo -able event ...
	if(m_StateRecorder)
		m_StateRecorder->commit_change_set(m_StateRecorder->stop_recording(), "Create new " + Factory->name());

	// Edit the new object ...
	if(k3d::application().user_interface())
		k3d::application().user_interface()->show(*object);
}

std::auto_ptr<iselection_filter> filter(iproperty& Data)
{
	return std::auto_ptr<iselection_filter>(new property_filter(Data));
}

} // namespace object_chooser

} // namespace k3d


