/*
 * Copyright (C) 2024 alx@fastestcode.org
 *  
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE
 */

/* $Id: xfile-xdg.c,v 1.6 2024/12/26 19:42:14 alx Exp $ */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <signal.h>
#include <dirent.h>
#include <unistd.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <dbus/dbus.h>

#define XFILE_BIN "xfile"
#define DBUS_NAME "org.freedesktop.FileManager1"

int decode_url(char *url);
int process_message(const char*, DBusMessage*);
void stderr_msg(const char *fmt, ...);
char* find_browser(void);

char *g_bin_name;
DBusConnection *g_dbus;

int main(int argc, char **argv)
{
	char *home;
	DBusError err;
	char *ptr;
	int i, status;
	int done = 0;
	
	if( (g_bin_name = strrchr(argv[0], '/')) )
		g_bin_name++;
	else
		g_bin_name = argv[0];

	dbus_error_init(&err);
	
	g_dbus = dbus_bus_get(DBUS_BUS_SESSION, &err);
	if(dbus_error_is_set(&err)) {
		stderr_msg("DBUS connection error: %s", err.message);
		dbus_error_free(&err);
		if(g_dbus) dbus_connection_unref(g_dbus);
		return EXIT_FAILURE;
	}

	status = dbus_bus_request_name(g_dbus, DBUS_NAME,
			DBUS_NAME_FLAG_REPLACE_EXISTING, &err);	
	if(dbus_error_is_set(&err)) {
		stderr_msg("Failed to obtain DBUS name (%s), %s",
			DBUS_NAME, err.message);
		dbus_error_free(&err);
		dbus_connection_unref(g_dbus);
		return EXIT_FAILURE;
	}
	
	while(!done) {
		DBusMessage *msg;
		DBusMessage *reply;
		char *iface;
		char *member;
		int i;
		
		dbus_connection_read_write_dispatch(g_dbus, 300);
		msg = dbus_connection_pop_message(g_dbus);
		if(msg == NULL) {
			stderr_msg("Timed out while waiting for messages.\n");
			break;
		}

		iface = (char*) dbus_message_get_interface(msg);
		member = (char*) dbus_message_get_member(msg);

		reply = dbus_message_new_method_return(msg);
		dbus_connection_send(g_dbus, reply, NULL);
		dbus_message_unref(reply);
				
		if(!strcmp(iface, DBUS_NAME)) {
			status = process_message(member, msg);
			done = 1;
		}
	}

	dbus_connection_unref(g_dbus);
	return status ? EXIT_FAILURE : EXIT_SUCCESS;
}

int process_message(const char *action, DBusMessage *msg)
{
	DBusMessageIter it, it_sub;
	DBusMessageIter *it_ptr;
	int arg_type;
	int nargs = 0;
	char **args;
	char **arg_ptr;
	char *exec_str;
	size_t len;
	char *browser_bin = find_browser();

	dbus_message_iter_init(msg, &it);

	for(it_ptr = &it ; ;) {
		arg_type = dbus_message_iter_get_arg_type(it_ptr);

		if(arg_type == DBUS_TYPE_INVALID) {
			if(it_ptr == &it) {
				break;
			} else {
				it_ptr = &it;
				continue;
			}
		} else if(arg_type == DBUS_TYPE_ARRAY) {
			dbus_message_iter_recurse(&it, &it_sub);
			it_ptr = &it_sub;
			dbus_message_iter_next(&it);
			continue;
		}
		nargs++;
		dbus_message_iter_next(it_ptr);
	}

	args = malloc((nargs + 1) * sizeof(char*));
	args[0] = NULL;

	if(nargs) {
		int i;
		
		arg_ptr = args;
		
		dbus_message_iter_init(msg, &it);
		
		for(it_ptr = &it; ; ) {
			arg_type = dbus_message_iter_get_arg_type(it_ptr);

			if(arg_type == DBUS_TYPE_INVALID) {
				if(it_ptr == &it) {
					break;
				} else {
					it_ptr = &it;
					continue;
				}
			} else if(arg_type == DBUS_TYPE_ARRAY) {
				dbus_message_iter_recurse(&it, &it_sub);
				it_ptr = &it_sub;
				dbus_message_iter_next(&it);
				continue;
			} else if(arg_type == DBUS_TYPE_STRING) {
				const char *value;
				dbus_message_iter_get_basic(it_ptr, &value);
				if(strlen(value)) {
					*arg_ptr = strdup(value);
					arg_ptr++;
				}
			}
			dbus_message_iter_next(it_ptr);
		}
		*arg_ptr = NULL;
	}


	dbus_message_unref(msg);
	dbus_connection_unref(g_dbus);	

	for(arg_ptr = args; *arg_ptr; arg_ptr++) {
		int rv;
		pid_t pid;
		struct stat st;
		char *exec_bin = XFILE_BIN;
		char *carg = *arg_ptr;
		char *exec_args[3];

		if(!strncasecmp("http://", carg, 7) ||
			!strncasecmp("https://", carg, 8)) {
			if(browser_bin) {
				exec_bin = browser_bin;
			} else {
				stderr_msg("Cannot find browser to handle %s\n", carg);
				continue;
			}
		} else {
			if(!strncasecmp("file://", carg, 7)) {
				carg += 7;
				if( (rv = decode_url(carg)) ) {
					stderr_msg(strerror(rv), carg);
					continue;
				}
			}

			if(!stat(carg, &st) && !S_ISDIR(st.st_mode)) {
				carg = dirname(carg);
			}
		}

		exec_args[0] = exec_bin;
		exec_args[1] = carg;
		exec_args[2] = NULL;
		
		pid = vfork();
		if(!pid) {
			if(execvp(exec_bin, exec_args) == -1)
				rv = errno;
			_exit(0);
		}
		if(rv) stderr_msg("Error executing '%s' (%s).\n",
			exec_str, strerror(rv));

		free(*arg_ptr);
	}
	
	free(args);
}

int decode_url(char *url)
{
	char *p = url;
	char *r;
	char *buf;
	
	r = buf = malloc(strlen(url) + 1);
	
	if(!buf) return ENOMEM;
	
	while(*p) {

		if(*p == '%' && p[1] && p[2]) {
			char val[3];
			
			p++;
			memcpy(val, p, 2);
			val[2] = '\0';
			*r = (char)strtol(val, NULL, 16);
			
			p++;
		} else {
			*r = *p;
		}
		
		r++;
		p++;
	};
	*r = '\0';

	strcpy(url, buf);
	free(buf);

	return 0;
}

char* find_browser(void)
{
	static char *bin = NULL;

	if(!bin) {
		bin = getenv("BROWSER");

		if(!bin && (!access("/usr/bin/x-www-browser", X_OK) ||
			!access("/usr/local/bin/x-www-browser", X_OK)) ) {
			bin = "x-www-browser";
		}
		
		if(!bin && (!access("/usr/bin/xdg-open", X_OK) ||
			!access("/usr/local/bin/xdg-open", X_OK)) ) {
			bin = "xdg-open";
		}
	}
	return bin;
}

void stderr_msg(const char *fmt, ...)
{
	va_list ap;

	fprintf(stderr, "[%s] ", g_bin_name);
	va_start(ap, fmt);
	vfprintf(stderr, fmt, ap);
	va_end(ap);
	fflush(stderr);
}
