/*
 * Copyright © 2002 Red Hat, Inc.
 * Copyright © 2008 Christian Persch
 * Copyright (C) 2012-2021 MATE Developers
 *
 * Mate-terminal 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.
 *
 * Mate-terminal 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, see <http://www.gnu.org/licenses/>.
 */

#include <config.h>

#include <string.h>

#include <gtk/gtk.h>

#include "terminal-app.h"
#include "terminal-debug.h"
#include "terminal-encoding.h"
#include "terminal-intl.h"
#include "terminal-profile.h"
#include "terminal-util.h"

/* Overview
 *
 * There's a list of character sets stored in GSettings, indicating
 * which encodings to display in the encoding menu.
 *
 * We have a pre-canned list of available encodings
 * (hardcoded in the table below) that can be added to
 * the encoding menu, and to give a human-readable name
 * to certain encodings.
 *
 * If the GSettings list contains an encoding not in the
 * predetermined table, then that encoding is
 * labeled "user defined" but still appears in the menu.
 */

static const struct
{
	const char *charset;
	const char *name;
} encodings[] =
{
	{ "ISO-8859-1",	N_("Western") },
	{ "ISO-8859-2",	N_("Central European") },
	{ "ISO-8859-3",	N_("South European") },
	{ "ISO-8859-4",	N_("Baltic") },
	{ "ISO-8859-5",	N_("Cyrillic") },
	{ "ISO-8859-6",	N_("Arabic") },
	{ "ISO-8859-7",	N_("Greek") },
	{ "ISO-8859-8",	N_("Hebrew Visual") },
	{ "ISO-8859-8-I",	N_("Hebrew") },
	{ "ISO-8859-9",	N_("Turkish") },
	{ "ISO-8859-10",	N_("Nordic") },
	{ "ISO-8859-13",	N_("Baltic") },
	{ "ISO-8859-14",	N_("Celtic") },
	{ "ISO-8859-15",	N_("Western") },
	{ "ISO-8859-16",	N_("Romanian") },
	{ "UTF-8",	N_("Unicode") },
	{ "ARMSCII-8",	N_("Armenian") },
	{ "BIG5",	N_("Chinese Traditional") },
	{ "BIG5-HKSCS",	N_("Chinese Traditional") },
	{ "CP866",	N_("Cyrillic/Russian") },
	{ "EUC-JP",	N_("Japanese") },
	{ "EUC-KR",	N_("Korean") },
	{ "EUC-TW",	N_("Chinese Traditional") },
	{ "GB18030",	N_("Chinese Simplified") },
	{ "GB2312",	N_("Chinese Simplified") },
	{ "GBK",	N_("Chinese Simplified") },
	{ "GEORGIAN-PS",	N_("Georgian") },
	{ "IBM850",	N_("Western") },
	{ "IBM852",	N_("Central European") },
	{ "IBM855",	N_("Cyrillic") },
	{ "IBM857",	N_("Turkish") },
	{ "IBM862",	N_("Hebrew") },
	{ "IBM864",	N_("Arabic") },
	{ "ISO-2022-JP",	N_("Japanese") },
	{ "ISO-2022-KR",	N_("Korean") },
	{ "ISO-IR-111",	N_("Cyrillic") },
	{ "KOI8-R",	N_("Cyrillic") },
	{ "KOI8-U",	N_("Cyrillic/Ukrainian") },
	{ "MAC_ARABIC",	N_("Arabic") },
	{ "MAC_CE",	N_("Central European") },
	{ "MAC_CROATIAN",	N_("Croatian") },
	{ "MAC-CYRILLIC",	N_("Cyrillic") },
	{ "MAC_DEVANAGARI",	N_("Hindi") },
	{ "MAC_FARSI",	N_("Persian") },
	{ "MAC_GREEK",	N_("Greek") },
	{ "MAC_GUJARATI",	N_("Gujarati") },
	{ "MAC_GURMUKHI",	N_("Gurmukhi") },
	{ "MAC_HEBREW",	N_("Hebrew") },
	{ "MAC_ICELANDIC",	N_("Icelandic") },
	{ "MAC_ROMAN",	N_("Western") },
	{ "MAC_ROMANIAN",	N_("Romanian") },
	{ "MAC_TURKISH",	N_("Turkish") },
	{ "MAC_UKRAINIAN",	N_("Cyrillic/Ukrainian") },
	{ "SHIFT_JIS",	N_("Japanese") },
	{ "TCVN",	N_("Vietnamese") },
	{ "TIS-620",	N_("Thai") },
	{ "UHC",	N_("Korean") },
	{ "VISCII",	N_("Vietnamese") },
	{ "WINDOWS-1250",	N_("Central European") },
	{ "WINDOWS-1251",	N_("Cyrillic") },
	{ "WINDOWS-1252",	N_("Western") },
	{ "WINDOWS-1253",	N_("Greek") },
	{ "WINDOWS-1254",	N_("Turkish") },
	{ "WINDOWS-1255",	N_("Hebrew") },
	{ "WINDOWS-1256",	N_("Arabic") },
	{ "WINDOWS-1257",	N_("Baltic") },
	{ "WINDOWS-1258",	N_("Vietnamese") },
#if 0
	/* These encodings do NOT pass-through ASCII, so are always rejected.
	 * FIXME: why are they in this table; or rather why do we need
	 * the ASCII pass-through requirement?
	 */
	{ "UTF-7",  N_("Unicode") },
	{ "UTF-16", N_("Unicode") },
	{ "UCS-2",  N_("Unicode") },
	{ "UCS-4",  N_("Unicode") },
	{ "JOHAB",  N_("Korean") },
#endif
};

typedef struct
{
	GtkWidget *dialog;
	GtkListStore *base_store;
	GtkTreeView *available_tree_view;
	GtkTreeSelection *available_selection;
	GtkTreeModel *available_model;
	GtkTreeView *active_tree_view;
	GtkTreeSelection *active_selection;
	GtkTreeModel *active_model;
	GtkWidget *add_button;
	GtkWidget *remove_button;
} EncodingDialogData;

static GtkWidget *encoding_dialog = NULL;

TerminalEncoding *
terminal_encoding_new (const char *charset,
                       const char *display_name,
                       gboolean is_custom,
                       gboolean force_valid)
{
	TerminalEncoding *encoding;

	encoding = g_slice_new (TerminalEncoding);
	encoding->refcount = 1;
	encoding->id = g_strdup (charset);
	encoding->name = g_strdup (display_name);
	encoding->valid = encoding->validity_checked = force_valid;
	encoding->is_custom = is_custom;
	encoding->is_active = FALSE;

	return encoding;
}

TerminalEncoding*
terminal_encoding_ref (TerminalEncoding *encoding)
{
	g_return_val_if_fail (encoding != NULL, NULL);

	encoding->refcount++;
	return encoding;
}

void
terminal_encoding_unref (TerminalEncoding *encoding)
{
	if (--encoding->refcount > 0)
		return;

	g_free (encoding->name);
	g_free (encoding->id);
	g_slice_free (TerminalEncoding, encoding);
}

const char *
terminal_encoding_get_id (TerminalEncoding *encoding)
{
	g_return_val_if_fail (encoding != NULL, NULL);

	return encoding->id;
}

const char *
terminal_encoding_get_charset (TerminalEncoding *encoding)
{
	g_return_val_if_fail (encoding != NULL, NULL);

	if (strcmp (encoding->id, "current") == 0)
	{
		const char *charset;

		g_get_charset (&charset);
		return charset;
	}

	return encoding->id;
}

gboolean
terminal_encoding_is_valid (TerminalEncoding *encoding)
{
	/* All of the printing ASCII characters from space (32) to the tilde (126) */
	static const char ascii_sample[] =
	    " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
	char *converted;
	gsize bytes_read = 0, bytes_written = 0;
	GError *error = NULL;

	if (encoding->validity_checked)
		return encoding->valid;

	/* Test that the encoding is a proper superset of ASCII (which naive
	 * apps are going to use anyway) by attempting to validate the text
	 * using the current encoding.  This also flushes out any encodings
	 * which the underlying GIConv implementation can't support.
	 */
	converted = g_convert (ascii_sample, sizeof (ascii_sample) - 1,
	                       terminal_encoding_get_charset (encoding), "UTF-8",
	                       &bytes_read, &bytes_written, &error);

	/* The encoding is only valid if ASCII passes through cleanly. */
	encoding->valid = (bytes_read == (sizeof (ascii_sample) - 1)) &&
	                  (converted != NULL) &&
	                  (strcmp (converted, ascii_sample) == 0);

#ifdef MATE_ENABLE_DEBUG
	_TERMINAL_DEBUG_IF (TERMINAL_DEBUG_ENCODINGS)
	{
		if (!encoding->valid)
		{
			_terminal_debug_print (TERMINAL_DEBUG_ENCODINGS,
			                       "Rejecting encoding %s as invalid:\n",
			                       terminal_encoding_get_charset (encoding));
			_terminal_debug_print (TERMINAL_DEBUG_ENCODINGS,
			                       " input  \"%s\"\n",
			                       ascii_sample);
			_terminal_debug_print (TERMINAL_DEBUG_ENCODINGS,
			                       " output \"%s\" bytes read %" G_GSIZE_FORMAT " written %" G_GSIZE_FORMAT "\n",
			                       converted ? converted : "(null)", bytes_read, bytes_written);
			if (error)
				_terminal_debug_print (TERMINAL_DEBUG_ENCODINGS,
				                       " Error: %s\n",
				                       error->message);
		}
		else
			_terminal_debug_print (TERMINAL_DEBUG_ENCODINGS,
			                       "Encoding %s is valid\n\n",
			                       terminal_encoding_get_charset (encoding));
	}
#endif

	g_clear_error (&error);
	g_free (converted);

	encoding->validity_checked = TRUE;
	return encoding->valid;
}

GType
terminal_encoding_get_type (void)
{
	static GType type = 0;

	if (G_UNLIKELY (type == 0))
	{
		type = g_boxed_type_register_static (I_("TerminalEncoding"),
		                                     (GBoxedCopyFunc) terminal_encoding_ref,
		                                     (GBoxedFreeFunc) terminal_encoding_unref);
	}

	return type;
}

static void
update_active_encodings_gsettings (void)
{
	GSList *list, *l;
	GArray *strings;
	const gchar *id_string;

	list = terminal_app_get_active_encodings (terminal_app_get ());
	strings = g_array_new (TRUE, TRUE, sizeof (gchar *));
	for (l = list; l != NULL; l = l->next)
	{
		TerminalEncoding *encoding = (TerminalEncoding *) l->data;
		id_string = terminal_encoding_get_id (encoding);

		strings = g_array_append_val (strings, id_string);
	}

	g_settings_set_strv (settings_global, "active-encodings", (const gchar **) strings->data);

	g_array_free (strings, TRUE);
	g_slist_foreach (list, (GFunc) terminal_encoding_unref, NULL);
	g_slist_free (list);
}

static void
response_callback (GtkWidget *window,
                   int        id,
                   EncodingDialogData *data)
{
	if (id == GTK_RESPONSE_HELP)
		terminal_util_show_help ("mate-terminal-encoding-add", GTK_WINDOW (window));
	else
		gtk_widget_destroy (GTK_WIDGET (window));
}

enum
{
    COLUMN_NAME,
    COLUMN_CHARSET,
    COLUMN_DATA,
    N_COLUMNS
};

static void
selection_changed_cb (GtkTreeSelection *selection,
                      EncodingDialogData *data)
{
	GtkWidget *button;
	gboolean have_selection;

	if (selection == data->available_selection)
		button = data->add_button;
	else if (selection == data->active_selection)
		button = data->remove_button;
	else
		g_assert_not_reached ();

	have_selection = gtk_tree_selection_get_selected (selection, NULL, NULL);
	gtk_widget_set_sensitive (button, have_selection);
}

static void
button_clicked_cb (GtkWidget *button,
                   EncodingDialogData *data)
{
	GtkTreeSelection *selection;
	GtkTreeModel *model;
	GtkTreeIter filter_iter, iter;
	TerminalEncoding *encoding;

	if (button == data->add_button)
		selection = data->available_selection;
	else if (button == data->remove_button)
		selection = data->active_selection;
	else
		g_assert_not_reached ();

	if (!gtk_tree_selection_get_selected (selection, &model, &filter_iter))
		return;

	gtk_tree_model_filter_convert_iter_to_child_iter (GTK_TREE_MODEL_FILTER (model),
	        &iter,
	        &filter_iter);

	model = GTK_TREE_MODEL (data->base_store);
	gtk_tree_model_get (model, &iter, COLUMN_DATA, &encoding, -1);
	g_assert (encoding != NULL);

	if (button == data->add_button)
		encoding->is_active = TRUE;
	else if (button == data->remove_button)
		encoding->is_active = FALSE;
	else
		g_assert_not_reached ();

	terminal_encoding_unref (encoding);

	/* We don't need to emit row-changed here, since updating the GSettings pref
	 * will update the models.
	 */
	update_active_encodings_gsettings ();
}

static void
liststore_insert_encoding (gpointer key,
                           TerminalEncoding *encoding,
                           GtkListStore *store)
{
	GtkTreeIter iter;

	if (!terminal_encoding_is_valid (encoding))
		return;

	gtk_list_store_insert_with_values (store, &iter, -1,
	                                   COLUMN_CHARSET, terminal_encoding_get_charset (encoding),
	                                   COLUMN_NAME, encoding->name,
	                                   COLUMN_DATA, encoding,
	                                   -1);
}

static gboolean
filter_active_encodings (GtkTreeModel *child_model,
                         GtkTreeIter *child_iter,
                         gpointer data)
{
	TerminalEncoding *encoding;
	gboolean active = GPOINTER_TO_UINT (data);
	gboolean visible;

	gtk_tree_model_get (child_model, child_iter, COLUMN_DATA, &encoding, -1);
	visible = active ? encoding->is_active : !encoding->is_active;
	terminal_encoding_unref (encoding);

	return visible;
}

static GtkTreeModel *
encodings_create_treemodel (GtkListStore *base_store,
                            gboolean active)
{
	GtkTreeModel *model;

	model = gtk_tree_model_filter_new (GTK_TREE_MODEL (base_store), NULL);
	gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (model),
	                                        filter_active_encodings,
	                                        GUINT_TO_POINTER (active), NULL);

	return model;
}

static void
encodings_list_changed_cb (TerminalApp *app,
                           EncodingDialogData *data)
{
	gtk_list_store_clear (data->base_store);

	g_hash_table_foreach (terminal_app_get_encodings (app), (GHFunc) liststore_insert_encoding, data->base_store);
}

static void
encoding_dialog_data_free (EncodingDialogData *data)
{
	g_signal_handlers_disconnect_by_func (terminal_app_get (),
	                                      G_CALLBACK (encodings_list_changed_cb),
	                                      data);

	g_free (data);
}

void
terminal_encoding_dialog_show (GtkWindow *transient_parent)
{
	TerminalApp *app;
	GtkCellRenderer *cell_renderer;
	GtkTreeViewColumn *column;
	GtkTreeModel *model;
	EncodingDialogData *data;

	if (encoding_dialog)
	{
		gtk_window_set_transient_for (GTK_WINDOW (encoding_dialog), transient_parent);
		gtk_window_present (GTK_WINDOW (encoding_dialog));
		return;
	}

	data = g_new (EncodingDialogData, 1);

	if (!terminal_util_load_builder_resource (TERMINAL_RESOURCES_PATH_PREFIX G_DIR_SEPARATOR_S "ui/encodings-dialog.ui",
	                                      "encodings-dialog", &data->dialog,
	                                      "add-button", &data->add_button,
	                                      "remove-button", &data->remove_button,
	                                      "available-treeview", &data->available_tree_view,
	                                      "displayed-treeview", &data->active_tree_view,
	                                      NULL))
	{
		g_free (data);
		return;
	}

	g_object_set_data_full (G_OBJECT (data->dialog), "GT::Data", data, (GDestroyNotify) encoding_dialog_data_free);

	gtk_window_set_transient_for (GTK_WINDOW (data->dialog), transient_parent);
	gtk_window_set_role (GTK_WINDOW (data->dialog), "mate-terminal-encodings");
	g_signal_connect (data->dialog, "response",
	                  G_CALLBACK (response_callback), data);

	/* buttons */
	g_signal_connect (data->add_button, "clicked",
	                  G_CALLBACK (button_clicked_cb), data);

	g_signal_connect (data->remove_button, "clicked",
	                  G_CALLBACK (button_clicked_cb), data);

	/* Tree view of available encodings */
	/* Column 1 */
	cell_renderer = gtk_cell_renderer_text_new ();
	column = gtk_tree_view_column_new_with_attributes (_("_Description"),
	         cell_renderer,
	         "text", COLUMN_NAME,
	         NULL);
	gtk_tree_view_append_column (data->available_tree_view, column);
	gtk_tree_view_column_set_sort_column_id (column, COLUMN_NAME);

	/* Column 2 */
	cell_renderer = gtk_cell_renderer_text_new ();
	column = gtk_tree_view_column_new_with_attributes (_("_Encoding"),
	         cell_renderer,
	         "text", COLUMN_CHARSET,
	         NULL);
	gtk_tree_view_append_column (data->available_tree_view, column);
	gtk_tree_view_column_set_sort_column_id (column, COLUMN_CHARSET);

	data->available_selection = gtk_tree_view_get_selection (data->available_tree_view);
	gtk_tree_selection_set_mode (data->available_selection, GTK_SELECTION_BROWSE);

	g_signal_connect (data->available_selection, "changed",
	                  G_CALLBACK (selection_changed_cb), data);

	/* Tree view of selected encodings */
	/* Column 1 */
	cell_renderer = gtk_cell_renderer_text_new ();
	column = gtk_tree_view_column_new_with_attributes (_("_Description"),
	         cell_renderer,
	         "text", COLUMN_NAME,
	         NULL);
	gtk_tree_view_append_column (data->active_tree_view, column);
	gtk_tree_view_column_set_sort_column_id (column, COLUMN_NAME);

	/* Column 2 */
	cell_renderer = gtk_cell_renderer_text_new ();
	column = gtk_tree_view_column_new_with_attributes (_("_Encoding"),
	         cell_renderer,
	         "text", COLUMN_CHARSET,
	         NULL);
	gtk_tree_view_append_column (data->active_tree_view, column);
	gtk_tree_view_column_set_sort_column_id (column, COLUMN_CHARSET);

	/* Add the data */

	data->active_selection = gtk_tree_view_get_selection (data->active_tree_view);
	gtk_tree_selection_set_mode (data->active_selection, GTK_SELECTION_BROWSE);

	g_signal_connect (data->active_selection, "changed",
	                  G_CALLBACK (selection_changed_cb), data);

	data->base_store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, TERMINAL_TYPE_ENCODING);

	app = terminal_app_get ();
	encodings_list_changed_cb (app, data);
	g_signal_connect (app, "encoding-list-changed",
	                  G_CALLBACK (encodings_list_changed_cb), data);

	/* Now turn on sorting */
	gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (data->base_store),
	                                      COLUMN_NAME,
	                                      GTK_SORT_ASCENDING);

	model = encodings_create_treemodel (data->base_store, FALSE);
	gtk_tree_view_set_model (data->available_tree_view, model);
	g_object_unref (model);

	model = encodings_create_treemodel (data->base_store, TRUE);
	gtk_tree_view_set_model (data->active_tree_view, model);
	g_object_unref (model);

	g_object_unref (data->base_store);

	gtk_window_present (GTK_WINDOW (data->dialog));

	encoding_dialog = data->dialog;
	g_signal_connect (data->dialog, "destroy",
	                  G_CALLBACK (gtk_widget_destroyed), &encoding_dialog);
}

GHashTable *
terminal_encodings_get_builtins (void)
{
	GHashTable *encodings_hashtable;
	guint i;
	TerminalEncoding *encoding;

	encodings_hashtable = g_hash_table_new_full (g_str_hash, g_str_equal,
	                      NULL,
	                      (GDestroyNotify) terminal_encoding_unref);


	/* Placeholder entry for the current locale's charset */
	encoding = terminal_encoding_new ("current",
	                                  _("Current Locale"),
	                                  FALSE,
	                                  TRUE);
	g_hash_table_insert (encodings_hashtable,
	                     (gpointer) terminal_encoding_get_id (encoding),
	                     encoding);

	for (i = 0; i < G_N_ELEMENTS (encodings); ++i)
	{
		encoding = terminal_encoding_new (encodings[i].charset,
		                                  _(encodings[i].name),
		                                  FALSE,
		                                  FALSE);
		g_hash_table_insert (encodings_hashtable,
		                     (gpointer) terminal_encoding_get_id (encoding),
		                     encoding);
	}

	return encodings_hashtable;
}
