#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim: set ft=python ts=4 sw=4 sts=4 et :

# Copyright (C) 2015 Jason Mehring <nrgaway@gmail.com>
# License: GPL-2+
# Authors: Jason Mehring
#
# 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 <http://www.gnu.org/licenses/>.

'''Installation and edition of desktop files.

Description:
  The desktop-file-install program is a tool to install, and optionally edit,
  desktop files.  They are mostly useful for developers and packagers.

  Various options are available to edit the desktop files. The edit options can
  be specified more than once and will be processed in the same order as the
  options passed to the program.

  The original .desktop files are left untouched and left in place.

  Qubes modifies the XDG_CONFIG_DIRS to first include the `/var/lib/qubes/xdg`
  directory (XDG_CONFIG_DIRS=/var/lib/qubes/xdg:/etc/xdg).

Usage:
  qubes-desktop-file-install [--dir DIR] [--force]
                             [--remove-show-in]
                             [--remove-key KEY]
                             [--remove-only-show-in ENVIRONMENT]
                             [--add-only-show-in ENVIRONMENT]
                             [--remove-not-show-in ENVIRONMENT]
                             [--add-not-show-in ENVIRONMENT]
                             [(--set-key KEY VALUE)]
                             FILE
  qubes-desktop-file-install -h | --help
  qubes-desktop-file-install --version

Examples:
  qubes-desktop-file-install --dir /var/lib/qubes/xdg/autostart --add-only-show-in X-QUBES /etc/xdg/autostart/pulseaudio.desktop

Arguments:
  FILE     Path to desktop entry file

Help Options:
  -h, --help                         show this help message and exit

Installation options for desktop file:
  --dir DIR                          Install desktop files to the DIR directory (default: <FILE>)
  --force                            Force overwrite of existing desktop files (default: False)

Edition options for desktop file:
  --remove-show-in                   Remove the "OnlyShowIn" and "NotShowIn" entries from the desktop file (default: False)
  --remove-key KEY                   Remove the KEY key from the desktop files, if present
  --set-key (KEY VALUE)              Set the KEY key to VALUE
  --remove-only-show-in ENVIRONMENT  Remove ENVIRONMENT from the list of desktop environment where the desktop files should be displayed
  --add-only-show-in ENVIRONMENT     Add ENVIRONMENT to the list of desktop environment where the desktop files should be displayed
  --remove-not-show-in ENVIRONMENT   Remove ENVIRONMENT from the list of desktop environment where the desktop files should not be displayed
  --add-not-show-in ENVIRONMENT      Add ENVIRONMENT to the list of desktop environment where the desktop files should not be displayed
'''

import argparse
import codecs
import io
import locale
import os
import sys

try:
    import ConfigParser as configparser
except ImportError:
    import configparser

from collections import OrderedDict

# Third party libs
import xdg.DesktopEntry

__all__ = []
__version__ = '1.0.0'

# This is almost always a good thing to do at the beginning of your programs.
locale.setlocale(locale.LC_ALL, '')

# Default Qubes directory that modified desktop entry config files are stored in
QUBES_XDG_CONFIG_DIR = '/vat/lib/qubes/xdg'


class DesktopEntry(xdg.DesktopEntry.DesktopEntry):
    '''Class to parse and validate Desktop Entries (OVERRIDE).

    xdg.DesktopEntry.DesktopEntry does not maintain order of content
    '''

    defaultGroup = 'Desktop Entry'

    def __init__(self, filename=None):
        """Create a new DesktopEntry

        If filename exists, it will be parsed as a desktop entry file. If not,
        or if filename is None, a blank DesktopEntry is created.
        """
        self.content = OrderedDict()
        if filename and os.path.exists(filename):
            self.parse(filename)
        elif filename:
            self.new(filename)

    def parse(self, filename):
        '''Parse a desktop entry file.'''
        headers = [u'Desktop Entry', u'KDE Desktop Entry']
        cfgparser = configparser.RawConfigParser()
        cfgparser.optionxform = unicode

        try:
            cfgparser.readfp(codecs.open(filename, 'r', 'utf8'))
        except configparser.MissingSectionHeaderError:
            sys.exit('{0} missing header!'.format(filename, headers[0]))

        self.filename = filename
        self.tainted = False

        for header in headers:
            if cfgparser.has_section(header):
                self.content[header] = OrderedDict(cfgparser.items(header))
                if not self.defaultGroup:
                    self.defaultGroup = header

        if not self.defaultGroup:
            sys.exit('{0} missing header!'.format(filename, headers[0]))

    # Write support broken in Wheezy; override here
    def write(self, filename=None, trusted=False):
        if not filename and not self.filename:
            raise ParsingError("File not found", "")

        if filename:
            self.filename = filename
        else:
            filename = self.filename

        if os.path.dirname(filename) and not os.path.isdir(os.path.dirname(filename)):
            os.makedirs(os.path.dirname(filename))

        with io.open(filename, 'w', encoding='utf-8') as fp:

            # An executable bit signifies that the desktop file is
            # trusted, but then the file can be executed. Add hashbang to
            # make sure that the file is opened by something that
            # understands desktop files.
            if trusted:
                fp.write(u("#!/usr/bin/env xdg-open\n"))

            if self.defaultGroup:
                fp.write(unicode("[%s]\n") % self.defaultGroup)
                for (key, value) in self.content[self.defaultGroup].items():
                    fp.write(unicode("%s=%s\n") % (key, value))
                fp.write(unicode("\n"))
            for (name, group) in self.content.items():
                if name != self.defaultGroup:
                    fp.write(unicode("[%s]\n") % name)
                    for (key, value) in group.items():
                        fp.write(unicode("%s=%s\n") % (key, value))
                    fp.write(unicode("\n"))

        # Add executable bits to the file to show that it's trusted.
        if trusted:
            oldmode = os.stat(filename).st_mode
            mode = oldmode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
            os.chmod(filename, mode)

        self.tainted = False


def delete(path):
    '''Delete a file.
    '''
    if os.path.exists(path):
        try:
            os.unlink(os.path.abspath(path))
        except IOError as error:
            sys.exit('Unable to delete file: {0}\n{1}'.format(path, error))


def set_key(entry, key, value):
    '''Set a key with value within an desktop-file entry object.
    '''
    key = unicode(key)
    if isinstance(value, list):
        entry.set(key, u';'.join(value))
    else:
        entry.set(key, unicode(value))


def remove_key(entry, key):
    '''Remove a key within an desktop-file entry object.
    '''
    entry.removeKey(unicode(key))


def add_value(entry, key, value):
    '''Add a value to a desktop-file entry object.
    '''
    values = entry.getList(unicode(value))
    for value in values:
        entry_values = entry.get(key, list=True)
        if value not in entry_values:
            entry_values.append(value)
            set_key(entry, key, entry_values)


def remove_value(entry, key, value):
    '''Remove a value to a desktop-file entry object.
    '''
    value = unicode(value)
    entry_values = entry.get(key, list=True)
    if value in entry_values:
        entry_values.remove(value)
        if entry_values:
            set_key(entry, key, entry_values)
        else:
            remove_key(entry, key)


def install(**kwargs):
    '''Install a copy of a desktop-file entry file to a new location and
    optionally edit it.
    '''
    paths = kwargs.get('path', [])
    for path in paths:
        if not path:
            sys.exit('No path selected!')

        filename, extension = os.path.splitext(path)
        if extension.lower() not in ['.desktop']:
            sys.exit("Invalid desktop extenstion '{0}'!  Was expecting '.desktop'.".format(extension))

        new_path = os.path.join(kwargs['dir'], os.path.basename(path))

        if os.path.exists(path) and os.path.isfile(path):
            stat_info = os.stat(path)

            # Don't update if file has previously been updated unless force is True
            if os.path.exists(new_path) and not kwargs['force']:
                if os.stat(new_path).st_mtime == stat_info.st_mtime:
                    continue
        else:
            if os.path.exists(new_path) and os.path.isfile(new_path):
                delete(new_path)
            continue

        entry = DesktopEntry(path)

        if kwargs['remove_show_in']:
            kwargs['remove_key'].append(u'OnlyShowIn')
            kwargs['remove_key'].append(u'NotShowIn')

        if kwargs['remove_key']:
            for value in kwargs['remove_key']:
                remove_key(entry, value)

        if kwargs['remove_only_show_in']:
            for value in kwargs['remove_only_show_in']:
                remove_value(entry, u'OnlyShowIn', value)

        if kwargs['add_only_show_in']:
            for value in kwargs['add_only_show_in']:
                add_value(entry, u'OnlyShowIn', value)

        if kwargs['remove_not_show_in']:
            for value in kwargs['remove_not_show_in']:
                remove_value(entry, u'NotShowIn', value)

        if kwargs['add_not_show_in']:
            for value in kwargs['add_not_show_in']:
                add_value(entry, u'NotShowIn', value)

        if kwargs['set_key']:
            for key, value in kwargs['set_key']:
                set_key(entry, key, value)

        entry.write(new_path)
        if stat_info:
            os.utime(new_path, (stat_info.st_atime, stat_info.st_mtime))



def parse(args):
    '''Argparse configuration.
    '''
    parser = argparse.ArgumentParser()

    parser.add_argument('--version', action='version', version='%(prog)s (version {0})'.format(__version__))

    parser.add_argument('--force', action='store_true', default=False, help='\
        Force overwrite of existing desktop files.')

    parser.add_argument('--dir', default=QUBES_XDG_CONFIG_DIR, help='\
        Install desktop files to the DIR directory.')

    parser.add_argument('--remove-show-in', action='store_true', default=False, help='\
        Remove the "OnlyShowIn" and "NotShowIn" entries from the desktop file')

    parser.add_argument('--remove-key', action='append', metavar='KEY', default=[], help='\
        Remove the KEY key from the desktop file')

    parser.add_argument('--remove-only-show-in', action='append', metavar='ENVIRONMENT', default=[], help='\
        Remove ENVIRONMENT from the list of desktop environments where the\
        desktop files should be displayed (key OnlyShowIn). If ENVIRONMENT was\
        not present in the list, this operation is a no-op.')

    parser.add_argument('--add-only-show-in', action='append', metavar='ENVIRONMENT', default=[], help='\
        Add ENVIRONMENT to the list of desktop environments where the desktop\
        files should be displayed (key OnlyShowIn). If ENVIRONMENT was already\
        present in the list, this operation is a no-op. A non-registered desktop\
        environment should be prefixed with X-. Note that an empty OnlyShowIn\
        key in a desktop file means that the desktop file will be displayed in\
        all environments.')

    parser.add_argument('--remove-not-show-in', action='append', metavar='ENVIRONMENT', default=[], help='\
        Remove ENVIRONMENT from the list of desktop environments where the\
        desktop files should not  be  displayed  (key  NotShowIn).  If\
        ENVIRONMENT was not present in the list, this operation is a no-op.')

    parser.add_argument('--add-not-show-in', action='append', metavar='ENVIRONMENT', default=[], help='\
        Add ENVIRONMENT to the list of desktop environments where the desktop\
        files should not be displayed (key NotShowIn). If ENVIRONMENT was\
        already present in the list, this operation is a no-op. A non-registered\
        desktop environment should be prefixed with X-.  Note that an empty\
        NotShowIn key in a desktop file means that the desktop file will be\
        displayed in all environments.')

    parser.add_argument('--set-key', action='append', nargs=2, metavar=('KEY', 'VALUE'), default=[], help='\
        Set the KEY key to the VALUE passed.')

    parser.add_argument('path', action='store', nargs='+', metavar='FILE', default=None,
        help='Path to desktop entry file')

    args = parser.parse_args(args)

    if not os.path.isabs(args.dir):
        args.dir = os.path.join(QUBES_XDG_CONFIG_DIR, args.dir)

    return args


def main(argv):
    '''Main function.
    '''
    args = parse(argv[1:])
    install(**vars(args))


if __name__ == '__main__':
    main(sys.argv)
    sys.exit(0)
