/*
 *  gtk と libgauche を使ったお手軽 S 式エディタ shiki
 *
 *  shiki.c author@WAKATSUKI toshihiro (alohakun@gmail.com)
 */

#include<gauche.h>
#include<gtk/gtk.h>
#include<gdk/gdkkeysyms.h>

#define INDENT_WIDTH 2

static GtkWidget *editor_window;
static GtkTextTag *parentEmphasisColorTag;

/* テキストバッファから全ての文字列を取り出す */
static gchar* get_all_buffer_contents(GtkTextBuffer *buffer) {
  GtkTextIter start, end;
  gtk_text_buffer_get_start_iter(buffer, &start);
  gtk_text_buffer_get_end_iter(buffer, &end);
  return gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
}

/* buffer の内容をファイル filename に保存 */
static gboolean save_text_buffer(const gchar *filename, GtkTextBuffer *buffer) {
  gchar *contents, *text;
  gsize br, bw;
  GError *err = NULL;

  if(!filename) return FALSE;
  contents = get_all_buffer_contents(buffer);
  text = g_locale_from_utf8(contents, -1, &br, &bw, &err);
  /* 文字列をファイルに保存 */
  g_file_set_contents(filename, contents, -1, NULL);
  g_free(contents); g_free(text);
  return TRUE;
}

/* ダイアログを開き，ファイル名を取得．msg はダイアログのメッセージ */
static gchar *get_filename_from_dialog(const gchar *msg) {

  GtkWidget *dialog = gtk_file_selection_new(msg);
  int resp = gtk_dialog_run(GTK_DIALOG(dialog));
  gchar *filename = NULL;

  /* gtk_file_selection_get_filename が返す文字列定数は静的に確保されたバッファを指しているので，コピーしなければならない */
  if(resp == GTK_RESPONSE_OK)
    filename = g_strdup(gtk_file_selection_get_filename(GTK_FILE_SELECTION(dialog)));

  gtk_widget_destroy(dialog);
  return filename;
}

/* 現在表示されているページのテキストバッファを取り出す */
static GtkTextBuffer* get_text_buffer_from_current_tabpage(GtkNotebook* notebook) {
  gint pagenum = gtk_notebook_get_current_page(notebook);
  GtkScrolledWindow *scrolledwindow = GTK_SCROLLED_WINDOW(gtk_notebook_get_nth_page(notebook, pagenum));
  /* GtkBin は一つしか子を持たない抽象コンテナクラス */
  GtkTextView *view = GTK_TEXT_VIEW(gtk_bin_get_child(GTK_BIN(scrolledwindow)));
  return gtk_text_view_get_buffer(view);
}

/* ファイルを保存するイベントハンドラ */
static void save_file_handler(GtkWidget *widget, GtkWidget *notebook) {
  GtkTextBuffer *buffer = get_text_buffer_from_current_tabpage(GTK_NOTEBOOK(notebook));
  gint current_page;
  gchar *filename;

  /* 変更が無ければ何もしない */
  if(!gtk_text_buffer_get_modified(buffer)) return;

  current_page = gtk_notebook_get_current_page(GTK_NOTEBOOK(notebook));
  filename = g_strdup(gtk_notebook_get_tab_label_text(GTK_NOTEBOOK(notebook), gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), current_page)));

  /* まだファイル名が設定されていなかったら，ダイアログを開いて入力させる */
  if(!filename || g_ascii_strcasecmp("*scratch*", filename) == 0) {
    g_free(filename);
    save_text_buffer(filename = get_filename_from_dialog("Save File As ..."), buffer);
  } else
    save_text_buffer(filename, buffer);

  if(!filename)
    return;

  if(g_ascii_strcasecmp("*scratch*", filename)) {
    g_free(filename);
    return;
  }

  gtk_window_set_title (GTK_WINDOW (editor_window), filename);
  g_free(filename);
}

/* ファイルを別名保存するイベントハンドラ */
static void save_file_as_handler(GtkWidget *widget, GtkWidget *notebook) {
  gint current_page;

  GtkTextBuffer *buffer = get_text_buffer_from_current_tabpage(GTK_NOTEBOOK(notebook));

  gchar *filename = get_filename_from_dialog("Save File As ...");

  if(!filename) return;
  save_text_buffer(filename, buffer);
  current_page = gtk_notebook_get_current_page(GTK_NOTEBOOK(notebook));
  gtk_notebook_set_tab_label_text(GTK_NOTEBOOK(notebook),
      gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), current_page),
      filename);    

  gtk_window_set_title (GTK_WINDOW (editor_window), filename);

  g_free(filename);
}

/* ファイルを開くイベントハンドラ */
static void open_file_handler(GtkWidget *widget,  GtkWidget *notebook) {

  GtkTextBuffer *buffer = get_text_buffer_from_current_tabpage(GTK_NOTEBOOK(notebook));

  gchar *contents, *text;
  gsize br, bw, len;
  GError *err = NULL;
  gint current_page;

  gchar *filename = get_filename_from_dialog("File Selection");

  if(!filename) return;

  if(g_file_get_contents(filename, &contents, &len, NULL)) {
    GtkTextIter p;

    if(!(text = g_locale_to_utf8(contents, -1, &br, &bw, &err)))
      gtk_text_buffer_set_text(buffer, contents, len);
    else
      gtk_text_buffer_set_text(buffer, text, len);

    current_page = gtk_notebook_get_current_page(GTK_NOTEBOOK(notebook));
    gtk_notebook_set_tab_label_text(GTK_NOTEBOOK(notebook),
        gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), current_page),
        filename);

    gtk_text_buffer_set_modified(buffer, FALSE);
    gtk_text_buffer_get_start_iter(buffer, &p);
    gtk_text_buffer_place_cursor(buffer, &p);
    gtk_window_set_title (GTK_WINDOW (editor_window), filename);
    g_free(contents); g_free(text); g_free(filename);
  } else
    g_printerr("Get file contents error !\n");
}

/* gauche を起動して文字列を評価 */
static gchar *eval_cstring_by_gauche(gchar *s) {
  gchar *msg;

  ScmObj result, error; 
  /* 出力文字列ポート開く */
  ScmObj os = Scm_MakeOutputStringPort(TRUE);

  /* Scheme レベルでエラーハンドリング */
  /* http://alohakun.blog7.fc2.com/blog-entry-517.html */
  Scm_Define(Scm_UserModule(), SCM_SYMBOL(SCM_INTERN("*input*")), SCM_MAKE_STR(s));
  Scm_Define(Scm_UserModule(), SCM_SYMBOL(SCM_INTERN("*error*")), SCM_FALSE);

  result = Scm_EvalCString("(guard (e (else (set! *error* e) #f)) (eval (read-from-string *input*) (current-module)))", SCM_OBJ(Scm_UserModule()));

  error = Scm_GlobalVariableRef(Scm_UserModule(), SCM_SYMBOL(SCM_INTERN("*error*")), 0);

  /* 文字列を評価した結果をポートに書き込む */
  if (!SCM_FALSEP(error))
    Scm_Write(error, os, SCM_WRITE_DISPLAY);
  else
    Scm_Write(result, os, SCM_WRITE_DISPLAY);

  msg = Scm_GetString(SCM_STRING(Scm_GetOutputString(SCM_PORT(os))));
  /* ポート閉じる */
  Scm_ClosePort(SCM_PORT(os));

  return msg;
}

/* 緑ボタンのハンドリング．バッファの選択されている S 式を評価 */
static void buffer_exec_handler(GtkWidget *widget,  GtkWidget *notebook) {
  GtkTextBuffer *buffer = get_text_buffer_from_current_tabpage(GTK_NOTEBOOK(notebook));

  GtkTextIter start, end, p;
  gchar *code;
  gtk_text_buffer_get_end_iter(buffer, &p);
  gtk_text_buffer_insert(buffer, &p, "\n\n", -1);

  /* マウスで選択されている範囲の文字列を取得 */
  if(gtk_text_buffer_get_selection_bounds(buffer, &start, &end)) {
    code = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
    gtk_text_buffer_insert(buffer, &p, eval_cstring_by_gauche(code), -1);
    g_free(code);
  }

}

// GtkTextCharPredicate
static gboolean is_kakko_or_kokka(gunichar ch, gpointer p) {
  return ch == '(' || ch == ')';
}
static gboolean is_kokka(gunichar ch, gpointer p) {return ch == ')';}


/* ')' に対応する '(' までの文字列 (S 式) を切り出す */
static gboolean search_sexp_string(GtkTextIter *start) {
  gint nest_level = 0;
  /* カーソル位置の前にある S 式を切り出す */
  while(1) {
    if(!gtk_text_iter_backward_find_char(start, is_kakko_or_kokka, NULL, NULL))
      return FALSE;

    if(gtk_text_iter_get_char(start) == ')')
      nest_level++;
    else {
      if(!nest_level)
        break;
      else
        nest_level--;
    }
  }
  return TRUE;
}

/* カーソル位置のネストレベルを返す */
static gint get_parent_nest_level_at_cursor(GtkTextBuffer *buffer) {
  gint nest_level = 0;
  GtkTextIter start, end;
  gtk_text_buffer_get_start_iter(buffer, &start);
  if(gtk_text_iter_get_char(&start) == '(') nest_level++;

  /* カーソルの位置 (= end) を取得 */
  gtk_text_buffer_get_iter_at_mark(buffer,&end, gtk_text_buffer_get_insert(buffer));

  while(1) {
    /* end まで '(' か ')' を探して見つからなかったら終了 */
    if(!gtk_text_iter_forward_find_char(&start, is_kakko_or_kokka, NULL, &end))
      return nest_level;

    if(gtk_text_iter_get_char(&start) == '(')
      nest_level++;
    else 
      nest_level--;
  }
}

/* 特定のキー操作のハンドリング */

/* キーが押された */
static gboolean signal_key_press (GtkWidget *notebook, GdkEventKey *event, gpointer p) {
  GtkTextBuffer *buffer = get_text_buffer_from_current_tabpage(GTK_NOTEBOOK(notebook));

  GtkTextIter start, end;

  /* タグの効果を無効に */
  gtk_text_buffer_get_start_iter(buffer, &start);
  gtk_text_buffer_get_end_iter(buffer, &end);
  gtk_text_buffer_remove_tag(buffer, parentEmphasisColorTag, &start, &end);

  /* Ctrl + j : カーソル手前の S 式の評価 */
  if(event->keyval == GDK_j && event->state & GDK_CONTROL_MASK) {
    gchar *code;
    GtkTextIter start, end;

    /* カーソルの位置を取得 */
    gtk_text_buffer_get_iter_at_mark(buffer, &end, gtk_text_buffer_get_insert(buffer));

    gtk_text_iter_backward_find_char(&end, is_kokka, NULL, NULL);
    start = end;
    gtk_text_iter_forward_char(&end);

    /* カーソル位置の前にある S 式を切り出す */
    if(!search_sexp_string(&start)) return FALSE;

    code = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
    gtk_text_buffer_insert(buffer, &end, "\n\n", -1);
    gtk_text_buffer_insert(buffer, &end, eval_cstring_by_gauche(code), -1);

    g_free(code);

  }

  return FALSE;
}

/* キーが離された */
static gboolean signal_key_release (GtkWidget *notebook, GdkEventKey *event, gpointer p) {

  GtkTextBuffer *buffer = get_text_buffer_from_current_tabpage(GTK_NOTEBOOK(notebook));

  if(event->keyval == GDK_parenright && event->state & GDK_SHIFT_MASK) {
    GtkTextIter start, end;

    /* カーソルの位置を取得 */
    gtk_text_buffer_get_iter_at_mark(buffer, &end, gtk_text_buffer_get_insert(buffer));

    start = end;
    gtk_text_iter_backward_char(&start);

    /* カーソル位置の前にある S 式を切り出す */
    if(!search_sexp_string(&start)) return FALSE;

    gtk_text_buffer_apply_tag (buffer, parentEmphasisColorTag, &start, &end);
  }

  /* 改行されるたびに，自動的に括弧のネストの深さに応じた数のスペース (インデント) が行頭に入る */
  if(event->keyval == GDK_Return) {
    gint indentWidth = get_parent_nest_level_at_cursor(buffer) * INDENT_WIDTH;
    gchar *indent = g_strnfill(indentWidth, ' ');
    gtk_text_buffer_insert_at_cursor(buffer, indent, -1);
    g_free(indent);
  }

  return FALSE;
}

/* YES ボタン，NO ボタンそれぞれで呼ばれる callback */
void really_quit_dialog_yes(GtkWidget *widget, gboolean *flag){*flag = FALSE;}
void really_quit_dialog_no(GtkWidget *widget, gint *flag){*flag = TRUE;}

/* 本当に終了してもよろしいですか ? */
gboolean delete_event(GtkWidget *widget, GdkEvent *event, GtkWidget *buffer){
  GtkWidget *yes_button, *no_button;
  static GtkWidget *dialog_window = NULL;

  /* 変更が無ければそのまま終了 */
  if(!gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(buffer))) return FALSE;

  if(dialog_window == NULL) {
    gboolean flag = TRUE;
    dialog_window = gtk_dialog_new ();

    /* 本当に終了してもよろしいですか ? ダイアログの初期化 */
    g_signal_connect(G_OBJECT(dialog_window), "delete_event", G_CALLBACK(gtk_false), NULL);
    g_signal_connect(G_OBJECT(dialog_window), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    gtk_window_set_title(GTK_WINDOW (dialog_window), "Really Quit ?");
    /* YES のボタン */
    yes_button = gtk_button_new_with_label("YES");
    g_signal_connect(GTK_OBJECT(yes_button), "clicked", G_CALLBACK(really_quit_dialog_yes), &flag);
    g_signal_connect_swapped(GTK_OBJECT(yes_button), "clicked", G_CALLBACK(gtk_widget_destroy), G_OBJECT(dialog_window));
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog_window)->action_area),  yes_button, TRUE, TRUE, 0);

    /* NO のボタン */
    no_button = gtk_button_new_with_label("NO");
    g_signal_connect(GTK_OBJECT(no_button), "clicked", G_CALLBACK(really_quit_dialog_no), &flag);
    g_signal_connect_swapped(GTK_OBJECT(no_button), "clicked", G_CALLBACK(gtk_widget_destroy), G_OBJECT(dialog_window));
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog_window)->action_area),  no_button, TRUE, TRUE, 0);

    gtk_window_set_modal(GTK_WINDOW(dialog_window), TRUE);
    gtk_window_set_transient_for(GTK_WINDOW(dialog_window), GTK_WINDOW (editor_window));

    gtk_widget_show_all(dialog_window);
    gtk_main ();
    dialog_window = NULL;

    /* "delete_event" の返り値が FALSE ならば"destory" が発行され，window は破壊される */
    return flag;
  }
  return TRUE;
}

/* タブが切り替わる時のイベントハンドリング */
static void switch_page(GtkNotebook *notebook, GtkNotebookPage *page, guint pagenum, gpointer p) {
  /* タブのラベルをウィンドウのタイトルに */
  gtk_window_set_title (GTK_WINDOW(editor_window), gtk_notebook_get_tab_label_text(notebook, GTK_WIDGET(gtk_notebook_get_nth_page(notebook, pagenum))));
}

/* ページのタブと境界線を on/off */
static void tabsborder_book(GtkButton *button, GtkNotebook *notebook) {
  gint tval = FALSE;
  gint bval = FALSE;
  if(notebook->show_tabs == FALSE)
    tval = TRUE; 
  if(notebook->show_border == FALSE)
    bval = TRUE;

  gtk_notebook_set_show_tabs(notebook, tval);
  gtk_notebook_set_show_border(notebook, bval);
}

/* ノートブックからページを削除 */
static void remove_book(GtkButton *button, GtkNotebook *notebook) {
  gint page = gtk_notebook_get_current_page(notebook);
  gtk_notebook_remove_page(notebook, page);
  /* ウィジットを強制的に再描画 */
  gtk_widget_queue_draw(GTK_WIDGET(notebook));
}

/* 新しいバッファを作る */
static GtkWidget *new_scrolled_text_buffer() {

  GtkWidget *scrolledwindow, *view;
  GtkTextBuffer *buffer;

  /* スクロールウィンドを作る */
  scrolledwindow = gtk_scrolled_window_new(NULL, NULL);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW(scrolledwindow), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

  /* テキストビューワとバッファを作る */
  view = gtk_text_view_new();
  buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
  gtk_container_add(GTK_CONTAINER(scrolledwindow), view);
  g_signal_connect(G_OBJECT(editor_window), "delete_event", G_CALLBACK(delete_event), buffer);
  gtk_widget_set_size_request(GTK_WIDGET(view), 500, 500);

  /* 様々な初期化処理 */
  parentEmphasisColorTag = gtk_text_buffer_create_tag (buffer, "parent_emphasis_background", "background", "green", NULL);

  return scrolledwindow;
}


/* ノートブックの末尾にページを追加 */
static void append_book(GtkButton *button, GtkNotebook *notebook) {

  gtk_notebook_append_page(notebook,
      new_scrolled_text_buffer(),
      gtk_label_new("*scratch*"));

  gtk_widget_show_all(GTK_WIDGET(notebook));
}

/* タブの位置を調整 */
static void rotate_book(GtkButton   *button, GtkNotebook *notebook ) {
  gtk_notebook_set_tab_pos(notebook, (notebook->tab_pos + 1) % 4);
}

/* エディタの編集画面の初期化 */
static void editor_window_init() {
  GtkWidget *vbox, *toolbar, *notebook;
  GtkToolItem *icon;
  GtkIconSize iconsize;
  /* 「開く」「保存」「別名保存」「選択領域を実行」アイコン */
  GtkToolItem *oicon, *sicon, *saicon, *eicon;

  /* 窓を作る */
  editor_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  g_signal_connect(G_OBJECT(editor_window), "destroy", G_CALLBACK(gtk_main_quit), NULL);

  /* パッキングボックスを作る */
  vbox = gtk_vbox_new(FALSE, 0);
  /* ツールバー作る */
  toolbar = gtk_toolbar_new();
  gtk_box_pack_start(GTK_BOX(vbox), toolbar, FALSE, FALSE, 0);

  notebook = gtk_notebook_new();
  g_signal_connect(G_OBJECT(notebook), "switch-page", GTK_SIGNAL_FUNC(switch_page), NULL);

  /* ツールバーに付けるアイコンの設定 */
  gtk_toolbar_set_style(GTK_TOOLBAR (toolbar), GTK_TOOLBAR_ICONS);
  iconsize = gtk_toolbar_get_icon_size (GTK_TOOLBAR (toolbar));

  /* アイコン作る */

  /* ファイル開く */
  oicon = gtk_tool_button_new(gtk_image_new_from_stock ("gtk-open", iconsize), "");
  /* 「開く」ボタンにファイルを読み込むアクションを関連付ける */
  g_signal_connect(G_OBJECT(oicon), "clicked", G_CALLBACK(open_file_handler), G_OBJECT(notebook));
  gtk_container_add(GTK_CONTAINER (toolbar), GTK_WIDGET(oicon));

  /* バッファ保存 */
  sicon = gtk_tool_button_new(gtk_image_new_from_stock ("gtk-save", iconsize), "");
  /* 「保存」ボタンにファイルを書き出すアクションを関連付ける */
  g_signal_connect(G_OBJECT(sicon), "clicked", G_CALLBACK(save_file_handler), G_OBJECT(notebook));
  gtk_container_add (GTK_CONTAINER (toolbar), GTK_WIDGET(sicon));      

  /* バッファを別名保存 */
  saicon = gtk_tool_button_new(gtk_image_new_from_stock ("gtk-save-as", iconsize), "");
  /* 「別名保存」ボタンに別名のファイル開いて内容を書き出すアクションを関連付ける */
  g_signal_connect(G_OBJECT(saicon), "clicked", G_CALLBACK(save_file_as_handler), G_OBJECT(notebook));

  gtk_container_add (GTK_CONTAINER (toolbar), GTK_WIDGET(saicon));

  /* バッファ実行 */
  eicon = gtk_tool_button_new(gtk_image_new_from_stock ("gtk-execute", iconsize), "");
  /* バッファを実行ボタンに libgauche を関連付ける */
  g_signal_connect(G_OBJECT(eicon), "clicked", G_CALLBACK(buffer_exec_handler), G_OBJECT(notebook));
  gtk_container_add (GTK_CONTAINER (toolbar), GTK_WIDGET(eicon));

  /* キーバインドのハンドリングを登録 */
  g_signal_connect(G_OBJECT(notebook), "key-press-event", G_CALLBACK (signal_key_press), NULL);
  g_signal_connect(G_OBJECT(notebook), "key-release-event", G_CALLBACK (signal_key_release), NULL);
  gtk_container_add(GTK_CONTAINER(editor_window), vbox);
  gtk_container_add(GTK_CONTAINER(vbox), notebook);

  /* デフォルトのページを追加 */
  gtk_notebook_prepend_page(GTK_NOTEBOOK(notebook),
      new_scrolled_text_buffer(),
      gtk_label_new("*scratch*"));

  /* 一つ前のタブ */
  icon = gtk_tool_button_new(gtk_image_new_from_stock ("gtk-media-previous", iconsize), "prev");
  g_signal_connect_swapped(G_OBJECT(icon), "clicked", G_CALLBACK(gtk_notebook_prev_page), G_OBJECT(notebook));
  gtk_container_add(GTK_CONTAINER (toolbar), GTK_WIDGET(icon));

  /* 次のタブ */
  icon = gtk_tool_button_new(gtk_image_new_from_stock ("gtk-media-next", iconsize), "next");
  g_signal_connect_swapped(G_OBJECT(icon), "clicked", G_CALLBACK(gtk_notebook_next_page), G_OBJECT(notebook));
  gtk_container_add(GTK_CONTAINER (toolbar), GTK_WIDGET(icon));

  /* タブの on/off */
  icon = gtk_tool_button_new(gtk_image_new_from_stock ("gtk-apply", iconsize), "append");
  g_signal_connect(G_OBJECT(icon), "clicked", G_CALLBACK(tabsborder_book), G_OBJECT(notebook));
  gtk_container_add(GTK_CONTAINER (toolbar), GTK_WIDGET(icon));

  /* タブの位置 */
  icon = gtk_tool_button_new(gtk_image_new_from_stock ("gtk-preferences", iconsize), "append");
  g_signal_connect(G_OBJECT(icon), "clicked", G_CALLBACK(rotate_book), G_OBJECT( notebook));
  gtk_container_add(GTK_CONTAINER (toolbar), GTK_WIDGET(icon));

  /* タブを追加 */
  icon = gtk_tool_button_new(gtk_image_new_from_stock ("gtk-add", iconsize), "append");
  g_signal_connect(G_OBJECT(icon), "clicked", G_CALLBACK(append_book), G_OBJECT( notebook));
  gtk_container_add(GTK_CONTAINER (toolbar), GTK_WIDGET(icon));

  /* 選択されているページを取り除く */
  icon = gtk_tool_button_new(gtk_image_new_from_stock ("gtk-close", iconsize), "remove");
  g_signal_connect(G_OBJECT(icon), "clicked", G_CALLBACK(remove_book), G_OBJECT( notebook));
  gtk_container_add(GTK_CONTAINER (toolbar), GTK_WIDGET(icon));

  gtk_widget_show_all(editor_window);
}

int main(int argc, char *argv[]) {
  /* 初期化してメインループへ */
  gtk_set_locale(); 
  gtk_init(&argc, &argv);
  GC_INIT(); Scm_Init(GAUCHE_SIGNATURE);
  editor_window_init();
  gtk_main();
  Scm_Exit(0);
  return 0;
}
