/*
 * This file is part of gitg
 *
 * Copyright (C) 2026 - Alberto Fanjul
 *
 * gitg 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.
 *
 * gitg 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 gitg. If not, see <http://www.gnu.org/licenses/>.
 */


namespace Gitg
{

public class UiUtils
{
	// Do this to pull in config.h before glib.h (for gettext...)
	private const string version = Gitg.Config.VERSION;

	public static Gtk.Dialog build_dialog(Gtk.Window parent, Gee.HashMap<string, string> dialog_vars, string? stdout_data, string? stderr_data) {
		var dlg = new Gitg.ResultDialog(parent,
		                                dialog_vars.get("dialog-title"),
		                                dialog_vars.get("dialog-label"));
		dlg.response.connect((d, resp) => {
		  dlg.destroy();
		});
		dlg.append_message(stdout_data);
		dlg.append_message(stderr_data);
		return dlg;
	}

	public delegate Gtk.MenuItem BuildMenuItem(string action_prefix, Gee.HashMap<string, Gtk.MenuItem> item_groups);

	public static void add_custom_actions(Gtk.Menu menu,
	                                      string type,
	                                      Ggit.Config config,
	                                      Regex regex_custom_actions,
	                                      Regex regex_custom_actions_group,
	                                      BuildMenuItem build_menu_item)
	{
		menu.set_data("items", 0);
		try
		{
			var item_groups = new Gee.HashMap<string, Gtk.MenuItem> ();
			config.match_foreach(regex_custom_actions_group, (match_info, val) => {
				if (!item_groups.has_key(val)) {
					var item_group = new Gtk.MenuItem.with_label(val);
					item_groups.set(val, item_group);
				}
				return 0;
			});
			config.match_foreach(regex_custom_actions, (match_info, val) => {
				string group = match_info.fetch(1);
				debug ("found custom action group: %s", group);
				string custom_link_regexp = val;
				debug ("found custom action value: %s", val);
				string action_key_prefix = "gitg.actions.%s.%s.".printf(type, group);
				try
				{
					var item = build_menu_item(action_key_prefix, item_groups);
					if (item != null) {
						item.show();
						menu.append(item);
						int items = menu.get_data<int>("items");
						menu.set_data("items", ++items);
					}
				} catch (Error e)
				{
					warning ("Cannot read git config: %s", e.message);
				}
				return 0;
			});

			foreach (var item in item_groups.values) {
				if (item.get_data<int>("items") > 0) {
					menu.append(item);
					int items = menu.get_data<int>("items");
					menu.set_data("items", ++items);
					item.show();
				}
			}
		} catch (Error e)
		{
			warning ("Cannot read git config: %s", e.message);
		}
	}

	public static string? render_template(string tmpl,
	                                     Gee.HashMap<string,string> object_vars,
	                                     Gee.HashMap<string,string> dialog_vars) {
		string result = tmpl;
		foreach (var k in object_vars.keys) {
			var placeholder = "${" + k + "}";
			var val = object_vars.get(k);
			if (val == null)
				continue;
			var tmp = result.replace (placeholder, val);
			if (tmp != null)
				result = tmp;
			placeholder = "$" + k;
			tmp = result.replace (placeholder, val);
			if (tmp != null)
				result = tmp;
		}
		var placeholder = "$input";
		if (result.contains(placeholder)) {
			var replacement = run_entry_dialog(null, dialog_vars);
			if (replacement != null) {
				var tmp = result.replace (placeholder, replacement);
				if (tmp != null)
					result = tmp;
			} else
				result = null;
		}
		placeholder = "$question";
		if (result.contains(placeholder)) {
			var replacement = run_question_dialog(null, dialog_vars);
			if (replacement != null) {
				var tmp = result.replace (placeholder, replacement);
				if (tmp != null)
					result = tmp;
			} else
				result = null;
		}
		return result;
	}

	private static string? get_config_string(Ggit.Config conf, string key, string? def) {
		string val;
		try {
			val = conf.get_string(key);
		} catch {
			val = def;
		}
		return val;
	}

	private static bool get_config_bool(Ggit.Config conf, string key, bool def) {
		bool val;
		try {
			val = conf.get_bool(key);
		} catch {
			val = def;
		}
		return val;
	}

	private static void add_from_config(Gee.HashMap<string,string> input_vars,
	                                    Ggit.Config config, string prefix, string key, string? def) {
		input_vars.set(key, get_config_string(config, prefix+key, def));
	}

	public delegate Gee.HashMap<string, string> BuildObjectVars();

	public static Gtk.MenuItem build_custom_action(Gitg.Window parent, Ggit.Config conf,
	                                               string action_key_prefix,
	                                               Gee.HashMap<string, Gtk.MenuItem> groups,
												   //BuildDialog build_dialog,
												   BuildObjectVars build_object_vars = null)
	{
		string name = get_config_string(conf, action_key_prefix+"name", "");
		string description = get_config_string(conf, action_key_prefix+"description", "");
		string command = get_config_string(conf, action_key_prefix+"command", "");
		string available = get_config_string(conf, action_key_prefix+"available", "");
		string enabled = get_config_string(conf, action_key_prefix+"enabled", "");
		string shortcut = get_config_string(conf, action_key_prefix+"shortcut", null);
		bool show_output = get_config_bool(conf, action_key_prefix+"show-output", false);
		bool show_error = get_config_bool(conf, action_key_prefix+"show-error", true);

		var dialog_vars = new Gee.HashMap<string,string> ();
		add_from_config(dialog_vars, conf, action_key_prefix, "dialog-title", _("Output"));
		add_from_config(dialog_vars, conf, action_key_prefix, "dialog-label", _("Results:"));
		add_from_config(dialog_vars, conf, action_key_prefix, "input-title", _("Input"));
		add_from_config(dialog_vars, conf, action_key_prefix, "input-label", _("Value:"));
		add_from_config(dialog_vars, conf, action_key_prefix, "input-placeholder", null);
		add_from_config(dialog_vars, conf, action_key_prefix, "input-text", null);
		add_from_config(dialog_vars, conf, action_key_prefix, "input-yes", null);
		add_from_config(dialog_vars, conf, action_key_prefix, "input-no", null);
		add_from_config(dialog_vars, conf, action_key_prefix, "input-history", "false");
		if ("false" == dialog_vars.get("input-history")) {
			dialog_vars.remove("input-history");
		} else {
			var input_history_path = dialog_vars.get("input-history");
			if ("true" == input_history_path) {
				input_history_path = "~/.config/gitg/history/$group.$name";
			}
			var group = get_config_string(conf, action_key_prefix+"group", "");
			input_history_path = input_history_path.replace("$group.", group.down());
			input_history_path = input_history_path.replace("$name", name.down());

			dialog_vars.set("input-history", Gitg.Utils.expand_home_dir(input_history_path));
		}
		add_from_config(dialog_vars, conf, action_key_prefix, "question-title", _("Question"));
		add_from_config(dialog_vars, conf, action_key_prefix, "question-message", _("Are you sure?"));
		add_from_config(dialog_vars, conf, action_key_prefix, "question-yes", null);
		add_from_config(dialog_vars, conf, action_key_prefix, "question-no", null);
		add_from_config(dialog_vars, conf, action_key_prefix, "question-level", null);

		var object_vars = new Gee.HashMap<string, string>();
		if (build_object_vars != null) {
			object_vars = build_object_vars();
		}

		try {
			var available_flag = conf.get_bool(action_key_prefix+"available");
			if (!available_flag)
				return null;
		} catch {}
		if (available != "") {
			var available_command = render_template (available, object_vars, dialog_vars);
			if (available_command == null)
				return null;

			int exit_status;
			string stdout_data, stderr_data;
			bool spawned = GLib.Process.spawn_command_line_sync (available_command,
			                                                     out stdout_data,
			                                                     out stderr_data,
			                                                     out exit_status);
			if (!spawned) {
				stderr.printf ("Failed to check available action %s\n: %s\n", name, stderr_data);
				return null;
			}

			if (exit_status != 0)
				return null;
		} else {
			bool available_flag = false;
			try {
				available_flag = conf.get_bool(action_key_prefix+"available");
			} catch {}
			if (!available_flag)
				return null;
		}

		var item = new Gtk.MenuItem.with_label(name);

		bool enabled_is_flag = false;
		try {
			var enabled_flag = conf.get_bool(action_key_prefix+"enabled");
			if (!enabled_flag)
				item.sensitive = false;
			enabled_is_flag = true;
		} catch {}
		if (!enabled_is_flag && enabled != "") {
			var enabled_command = render_template (enabled, object_vars, dialog_vars);
			if (enabled_command == null)
				item.sensitive = false;
			else {
				int exit_status;
				string stdout_data, stderr_data;
				bool spawned = GLib.Process.spawn_command_line_sync (enabled_command,
				                                                out stdout_data,
				                                                out stderr_data,
				                                                out exit_status);
				if (!spawned) {
					stderr.printf ("Failed to check enabled action %s\n: %s\n", name, stderr_data);
					item.sensitive = false;
				} else if (exit_status != 0)
					item.sensitive = false;
			}
		}

		var item_result = item;

		try {
			var group = conf.get_string(action_key_prefix+"group");
			var item_group = groups.get(group);
			Gtk.Menu submenu;
			if (item_group.submenu != null)
				submenu = item_group.submenu;
			else {
				submenu = new Gtk.Menu();
				item_group.submenu = submenu;
				item_group.set_data("items", 0);
			}
			submenu.add(item);
			int items = item_group.get_data<int>("items");
			item_group.set_data("items", ++items);
			item.show();
			item_result = null;
		} catch {}

		item.set_tooltip_text(description);

		if (shortcut != null) {
			uint key;
			Gdk.ModifierType mods;

			Gtk.accelerator_parse (shortcut, out key, out mods);

			//var accel_group = new Gtk.AccelGroup ();
			var accel_group = parent.get_accel_group();
			//TODO: Get accel_group from main window

			if (key != 0) {
				item.add_accelerator (
					"activate",
					accel_group,
					key,
					mods,
					Gtk.AccelFlags.VISIBLE
				);
			}
		}

		item.activate.connect(() => {
			var cmd = render_template (command, object_vars, dialog_vars);
			if (cmd == null)
				return;

			int exit_status;
			string stdout_data, stderr_data;

			bool spawned = GLib.Process.spawn_command_line_sync (cmd,
			                                                out stdout_data,
			                                                out stderr_data,
			                                                out exit_status);
			if (!spawned) {
				stderr.printf ("Failed to start command: %s\n", stderr_data);
				return;
			}

			if (show_output && stdout_data != null) {
				stdout.printf (stdout_data);
				var dlg = build_dialog(parent, dialog_vars, stdout_data, null);
				dlg.show();
			}

			if (exit_status != 0 && show_error) {
				stdout.printf (stdout_data);
				stdout.printf (stderr_data);

				var dlg = build_dialog(parent, dialog_vars, stdout_data, stderr_data);
				dlg.show();
			}
		});

		return item_result;
	}

	public static string? get (Gee.HashMap<string, string> map, string key, string? def) {
		bool has_key_not_null = map.has_key (key) && map.get(key) != null;
		return has_key_not_null ? map.get(key) : def;
	}

	public static string? run_question_dialog (Gtk.Window? parent,
	                                        Gee.HashMap<string,string> vars) {
		var title = get(vars, "question-title", _("Input"));
		var message = get(vars, "question-message", "");
		var level = get(vars, "question-level", "info");
		var cancel = get(vars, "question-no", _("Cancel"));
		var yes = get(vars, "question-yes", _("Ok"));
		Gtk.MessageType type;
		string icon_name;
		switch(level) {
			case "info":
				type = Gtk.MessageType.INFO;
				icon_name = "dialog-information-symbolic";
				break;
			case "warning":
				type = Gtk.MessageType.WARNING;
				icon_name = "dialog-warning-symbolic";
				break;
			case "error":
				type = Gtk.MessageType.ERROR;
				icon_name = "dialog-error-symbolic";
				break;
			case "other":
				type = Gtk.MessageType.OTHER;
				icon_name = "dialog-question-symbolic";
				break;
			default:
				type = Gtk.MessageType.INFO;
				icon_name = "dialog-information-symbolic";
				break;
		}
		var dialog = new Gtk.MessageDialog (parent,
		                                    Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
		                                    type, Gtk.ButtonsType.OK_CANCEL, message);

		var img = new Gtk.Image.from_icon_name (icon_name, Gtk.IconSize.DIALOG);
		dialog.set_image (img);

		dialog.set_default_size (360, 120);
		dialog.set_resizable (false);

		dialog.show_all ();

		int response = dialog.run ();
		string? result = null;
		if (response == (int) Gtk.ResponseType.OK) {
			result = "";
		}

		dialog.destroy ();
		return result;
	}

	public static string? run_entry_dialog (Gtk.Window? parent,
	                                        Gee.HashMap<string,string> vars) {
		var title = get(vars, "input-title", _("Input"));
		var cancel = get(vars, "input-no", _("Cancel"));
		var yes = get(vars, "input-yes", _("Ok"));
		var label_text = get(vars, "input-label", _("Value"));
		var placeholder_text = get(vars, "input-placeholder", null);
		var force_show_placeholder = bool.parse(get(vars, "input-force-show-placeholder", "false"));
		var initial_text = get(vars, "input-text", null);
		var history_path = get(vars, "input-history", null);

		var dialog = new Gtk.Dialog.with_buttons (
			title,
			parent,
			Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT |
			Gtk.DialogFlags.USE_HEADER_BAR,
			cancel, Gtk.ResponseType.CANCEL,
			yes, Gtk.ResponseType.OK
		);
		dialog.set_default_size (360, 120);
		dialog.set_resizable (false);

		var content = dialog.get_content_area ();
		var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6);
		box.set_margin_top (12);
		box.set_margin_bottom (12);
		box.set_margin_start (12);
		box.set_margin_end (12);
		content.pack_start (box, true, true, 0);

		var lbl = new Gtk.Label (label_text);
		lbl.set_halign (Gtk.Align.START);
		box.pack_start (lbl, false, false, 0);

		Gtk.Entry entry;
		if (history_path != null) {
			entry = new Gitg.EntryHistory (history_path);
		} else {
			entry = new Gtk.Entry ();
		}
		if (placeholder_text != null) {
			entry.set_placeholder_text (placeholder_text);
			if (force_show_placeholder)
				entry.set_can_focus(false);
		}
		if (initial_text != null)
			entry.set_text (initial_text);
		entry.set_activates_default (true);
		box.pack_start (entry, false, false, 0);

		var ok_button = dialog.get_widget_for_response (Gtk.ResponseType.OK) as Gtk.Button;
		if (ok_button != null) {
			dialog.set_default_response (Gtk.ResponseType.OK);
			ok_button.set_sensitive (entry.text.strip().length > 0);
		}

		entry.changed.connect (() => {
			if (ok_button != null)
				ok_button.set_sensitive (entry.text.strip().length > 0);
		});

		dialog.show_all ();

		if (force_show_placeholder &&  placeholder_text != null) {
			GLib.Idle.add (() => {
				entry.set_can_focus (true);
				return false;
			});
		}

		int response = dialog.run ();
		string? result = null;
		if (response == (int) Gtk.ResponseType.OK) {
			result = entry.get_text ();
		}

		dialog.destroy ();
		return result;
	}

	public delegate void UpdateTreeViewColumnsCallBack(Gtk.TreeView treeview);

	public static void store_visible_columns_on_gsettings(Gtk.TreeView treeview) {
		string[] visible_columns = {};
		print("storing columns [");
		foreach( var col in treeview.get_columns()) {
			if (!col.visible)
				continue;
			var cmc = col.get_data<Gitg.CommitModelColumns>("enum");
			 print("%s,", cmc.nick());
			visible_columns += cmc.nick();
		}
		print("]\n");
		var settings = new Settings(Gitg.Config.APPLICATION_ID + ".preferences.general");
		settings.set_strv ("visible-columns", visible_columns);
	}

	public static Gtk.TreeView build_treeview_visible_columns(string[] visible_columns,
	                                                          UpdateTreeViewColumnsCallBack callback) {
		var treeview = new Gtk.TreeView();
		EnumClass ec = (EnumClass) typeof (Gitg.CommitModelColumns).class_ref ();

		for (int i = 0; i < visible_columns.length; i++) {
			unowned EnumValue? ev = ec.get_value_by_nick (visible_columns[i]);
			if (ev != null) {
				var cmc = (Gitg.CommitModelColumns)ev.value;
				var column = new Gtk.TreeViewColumn ();
				column.set_data<Gitg.CommitModelColumns>("enum", cmc);
				column.title = cmc.name();
				column.visible = true;
				column.notify["visible"].connect (() => {
					callback (treeview);
				});
				treeview.append_column(column);
			}
		}

		EnumClass enum_class = (EnumClass) typeof (Gitg.CommitModelColumns).class_ref ();
		var non_visible_columns =enum_class.n_values - 2 - visible_columns.length;
		Gitg.CommitModelColumns[] enum_columns = new Gitg.CommitModelColumns[non_visible_columns];

		for (int i = 0; i < enum_class.n_values; i++) {
			unowned EnumValue enum_value = enum_class.values[i];
			var cmc = (Gitg.CommitModelColumns)enum_value.value;
			if (cmc == Gitg.CommitModelColumns.NUM ||
			    cmc == Gitg.CommitModelColumns.COMMIT ||
			    enum_value.value_nick in visible_columns) {
				continue;
			}
			var column = new Gtk.TreeViewColumn ();
			column.set_data<Gitg.CommitModelColumns>("enum", cmc);
			column.title = cmc.name();
			column.visible = false;
			column.notify["visible"].connect (() => {
				callback (treeview);
			});
			treeview.append_column(column);
		}
		return treeview;
	}

	public static DragListBox build_listbox_visible_columns (Gtk.TreeView treeview) {
		var list_model = new GLib.ListStore(typeof(ListRow));
		var listbox = new DragListBox(list_model);
		listbox.set_selection_mode (Gtk.SelectionMode.NONE);

		listbox.row_reorder.connect((from, to) => {
			sync_listbox_actions(list_model, listbox);
			var cols = treeview.get_columns();
			int n = (int)cols.length();
			if (n == 0)
				return;

			var col = cols.nth_data(from);

			Gtk.TreeViewColumn[] without = new Gtk.TreeViewColumn[n - 1];
			int j = 0;
			for (int i = 0; i < n; i++) {
				var current_col = cols.nth_data(i);
				if (current_col == col)
					continue;
				if (!current_col.visible)
					continue;
				without[j++] = current_col;
			}

			Gtk.TreeViewColumn? base_col = null;
			if (to > 0)
				base_col = without[to - 1];

			treeview.move_column_after(col, base_col);
		});

		list_model.items_changed.connect((p, r, a) => {
			sync_listbox_actions(list_model, listbox);
		});

		sync_listbox_actions(list_model, listbox);

		foreach (var column in treeview.get_columns()) {
			list_model.append(new ListRow(column.title, column));
		}

		treeview.columns_changed.connect(() => {
			var cols = treeview.get_columns();

			for (int i = 0; i < cols.length(); i++) {
				var title = cols.nth_data(i).title;
				var children = listbox.get_children();
				int from = -1;
				ListBoxRowDnD rowj = null;
				for (int j = 0; j < children.length(); j++) {
					rowj = listbox.get_row_at_index(j) as ListBoxRowDnD;
					var row_title = rowj.title;
					if (row_title == title) {
						from = j;
						break;
					}
				}
				if (from == -1)
					continue;

				int to = i;

				if (to == from)
					continue;

				var list_row = (ListRow)list_model.get_item(from);
				list_model.remove(from);
				list_model.insert(to, list_row);
			}
		});

		return listbox;
	}

	public static Gtk.Box build_switch_show_headers (Gtk.TreeView treeview) {
		var settings = new Settings(Gitg.Config.APPLICATION_ID + ".preferences.general");
		var switch_show_col_headers = new Gtk.Switch();
		var label = new Gtk.Label("Show column headers");
		switch_show_col_headers.state_set.connect ((state) => {
			treeview.headers_visible = switch_show_col_headers.active;
			settings.set_boolean ("columns-header-visible", treeview.headers_visible);
			return false;
		});
		switch_show_col_headers.active = settings.get_boolean ("columns-header-visible");
		var box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 5);
		box.pack_start(switch_show_col_headers, false, false, 0);
		box.pack_start(label, false, false, 0);

		return box;
	}

	public static void sync_row_actions(GLib.ListStore list_model, ListBoxRowDnD row, int pos) {
		bool up = true;
		bool down = true;
		int count = (int)list_model.get_n_items();

		if (pos == 0) {
			up = false;
		}
		if (pos == count - 1) {
			down = false;
		}
		row.enable_up(up);
		row.enable_down(down);
	}

	public static void sync_listbox_actions(GLib.ListStore list_model, DragListBox listbox) {
		int pos = 0;
		var row = listbox.get_row_at_index(pos) as ListBoxRowDnD;
		while( row != null) {
			sync_row_actions(list_model, row, pos);
			row = listbox.get_row_at_index(++pos) as ListBoxRowDnD;
		}
	}
}
}

// ex:ts=4 noet
