/*
    Copyright (C) 2004 Paul Davis
    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., 675 Mass Ave, Cambridge, MA 02139, USA.

    $Id: barcontroller.cc,v 1.9 2004/11/20 02:21:30 pauld Exp $
*/

#include <string>
#include <climits>
#include <cstdio>
#include <cmath>
#include <algorithm>

#include <midi++/controllable.h>

#include <gtkmmext/gtk_ui.h>
#include <gtkmmext/barcontroller.h>

#include "i18n.h"

using namespace std;
using namespace Gtk;
using namespace Gtkmmext;

BarController::BarController (Gtk::Adjustment& adj,
			      MIDI::Controllable *mc,
			      SigC::Slot2<void,char*,unsigned int> lc) 

	: adjustment (adj),
	  prompter (GTK_WIN_POS_MOUSE, 30000, false),
	  midi_control (mc),
	  label_callback (lc),
	  spinner (adjustment),
	  bind_button (2),
	  bind_statemask (GDK_CONTROL_MASK)

{			  
	_style = LeftToRight;
	grabbed = false;
	switching = false;
	switch_on_release = false;
	with_text = true;
	use_parent = false;

	set_shadow_type (GTK_SHADOW_NONE);

	initial_value = adjustment.get_value ();

	adjustment.value_changed.connect (slot (*this, &Gtk::Widget::queue_draw));
	adjustment.changed.connect (slot (*this, &Gtk::Widget::queue_draw));

	darea.add_events (GDK_BUTTON_RELEASE_MASK|
			  GDK_BUTTON_PRESS_MASK|
			  GDK_POINTER_MOTION_MASK|
			  GDK_ENTER_NOTIFY_MASK|
			  GDK_LEAVE_NOTIFY_MASK);

	darea.expose_event.connect (slot (*this, &BarController::expose));
	darea.motion_notify_event.connect (slot (*this, &BarController::motion));
	darea.button_press_event.connect (slot (*this, &BarController::button_press));
	darea.button_release_event.connect (slot (*this, &BarController::button_release));

	prompter.unmap_event.connect (slot (*this, &BarController::prompter_hiding));
	
	prompting = false;
	unprompting = false;
	
	if (mc) {
		mc->learning_started.connect (slot (*this, &BarController::midicontrol_prompt));
		mc->learning_stopped.connect (slot (*this, &BarController::midicontrol_unprompt));
	}

	spinner.activate.connect (slot (*this, &BarController::entry_activated));
	spinner.focus_out_event.connect (slot (*this, &BarController::entry_focus_out));
	spinner.set_digits (3);

	add (darea);
	show_all ();
}

void
BarController::set_bind_button_state (guint button, guint statemask)
{
	bind_button = button;
	bind_statemask = statemask;
}

void
BarController::get_bind_button_state (guint &button, guint &statemask)
{
	button = bind_button;
	statemask = bind_statemask;
}


gint
BarController::button_press (GdkEventButton* ev)
{
	switch (ev->button) {
	case 1:
		if (ev->type == GDK_2BUTTON_PRESS) {
			switch_on_release = true;
		} else {
			switch_on_release = false;
			Gtk::Main::grab_add (darea);
			grabbed = true;
			grab_x = ev->x;
			grab_window = ev->window;
			StartGesture ();
		}
		return TRUE;
		break;

	case 2:
	case 3:
		break;

	case 4:
	case 5:
		break;
	}

	return FALSE;
}

gint
BarController::button_release (GdkEventButton* ev)
{
	switch (ev->button) {
	case 1:
		if (switch_on_release) {
			Main::idle.connect (slot (*this, &BarController::switch_to_spinner));
			return TRUE;
		}

		if ((ev->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK)) == GDK_SHIFT_MASK) {
			adjustment.set_value (initial_value);
		} else {
			double scale;

			if (ev->state & (GDK_CONTROL_MASK|GDK_SHIFT_MASK) == (GDK_CONTROL_MASK|GDK_SHIFT_MASK)) {
				scale = 0.01;
			} else if (ev->state & GDK_CONTROL_MASK) {
				scale = 0.1;
			} else {
				scale = 1.0;
			}

			mouse_control (ev->x, ev->window, scale);
		}
		grabbed = false;
		Gtk::Main::grab_remove (darea);
		StopGesture ();
		break;

	case 2:
		if ((ev->state & bind_statemask) && bind_button == 2) {
			midi_learn ();
		} else {
			double fract;
			fract = ev->x / (darea.width() - 2.0);
			adjustment.set_value (adjustment.get_lower() + 
					      fract * (adjustment.get_upper() - adjustment.get_lower()));
		}
		return TRUE;

	case 3:
		if ((ev->state & bind_statemask) && bind_button == 3) {
			midi_learn ();
			return TRUE;
		}
		return FALSE;
		
	case 4:
		adjustment.set_value (adjustment.get_value() +
				      adjustment.get_step_increment());
		break;
	case 5:
		adjustment.set_value (adjustment.get_value() -
				      adjustment.get_step_increment());
		break;
	}

	return TRUE;
}

gint
BarController::motion (GdkEventMotion* ev)
{
	double scale;
	
	if (!grabbed) {
		return TRUE;
	}

	if ((ev->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK)) == GDK_SHIFT_MASK) {
		return TRUE;
	}

	if (ev->state & (GDK_CONTROL_MASK|GDK_SHIFT_MASK) == (GDK_CONTROL_MASK|GDK_SHIFT_MASK)) {
		scale = 0.01;
	} else if (ev->state & GDK_CONTROL_MASK) {
		scale = 0.1;
	} else {
		scale = 1.0;
	}

	return mouse_control (ev->x, ev->window, scale);
}

gint
BarController::mouse_control (double x, GdkWindow* window, double scaling)
{
	double fract;
	double delta;

	if (window != grab_window) {
		grab_x = x;
		grab_window = window;
		return TRUE;
	}

	delta = x - grab_x;
	grab_x = x;

	switch (_style) {
	case Line:
	case LeftToRight:
		fract = scaling * (delta / (darea.width() - 2));
		fract = min (1.0, fract);
		fract = max (-1.0, fract);
		adjustment.set_value (adjustment.get_value() + fract * (adjustment.get_upper() - adjustment.get_lower()));
		break;

	default:
		fract = 0.0;
	}
	
	
	return TRUE;
}

gint
BarController::expose (GdkEventExpose* event)
{
	Gdk_Window win (darea.get_window());
	Widget* parent;
	gint x1, x2, y1, y2;
	gint w, h;
	double fract;

	w = darea.width() - 2;
	h = darea.height() - 2;

	fract = ((adjustment.get_value() - adjustment.get_lower()) /
		 (adjustment.get_upper() - adjustment.get_lower()));
	
	switch (_style) {
	case Line:
		x1 = (gint) floor (w * fract);
		x2 = x1;
		y1 = 0;
		y2 = h - 1;

		if (use_parent) {
			parent = get_parent();
			
			if (parent) {
				win.draw_rectangle (parent->get_style()->get_fg_gc (parent->get_state()),
						    true,
						    0, 0, darea.width(), darea.height());
			}
		} else {
			win.draw_rectangle (get_style()->get_bg_gc (get_state()),
					    true,
					    0, 0, darea.width(), darea.height());
		}

		if (fract == 0.0) {
			win.draw_rectangle (get_style()->get_fg_gc (get_state()),
					    true, x1, 1, 2, darea.height() - 2);
		} else {
			win.draw_rectangle (get_style()->get_fg_gc (get_state()),
					    true, x1 - 1, 1, 3, darea.height() - 2);
		}
		break;

	case CenterOut:
		break;

	case LeftToRight:
		x1 = 0;
		x2 = (gint) floor (w * fract);
		y1 = 0;
		y2 = h - 1;

		win.draw_rectangle (get_style()->get_bg_gc (get_state()),
				    false,
				    0, 0, darea.width(), darea.height());

		/* draw active box */
		
		win.draw_rectangle (get_style()->get_fg_gc (get_state()),
				    true,
				    1 + x1,
				    1 + y1,
				    x2,
				    1 + y2);
		
		/* draw inactive box */

		win.draw_rectangle (get_style()->get_fg_gc (GTK_STATE_INSENSITIVE),
				    true,
				    1 + x2,
				    1 + y1,
				    w - x2,
				    1 + y2);

		break;

	case RightToLeft:
		break;
	case TopToBottom:
		break;
	case BottomToTop:
		break;
	}

	if (with_text) {
		/* draw label */
		
		char buf[64];
		buf[0] = '\0';

		label_callback (buf, 64);

		if (buf[0] != '\0') {
		
			gint width;
			gint lbearing;
			gint rbearing;
			gint ascent;
			gint descent;
			
			gdk_string_extents (get_style()->get_font(),
					    buf,
					    &lbearing,
					    &rbearing,
					    &width,
					    &ascent,
					    &descent);
			
			int xpos;
			
			/* don't allow the text to bump into the edges,
			   let alone overflow them.
			*/
			
			xpos = max (3, 1 + (x2 - (width/2)));
			xpos = min (darea.width() - width - 3, xpos);
			
			win.draw_text (get_style()->get_font(),
				       get_style()->get_text_gc (get_state()),
				       xpos,
				       1 + (h - (ascent+descent)) + 5,
				       buf,
				       strlen (buf));
		}
	}
	return TRUE;
}

void
BarController::set_with_text (bool yn)
{
	if (with_text != yn) {
		with_text = yn;
		queue_draw ();
	}
}

void
BarController::midicontrol_set_tip ()
{
	if (midi_control) {
		// Gtkmmext::UI::instance()->set_tip (&darea, midi_control->control_description());
	}
}

void
BarController::midi_learn()
{
	if (midi_control) {
		prompting = true;
		midi_control->learn_about_external_control ();
	}
}


void
BarController::midicontrol_prompt ()
{
	if (prompting) {
		string prompt = _("operate MIDI controller now");
		prompter.set_text (prompt);
		Gtkmmext::UI::instance()->touch_display (&prompter);

		unprompting = true;
		prompting = false;
	}
}

void
BarController::midicontrol_unprompt ()
{
	if (unprompting) {
		Gtkmmext::UI::instance()->touch_display (&prompter);
		
		unprompting = false;
	}
}

gint
BarController::prompter_hiding (GdkEventAny *ev)
{
	if (unprompting) {
		if (midi_control) {
			midi_control->stop_learning();
		}
		unprompting = false;
	}
	
	return FALSE;
}


void
BarController::set_style (Style s)
{
	_style = s;
	darea.queue_draw ();
}

gint
BarController::switch_to_bar ()
{
	if (switching) {
		return FALSE;
	}

	switching = true;

	if (get_child() == &darea) {
		return FALSE;
	}

	remove ();
	add (darea);
	darea.show ();

	switching = false;
	return FALSE;
}

gint
BarController::switch_to_spinner ()
{
	if (switching) {
		return FALSE;
	}

	switching = true;

	if (get_child() == &spinner) {
		return FALSE;
	}

	remove ();
	add (spinner);
	spinner.show ();
	spinner.grab_focus ();

	switching = false;
	return FALSE;
}

void
BarController::entry_activated ()
{
	string text = spinner.get_text ();
	float val;

	if (sscanf (text.c_str(), "%f", &val) == 1) {
		adjustment.set_value (val);
	}
	
	switch_to_bar ();
}

gint
BarController::entry_focus_out (GdkEventFocus* ev)
{
	entry_activated ();
	return TRUE;
}

void
BarController::set_use_parent (bool yn)
{
	use_parent = yn;
	queue_draw ();
}
