/*
 * Gtk+ Simple File Verifier.
 *
 * Copyright (c) 2008, Andreas Henriksson <andreas@fatal.se>
 *
 * This program 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.
 *
 * This program 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MAi
 * 02110-1301, USA.
 */

#define _BSD_SOURCE /* for strdup */

#include <stdio.h>
#include <stdlib.h> /* exit */
#include <string.h> /* strlen */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <pthread.h>
#include <signal.h>

#include <glib.h>
#include <gtk/gtk.h>
#include <gio/gio.h>

#include "zlib.h"

/* internal definitions */
#ifndef DATA_DIR
#define DATA_DIR "/home/gem/opt/ahsfv/"
#endif

#ifndef PROGNAME
#define PROGNAME "ahsfv"
#endif

struct sfvresult {
	gboolean checksum_ok;
	char *filename;
};


/* global variables */
static gboolean verbose = FALSE;

GtkWidget *window, *treeview, *quit_button, *cancel_button, *status_label;
GdkPixbuf *pixbuf_success, *pixbuf_failure;

pthread_mutex_t results_lock;
GQueue *results = NULL;
gboolean results_finished;

pthread_t producer;

/***************************************************************************
 * SFV functions 
 ***************************************************************************/

void producer_sighandler(int signum)
{
	/* make sure mutex is unlocked. ignore errors. */
	pthread_mutex_unlock (&results_lock);

	pthread_exit((void*)1);
}

void produce_result (char *filename, char *crc, gboolean checksum_ok)
{
	struct sfvresult *r;

	/* create a sfvresult slot to insert in results */
	r = malloc (sizeof (struct sfvresult));
	if (!r) {
		fprintf(stderr, "ERROR: malloc failed.\n");
		exit(9997);
	}

	/* crc checksum parameter hits the bitbucket here. No use for it. */
	r->checksum_ok = checksum_ok;
	r->filename = strdup(filename);

	/* take lock */
	if (pthread_mutex_lock (&results_lock) != 0) {
		fprintf(stderr, "ERROR: could not aquire results lock!\n");
		exit(9998);
	}

	/* append result slot */
	g_queue_push_tail (results, r);

	/* release lock */
	if (pthread_mutex_unlock (&results_lock) != 0) {
		fprintf(stderr, "ERROR: could not release results lock!\n");
		exit(9999);
	}

	/* signal consumer */
	kill(getpid(), SIGUSR1);
}

/**
 * validate_crc:
 * @sfvfile: a GFile handle to the parsed sfv (to know working directory).
 * @sfvrelpath: the (mangled) file path from the .sfv file.
 * @sfvcrc: the crc from the same row in the .sfv file.
 *
 * Check that a file parsed from a .sfv has the correct checksum.
 **/
int validate_crc(GFile *sfvfile, char *sfvrelpath, char *sfvcrc)
{
	GFile *parent, *file;
	char *parent_uri, *uri;
	GFileInputStream *fis;
	GError *err = NULL;
	char buffer[1024];
	gssize length;
	unsigned long crc = crc32 (0UL, NULL, 0);

	/* open file */
	parent = g_file_get_parent(sfvfile);
	parent_uri = g_file_get_uri(parent);
	uri = g_build_filename(parent_uri, sfvrelpath, NULL);

	file = g_file_new_for_uri(uri);

	g_free(uri);
	g_free(parent_uri);
	g_object_unref(parent);

	/* get input stream */
	fis = g_file_read(file, NULL, &err);

	if (!fis) {
		fprintf(stderr, "ERROR: opening %s: %s\n", file, err->message);
		g_object_unref(file);
		return -2;
	}

	/* fill buffer and check crc */
	while ((length = g_input_stream_read (G_INPUT_STREAM(fis),
			buffer, 1024, NULL, &err)) > 0) {
		crc = crc32 (crc, buffer, length);
	}

	/* close up */
	g_object_unref(fis);
	g_object_unref(file);

	/* compare results */
	sprintf(buffer, "%08X", crc);
	return (strcasecmp(buffer, sfvcrc) == 0);
}

/**
 * parse_sfv:
 * @sfvfilename: the uri or path to an .sfv file to validate.
 *
 * Parses and validates checksum for all files listed in the given .sfv file.
 **/
int parse_sfv(char *sfvfilename)
{
	GFile *file;
	GFileInputStream *fis;
	GDataInputStream *dis;
	GError *err = NULL;
	char *line;
	gsize readlen = 1024;

	if (!g_uri_parse_scheme (sfvfilename)) {
		file = g_file_new_for_path(sfvfilename);
	} else {
		file = g_file_new_for_uri(sfvfilename);
	}

	fis = g_file_read(file, NULL, &err);
	dis = g_data_input_stream_new(G_INPUT_STREAM(fis));

	if (!fis || !dis) {
		fprintf(stderr, "ERROR: Failed to open %s: %s\n", sfvfilename,
				err->message);
		g_object_unref(file);
		return -1;
	}


	/* for each line in the sfv file */
	while ((line = g_data_input_stream_read_line(dis,
			&readlen, NULL, &err)) != NULL) {
		char *crc;
	
		/* skip comment lines */
		if (*line == ';') {
			g_free (line);
			readlen = 1024;
			continue;
		}

		/* find end of name, beginning of crc */
		crc = strrchr(line, ' ');
		if (!crc) {
			fprintf(stderr, "ERROR: parsing '%s'\n", line);
			continue;
		}
		*(crc++) = '\0';

		/* crc has 8 characters,
		 * strip of trailing newlines and whatever.
		 */
		*(crc+8) = '\0';


		/* convert paths to unix style */
		for (int i = 0; i < strlen(line); i++) {
			/* replace dos dir separator character */
			if (line[i] == '\\')
				line[i]='/';
		}

		printf("DEBUG: name=%s, crc=%s (readlen=%d)\n", line, crc, readlen);

		if (!validate_crc(file, line, crc)) {
			produce_result(line, crc, FALSE);
			fprintf(stderr, "CRC for %s does not match %s!\n",
					line, crc);
		} else {
			produce_result(line, crc, TRUE);
			if (verbose)
				printf("CRC %s match for %s\n", crc, line);
		}
		g_free(line);
		readlen = 1024;
	}
	
	g_object_unref(dis);
	g_object_unref(fis);
	g_object_unref(file);

	return 0;
}

/* thread main function */
void *task_sfvchecker (void *indata)
{
	char *sfvfilename = indata;
	struct sigaction hupact;

	hupact.sa_handler = producer_sighandler;
	sigemptyset (&hupact.sa_mask);
	hupact.sa_flags = 0;

	sigaction (SIGHUP, &hupact, NULL);

	results_finished = FALSE;

	parse_sfv(sfvfilename);

	results_finished = TRUE;
	/* signal consumer */
	kill(getpid(), SIGUSR1);

	return NULL;
}

/***************************************************************************
 * Gtk functions 
 ***************************************************************************/

#define STATUS_COLUMN 0
#define FILENAME_COLUMN 1
#define MATCH_COLUMN 2

/* OK button */
void on_button1_clicked (GtkWidget *w, gpointer user_data)
{
	/* stop worker thread */

	/* exit program */
	gtk_main_quit();
}

/* Cancel button */
void on_button2_clicked (GtkWidget *w, gpointer user_data)
{
	/* if worker thread is active, stop it. */
	pthread_kill (producer, SIGHUP);
	/* enable ok button, disable self. */
	gtk_widget_set_sensitive(GTK_WIDGET(quit_button), TRUE);
	gtk_widget_set_sensitive(GTK_WIDGET(user_data), FALSE);
}

gboolean on_window1_delete_event (GtkWidget *widget, GdkEvent *event,
		gpointer user_data)
{
	gtk_main_quit();
	return FALSE;
}


void ahsfv_treeview_init(GtkTreeView *tv)
{
	GtkListStore *ls = gtk_list_store_new(3, 
			GDK_TYPE_PIXBUF, G_TYPE_STRING, G_TYPE_BOOLEAN);
	GtkCellRenderer *pixrend, *textrend;
	GtkTreeViewColumn *column;

	gtk_tree_view_set_model(tv, GTK_TREE_MODEL(ls));
	g_object_unref(ls); /* destroy model/store with treeview */

	pixrend = gtk_cell_renderer_pixbuf_new();
	textrend = gtk_cell_renderer_text_new();

	column = gtk_tree_view_column_new_with_attributes ("Status",
		pixrend, "pixbuf", STATUS_COLUMN, NULL);
	gtk_tree_view_append_column (GTK_TREE_VIEW (tv), column);

	column = gtk_tree_view_column_new_with_attributes ("Filename",
			textrend, "text", FILENAME_COLUMN, NULL);
	gtk_tree_view_append_column (GTK_TREE_VIEW (tv), column);

	pixbuf_success = gdk_pixbuf_new_from_file (
			"/usr/share/pixmaps/apple-green.png", NULL);
	pixbuf_failure = gdk_pixbuf_new_from_file (
			"/usr/share/pixmaps/apple-red.png", NULL);
}

void ahsfv_treeview_add(char *filename, gboolean checksum_ok)
{
	GtkTreeView *tv = GTK_TREE_VIEW(treeview);
	GtkListStore *ls = GTK_LIST_STORE(gtk_tree_view_get_model(tv));
	GtkTreeIter iter;
	GdkPixbuf *pixbuf;

	if (checksum_ok == TRUE && verbose == FALSE) {
		/* just update status label. */
		char buf[1024];

		snprintf(buf, 1024, "Processing %s...", filename);

		gtk_label_set_text(GTK_LABEL(status_label), buf);
		gtk_widget_show(status_label);

		return;
	}

	if (checksum_ok)
		pixbuf = pixbuf_success;
	else 
		pixbuf = pixbuf_failure;

	gtk_list_store_append (ls, &iter);
	gtk_list_store_set (ls, &iter,
		STATUS_COLUMN, pixbuf,
		FILENAME_COLUMN, filename,
		MATCH_COLUMN, checksum_ok,
		-1);
	
	gtk_widget_show(treeview);

}

void ahsfv_opensfv(char *sfvfilename)
{
	char buf[1024];

	/* initialize */
	results = g_queue_new ();

	/* set window title */
	snprintf(buf, 1024, "%s - %s", PROGNAME, sfvfilename);
	gtk_window_set_title(GTK_WINDOW(window), buf);

	/* start worker thread */
	pthread_create(&producer, NULL, task_sfvchecker, sfvfilename);
	pthread_detach(producer);
}

void consume_result (void)
{

	/* try to lock or bail out. */
	if (pthread_mutex_trylock(&results_lock) != 0) {
		fprintf(stderr, "WARN: consumer failed to lock, skipping.\n");
		return;
	}

	/* consume what's in the queue */
	while (!g_queue_is_empty (results)) {
		struct sfvresult *r = g_queue_pop_head (results);

		ahsfv_treeview_add(r->filename, r->checksum_ok);
	
		free(r->filename);
	}

	if (results_finished == TRUE) {
		/* calculate summary */
		GtkTreeModel *tvm = gtk_tree_view_get_model(
				GTK_TREE_VIEW(treeview));
		GtkTreeIter iter;

		if (gtk_tree_model_get_iter_first(tvm, &iter)) {
			int failure = 0;

			do {
				GValue value;

				g_value_init(&value, G_TYPE_BOOLEAN);

				GdkPixbuf *pixbuf_tmp;
				gtk_tree_model_get_value (tvm, &iter,
						MATCH_COLUMN,
						&value);
				

				if (!g_value_get_boolean(&value))
					failure++;
			} while (gtk_tree_model_iter_next (tvm, &iter));

			if (failure == 0)
				gtk_label_set_text(GTK_LABEL(status_label), 
						"All OK!");
			else {
				char buf[1024];

				sprintf(buf, "%d file(s) failed CRC check!",
					failure);

				gtk_label_set_text(GTK_LABEL(status_label),
					buf);
			}


		} else
			gtk_label_set_text(GTK_LABEL(status_label), 
					"Success!");


		/* disable cancel, enable quit buttons */
		gtk_widget_set_sensitive(GTK_WIDGET(cancel_button), FALSE);
		gtk_widget_set_sensitive(GTK_WIDGET(quit_button), TRUE);
	}

	/* unlock */
	if (pthread_mutex_unlock(&results_lock) != 0) {
		fprintf(stderr, "ERR: consumer failed to unlock results!\n");
		exit(8888);
	}

}

void consumer_sighandler (int signum)
{
	consume_result();
}

/***************************************************************************
 * Main 
 ***************************************************************************/

void usage(char *name)
{
	fprintf(stderr, "Usage: %s [-v] <file.sfv>\n", name);
}

int main (int argc, char **argv)
{
	char *sfvfile;

	GtkBuilder* gbuild;
	gpointer user_data = NULL; /* FIXME */

	GError *err = NULL;
	char *translation_domain = NULL; /* FIXME */

	static GOptionEntry entries[] = {
		{
			.long_name = "verbose",
			.short_name = 'v',
			.flags = G_OPTION_FLAG_IN_MAIN,
			.arg = G_OPTION_ARG_NONE,
			.arg_data = &verbose,
			.description = "Enable verbose output.",
			.arg_description = NULL,
		},
		{
			NULL
		}
	};

	struct sigaction usr1act;

	usr1act.sa_handler = consumer_sighandler;
	sigemptyset (&usr1act.sa_mask);
	usr1act.sa_flags = 0;

	sigaction (SIGUSR1, &usr1act, NULL);

	if (!gtk_init_with_args (&argc, &argv,
			"ahgtksfv [-v] <file.sfv>",
			entries,
			translation_domain, &err)) {
		fprintf(stderr, "GTK+ initialization failed.\n");
		exit(-1);
	}

	if (argc < 2) {
		usage(argv[0]);
		exit(-1);
	}

	sfvfile = argv[1];

	gbuild = gtk_builder_new();
	if (gtk_builder_add_from_file(gbuild, DATA_DIR "ahgtksfv.ui",
			&err) == 0) {
		fprintf(stderr, "ERROR: building gtk interface failed: %s.\n",
				err->message);
		exit(-1);
	}

	/* user_data is sent in as user data to all signal handlers */
	gtk_builder_connect_signals (gbuild, user_data);

	window = GTK_WIDGET (gtk_builder_get_object (gbuild, "window1"));
	treeview = GTK_WIDGET (gtk_builder_get_object (gbuild, 
			"treeview1"));
	quit_button = GTK_WIDGET (gtk_builder_get_object (gbuild, "button1"));
	cancel_button = GTK_WIDGET (gtk_builder_get_object (gbuild,
			"button2"));
	status_label = GTK_WIDGET (gtk_builder_get_object (gbuild, "label1"));

	ahsfv_treeview_init(GTK_TREE_VIEW(treeview));


	ahsfv_opensfv(sfvfile);

	gtk_widget_show_all (window);

	gtk_main();

	return 0;
}


