/* This file is part of Patchage.
 * Copyright 2007-2020 David Robillard <d@drobilla.net>
 *
 * Patchage 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 3 of the License, or (at your option)
 * any later version.
 *
 * Patchage 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 details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Patchage.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "Patchage.hpp"

#include "Configuration.hpp"
#include "Legend.hpp"
#include "PatchageCanvas.hpp"
#include "PatchageEvent.hpp"
#include "PatchagePort.hpp"
#include "UIFile.hpp"
#include "event_to_string.hpp"
#include "handle_event.hpp"
#include "patchage_config.h"
#include "warnings.hpp"

#if defined(HAVE_JACK_DBUS)
#	include "JackDbusDriver.hpp"
#elif defined(PATCHAGE_LIBJACK)
#	include "JackDriver.hpp"
#	include <jack/statistics.h>
#endif

#ifdef HAVE_ALSA
#	include "AlsaDriver.hpp"
#endif

PATCHAGE_DISABLE_GANV_WARNINGS
#include "ganv/Edge.hpp"
#include "ganv/Module.hpp"
PATCHAGE_RESTORE_WARNINGS

PATCHAGE_DISABLE_FMT_WARNINGS
#include <fmt/core.h>
PATCHAGE_RESTORE_WARNINGS

#include <glib.h>
#include <glib/gstdio.h>
#include <gtk/gtkwindow.h>
#include <gtkmm/button.h>
#include <gtkmm/filechooserdialog.h>
#include <gtkmm/liststore.h>
#include <gtkmm/menuitem.h>
#include <gtkmm/messagedialog.h>
#include <gtkmm/stock.h>
#include <gtkmm/treemodel.h>

#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <fstream>

#ifdef PATCHAGE_GTK_OSX

#	include <gtkosxapplication.h>

namespace {

gboolean
can_activate_cb(GtkWidget* widget, guint signal_id, gpointer data)
{
	return gtk_widget_is_sensitive(widget);
}

void
terminate_cb(GtkosxApplication* app, gpointer data)
{
	Patchage* patchage = (Patchage*)data;
	patchage->save();
	Gtk::Main::quit();
}

} // namespace

#endif

namespace {

bool
configure_cb(GtkWindow*, GdkEvent*, gpointer data)
{
	static_cast<Patchage*>(data)->store_window_location();
	return FALSE;
}

int
port_order(const GanvPort* a, const GanvPort* b, void*)
{
	const auto* pa = dynamic_cast<const PatchagePort*>(Glib::wrap(a));
	const auto* pb = dynamic_cast<const PatchagePort*>(Glib::wrap(b));
	if (pa && pb) {
		if (pa->order() && pb->order()) {
			return *pa->order() - *pb->order();
		}

		if (pa->order()) {
			return -1;
		}

		if (pb->order()) {
			return 1;
		}

		return pa->name().compare(pb->name());
	}
	return 0;
}

} // namespace

#define INIT_WIDGET(x) x(_xml, (#x) + 1)

Patchage::Patchage(int argc, char** argv)
    : _xml(UIFile::open("patchage"))
    , _gtk_main(nullptr)
    , INIT_WIDGET(_about_win)
    , INIT_WIDGET(_main_scrolledwin)
    , INIT_WIDGET(_main_win)
    , INIT_WIDGET(_main_vbox)
    , INIT_WIDGET(_menubar)
    , INIT_WIDGET(_menu_alsa_connect)
    , INIT_WIDGET(_menu_alsa_disconnect)
    , INIT_WIDGET(_menu_file_quit)
    , INIT_WIDGET(_menu_export_image)
    , INIT_WIDGET(_menu_help_about)
    , INIT_WIDGET(_menu_jack_connect)
    , INIT_WIDGET(_menu_jack_disconnect)
    , INIT_WIDGET(_menu_view_arrange)
    , INIT_WIDGET(_menu_view_sprung_layout)
    , INIT_WIDGET(_menu_view_messages)
    , INIT_WIDGET(_menu_view_toolbar)
    , INIT_WIDGET(_menu_view_refresh)
    , INIT_WIDGET(_menu_view_human_names)
    , INIT_WIDGET(_menu_view_sort_ports)
    , INIT_WIDGET(_menu_zoom_in)
    , INIT_WIDGET(_menu_zoom_out)
    , INIT_WIDGET(_menu_zoom_normal)
    , INIT_WIDGET(_menu_zoom_full)
    , INIT_WIDGET(_menu_increase_font_size)
    , INIT_WIDGET(_menu_decrease_font_size)
    , INIT_WIDGET(_menu_normal_font_size)
    , INIT_WIDGET(_toolbar)
    , INIT_WIDGET(_clear_load_but)
    , INIT_WIDGET(_xrun_progress)
    , INIT_WIDGET(_buf_size_combo)
    , INIT_WIDGET(_latency_label)
    , INIT_WIDGET(_legend_alignment)
    , INIT_WIDGET(_main_paned)
    , INIT_WIDGET(_log_scrolledwindow)
    , INIT_WIDGET(_status_text)
    , _legend(nullptr)
    , _log(_status_text)
    , _connector(_log)
    , _pane_initialized(false)
    , _attach(true)
    , _jack_driver_autoattach(true)
#ifdef HAVE_ALSA
    , _alsa_driver_autoattach(true)
#endif
{
	_canvas = std::make_shared<PatchageCanvas>(_connector, 1600 * 2, 1200 * 2);

	while (argc > 0) {
		if (!strcmp(*argv, "-h") || !strcmp(*argv, "--help")) {
			std::cout << "Usage: patchage [OPTION]...\n";
			std::cout << "Visually connect JACK and ALSA Audio/MIDI ports.\n\n";
			std::cout << "Options:\n";
			std::cout << "\t-h  --help     Show this help\n";
			std::cout
			    << "\t-A  --no-alsa  Do not automatically attach to ALSA\n";
			std::cout
			    << "\t-J  --no-jack  Do not automatically attack to JACK\n";
			exit(0);
#ifdef HAVE_ALSA
		} else if (!strcmp(*argv, "-A") || !strcmp(*argv, "--no-alsa")) {
			_alsa_driver_autoattach = false;
#endif
#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS)
		} else if (!strcmp(*argv, "-J") || !strcmp(*argv, "--no-jack")) {
			_jack_driver_autoattach = false;
#endif
		}

		argv++;
		argc--;
	}

	Glib::set_application_name("Patchage");
	_about_win->property_program_name()   = "Patchage";
	_about_win->property_logo_icon_name() = "patchage";
	gtk_window_set_default_icon_name("patchage");

	// Create list model for buffer size selector
	Glib::RefPtr<Gtk::ListStore> buf_size_store =
	    Gtk::ListStore::create(_buf_size_columns);
	for (size_t i = 32; i <= 4096; i *= 2) {
		Gtk::TreeModel::Row row      = *(buf_size_store->append());
		row[_buf_size_columns.label] = std::to_string(i);
	}

	_buf_size_combo->set_model(buf_size_store);
	_buf_size_combo->pack_start(_buf_size_columns.label);

	_main_scrolledwin->add(_canvas->widget());

	_main_scrolledwin->property_hadjustment().get_value()->set_step_increment(
	    10);
	_main_scrolledwin->property_vadjustment().get_value()->set_step_increment(
	    10);

	_main_scrolledwin->signal_scroll_event().connect(
	    sigc::mem_fun(this, &Patchage::on_scroll));
	_clear_load_but->signal_clicked().connect(
	    sigc::mem_fun(this, &Patchage::clear_load));
	_buf_size_combo->signal_changed().connect(
	    sigc::mem_fun(this, &Patchage::buffer_size_changed));
	_status_text->signal_size_allocate().connect(
	    sigc::mem_fun(this, &Patchage::on_messages_resized));

#ifdef HAVE_ALSA
	_menu_alsa_connect->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::menu_alsa_connect));
	_menu_alsa_disconnect->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::menu_alsa_disconnect));
#else
	_menu_alsa_connect->set_sensitive(false);
	_menu_alsa_disconnect->set_sensitive(false);
#endif

	_menu_file_quit->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_quit));
	_menu_export_image->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_export_image));
	_menu_view_refresh->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::refresh));
	_menu_view_human_names->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_view_human_names));
	_menu_view_sort_ports->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_view_sort_ports));
	_menu_view_arrange->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_arrange));
	_menu_view_sprung_layout->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_sprung_layout_toggled));
	_menu_view_messages->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_view_messages));
	_menu_view_toolbar->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_view_toolbar));
	_menu_help_about->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_help_about));
	_menu_zoom_in->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_zoom_in));
	_menu_zoom_out->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_zoom_out));
	_menu_zoom_normal->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_zoom_normal));
	_menu_zoom_full->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_zoom_full));
	_menu_increase_font_size->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_increase_font_size));
	_menu_decrease_font_size->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_decrease_font_size));
	_menu_normal_font_size->signal_activate().connect(
	    sigc::mem_fun(this, &Patchage::on_normal_font_size));

	if (_canvas->supports_sprung_layout()) {
		_menu_view_sprung_layout->set_active(true);
	} else {
		_menu_view_sprung_layout->set_active(false);
		_menu_view_sprung_layout->set_sensitive(false);
	}

	_canvas->widget().show();
	_main_win->present();

	_conf.set_font_size(_canvas->get_default_font_size());
	_conf.load();
	_canvas->set_zoom(_conf.get_zoom());
	_canvas->set_font_size(_conf.get_font_size());
	if (_conf.get_sort_ports()) {
		_canvas->set_port_order(port_order, nullptr);
	}

	_main_win->resize(static_cast<int>(_conf.get_window_size().x),
	                  static_cast<int>(_conf.get_window_size().y));

	_main_win->move(static_cast<int>(_conf.get_window_location().x),
	                static_cast<int>(_conf.get_window_location().y));

	_legend = new Legend(_conf);
	_legend->signal_color_changed.connect(
	    sigc::mem_fun(this, &Patchage::on_legend_color_change));
	_legend_alignment->add(*Gtk::manage(_legend));
	_legend->show_all();

	_about_win->set_transient_for(*_main_win);
#ifdef __APPLE__
	try {
		_about_win->set_logo(Gdk::Pixbuf::create_from_file(
		    bundle_location() + "/Resources/Patchage.icns"));
	} catch (const Glib::Exception& e) {
		_log.error(
		    fmt::format("Failed to set logo ({})", std::string(e.what())));
	}
#endif

#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS)
	_jack_driver = std::unique_ptr<JackDriver>{new JackDriver(
	    _log, [this](const PatchageEvent& event) { on_driver_event(event); })};

	_connector.add_driver(PortID::Type::jack, _jack_driver.get());

	_menu_jack_connect->signal_activate().connect(sigc::bind(
	    sigc::mem_fun(_jack_driver.get(), &JackDriver::attach), true));
	_menu_jack_disconnect->signal_activate().connect(
	    sigc::mem_fun(_jack_driver.get(), &JackDriver::detach));
#endif

#ifdef HAVE_ALSA
	_alsa_driver = std::unique_ptr<Driver>{new AlsaDriver(
	    _log, [this](const PatchageEvent& event) { on_driver_event(event); })};

	_connector.add_driver(PortID::Type::alsa, _alsa_driver.get());
#endif

	update_state();
	_menu_view_toolbar->set_active(_conf.get_show_toolbar());
	_menu_view_sprung_layout->set_active(_conf.get_sprung_layout());
	_menu_view_sort_ports->set_active(_conf.get_sort_ports());

	g_signal_connect(
	    _main_win->gobj(), "configure-event", G_CALLBACK(configure_cb), this);

	_canvas->widget().grab_focus();

	// Idle callback, check if we need to refresh
	Glib::signal_timeout().connect(
	    sigc::mem_fun(this, &Patchage::idle_callback), 100);

#ifdef PATCHAGE_GTK_OSX
	// Set up Mac menu bar
	GtkosxApplication* osxapp =
	    (GtkosxApplication*)g_object_new(GTKOSX_TYPE_APPLICATION, nullptr);
	_menubar->hide();
	_menu_file_quit->hide();
	gtkosx_application_set_menu_bar(osxapp, GTK_MENU_SHELL(_menubar->gobj()));
	gtkosx_application_insert_app_menu_item(
	    osxapp, GTK_WIDGET(_menu_help_about->gobj()), 0);
	g_signal_connect(_menubar->gobj(),
	                 "can-activate-accel",
	                 G_CALLBACK(can_activate_cb),
	                 nullptr);
	g_signal_connect(
	    osxapp, "NSApplicationWillTerminate", G_CALLBACK(terminate_cb), this);
	gtkosx_application_ready(osxapp);
#endif
}

Patchage::~Patchage()
{
#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS)
	_jack_driver.reset();
#endif
#ifdef HAVE_ALSA
	_alsa_driver.reset();
#endif

	_about_win.destroy();
	_xml.reset();
}

void
Patchage::attach()
{
#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS)
	if (_jack_driver_autoattach) {
		_jack_driver->attach(true);
	}
#endif

#ifdef HAVE_ALSA
	if (_alsa_driver_autoattach) {
		_alsa_driver->attach(false);
	}
#endif

	process_events();
	refresh();
	update_toolbar();
}

bool
Patchage::idle_callback()
{
	// Initial run, attach
	if (_attach) {
		attach();
		_menu_view_messages->set_active(_conf.get_show_messages());
		_attach = false;
	}

	// Process any events from drivers
	process_events();

	// Update load every 5 idle callbacks
	static int count = 0;
	if (++count == 5) {
		update_load();
		count = 0;
	}

	return true;
}

void
Patchage::update_toolbar()
{
	static bool updating = false;
	if (updating) {
		return;
	}

	updating = true;

#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS)
	if (_jack_driver->is_attached()) {
		const auto buffer_size = _jack_driver->buffer_size();
		const auto sample_rate = _jack_driver->sample_rate();
		if (sample_rate != 0) {
			const int latency_ms =
			    lrintf(buffer_size * 1000 / float(sample_rate));
			std::stringstream ss;
			ss << " frames @ " << (sample_rate / 1000) << "kHz (" << latency_ms
			   << "ms)";
			_latency_label->set_label(ss.str());
			_latency_label->set_visible(true);
			_buf_size_combo->set_active(
			    static_cast<int>(log2f(_jack_driver->buffer_size()) - 5));
			updating = false;
			return;
		}
	}
#endif
	_latency_label->set_visible(false);
	updating = false;
}

bool
Patchage::update_load()
{
#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS)
	if (_jack_driver->is_attached()) {
		char buf[8];
		snprintf(buf, sizeof(buf), "%u", _jack_driver->get_xruns());
		_xrun_progress->set_text(std::string(buf) + " Dropouts");
		_xrun_progress->set_fraction(_jack_driver->get_max_dsp_load());
	}
#endif

	return true;
}

void
Patchage::zoom(double z)
{
	_conf.set_zoom(z);
	_canvas->set_zoom(z);
}

void
Patchage::refresh()
{
	auto sink = [this](const PatchageEvent& event) {
		_log.info("Refresh: " + event_to_string(event));
		handle_event(*this, event);
	};

	if (_canvas) {
		_canvas->clear();

#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS)
		if (_jack_driver) {
			_jack_driver->refresh(sink);
		}
#endif

		if (_alsa_driver) {
			_alsa_driver->refresh(sink);
		}
	}
}

void
Patchage::driver_attached(const ClientType type)
{
	switch (type) {
	case ClientType::jack:
		_menu_jack_connect->set_sensitive(false);
		_menu_jack_disconnect->set_sensitive(true);

		if (_jack_driver) {
			_jack_driver->refresh([this](const PatchageEvent& event) {
				handle_event(*this, event);
			});
		}

		break;
	case ClientType::alsa:
		_menu_alsa_connect->set_sensitive(false);
		_menu_alsa_disconnect->set_sensitive(true);

		if (_alsa_driver) {
			_alsa_driver->refresh([this](const PatchageEvent& event) {
				handle_event(*this, event);
			});
		}

		break;
	}
}

void
Patchage::driver_detached(const ClientType type)
{
	switch (type) {
	case ClientType::jack:
		_menu_jack_connect->set_sensitive(true);
		_menu_jack_disconnect->set_sensitive(false);

		if (_jack_driver && !_jack_driver->is_attached()) {
			_canvas->remove_ports([](const PatchagePort* port) {
				return (port->type() == PortType::jack_audio ||
				        port->type() == PortType::jack_midi ||
				        port->type() == PortType::jack_osc ||
				        port->type() == PortType::jack_cv);
			});
		}

		break;

	case ClientType::alsa:
		_menu_alsa_connect->set_sensitive(true);
		_menu_alsa_disconnect->set_sensitive(false);

		_canvas->remove_ports([](const PatchagePort* port) {
			return port->type() == PortType::alsa_midi;
		});

		break;
	}
}

void
Patchage::store_window_location()
{
	int loc_x = 0;
	int loc_y = 0;
	_main_win->get_position(loc_x, loc_y);

	int size_x = 0;
	int size_y = 0;
	_main_win->get_size(size_x, size_y);

	_conf.set_window_location({double(loc_x), double(loc_y)});
	_conf.set_window_size({double(size_x), double(size_y)});
}

void
Patchage::clear_load()
{
#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS)
	_xrun_progress->set_fraction(0.0);
	_jack_driver->reset_xruns();
	_jack_driver->reset_max_dsp_load();
#endif
}

static void
load_module_location(GanvNode* node, void*)
{
	if (GANV_IS_MODULE(node)) {
		Ganv::Module* gmod = Glib::wrap(GANV_MODULE(node));
		auto*         pmod = dynamic_cast<PatchageModule*>(gmod);
		if (pmod) {
			pmod->load_location();
		}
	}
}

void
Patchage::update_state()
{
	_canvas->for_each_node(load_module_location, nullptr);
}

void
Patchage::on_driver_event(const PatchageEvent& event)
{
	std::lock_guard<std::mutex> lock{_events_mutex};

	_driver_events.emplace(event);
}

void
Patchage::process_events()
{
	std::lock_guard<std::mutex> lock{_events_mutex};

	while (!_driver_events.empty()) {
		_log.info(event_to_string(_driver_events.front()));
		handle_event(*this, _driver_events.front());
		_driver_events.pop();
	}
}

void
Patchage::menu_alsa_connect()
{
	if (_alsa_driver) {
		_alsa_driver->attach(false);
	}
}

void
Patchage::menu_alsa_disconnect()
{
	if (_alsa_driver) {
		_alsa_driver->detach();
	}
}

void
Patchage::on_arrange()
{
	if (_canvas) {
		_canvas->arrange();
	}
}

void
Patchage::on_sprung_layout_toggled()
{
	const bool sprung = _menu_view_sprung_layout->get_active();

	_canvas->set_sprung_layout(sprung);
	_conf.set_sprung_layout(sprung);
}

void
Patchage::on_help_about()
{
	_about_win->run();
	_about_win->hide();
}

static void
update_labels(GanvNode* node, void* data)
{
	const bool human_names = *static_cast<const bool*>(data);
	if (GANV_IS_MODULE(node)) {
		Ganv::Module* gmod = Glib::wrap(GANV_MODULE(node));
		auto*         pmod = dynamic_cast<PatchageModule*>(gmod);
		if (pmod) {
			for (Ganv::Port* gport : *gmod) {
				auto* pport = dynamic_cast<PatchagePort*>(gport);
				if (pport) {
					pport->show_human_name(human_names);
				}
			}
		}
	}
}

void
Patchage::on_view_human_names()
{
	bool human_names = show_human_names();
	_canvas->for_each_node(update_labels, &human_names);
}

void
Patchage::on_view_sort_ports()
{
	const bool sort_ports = this->sort_ports();
	_canvas->set_port_order(sort_ports ? port_order : nullptr, nullptr);
	_conf.set_sort_ports(sort_ports);
	refresh();
}

void
Patchage::on_zoom_in()
{
	const float zoom = _canvas->get_zoom() * 1.25;
	_canvas->set_zoom(zoom);
	_conf.set_zoom(zoom);
}

void
Patchage::on_zoom_out()
{
	const float zoom = _canvas->get_zoom() * 0.75;
	_canvas->set_zoom(zoom);
	_conf.set_zoom(zoom);
}

void
Patchage::on_zoom_normal()
{
	_canvas->set_zoom(1.0);
	_conf.set_zoom(1.0);
}

void
Patchage::on_zoom_full()
{
	_canvas->zoom_full();
	_conf.set_zoom(_canvas->get_zoom());
}

void
Patchage::on_increase_font_size()
{
	const float points = _canvas->get_font_size() + 1.0;
	_canvas->set_font_size(points);
	_conf.set_font_size(points);
}

void
Patchage::on_decrease_font_size()
{
	const float points = _canvas->get_font_size() - 1.0;
	_canvas->set_font_size(points);
	_conf.set_font_size(points);
}

void
Patchage::on_normal_font_size()
{
	_canvas->set_font_size(_canvas->get_default_font_size());
	_conf.set_font_size(_canvas->get_default_font_size());
}

static inline guint
highlight_color(guint c, guint delta)
{
	const guint max_char = 255;
	const guint r        = MIN((c >> 24) + delta, max_char);
	const guint g        = MIN(((c >> 16) & 0xFF) + delta, max_char);
	const guint b        = MIN(((c >> 8) & 0xFF) + delta, max_char);
	const guint a        = c & 0xFF;

	return ((r << 24u) | (g << 16u) | (b << 8u) | a);
}

static void
update_port_colors(GanvNode* node, void* data)
{
	auto* patchage = static_cast<Patchage*>(data);
	if (!GANV_IS_MODULE(node)) {
		return;
	}

	Ganv::Module* gmod = Glib::wrap(GANV_MODULE(node));
	auto*         pmod = dynamic_cast<PatchageModule*>(gmod);
	if (!pmod) {
		return;
	}

	for (Ganv::Port* p : *pmod) {
		auto* port = dynamic_cast<PatchagePort*>(p);
		if (port) {
			const uint32_t rgba = patchage->conf().get_port_color(port->type());
			port->set_fill_color(rgba);
			port->set_border_color(highlight_color(rgba, 0x20));
		}
	}
}

static void
update_edge_color(GanvEdge* edge, void* data)
{
	auto*       patchage = static_cast<Patchage*>(data);
	Ganv::Edge* edgemm   = Glib::wrap(edge);

	auto* tail = dynamic_cast<PatchagePort*>((edgemm)->get_tail());
	if (tail) {
		edgemm->set_color(patchage->conf().get_port_color(tail->type()));
	}
}

void
Patchage::on_legend_color_change(PortType id, const std::string&, uint32_t rgba)
{
	_conf.set_port_color(id, rgba);
	_canvas->for_each_node(update_port_colors, this);
	_canvas->for_each_edge(update_edge_color, this);
}

void
Patchage::on_messages_resized(Gtk::Allocation&)
{
	const int max_pos = _main_paned->get_allocation().get_height();
	_conf.set_messages_height(max_pos - _main_paned->get_position());
}

void
Patchage::save()
{
	_conf.set_zoom(_canvas->get_zoom()); // Can be changed by ganv
	_conf.save();
}

void
Patchage::on_quit()
{
#ifdef HAVE_ALSA
	_alsa_driver->detach();
#endif
#if defined(PATCHAGE_LIBJACK) || defined(HAVE_JACK_DBUS)
	_jack_driver->detach();
#endif
	_main_win->hide();
}

void
Patchage::on_export_image()
{
	Gtk::FileChooserDialog dialog("Export Image",
	                              Gtk::FILE_CHOOSER_ACTION_SAVE);
	dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
	dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_OK);
	dialog.set_default_response(Gtk::RESPONSE_OK);
	dialog.set_transient_for(*_main_win);

	using Types = std::map<std::string, std::string>;

	Types types;
	types["*.dot"] = "Graphviz DOT";
	types["*.pdf"] = "Portable Document Format";
	types["*.ps"]  = "PostScript";
	types["*.svg"] = "Scalable Vector Graphics";
	for (const auto& t : types) {
		Gtk::FileFilter filt;
		filt.add_pattern(t.first);
		filt.set_name(t.second);
		dialog.add_filter(filt);
	}

	auto* bg_but = new Gtk::CheckButton("Draw _Background", true);
	auto* extra  = new Gtk::Alignment(1.0, 0.5, 0.0, 0.0);
	bg_but->set_active(true);
	extra->add(*Gtk::manage(bg_but));
	extra->show_all();
	dialog.set_extra_widget(*Gtk::manage(extra));

	if (dialog.run() == Gtk::RESPONSE_OK) {
		const std::string filename = dialog.get_filename();
		if (Glib::file_test(filename, Glib::FILE_TEST_EXISTS)) {
			Gtk::MessageDialog confirm(std::string("File exists!  Overwrite ") +
			                               filename + "?",
			                           true,
			                           Gtk::MESSAGE_WARNING,
			                           Gtk::BUTTONS_YES_NO,
			                           true);
			confirm.set_transient_for(dialog);
			if (confirm.run() != Gtk::RESPONSE_YES) {
				return;
			}
		}
		_canvas->export_image(filename.c_str(), bg_but->get_active());
	}
}

void
Patchage::on_view_messages()
{
	if (_menu_view_messages->get_active()) {
		Glib::RefPtr<Gtk::TextBuffer> buffer = _status_text->get_buffer();
		if (!_pane_initialized) {
			const int min_height  = _log.min_height();
			const int max_pos     = _main_paned->get_allocation().get_height();
			const int conf_height = _conf.get_messages_height();
			_main_paned->set_position(max_pos -
			                          std::max(conf_height, min_height));

			_pane_initialized = true;
		}

		_log_scrolledwindow->show();
		_status_text->scroll_to_mark(_status_text->get_buffer()->get_insert(),
		                             0);
		_conf.set_show_messages(true);
	} else {
		_log_scrolledwindow->hide();
		_conf.set_show_messages(false);
	}
}

void
Patchage::on_view_toolbar()
{
	if (_menu_view_toolbar->get_active()) {
		_toolbar->show();
	} else {
		_toolbar->hide();
	}
	_conf.set_show_toolbar(_menu_view_toolbar->get_active());
}

bool
Patchage::on_scroll(GdkEventScroll*)
{
	return false;
}

void
Patchage::buffer_size_changed()
{
#if defined(HAVE_JACK) || defined(HAVE_JACK_DBUS)
	const int selected = _buf_size_combo->get_active_row_number();

	if (selected == -1) {
		update_toolbar();
	} else {
		const uint32_t buffer_size = 1u << (selected + 5);
		_jack_driver->set_buffer_size(buffer_size);
		update_toolbar();
	}
#endif
}