/*
    Copyright (C) 1999-2002 Paul Barton-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: gtk_ui.cc,v 1.28 2006/02/15 15:37:31 essej Exp $
*/

#include <cmath>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <cerrno>
#include <climits>
#include <cctype>

#include <gtk--.h>
#include <pbd/error.h>
#include <pbd/touchable.h>
#include <pbd/failed_constructor.h>
#include <pbd/pthread_utils.h>

#include <gtkmmext/gtk_ui.h>
#include <gtkmmext/textviewer.h>
#include <gtkmmext/popup.h>
#include <gtkmmext/utils.h>

#include "i18n.h"

using namespace Gtkmmext;
using std::map;

pthread_t UI::gui_thread;
UI       *UI::theGtkUI = 0;

UI::UI (string name, int *argc, char ***argv, string rcfile) 
	: _ui_name (name)
{
	theMain = new Gtk::Main (argc, argv);
	tips = new Gtk::Tooltips;

	if (pthread_key_create (&thread_request_buffer_key, 0)) {
		cerr << _("cannot create thread request buffer key") << endl;
		throw failed_constructor();
	}

	PBD::ThreadCreated.connect (slot (*this, &UI::register_thread));

	_ok = false;
	_active = false;

	if (!theGtkUI) {
		theGtkUI = this;
		gui_thread = pthread_self ();
	} else {
		fatal << "duplicate UI requested" << endmsg;
		/* NOTREACHED */
	}

	if (setup_signal_pipe ()) {
		return;
	}

	load_rcfile (rcfile);

	errors = new TextViewer (850,100);
	errors->text().set_editable (false); 
	errors->text().set_name ("ErrorText");

	string title;
	title = _ui_name;
	title += ": Log";
	errors->set_title (title);

	errors->dismiss_button().set_name ("ErrorLogCloseButton");
	errors->realize();
	errors->get_window().set_decorations (GdkWMDecoration (GDK_DECOR_BORDER|GDK_DECOR_RESIZEH));

	errors->delete_event.connect (bind (slot (just_hide_it), (Gtk::Window *) errors));

	register_thread (pthread_self(), X_("GUI"));

	_ok = true;
}

UI::~UI ()
{
	close (signal_pipe[0]);
	close (signal_pipe[1]);
}

int
UI::load_rcfile (string path)
{
	if (path.length() == 0) {
		return -1;
	}

	if (access (path.c_str(), R_OK)) {
		error << "UI: couldn't find rc file \"" 
		      << path
		      << '"'
		      << endmsg;
		return -1;
	}
	
	gtk_rc_parse (path.c_str());

	Gtk::Label *a_widget1;
	Gtk::Label *a_widget2;
	Gtk::Label *a_widget3;
	Gtk::Label *a_widget4;
	Gtk::Style *style;

	a_widget1 = new Gtk::Label;
	a_widget2 = new Gtk::Label;
	a_widget3 = new Gtk::Label;
	a_widget4 = new Gtk::Label;

	a_widget1->set_name ("FatalMessage");
	a_widget1->ensure_style ();
	style = a_widget1->get_style ();
	fatal_message_style = style->gtkobj();

	a_widget2->set_name ("ErrorMessage");
	a_widget2->ensure_style ();
	style = a_widget2->get_style ();
	error_message_style = style->gtkobj();

	a_widget3->set_name ("WarningMessage");
	a_widget3->ensure_style ();
	style = a_widget3->get_style ();
	warning_message_style = style->gtkobj();

	a_widget4->set_name ("InfoMessage");
	a_widget4->ensure_style ();
	style = a_widget4->get_style ();
	info_message_style = style->gtkobj();

	delete a_widget1;
	delete a_widget2;
	delete a_widget3;
	delete a_widget4;

	return 0;
}

void
UI::run (Receiver &old_receiver)
{
	listen_to (error);
	listen_to (info);
	listen_to (warning);
	listen_to (fatal);

	old_receiver.hangup ();
	starting ();
	_active = true;	
	theMain->run ();
	_active = false;
	stopping ();
	hangup ();
	return;
}

bool
UI::running ()
{
	return _active;
}

void
UI::kill ()
{
	if (_active) {
		pthread_kill (gui_thread, SIGKILL);
	} 
}

void
UI::quit ()
{
	request (Quit);
}

void
UI::do_quit ()
{
	Gtk::Main::quit();
}

void
UI::touch_display (Touchable *display)
{
	Request *req = get_request (TouchDisplay);

	if (req == 0) {
		return;
	}

	req->display = display;

	send_request (req);
}	

void
UI::call_slot (SigC::Slot0<void> slot)
{
	Request *req = get_request (CallSlot);

	if (req == 0) {
		return;
	}

	req->slot = slot;

	send_request (req);
}	

void
UI::call_slot_locked (SigC::Slot0<void> slot)
{
	if (caller_is_gui_thread()) {
		call_slot (slot);
		return;
	}

	Request *req = get_request (CallSlotLocked);

	if (req == 0) {
		return;
	}

	req->slot = slot;

	pthread_mutex_init (&req->slot_lock, NULL);
	pthread_cond_init (&req->slot_cond, NULL);
	pthread_mutex_lock (&req->slot_lock);

	send_request (req);

	pthread_cond_wait (&req->slot_cond, &req->slot_lock);
	pthread_mutex_unlock (&req->slot_lock);

	delete req;
}	

void
UI::set_tip (Gtk::Widget *w, const gchar *tip, const gchar *hlp)
{
	Request *req = get_request (SetTip);

	if (req == 0) {
		return;
	}

	req->widget = w;
	req->msg = tip;
	req->msg2 = hlp;

	send_request (req);
}

void
UI::set_state (Gtk::Widget *w, GtkStateType state)
{
	Request *req = get_request (StateChange);
	
	if (req == 0) {
		return;
	}

	req->new_state = state;
	req->widget = w;

	send_request (req);
}

void
UI::idle_add (int (*func)(void *), void *arg)
{
	Request *req = get_request (AddIdle);

	if (req == 0) {
		return;
	}

	req->function = func;
	req->arg = arg;

	send_request (req);
}

void
UI::timeout_add (unsigned int timeout, int (*func)(void *), void *arg)
{
	Request *req = get_request (AddTimeout);

	if (req == 0) {
		return;
	}

	req->function = func;
	req->arg = arg;
	req->timeout = timeout;

	send_request (req);
}

/* END abstract_ui interfaces */

/* Handling requests */

void
UI::register_thread (pthread_t thread_id, string name)
{
	RingBufferNPT<Request>* b = new RingBufferNPT<Request> (2048);

	{
		PBD::LockMonitor lm (request_buffer_map_lock, __LINE__, __FILE__);
		request_buffers[thread_id] = b;
	}

	pthread_setspecific (thread_request_buffer_key, b);
}

UI::Request::Request()
{

}

UI::Request*
UI::get_request (RequestType rt)
{
	RingBufferNPT<Request>* rbuf = static_cast<RingBufferNPT<Request>* >(pthread_getspecific (thread_request_buffer_key));

	if (rbuf == 0) {
		/* Cannot happen, but if it does we can't use the error reporting mechanism */
		cerr << _("programming error: ")
		     << compose (X_("no GUI request buffer found for thread %1"), pthread_self())
		     << endl;
		abort ();
	}
	
	RingBufferNPT<Request>::rw_vector vec;
	
	rbuf->get_write_vector (&vec);

	if (vec.len[0] == 0) {
		if (vec.len[1] == 0) {
			cerr << compose (X_("no space in GUI request buffer for thread %1"), pthread_self())
			     << endl;
			return 0;
		} else {
			vec.buf[1]->type = rt;
			return vec.buf[1];
		}
	} else {
		vec.buf[0]->type = rt;
		return vec.buf[0];
	}
}

int
UI::setup_signal_pipe ()
{
	/* setup the pipe that other threads send us notifications/requests
	   through.
	*/

	if (pipe (signal_pipe)) {
		error << "UI: cannot create error signal pipe ("
		      << strerror (errno) << ")" 
		      << endmsg;

		return -1;
	}

	if (fcntl (signal_pipe[0], F_SETFL, O_NONBLOCK)) {
		error << "UI: cannot set O_NONBLOCK on "
			 "signal read pipe ("
		      << strerror (errno) << ")"
		      << endmsg;
		return -1;
	}

	if (fcntl (signal_pipe[1], F_SETFL, O_NONBLOCK)) {
		error << "UI: cannot set O_NONBLOCK on "
			 "signal write pipe ("
		      << strerror (errno) 
		      << ")" 
		      << endmsg;
		return -1;
	}

	/* add the pipe to the select/poll loop that GDK does */

	gdk_input_add (signal_pipe[0],
		       GDK_INPUT_READ,
		       UI::signal_pipe_callback,
		       this);

	return 0;
}

void
UI::signal_pipe_callback (void *arg, int fd, GdkInputCondition cond)
{
	char buf[256];
	
	/* flush (nonblocking) pipe */
	
	while (read (fd, buf, 256) > 0);
	
	((UI *) arg)->handle_ui_requests ();
}

void
UI::handle_ui_requests ()
{
	RequestBufferMap::iterator i;

	request_buffer_map_lock.lock ();

	for (i = request_buffers.begin(); i != request_buffers.end(); ++i) {

		RingBufferNPT<Request>::rw_vector vec;

		while (true) {

			/* we must process requests 1 by 1 because
			   the request may run a recursive main
			   event loop that will itself call
			   handle_ui_requests. when we return
			   from the request handler, we cannot
			   expect that the state of queued requests
			   is even remotely consistent with
			   the condition before we called it.
			*/

			i->second->get_read_vector (&vec);

			if (vec.len[0] == 0) {
				break;
			} else {
				/* copy constructor does a deep
				   copy of the Request object,
				   unlike Ringbuffer::read()
				*/
				Request req (*vec.buf[0]);
				i->second->increment_read_ptr (1);
				request_buffer_map_lock.unlock ();
				do_request (&req);
				request_buffer_map_lock.lock ();
			} 
		}
	}

	request_buffer_map_lock.unlock ();
}

void
UI::do_request (Request* req)
{
	switch (req->type) {
	case ErrorMessage:
		// cerr << "error message\n";
		process_error_message (req->chn, req->msg);
		free (const_cast<char*>(req->msg)); /* it was strdup'ed */
		req->msg = 0;
		break;
		
	case Quit:
		// cerr << "quit\n";
		do_quit ();
		break;
		
	case CallSlot:
		// cerr << "call slot\n";
		req->slot.call ();
		break;

	case CallSlotLocked:
		// cerr << "call slot locked\n";
		pthread_mutex_lock (&req->slot_lock);
		req->slot.call ();
		pthread_cond_signal (&req->slot_cond);
		pthread_mutex_unlock (&req->slot_lock);
		break;
		
	case TouchDisplay:
		// cerr << "touch display\n";
		req->display->touch ();
		if (req->display->delete_after_touch()) {
			delete req->display;
		}
		break;
		
	case StateChange:
		// cerr << "state change\n";
		req->widget->set_state (req->new_state);
		break;
		
	case SetTip:
		// cerr << "set tip\n";
		/* XXX need to figure out how this works */
		break;
		
	case AddIdle:
		// cerr << "add idle\n";
		gtk_idle_add (req->function, req->arg);
		break;
		
	case AddTimeout:
		// cerr << "add timeout\n";
		gtk_timeout_add (req->timeout, req->function, req->arg);
		break;
		
	default:
		error << "UI: unknown request type "
		      << (int) req->type
		      << endmsg;
	}	       
}

void
UI::send_request (Request *req)
{
	if (instance() == 0) {
		return; /* XXX is this the right thing to do ? */
	}
	
	if (caller_is_gui_thread()) {
		// cerr << "GUI thread sent request " << req << " type = " << req->type << endl;
		do_request (req);
	} else {	
		const char c = 0;
		RingBufferNPT<Request*>* rbuf = static_cast<RingBufferNPT<Request*> *> (pthread_getspecific (thread_request_buffer_key));

		if (rbuf == 0) {
			/* can't use the error system to report this, because this
			   thread isn't registered!
			*/
			cerr << _("programming error: ")
			     << compose (X_("UI::send_request() called from %1, but no request buffer exists for that thread"),
					 pthread_self())
			     << endl;
			abort ();
		}
		
		// cerr << "thread " << pthread_self() << " sent request " << req << " type = " << req->type << endl;
		rbuf->increment_write_ptr (1);
		write (signal_pipe[1], &c, 1);
	}
}

void
UI::request (RequestType rt)
{
	Request *req = get_request (rt);

	if (req == 0) {
		return;
	}

	send_request (req);
}

/*======================================================================
  Error Display
  ======================================================================*/

void
UI::receive (Transmitter::Channel chn, const char *str)
{
	if (caller_is_gui_thread()) {
		process_error_message (chn, str);
	} else {
		Request* req = get_request (ErrorMessage);

		if (req == 0) {
			return;
		}

		req->chn = chn;
		req->msg = strdup (str);

		send_request (req);
	}
}

#define OLD_STYLE_ERRORS 1

void
UI::process_error_message (Transmitter::Channel chn, const char *str)
{
	GtkStyle *style;
	char *prefix;
	size_t prefix_len;
	bool fatal_received = false;
#ifndef OLD_STYLE_ERRORS
	PopUp* popup = new PopUp (GTK_WIN_POS_CENTER, 0, true);
#endif

	switch (chn) {
	case Transmitter::Fatal:
		prefix = "[FATAL]: ";
		style = fatal_message_style;
		prefix_len = 9;
		fatal_received = true;
		break;
	case Transmitter::Error:
#if OLD_STYLE_ERRORS
		prefix = "[ERROR]: ";
		style = error_message_style;
		prefix_len = 9;
#else
		popup->set_name ("ErrorMessage");
		popup->set_text (str);
		popup->touch ();
		return;
#endif
		break;
	case Transmitter::Info:
#if OLD_STYLE_ERRORS	
		prefix = "[INFO]: ";
		style = info_message_style;
		prefix_len = 8;
#else
		popup->set_name ("InfoMessage");
		popup->set_text (str);
		popup->touch ();
		return;
#endif

		break;
	case Transmitter::Warning:
#if OLD_STYLE_ERRORS
		prefix = "[WARNING]: ";
		style = warning_message_style;
		prefix_len = 11;
#else
		popup->set_name ("WarningMessage");
		popup->set_text (str);
		popup->touch ();
		return;
#endif
		break;
	default:
		/* no choice but to use text/console output here */
		cerr << "programmer error in UI::check_error_messages (channel = " << chn << ")\n";
		::exit (1);
	}
	
	errors->text().freeze();

	if (fatal_received) {
		handle_fatal (str);
	} else {
		
		display_message (prefix, prefix_len, style, str);
		
		if (!errors->is_visible()) {
			toggle_errors();
		}
	}

	errors->text().thaw();
}

void
UI::toggle_errors ()
{
	if (!errors->is_visible()) {
		errors->set_position (GTK_WIN_POS_MOUSE);
		errors->show ();
	} else {
		errors->hide ();
	}
}

void
UI::display_message (const char *prefix, gint prefix_len, GtkStyle *style,
		       const char *msg)
{
	/* stupid, stupid, stupid Gtk-- */

	Gdk_Font pfont(style->font);
	Gdk_Font mfont (style->font);
	Gdk_Color pfg (&style->fg[GTK_STATE_ACTIVE]);
	Gdk_Color pbg (&style->bg[GTK_STATE_ACTIVE]);
	Gdk_Color mfg (&style->fg[GTK_STATE_NORMAL]);
	Gdk_Color mbg (&style->bg[GTK_STATE_NORMAL]);

	errors->text().insert (pfont, pfg, pbg, prefix, prefix_len);
	errors->text().insert (mfont, mfg, mbg, msg, strlen (msg));
	errors->text().insert (mfont, mfg, mbg, "\n", 1);
	errors->scroll_to_bottom ();
}	

void
UI::handle_fatal (const char *message)
{
	Gtk::Window win (GTK_WINDOW_DIALOG);
	Gtk::VBox packer;
	Gtk::Label label (message);
	Gtk::Button quit (_("Press To Exit"));

	win.set_default_size (400, 100);
	
	string title;
	title = _ui_name;
	title += ": Fatal Error";
	win.set_title (title);

	win.set_position (GTK_WIN_POS_MOUSE);
	win.add (packer);

	packer.pack_start (label, true, true);
	packer.pack_start (quit, false, false);
	quit.clicked.connect(SigC::slot(this,&UI::quit));
	
	win.show_all ();
	win.set_modal (true);

	theMain->run ();
	
	exit (1);
}

void
UI::popup_error (const char *text)
{
	PopUp *pup;

	if (!caller_is_gui_thread()) {
		error << "non-UI threads can't use UI::popup_error" 
		      << endmsg;
		return;
	}
	
	pup = new PopUp (GTK_WIN_POS_MOUSE, 0, true);
	pup->set_text (text);
	pup->touch ();
}


void
UI::flush_pending ()
{
	if (!caller_is_gui_thread()) {
		error << "non-UI threads cannot call UI::flush_pending()"
		      << endmsg;
		return;
	}

	gtk_main_iteration();

	while (gtk_events_pending()) {
		gtk_main_iteration();
	}
}

gint
UI::just_hide_it (GdkEventAny *ev, Gtk::Window *win)
{
	win->hide_all ();
	return TRUE;
}

GdkColor
UI::get_color (const string& prompt, bool& picked, gdouble *initial)
{
	GdkColor color;

	Gtk::ColorSelectionDialog color_dialog (prompt);


	color_dialog.set_modal (true);
	color_dialog.get_cancel_button()->clicked.connect (bind (slot (this, &UI::color_selection_done), false));
	color_dialog.get_ok_button()->clicked.connect (bind (slot (this, &UI::color_selection_done), true));
	color_dialog.delete_event.connect (slot (*this, &UI::color_selection_deleted));

	if (initial) {
		color_dialog.get_colorsel()->set_color (initial);
	}

	color_dialog.show_all ();
	color_picked = false;
	picked = false;

	Gtk::Main::run();

	color_dialog.hide_all ();

	if (color_picked) {
		gdouble f_rgba[4];

		color_dialog.get_colorsel()->get_color (f_rgba);
		
		color.red = (gushort) rint (f_rgba[0] * 65535);
		color.green = (gushort) rint (f_rgba[1] * 65535);
		color.blue = (gushort) rint (f_rgba[2] * 65535);

		picked = true;
	}

	return color;
}

void
UI::color_selection_done (bool status)
{
	color_picked = status;
	Gtk::Main::quit ();
}

gint
UI::color_selection_deleted (GdkEventAny *ev)
{
	Gtk::Main::quit ();
	return TRUE;
}
