/*
 * Copyright (C) 2016, Gaetan Bisson <bisson@archlinux.org>.
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*
 * Squaw. The simplistic quiet user agent for the Web.
 *
 * Squaw is a Web browser based on the GTK+ port of WebKit2 which strives to be
 * minimalistic and flexible; it achieves both by merely consisting of this
 * short, easy-to-hack C file.
 *
 * Compile with:
 *
 *   cc -O2 `pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.0` -o squaw squaw.c
 */

#define _DEFAULT_SOURCE

#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
#include <webkit2/webkit2.h>

char *argv0;
char *cdir;

GtkWindow *win;
WebKitWebView *view;

/*
 * Block unwanted hosts.
 *
 * Binding block_resource() on WebKitWebView's resource-load-started signal
 * does not catch all network requests: some appear to go through, perhaps from
 * the JavaScript stack. The code below is left dead in the hope that it can be
 * useful again when a future WebKit2 release adresses this issue.
 *
 * In the meantime, use a more radical host-blocking solution if you need one:
 * add entries to /etc/hosts or LD_PRELOAD your own getaddrinfo().
 */

char *block_text = NULL;
GHashTable *block_hash;

void block_init () {
	char *f = g_build_filename(cdir, "block.txt", NULL);
	g_file_get_contents(f, &block_text, NULL, NULL);
	block_hash = g_hash_table_new(g_str_hash, g_str_equal);
	if (block_text) {
		char *h = strtok(block_text, "\x0a");
		do g_hash_table_add(block_hash, h);
		while (h=strtok(NULL, "\x0a"));
	}
	g_free(f);
}

void block_resource (WebKitWebView *v, WebKitWebResource *r, WebKitURIRequest *q) {
	char *u = g_strdup(webkit_uri_request_get_uri(q));
	char *h = strstr(u, "://");
	if (h) {
		h += 3;
		strtok(h, "/");
		if (g_hash_table_contains(block_hash, h)) webkit_uri_request_set_uri(q, "about:blank");
	}
	g_free(u);
}

/*
 * Enable spell checking. Set cache model.
 * Allow untrusted SSL. Enable cookie storage.
 */

const char const* lang[] = { "en_US", "fr_FR", NULL };

void context_init() {
	WebKitWebContext *c = webkit_web_context_get_default();

	webkit_web_context_set_spell_checking_enabled(c, true);
	webkit_web_context_set_spell_checking_languages(c, lang);
	webkit_web_context_set_cache_model(c, WEBKIT_CACHE_MODEL_WEB_BROWSER);
	webkit_web_context_set_tls_errors_policy(c, WEBKIT_TLS_ERRORS_POLICY_FAIL);

	char *f = g_build_filename(cdir, "cookies.db", NULL);
	WebKitCookieManager *m = webkit_web_context_get_cookie_manager(c);
	webkit_cookie_manager_set_accept_policy(m, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
	webkit_cookie_manager_set_persistent_storage(m, f, WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
}

/*
 * Add user style and script.
 */

WebKitUserContentManager *user_cm;

void user_init() {
	user_cm = webkit_user_content_manager_new();

	char *f = g_build_filename(cdir, "user.css", NULL);
	char *g = g_build_filename(cdir, "user.js", NULL);
	char *c = NULL;
	char *d = NULL;
	g_file_get_contents(f, &c, NULL, NULL);
	g_file_get_contents(g, &d, NULL, NULL);

	WebKitUserStyleSheet *style = webkit_user_style_sheet_new(c,
		WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
		WEBKIT_USER_STYLE_LEVEL_USER,
		NULL, NULL);

	WebKitUserScript *script = webkit_user_script_new(d,
		WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES,
		WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
		NULL, NULL);

	webkit_user_content_manager_add_style_sheet(user_cm, style);
	webkit_user_content_manager_add_script(user_cm, script);

	g_free(f);
	g_free(g);
}

/*
 * Set window title.
 */

char *title = NULL;
char *href = NULL;
int hrefn = 0;
gdouble perc = 0;
gboolean tls;
gboolean secure;

void title_update () {
	char *t;

	if (perc<1)
		t = g_strdup_printf("[%2i%%] %s", (int)(100*perc), title);
	else if (href && hrefn)
		t = g_strdup_printf("-[]-> %s", href);
	else if (href)
		t = g_strdup_printf("----> %s", href);
	else if (!secure)
		t = g_strdup_printf("[=/=] %s", title);
	else if (tls)
		t = g_strdup_printf("[===] %s", title);
	else 
		t = g_strdup_printf("[---] %s", title);

	gtk_window_set_title(win, t);
	g_free(t);
}

void title_set (const char *t) {
	g_free(title);
	title = g_strdup(t);
	title_update();
}

void title_p (gdouble p) {
	perc = p;
	title_update();
}

/*
 * React to view events.
 *
 * Those functions are bound to WebKitWebView signals in main().
 */

void view_load (WebKitWebView *v, WebKitLoadEvent l) {
	switch (l) {
	case WEBKIT_LOAD_STARTED:
		title_p(0);
		break;
	case WEBKIT_LOAD_REDIRECTED:
	case WEBKIT_LOAD_COMMITTED:
		secure = true;
		tls = webkit_web_view_get_tls_info(view, NULL, NULL);
		title_set(webkit_web_view_get_uri(v));
		break;
	case WEBKIT_LOAD_FINISHED:
		title_p(1);
		break;
	}
}

gboolean view_tls (WebKitWebView *v, gchar *r, GTlsCertificate *c, GTlsCertificateFlags e) {
	secure = false;
	title_update();
	return true;
}

void view_title (WebKitWebView *v) {
	title_set(webkit_web_view_get_title(v));
}

void view_progress (WebKitWebView *v) {
	title_p(webkit_web_view_get_estimated_load_progress(v));
}

void view_mouse (WebKitWebView *v, WebKitHitTestResult *h, guint m) {
	hrefn = 0;
	g_free(href);
	if (!webkit_hit_test_result_context_is_link(h)) href = NULL;
	else href = g_strdup(webkit_hit_test_result_get_link_uri(h));
	title_update();
}

/*
 * Define useful actions and bind them to a bunch of CTRL+keys.
 */

char *needle = NULL;

gboolean do_go_back () { webkit_web_view_go_back(view); return true; }
gboolean do_go_frwd () { webkit_web_view_go_forward(view); return true; }
gboolean do_zoom_out () { webkit_web_view_set_zoom_level(view, webkit_web_view_get_zoom_level(view)/1.1); return true; }
gboolean do_zoom_in () { webkit_web_view_set_zoom_level(view, webkit_web_view_get_zoom_level(view)*1.1); return true; }
gboolean do_next () { webkit_find_controller_search_next(webkit_web_view_get_find_controller(view)); return true; }
gboolean do_print () { webkit_print_operation_run_dialog(webkit_print_operation_new(view), NULL); return true; }
gboolean do_reload () { webkit_web_view_reload_bypass_cache(view); return true; }
gboolean do_copy () { gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY), href ? href : webkit_web_view_get_uri(view), -1); return true; }
gboolean do_undo () { webkit_web_view_execute_editing_command(view, WEBKIT_EDITING_COMMAND_UNDO); return true; }
gboolean do_redo () { webkit_web_view_execute_editing_command(view, WEBKIT_EDITING_COMMAND_REDO); return true; }

gboolean do_find () {
	char *f = g_strdup_printf("/tmp/squaw-%i", getpid());
	char *c = g_strdup_printf("xterm -name xrun -g x1-0+0 -e sh -c 'head -n1 > %s'",f);
	if (system(c)) return true;
	g_free(needle);
	g_file_get_contents(f, &needle, NULL, NULL);
	if (needle) {
		strtok(needle, "\x0a");
		WebKitFindController *r = webkit_web_view_get_find_controller(view);
		webkit_find_controller_search_finish(r);
		webkit_find_controller_search(r, needle, WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE | WEBKIT_FIND_OPTIONS_WRAP_AROUND, 999);
	}
	unlink(f);
	g_free(f);
	g_free(c);
	return true;
}

#define KEYS_BIND(mod,key,act) gtk_accel_group_connect(g, key, mod, 0, g_cclosure_new(G_CALLBACK(act), NULL, NULL));

void keys_init() {
	GtkAccelGroup *g = gtk_accel_group_new();
	KEYS_BIND(GDK_CONTROL_MASK,                  GDK_KEY_Left,  do_go_back);
	KEYS_BIND(GDK_CONTROL_MASK,                  GDK_KEY_Right, do_go_frwd);
	KEYS_BIND(GDK_CONTROL_MASK,                  GDK_KEY_minus, do_zoom_out);
	KEYS_BIND(GDK_CONTROL_MASK,                  GDK_KEY_plus,  do_zoom_in);
	KEYS_BIND(GDK_CONTROL_MASK,                  GDK_KEY_f,     do_find);
	KEYS_BIND(GDK_CONTROL_MASK,                  GDK_KEY_n,     do_next);
	KEYS_BIND(GDK_CONTROL_MASK,                  GDK_KEY_p,     do_print);
	KEYS_BIND(GDK_CONTROL_MASK,                  GDK_KEY_r,     do_reload);
	KEYS_BIND(GDK_CONTROL_MASK,                  GDK_KEY_u,     do_copy);
	KEYS_BIND(GDK_CONTROL_MASK,                  GDK_KEY_z,     do_undo);
	KEYS_BIND(GDK_CONTROL_MASK | GDK_SHIFT_MASK, GDK_KEY_z,     do_redo);
	gtk_window_add_accel_group(win, g);
}

/*
 * Downloads.
 *
 * Show if new window requested. Download anything unsupported, show progress
 * and run `xdg-open` when done. Stuff lands in XDG_DOWNLOAD_DIR.
 */

gboolean view_decide (WebKitWebView *v, WebKitPolicyDecision *d, WebKitPolicyDecisionType t) {
	switch (t) {
	case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION:
		hrefn = 1;
		title_update();
		return false;
	case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
		;WebKitResponsePolicyDecision *dd = WEBKIT_RESPONSE_POLICY_DECISION(d);
		if (webkit_response_policy_decision_is_mime_type_supported(dd)) return false;
		webkit_policy_decision_download(d);
		return true;
	}
	return false;
}

void download_progress (WebKitDownload *d) {
	title_p(webkit_download_get_estimated_progress(d));
}

void download_finished (WebKitDownload *d) {
	title_p(1);
	if (!fork()) {
		setsid();
		char *cmd[] = { "xdg-open", (char *)webkit_download_get_destination(d), NULL };
		execvp("xdg-open", cmd);
	}
}

void download_started (WebKitWebContext *c, WebKitDownload *d) {
	g_signal_connect(d, "notify::estimated-progress", G_CALLBACK(download_progress), NULL);
	g_signal_connect(d, "finished", G_CALLBACK(download_finished), NULL);
}

/*
 * Spawn new instance when link clicked.
 */

void spawn_link() {
	WebKitSettings *s = webkit_web_view_get_settings(view);
	char *useragent = (char *)webkit_settings_get_user_agent(s);
	char zoom[9];
	snprintf(zoom, 9, "%f", webkit_web_view_get_zoom_level(view));
	char *cmd[] = { argv0, "-z", zoom, "-u", useragent, href, NULL };
	if (!fork()) {
		setsid();
		execvp(argv0, cmd);
	}
}

gboolean spawn_button (WebKitWebView *v, GdkEventButton *e) {
	if (!href) return false;
	switch (e->button) {
	case 2: break;
	case 1: if (e->state & GDK_CONTROL_MASK) break;
	default: return false;
	}
	spawn_link();
	return true;
}

/*
 * Find resource key match, return URL.
 *
 * Each line of the form "key=url" in the "keys.txt" file has the effect that
 * the argument list "key foo bar" is mapped to the URL "urlfoo+bar".
 */

char *res (int argc, char *argv[]) {
	char *s = argc ? argv[0] : "";
	if (strstr(s, "://")) return s;
	if (strstr(s, ".")) return g_strdup_printf("http://%s", s);

	char *c;
	char *d = "https://duckduckgo.com/?q=";
	char *f = g_build_filename(cdir, "keys.txt", NULL);
	g_file_get_contents(f, &c, NULL, NULL);
	if (c) {
		char *e = strtok(c, "=");
			do if (!strcmp(e,s)) {
			d = g_strdup(strtok(NULL, "\x0a"));
			if (argc) { argc--; argv++; }
			break;
		} else strtok(NULL, "\x0a");
		while (e=strtok(NULL, "="));
	}
	g_free(c);
	g_free(f);

	int i;
	if (!argc) return d;
	char *u = g_strjoin("", d, argv[0], NULL);
	for (i=1;i<argc;i++) {
		char *t = g_strjoin("+", u, argv[i], NULL);
		g_free(u);
		u = t;
	}
	return u;
}

/*
 * Putting all together.
 */

int main (int argc, char *argv[]) {
	umask(0077);
	cdir = g_build_filename(getenv("HOME"), ".squaw", NULL);
	mkdir(cdir, 0700);

	gtk_init(NULL, NULL);
	win = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));

	// block_init();
	context_init();
	user_init();
	keys_init();

	view = WEBKIT_WEB_VIEW(webkit_web_view_new_with_user_content_manager(user_cm));

	g_signal_connect(G_OBJECT(view), "decide-policy", G_CALLBACK(view_decide), NULL);
	g_signal_connect(G_OBJECT(view), "load-changed", G_CALLBACK(view_load), NULL);
	g_signal_connect(G_OBJECT(view), "notify::title", G_CALLBACK(view_title), NULL);
	g_signal_connect(G_OBJECT(view), "notify::estimated-load-progress", G_CALLBACK(view_progress), NULL);
	g_signal_connect(G_OBJECT(view), "load-failed-with-tls-errors", G_CALLBACK(view_tls), NULL);
	g_signal_connect(G_OBJECT(view), "mouse-target-changed", G_CALLBACK(view_mouse), NULL);
	g_signal_connect(G_OBJECT(view), "button-release-event", G_CALLBACK(spawn_button), NULL);
	// g_signal_connect(G_OBJECT(view), "resource-load-started", G_CALLBACK(block_resource), NULL);

	WebKitWebContext *c = webkit_web_context_get_default();
	g_signal_connect(G_OBJECT(c), "download-started", G_CALLBACK(download_started), NULL);
	g_signal_connect(G_OBJECT(win), "destroy", G_CALLBACK(gtk_main_quit), NULL);

	WebKitSettings *s = webkit_web_view_get_settings(view);
	webkit_settings_set_enable_smooth_scrolling(s, true);
	webkit_settings_set_enable_developer_extras(s, true);
	webkit_settings_set_enable_dns_prefetching(s, true);
	webkit_settings_set_print_backgrounds(s, false);

	GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
	GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
	GtkWidget *pane = gtk_paned_new(GTK_ORIENTATION_VERTICAL);
	gtk_container_add(GTK_CONTAINER(scroll), GTK_WIDGET(view));
	gtk_container_add(GTK_CONTAINER(vbox), scroll);
	gtk_box_set_child_packing(GTK_BOX(vbox), scroll, true, true, 0, GTK_PACK_START);
	gtk_paned_pack1(GTK_PANED(pane), vbox, true, true);
	gtk_container_add(GTK_CONTAINER(win), pane);

	gtk_widget_show(GTK_WIDGET(win));
	gtk_widget_show(scroll);
	gtk_widget_show(vbox);
	gtk_widget_show(pane);
	gtk_widget_show(GTK_WIDGET(view));
	gtk_widget_grab_focus(GTK_WIDGET(view));

	int i = 1;
	argv0 = argv[0];
	while (i<argc) {
		if (argv[i][0]!='-') break;
		switch (argv[i][1]) {
		case 'u': webkit_settings_set_user_agent(s, argv[i+1]); break;
		case 'z': webkit_web_view_set_zoom_level(view, strtof(argv[i+1], NULL)); break;
		}
		i+=2;
	}
	char *u = res(argc-i, argv+i);

	webkit_web_view_load_uri(view, u);
	title_set(u);
	gtk_main();

	return 0;
}
