/******************************************************************************
License: GNU General Public License v3.0 only
Copyright:
	(C) 2008 - 2024 Alexander Shaduri <ashaduri@gmail.com>
******************************************************************************/
/// \file
/// \author Alexander Shaduri
/// \ingroup gsc
/// \weakgroup gsc
/// @{

#include "hz/string_algo.h"
#include <glibmm.h>
#include <gtkmm.h>
#include <gdk/gdk.h>  // GDK_KEY_Escape
#include <sstream>
#include <cstddef>  // std::size_t
#include <memory>
#include <vector>

#include "applib/app_gtkmm_tools.h"  // app_gtkmm_create_tree_view_column
#include "hz/fs.h"
#include "rconfig/rconfig.h"

#include "gsc_executor_log_window.h"
#include "gsc_init.h"  // app_get_debug_buffer_str()




GscExecutorLogWindow::GscExecutorLogWindow(BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder> ui)
		: AppBuilderWidget<GscExecutorLogWindow, false>(gtkcobj, std::move(ui))
{
	// Connect callbacks

	Gtk::Button* window_close_button = nullptr;
	APP_BUILDER_AUTO_CONNECT(window_close_button, clicked);

	Gtk::Button* window_save_current_button = nullptr;
	APP_BUILDER_AUTO_CONNECT(window_save_current_button, clicked);

	Gtk::Button* window_save_all_button = nullptr;
	APP_BUILDER_AUTO_CONNECT(window_save_all_button, clicked);


	Gtk::Button* clear_command_list_button = nullptr;
	APP_BUILDER_AUTO_CONNECT(clear_command_list_button, clicked);



	// Accelerators

	const Glib::RefPtr<Gtk::AccelGroup> accel_group = this->get_accel_group();
	if (window_close_button) {
		window_close_button->add_accelerator("clicked", accel_group, GDK_KEY_Escape,
				Gdk::ModifierType(0), Gtk::AccelFlags(0));
	}


	// --------------- Make a treeview

	auto* treeview = this->lookup_widget<Gtk::TreeView*>("command_list_treeview");
	if (treeview) {
		Gtk::TreeModelColumnRecord model_columns;

		// #, Command + parameters, [EntryPtr]

		model_columns.add(col_num_);
		app_gtkmm_create_tree_view_column(col_num_, *treeview,
				"#", _("# of executed command"), true);  // sortable

		model_columns.add(col_command_);
		app_gtkmm_create_tree_view_column(col_command_, *treeview,
				_("Command"), _("Command with parameters"), true);  // sortable

		model_columns.add(col_entry_);


		// create a TreeModel (ListStore)
		list_store_ = Gtk::ListStore::create(model_columns);
		// list_store->set_sort_column(col_num, Gtk::SORT_DESCENDING);  // default sort
		treeview->set_model(list_store_);


		selection_ = treeview->get_selection();
		selection_->signal_changed().connect(sigc::mem_fun(*this,
				&GscExecutorLogWindow::on_tree_selection_changed) );

	}



	// Hide command text entry in win32.
	// Setting text on this entry segfaults under win32, (utf8 conversion doesn't
	// help, not that it should). Seems to be connected to non-english locale.
	// Surprisingly, the treeview column text still works.

	// The problem seems to have disappeared (new compiler/runtime?)
// if constexpr(BuildEnv::is_kernel_family_windows()) {
// 	Gtk::Box* command_hbox = this->lookup_widget<Gtk::Box*>("command_hbox");
// 	if (command_hbox)
// 		command_hbox->hide();
// }

	// ---------------

	// Connect to CommandExecutor signal
	cmdex_sync_signal_execute_finish().connect(sigc::mem_fun(*this,
			&GscExecutorLogWindow::on_command_output_received));

	// show();
}



void GscExecutorLogWindow::show_last()
{
	auto* treeview = this->lookup_widget<Gtk::TreeView*>("command_list_treeview");

	if (treeview != nullptr && !list_store_->children().empty()) {
// 		Gtk::TreeRow row = *(list_store->children().rbegin());  // this causes invalid read error in valgrind
		const Gtk::TreeRow row = *(--(list_store_->children().end()));
		selection_->select(row);
		// you would think that scroll_to_row would accept a TreeRow for a change (shock!)
		treeview->scroll_to_row(list_store_->get_path(row));
	}

	show();
}



void GscExecutorLogWindow::clear_view_widgets()
{
	auto* window_save_current_button = this->lookup_widget<Gtk::Button*>("window_save_current_button");
	if (window_save_current_button)
		window_save_current_button->set_sensitive(false);

	auto* output_textview = this->lookup_widget<Gtk::TextView*>("output_textview");
	if (output_textview) {
		const Glib::RefPtr<Gtk::TextBuffer> buffer = output_textview->get_buffer();
		buffer->set_text("");
	}

	auto* command_entry = this->lookup_widget<Gtk::Entry*>("command_entry");
	if (command_entry)
		command_entry->set_text("");
}



void GscExecutorLogWindow::on_command_output_received(const CommandExecutorResult& info)
{
	auto entry = std::make_shared<CommandExecutorResult>(info);
	entries_.push_back(entry);

	std::vector<std::string> command = {info.command};
	command.insert(command.end(), info.parameters.begin(), info.parameters.end());

	// update tree model
	const Gtk::TreeRow row = *(list_store_->append());
	row[col_num_] = entries_.size();
	row[col_command_] = hz::string_join(command, " ");
	row[col_entry_] = entry;

	// if visible, set the selection to it
	if (auto* treeview = this->lookup_widget<Gtk::TreeView*>("command_list_treeview")) {
		selection_->select(row);
		treeview->scroll_to_row(list_store_->get_path(row));
	}
}



bool GscExecutorLogWindow::on_delete_event([[maybe_unused]] GdkEventAny* e)
{
	on_window_close_button_clicked();
	return true;  // event handled
}



void GscExecutorLogWindow::on_window_close_button_clicked()
{
	this->hide();  // hide only, don't destroy
}



void GscExecutorLogWindow::on_window_save_current_button_clicked()
{
	if (selection_->count_selected_rows() == 0)
		return;

	const Gtk::TreeIter iter = selection_->get_selected();
	const std::shared_ptr<CommandExecutorResult> entry = (*iter)[col_entry_];

	static std::string last_dir;
	if (last_dir.empty()) {
		last_dir = rconfig::get_data<std::string>("gui/drive_data_open_save_dir");
	}
	int result = 0;

	const Glib::RefPtr<Gtk::FileFilter> specific_filter = Gtk::FileFilter::create();
	specific_filter->set_name(_("JSON and Text Files"));
	specific_filter->add_pattern("*.json");
	specific_filter->add_pattern("*.txt");

	const Glib::RefPtr<Gtk::FileFilter> all_filter = Gtk::FileFilter::create();
	all_filter->set_name(_("All Files"));
	all_filter->add_pattern("*");

#if GTK_CHECK_VERSION(3, 20, 0)
	const std::unique_ptr<GtkFileChooserNative, decltype(&g_object_unref)> dialog(gtk_file_chooser_native_new(
			_("Save Data As..."), this->gobj(), GTK_FILE_CHOOSER_ACTION_SAVE, nullptr, nullptr),
			&g_object_unref);

	gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog.get()), TRUE);

	gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog.get()), specific_filter->gobj());
	gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog.get()), all_filter->gobj());

	if (!last_dir.empty())
		gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog.get()), last_dir.c_str());

	gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog.get()), ".txt");

	result = gtk_native_dialog_run(GTK_NATIVE_DIALOG(dialog.get()));

#else
	Gtk::FileChooserDialog dialog(*this, _("Save Data As..."),
			Gtk::FILE_CHOOSER_ACTION_SAVE);

	// Add response buttons the the dialog
	dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
	dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_ACCEPT);

	dialog.set_do_overwrite_confirmation(true);

	dialog.add_filter(specific_filter);
	dialog.add_filter(all_filter);

	if (!last_dir.empty())
		dialog.set_current_folder(last_dir);

	dialog.set_current_name(".json");

	// Show the dialog and wait for a user response
	result = dialog.run();  // the main cycle blocks here
#endif

	// Handle the response
	switch (result) {
		case Gtk::RESPONSE_ACCEPT:
		{
			hz::fs::path file;
#if GTK_CHECK_VERSION(3, 20, 0)
			file = hz::fs_path_from_string(app_string_from_gchar(gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog.get()))));
			last_dir = hz::fs_path_to_string(file.parent_path());
#else
			file = hz::fs_path_from_string(dialog.get_filename());  // in fs encoding
			last_dir = dialog.get_current_folder();  // save for the future
#endif
			rconfig::set_data("gui/drive_data_open_save_dir", last_dir);

			if (file.extension() != ".json" && file.extension() != ".txt") {
				file += ".json";
			}

			auto ec = hz::fs_file_put_contents(file, entry->std_output);
			if (ec) {
				gui_show_error_dialog(_("Cannot save data to file"), ec.message(), this);
			}
			break;
		}

		case Gtk::RESPONSE_CANCEL: case Gtk::RESPONSE_DELETE_EVENT:
			// nothing, the dialog is closed already
			break;

		default:
			debug_out_error("app", DBG_FUNC_MSG << "Unknown dialog response code: " << result << ".\n");
			break;
	}

}




void GscExecutorLogWindow::on_window_save_all_button_clicked()
{
	// complete libdebug output + execution logs

	std::ostringstream exss;

	exss << "\n------------------------- LIBDEBUG LOG -------------------------\n\n\n";
	exss << app_get_debug_buffer_str() << "\n\n\n";

	exss << "\n\n\n------------------------- EXECUTION LOG -------------------------\n\n\n";

	for (std::size_t i = 0; i < entries_.size(); ++i) {
		exss << "\n\n\n------------------------- EXECUTED COMMAND " << (i+1) << " -------------------------\n\n";
		exss << "\n---------------" << "Command" << "---------------\n";
		exss << entries_[i]->command << "\n";
		exss << "\n---------------" << "Parameters" << "---------------\n";
		for (const auto& param : entries_[i]->parameters) {
			exss << param << "\n";
		}
		exss << "\n---------------" << "STDOUT" << "---------------\n";
		exss << entries_[i]->std_output << "\n\n";
		exss << "\n---------------" << "STDERR" << "---------------\n";
		exss << entries_[i]->std_error << "\n\n";
		exss << "\n---------------" << "Error Message" << "---------------\n";
		exss << entries_[i]->error_message << "\n\n";
	}


	static std::string last_dir;
	if (last_dir.empty()) {
		last_dir = rconfig::get_data<std::string>("gui/drive_data_open_save_dir");
	}
	int result = 0;

	const Glib::RefPtr<Gtk::FileFilter> specific_filter = Gtk::FileFilter::create();
	specific_filter->set_name(_("Text Files"));
	specific_filter->add_pattern("*.txt");

	const Glib::RefPtr<Gtk::FileFilter> all_filter = Gtk::FileFilter::create();
	all_filter->set_name(_("All Files"));
	all_filter->add_pattern("*");

#if GTK_CHECK_VERSION(3, 20, 0)
	const std::unique_ptr<GtkFileChooserNative, decltype(&g_object_unref)> dialog(gtk_file_chooser_native_new(
			_("Save Data As..."), this->gobj(), GTK_FILE_CHOOSER_ACTION_SAVE, nullptr, nullptr),
			&g_object_unref);

	gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog.get()), TRUE);

	gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog.get()), specific_filter->gobj());
	gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog.get()), all_filter->gobj());

	if (!last_dir.empty())
		gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog.get()), last_dir.c_str());

	gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog.get()), ".txt");

	result = gtk_native_dialog_run(GTK_NATIVE_DIALOG(dialog.get()));

#else
	Gtk::FileChooserDialog dialog(*this, _("Save Data As..."),
			Gtk::FILE_CHOOSER_ACTION_SAVE);

	// Add response buttons the the dialog
	dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
	dialog.add_button(Gtk::Stock::SAVE, Gtk::RESPONSE_ACCEPT);

	dialog.set_do_overwrite_confirmation(true);

	dialog.add_filter(specific_filter);
	dialog.add_filter(all_filter);

	if (!last_dir.empty())
		dialog.set_current_folder(last_dir);

	dialog.set_current_name(".txt");

	// Show the dialog and wait for a user response
	result = dialog.run();  // the main cycle blocks here
#endif

	// Handle the response
	switch (result) {
		case Gtk::RESPONSE_ACCEPT:
		{
			hz::fs::path file;
#if GTK_CHECK_VERSION(3, 20, 0)
			file = hz::fs_path_from_string(app_string_from_gchar(gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog.get()))));
			last_dir = hz::fs_path_to_string(file.parent_path());
#else
			file = hz::fs_path_from_string(dialog.get_filename());  // in fs encoding
			last_dir = dialog.get_current_folder();  // save for the future
#endif
			rconfig::set_data("gui/drive_data_open_save_dir", last_dir);

			if (file.extension() != ".txt") {
				file += ".txt";
			}

			auto ec = hz::fs_file_put_contents(file, exss.str());
			if (ec) {
				gui_show_error_dialog(_("Cannot save data to file"), ec.message(), this);
			}
			break;
		}

		case Gtk::RESPONSE_CANCEL: case Gtk::RESPONSE_DELETE_EVENT:
			// nothing, the dialog is closed already
			break;

		default:
			debug_out_error("app", DBG_FUNC_MSG << "Unknown dialog response code: " << result << ".\n");
			break;
	}

}



void GscExecutorLogWindow::on_clear_command_list_button_clicked()
{
	entries_.clear();
	list_store_->clear();  // this will unselect & clear widgets too.
}



void GscExecutorLogWindow::on_tree_selection_changed()
{
	this->clear_view_widgets();

	if (selection_->count_selected_rows() > 0) {
		const Gtk::TreeIter iter = selection_->get_selected();
		const Gtk::TreeRow& row = *iter;

		const std::shared_ptr<CommandExecutorResult> entry = row[col_entry_];

		if (auto* output_textview = this->lookup_widget<Gtk::TextView*>("output_textview")) {
			const Glib::RefPtr<Gtk::TextBuffer> buffer = output_textview->get_buffer();
			if (buffer) {
				buffer->set_text(app_make_valid_utf8_from_command_output(entry->std_output));

				Glib::RefPtr<Gtk::TextTag> tag;
				const Glib::RefPtr<Gtk::TextTagTable> table = buffer->get_tag_table();
				if (table)
					tag = table->lookup("font");
				if (!tag)
					tag = buffer->create_tag("font");

				tag->property_family() = "Monospace";
				buffer->apply_tag(tag, buffer->begin(), buffer->end());
			}
		}

		if (auto* command_entry = this->lookup_widget<Gtk::Entry*>("command_entry")) {
			std::vector<std::string> command = {entry->command};
			command.insert(command.end(), entry->parameters.begin(), entry->parameters.end());
			command_entry->set_text(app_make_valid_utf8_from_command_output(hz::string_join(command, " ")));
		}

		if (auto* window_save_current_button = this->lookup_widget<Gtk::Button*>("window_save_current_button"))
			window_save_current_button->set_sensitive(true);
	}

}







/// @}
