/* BEGIN_COMMON_COPYRIGHT_HEADER
 * (c)LGPL2+
 *
 * Razor - a lightweight, Qt based, desktop toolset
 * http://razor-qt.org
 *
 * Copyright: 2010-2011 Razor team
 * Authors:
 *   Alexander Sokoloff <sokoloff.a@gmail.com>
 *
 * This program or library is free software; you can redistribute it
 * and/or modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301 USA
 *
 * END_COMMON_COPYRIGHT_HEADER */

#include "desktopenvironment_p.cpp"
#include "xdgdesktopfile.h"
#include "xdgdesktopfile_p.h"
#include "xdgdirs.h"
#include "xdgicon.h"

#include <stdlib.h>
#include <unistd.h>

#include <QDebug>
#include <QDBusInterface>
#include <QDesktopServices>
#include <QDir>
#include <QSharedData>
#include <QFile>
#include <QFileInfo>
#include <QHash>
#include <QList>
#include <QMimeDatabase>
#include <QMimeType>
#include <QProcess>
#include <QSettings>
#include <QStandardPaths>
#include <QStringBuilder> // for the % operator
#include <QTextStream>
#include <QUrl>
#include <QtAlgorithms>


/**
 *  See: http://standards.freedesktop.org/desktop-entry-spec
 */

// A list of executables that can't be run with QProcess::startDetached(). They
// will be run with QProcess::start()
static const QStringList nonDetachExecs = QStringList()
    << QLatin1String("pkexec");

static const char onlyShowInKey[] = "OnlyShowIn";
static const char notShowInKey[] = "NotShowIn";
static const char categoriesKey[] = "Categories";
static const char extendPrefixKey[] = "X-";

// Helper functions prototypes
bool checkTryExec(const QString& progName);
QString &doEscape(QString& str, const QHash<QChar,QChar> &repl);
QString &doUnEscape(QString& str, const QHash<QChar,QChar> &repl);
QString &escape(QString& str);
QString &escapeExec(QString& str);
QString expandDynamicUrl(QString url);
QString expandEnvVariables(const QString str);
QStringList expandEnvVariables(const QStringList strs);
QString findDesktopFile(const QString& dirName, const QString& desktopName);
QString findDesktopFile(const QString& desktopName);
static QStringList parseCombinedArgString(const QString &program);
bool read(const QString &prefix);
void replaceVar(QString &str, const QString &varName, const QString &after);
QString &unEscape(QString& str);
QString &unEscapeExec(QString& str);
void loadMimeCacheDir(const QString& dirName, QHash<QString, QList<XdgDesktopFile*> > & cache);


QString &doEscape(QString& str, const QHash<QChar,QChar> &repl)
{
    // First we replace slash.
    str.replace('\\', "\\\\");

    QHashIterator<QChar,QChar> i(repl);
    while (i.hasNext()) {
        i.next();
        if (i.key() != '\\')
            str.replace(i.key(), QString("\\\\%1").arg(i.value()));
    }

    return str;
}

/************************************************
 The escape sequences \s, \n, \t, \r, and \\ are supported for values
 of type string and localestring, meaning ASCII space, newline, tab,
 carriage return, and backslash, respectively.
 ************************************************/
QString &escape(QString& str)
{
    QHash<QChar,QChar> repl;
    repl.insert('\n',  'n');
    repl.insert('\t',  't');
    repl.insert('\r',  'r');

    return doEscape(str, repl);
}


/************************************************
 Quoting must be done by enclosing the argument between double quotes and
 escaping the
    double quote character,
    backtick character ("`"),
    dollar sign ("$") and
    backslash character ("\")
by preceding it with an additional backslash character.
Implementations must undo quoting before expanding field codes and before
passing the argument to the executable program.

Note that the general escape rule for values of type string states that the
backslash character can be escaped as ("\\") as well and that this escape
rule is applied before the quoting rule. As such, to unambiguously represent a
literal backslash character in a quoted argument in a desktop entry file
requires the use of four successive backslash characters ("\\\\").
Likewise, a literal dollar sign in a quoted argument in a desktop entry file
is unambiguously represented with ("\\$").
 ************************************************/
QString &escapeExec(QString& str)
{
    QHash<QChar,QChar> repl;
    // The parseCombinedArgString() splits the string by the space symbols,
    // we temporarily replace them on the special characters.
    // Replacement will reverse after the splitting.
    repl.insert('"', '"');    // double quote,
    repl.insert('\'', '\'');  // single quote ("'"),
    repl.insert('\\', '\\');  // backslash character ("\"),
    repl.insert('$', '$');    // dollar sign ("$"),

    return doEscape(str, repl);
}


QString &doUnEscape(QString& str, const QHash<QChar,QChar> &repl)
{
    int n = 0;
    while (1)
    {
        n=str.indexOf("\\", n);
        if (n < 0 || n > str.length() - 2)
            break;

        if (repl.contains(str.at(n+1)))
        {
            str.replace(n, 2, repl.value(str.at(n+1)));
        }

        n++;
    }

    return str;
}


/************************************************
 The escape sequences \s, \n, \t, \r, and \\ are supported for values
 of type string and localestring, meaning ASCII space, newline, tab,
 carriage return, and backslash, respectively.
 ************************************************/
QString &unEscape(QString& str)
{
    QHash<QChar,QChar> repl;
    repl.insert('\\', '\\');
    repl.insert('s',  ' ');
    repl.insert('n',  '\n');
    repl.insert('t',  '\t');
    repl.insert('r',  '\r');

    return doUnEscape(str, repl);
}


/************************************************
 Quoting must be done by enclosing the argument between double quotes and
 escaping the
    double quote character,
    backtick character ("`"),
    dollar sign ("$") and
    backslash character ("\")
by preceding it with an additional backslash character.
Implementations must undo quoting before expanding field codes and before
passing the argument to the executable program.

Reserved characters are
    space (" "),
    tab,
    newline,
    double quote,
    single quote ("'"),
    backslash character ("\"),
    greater-than sign (">"),
    less-than sign ("<"),
    tilde ("~"),
    vertical bar ("|"),
    ampersand ("&"),
    semicolon (";"),
    dollar sign ("$"),
    asterisk ("*"),
    question mark ("?"),
    hash mark ("#"),
    parenthesis ("(") and (")")
    backtick character ("`").

Note that the general escape rule for values of type string states that the
backslash character can be escaped as ("\\") as well and that this escape
rule is applied before the quoting rule. As such, to unambiguously represent a
literal backslash character in a quoted argument in a desktop entry file
requires the use of four successive backslash characters ("\\\\").
Likewise, a literal dollar sign in a quoted argument in a desktop entry file
is unambiguously represented with ("\\$").
 ************************************************/
QString &unEscapeExec(QString& str)
{
    unEscape(str);
    QHash<QChar,QChar> repl;
    // The parseCombinedArgString() splits the string by the space symbols,
    // we temporarily replace them on the special characters.
    // Replacement will reverse after the splitting.
    repl.insert(' ',  01);    // space
    repl.insert('\t', 02);    // tab
    repl.insert('\n', 03);    // newline,

    repl.insert('"', '"');    // double quote,
    repl.insert('\'', '\'');  // single quote ("'"),
    repl.insert('\\', '\\');  // backslash character ("\"),
    repl.insert('>', '>');    // greater-than sign (">"),
    repl.insert('<', '<');    // less-than sign ("<"),
    repl.insert('~', '~');    // tilde ("~"),
    repl.insert('|', '|');    // vertical bar ("|"),
    repl.insert('&', '&');    // ampersand ("&"),
    repl.insert(';', ';');    // semicolon (";"),
    repl.insert('$', '$');    // dollar sign ("$"),
    repl.insert('*', '*');    // asterisk ("*"),
    repl.insert('?', '?');    // question mark ("?"),
    repl.insert('#', '#');    // hash mark ("#"),
    repl.insert('(', '(');    // parenthesis ("(")
    repl.insert(')', ')');    // parenthesis (")")
    repl.insert('`', '`');    // backtick character ("`").

    return doUnEscape(str, repl);
}

class XdgDesktopFileData: public QSharedData {
public:
    XdgDesktopFileData();
    bool read(const QString &prefix);
    XdgDesktopFile::Type detectType(XdgDesktopFile *q) const;
    bool startApplicationDetached(const XdgDesktopFile *q, const QStringList& urls) const;
    bool startLinkDetached(const XdgDesktopFile *q) const;
    bool startByDBus(const QStringList& urls) const;

    QString mFileName;
    bool mIsValid;
    mutable bool mValidIsChecked;
    mutable QHash<QString, bool> mIsShow;
    QMap<QString, QVariant> mItems;

    XdgDesktopFile::Type mType;
};


XdgDesktopFileData::XdgDesktopFileData():
    mIsValid(false),
    mValidIsChecked(false)
{
}


bool XdgDesktopFileData::read(const QString &prefix)
{
    QFile file(mFileName);

    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
        return false;

    QString section;
    QTextStream stream(&file);
    bool prefixExists = false;
    while (!stream.atEnd()) {
        QString line = stream.readLine().trimmed();

        // Skip comments ......................
        if (line.startsWith('#'))
            continue;


        // Section ..............................
        if (line.startsWith('[') && line.endsWith(']'))
        {
            section = line.mid(1, line.length()-2);
            if (section == prefix)
                prefixExists = true;

            continue;
        }

        QString key = line.section('=', 0, 0).trimmed();
        QString value = line.section('=', 1).trimmed();

        if (key.isEmpty())
            continue;

        mItems[section + "/" + key] = QVariant(value);
    }


    // Not check for empty prefix
    mIsValid = (prefix.isEmpty()) || prefixExists;
    return mIsValid;
}

XdgDesktopFile::Type XdgDesktopFileData::detectType(XdgDesktopFile *q) const
{
    QString typeStr = q->value("Type").toString();
    if (typeStr == "Application")
        return XdgDesktopFile::ApplicationType;

    if (typeStr == "Link")
        return XdgDesktopFile::LinkType;

    if (typeStr == "Directory")
        return XdgDesktopFile::DirectoryType;

    if (!q->value("Exec").toString().isEmpty())
        return XdgDesktopFile::ApplicationType;

    return XdgDesktopFile::UnknownType;
}


bool XdgDesktopFileData::startApplicationDetached(const XdgDesktopFile *q, const QStringList& urls) const
{
    //DBusActivatable handling
    if (q->value(QLatin1String("DBusActivatable"), false).toBool())
        return startByDBus(urls);

    QStringList args = q->expandExecString(urls);

    if (args.isEmpty())
        return false;

    if (q->value("Terminal").toBool())
    {
        QString term = getenv("TERM");
        if (term.isEmpty())
            term = "xterm";

        args.prepend("-e");
        args.prepend(term);
    }

    bool nonDetach = false;
    foreach(const QString &s, nonDetachExecs)
    {
        foreach(const QString &a, args)
        {
            if (a.contains(s))
            {
                nonDetach = true;
            }
        }
    }

    QString cmd = args.takeFirst();

    if (nonDetach)
    {
        QScopedPointer<QProcess> p(new QProcess);
        p->setStandardInputFile(QProcess::nullDevice());
        p->setProcessChannelMode(QProcess::ForwardedChannels);
        p->start(cmd, args);
        bool started = p->waitForStarted();
        if (started)
        {
            QProcess* proc = p.take(); //release the pointer(will be selfdestroyed upon finish)
            QObject::connect(proc, SIGNAL(finished(int, QProcess::ExitStatus)), proc, SLOT(deleteLater()));
        }
        return started;
    }
    else
    {
        return QProcess::startDetached(cmd, args);
    }
}


bool XdgDesktopFileData::startLinkDetached(const XdgDesktopFile *q) const
{
    QString url = q->url();

    if (url.isEmpty())
    {
        qWarning() << "XdgDesktopFileData::startLinkDetached: url is empty.";
        return false;
    }

    QString scheme = QUrl(url).scheme();

    if (scheme.isEmpty() || scheme.toUpper() == "FILE")
    {
        // Local file
        QFileInfo fi(url);

        QMimeDatabase db;
        QMimeType mimeInfo = db.mimeTypeForFile(fi);
        XdgDesktopFile* desktopFile = XdgDesktopFileCache::getDefaultApp(mimeInfo.name());

        if (desktopFile)
            return desktopFile->startDetached(url);
    }
    else
    {
        // Internet URL
        return QDesktopServices::openUrl(QUrl::fromEncoded(url.toLocal8Bit()));
    }

    return false;
}

// TODO: Handle ActivateAction
bool XdgDesktopFileData::startByDBus(const QStringList& urls) const
{
    QFileInfo f(mFileName);
    QString path(f.completeBaseName());

    QVariantMap platformData;
    platformData.insert(QLatin1String("desktop-startup-id"), QString::fromUtf8(qgetenv("DESKTOP_STARTUP_ID")));

    path = path.replace(QLatin1Char('.'), QLatin1Char('/')).prepend(QLatin1Char('/'));
    QDBusInterface app(f.completeBaseName(), path, QLatin1String("org.freedesktop.Application"));
    QDBusMessage reply;
    if (urls.isEmpty())
        reply = app.call(QLatin1String("Activate"), platformData);
    else
        reply = app.call(QLatin1String("Open"), urls, platformData);

    return QDBusMessage::ErrorMessage != reply.type();
}


XdgDesktopFile::XdgDesktopFile():
    d(new XdgDesktopFileData)
{
}


XdgDesktopFile::XdgDesktopFile(const XdgDesktopFile& other):
    d(other.d)
{
}


XdgDesktopFile::XdgDesktopFile(Type type, const QString& name, const QString &value):
    d(new XdgDesktopFileData)
{
    d->mFileName = name + ".desktop";
    d->mType = type;
    setValue("Version", "1.0");
    setValue("Name", name);
    if (type == XdgDesktopFile::ApplicationType)
    {
        setValue("Type", "Application");
        setValue("Exec", value);
    }
    else if (type == XdgDesktopFile::LinkType)
    {
        setValue("Type", "Link");
        setValue("URL", value);
    }
    else if (type == XdgDesktopFile::DirectoryType)
    {
        setValue("Type", "Directory");
    }
    d->mIsValid = check();
}


XdgDesktopFile::~XdgDesktopFile()
{
}


XdgDesktopFile& XdgDesktopFile::operator=(const XdgDesktopFile& other)
{
    d = other.d;
    return *this;
}


bool XdgDesktopFile::operator==(const XdgDesktopFile &other) const
{
    return d->mItems == other.d->mItems;
}


bool XdgDesktopFile::load(const QString& fileName)
{
    if (fileName.startsWith(QDir::separator())) { // absolute path
        QFileInfo f(fileName);
        if (f.exists())
            d->mFileName = f.canonicalFilePath();
        else
            d->mFileName = QString();
    } else { // relative path
        d->mFileName = findDesktopFile(fileName);
    }
    d->read(prefix());
    d->mIsValid = d->mIsValid && check();
    d->mType = d->detectType(this);
    return isValid();
}


bool XdgDesktopFile::save(QIODevice *device) const
{
    QTextStream stream(device);
    QMap<QString, QVariant>::const_iterator i = d->mItems.constBegin();

    QString section;
    while (i != d->mItems.constEnd())
    {
        QString path = i.key();
        QString sect =  path.section('/',0,0);
        if (sect != section)
        {
            section = sect;
            stream << "[" << section << "]" << endl;
        }
        QString key = path.section('/', 1);
        stream << key << "=" << i.value().toString() << endl;
        ++i;
    }
    return true;
}


bool XdgDesktopFile::save(const QString &fileName) const
{
    QFile file(fileName);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
        return false;

    return save(&file);
}


QVariant XdgDesktopFile::value(const QString& key, const QVariant& defaultValue) const
{
    QString path = (!prefix().isEmpty()) ? prefix() + "/" + key : key;
    QVariant res = d->mItems.value(path, defaultValue);
    if (res.type() == QVariant::String)
    {
        QString s = res.toString();
        return unEscape(s);
    }

    return res;
}


void XdgDesktopFile::setValue(const QString &key, const QVariant &value)
{
    QString path = (!prefix().isEmpty()) ? prefix() + "/" + key : key;
    if (value.type() == QVariant::String)
    {

        QString s=value.toString();
        if (key.toUpper() == "EXEC")
            escapeExec(s);
        else
            escape(s);

        d->mItems[path] = QVariant(s);

        if (key.toUpper() == "TYPE")
            d->mType = d->detectType(this);
    }
    else
    {
        d->mItems[path] = value;
    }
}


void XdgDesktopFile::setLocalizedValue(const QString &key, const QVariant &value)
{
    setValue(localizedKey(key), value);
}


/************************************************
 LC_MESSAGES value      Possible keys in order of matching
 lang_COUNTRY@MODIFIER  lang_COUNTRY@MODIFIER, lang_COUNTRY, lang@MODIFIER, lang,
                        default value
 lang_COUNTRY           lang_COUNTRY, lang, default value
 lang@MODIFIER          lang@MODIFIER, lang, default value
 lang                   lang, default value
 ************************************************/
QString XdgDesktopFile::localizedKey(const QString& key) const
{
    QString lang = getenv("LC_MESSAGES");

    if (lang.isEmpty())
        lang = getenv("LC_ALL");

    if (lang.isEmpty())
         lang = getenv("LANG");

    QString modifier = lang.section('@', 1);
    if (!modifier.isEmpty())
        lang.truncate(lang.length() - modifier.length() - 1);

    QString encoding = lang.section('.', 1);
    if (!encoding.isEmpty())
        lang.truncate(lang.length() - encoding.length() - 1);


    QString country = lang.section('_', 1);
    if (!country.isEmpty())
        lang.truncate(lang.length() - country.length() - 1);

    //qDebug() << "LC_MESSAGES: " << getenv("LC_MESSAGES");
    //qDebug() << "Lang:" << lang;
    //qDebug() << "Country:" << country;
    //qDebug() << "Encoding:" << encoding;
    //qDebug() << "Modifier:" << modifier;


    if (!modifier.isEmpty() && !country.isEmpty())
    {
        QString k = QString("%1[%2_%3@%4]").arg(key, lang, country, modifier);
        //qDebug() << "\t try " << k << contains(k);
        if (contains(k))
            return k;
    }

    if (!country.isEmpty())
    {
        QString k = QString("%1[%2_%3]").arg(key, lang, country);
        //qDebug() << "\t try " << k  << contains(k);
        if (contains(k))
            return k;
    }

    if (!modifier.isEmpty())
    {
        QString k = QString("%1[%2@%3]").arg(key, lang, modifier);
        //qDebug() << "\t try " << k  << contains(k);
        if (contains(k))
            return k;
    }

    QString k = QString("%1[%2]").arg(key, lang);
    //qDebug() << "\t try " << k  << contains(k);
    if (contains(k))
        return k;


    //qDebug() << "\t try " << key  << contains(key);
    return key;
}


QVariant XdgDesktopFile::localizedValue(const QString& key, const QVariant& defaultValue) const
{
    return value(localizedKey(key), defaultValue);
}


QStringList XdgDesktopFile::categories() const
{
    QString key;
    if (contains(QLatin1String(categoriesKey)))
    {
        key = QLatin1String(categoriesKey);
    }
    else
    {
        key = QLatin1String(extendPrefixKey) % QLatin1String(categoriesKey);
        if (!contains(key))
            return QStringList();
    }

    QStringList cats = value(key).toString().split(QLatin1Char(';'));
    return cats;
}


void XdgDesktopFile::removeEntry(const QString& key)
{
    QString path = (!prefix().isEmpty()) ? prefix() + "/" + key : key;
    d->mItems.remove(path);
}


bool XdgDesktopFile::contains(const QString& key) const
{
    QString path = (!prefix().isEmpty()) ? prefix() + "/" + key : key;
    return d->mItems.contains(path);
}


bool XdgDesktopFile::isValid() const
{
    return d->mIsValid;
}


QString XdgDesktopFile::fileName() const
{
    return d->mFileName;
}


QIcon const XdgDesktopFile::icon(const QIcon& fallback) const
{
    QIcon result = XdgIcon::fromTheme(value("Icon").toString(), fallback);

    if (result.isNull() && type() == ApplicationType) {
        result = XdgIcon::fromTheme("application-x-executable.png");
        // TODO Maybe defaults for other desktopfile types as well..
    }

    return result;
}


QString const XdgDesktopFile::iconName() const
{
    return value("Icon").toString();
}


XdgDesktopFile::Type XdgDesktopFile::type() const
{
    return d->mType;
}


/************************************************
 Starts the program defined in this desktop file in a new process, and detaches
 from it. Returns true on success; otherwise returns false. If the calling process
 exits, the detached process will continue to live.

 Urls - the list of URLs or files to open, can be empty (app launched without
  argument)
 If the function is successful then *pid is set to the process identifier of the
 started process.
 ************************************************/
bool XdgDesktopFile::startDetached(const QStringList& urls) const
{
    switch(d->mType)
    {
    case ApplicationType:
        return d->startApplicationDetached(this, urls);

    case LinkType:
        return d->startLinkDetached(this);

    default:
        return false;
    }
}


/************************************************
 This is an overloaded function.
 ************************************************/
bool XdgDesktopFile::startDetached(const QString& url) const
{
    if (url.isEmpty())
        return startDetached(QStringList());
    else
        return startDetached(QStringList(url));
}


static QStringList parseCombinedArgString(const QString &program)
{
    QStringList args;
    QString tmp;
    int quoteCount = 0;
    bool inQuote = false;

    // handle quoting. tokens can be surrounded by double quotes
    // "hello world". three consecutive double quotes represent
    // the quote character itself.
    for (int i = 0; i < program.size(); ++i) {
        if (program.at(i) == QLatin1Char('"')) {
            ++quoteCount;
            if (quoteCount == 3) {
                // third consecutive quote
                quoteCount = 0;
                tmp += program.at(i);
            }
            continue;
        }
        if (quoteCount) {
            if (quoteCount == 1)
                inQuote = !inQuote;
            quoteCount = 0;
        }
        if (!inQuote && program.at(i).isSpace()) {
            if (!tmp.isEmpty()) {
                args += tmp;
                tmp.clear();
            }
        } else {
            tmp += program.at(i);
        }
    }
    if (!tmp.isEmpty())
        args += tmp;

    return args;
}


void replaceVar(QString &str, const QString &varName, const QString &after)
{
    str.replace(QRegExp(QString("\\$%1(?!\\w)").arg(varName)), after);
    str.replace(QRegExp(QString("\\$\\{%1\\}").arg(varName)), after);
}


QString expandEnvVariables(const QString str)
{
    QString scheme = QUrl(str).scheme();

    if (scheme == "http"   || scheme == "https" || scheme == "shttp" ||
        scheme == "ftp"    || scheme == "ftps"  ||
        scheme == "pop"    || scheme == "pops"  ||
        scheme == "imap"   || scheme == "imaps" ||
        scheme == "mailto" ||
        scheme == "nntp"   ||
        scheme == "irc"    ||
        scheme == "telnet" ||
        scheme == "xmpp"   ||
        scheme == "irc"    ||
        scheme == "nfs"
      )
        return str;

    QString res = str;
    res.replace(QRegExp("~(?=$|/)"), getenv("HOME"));

    replaceVar(res, "HOME", getenv("HOME"));
    replaceVar(res, "USER", getenv("USER"));

    replaceVar(res, "XDG_DESKTOP_DIR",   QStandardPaths::writableLocation(QStandardPaths::DesktopLocation));
    replaceVar(res, "XDG_TEMPLATES_DIR", QStandardPaths::writableLocation(QStandardPaths::TempLocation));
    replaceVar(res, "XDG_DOCUMENTS_DIR", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
    replaceVar(res, "XDG_MUSIC_DIR",     QStandardPaths::writableLocation(QStandardPaths::MusicLocation));
    replaceVar(res, "XDG_PICTURES_DIR",  QStandardPaths::writableLocation(QStandardPaths::PicturesLocation));
    replaceVar(res, "XDG_VIDEOS_DIR",    QStandardPaths::writableLocation(QStandardPaths::MoviesLocation));
    replaceVar(res, "XDG_PHOTOS_DIR",    QStandardPaths::writableLocation(QStandardPaths::PicturesLocation));

    return res;
}


QStringList expandEnvVariables(const QStringList strs)
{
    QStringList res;
    foreach(QString s, strs)
        res << expandEnvVariables(s);

    return res;
}


QStringList XdgDesktopFile::expandExecString(const QStringList& urls) const
{
    if (d->mType != ApplicationType)
        return QStringList();

    QStringList result;

    QString execStr = value("Exec").toString();
    unEscapeExec(execStr);
    QStringList tokens = parseCombinedArgString(execStr);

    foreach (QString token, tokens)
    {
        // The parseCombinedArgString() splits the string by the space symbols,
        // we temporarily replaced them on the special characters.
        // Now we reverse it.
        token.replace(01, ' ');
        token.replace(02, '\t');
        token.replace(03, '\n');

        // ----------------------------------------------------------
        // A single file name, even if multiple files are selected.
        if (token == "%f")
        {
            if (!urls.isEmpty())
                result << expandEnvVariables(urls.at(0));
            continue;
        }

        // ----------------------------------------------------------
        // A list of files. Use for apps that can open several local files at once.
        // Each file is passed as a separate argument to the executable program.
        if (token == "%F")
        {
            result << expandEnvVariables(urls);
            continue;
        }

        // ----------------------------------------------------------
        // A single URL. Local files may either be passed as file: URLs or as file path.
        if (token == "%u")
        {
            if (!urls.isEmpty())
            {
                QUrl url;
                url.setUrl(expandEnvVariables(urls.at(0)));
                result << ((!url.toLocalFile().isEmpty()) ? url.toLocalFile() : url.toEncoded());
            }
            continue;
        }

        // ----------------------------------------------------------
        // A list of URLs. Each URL is passed as a separate argument to the executable
        // program. Local files may either be passed as file: URLs or as file path.
        if (token == "%U")
        {
            foreach (QString s, urls)
            {
                QUrl url(expandEnvVariables(s));
                result << ((!url.toLocalFile().isEmpty()) ? url.toLocalFile() : url.toEncoded());
            }
            continue;
        }

        // ----------------------------------------------------------
        // 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.
        if (token == "%i")
        {
            QString icon = value("Icon").toString();
            if (!icon.isEmpty())
                result << "-icon" << icon.replace('%', "%%");
            continue;
        }


        // ----------------------------------------------------------
        // The translated name of the application as listed in the appropriate Name key
        // in the desktop entry.
        if (token == "%c")
        {
            result << localizedValue("Name").toString().replace('%', "%%");
            continue;
        }

        // ----------------------------------------------------------
        // 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.
        if (token == "%k")
        {
            result << fileName().replace('%', "%%");
            break;
        }

        // ----------------------------------------------------------
        // Deprecated.
        // Deprecated field codes should be removed from the command line and ignored.
        if (token == "%d" || token == "%D" ||
            token == "%n" || token == "%N" ||
            token == "%v" || token == "%m"
            )
        {
            continue;
        }

        // ----------------------------------------------------------
        result << expandEnvVariables(token);
    }

    return result;
}


bool checkTryExec(const QString& progName)
{
    if (progName.startsWith(QDir::separator()))
        return QFileInfo(progName).isExecutable();

    QStringList dirs = QString(getenv("PATH")).split(":");

    foreach (QString dir, dirs)
    {
        if (QFileInfo(QDir(dir), progName).isExecutable())
            return true;
    }

    return false;
}


bool XdgDesktopFile::isShow(const QString& environment) const
{
    const QString env = environment.toUpper();

    if (d->mIsShow.contains(env))
        return d->mIsShow.value(env);

    d->mIsShow.insert(env, false);
    // Means "this application exists, but don't display it in the menus".
    if (value("NoDisplay").toBool())
        return false;

    // The file is inapplicable to the current environment
    if (!isSuitable(true, env))
        return false;


    d->mIsShow.insert(env, true);
    return true;
}


bool XdgDesktopFile::isShown(const QString &environment) const
{
    const QString env = environment.toUpper();

    if (d->mIsShow.contains(env))
        return d->mIsShow.value(env);

    d->mIsShow.insert(env, false);

    // Means "this application exists, but don't display it in the menus".
    if (value("NoDisplay").toBool())
        return false;

    // The file is not suitable to the current environment
    if (!isSuitable(true, env))
        return false;

    d->mIsShow.insert(env, true);
    return true;
}


bool XdgDesktopFile::isApplicable(bool excludeHidden, const QString& environment) const
{
    // Hidden should have been called Deleted. It means the user deleted
    // (at his level) something that was present
    if (excludeHidden && value("Hidden").toBool())
        return false;

    // A list of strings identifying the environments that should display/not
    // display a given desktop entry.
    // OnlyShowIn ........
    if (contains("OnlyShowIn"))
    {
        QStringList s = value("OnlyShowIn").toString().split(';');
        if (!s.contains(environment))
            return false;
    }

    // NotShowIn .........
    if (contains("NotShowIn"))
    {
        QStringList s = value("NotShowIn").toString().split(';');
        if (s.contains(environment))
            return false;
    }

    // actually installed. If not, entry may not show in menus, etc.
    QString s = value("TryExec").toString();
    if (!s.isEmpty() && ! checkTryExec(s))
        return false;

    return true;
}


bool XdgDesktopFile::isSuitable(bool excludeHidden, const QString &environment) const
{
    // Hidden should have been called Deleted. It means the user deleted
    // (at his level) something that was present
    if (excludeHidden && value("Hidden").toBool())
        return false;

    // A list of strings identifying the environments that should display/not
    // display a given desktop entry.
    // OnlyShowIn ........
    QString env;
    if (environment.isEmpty())
        env = QString(detectDesktopEnvironment());
    else {
        env = environment.toUpper();
    }

    QString key;
    bool keyFound = false;
    if (contains(QLatin1String(onlyShowInKey)))
    {
        key = QLatin1String(onlyShowInKey);
        keyFound = true;
    }
    else
    {
        key = QLatin1String(extendPrefixKey) % QLatin1String(onlyShowInKey);
        keyFound = contains(key) ? true : false;
    }

    if (keyFound)
    {
        QStringList s = value(key).toString().toUpper().split(QLatin1Char(';'));
        if (!s.contains(env))
            return false;
    }

    // NotShowIn .........
    keyFound = false;
    if (contains(QLatin1String(notShowInKey)))
    {
        key = QLatin1String(notShowInKey);
        keyFound = true;
    }
    else
    {
        key = QLatin1String(extendPrefixKey) % QLatin1String(notShowInKey);
        keyFound = contains(key) ? true : false;
    }

    if (keyFound)
    {
        QStringList s = value(key).toString().toUpper().split(QLatin1Char(';'));
        if (s.contains(env))
            return false;
    }

    // actually installed. If not, entry may not show in menus, etc.
    QString s = value("TryExec").toString();
    if (!s.isEmpty() && ! checkTryExec(s))
        return false;

    return true;
}


QString expandDynamicUrl(QString url)
{
    foreach(QString line, QProcess::systemEnvironment())
    {
        QString name = line.section("=", 0, 0);
        QString val =  line.section("=", 1);
        url.replace(QString("$%1").arg(name), val);
        url.replace(QString("${%1}").arg(name), val);
    }

    return url;
}


QString XdgDesktopFile::url() const
{
    if (type() != LinkType)
        return QString();

   QString url;

   url = value("URL").toString();
   if (!url.isEmpty())
   return url;

    // WTF? What standard describes it?
    url = expandDynamicUrl(value("URL[$e]").toString());
    if (!url.isEmpty())
        return url;

    return QString();
}


QString findDesktopFile(const QString& dirName, const QString& desktopName)
{
    QDir dir(dirName);
    QFileInfo fi(dir, desktopName);

    if (fi.exists())
        return fi.canonicalFilePath();

    // Working recursively ............
    QFileInfoList dirs = dir.entryInfoList(QStringList(), QDir::Dirs | QDir::NoDotAndDotDot);
    foreach (QFileInfo d, dirs)
    {
        QString cn = d.canonicalFilePath();
        if (dirName != cn)
        {
            QString f = findDesktopFile(cn, desktopName);
            if (!f.isEmpty())
                return f;
        }
    }

    return QString();
}


QString findDesktopFile(const QString& desktopName)
{
    QStringList dataDirs = XdgDirs::dataDirs();
    dataDirs.prepend(XdgDirs::dataHome(false));

    foreach (QString dirName, dataDirs)
    {
        QString f = findDesktopFile(dirName + "/applications", desktopName);
        if (!f.isEmpty())
            return f;
    }

    return QString();
}


XdgDesktopFile* XdgDesktopFileCache::getFile(const QString& fileName)
{
    if (instance().m_fileCache.contains(fileName))
    {
        return instance().m_fileCache.value(fileName);
    }

    if (fileName.startsWith(QDir::separator()))
    {
        // Absolute path ........................
        //qDebug() << "XdgDesktopFileCache: add new file" << fileName;
        XdgDesktopFile* desktopFile = load(fileName);
        if (desktopFile->isValid())
            instance().m_fileCache.insert(fileName, desktopFile);
        return desktopFile;
    }
    else
    {
        // Search desktop file ..................
        QString filePath = findDesktopFile(fileName);
        XdgDesktopFile* desktopFile;
        //qDebug() << "Sokoloff XdgDesktopFileCache::getFile found fileName" << fileName << filePath;
        if (!filePath.isEmpty())
        {
            // The file was found
            if (!instance().m_fileCache.contains(filePath))
            {
                desktopFile = load(filePath);
                instance().m_fileCache.insert(filePath, desktopFile);
            }
            else
                desktopFile = instance().m_fileCache.value(filePath);

            return desktopFile;

        }
        else
        {
            return new XdgDesktopFile;
        }
    }
}

QList<XdgDesktopFile*> XdgDesktopFileCache::getAllFiles()
{
    return instance().m_fileCache.values();
}



XdgDesktopFileCache & XdgDesktopFileCache::instance()
{
    static XdgDesktopFileCache cache;
    if (!cache.m_IsInitialized)
    {
       cache.initialize();
       cache.m_IsInitialized = true;
    }

    return cache;
}


/*!
 * Handles files with a syntax similar to desktopfiles as QSettings files.
 * The differences between ini-files and desktopfiles are:
 * desktopfiles uses '#' as comment marker, and ';' as list-separator.
 * Every key/value must be inside a section (i.e. there is no 'General' pseudo-section)
 */
bool readDesktopFile(QIODevice & device, QSettings::SettingsMap & map)
{
    QString section;
    QTextStream stream(&device);

    while (!stream.atEnd()) {
        QString line = stream.readLine().trimmed();

        // Skip comments and empty lines
        if (line.startsWith('#') || line.isEmpty())
            continue;

        // Section ..............................
        if (line.startsWith('[') && line.endsWith(']'))
        {
            section = line.mid(1, line.length()-2);
            continue;
        }

        QString key = line.section('=', 0, 0).trimmed();
        QString value = line.section('=', 1).trimmed();

        if (key.isEmpty())
            continue;

        if (section.isEmpty())
        {
            qWarning() << "key=value outside section";
            return false;
        }

        key.prepend("/");
        key.prepend(section);

        if (value.contains(";"))
        {
            map.insert(key, value.split(";"));
        }
        else
        {
            map.insert(key, value);
        }

    }

    return true;
}


/*! See readDesktopFile
 */
bool writeDesktopFile(QIODevice & device, const QSettings::SettingsMap & map)
{
    QTextStream stream(&device);
    QString section;

    foreach (QString key, map.keys())
    {
        if (! map.value(key).canConvert<QString>())
        {
            return false;
        }

        QString thisSection = key.section("/", 0, 0);
        if (thisSection.isEmpty())
        {
            qWarning() << "No section defined";
            return false;
        }

        if (thisSection != section)
        {
            stream << "[" << thisSection << "]" << "\n";
            section = thisSection;
        }

        QString remainingKey = key.section("/", 1, -1);

        if (remainingKey.isEmpty())
        {
            qWarning() << "Only one level in key..." ;
            return false;
        }

        stream << remainingKey << "=" << map.value(key).toString() << "\n";

    }

    return true;
}


void XdgDesktopFileCache::initialize(const QString& dirName)
{
    QDir dir(dirName);
    // Directories have the type "application/x-directory", but in the desktop file
    // are shown as "inode/directory". To handle these cases, we use this hash.
    QHash<QString, QString> specials;
    specials.insert("inode/directory", "application/x-directory");


    // Working recursively ............
    QFileInfoList files = dir.entryInfoList(QStringList(), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
    foreach (QFileInfo f, files)
    {
        if (f.isDir())
        {
            initialize(f.absoluteFilePath());
            continue;
        }


        XdgDesktopFile* df = load(f.absoluteFilePath());
        if (!df)
            continue;

        if (! m_fileCache.contains(f.absoluteFilePath()))
        {
            m_fileCache.insert(f.absoluteFilePath(), df);
        }

        QStringList mimes = df->value("MimeType").toString().split(';', QString::SkipEmptyParts);

        foreach (QString mime, mimes)
        {
            int pref = df->value("InitialPreference", 0).toInt();
            // We move the desktopFile forward in the list for this mime, so that
            // no desktopfile in front of it have a lower initialPreference.
            int position = m_defaultAppsCache[mime].length();
            while (position > 0 && m_defaultAppsCache[mime][position - 1]->value("InitialPreference, 0").toInt() < pref)
            {
                position--;
            }
            m_defaultAppsCache[mime].insert(position, df);
        }
    }

}


XdgDesktopFile* XdgDesktopFileCache::load(const QString& fileName)
{
    XdgDesktopFile* desktopFile = new XdgDesktopFile();
    desktopFile->load(fileName);
    return desktopFile;
}


void loadMimeCacheDir(const QString& dirName, QHash<QString, QList<XdgDesktopFile*> > & cache)
{
    QDir dir(dirName);
    // Directories have the type "application/x-directory", but in the desktop file
    // are shown as "inode/directory". To handle these cases, we use this hash.
    QHash<QString, QString> specials;
    specials.insert("inode/directory", "application/x-directory");


    // Working recursively ............
    QFileInfoList files = dir.entryInfoList(QStringList(), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
    foreach (QFileInfo f, files)
    {
        if (f.isDir())
        {
            loadMimeCacheDir(f.absoluteFilePath(), cache);
            continue;
        }


        XdgDesktopFile* df = XdgDesktopFileCache::getFile(f.absoluteFilePath());
        if (!df)
            continue;

        QStringList mimes = df->value("MimeType").toString().split(';', QString::SkipEmptyParts);

        foreach (QString mime, mimes)
        {
            int pref = df->value("InitialPreference", 0).toInt();
            // We move the desktopFile forward in the list for this mime, so that
            // no desktopfile in front of it have a lower initialPreference.
            int position = cache[mime].length();
            while (position > 0 && cache[mime][position - 1]->value("InitialPreference, 0").toInt() < pref)
            {
                position--;
            }
            cache[mime].insert(position, df);
        }
    }
}

QSettings::Format XdgDesktopFileCache::desktopFileSettingsFormat()
{
    static QSettings::Format format = QSettings::InvalidFormat;

    if (format == QSettings::InvalidFormat)
    {
        format = QSettings::registerFormat("*.list", readDesktopFile, writeDesktopFile);
        qDebug() << "registerFormat returned:" << format;
    }

    return format;
}


XdgDesktopFileCache::XdgDesktopFileCache() :
    m_IsInitialized(false),
        m_defaultAppsCache(),
        m_fileCache()
{
}


XdgDesktopFileCache::~XdgDesktopFileCache()
{
}


void XdgDesktopFileCache::initialize()
{
    QStringList dataDirs = XdgDirs::dataDirs();
    dataDirs.prepend(XdgDirs::dataHome(false));

    foreach (const QString dirname, dataDirs)
    {
        initialize(dirname + "/applications");
//        loadMimeCacheDir(dirname + "/applications", m_defaultAppsCache);
    }
}

QList<XdgDesktopFile*> XdgDesktopFileCache::getAppsOfCategory(const QString& category)
{
    QList<XdgDesktopFile*> list;
    const QString _category = category.toUpper();
    foreach (XdgDesktopFile *desktopFile, instance().m_fileCache.values())
    {
        QStringList categories = desktopFile->value("Categories").toString().toUpper().split(QLatin1Char(';'));
        if (!categories.isEmpty() && (categories.contains(_category) || categories.contains(QLatin1String("X-") % _category)))
            list.append(desktopFile);
    }
    return list;
}

QList<XdgDesktopFile*>  XdgDesktopFileCache::getApps(const QString& mimetype)
{
    return instance().m_defaultAppsCache.value(mimetype);
}


XdgDesktopFile* XdgDesktopFileCache::getDefaultApp(const QString& mimetype)
{
    // First, we look in ~/.local/share/applications/mimeapps.list, /usr/local/share/applications/mimeapps.list and
    // /usr/share/applications/mimeapps.list (in that order) for a default.
    QStringList dataDirs = XdgDirs::dataDirs();
    dataDirs.prepend(XdgDirs::dataHome(false));
    foreach(const QString dataDir, dataDirs)
    {
        QString defaultsListPath = dataDir + "/applications/mimeapps.list";
        if (QFileInfo(defaultsListPath).exists())
        {
            QSettings defaults(defaultsListPath, desktopFileSettingsFormat());


            defaults.beginGroup("Default Applications");
            if (defaults.contains(mimetype))
            {
                QVariant value = defaults.value(mimetype);
                if (value.canConvert<QStringList>()) // A single string can also convert to a stringlist
                {
                    foreach (const QString desktopFileName, value.toStringList())
                    {
                        XdgDesktopFile* desktopFile = XdgDesktopFileCache::getFile(desktopFileName);
                        if (desktopFile->isValid())
                        {
                            return desktopFile;
                        }
                        else
                        {
                            qWarning() << desktopFileName << "not a valid desktopfile";
                        }
                    }
                }
            }
            defaults.endGroup();
        }
    }

    // If we havent found anything up to here, we look for a desktopfile that declares
    // the ability to handle the given mimetype. See getApps.
    QList<XdgDesktopFile*> apps = getApps(mimetype);
    XdgDesktopFile* desktopFile = apps.isEmpty() ? 0 : apps[0];
    return desktopFile;
}
