/*
 * $Id: ede-autostart.cpp 3523 2013-02-04 00:02:28Z karijes $
 *
 * ede-autostart
 * Part of Equinox Desktop Environment (EDE).
 * Copyright (c) 2007-2009 EDE Authors.
 *
 * This program is licensed under terms of the 
 * GNU General Public License version 2 or newer.
 * See COPYING for details.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>

#include <FL/Fl.H>
#include <FL/Fl_Check_Browser.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Box.H>

#include <edelib/String.h>
#include <edelib/StrUtil.h>
#include <edelib/List.h>
#include <edelib/Util.h>
#include <edelib/DesktopFile.h>
#include <edelib/Directory.h>
#include <edelib/Debug.h>
#include <edelib/Run.h>
#include <edelib/FileTest.h>
#include <edelib/MessageBox.h>
#include <edelib/Window.h>
#include <edelib/IconLoader.h>
#include <edelib/WindowUtils.h>
#include <edelib/Ede.h>

EDELIB_NS_USING_AS(Window, AppWindow)
EDELIB_NS_USING_LIST(17, (String,
						  DesktopFile,
						  IconLoader,
						  list,
						  dir_list,
						  system_config_dirs,
						  user_config_dir,
						  str_ends,
						  str_tolower,
						  run_async,
						  ask,
						  alert,
						  file_test,
                          window_center_on_screen,
						  FILE_TEST_IS_REGULAR,
						  FILE_TEST_IS_EXECUTABLE,
						  ICON_SIZE_MEDIUM ))

#define CHECK_ARGV(argv, pshort, plong) ((strcmp(argv, pshort) == 0) || (strcmp(argv, plong) == 0))

#ifdef DEBUG_AUTOSTART_RUN
 #define AUTOSTART_RUN(s) E_DEBUG("Executing %s\n", s)
#else
 #define AUTOSTART_RUN(s) run_async(s)
#endif

#define AUTOSTART_DIRNAME "/autostart/"

/* ordered names that will be searched */
static const char* autostart_names[] = {
	".autorun",
	"autorun",
	"autorun.sh",
	0
};

struct DialogEntry {
	String name;
	String exec;
	String comment;
};

typedef list<String>           StringList;
typedef list<String>::iterator StringListIter;

typedef list<DialogEntry*>                 DialogEntryList;
typedef list<DialogEntry*>::iterator       DialogEntryListIter;

static AppWindow*        dialog_win;
static Fl_Check_Browser* cbrowser;

static char* get_basename(const char* path) {
	char* p = (char*)strrchr(path, '/');
	if(p) 
		return (p + 1);

	return (char*)path;
}

/*
 * 'Remove' duplicate entries by looking at their basename
 * (aka. filename, but ignoring directory path). Item is not actually removed from
 * the list (will mess up list pointers, but this can be resolved), but data it points
 * to is cleared, which is a sort of marker to caller to skip it. Dumb yes, but very simple.
 *
 * It will use brute force for lookup and 'removal' and (hopfully) it should not have 
 * a large impact on startup since, afaik, no one keeps hundreds of files in autostart 
 * directories (if keeps them, then that issue is not up to this program :-P).
 *
 * Alternative would be to sort items (by their basename) and apply consecutive unique on 
 * them, but... is it worth ?
 */
static void unique_by_basename(StringList& lst) {
	if(lst.empty())
		return;

	StringListIter first, last, first1, last1;
	first = lst.begin();
	last  = lst.end();

	if(first == last)
		return;

	const char* p1, *p2;
	for(; first != last; ++first) {
		for(first1 = lst.begin(), last1 = lst.end(); first1 != last1; ++first1) {
			p1 = (*first).c_str();
			p2 = (*first1).c_str();

			if(first != first1 && strcmp(get_basename(p1), get_basename(p2)) == 0)
				(*first1).clear();
		}
	}
}

static void entry_list_run_clear(DialogEntryList& l, bool run) {
	DialogEntryListIter dit = l.begin(), dite = l.end();
	for(; dit != dite; ++dit) {
		if(run)
			AUTOSTART_RUN((*dit)->exec.c_str());
		delete *dit;
	}

	l.clear();
}

static void dialog_runsel_cb(Fl_Widget*, void* e) {
	DialogEntryList* lst = (DialogEntryList*)e;
	E_ASSERT(lst->size() == (unsigned int)cbrowser->nitems() && "Size mismatch in local list and browser widget");

	DialogEntryListIter it = lst->begin();
	for(int i = 1; i <= cbrowser->nitems(); i++, ++it) {
		if(cbrowser->checked(i))
			AUTOSTART_RUN((*it)->exec.c_str());
	}

	dialog_win->hide();
	entry_list_run_clear(*lst, false);
}

static void dialog_runall_cb(Fl_Widget*, void* e) {
	DialogEntryList* lst = (DialogEntryList*)e;
	E_ASSERT(lst->size() == (unsigned int)cbrowser->nitems() && "Size mismatch in local list and browser widget");

	dialog_win->hide();
	entry_list_run_clear(*lst, true);
}

static void dialog_close_cb(Fl_Widget*, void* e) {
	dialog_win->hide();

	DialogEntryList* lst = (DialogEntryList*)e;
	entry_list_run_clear(*lst, false);
}

static void run_autostart_dialog(DialogEntryList& l) {
	DialogEntryList* ptr = (DialogEntryList*)&l;

	dialog_win = new AppWindow(370, 305, _("Autostart warning"));
	dialog_win->begin();
		Fl_Box* img = new Fl_Box(10, 10, 65, 60);
		Fl_Image* ii = (Fl_Image*)IconLoader::get("dialog-warning", ICON_SIZE_MEDIUM);
		img->image(ii);

		Fl_Box* txt = new Fl_Box(80, 10, 280, 60, _("The following applications are "
													"registered for starting. Please choose what to do next"));
		txt->align(FL_ALIGN_INSIDE | FL_ALIGN_LEFT | FL_ALIGN_WRAP);
		cbrowser = new Fl_Check_Browser(10, 75, 350, 185);

		DialogEntryListIter it = l.begin(), ite = l.end();
		for(; it != ite; ++it) {
			if((*it)->comment.empty())
				cbrowser->add((*it)->name.c_str());
			else {
				char buf[256];
				
				snprintf(buf, sizeof(buf), "%s (%s)", (*it)->name.c_str(), (*it)->comment.c_str());
				cbrowser->add(buf);
			}
		}

		Fl_Button* rsel = new Fl_Button(45, 270, 125, 25, _("Run &selected"));
		rsel->callback(dialog_runsel_cb, ptr);

		Fl_Button* rall = new Fl_Button(175, 270, 90, 25, _("&Run all"));
		rall->callback(dialog_runall_cb, ptr);

		Fl_Button* cancel  = new Fl_Button(270, 270, 90, 25, _("&Cancel"));
		cancel->callback(dialog_close_cb, ptr);
		cancel->take_focus();
	dialog_win->end();
	window_center_on_screen(dialog_win);
	dialog_win->show();

	while(dialog_win->shown())
		Fl::wait();
}

static void perform_autostart(bool safe) {
	String adir = edelib::user_config_dir();
	adir += AUTOSTART_DIRNAME;

	StringList dfiles, sysdirs, tmp;
	StringListIter it, ite, tmp_it, tmp_ite;

	dir_list(adir.c_str(), dfiles, true);

	system_config_dirs(sysdirs);
	if(!sysdirs.empty()) {
		for(it = sysdirs.begin(), ite = sysdirs.end(); it != ite; ++it) {
			*it += AUTOSTART_DIRNAME;

			/*
			 * append content
			 * FIXME: too much of copying. There should be some way to merge list items
			 * probably via merge() member
			 */
			dir_list((*it).c_str(), tmp, true);
			for(tmp_it = tmp.begin(), tmp_ite = tmp.end(); tmp_it != tmp_ite; ++tmp_it)
				dfiles.push_back(*tmp_it);
		}
	}

	if(dfiles.empty()) return;

	/*
	 * Remove duplicates where first one seen have priority to be keept. This way is required by spec. 
	 *
	 * Also handle this case (noted in spec): * if $XDG_CONFIG_HOME/autostart/foo.desktop and $XDG_CONFIG_DIRS/autostart/foo.desktop
	 * exists, but $XDG_CONFIG_HOME/autostart/foo.desktop have 'Hidden = true', * $XDG_CONFIG_DIRS/autostart/foo.autostart is ignored too.
	 *
	 * Later is implied via unique_by_basename().
	 */
	unique_by_basename(dfiles);

	const char* name;
	char buf[1024];
	DesktopFile df;
	DialogEntryList entry_list;

	for(it = dfiles.begin(), ite = dfiles.end(); it != ite; ++it) {
		if((*it).empty())
			continue;

		name = (*it).c_str();

		if(!str_ends(name, ".desktop"))
			continue;

		if(!df.load(name)) {
			E_WARNING(E_STRLOC ": Can't load '%s'. Skipping...\n", name);
			continue;
		}

		/* obey to OnlyShowIn rule */
		if(df.only_show_in(buf, sizeof(buf))) {
			str_tolower((unsigned char*)buf);
			if(strstr(buf, "ede") == NULL)
				continue;
		}

		/* obey to NotShowIn rule */
		if(df.not_show_in(buf, sizeof(buf))) {
			str_tolower((unsigned char*)buf);
			if(strstr(buf, "ede") != NULL)
				continue;
		}
		
 		/* files marked as hidden must be skipped */
		if(df.hidden() || !df.exec(buf, sizeof(buf)))
			continue;

		DialogEntry* en = new DialogEntry;
		en->exec = buf;

		/* figure out the name */
		if(df.name(buf, sizeof(buf)))
			en->name = buf;
		else
			en->name = name;

		/* get the comment */
		if(df.comment(buf, sizeof(buf)))
			en->comment = buf;

		entry_list.push_back(en);
	}

	if(entry_list.empty())
		return;

	if(safe)
		run_autostart_dialog(entry_list);
	else
		entry_list_run_clear(entry_list, true);
}

static void perform_autostart_scripts(const char* dir) {
	char path[PATH_MAX];

	for(int i = 0; autostart_names[i]; i++) {
		snprintf(path, sizeof(path), "%s%s%s", dir, E_DIR_SEPARATOR_STR, autostart_names[i]);

		if(file_test(path, FILE_TEST_IS_REGULAR | FILE_TEST_IS_EXECUTABLE)) {
			if(ask(_("Mounted media at '%s' would like to start some files. "
					 "Content of these files is not checked and could be malicious. "
					 "Would you like to start them?"), dir))
			{
				/* spec said how we must chdir to the root of the medium */
				errno = 0;
				if(chdir(dir) == 0)
					AUTOSTART_RUN(path);
				else
					alert(_("Unable to change folder: %s"), strerror(errno));
			}

			/* we only match the one file */
			break;
		}
	}
}

static void help(void) {
	puts("Usage: ede-autostart [OPTIONS]");
	puts("EDE autostart utility");
	puts("Options:");
	puts("  -h, --help         this help");
	puts("  -s, --safe         show dialog of commands to be executed");
	puts("  -m, --media [DEST] execute autostart scripts from mounted [DEST]");
}

static const char* next_param(int curr, char** argv, int argc) {
	int j = curr + 1;
	if(j >= argc)
		return NULL;
	if(argv[j][0] == '-')
		return NULL;
	return argv[j];
}

int main(int argc, char** argv) {
	EDE_APPLICATION("ede-autostart");

	if(argc == 1) {
		perform_autostart(false);
		return 0;
	}

	const char *a, *media = NULL;
	bool safe = false;

	for(int i = 1; i < argc; i++) {
		a = argv[i];
		if(CHECK_ARGV(a, "-h", "--help")) {
			help();
			return 0;
		}

		if(CHECK_ARGV(a, "-s", "--safe")) {
			safe = true;
		} else if(CHECK_ARGV(a, "-m", "--media")) {
			media = next_param(i, argv, argc);
			if(!media) {
				puts("Missing media parameter");
				return 1;
			}

			i++;
		} else {
			printf("Unknown '%s' parameter. Run 'ede-autostart -h' for options\n", a);
			return 1;
		}
	}

	if(media == NULL)
		perform_autostart(safe);
	else
		perform_autostart_scripts(media);
	return 0;
}
