// 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 the k3d::spin_button class, which provides a UI for numeric quantities
		\author Tim Shead <tshead@k-3d.com>
		\author Dan Erikson <derikson@montana.com>
*/

#include "gtkml.h"
#include "spin_button.h"

#include <k3dsdk/application.h>
#include <k3dsdk/state_change_set.h>
#include <k3dsdk/iproperty.h>
#include <k3dsdk/istate_recorder.h>
#include <k3dsdk/iwritable_property.h>
#include <k3dsdk/measurement.h>
#include <k3dsdk/ioptions.h>
#include <k3dsdk/string_modifiers.h>

#include <sdpgtk/sdpgtkloopevents.h>
#include <sdpgtk/sdpgtkutility.h>

#include <iomanip>

using namespace k3d::measurement;

namespace
{

const std::string control_value = "value";
const std::string control_up = "up";
const std::string control_down = "down";

/////////////////////////////////////////////////////////////////////////////
// data_proxy

/// Specialization of k3d::spin_button::data_proxy for use with k3d::iproperty objects
class property_proxy :
	public k3d::spin_button::idata_proxy
{
public:
	typedef k3d::iproperty data_t;

	explicit property_proxy(data_t& Data) :
		m_readable_data(Data),
		m_writable_data(dynamic_cast<k3d::iwritable_property*>(&Data))
	{
	}

	bool writable()
	{
		return m_writable_data ? true : false;
	}

	double value()
	{
		const std::type_info& type = m_readable_data.type();
		if(type == typeid(double))
			return boost::any_cast<double>(m_readable_data.value());
		else if(type == typeid(float))
			return boost::any_cast<float>(m_readable_data.value());
		else if(type == typeid(long))
			return boost::any_cast<long>(m_readable_data.value());
		else if(type == typeid(unsigned long))
			return boost::any_cast<unsigned long>(m_readable_data.value());
		else if(type == typeid(int))
			return boost::any_cast<int>(m_readable_data.value());
		else if(type == typeid(unsigned int))
			return boost::any_cast<unsigned int>(m_readable_data.value());
		else
			std::cerr << __PRETTY_FUNCTION__ << ": unsupported property type" << std::endl;

		return 0.0;
	}

	void set_value(const double Value)
	{
		return_if_fail(m_writable_data);

		const std::type_info& type = m_readable_data.type();
		if(type == typeid(double))
			m_writable_data->set_value(Value);
		else if(type == typeid(float))
			m_writable_data->set_value(static_cast<float>(Value));
		else if(type == typeid(long))
			m_writable_data->set_value(static_cast<long>(Value));
		else if(type == typeid(unsigned long))
			m_writable_data->set_value(static_cast<unsigned long>(Value));
		else if(type == typeid(int))
			m_writable_data->set_value(static_cast<int>(Value));
		else if(type == typeid(unsigned int))
			m_writable_data->set_value(static_cast<unsigned int>(Value));
		else
			std::cerr << __PRETTY_FUNCTION__ << ": unsupported property type" << std::endl;
	}

	changed_signal_t& changed_signal()
	{
		return m_readable_data.changed_signal();
	}

private:
	property_proxy(const property_proxy& RHS);
	property_proxy& operator=(const property_proxy& RHS);
	~property_proxy() {}

	data_t& m_readable_data;
	k3d::iwritable_property* const m_writable_data;
};

} // namespace

namespace k3d
{

namespace spin_button
{

std::auto_ptr<idata_proxy> proxy(iproperty& Data)
{
	return std::auto_ptr<idata_proxy>(new property_proxy(Data));
}

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

control::control(iunknown* const CommandNodeParent, const std::string CommandNodeName) :
	base(CommandNodeParent, CommandNodeName),
	m_scroll_mode(NONE),
	m_scrolling_event(0),
	m_start_scrolling_event(0),
	m_faster_scrolling_event(0),
	m_fastest_scrolling_event(0),
	m_step_increment(0.01),
	m_precision(2),
	m_units(&typeid(scalar))
{
	// Create and load our UI template ...
	std::istringstream uitemplate(
		"<gtkml>"
			"<eventbox>"
				"<hbox homogeneous=\"false\">"
					"<event signal=\"destroy\" name=\"destroy\"/>"

					"<entry expand=\"true\" fill=\"true\" name=\"value\">"
						"<event signal=\"button-press-event\" name=\"value_focus_in\" after=\"true\"/>"
						"<event signal=\"focus-in-event\" name=\"value_focus_in\" after=\"true\"/>"
						"<event signal=\"focus-out-event\" name=\"value_focus_out\"/>"
						"<event signal=\"activate\" name=\"value_activate\"/>"
					"</entry>"

					"<vbox homogeneous=\"true\">"
						"<button fill=\"true\" name=\"up\">"
							"<event signal=\"pressed\" name=\"up_pressed\"/>"
							"<event signal=\"released\" name=\"up_released\"/>"
							"<arrow direction=\"up\"/>"
						"</button>"
						"<button fill=\"true\" name=\"down\">"
							"<event signal=\"pressed\" name=\"down_pressed\"/>"
							"<event signal=\"released\" name=\"down_released\"/>"
							"<arrow direction=\"down\"/>"
						"</button>"
					"</vbox>"
				"</hbox>"
			"</eventbox>"
		"</gtkml>\n");

	return_if_fail(load_gtkml(uitemplate, "spin button builtin template", *this));

	RootWidget().Show();

	// Make sure the spin buttons can't get the focus (makes tabbing difficult)
	Widget(control_up).Object()->flags &= ~GTK_CAN_FOCUS;
	Widget(control_down).Object()->flags &= ~GTK_CAN_FOCUS;
}

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("k3dspinbutton");
}

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

	// Get the requested precision ...
	m_precision = sdpxml::GetAttribute<unsigned long>(Element, "precision", 2);
	sdpGtkMarkAttribute(Document, Element, "precision");

	// Get the display data type, so we can show real-world units-of-measure ...
	const std::string datatype = sdpxml::GetAttribute<std::string>(Element, "datatype", "");
	sdpGtkMarkAttribute(Document, Element, "datatype");

	if(datatype.empty())
		m_units = &typeid(scalar);
	else if(datatype == "angle")
		m_units = &typeid(angle);
	else if(datatype == "area")
		m_units = &typeid(area);
	else if(datatype == "distance")
		m_units = &typeid(distance);
	else if(datatype == "force")
		m_units = &typeid(force);
	else if(datatype == "mass")
		m_units = &typeid(mass);
	else if(datatype == "pressure")
		m_units = &typeid(pressure);
	else if(datatype == "time")
		m_units = &typeid(k3d::measurement::time);
	else if(datatype == "volume")
		m_units = &typeid(volume);
	else
		m_units = &typeid(scalar);

	// Now that we know our data type, get the step increment, allowing unit conversion, etc.
	std::string step_increment_text("1");
	sdpxml::ParseAttribute(Element, "stepincrement", step_increment_text);
	sdpGtkMarkAttribute(Document, Element, "stepincrement");
	k3d::measurement::parse(step_increment_text, m_step_increment, m_units);

	return true;
}

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

	// Take ownership of the data source ...
	m_data = Data;

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

	// Adjust our appearance for read-only data sources ...
	if(!m_data->writable())
		{
			Widget(control_up).Hide();
			Widget(control_down).Hide();
			Entry(control_value).SetEditable(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));

	return true;
}

void control::set_precision(const unsigned long Precision)
{
	m_precision = Precision;
}

void control::set_step_increment(const double StepIncrement)
{
	m_step_increment = StepIncrement;
}

void control::set_units(const std::type_info& Units)
{
	m_units = &Units;
}

bool control::execute_command(const std::string& Command, const std::string& Arguments)
{
	if(Command == control_value)
		{
			return_val_if_fail(InteractiveSetText(control_value.c_str(), Arguments.c_str(), k3d::application().options().tutorial_speed(), true), false);
			on_value_focus_out();
			return true;
		}

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

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

	format_edit_control(m_data->value());
}

void control::OnEvent(sdpGtkEvent* Event)
{
	// Sanity checks ...
	assert_warning(Event);

	if(Event->Name() == "scrolling")
		on_scrolling();
	else if(Event->Name() == "start_scrolling")
		on_start_scrolling();
	else if(Event->Name() == "faster_scrolling")
		on_faster_scrolling();
	else if(Event->Name() == "fastest_scrolling")
		on_fastest_scrolling();
	else if(Event->Name() == "value_focus_in")
		on_value_focus_in();
	else if(Event->Name() == "value_focus_out")
		on_value_focus_out();
	else if(Event->Name() == "value_activate")
		on_value_activate();
	else if(Event->Name() == "up_pressed")
		on_up_pressed();
	else if(Event->Name() == "up_released")
		on_up_released();
	else if(Event->Name() == "down_pressed")
		on_down_pressed();
	else if(Event->Name() == "down_released")
		on_down_released();
	else if(Event->Name() == "destroy")
		on_destroy();
	else
		base::OnEvent(Event);
}

void control::on_scrolling()
{
	switch(m_scroll_mode)
		{
			case NONE:
				cancel_scrolling();
				break;
			case INCREMENT:
				increment();
				break;
			case DECREMENT:
				decrement();
				break;
			default:
				return_if_fail(0);
		}
}

void control::on_start_scrolling()
{
	if(m_start_scrolling_event)
		{
			m_start_scrolling_event->Disconnect();
			DeleteEvent(m_start_scrolling_event);
			m_start_scrolling_event = 0;
		}

	m_scrolling_event = static_cast<sdpGtkEventTimeout*>(MapEvent("timeout", "scrolling", false, RootWidget(), false));
	m_scrolling_event->SetDuration(60);
	m_scrolling_event->Connect();
}

void control::on_faster_scrolling()
{
	if(m_faster_scrolling_event)
		{
			m_faster_scrolling_event->Disconnect();
			DeleteEvent(m_faster_scrolling_event);
			m_faster_scrolling_event = 0;
		}

	if(m_scrolling_event)
		m_scrolling_event->SetDuration(35);
}

void control::on_fastest_scrolling()
{
	if(m_fastest_scrolling_event)
		{
			m_fastest_scrolling_event->Disconnect();
			DeleteEvent(m_fastest_scrolling_event);
			m_fastest_scrolling_event = 0;
		}

	if(m_scrolling_event)
		m_scrolling_event->SetDuration(10);
}

void control::on_value_focus_in()
{
	// Make sure everything's selected ...
	Editable(control_value).SelectRegion(0, -1);
}

void control::on_value_focus_out()
{
	on_value_activate();
}

void control::on_value_activate()
{
	// Get rid of the selection ...
	Editable(control_value).SelectRegion(0, 0);

	// Sanity checks ...
	return_if_fail(m_data.get());

	// Get the edit control text ...
	const std::string new_text = Editable(control_value).GetText();
	// Default our results to the current value, in case it doesn't parse ...
	double new_value = m_data->value();
	// Parse the input expression into value converting it to SI units automatically (it can do mathematical expressions, too, for fun)
	if(!k3d::measurement::parse(new_text, new_value, m_units))
		{
			format_edit_control(new_value);
			return;
		}

	// If the value has changed, record it ...
	if(new_value != m_data->value())
		{
			push_editing();
			set_value(new_value);
			pop_editing(new_text);
		}
	else
		{
			format_edit_control(new_value);
		}
}

void control::on_up_pressed()
{
	// Sanity checks ...
	return_if_fail(m_data.get());

	// Let everyone know we're going to be making changes ...
	push_editing();

	// Make sure any manual edits get recorded ...
	const double new_value = read_edit_control();
	if(new_value != m_data->value())
		set_value(new_value);

	// Increment the current value ...
	increment();

	// Start the scrolling process ...
	start_scrolling(INCREMENT);
}

void control::on_up_released()
{
	// Sanity checks ...
	return_if_fail(m_data.get());

	// We're done making changes ...
	pop_editing();

	// Cancel any scrolling ...
	cancel_scrolling();
}

void control::on_down_pressed()
{
	// Sanity checks ...
	return_if_fail(m_data.get());

	// Let the world know we're going to be making changes ...
	push_editing();

	// Make sure any manual edits get recorded ...
	const double new_value = read_edit_control();
	if(new_value != m_data->value())
		set_value(new_value);

	// Decrement the current value ...
	decrement();

	// Start the scrolling process ...
	start_scrolling(DECREMENT);
}

void control::on_down_released()
{
	// Sanity checks ...
	return_if_fail(m_data.get());

	// We're done making changes ...
	pop_editing();

	// Cancel any scrolling ...
	cancel_scrolling();
}

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

void control::start_scrolling(const scroll_t ScrollMode)
{
	m_scroll_mode = ScrollMode;

	m_start_scrolling_event = static_cast<sdpGtkEventTimeout*>(MapEvent("timeout", "start_scrolling", false, RootWidget(), false));
	m_start_scrolling_event->SetDuration(200);

	m_faster_scrolling_event = static_cast<sdpGtkEventTimeout*>(MapEvent("timeout", "faster_scrolling", false, RootWidget(), false));
	m_faster_scrolling_event->SetDuration(400);

	m_fastest_scrolling_event = static_cast<sdpGtkEventTimeout*>(MapEvent("timeout", "fastest_scrolling", false, RootWidget(), false));
	m_fastest_scrolling_event->SetDuration(600);

	m_start_scrolling_event->Connect();
	m_faster_scrolling_event->Connect();
	m_fastest_scrolling_event->Connect();
}

void control::cancel_scrolling()
{
	m_scroll_mode = NONE;

	if(m_start_scrolling_event)
		{
			m_start_scrolling_event->Disconnect();
			DeleteEvent(m_start_scrolling_event);
			m_start_scrolling_event = 0;
		}

	if(m_faster_scrolling_event)
		{
			m_faster_scrolling_event->Disconnect();
			DeleteEvent(m_faster_scrolling_event);
			m_faster_scrolling_event = 0;
		}

	if(m_fastest_scrolling_event)
		{
			m_fastest_scrolling_event->Disconnect();
			DeleteEvent(m_fastest_scrolling_event);
			m_fastest_scrolling_event = 0;
		}

	if(m_scrolling_event)
		{
			m_scrolling_event->Disconnect();
			DeleteEvent(m_scrolling_event);
			m_scrolling_event = 0;
		}
}

void control::format_edit_control(const double Value)
{
	// Format the value ...
	std::ostringstream buffer;
	buffer << std::fixed << std::setprecision(m_precision);
	k3d::measurement::format(buffer, Value, *m_units);

	// Put it into our edit control ...
	Editable(control_value).SetText(buffer.str().c_str());
}

const double control::read_edit_control()
{
	// Sanity checks ...
	return_val_if_fail(m_data.get(), 0.0);

	// Default our results to the current value, in case it doesn't parse ...
	double value = m_data->value();

	// Parse the input expression into value converting it to SI units automatically (it can do mathematical expressions, too, for fun)
	k3d::measurement::parse(Editable(control_value).GetText(), value, m_units);

	return value;
}

void control::push_editing()
{
	// Sanity checks ...
	return_if_fail(m_data.get());

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

void control::increment()
{
	// Sanity checks ...
	return_if_fail(m_data.get());

	// Update everything ...
	set_value(m_data->value() + m_step_increment);
}

void control::decrement()
{
	// Sanity checks ...
	return_if_fail(m_data.get());

	// Update everything ...
	set_value(m_data->value() - m_step_increment);
}

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

	// Update storage with the new value ...
	m_data->set_value(Value);
}

void control::pop_editing()
{
	// Sanity checks ...
	return_if_fail(m_data.get());

	pop_editing(k3d::to_string(m_data->value()));
}

void control::pop_editing(const std::string NewValue)
{
	// Sanity checks ...
	return_if_fail(m_data.get());

	// Record the command for posterity (tutorials) ...
	k3d::application().command_signal().emit(this, k3d::icommand_node::command_t::USER_INTERFACE, control_value, NewValue);

	// Turn this into an undo/redo -able event ...
	if(m_StateRecorder)
		m_StateRecorder->commit_change_set(m_StateRecorder->stop_recording(), m_StateChangeName + ' ' + '"' + NewValue + '"');
}

} // namespace spin_button

} // namespace k3d


