/*
  This file is part of Ingen.
  Copyright 2007-2017 David Robillard <http://drobilla.net/>

  Ingen is free software: you can redistribute it and/or modify it under the
  terms of the GNU Affero General Public License as published by the Free
  Software Foundation, either version 3 of the License, or any later version.

  Ingen 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 Affero General Public License for details.

  You should have received a copy of the GNU Affero General Public License
  along with Ingen.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "App.hpp"
#include "GraphCanvas.hpp"
#include "GraphView.hpp"
#include "GraphWindow.hpp"
#include "LoadPluginWindow.hpp"
#include "ingen_config.h"

#include "ingen/Interface.hpp"
#include "ingen/client/ClientStore.hpp"
#include "ingen/client/GraphModel.hpp"

#include <string>
#include <cstddef>
#include <cassert>
#include <algorithm>

using std::string;

namespace ingen {

using namespace client;

namespace gui {

LoadPluginWindow::LoadPluginWindow(BaseObjectType*                   cobject,
                                   const Glib::RefPtr<Gtk::Builder>& xml)
	: Window(cobject)
	, _name_offset(0)
	, _has_shown(false)
	, _refresh_list(true)
{
	xml->get_widget("load_plugin_plugins_treeview", _plugins_treeview);
	xml->get_widget("load_plugin_polyphonic_checkbutton", _polyphonic_checkbutton);
	xml->get_widget("load_plugin_name_entry", _name_entry);
	xml->get_widget("load_plugin_add_button", _add_button);
	xml->get_widget("load_plugin_close_button", _close_button);

	xml->get_widget("load_plugin_filter_combo", _filter_combo);
	xml->get_widget("load_plugin_search_entry", _search_entry);

	// Set up the plugins list
	_plugins_liststore = Gtk::ListStore::create(_plugins_columns);
	_plugins_treeview->set_model(_plugins_liststore);
	_plugins_treeview->append_column("_Name", _plugins_columns._col_name);
	_plugins_treeview->append_column("_Type", _plugins_columns._col_type);
	_plugins_treeview->append_column("_Project", _plugins_columns._col_project);
	_plugins_treeview->append_column("_Author", _plugins_columns._col_author);
	_plugins_treeview->append_column("_URI", _plugins_columns._col_uri);

	// This could be nicer.. store the TreeViewColumns locally maybe?
	_plugins_treeview->get_column(0)->set_sort_column(_plugins_columns._col_name);
	_plugins_treeview->get_column(1)->set_sort_column(_plugins_columns._col_type);
	_plugins_treeview->get_column(2)->set_sort_column(_plugins_columns._col_project);
	_plugins_treeview->get_column(2)->set_sort_column(_plugins_columns._col_author);
	_plugins_treeview->get_column(3)->set_sort_column(_plugins_columns._col_uri);
	for (int i = 0; i < 5; ++i) {
		_plugins_treeview->get_column(i)->set_resizable(true);
	}

	// Set up the search criteria combobox
	_criteria_liststore = Gtk::ListStore::create(_criteria_columns);
	_filter_combo->set_model(_criteria_liststore);

	Gtk::TreeModel::iterator iter = _criteria_liststore->append();
	Gtk::TreeModel::Row      row  = *iter;
	row[_criteria_columns._col_label] = "Name contains";
	row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::NAME;
	_filter_combo->set_active(iter);

	row = *(iter = _criteria_liststore->append());
	row[_criteria_columns._col_label] = "Type contains";
	row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::TYPE;

	row = *(iter = _criteria_liststore->append());
	row[_criteria_columns._col_label] = "Project contains";
	row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::PROJECT;

	row = *(iter = _criteria_liststore->append());
	row[_criteria_columns._col_label] = "Author contains";
	row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::AUTHOR;

	row = *(iter = _criteria_liststore->append());
	row[_criteria_columns._col_label] = "URI contains";
	row[_criteria_columns._col_criteria] = CriteriaColumns::Criteria::URI;
	_filter_combo->pack_start(_criteria_columns._col_label);

	_add_button->signal_clicked().connect(
		sigc::mem_fun(this, &LoadPluginWindow::add_clicked));
	_close_button->signal_clicked().connect(
		sigc::mem_fun(this, &Window::hide));
	_plugins_treeview->signal_row_activated().connect(
		sigc::mem_fun(this, &LoadPluginWindow::plugin_activated));
	_search_entry->signal_activate().connect(
		sigc::mem_fun(this, &LoadPluginWindow::add_clicked));
	_search_entry->signal_changed().connect(
		sigc::mem_fun(this, &LoadPluginWindow::filter_changed));
	_name_entry->signal_changed().connect(
		sigc::mem_fun(this, &LoadPluginWindow::name_changed));

	_search_entry->signal_icon_release().connect(
		sigc::mem_fun(this, &LoadPluginWindow::name_cleared));

	_selection = _plugins_treeview->get_selection();
	_selection->set_mode(Gtk::SELECTION_MULTIPLE);
	_selection->signal_changed().connect(
		sigc::mem_fun(this, &LoadPluginWindow::plugin_selection_changed));

	//m_add_button->grab_default();
}

void
LoadPluginWindow::present(SPtr<const GraphModel> graph,
                          Properties             data)
{
	set_graph(graph);
	_initial_data = data;
	Gtk::Window::present();
}

/** Called every time the user types into the name input box.
 * Used to display warning messages, and enable/disable the OK button.
 */
void
LoadPluginWindow::name_changed()
{
	// Toggle add button sensitivity according name legality
	if (_selection->get_selected_rows().size() == 1) {
		const string sym = _name_entry->get_text();
		if (!Raul::Symbol::is_valid(sym)) {
			_add_button->property_sensitive() = false;
		} else if (_app->store()->find(_graph->path().child(Raul::Symbol(sym)))
		           != _app->store()->end()) {
			_add_button->property_sensitive() = false;
		} else {
			_add_button->property_sensitive() = true;
		}
	}
}

void
LoadPluginWindow::name_cleared(Gtk::EntryIconPosition pos, const GdkEventButton* event)
{
	_search_entry->set_text("");
}

/** Sets the graph controller for this window and initializes everything.
 *
 * This function MUST be called before using the window in any way!
 */
void
LoadPluginWindow::set_graph(SPtr<const GraphModel> graph)
{
	if (_graph) {
		_graph = graph;
		plugin_selection_changed();
	} else {
		_graph = graph;
	}
}

/** Populates the plugin list on the first show.
 *
 * This is done here instead of construction time as the list population is
 * really expensive and bogs down creation of a graph.  This is especially
 * important when many graph notifications are sent at one time from the
 * engine.
 */
void
LoadPluginWindow::on_show()
{
	if (!_has_shown) {
		_app->store()->signal_new_plugin().connect(
			sigc::mem_fun(this, &LoadPluginWindow::add_plugin));
		_has_shown = true;
	}

	if (_refresh_list) {
		set_plugins(_app->store()->plugins());
		_refresh_list = false;
	}

	Gtk::Window::on_show();
}

void
LoadPluginWindow::set_plugins(SPtr<const ClientStore::Plugins> plugins)
{
	_rows.clear();
	_plugins_liststore->clear();

	for (const auto& p : *plugins.get()) {
		add_plugin(p.second);
	}

	_plugins_liststore->set_sort_column(1, Gtk::SORT_ASCENDING);
	_plugins_treeview->columns_autosize();
}

void
LoadPluginWindow::new_plugin(SPtr<const PluginModel> pm)
{
	if (is_visible()) {
		add_plugin(pm);
	} else {
		_refresh_list = true;
	}
}

static std::string
get_project_name(SPtr<const PluginModel> plugin)
{
	std::string name;
	if (plugin->lilv_plugin()) {
		LilvNode* project = lilv_plugin_get_project(plugin->lilv_plugin());
		if (!project) {
			return "";
		}

		LilvNode* doap_name = lilv_new_uri(
			plugin->lilv_world(), "http://usefulinc.com/ns/doap#name");
		LilvNodes* names = lilv_world_find_nodes(
			plugin->lilv_world(), project, doap_name, nullptr);

		if (names) {
			name = lilv_node_as_string(lilv_nodes_get_first(names));
		}

		lilv_nodes_free(names);
		lilv_node_free(doap_name);
		lilv_node_free(project);
	}
	return name;
}

static std::string
get_author_name(SPtr<const PluginModel> plugin)
{
	std::string name;
	if (plugin->lilv_plugin()) {
		LilvNode* author = lilv_plugin_get_author_name(plugin->lilv_plugin());
		if (author) {
			name = lilv_node_as_string(author);
		}
		lilv_node_free(author);
	}
	return name;
}

void
LoadPluginWindow::set_row(Gtk::TreeModel::Row&    row,
                          SPtr<const PluginModel> plugin)
{
	const URIs&       uris = _app->uris();
	const Atom& name = plugin->get_property(uris.doap_name);
	if (name.is_valid() && name.type() == uris.forge.String) {
		row[_plugins_columns._col_name] = name.ptr<char>();
	}

	if (uris.lv2_Plugin == plugin->type()) {
		row[_plugins_columns._col_type] = lilv_node_as_string(
			lilv_plugin_class_get_label(
				lilv_plugin_get_class(plugin->lilv_plugin())));

		row[_plugins_columns._col_project] = get_project_name(plugin);
		row[_plugins_columns._col_author]  = get_author_name(plugin);
	} else if (uris.ingen_Internal == plugin->type()) {
		row[_plugins_columns._col_type]    = "Internal";
		row[_plugins_columns._col_project] = "Ingen";
		row[_plugins_columns._col_author]  = "David Robillard";
	} else if (uris.ingen_Graph == plugin->type()) {
		row[_plugins_columns._col_type] = "Graph";
	} else {
		row[_plugins_columns._col_type] = "";
	}

	row[_plugins_columns._col_uri]    = plugin->uri().string();
	row[_plugins_columns._col_plugin] = plugin;
}

void
LoadPluginWindow::add_plugin(SPtr<const PluginModel> plugin)
{
	if (plugin->lilv_plugin() && lilv_plugin_is_replaced(plugin->lilv_plugin())) {
		return;
	}

	Gtk::TreeModel::iterator iter = _plugins_liststore->append();
	Gtk::TreeModel::Row      row  = *iter;
	_rows.emplace(plugin->uri(), iter);

	set_row(row, plugin);

	plugin->signal_property().connect(
		sigc::bind<0>(sigc::mem_fun(this, &LoadPluginWindow::plugin_property_changed),
		              plugin->uri()));
}

///// Event Handlers //////

void
LoadPluginWindow::plugin_activated(const Gtk::TreeModel::Path& path,
                                   Gtk::TreeViewColumn*        col)
{
	add_clicked();
}

void
LoadPluginWindow::plugin_selection_changed()
{
	size_t n_selected = _selection->get_selected_rows().size();
	if (n_selected == 0) {
		_name_offset = 0;
		_name_entry->set_text("");
		_name_entry->set_sensitive(false);
	} else if (n_selected == 1) {
		Gtk::TreeModel::iterator iter = _plugins_liststore->get_iter(
			*_selection->get_selected_rows().begin());
		if (iter) {
			Gtk::TreeModel::Row row = *iter;
			SPtr<const PluginModel> p = row.get_value(
				_plugins_columns._col_plugin);
			_name_offset = _app->store()->child_name_offset(
				_graph->path(), p->default_block_symbol());
			_name_entry->set_text(generate_module_name(p, _name_offset));
			_name_entry->set_sensitive(true);
		} else {
			_name_offset = 0;
			_name_entry->set_text("");
			_name_entry->set_sensitive(false);
		}
	} else {
		_name_entry->set_text("");
		_name_entry->set_sensitive(false);
	}
}

/** Generate an automatic name for this Node.
 *
 * Offset is an offset of the number that will be appended to the plugin's
 * label, needed if the user adds multiple plugins faster than the engine
 * sends the notification back.
 */
string
LoadPluginWindow::generate_module_name(SPtr<const PluginModel> plugin,
                                       int                     offset)
{
	std::stringstream ss;
	ss << plugin->default_block_symbol();
	if (offset != 0) {
		ss << "_" << offset;
	}
	return ss.str();
}

void
LoadPluginWindow::load_plugin(const Gtk::TreeModel::iterator& iter)
{
	const URIs&             uris       = _app->uris();
	Gtk::TreeModel::Row     row        = *iter;
	SPtr<const PluginModel> plugin     = row.get_value(_plugins_columns._col_plugin);
	bool                    polyphonic = _polyphonic_checkbutton->get_active();
	string                  name       = _name_entry->get_text();

	if (name.empty()) {
		name = generate_module_name(plugin, _name_offset);
	}

	if (name.empty() || !Raul::Symbol::is_valid(name)) {
		Gtk::MessageDialog dialog(
			*this,
			"Unable to choose a default name, please provide one",
			false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true);

		dialog.run();
	} else {
		Raul::Path path  = _graph->path().child(Raul::Symbol::symbolify(name));
		Properties props = _initial_data;
		props.emplace(uris.rdf_type, Property(uris.ingen_Block));
		props.emplace(uris.lv2_prototype, _app->forge().make_urid(plugin->uri()));
		props.emplace(uris.ingen_polyphonic, _app->forge().make(polyphonic));
		_app->interface()->put(path_to_uri(path), props);

		if (_selection->get_selected_rows().size() == 1) {
			_name_offset = (_name_offset == 0) ? 2 : _name_offset + 1;
			_name_entry->set_text(generate_module_name(plugin, _name_offset));
		}

		// Cascade next block
		Atom& x = _initial_data.find(uris.ingen_canvasX)->second;
		x = _app->forge().make(x.get<float>() + 20.0f);
		Atom& y = _initial_data.find(uris.ingen_canvasY)->second;
		y = _app->forge().make(y.get<float>() + 20.0f);
	}
}

void
LoadPluginWindow::add_clicked()
{
	_selection->selected_foreach_iter(
		sigc::mem_fun(*this, &LoadPluginWindow::load_plugin));
}

void
LoadPluginWindow::filter_changed()
{
	_rows.clear();
	_plugins_liststore->clear();
	string search = _search_entry->get_text();
	transform(search.begin(), search.end(), search.begin(), ::toupper);

	// Get selected criteria
	const Gtk::TreeModel::Row row = *(_filter_combo->get_active());
	CriteriaColumns::Criteria criteria = row[_criteria_columns._col_criteria];

	string field;

	Gtk::TreeModel::Row      model_row;
	Gtk::TreeModel::iterator model_iter;
	size_t                   num_visible = 0;
	const URIs&              uris        = _app->uris();

	for (const auto& p : *_app->store()->plugins().get()) {
		const SPtr<PluginModel> plugin = p.second;
		const Atom& name = plugin->get_property(uris.doap_name);

		switch (criteria) {
		case CriteriaColumns::Criteria::NAME:
			if (name.is_valid() && name.type() == uris.forge.String) {
				field = name.ptr<char>();
			}
			break;
		case CriteriaColumns::Criteria::TYPE:
			if (plugin->lilv_plugin()) {
				field = lilv_node_as_string(
					lilv_plugin_class_get_label(
						lilv_plugin_get_class(plugin->lilv_plugin())));
			}
			break;
		case CriteriaColumns::Criteria::PROJECT:
			field = get_project_name(plugin);
			break;
		case CriteriaColumns::Criteria::AUTHOR:
			field = get_author_name(plugin);
			break;
		case CriteriaColumns::Criteria::URI:
			field = plugin->uri();
			break;
		}

		transform(field.begin(), field.end(), field.begin(), ::toupper);

		if (field.find(search) != string::npos) {
			model_iter = _plugins_liststore->append();
			model_row = *model_iter;
			set_row(model_row, plugin);
			++num_visible;
		}
	}

	if (num_visible == 1) {
		_selection->unselect_all();
		_selection->select(model_iter);
	}
}

bool
LoadPluginWindow::on_key_press_event(GdkEventKey* event)
{
	if (event->keyval == GDK_w && event->state & GDK_CONTROL_MASK) {
		hide();
		return true;
	} else {
		return Gtk::Window::on_key_press_event(event);
	}
}

void
LoadPluginWindow::plugin_property_changed(const URI&  plugin,
                                          const URI&  predicate,
                                          const Atom& value)
{
	const URIs& uris = _app->uris();
	if (predicate == uris.doap_name) {
		Rows::const_iterator i = _rows.find(plugin);
		if (i != _rows.end() && value.type() == uris.forge.String) {
			(*i->second)[_plugins_columns._col_name] = value.ptr<char>();
		}
	}
}

} // namespace gui
} // namespace ingen