/* Copyright © Nils O.Selåsdal <NOS@Utel.no> 
   Comments/bug reports/anything goes to NOS@Utel.no
   License is BSD, see the LICENSE file
*/
#include <sys/types.h>
#include <unistd.h>
#include <getopt.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <glade/glade.h>
#include "ghasher.h"
#include "hash.xpm"
#include "ghasher-glade.c"


static gboolean hash_some(struct SumContext *s_ctx);

/*event/source id if running calculation(idle function) */
static gint current_idle_id = -1;
const char *const hashfunctions[] = {
	"md2",
	"md4",
	"md5",
	"sha1",
	"sha",
	"ripemd160",
	"dss1"
};

/*called when the calculation(idle function) is finished */
static void hash_idle_destroy(gpointer p)
{
	struct GHApp *app = p;
	current_idle_id = -1;

	gtk_widget_set_sensitive(GTK_WIDGET(app->addbutton), TRUE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->digestdropdown), TRUE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->cancelbutton), FALSE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->clearbutton), TRUE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->digestbutton), TRUE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->savebutton), TRUE);
	//gtk_widget_set_sensitive(GTK_WIDGET(app->verifybutton), TRUE);

}

static void destroy_sum_context(struct SumContext *s_ctx)
{
	unsigned int md_len;
	unsigned char b[EVP_MAX_MD_SIZE]; /*fun thought: OpenSSL decides to extend this, and ghasher */
	char b_hex[EVP_MAX_MD_SIZE * 2 + 1]; /*is compiled with an older lib.*/
	if (s_ctx == NULL)
		return;

	close(s_ctx->currentfile);
	md_len = 0;
/*openssl prior to 0.9.7 needs a Final else we leak memory afaik,
  above 0.9.7 openssl provides a _cleanup function. */
#if OPENSSL_VERSION_NUMBER < 0x00907000L
	EVP_DigestFinal(&s_ctx->ctx, b, &md_len);
#endif

	if (s_ctx->allok) {
#if OPENSSL_VERSION_NUMBER >= 0x00907000L
		EVP_DigestFinal(&s_ctx->ctx, b, &md_len);
#endif
		hex_dump(b, md_len, b_hex, md_len * 2 + 1);
		set_hashfield(GTK_TREE_VIEW(s_ctx->app->listview), b_hex, g_ptr_array_index(s_ctx->app->files, s_ctx->app->currentfile - 1));

	}
#if OPENSSL_VERSION_NUMBER >= 0x00907000L
	EVP_MD_CTX_cleanup(&s_ctx->ctx);
#endif
	g_free(s_ctx);
}

struct SumContext *new_sum_context(struct GHApp *app, const gchar *filename)
{
	struct SumContext *s_ctx;

	s_ctx = g_malloc0(sizeof(*s_ctx));
	
	if (s_ctx == NULL || stat(filename, &s_ctx->statbuf) == -1) {
		gchar *error;
		error = g_strdup_printf("Failed: %s", g_strerror(errno));
		set_hashfield(GTK_TREE_VIEW(app->listview), error, g_ptr_array_index(app->files, app->currentfile));
		g_free(error);
		g_free(s_ctx);

		return NULL;
	}

	s_ctx->md = EVP_get_digestbyname(app->digesttype);
	if (s_ctx->md == NULL) {
		gchar *error;
		error = g_strdup_printf("Digest type '%s' is not supported.", app->digesttype);
		set_hashfield(GTK_TREE_VIEW(app->listview), error, g_ptr_array_index(app->files, app->currentfile));
		g_free(error);
		g_free(s_ctx);

		return NULL;

	}

	EVP_DigestInit(&s_ctx->ctx, s_ctx->md);
	s_ctx->currentfile = open(filename, O_RDONLY);
	if (s_ctx->currentfile < 0) {
		gchar *error;
		error = g_strdup_printf("Failed: %s", g_strerror(errno));
		set_hashfield(GTK_TREE_VIEW(app->listview), error, g_ptr_array_index(app->files, app->currentfile));
		g_free(error);
		g_free(s_ctx);

		return NULL;
	}

	s_ctx->allok = FALSE;
	s_ctx->finished = FALSE;
	s_ctx->app = app;

	return s_ctx;

}

static gboolean hash_idle(gpointer p)
{
	struct GHApp *app = p;


	if (app->currentsum == NULL || app->currentsum->finished) {
		gchar *filename;
		if ((gint) app->files->len <= app->currentfile) {
			destroy_sum_context(app->currentsum);
			app->currentsum = NULL;
			app->currentfile = 0;
			gtk_progress_bar_set_text(GTK_PROGRESS_BAR(app->progress), "Digesting finished");
			return FALSE;
		}
		filename = g_ptr_array_index(app->files, app->currentfile);
		destroy_sum_context(app->currentsum);
		app->currentsum = new_sum_context(app, filename);
		app->currentfile++;

	}

	if (app->currentsum)
		hash_some(app->currentsum);

	return TRUE;
}
#define BLOCKSZ 3
static gboolean hash_some(struct SumContext *s_ctx)
{
	unsigned char buffer[s_ctx->statbuf.st_blksize * BLOCKSZ];
	int cnt = 42; /*Meaning of life.*/

	while (cnt--) {
		ssize_t ret;

		ret = read(s_ctx->currentfile, buffer, s_ctx->statbuf.st_blksize * BLOCKSZ);
		if (ret > 0) {
			EVP_DigestUpdate(&s_ctx->ctx, buffer, (unsigned long) ret);
			s_ctx->read_acc += (off_t) ret;
		} else if (ret == 0) {
			s_ctx->allok = TRUE;
			s_ctx->finished = TRUE;
			gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(s_ctx->app->progress), 1.0);
		} else if (ret == -1) {
			gchar *error;
			if (errno == EINTR) {
				return TRUE;
			}
			error = g_strdup_printf("Failed: %s", g_strerror(errno));
			set_hashfield(GTK_TREE_VIEW(s_ctx->app->listview), error, g_ptr_array_index(s_ctx->app->files, s_ctx->app->currentfile - 1));
			g_free(error);
			s_ctx->finished = TRUE;
		}

		if (s_ctx->finished)
			return FALSE;
	}

	gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(s_ctx->app->progress), (float) s_ctx->read_acc / (float) s_ctx->statbuf.st_size);

	return TRUE;
}

static void  clearbutton_clicked(GtkButton *button, struct GHApp *app)
{

	g_ptr_array_free(app->files,TRUE);
	app->files = g_ptr_array_new();
	app->currentfile = 0;	
	fill_list_view(GTK_TREE_VIEW(app->listview), app->files);

	gtk_progress_bar_set_text(GTK_PROGRESS_BAR(app->progress), "...");
	gtk_widget_set_sensitive(GTK_WIDGET(app->addbutton), TRUE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->digestdropdown), TRUE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->digestbutton), FALSE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->cancelbutton), FALSE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->clearbutton), FALSE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->savebutton), FALSE);
	//gtk_widget_set_sensitive(GTK_WIDGET(app->verifybutton), FALSE);
}


static void  savebutton_clicked(GtkButton *button, struct GHApp *app)
{
	GtkWidget *file_selector;
	gint ret;
	static gchar *lastdir = NULL;

	file_selector = gtk_file_chooser_dialog_new("Select a file to digest. ", GTK_WINDOW(app->window), 
		GTK_FILE_CHOOSER_ACTION_SAVE, GTK_STOCK_SAVE,GTK_RESPONSE_OK, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,NULL);
	gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(file_selector), TRUE);	/*we only handle kernel filesystems*/
	if (lastdir != NULL)
		gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(file_selector), lastdir);

	ret = gtk_dialog_run(GTK_DIALOG(file_selector));
	if (ret == GTK_RESPONSE_OK) {
		gchar *filename;
		FILE *f;

		filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(file_selector));
		f = fopen(filename,"w");
		if(f != NULL) {
			GString *sums;
			size_t w = 0;

			sums = listview_get_data(app);
			while(w < (size_t)sums->len){
				size_t wt;	
				wt = fwrite(sums->str+w,1,sums->len-w,f);
				if(wt <= 0 || ferror(f)){
					show_error_dialog(app->window,"Writing '%s' failed:\n%s",filename,g_strerror(errno)); 	
					break;
				}
				w += wt;
			}

			g_string_free(sums,TRUE);
			fclose(f);
		} else
			show_error_dialog(app->window,"Writing '%s' failed:\n%s",filename,g_strerror(errno)); 	

		g_free(filename);
		g_free(lastdir);
		lastdir = gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(file_selector));
	}

	gtk_widget_destroy(file_selector);
}


static void digestbutton_clicked(GtkButton *button, struct GHApp *app)
{

	fill_list_view(GTK_TREE_VIEW(app->listview), app->files);
	current_idle_id = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, hash_idle, app, hash_idle_destroy);

	gtk_progress_bar_set_text(GTK_PROGRESS_BAR(app->progress), "Digesting...");
	gtk_widget_set_sensitive(GTK_WIDGET(app->addbutton), FALSE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->digestdropdown), FALSE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->digestbutton), FALSE);
	gtk_widget_set_sensitive(GTK_WIDGET(app->cancelbutton), TRUE);
	//gtk_widget_set_sensitive(GTK_WIDGET(app->verifybutton), FALSE);
}

static void cancelbutton_clicked(GtkButton *button, struct GHApp *app)
{
	if (current_idle_id != -1) {	/*wtf ?*/
		g_source_remove(current_idle_id);
		gtk_progress_bar_set_text(GTK_PROGRESS_BAR(app->progress), "Digesting cancelled");
		destroy_sum_context(app->currentsum);
		app->currentfile = 0;
		app->currentsum = NULL;
	}

}

static void addbutton_clicked(GtkButton *button, gpointer userdata)
{
	struct GHApp *app = userdata;
	GtkWidget *file_selector;
	gint ret;
	static gchar *lastdir = NULL;

	/*So, anything to eat in this joint*/
	file_selector = gtk_file_chooser_dialog_new("Select a file to digest. ", GTK_WINDOW(app->window), 
		GTK_FILE_CHOOSER_ACTION_OPEN, GTK_STOCK_ADD,GTK_RESPONSE_OK, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,NULL);
	gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(file_selector), TRUE);
	gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(file_selector), TRUE);	/*we only handle kernel filesystems*/
	if (lastdir != NULL) {
		gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(file_selector), lastdir);
	}
	/*Hmm, let me see the menu please */
	ret = gtk_dialog_run(GTK_DIALOG(file_selector));
	if (ret == GTK_RESPONSE_OK) {
		GSList *filelist, *tmp;
		/*Can you please take my order now ?*/
		filelist = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(file_selector));
		tmp = filelist;
		if(tmp){
			gtk_widget_set_sensitive(GTK_WIDGET(app->digestbutton), TRUE);
			gtk_widget_set_sensitive(GTK_WIDGET(app->clearbutton), TRUE);
			gtk_widget_set_sensitive(GTK_WIDGET(app->savebutton), FALSE);
			//gtk_widget_set_sensitive(GTK_WIDGET(app->verifybutton), FALSE);
		} 
		while (tmp) {
			g_ptr_array_add(app->files, tmp->data);
			tmp = tmp->next;
		}

		g_slist_free(filelist);
		app->currentfile = 0;
		fill_list_view(GTK_TREE_VIEW(app->listview), app->files);
		gtk_progress_bar_set_text(GTK_PROGRESS_BAR(app->progress), "...");
		gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(app->progress), 0.0);
		g_free(lastdir);
		lastdir = gtk_file_chooser_get_current_folder(GTK_FILE_CHOOSER(file_selector));
	}
	/*Let me have that menu back please.*/
	gtk_widget_destroy(file_selector);
}

static void setup_window_icon(GtkWidget *window)
{
	GdkPixbuf *icon;
	icon = gdk_pixbuf_new_from_xpm_data((const char **) hash_xpm);
	if (icon != NULL) {
		gtk_window_set_icon(GTK_WINDOW(window), icon);
		g_object_unref(icon);
	}

}

static void print_usage()
{
	g_print("Copyright © Nils O.Selåsdal <NOS@Utel.no> \n");
	g_print("Usage: ghasher [-h] [-d type] [filename ...]\n");
	g_print("\t-d calculate 'type' digest where 'type' is md2|md4|md5|sha1|sha|ripemd160|dss1\n");
	g_print("\t-h this text\n");
	g_print("\tfilename(s) name of file(s) to digest\n");
}

static void parse_options(int argc, char *argv[], struct GHApp *app)
{
	int c;

	app->digesttype = "md5";	/*default is md5*/
	app->files = g_ptr_array_new();
	app->currentfile = 0;
	app->currentsum = NULL;

	while ((c = getopt(argc, argv, "d:h")) != -1) {
		switch (c) {
		case 'h':
			print_usage();
			exit(1);
		case 'd':
			app->digesttype = optarg;
			break;
		case '?':
			break;
		default:
			g_print("Unknown option %c\n", c);
			break;
		}
	}
	while (optind < argc) {
		g_ptr_array_add(app->files, g_strdup(argv[optind]));
		optind++;
	}
}

/*A hack: */
static gboolean start_hash(gpointer userdata)
{
	struct GHApp *app = userdata;

	g_signal_emit_by_name(G_OBJECT(app->digestbutton), "clicked", app, NULL);

	return TRUE;
}

static void digest_changed(GtkComboBox *box, gpointer userdata)
{
	struct GHApp *app = userdata;
	gint index;
	g_object_get(G_OBJECT(box), "active", &index, NULL);
	app->digesttype = hashfunctions[index];
	fill_list_view(GTK_TREE_VIEW(app->listview),app->files); 

}

static GtkWidget *hashdropdown(struct GHApp *app, GtkWidget *combo)
{
	guint i;

	for (i = 0; i < G_N_ELEMENTS(hashfunctions); i++) {
		gtk_combo_box_append_text(GTK_COMBO_BOX(combo), hashfunctions[i]);
		if (!g_ascii_strncasecmp(app->digesttype, hashfunctions[i], MIN(strlen(app->digesttype), strlen(hashfunctions[i]))))
			g_object_set(G_OBJECT(combo), "active", i, NULL);
	}

	g_signal_connect(G_OBJECT(combo), "changed", G_CALLBACK(digest_changed), app);

	return combo;
}

int main(int argc, char *argv[])
{
	struct GHApp app;
	GladeXML *xml;

	gtk_init(&argc, &argv);
	parse_options(argc, argv, &app);
	OpenSSL_add_all_digests();
	xml = glade_xml_new_from_buffer(ui,strlen(ui),"mainWindow",NULL);
	app.window = glade_xml_get_widget(xml, "mainWindow");
	g_signal_connect(G_OBJECT(app.window), "delete-event", G_CALLBACK(gtk_main_quit), NULL);

	app.digestdropdown = hashdropdown(&app,glade_xml_get_widget(xml, "digestdropdown"));

	app.listview = create_list_view(glade_xml_get_widget(xml, "treeview"));

	app.progress = glade_xml_get_widget(xml, "progress");

	app.addbutton = glade_xml_get_widget(xml, "addtool");
	g_signal_connect(G_OBJECT(app.addbutton), "clicked", G_CALLBACK(addbutton_clicked), &app);
	app.digestbutton = glade_xml_get_widget(xml, "digesttool");
	g_signal_connect(G_OBJECT(app.digestbutton), "clicked", G_CALLBACK(digestbutton_clicked), &app);
	app.cancelbutton = glade_xml_get_widget(xml, "canceltool");
	g_signal_connect(G_OBJECT(app.cancelbutton), "clicked", G_CALLBACK(cancelbutton_clicked), &app);
	app.clearbutton = glade_xml_get_widget(xml, "cleartool");
	g_signal_connect(G_OBJECT(app.clearbutton), "clicked", G_CALLBACK(clearbutton_clicked), &app);
	app.savebutton = glade_xml_get_widget(xml, "savetool");
	g_signal_connect(G_OBJECT(app.savebutton), "clicked", G_CALLBACK(savebutton_clicked), &app);
	app.pathbutton = glade_xml_get_widget(xml, "pathtool");
	g_signal_connect(G_OBJECT(app.pathbutton), "toggled", G_CALLBACK(pathbutton_toggled), &app);
	//app.verifybutton = glade_xml_get_widget(xml, "verifytool");
	//g_signal_connect(G_OBJECT(app.verifybutton), "clicked", G_CALLBACK(verifybutton_clicked), &app);

	g_signal_connect(G_OBJECT(glade_xml_get_widget(xml, "quittool")), "clicked", G_CALLBACK(gtk_main_quit), NULL);

	gtk_widget_show_all(app.window);
	setup_window_icon(app.window);
	if (app.files->len > 0) 
		gtk_init_add(start_hash, &app);

	g_object_unref(xml);
	gtk_main();

	return 0;
}


syntax highlighted by Code2HTML, v. 0.9.1