/*
 * ted.c
 *
 * Tiny Editor in C/GTK3
 *
 * 2024 asdala.de
 *
 * Das Programm kann kostenlos kopiert, benutzt und geaendert werden.
 * Keine Gewaehr fuer Verwendbarkeit oder Korrektheit, keine Haftung.
 *
 */

#include <gtk/gtk.h>
#include <locale.h>
#include <libgen.h>

static const char appdes[] = "Tiny Editor in C/GTK3";

static struct ted { GtkApplication *app; GtkWidget *topwin; GtkWidget *view; char *fname; } ed;

/** Nachrichten **/

static void out(const gchar s[], int msg_type)
  {
  const gchar *es = (msg_type >= GTK_MESSAGE_WARNING && errno > 0) ? g_strerror(errno) : "";
  GtkWidget *dlg = gtk_message_dialog_new(GTK_WINDOW(ed.topwin), GTK_DIALOG_DESTROY_WITH_PARENT,
    msg_type, GTK_BUTTONS_CLOSE, "%s\n%s", s, es);
  gtk_dialog_run(GTK_DIALOG(dlg));
  gtk_widget_destroy(dlg);
  }

static void msg(const gchar s[])
  {
  out(s, GTK_MESSAGE_INFO);
  }

static void wrn(const gchar s[])
  {
  out(s, GTK_MESSAGE_WARNING);
  }

static int cnf(const gchar title[])
  {
  GtkWidget *dlg = gtk_dialog_new_with_buttons(title, GTK_WINDOW(ed.topwin), GTK_DIALOG_DESTROY_WITH_PARENT,
    "_Yes", GTK_RESPONSE_YES, "_No", GTK_RESPONSE_NO, "_Cancel", GTK_RESPONSE_CANCEL, NULL);
  gint ret = gtk_dialog_run(GTK_DIALOG(dlg));
  gtk_widget_destroy(dlg);
  return ret;
  }

/** Dateidialoge **/

static char *dlg_open(void)
  {
  char *fname = NULL;
  GtkWidget *dlg;
  gint ret;
  dlg = gtk_file_chooser_dialog_new("Open File", GTK_WINDOW(ed.topwin), GTK_FILE_CHOOSER_ACTION_OPEN,
    "_Cancel", GTK_RESPONSE_CANCEL, "_Open", GTK_RESPONSE_ACCEPT, NULL);
  ret = gtk_dialog_run(GTK_DIALOG(dlg));
  if (ret == GTK_RESPONSE_ACCEPT) {
    GtkFileChooser *chooser = GTK_FILE_CHOOSER(dlg);
    fname = gtk_file_chooser_get_filename(chooser);
    }
  gtk_widget_destroy(dlg);
  return fname;
  }

static char *dlg_save(void)
  {
  char *fname = NULL;
  GtkWidget *dlg;
  gint ret;
  dlg = gtk_file_chooser_dialog_new("Save As", GTK_WINDOW(ed.topwin), GTK_FILE_CHOOSER_ACTION_SAVE,
    "_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL);
  ret = gtk_dialog_run(GTK_DIALOG(dlg));
  if (ret == GTK_RESPONSE_ACCEPT) {
    GtkFileChooser *chooser = GTK_FILE_CHOOSER(dlg);
    fname = gtk_file_chooser_get_filename(chooser);
    }
  gtk_widget_destroy(dlg);
  return fname;
  }

/** Dateioperationen **/

static int file_save(void)
  {
  GtkTextBuffer *tbuf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(ed.view));
  if (gtk_text_buffer_get_modified(tbuf) == FALSE)
    return 1;
  GtkTextIter start, end;
  if (ed.fname == NULL)
    ed.fname = dlg_save();
  if (ed.fname == NULL)
    return 0;
  gtk_text_buffer_get_bounds(tbuf, &start, &end);
  char *fbuf = gtk_text_buffer_get_text(tbuf, &start, &end, FALSE);
  if (g_file_set_contents(ed.fname, fbuf, -1, NULL) == 0)
    wrn(ed.fname);
  else
    gtk_text_buffer_set_modified(tbuf, FALSE);
  g_free(fbuf);
  return 1;
  }

static int file_close(void)
  {
  GtkTextBuffer *tbuf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(ed.view));
  if (gtk_text_buffer_get_modified(tbuf)) {
    int ret = cnf("Save changes?");
    switch (ret) {
      case GTK_RESPONSE_YES: if (file_save() == 0) return 0; break;
      case GTK_RESPONSE_NO: break;
      default: return 0;
      }
    }
  if (ed.fname) {
    g_free(ed.fname);
    ed.fname = NULL;
    }
  return 1;
  }

static int file_open(char fname[])
  {
  char *fbuf = NULL;
  gsize flen = 0;
  if (file_close() == 0)
    return 0;
  if (fname) {
    if (g_file_get_contents(fname, &fbuf, &flen, NULL) == 0) {
      wrn(fname);
      return 0;
      }
    }
  if (!g_utf8_validate(fbuf, flen, NULL)) {
    errno = 0;
    wrn("No valid UTF8 file!");
    return 0;
    }
  gtk_window_set_title(GTK_WINDOW(ed.topwin), fname ? basename(fname) : "New");
  ed.fname = g_strdup(fname);
  GtkTextBuffer *tbuf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(ed.view));
  gtk_text_buffer_set_text(tbuf, fbuf ? fbuf : "", flen);
  gtk_text_buffer_set_modified(tbuf, FALSE);
  g_free(fbuf);
  return 1;
  }

/** Menue-Funktionen **/

static void cb_new(GSimpleAction *action, GVariant *parameter, gpointer user_data)
  {
  file_open(NULL);
  }

static void cb_save(GSimpleAction *action, GVariant *parameter, gpointer user_data)
  {
  file_save();
  }

static void cb_open(GSimpleAction *action, GVariant *parameter, gpointer user_data)
  {
  char *fname = dlg_open();
  if (fname == NULL)
    return;
  file_open(fname);
  g_free(fname);
  }

static void cb_help(GSimpleAction *action, GVariant *parameter, gpointer user_data)
  {
  msg(appdes);
  }

static void cb_about(GSimpleAction *action, GVariant *parameter, gpointer user_data)
  {
  gtk_show_about_dialog(GTK_WINDOW(ed.topwin), "program-name", "ted", "version", "0.1", "logo-icon-name",
    "gtk-edit", "website", "http://asdala.de/prg", "comments", appdes, NULL);
  }

static void activate_toggle(GSimpleAction *action, GVariant *parameter, gpointer user_data)
  {
  GVariant *state;
  state = g_action_get_state(G_ACTION(action));
  g_action_change_state(G_ACTION(action), g_variant_new_boolean(!g_variant_get_boolean(state)));
  g_variant_unref(state);
  }

static void cb_toggle_fullscreen(GSimpleAction *action, GVariant *state, gpointer user_data)
  {
  gboolean full = g_variant_get_boolean(state);
  gtk_application_window_set_show_menubar(GTK_APPLICATION_WINDOW(ed.topwin), !full);
  if (full)
    gtk_window_fullscreen(user_data);
  else
    gtk_window_unfullscreen(user_data);
  g_simple_action_set_state(action, state);
  }

static void cb_quit(GSimpleAction *action, GVariant *parameter, gpointer user_data)
  {
  gtk_window_close(GTK_WINDOW(ed.topwin));
  }

/** Signal-Handler **/

static void on_drag_data_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
  GtkSelectionData *gtkseldata, guint info, guint time, gpointer data)
  {
  gchar **uris = gtk_selection_data_get_uris(gtkseldata);
  char *fname = g_filename_from_uri(uris[0], NULL, NULL);
  if (fname) {
    file_open(fname);
    g_free(fname);
    }
  g_strfreev(uris);
  gtk_drag_finish(context, TRUE, FALSE, time);
  }

static gboolean on_delete_event(GtkWidget *widget, GdkEvent *event, gpointer data)
  {
  return file_close() == 0;
  }

static void on_activate(GApplication *gapp, gpointer user_data)
  {
  GtkApplication *app = GTK_APPLICATION(gapp);
  GtkWindow *win = gtk_application_get_active_window(app);
  gtk_window_present(win);
  }

static void on_open(GApplication *gapp, GFile **files, gint n_files, const gchar *hint)
  {
  GtkApplication *app = GTK_APPLICATION(gapp);
  GtkWindow *win = gtk_application_get_active_window(app);
  gtk_window_present(win);
  gchar *fname = g_file_get_parse_name(files[0]);
  file_open(fname);
  g_free(fname);
  }

static void on_startup(GApplication *gapp, gpointer user_data)
  {
  GtkApplication *app = GTK_APPLICATION(gapp);

  /* Tastaturkuerzel */
  struct tacc { const char *action_and_target; const char *accelerators[2]; };
  struct tacc acc[] = {
    { "app.new", { "<Control>n", NULL } },
    { "app.open", { "<Control>o", NULL } },
    { "app.save", { "<Control>s", NULL } },
    { "app.quit", { "<Control>q", NULL } },
    { "app.help", { "F1", NULL } },
    { "win.fullscreen", { "F11", NULL } },
    };
  for (int i = 0; i < G_N_ELEMENTS(acc); ++i)
    gtk_application_set_accels_for_action(app, acc[i].action_and_target, acc[i].accelerators);

  /* App-Initialisierung */
  static const GActionEntry app_actions[] = {
    { "new", cb_new },
    { "open", cb_open },
    { "save", cb_save },
    { "quit", cb_quit },
    { "help", cb_help },
    { "about", cb_about },
    };
  g_action_map_add_action_entries(G_ACTION_MAP(app), app_actions, G_N_ELEMENTS(app_actions), app);

  /* Menue */
  GMenu *menu_bar, *menu;
  menu_bar = g_menu_new();
  menu = g_menu_new();
  g_menu_append(menu, "_New", "app.new");
  g_menu_append(menu, "_Open", "app.open");
  g_menu_append(menu, "_Save", "app.save");
  g_menu_append(menu, "_Quit", "app.quit");
  g_menu_append_submenu(menu_bar, "_File", G_MENU_MODEL(menu));
  g_object_unref(menu);
  menu = g_menu_new();
  g_menu_append(menu, "_Full Screen", "win.fullscreen");
  g_menu_append_submenu(menu_bar, "_View", G_MENU_MODEL(menu));
  g_object_unref(menu);
  menu = g_menu_new();
  g_menu_append(menu, "_Contents", "app.help");
  g_menu_append(menu, "_About", "app.about");
  g_menu_append_submenu(menu_bar, "_Help", G_MENU_MODEL(menu));
  g_object_unref(menu);
  gtk_application_set_menubar(app, G_MENU_MODEL(menu_bar));
  g_object_unref(menu_bar);

  /* Win-Initialisierung */
  GtkWidget *win, *view;
  win = gtk_application_window_new(app);
  g_signal_connect(win, "delete-event", G_CALLBACK(on_delete_event), NULL);
  static GActionEntry win_actions[] = {
    { "fullscreen", activate_toggle, NULL, "false", cb_toggle_fullscreen }
    };
  g_action_map_add_action_entries(G_ACTION_MAP(win), win_actions, G_N_ELEMENTS(win_actions), win);
  gtk_window_set_title(GTK_WINDOW(win), "ted");
  gtk_window_set_default_icon_name("gtk-edit");
  gtk_window_set_default_size(GTK_WINDOW(win), 640, 480);
  view = gtk_text_view_new();
  gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(view), GTK_WRAP_WORD);
  GtkWidget *scrollwin = gtk_scrolled_window_new(NULL, NULL);
  gtk_container_add(GTK_CONTAINER(scrollwin), view);
  gtk_container_set_border_width(GTK_CONTAINER(scrollwin), 5);
  gtk_container_add(GTK_CONTAINER(win), scrollwin);
  gtk_drag_dest_set_target_list(view, NULL);
  gtk_drag_dest_add_uri_targets(view);
  g_signal_connect(view, "drag-data-received", G_CALLBACK(on_drag_data_received), NULL);
  gtk_widget_show_all(GTK_WIDGET(win));
  ed.topwin = win;
  ed.view = view;
  }

int main(int argc, char **argv)
  {
  int ret;
  setlocale(LC_ALL, "");
  ed.app = gtk_application_new(NULL, G_APPLICATION_HANDLES_OPEN);
  g_signal_connect(ed.app, "startup", G_CALLBACK(on_startup), NULL);
  g_signal_connect(ed.app, "activate", G_CALLBACK(on_activate), NULL);
  g_signal_connect(ed.app, "open", G_CALLBACK(on_open), NULL);
  ret = g_application_run(G_APPLICATION(ed.app), argc, argv);
  g_object_unref(ed.app);
  return ret;
  }