# -*- python -*-
#
# fuss-launcher application information cache
#
# Copyright (C) 2010 Enrico Zini <enrico@truelite.it>
# Copyright (C) 2010 Christopher R. Gabriel <cgabriel@truelite.it>
# Copyright (C) 2010 The Fuss Project <info@fuss.bz.it>
#
# Authors: Christopher R. Gabriel <cgabriel@truelite.it>
#          Enrico Zini <enrico@truelite.it>
#
# Sponsored by the Fuss Project: http://www.fuss.bz.it/
#
# 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 3 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 <http://www.gnu.org/licenses/>.


import os, os.path
import sys
import errno
import shlex
import pygtk
pygtk.require('2.0')
import gtk
import gio
import fusslauncher
import fusslauncher.conf as conf
import pipes

import gettext
_ = gettext.gettext
from xdg.DesktopEntry import DesktopEntry
import xdg.Menu

# TODO: get the location from:
#   FreeDesktop menu spec
#   FreeDesktop basedir spec
APPDIR="/usr/share/applications/"
APPINSTDIR="/usr/share/app-install/desktop/"
APPINSTICONDIR="/usr/share/app-install/icons/"
ICONDIRS=("/usr/share/pixmaps/", APPINSTICONDIR)

class Notifier(object):
    def __init__(self):
        self.reopen()

    def reopen(self):
        try:
            import dbus
            sesbus = dbus.SessionBus()
            notify_obj = sesbus.get_object('org.freedesktop.Notifications',
                    '/org/freedesktop/Notifications')
            self.notify_if = dbus.Interface(notify_obj, 'org.freedesktop.Notifications')
        except:
            self.notify_if = None

    def _notify(self, operation, command):
        if self.notify_if:
            self.notify_if.Notify(_('Starting application - please wait'),0,'',_("Executing: %s") % operation,_("Executed command: %s") % command,[],{},-1)
        else:
            print "Executing %s (%s)" % (operation, command)

    def notify(self, operation, command):
        if not self.notify_if:
            self.reopen()
        try:
            self._notify(operation, command)
        except:
            self.reopen()
            self._notify(operation, command)

notifier = Notifier()

icon_theme = gtk.icon_theme_get_default()

class RunStats(object):
    def __init__(self):
        self.load()

    def load(self):
        "Load saved statistics"
        self.stats = {}
        fname = os.path.expanduser("~/.fuss-launcher/runstats")
        if os.path.exists(fname):
            for line in open(fname):
                row = line.split()
                self.stats[row[0]] = int(row[1])
        self.dirty = False

    def save(self):
        "Atomically save stats"
        # Do not save if nothing has changed
        if not self.dirty: return

        homedir = os.path.expanduser("~/.fuss-launcher")
        if not os.path.isdir(homedir):
            os.mkdir(homedir)
        fname = os.path.join(homedir, "runstats")
        tmpfname = fname + ".tmp"
        out = open(tmpfname, "w")
        for k, v in self.stats.iteritems():
            print >>out, k, v
        out.close()
        os.rename(tmpfname, fname)
        self.dirty = False

    def notify_run(self, name):
        "Notify that a given app has been run"
        old = self.stats.get(name, 0)
        self.stats[name] = old + 1
        self.dirty = True
        self.save()

    def get(self):
        "Return the .desktop files sorted by decreasing run frequency"
        res = self.stats.keys()
        res.sort(key=lambda x:self.stats[x], reverse=True)
        return res

run_stats = RunStats()

class Favourites(object):
    def __init__(self):
        self.load()

    def load(self):
        "Load favourites"
        self.favourites = set()
        fname = os.path.expanduser("~/.fuss-launcher/favourites")
        if os.path.exists(fname):
            for line in open(fname):
                line = line.strip()
                if not line: continue
                self.favourites.add(line)
        self.dirty = False

    def save(self):
        "Atomically save stats"
        # Do not save if nothing has changed
        if not self.dirty: return

        homedir = os.path.expanduser("~/.fuss-launcher")
        if not os.path.isdir(homedir):
            os.mkdir(homedir)
        fname = os.path.join(homedir, "favourites")
        tmpfname = fname + ".tmp"
        out = open(tmpfname, "w")
        for line in self.favourites:
            print >>out, line
        out.close()
        os.rename(tmpfname, fname)
        self.dirty = False

    def add(self, name):
        "Add an entry to favourites"
        if name.startswith(APPDIR):
            name = os.path.basename(name)
        if name in self.favourites: return False
        self.favourites.add(name)
        self.dirty = True
        self.save()
        return True

    def remove(self, name):
        "Remove an entry from favourites"
        if name.startswith(APPDIR):
            name = os.path.basename(name)
        if name not in self.favourites: return False
        self.favourites.remove(name)
        self.dirty = True
        self.save()
        return True

    def get(self):
        "Return the .desktop files sorted by decreasing run frequency"
        return self.favourites

favourites = Favourites()

class MimeDB(object):
    def __init__(self):
        self.mimemap = {}
        for f in os.listdir(APPDIR):
            if f[0] == '.' or not f.endswith(".desktop"): continue
            fname = os.path.join(APPDIR, f)
            de = DesktopEntry(fname)
            for mt in de.getMimeTypes():
                self.mimemap.setdefault(mt, []).append(f)

    def lookup(self, mt):
        return self.mimemap.get(mt, [])

    def lookup_uris(self, uris):
        desktops = set()
        for u in uris:
            f = gio.File(uri=u)
            info = f.query_info("standard::content-type", gio.FILE_QUERY_INFO_NONE)
            content_type = info.get_content_type()
            mt = gio.content_type_get_mime_type(content_type)
            res = self.lookup(mt)
            if not res and mt.startswith("text/"):
                res = self.lookup("text/plain")
            desktops.update(res)
        return desktops

mime_db = MimeDB()

class AppInfo(object):
    MISSING_ICON = icon_theme.load_icon("fuss-launcher-missing", 48, 0)

    def __init__(self, basename):
        if not basename.startswith("/"):
            dfname = os.path.join(APPDIR, basename)
        else:
            dfname = basename
        if not os.path.exists(dfname):
            self.inst = False
            dfname = os.path.join(APPINSTDIR, basename)
            if not os.path.exists(dfname):
                self.name = basename
                self.icon = self.MISSING_ICON
                self.ipath = None
                self.command = None
                return
        else:
            self.inst = True

        # Desktop file name
        self.dpath = dfname

        # Get info from .desktop file
        item = DesktopEntry(dfname)
        self.name = item.getName()
        self.command = item.getExec()

        # Load icon
        info = icon_theme.lookup_icon(item.getIcon(), 48, 0)
        if info is not None:
            self.icon = info.load_icon()
            self.ipath = info.get_filename()
            if self.ipath is None and info.get_builtin_pixbuf() is not None:
                self.ipath = "(builtin)"
        else:
            ifname = self._lookup_icon(item.getIcon())
            try:
                icon = gtk.gdk.pixbuf_new_from_file(ifname)
                if icon.get_width() > 48:
                    icon = icon.scale_simple(48, 48, gtk.gdk.INTERP_BILINEAR)
                self.ipath = ifname
            except Exception, e:
                print >>sys.stderr, "%s: Cannot load icon %s: %s" % (basename, ifname, str(e))
                icon = icon_theme.load_icon("fuss-launcher-missing", 48, 0)
                self.ipath = None
            self.icon = icon

    def _lookup_icon(self, fname):
        "Aggressively look for icons"
        if fname.startswith("/"):
            return fname

        if self.inst:
            # Try all places where we know there are icons
            for d in ICONDIRS:
                ifname = os.path.join(d, fname)
                if os.path.exists(ifname): break
            return ifname
        else:
            # Package not installed, only look in app-install-data icon dir
            return os.path.join(APPINSTICONDIR, fname)

    def deurl(self, arg):
        arg = arg.replace("%20", " ")
        if arg.startswith("file://"):
            return arg[7:]
        else:
            return arg

    def expand_arg(self, arg, parms):
        if arg == "%f":
            if not parms: return []
            return [self.deurl(parms[0])]
        elif arg == "%F":
            if not parms: return []
            return map(self.deurl, parms)
        elif arg == "%u":
            if not parms: return []
            return [parms[0]]
        elif arg == "%U":
            if not parms: return []
            return list(parms)
        elif arg == "%i":
            return ["--icon", self.ipath]
        elif arg == "%c":
            return [self.name]
        elif arg == "%k":
            return [self.dpath]
        else:
            return [arg]

    def run(self, parms=()):
        global run_stats
        cmd = self.command
        print "CMD", cmd
        args = shlex.split(cmd)
        print "ARGS", repr(args)
        print "PARMS", parms
        args = sum([self.expand_arg(x, parms) for x in args], [])
        print "XARGS", repr(args)
        cmdline = " ".join(map(pipes.quote, args))
        print "CMDLINE", cmdline
        run_stats.notify_run(os.path.basename(self.dpath))
        notifier.notify(self.name, cmdline)
        #subprocess.check_call(args + ["&"], shell=True)
        #os.system("%s&" % cmd)
        os.system(cmdline + " &")

class AppInfoCache(object):
    def __init__(self):
        self.cache = {}

    def reset(self):
        self.cache = {}

    def __getitem__(self, name):
        val = self.cache.get(name, None)
        if val is not None: return val
        val = AppInfo(name)
        self.cache[name] = val
        return val

class ResourceDir(object):
    """
    Directory (or set of directories) where we find resources
    """

    # Base directory for our resource directories
    ROOTS = [
            "/etc/fuss-launcher/",
            "/usr/local/etc/fuss-launcher/",
            os.path.expanduser("~/.fuss-launcher/")
    ]

    def __init__(self, name):
        # Resource directory named 'name' under any of our roots
        self.roots = [os.path.join(x, name) for x in self.ROOTS]
        self.last_ts = 0

    def timestamp(self):
        """
        Return the maximum timestamp of all our directories
        """
        res = [self.last_ts]
        for r in self.roots:
            try:
                ts = os.path.getmtime(r)
                res.append(ts)
            except OSError, e:
                if e.errno == errno.ENOENT:
                    continue
        return max(res)

    def list(self):
        """
        List the contents of all our directories.

        Contents are unique by basename, which means that a file in our second
        directory can override the same file in our first one.
        """
        res = dict()
        for r in self.roots:
            if not os.path.exists(r): continue
            for f in os.listdir(r):
                if f.startswith("."): continue
                res[f] = os.path.join(r, f)
        return res.itervalues()

def flatten_menu(menu):
    """
    Returns the DesktopEntry items contained in the menu
    """
    def xtract_desktops(m, res):
        for e in m.getEntries():
            de = getattr(e, "DesktopEntry", None)
            if de is not None:
                res[e.Filename] = de
            elif hasattr(e, "getEntries"):
                xtract_desktops(e, res)
    res = dict()
    xtract_desktops(menu, res)
    return res.keys()

class Extras(ResourceDir):
    def __init__(self):
        super(Extras, self).__init__("extras")
        self.groups = []
        self.links = []

    def refresh(self):
        """"
        Refresh if needed, and return a bool telling if the results changed
        since last time
        """
        ts = self.timestamp()
        if ts <= self.last_ts:
            return False
        self.groups = []
        self.links = []
        for fname in self.list():
            # Ignore empty files, so they can be used to suppress things in
            # other dirs
            if os.path.getsize(fname) == 0: continue
            if fname.endswith(".menu"):
                self.groups.append(xdg.Menu.parse(fname))
            elif fname.endswith(".desktop"):
                de = DesktopEntry(fname)
                if de.getType() == "Link":
                    self.links.append(de)
        self.last_ts = ts
        return True
