/*
 * wmmenugen - Window Maker PropList menu generator
 *
 * Desktop Entry Specification parser functions
 *
 * Copyright (c) 2010. Tamas Tevesz <ice@extreme.hu>
 *
 * 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, see <https://www.gnu.org/licenses/>.
 */

/*
 * http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.1.html
 * http://standards.freedesktop.org/menu-spec/menu-spec-1.1.html
 *
 * We will only deal with Type == "Application" entries in [Desktop Entry]
 * groups. Since there is no passing of file name arguments or anything of
 * the sort to applications from the menu, execname is determined as follows:
 * - If `TryExec' is present, use that;
 * - else use `Exec' with any switches stripped
 *
 * Only the (first, though there should not be more than one) `Main Category'
 * is used to place the entry in a submenu.
 *
 * Basic validation of the .desktop file is done.
 */

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

#include <ctype.h>
#include <ftw.h>
#if DEBUG
#include <errno.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "wmmenugen.h"

/* LocaleString match levels */
enum {
	MATCH_DEFAULT,
	MATCH_LANG,
	MATCH_LANG_MODIFIER,
	MATCH_LANG_COUNTRY,
	MATCH_LANG_COUNTRY_MODIFIER
};

typedef struct {
	char	*Name;		/* Name */				/* localestring */
	int	 MatchLevel;	/* LocaleString match type */		/* int */
	char	*TryExec;	/* TryExec */				/* string */
	char	*Exec;		/* Exec */				/* string */
	char	*Path;		/* Path */				/* string */
	int	 Flags;		/* Flags */
	char	*Category;	/* Categories (first item only) */	/* string */
} XDGMenuEntry;

static void  getKey(char **target, const char *line);
static void  getStringValue(char **target, const char *line);
static void  getLocalizedStringValue(char **target, const char *line, int *match_level);
static int   getBooleanValue(const char *line);
static void  getMenuHierarchyFor(char **xdgmenuspec);
static int   compare_matchlevel(int *current_level, const char *found_locale);
static Bool  xdg_to_wm(XDGMenuEntry *xdg, WMMenuEntry *wmentry);
static char *parse_xdg_exec(char *exec);
static void  init_xdg_storage(XDGMenuEntry *xdg);
static void  init_wm_storage(WMMenuEntry *wm);


void parse_xdg(const char *file, cb_add_menu_entry *addWMMenuEntryCallback)
{
	FILE *fp;
	char buf[1024];
	char *p, *tmp, *key;
	WMMenuEntry *wm;
	XDGMenuEntry *xdg;
	int InGroup;

	fp = fopen(file, "r");
	if (!fp) {
#if DEBUG
		fprintf(stderr, "Error opening file %s: %s\n", file, strerror(errno));
#endif
		return;
	}

	xdg = (XDGMenuEntry *)wmalloc(sizeof(XDGMenuEntry));
	wm = (WMMenuEntry *)wmalloc(sizeof(WMMenuEntry));
	InGroup = 0;
	memset(buf, 0, sizeof(buf));

	while (fgets(buf, sizeof(buf), fp)) {

		p = buf;

		/* skip whitespaces */
		while (isspace(*p))
			p++;
		/* skip comments, empty lines */
		if (*p == '\r' || *p == '\n' || *p == '#') {
			memset(buf, 0, sizeof(buf));
			continue;
		}
		/* trim crlf */
		buf[strcspn(buf, "\r\n")] = '\0';
		if (strlen(buf) == 0)
			continue;

		if (strcmp(p, "[Desktop Entry]") == 0) {
			/* if currently processing a group, we've just hit the
			 * end of its definition, try processing it
			 */
			if (InGroup && xdg_to_wm(xdg, wm)) {
				(*addWMMenuEntryCallback)(wm);
			}
			init_xdg_storage(xdg);
			init_wm_storage(wm);
			InGroup = 1;
			/* start processing group */
			memset(buf, 0, sizeof(buf));
			continue;
		} else if (p[0] == '[') {
			/* If we find a new group and the previous group was the main one,
			 * we stop all further processing
			 */
			if (InGroup) break;
		}

		if (!InGroup) {
			memset(buf, 0, sizeof(buf));
			continue;
		}

		getKey(&key, p);
		if (key == NULL) { /* not `key' = `value' */
			memset(buf, 0, sizeof(buf));
			continue;
		}

		if (strcmp(key, "Type") == 0) {
			getStringValue(&tmp, p);
			if (strcmp(tmp, "Application") != 0)
				InGroup = 0;	/* if not application, skip current group */
			wfree(tmp);
			tmp = NULL;
		} else if (strcmp(key, "Name") == 0) {
			getLocalizedStringValue(&xdg->Name, p, &xdg->MatchLevel);
		} else if (strcmp(key, "NoDisplay") == 0) {
			if (getBooleanValue(p))	/* if nodisplay, skip current group */
				InGroup = 0;
		} else if (strcmp(key, "Hidden") == 0) {
			if (getBooleanValue(p))
				InGroup = 0;	/* if hidden, skip current group */
		} else if (strcmp(key, "TryExec") == 0) {
			getStringValue(&xdg->TryExec, p);
		} else if (strcmp(key, "Exec") == 0) {
			getStringValue(&xdg->Exec, p);
		} else if (strcmp(key, "Path") == 0) {
			getStringValue(&xdg->Path, p);
		} else if (strcmp(key, "Terminal") == 0) {
			if (getBooleanValue(p))
				xdg->Flags |= F_TERMINAL;
		} else if (strcmp(key, "Categories") == 0) {
			getStringValue(&xdg->Category, p);
			getMenuHierarchyFor(&xdg->Category);
		}

		if (xdg->Category == NULL)
			xdg->Category = wstrdup(_("Other"));

		wfree(key);
		key = NULL;
	}

	fclose(fp);

	/* at the end of the file, might as well try to menuize what we have
	 * unless there was no group at all or it was marked as hidden
	 */
	if (InGroup && xdg_to_wm(xdg, wm))
		(*addWMMenuEntryCallback)(wm);

}


/* coerce an xdg entry type into a wm entry type
 */
static Bool xdg_to_wm(XDGMenuEntry *xdg, WMMenuEntry *wm)
{
	char *p;

	/* Exec or TryExec is mandatory */
	if (!(xdg->Exec || xdg->TryExec))
		return False;

	/* if there's no Name, use the first word of Exec or TryExec
	 */
	if (xdg->Name) {
		wm->Name = xdg->Name;
	} else  {
		if (xdg->TryExec)
			wm->Name = wstrdup(xdg->TryExec);
		else /* xdg->Exec */
			wm->Name = wstrdup(xdg->Exec);

		p = strchr(wm->Name, ' ');
		if (p)
			*p = '\0';
	}

	if (xdg->TryExec)
		wm->CmdLine = xdg->TryExec;
	else { /* xdg->Exec */
		wm->CmdLine = parse_xdg_exec(xdg->Exec);
		if (!wm->CmdLine)
			return False;
	}

	wm->SubMenu = xdg->Category;
	wm->Flags = xdg->Flags;
	if (wm->CmdLine != xdg->TryExec)
		wm->Flags |= F_FREE_CMD_LINE;

	return True;
}

static char *parse_xdg_exec(char *exec)
{
	char *cmd_line, *dst, *src;
	Bool quoted = False;

	cmd_line = wstrdup(exec);

	for (dst = src = cmd_line; *src; src++) {
		if (quoted) {
			if (*src == '"')
				quoted = False;
			else if (*src == '\\')
				switch (*++src) {
				case '"':
				case '`':
				case '$':
				case '\\':
					*dst++ = *src;
					break;
				default:
					goto err_out;
				}
			else
				*dst++ = *src;
		} else {
			if (*src == '"')
				quoted = True;
			else if (*src == '%') {
				src++;
				if (*src == '%')
					*dst++ = *src;
				else if (strchr ("fFuUdDnNickvm", *src))
					/*
					 * Skip valid field-code.
					 */
					;
				else
					/*
					 * Invalid field-code.
					 */
					goto err_out;
			} else
				*dst++ = *src;
		}
	}

	if (quoted)
		goto err_out;

	do
		*dst = '\0';
	while (dst > cmd_line && isspace(*--dst));

	return cmd_line;

err_out:
	wfree(cmd_line);
	return NULL;
}

/* (re-)initialize a XDGMenuEntry storage
 */
static void init_xdg_storage(XDGMenuEntry *xdg)
{

	if (xdg->Name)
		wfree(xdg->Name);
	if (xdg->TryExec)
		wfree(xdg->TryExec);
	if (xdg->Exec)
		wfree(xdg->Exec);
	if (xdg->Category)
		wfree(xdg->Category);
	if (xdg->Path)
		wfree(xdg->Path);

	xdg->Name = NULL;
	xdg->TryExec = NULL;
	xdg->Exec = NULL;
	xdg->Category = NULL;
	xdg->Path = NULL;
	xdg->Flags = 0;
	xdg->MatchLevel = -1;
}

/* (re-)initialize a WMMenuEntry storage
 */
static void init_wm_storage(WMMenuEntry *wm)
{
	if (wm->Flags & F_FREE_CMD_LINE)
		wfree(wm->CmdLine);

	wm->Name = NULL;
	wm->CmdLine = NULL;
	wm->Flags = 0;
}

/* get a key from line. allocates target, which must be wfreed later */
static void getKey(char **target, const char *line)
{
	const char *p;
	int kstart, kend;

	p = line;

	if (strchr(p, '=') == NULL) {		/* not `key' = `value' */
		*target = NULL;
		return;
	}

	kstart = 0;

	/* skip whitespace */
	while (isspace(*(p + kstart)))
		kstart++;

	/* skip up until first whitespace or '[' (localestring) or '=' */
	kend = kstart + 1;
	while (*(p + kend) && !isspace(*(p + kend)) && *(p + kend) != '=' && *(p + kend) != '[')
		kend++;

	*target = wstrndup(p + kstart, kend - kstart);
}

/* get a string value from line. allocates target, which must be wfreed later. */
static void getStringValue(char **target, const char *line)
{
	const char *p;
	char *q;
	int kstart;

	p = line;
	kstart = 0;

	/* skip until after '=' */
	while (*(p + kstart) && *(p + kstart) != '=')
		kstart++;
	kstart++;

	/* skip whitespace */
	while (*(p + kstart) && isspace(*(p + kstart)))
		kstart++;

	*target = wstrdup(p + kstart);

	for (p = q = *target; *p; p++) {
		if (*p != '\\') {
			*q++ = *p;
		} else {
			switch (*++p) {
			case 's':  *q++ = ' ';  break;
			case 'n':  *q++ = '\n'; break;
			case 't':  *q++ = '\t'; break;
			case 'r':  *q++ = '\r'; break;
			case '\\': *q++ = '\\'; break;
			default:
				/*
				 * Skip invalid escape.
				 */
				break;
			}
		}
	}
	*q = '\0';
}

/* get a localized string value from line. allocates target, which must be wfreed later.
 * matching is dependent on the current value of target as well as on the
 * level the current value is matched on. guts matching algorithm is in
 * compare_matchlevel().
 */
static void getLocalizedStringValue(char **target, const char *line, int *match_level)
{
	const char *p;
	char *locale;
	int kstart;
	int sqbstart, sqbend;

	p = line;
	kstart = 0;
	sqbstart = 0;
	sqbend = 0;
	locale = NULL;

	/* skip until after '=', mark if '[' and ']' is found */
	while (*(p + kstart) && *(p + kstart) != '=') {
		switch (*(p + kstart)) {
			case '[': sqbstart = kstart + 1;break;
			case ']': sqbend = kstart;	break;
			default	:			break;
		}
		kstart++;
	}
	kstart++;

	/* skip whitespace */
	while (isspace(*(p + kstart)))
		kstart++;

	if (sqbstart > 0 && sqbend > sqbstart)
		locale = wstrndup(p + sqbstart, sqbend - sqbstart);

	/* if there is no value yet and this is the default key, return */
	if (!*target && !locale) {
		*match_level = MATCH_DEFAULT;
		*target = wstrdup(p + kstart);
		return;
	}

	if (compare_matchlevel(match_level, locale)) {
		*target = wstrdup(p + kstart);
	}

	wfree(locale);

	return;
}

/* get a boolean value from line */
static Bool getBooleanValue(const char *line)
{
	char *p;
	int ret;

	getStringValue(&p, line);
	ret = strcmp(p, "true") == 0 ? True : False;
	wfree(p);

	return ret;
}

/* perform locale matching by implementing the algorithm specified in
 * xdg desktop entry specification, section "localized values for keys".
 */
static Bool compare_matchlevel(int *current_level, const char *found_locale)
{
	/* current key locale */
	char *key_lang, *key_ctry, *key_enc, *key_mod;

	parse_locale(found_locale, &key_lang, &key_ctry, &key_enc, &key_mod);

	if (env_lang && key_lang && strcmp(env_lang, key_lang) != 0) {
		/*
		 * Shortcut: if key and env languages don't match,
		 * don't even bother. This takes care of the great
		 * majority of the cases without having to go through
		 * the more theoretical parts of the spec'd algo.
		 */
		wfree(key_lang);
		wfree(key_ctry);
		wfree(key_enc);
		wfree(key_mod);
		return False;
	}

	if (!env_mod && key_mod) {
		/*
		 * If LC_MESSAGES does not have a MODIFIER field,
		 * then no key with a modifier will be matched.
		 */
		wfree(key_lang);
		wfree(key_ctry);
		wfree(key_enc);
		wfree(key_mod);
		return False;
	}

	if (!env_ctry && key_ctry) {
		/*
		 * Similarly, if LC_MESSAGES does not have a COUNTRY field,
		 * then no key with a country specified will be matched.
		 */
		wfree(key_lang);
		wfree(key_ctry);
		wfree(key_enc);
		wfree(key_mod);
		return False;
	}

	/* LC_MESSAGES value: lang_COUNTRY@MODIFIER */
	if (env_lang && env_ctry && env_mod) {		/* lang_COUNTRY@MODIFIER */
		if (key_lang && key_ctry && key_mod &&
		    strcmp(env_lang, key_lang) == 0 &&
		    strcmp(env_ctry, key_ctry) == 0 &&
		    strcmp(env_mod,  key_mod) == 0) {
			*current_level = MATCH_LANG_COUNTRY_MODIFIER;
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return True;
		} else if (key_lang && key_ctry &&	/* lang_COUNTRY */
		    strcmp(env_lang, key_lang) == 0 &&
		    strcmp(env_ctry, key_ctry) == 0 &&
		    *current_level < MATCH_LANG_COUNTRY) {
			*current_level = MATCH_LANG_COUNTRY;
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return True;
		} else if (key_lang && key_mod &&	/* lang@MODIFIER */
		    strcmp(env_lang, key_lang) == 0 &&
		    strcmp(env_mod,  key_mod) == 0 &&
		    *current_level < MATCH_LANG_MODIFIER) {
			*current_level = MATCH_LANG_MODIFIER;
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return True;
		} else if (key_lang &&			/* lang */
		    strcmp(env_lang, key_lang) == 0 &&
		    *current_level < MATCH_LANG) {
			*current_level = MATCH_LANG;
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return True;
		} else {
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return False;
		}
	}

	/* LC_MESSAGES value: lang_COUNTRY */
	if (env_lang && env_ctry) {			/* lang_COUNTRY */
		if (key_lang && key_ctry &&
		    strcmp(env_lang, key_lang) == 0 &&
		    strcmp(env_ctry, key_ctry) == 0 &&
		    *current_level < MATCH_LANG_COUNTRY) {
			*current_level = MATCH_LANG_COUNTRY;
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return True;
		} else if (key_lang &&			/* lang */
		    strcmp(env_lang, key_lang) == 0 &&
		    *current_level < MATCH_LANG) {
			*current_level = MATCH_LANG;
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return True;
		} else {
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return False;
		}
	}

	/* LC_MESSAGES value: lang@MODIFIER */
	if (env_lang && env_mod) {			/* lang@MODIFIER */
		if (key_lang && key_mod &&
		    strcmp(env_lang, key_lang) == 0 &&
		    strcmp(env_mod,  key_mod) == 0 &&
		    *current_level < MATCH_LANG_MODIFIER) {
			*current_level = MATCH_LANG_MODIFIER;
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return True;
		} else if (key_lang &&			/* lang */
		    strcmp(env_lang, key_lang) == 0 &&
		    *current_level < MATCH_LANG) {
			*current_level = MATCH_LANG;
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return True;
		} else {
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return False;
		}
	}

	/* LC_MESSAGES value: lang */
	if (env_lang) {					/* lang */
		if (key_lang &&
		    strcmp(env_lang, key_lang) == 0 &&
		    *current_level < MATCH_LANG) {
			*current_level = MATCH_LANG;
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return True;
		} else {
			wfree(key_lang);
			wfree(key_ctry);
			wfree(key_enc);
			wfree(key_mod);
			return False;
		}
	}

	/* MATCH_DEFAULT is handled in getLocalizedStringValue */

	wfree(key_lang);
	wfree(key_ctry);
	wfree(key_enc);
	wfree(key_mod);
	return False;
}

/* get the (first) xdg main category from a list of categories
 */
static void  getMenuHierarchyFor(char **xdgmenuspec)
{
	char *category, *p;
	char buf[1024];

	if (!*xdgmenuspec || !**xdgmenuspec)
		return;

	category = wstrdup(*xdgmenuspec);
	wfree(*xdgmenuspec);
	memset(buf, 0, sizeof(buf));

	p = strtok(category, ";");
	while (p) {		/* get a known category */
		if (strcmp(p, "AudioVideo") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Audio & Video"));
			break;
		} else if (strcmp(p, "Audio") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Audio"));
			break;
		} else if (strcmp(p, "Video") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Video"));
			break;
		} else if (strcmp(p, "Development") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Development"));
			break;
		} else if (strcmp(p, "Education") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Education"));
			break;
		} else if (strcmp(p, "Game") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Game"));
			break;
		} else if (strcmp(p, "Graphics") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Graphics"));
			break;
		} else if (strcmp(p, "Network") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Network"));
			break;
		} else if (strcmp(p, "Office") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Office"));
			break;
		} else if (strcmp(p, "Science") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Science"));
			break;
		} else if (strcmp(p, "Settings") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Settings"));
			break;
		} else if (strcmp(p, "System") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("System"));
			break;
		} else if (strcmp(p, "Utility") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Utility"));
			break;
		/* reserved categories */
		} else if (strcmp(p, "Screensaver") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Screensaver"));
			break;
		} else if (strcmp(p, "TrayIcon") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Tray Icon"));
			break;
		} else if (strcmp(p, "Applet") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Applet"));
			break;
		} else if (strcmp(p, "Shell") == 0) {
			snprintf(buf, sizeof(buf), "%s", _("Shell"));
			break;
		}
		p = strtok(NULL, ";");
	}

	wfree(category);

	if (!*buf)		/* come up with something if nothing found */
		snprintf(buf, sizeof(buf), "%s", _("Other"));

	*xdgmenuspec = wstrdup(buf);
}
