/*
 * Copyright (c) 2011 Paulo Zanoni
 *
 * 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 (including the next
 * paragraph) 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.
 */


#include <cassert>
#include <cstdio>
#include <cstring>
#include <cstdlib>

#include <iostream>

extern "C" {
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
}

#include "XDGDesktopEntry.h"
#include "XDGUtils.h"
#include "XDGMenu.h"

XDGDesktopEntry::XDGEntryDescription XDGDesktopEntry::registeredEntries[] = {
    // Note: "Type" is special and treated in a different way

    // name,           valueType,       reqrd, type,        dpctd, valdValFnc
    //{ "Type",         XDGString,       true,  All,         false, NULL },
    { "Version",        XDGString,       false, All,         false, NULL },
    { "Name",           XDGLocalestring, true,  All,         false, NULL },
    { "GenericName",    XDGLocalestring, false, All,         false, NULL },
    { "NoDisplay",      XDGBoolean,      false, All,         false, NULL },
    { "Comment",        XDGLocalestring, false, All,         false, NULL },
    { "Icon",           XDGLocalestring, false, All,         false, NULL },
    { "Hidden",         XDGBoolean,      false, All,         false, NULL },
    { "OnlyShowIn",     XDGStrings,      false, All,         false,
	XDGMenu::validEnvironments },
    { "NotShowIn",      XDGStrings,      false, All,         false,
	XDGMenu::validEnvironments },
    { "TryExec",        XDGString,       false, Application, false, NULL },
    { "Exec",           XDGString,       true,  Application, false,
	validExec },
    { "Path",           XDGString,       false, Application, false, NULL },
    { "Terminal",       XDGBoolean,      false, Application, false, NULL },
    { "MimeType",       XDGStrings,      false, Application, false, NULL },
    { "Categories",     XDGStrings,      false, Application, false,
	XDGMenu::validCategories },
    { "StartupNotify",  XDGBoolean,      false, Application, false, NULL },
    { "StartupWMClass", XDGString,       false, Application, false, NULL },
    { "URL",            XDGString,       true,  Link,        false, NULL },

    // Old KDE stuff:
    // XXX: their valueType is not specified in the spec!
    //      I'm not sure they're actually considered deprecated...
    { "ServiceTypes",      XDGLocalestring, false, All,         false, NULL },
    { "DocPath",           XDGLocalestring, false, All,         false, NULL },
    { "Keywords",          XDGLocalestring, false, All,         false, NULL },
    { "InitialPreference", XDGLocalestring, false, All,         false, NULL },
    { "Dev",               XDGString,       false, FSDevice,    false, NULL },
    { "FSType",            XDGString,       false, FSDevice,    false, NULL },
    { "MountPoint",        XDGString,       false, FSDevice,    false, NULL },
    { "ReadOnly",          XDGBoolean,      false, FSDevice,    false, NULL },
    { "UnmountIcon",       XDGLocalestring, false, FSDevice,    false, NULL },

    // Really deprecated items
    // valueType not specified...
    { "Patterns",        XDGLocalestring, false, MimeType,    true,  NULL },
    { "DefaultApp",      XDGLocalestring, false, MimeType,    true,  NULL },
    { "Encoding",        XDGLocalestring, false, All,         true,  NULL },
    { "MiniIcon",        XDGLocalestring, false, All,         true,  NULL },
    { "TerminalOptions", XDGLocalestring, false, All,         true,  NULL },
    { "Protocols",       XDGLocalestring, false, All,         true,  NULL },
    { "Extensions",      XDGLocalestring, false, All,         true,  NULL },
    { "BinaryPattern",   XDGLocalestring, false, All,         true,  NULL },
    { "MapNotify",       XDGLocalestring, false, All,         true,  NULL },
    { "SwallowTitle",    XDGLocalestring, false, All,         true,  NULL },
    { "SwallowExec",     XDGString,       false, All,         true,  NULL },
    { "SortOrder",       XDGStrings,      false, All,         true,  NULL },
    { "FilePattern",     XDGLocalestring, false, All,         true,  NULL },


    // End of list
    { NULL,             XDGStrings,      false, All,         false, NULL },
    //{ "",    XDGLocalestring, false, All, false, NULL },

    // XXX: Keys that have a specific set of values (so we need to implement
    //      validValueFunc):
    //      - Encoding (deprecated)
    // XXX: FilePattern actually had its own "regex" type
    // XXX: We're only 1.0-compliant, so why bother registering deprecated stuff
    //      here? Maybe we should just fail?
};


XDGDesktopEntry::XDGDesktopEntry(const char *fileName)
    : valid_(false),
      fileName_(NULL),
      fileContent_(NULL),
      entries_(NULL)
{
    struct stat statBuffer;
    int fd;

    fileName_ = strdup(fileName);
    assert(fileName_);

    if (stat(fileName_, &statBuffer) != 0) {
	std::cerr << "Error: can't stat " << fileName_ << "\n";
	return;
    }

    // We're going to slurp the whole file, add a few '\0's and point structs to
    // it.
    fileContent_ = (char *)malloc(statBuffer.st_size+1);
    assert(fileContent_);
    fileContent_[statBuffer.st_size] = '\0';

    fd = open(fileName_, O_RDONLY);
    if (fd == -1) {
	std::cerr << "Error: can't open " << fileName_ << "\n";
	return;
    }

    ssize_t ret, readed = 0;
    while (readed < statBuffer.st_size) {
	ret = read(fd, fileContent_, statBuffer.st_size - readed);
	if (ret == -1) {
	    std::cerr << "Error: can't read " << fileName_ << "\n";
	    return;
	}
	readed += ret;
    }

    if (close(fd) == -1) {
	std::cerr << "Error: can't close " << fileName_ << "\n";
	return;
    }

    XDGUtils::log(XDGUtils::Debug, "File:\n%s\n", fileContent_);

    char *line;
    int lineNumber = 1;
    bool desktopEntryGroupFound = false;
    for(int i = 0; fileContent_[i] != '\0'; i++, lineNumber++) {
	line = &fileContent_[i];

	if (line[0] == '\n') {
	    continue;
	}

	if (line[0] == '#') {
	    for(; fileContent_[i] != '\n'; i++)
		;
	    continue;
	}

	if (line[0] == '[') {
	    if (desktopEntryGroupFound) {
		// We don't care about reading other groups for now.
		// Later we might parse them and check for correct syntax
		break;
	    } else {
		if (strncmp(line, "[Desktop Entry]\n", 16) == 0) {
		    desktopEntryGroupFound = true;
		    for(; fileContent_[i] != '\n'; i++)
			;
		    continue;
		} else {
		    std::cerr << fileName_ << " error: first group must be "
			      << "[Desktop Entry]\n";
		    return;
		}
	    }
	}

	// processEntry will mess with the line, so we better advance now
	for(; fileContent_[i] != '\n'; i++)
	    ;
	if (!processEntry(line)) {
	    std::cerr << fileName_ << " error: invalid entry at line "
		      << lineNumber << "\n";
	    return;
	}
    }

    validateEntries();
}

XDGDesktopEntry::~XDGDesktopEntry()
{
    free(fileName_);
    free(fileContent_);
    struct XDGEntry *pEntry = entries_;
    struct XDGEntry *prev;

    while (pEntry != NULL) {
	prev = pEntry;
	pEntry = pEntry->next;
	free(prev);
    }
}

bool XDGDesktopEntry::processEntry(char *line)
{
    unsigned int i, keyEnd = 0, valueBegin = 0;
    unsigned int localeBegin = 0, localeEnd = 0;
    char lastChar;

    struct XDGEntry *entry = (struct XDGEntry*)malloc(sizeof(struct XDGEntry));
    assert(entry);

    // Look for the key
    for(i = 0; line[i] != '\n' && line[i] != '\0'; i++) {
	if (!isalnum(line[i]) && line[i] != '-') {
	    keyEnd = i;
	    break;
	}
    }

    if (keyEnd == 0) {
	free(entry);
	return false;
    }

    lastChar = line[keyEnd];
    line[keyEnd] = '\0';
    entry->key = &line[0];
    XDGUtils::log(XDGUtils::Debug, "key[%s]\n", entry->key);
    i++;

    // Check for localized entries
    if (lastChar == '[') {
	localeBegin = i;
	for(; line[i] != '\n' && line[i] != '\0'; i++) {
	    if (line[i] == ']') {
		localeEnd = i;
		i++;
		break;
	    }
	}
	if (localeEnd != 0) {
	    lastChar = line[localeEnd];
	    line[localeEnd] = '\0';
	    entry->locale = &line[localeBegin];
	} else {
	    free(entry);
	    return false;
	}
    } else {
	entry->locale = NULL;
    }

    if (entry->locale)
	XDGUtils::log(XDGUtils::Debug, "locale[%s]\n", entry->locale);
    else
	XDGUtils::log(XDGUtils::Debug, "locale[NULL]\n");

    // Now eat spaces until the '=' char
    if (lastChar != '=') {
	for(; line[i] != '\n' && line[i] != '\0'; i++) {
	    if (!isspace(line[i]))
		break;
	}
	if (line[i] != '=') {
	    std::cerr << fileName_ << " error: '=' expected, but found '"
		      << line[i] << "'\n";
	    free(entry);
	    return false;
	} else {
	    i++;
	}
    }

    // Now eat spaces until the value starts
    for(; line[i] != '\n' && line[i] != '\0'; i++) {
	if (!isspace(line[i])) {
	    valueBegin = i;
	    break;
	}
    }

    if (line[i] == '\n' || line[i] == '\0' ||
	line[valueBegin] == '\n' || line[valueBegin] == '\0') {
	// Although the spec doesn't mention, there are desktop files with
	// empty values
	entry->value = NULL;
    } else {
	entry->value = &line[valueBegin];
	for(; line[i] != '\n' && line[i] != '\0'; i++)
	    ;
	line[i] = '\0';
    }

    if (entry->value)
	XDGUtils::log(XDGUtils::Debug, "value[%s]\n", entry->value);
    else
	XDGUtils::log(XDGUtils::Debug, "value[NULL]\n");

    return storeEntry(entry);
}

bool XDGDesktopEntry::storeEntry(struct XDGEntry *entry)
{
    struct XDGEntry *pEntry;

    entry->next = NULL;

    if (entries_ == NULL) {
	entries_ = entry;
	return true;
    }
    pEntry = entries_;
    while (pEntry->next != NULL) {
	if (strcmp(entry->key, pEntry->key) == 0) {
	    if (entry->locale == NULL && pEntry->locale == NULL) {
		std::cerr << fileName_ << " error: key " << entry->key
			  << "already stored!\n";
		return false;
	    }

	    if (!(entry->locale == NULL || pEntry->locale == NULL)) {
		if (strcmp(entry->locale, pEntry->locale) == 0) {
		    std::cerr << fileName_ << " error: key " << entry->key
			      << "[" << entry->locale  << "] already stored!\n";
		    return false;
		}
	    }

	}
	pEntry = pEntry->next;
    }

    pEntry->next = entry;
    return true;
}

bool XDGDesktopEntry::getEntry(const char *key, const char *locale,
			       const char **value)
{
    struct XDGEntry *pEntry;

    pEntry = entries_;
    while (pEntry != NULL) {
	if (strcmp(pEntry->key, key) == 0) {
	    if ((pEntry->locale == NULL && locale == NULL) ||
		(strcmp(pEntry->locale, locale) == 0)) {
		*value = pEntry->value;
		return true;
	    }
	}
	pEntry = pEntry->next;
    }
    return false;
}

void XDGDesktopEntry::validateEntries()
{
    int i;
    char *key = NULL;
    char *locale;
    const char *value;
    XDGTypeMask type;

    valid_ = false;

    if (!getEntry("Type", NULL, &value)) {
	std::cerr << fileName_ << " error: \"Type\" key not found!\n";
	return;
    } else {
	if (strcmp(value, "Application") == 0)
	    type = Application;
	else if (strcmp(value, "Link") == 0)
	    type = Link;
	else if (strcmp(value, "Directory") == 0)
	    type = Directory;
	else if (strcmp(value, "ServiceType") == 0)
	    type = ServiceType;
	else if (strcmp(value, "Service") == 0)
	    type = Service;
	else if (strcmp(value, "FSDevice") == 0)
	    type = FSDevice;
	else if (strcmp(value, "MimeType") == 0) {
	    type = MimeType;
	    std::cerr << fileName_
		      << " error: type \"MimeType\" is deprecated!\n";
	} else {
	    std::cerr << fileName_ << " error: invalid type!\n";
	    return;
	}
    }

    // For each key we have, compare against the registered ones
    struct XDGEntry *pEntry;
    for(pEntry = entries_; pEntry != NULL; pEntry = pEntry->next) {
	key = pEntry->key;
	locale = pEntry->locale;
	value = pEntry->value;

	if ((key[0] == 'X' && key[1] == '-') || (strcmp(key, "Type") == 0))
	    continue;

	bool found = false;
	for(i = 0; registeredEntries[i].name != NULL; i++) {
	    if (strcmp(registeredEntries[i].name, key) != 0)
		continue;

	    found = true;

	    if (!(registeredEntries[i].type & type)) {
		std::cerr << fileName_ << " error: key " << key
			  << " invalid for this type\n";
		return;
	    }

	    if (locale != NULL &&
		registeredEntries[i].valueType != XDGLocalestring) {
		std::cerr << fileName_ << " error: only localestring can "
			  << "have localized values\n";
		return;
	    }

	    if (!checkValueType(value, registeredEntries[i].valueType)) {
		std::cerr << fileName_ << " error: invalid value "
			  << value << " for type\n";
		return;
	    }

	    if (registeredEntries[i].deprecated)
		std::cerr << fileName_ << " warning: key " << key
			  << " is deprecated\n";

	    if (registeredEntries[i].validValueFunc != NULL) {
		if (!(registeredEntries[i].validValueFunc)(value)) {
		    std::cerr << fileName_ << " error: invalid value " << value
			      << "\n";
		    return;
		}
	    }

	    break;
	}
	if (!found) {
	    std::cerr << fileName_ << " error: key " << key
		      << " is not registered\n";
	    return;
	}

    }

    // Now find missing required keys
    for(i = 0; registeredEntries[i].name != NULL; i++) {
	if (!registeredEntries[i].type & type)
	    continue;

	if (registeredEntries[i].required) {
	    if (!getEntry(key, NULL, &value)) {
		std::cerr << fileName_ << " error: key " << key
			  << " entry required but not found\n";
		return;
	    }
	}
    }


    if (getEntry("OnlyShowIn", NULL, &value) &&
	getEntry("NotShowIn", NULL, &value)) {
	std::cerr << fileName_ << " error: only one of OnlyShowIn or "
		  << "NotShowIn is allowed\n";
	return;
    }

    // XXX: this way, we only check for the "defined" entries. Anything else is
    //      just ignored! Shouldn't we fail for unknown entries?


    valid_ = true;
}

bool XDGDesktopEntry::checkValueType(const char *value, XDGEntryValueType type)
{
    unsigned int i;

    // Allow empty values for now
    if (!value)
	return true;

    if (type == XDGString) {
	// "Values of type string may contain all ASCII characters except for
	// control characters."
	for(i = 0; value[i] != '\0'; i++)
	    if (!isascii(value[i]) || iscntrl(value[i]))
		return false;
	return true;

    } else if (type == XDGStrings) {
	char *s, *tokPtr, *tokStr;
	if (!XDGUtils::validSemicolonList(value))
	    return false;

	s = strdup(value);
	ITERATE_ESCAPED_STRTOK_R(s, ";", &tokPtr, tokStr) {
	    if (!checkValueType(tokStr, XDGString)) {
		free(s);
		return false;
	    }
	}
	free(s);
	return true;

    } else if (type == XDGLocalestring) {
	// "Values of type localestring are user displayable, and are encoded in
	// UTF-8."
	// XXX: implement! check for displayability and utf-8!
	return true;

    } else if (type == XDGBoolean) {
	// "Values of type boolean must either be the string true or false."
	if (strcmp(value, "0") == 0 || strcmp(value, "1") == 0) {
	    std::cerr << fileName_ << " warning: boolean values \"0\" and "
		      << "\"1\" are deprecated!\n";
	    return true;
	}
	if ((strcmp(value, "true") != 0) && (strcmp(value, "false") != 0))
	    return false;
	else
	    return true;

    } else if (type == XDGNumeric) {
	// "Values of type numeric must be a valid floating point number as
	// recognized by the %f specifier for scanf in the C locale."
	float tmp;
	if (sscanf(value, "%f", &tmp) != 1)
	    return false;
	else
	    return true;

    } else {
	std::cerr << fileName_ << " error: invalid XDGEntryType\n";
	return false;
    }
}

bool XDGDesktopEntry::valid()
{
    return valid_;
}

bool XDGDesktopEntry::showInEnvironment(const char *environment)
{
    if (!valid_) {
	XDGUtils::log(XDGUtils::Verbose,
		      "%s error: invalid desktop file\n", fileName_);
	return false;
    }

    const char *value;
    char *s, *tokPtr, *tokStr;
    if (getEntry("OnlyShowIn", NULL, &value)) {
	s = strdup(value);
	ITERATE_ESCAPED_STRTOK_R(s, ";", &tokPtr, tokStr) {
	    if (strcmp(tokStr, environment) == 0) {
		free(s);
		return true;
	    }
	}
	XDGUtils::log(XDGUtils::Verbose, "%s not in OnlyShowIn list\n",
		      fileName_);
	free(s);
	return false;
    } else if (getEntry("NotShowIn", NULL, &value)) {
	s = strdup(value);
	ITERATE_ESCAPED_STRTOK_R(s, ";", &tokPtr, tokStr) {
	    if (strcmp(tokStr, environment) == 0) {
		XDGUtils::log(XDGUtils::Verbose,
			      "%s present in NotShowIn list\n", fileName_);
		free(s);
		return false;

	    }
	}
	free(s);
	return true;
    }

    return true;
}

int XDGDesktopEntry::run(const char *args)
{
    const char *value;

    if (!valid_)
	return 0;

    XDGUtils::log(XDGUtils::Verbose, "%s: --> Running entry!\n", fileName_);
    std::string executablePath;

    // Test TryExec:
    if (getEntry("TryExec", NULL, &value)) {
	XDGUtils::log(XDGUtils::Verbose, "%s: TryExec: %s\n", fileName_, value);

	if (value[0] == '/' || value[0] == '.') {
	    if (!XDGUtils::fileExists(value)) {
		XDGUtils::log(XDGUtils::Verbose,
			      "%s error: TryExec failed: file %s not found\n",
			      fileName_, value);
		return 0;
	    }
	    executablePath = value;
	} else {
	    if (!XDGUtils::findFileInPath(value, executablePath)) {
		XDGUtils::log(XDGUtils::Verbose,
			      "%s error: TryExec failed: file %s not found\n",
			      fileName_, value);
		return 0;
	    }
	}
	if (access(executablePath.c_str(), X_OK) == -1) {
	    XDGUtils::log(XDGUtils::Verbose,
			  "%s TryExec failed: file %s not executable\n",
			  fileName_, value);
	    return 0;
	}
	XDGUtils::log(XDGUtils::Verbose, "%s: -- TryExec success\n",
		      fileName_);
    }

    // Now run the desktop file
    getEntry("Exec", NULL, &value);

    // XXX: For now, just remove the field codes...
    std::string commandString;
    for(unsigned int i = 0; value[i] != '\0'; i++) {
	if (value[i] == '%') {
	    i++;
	    if (value[i] == 'i') {
		// "The Icon key of the desktop entry expanded as two arguments,
		// first --icon and then the value of the Icon key. Should not
		// expand to any arguments if the Icon key is empty or missing."
		const char *iconValue;
		if (getEntry("Icon", NULL, &iconValue)) {
		    commandString += std::string("-- icon ") + iconValue;
		}
	    } else if (value[i] == 'c') {
		// "The translated name of the application as listed in the
		// appropriate Name key in the desktop entry."
		// XXX: implement
	    } else if (value[i] == 'k') {
		// "The location of the desktop file as either a URI (if for
		// example gotten from the vfolder system) or a local filename
		// or empty if no location is known."
		// XXX: implement
	    } else {
		continue;
	    }
	}
	commandString += value[i];
    }

    commandString += std::string(" ") + args;

    return runCommand(commandString.c_str());
}

int XDGDesktopEntry::runCommand(const char *commandString)
{
    // XXX: use alloca instead of new?
    unsigned int i, buf_i, j;
    bool lookingForQuote = false;
    pid_t pid;

    // "buffer" contains consecutive strings separated by '\0's
    char *buffer = (char *) alloca(strlen(commandString) * sizeof(char));
    char *arg = &buffer[0];
    unsigned int arguments = 0;

    for(i = 0, buf_i = 0; commandString[i] != '\0'; i++) {
	if (commandString[i] == '\\') {
	    i++;
	} else if (commandString[i] == ' ' && (!lookingForQuote)) {
	    if (!(arg == &buffer[buf_i])) {
		// Not two consecutive spaces
		buffer[buf_i] = '\0';
		arguments++;
		buf_i++;
		arg = &buffer[buf_i];
	    }
	} else if (commandString[i] == '"') {
	    lookingForQuote = !lookingForQuote;
	} else {
	    buffer[buf_i] = commandString[i];
	    buf_i++;
	}
    }
    if (arg != &buffer[buf_i]) {
	buffer[buf_i] = '\0';
	arguments++;
    }

    if (lookingForQuote) {
        std::cerr << "Error: missing quotes\n";
        return 0;
    }

    if (arguments == 0) {
        std::cerr << "Error: empty command\n";
        return 0;
    }

    char **c_args = (char **) alloca(sizeof(char *) * arguments+1);
    char *c_command;

    c_command = buffer;
    for(i = 0, j = 0; i < arguments; i++) {
	c_args[i] = &buffer[j];
	// Skip to the next argument:
	for(; buffer[j] != '\0'; j++)
	    ;
	j++;
    }
    c_args[i] = NULL;

    pid = fork();
    if (pid == 0) {
        execvp(c_command, c_args);
        exit(0);
    }

    return pid;
}


bool XDGDesktopEntry::validExec(const char *execString)
{
    bool exclusiveFieldFound = false;
    for(unsigned int i = 0; execString[i] != '\0'; i++) {
	if (execString[i] == '%') {
	    i++;
	    if (execString[i] == 'f' || execString[i] == 'F' ||
		execString[i] == 'u' || execString[i] == 'U') {
		if (exclusiveFieldFound) {
		    std::cerr << "Error: Exec fields %f, %F, %u "
			         "or %U are mutually exclusive\n";
		    return false;
		} else {
		    exclusiveFieldFound = true;
		}
	    } else if (execString[i] == 'd' || execString[i] == 'D' ||
		       execString[i] == 'n' || execString[i] == 'N' ||
		       execString[i] == 'v' || execString[i] == 'm' ) {
		std::cerr << "Warning: deprecated Exec field code %"
			  << execString[i] << "\n";
	    } else if (execString[i] != 'i' && execString[i] != 'c' &&
		       execString[i] != 'k') {
		std::cerr << "Error: invalid Exec field %"
			  << execString[i] << "\n";
		return false;
	    }
	}
    }
    return true;
}
