/*
 * Copyright (C) 2016-2021, 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 `pkg-config --cflags --libs gtk4 webkit2gtk-5.0` -o squaw squaw.c
 */

#define _DEFAULT_SOURCE

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

int gc;
char **gv;
char *cdir;

GtkApplication *app;
GtkWidget *win;
WebKitWebView *view;
WebKitSettings *set;
WebKitWebContext *context;
WebKitUserContentManager *user;

/*
 * Enable spell checking. Enable cookie storage.
 */

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

void context_init() {
	context = webkit_web_context_get_default();

	webkit_web_context_set_spell_checking_enabled(context, true);
	webkit_web_context_set_spell_checking_languages(context, lang);

	char *f = g_build_filename(cdir, "cookies.db", NULL);
	WebKitCookieManager *m = webkit_web_context_get_cookie_manager(context);
	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.
 */

void user_init() {
	user = 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;
	char *d;
	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, style);
	webkit_user_content_manager_add_script(user, script);

	g_free(f);
	g_free(g);
}

/*
 * Set window title.
 */

char *title = NULL;
char *href = NULL;
int hrefn = false;
int secure = true;
double perc = 0;

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
		t = g_strdup_printf("[-/-] %s", title);

	gtk_window_set_title(GTK_WINDOW(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. Functions are bound to WebKitWebView signals in startup().
 */

void view_load (WebKitWebView *v, WebKitLoadEvent l) {
	switch (l) {
	case WEBKIT_LOAD_STARTED:
		title_p(0);
		break;
	case WEBKIT_LOAD_REDIRECTED:
		title_set(webkit_web_view_get_uri(v));
		break;
	case WEBKIT_LOAD_COMMITTED:
		secure &= 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;
	}
}

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 = false;
	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 CTRL+key.
 */

void do_go_back () { secure=true; webkit_web_view_go_back(view); }
void do_go_frwd () { secure=true; webkit_web_view_go_forward(view); }
void do_reload () { secure=true; webkit_web_view_reload_bypass_cache(view); }
void do_zoom_out () { webkit_web_view_set_zoom_level(view, webkit_web_view_get_zoom_level(view)/1.1); }
void do_zoom_in () { webkit_web_view_set_zoom_level(view, webkit_web_view_get_zoom_level(view)*1.1); }
void do_next () { webkit_find_controller_search_next(webkit_web_view_get_find_controller(view)); }
void do_print () { webkit_print_operation_run_dialog(webkit_print_operation_new(view), NULL); }
void do_copy () { gdk_clipboard_set_text(gdk_display_get_primary_clipboard(gdk_display_get_default()), href ? href : webkit_web_view_get_uri(view)); }
void do_undo () { webkit_web_view_execute_editing_command(view, WEBKIT_EDITING_COMMAND_UNDO); }
void do_redo () { webkit_web_view_execute_editing_command(view, WEBKIT_EDITING_COMMAND_REDO); }

void do_find () {
	char *out = NULL;
	char *cmd[] = { "xterm", "-name", "xrun", "-g", "x1-0+0", "-l", "-lf", "-", "-e", "head -n1", NULL };
	g_spawn_sync(NULL, cmd, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, &out, NULL, NULL, NULL);
	WebKitFindController *r = webkit_web_view_get_find_controller(view);
	webkit_find_controller_search_finish(r);
	if (out) {
		char **line = g_strsplit(out, "\r\n", 3);
		if (line[0] && line[1])
			webkit_find_controller_search(r, line[1], WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE | WEBKIT_FIND_OPTIONS_WRAP_AROUND, 999);
		g_strfreev(line);
		g_free(out);
	}
}

const GActionEntry actions[] = {
	{ "go_back",  do_go_back,  NULL, NULL, NULL },
	{ "go_frwd",  do_go_frwd,  NULL, NULL, NULL },
	{ "zoom_out", do_zoom_out, NULL, NULL, NULL },
	{ "zoom_in",  do_zoom_in,  NULL, NULL, NULL },
	{ "find",     do_find,     NULL, NULL, NULL },
	{ "next",     do_next,     NULL, NULL, NULL },
	{ "print",    do_print,    NULL, NULL, NULL },
	{ "reload",   do_reload,   NULL, NULL, NULL },
	{ "copy",     do_copy,     NULL, NULL, NULL },
	{ "undo",     do_undo,     NULL, NULL, NULL },
	{ "redo",     do_redo,     NULL, NULL, NULL },
};

const char *bindings[] = {
	"app.go_back",  "<Ctrl>Left",     NULL,
	"app.go_frwd",  "<Ctrl>Right",    NULL,
	"app.zoom_out", "<Ctrl>minus",    NULL,
	"app.zoom_in",  "<Ctrl>plus",     NULL,
	"app.find",     "<Ctrl>f",        NULL,
	"app.next",     "<Ctrl>n",        NULL,
	"app.print",    "<Ctrl>p",        NULL,
	"app.reload",   "<Ctrl>r",        NULL,
	"app.copy",     "<Ctrl>u",        NULL,
	"app.undo",     "<Ctrl>z",        NULL,
	"app.redo",     "<Shift><Ctrl>z", NULL,
	NULL
};

void keys_init () {
	g_action_map_add_action_entries(G_ACTION_MAP(app), actions, G_N_ELEMENTS(actions), app);
	for (const char **i=bindings; *i; i+=3) gtk_application_set_accels_for_action(app,*i,i+1);
}

/*
 * Links and downloads. Spawn new instance with middle or control click. Show
 * if new window requested. Download anything unsupported, show progress and
 * run `xdg-open` when done. Stuff lands in XDG_DOWNLOAD_DIR.
 */

void spawn_link() {
	char *zoom = g_strdup_printf("%f", webkit_web_view_get_zoom_level(view));
	char *agent = (char *)webkit_settings_get_user_agent(set);
	char *cmd[] = { gv[0], "-z", zoom, "-u", agent, href, NULL };
	g_spawn_async(NULL, cmd, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL);
	g_free(zoom);
}

void view_decide (WebKitWebView *v, WebKitPolicyDecision *d, WebKitPolicyDecisionType t) {
	WebKitResponsePolicyDecision *dd;
	WebKitNavigationAction *a;
	switch (t) {
	case WEBKIT_POLICY_DECISION_TYPE_NEW_WINDOW_ACTION:
		hrefn = true;
		title_update();
		webkit_policy_decision_ignore(d);
		break;
	case WEBKIT_POLICY_DECISION_TYPE_RESPONSE:
		dd = WEBKIT_RESPONSE_POLICY_DECISION(d);
		if (webkit_response_policy_decision_is_mime_type_supported(dd)) webkit_policy_decision_use(d);
		else webkit_policy_decision_download(d);
		break;
	case WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION:
		a = webkit_navigation_policy_decision_get_navigation_action(WEBKIT_NAVIGATION_POLICY_DECISION(d));
		if (webkit_navigation_action_get_navigation_type(a) == WEBKIT_NAVIGATION_TYPE_LINK_CLICKED && (
			webkit_navigation_action_get_mouse_button(a) == GDK_BUTTON_MIDDLE ||
			webkit_navigation_action_get_modifiers(a) & GDK_CONTROL_MASK)) {
				spawn_link();
				webkit_policy_decision_ignore(d);
		}
		break;
	}
}

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

void download_finished (WebKitDownload *d) {
	title_p(1);
	char *cmd[] = { "xdg-open", (char *)webkit_download_get_destination(d), NULL };
	g_spawn_async(NULL, cmd, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL);
}

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);
}

/*
 * 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.
 */

void startup (GtkApplication *app) {
	win = gtk_application_window_new(app);

	context_init();
	keys_init();
	user_init();

	view = WEBKIT_WEB_VIEW(webkit_web_view_new_with_user_content_manager(user));
	set = webkit_web_view_get_settings(view);

	g_signal_connect(view, "decide-policy", G_CALLBACK(view_decide), NULL);
	g_signal_connect(view, "load-changed", G_CALLBACK(view_load), NULL);
	g_signal_connect(view, "notify::title", G_CALLBACK(view_title), NULL);
	g_signal_connect(view, "notify::estimated-load-progress", G_CALLBACK(view_progress), NULL);
	g_signal_connect(view, "mouse-target-changed", G_CALLBACK(view_mouse), NULL);
	g_signal_connect(context, "download-started", G_CALLBACK(download_started), NULL);

	//webkit_settings_set_enable_dns_prefetching(set, true);
	webkit_settings_set_enable_developer_extras(set, true);
	webkit_settings_set_print_backgrounds(set, false);

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

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

void activate (GtkApplication *app) {
	gtk_window_set_child(GTK_WINDOW(win), GTK_WIDGET(view));
	gtk_widget_grab_focus(GTK_WIDGET(view));
	gtk_widget_show(win);
}

/*
 * Running it all.
 */

int main (int argc, char *argv[]) {
	umask(S_IRWXG|S_IRWXO);
	cdir = g_build_filename(getenv("HOME"), ".squaw", NULL);
	mkdir(cdir, S_IRWXU);
	gv = argv;
	gc = argc;

	gtk_init();
	app = gtk_application_new("org.vesath.squaw", G_APPLICATION_NON_UNIQUE);
	g_signal_connect(app, "startup", G_CALLBACK(startup), NULL);
	g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
	g_application_run(G_APPLICATION(app),0,NULL);
	g_object_unref(app);

	return 0;
}
