#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
User Daemon to suggest packages to install when new hardware
is inserted into the machine.
"""
# Copyright (C) 2013,2015-2016 Petter Reinholdtsen <pere@debian.org>
# AptDaemon gtk client code based on gtk-demo, copyright (C) 2008-2009
# Sebastian Heinlein <sevel@glatzor.de>
#
# Licensed under the GNU General Public License Version 2
#
# 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, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

__author__ = "Petter Reinholdtsen <pere@hungry.com>"

use_apt_daemon = False

import string
import subprocess
import glob
import fnmatch
import os

import gi
from gi.repository import GLib
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
gi.require_version('GUdev', '1.0')
from gi.repository import GUdev
gi.require_version('Notify', '0.7')
from gi.repository import Notify
gi.require_version('PackageKitGlib', '1.0')
from gi.repository import PackageKitGlib as packagekit
from gi.repository import Gio

import isenkram.lookup
import isenkram.usb

if use_apt_daemon:
    import aptdaemon.client
    from aptdaemon.gtk3widgets import AptErrorDialog, \
                                     AptConfirmDialog, \
                                     AptProgressDialog
    import aptdaemon.errors

class AptDaemonGUIClient(object):

    """Provides a graphical interface to aptdaemon."""

    def _run_transaction(self, transaction):
        dia = AptProgressDialog(transaction, parent=self.win)
        dia.run(close_on_finished=True, show_error=True,
                reply_handler=lambda: True,
                error_handler=self._on_error)

    def _simulate_trans(self, trans):
        trans.simulate(reply_handler=lambda: self._confirm_deps(trans),
                       error_handler=self._on_error)

    def _confirm_deps(self, trans):
        if [pkgs for pkgs in trans.dependencies if pkgs]:
            dia = AptConfirmDialog(trans, parent=self.win)
            res = dia.run()
            dia.hide()
            if res != Gtk.ResponseType.OK:
                return
        self._run_transaction(trans)

    def _on_error(self, error):
        try:
            raise error
        except aptdaemon.errors.NotAuthorizedError:
            # Silently ignore auth failures
            return
        except aptdaemon.errors.TransactionFailed as error:
            pass
        except Exception as error:
            error = aptdaemon.errors.TransactionFailed(aptdaemon.enums.ERROR_UNKNOWN,
                                                       str(error))
        dia = AptErrorDialog(error)
        dia.run()
        dia.hide()

    def request_installation(self, *args):
        self.ac.install_packages([self.package],
                                 reply_handler=self._simulate_trans,
                                 error_handler=self._on_error)

    def __init__(self, package):
        self.win = None
        self.package = package
        self.loop = GLib.MainLoop()
        self.ac = aptdaemon.client.AptClient()

    def run(self):
        self.loop.run()


# Good references for programming with PackageKit:
# https://lazka.github.io/pgi-docs/
# https://www.freedesktop.org/software/PackageKit/gtk-doc/libpackagekit-gobject.html
class PackageException(Exception):
    """A package operation has failed."""

    def __init__(self, error_string=None, error_details=None, *args, **kwargs):
        """Store packagekit error string and details."""
        super(PackageException, self).__init__(*args, **kwargs)

        self.error_string = error_string
        self.error_details = error_details

    def __str__(self):
        """Return the strin representation of the exception."""
        return 'PackageException(error_string="{0}", error_details="{1}")' \
            .format(self.error_string, self.error_details)


class PackageKitInstaller(object):
    """Helper to install packages using PackageKit."""

    def __init__(self, package_names):
        """Initialize transaction object.

        Set most values to None until they are sent as progress update.
        """
        self.package_names = package_names

        # Progress
        self.allow_cancel = None
        self.percentage = None
        self.status = None
        self.status_string = None
        self.flags = None
        self.package = None
        self.package_id = None
        self.item_progress = None
        self.role = None
        self.caller_active = None
        self.download_size_remaining = None
        self.speed = None

    def get_id(self):
        """Return a identifier to use as a key in a map of transactions."""
        return frozenset(self.package_names)

    def __str__(self):
        """Return the string representation of the object"""
        return ('Transaction(packages={0}, allow_cancel={1}, status={2}, '
                ' percentage={3}, package={4}, item_progress={5})').format(
                    self.package_names, self.allow_cancel, self.status_string,
                    self.percentage, self.package, self.item_progress)

    def request_installation(self):
        """Notify, start installation and then notify success/error."""
        start_notification = Notify.Notification(
            summary='Installing packages',
            body='Installing packages - {packages}'.format(
                packages=', '.join(self.package_names)))
        start_notification.set_timeout(10000)
        start_notification.show()

        error = None
        try:
            self.install()
        except PackageException as exception:
            error = exception

        start_notification.close()
        if not error:
            final_notification = Notify.Notification(
                summary='Installation successful',
                body='Packages have been successfully installed - {packages}'.
                format(packages=', '.join(self.package_names)))
        else:
            final_notification = Notify.Notification(
                summary='Installation failed',
                body='Error installing packages: {string}, {details}'.format(
                    string=error.error_string, details=error.error_details))
        final_notification.set_timeout(10000)
        final_notification.show()

    def install(self):
        """Run a PackageKit transaction to install given packages."""
        try:
            self._do_install()
        except GLib.Error as exception:
            raise PackageException(exception.message)

    def _do_install(self):
        """Run a PackageKit transaction to install given packages.

        Raise exception in case of error.
        """
        client = packagekit.Client()
        client.set_interactive(False)

        # Refresh package cache from all enabled repositories
        results = client.refresh_cache(
            False, None, self.progress_callback, self)
        self._assert_success(results)

        # Resolve packages again to get the latest versions after refresh
        results = client.resolve(packagekit.FilterEnum.INSTALLED,
                                 tuple(self.package_names) + (None, ),
                                 None, self.progress_callback, self)
        self._assert_success(results)

        packages_resolved = {}
        for package in results.get_package_array():
            packages_resolved[package.get_name()] = package

        package_ids = []
        for package_name in self.package_names:
            if package_name not in packages_resolved or \
               not packages_resolved[package_name]:
                raise PackageException('packages not found')

            package_ids.append(packages_resolved[package_name].get_id())

        # Start package installation
        results = client.install_packages(
            1 << packagekit.TransactionFlagEnum.ONLY_TRUSTED,
            package_ids + [None],
            None, self.progress_callback, self)
        self._assert_success(results)

    def _assert_success(self, results):
        """Check that the most recent operation was a success."""
        if results and results.get_error_code() is not None:
            error = results.get_error_code()
            error_code = error.get_code() if error else None
            error_string = packagekit.ErrorEnum.to_string(error_code) \
                if error_code else None
            error_details = error.get_details() if error else None
            raise PackageException(error_string, error_details)

    def progress_callback(self, progress, progress_type, user_data):
        """Process progress updates on package resolve operation"""
        return
        if progress_type == packagekit.ProgressType.PERCENTAGE:
            self.percentage = progress.props.percentage
        elif progress_type == packagekit.ProgressType.PACKAGE:
            self.package = progress.props.package
        elif progress_type == packagekit.ProgressType.ALLOW_CANCEL:
            self.allow_cancel = progress.props.allow_cancel
        elif progress_type == packagekit.ProgressType.PACKAGE_ID:
            self.package_id = progress.props.package_id
        elif progress_type == packagekit.ProgressType.ITEM_PROGRESS:
            self.item_progress = progress.props.item_progress
        elif progress_type == packagekit.ProgressType.STATUS:
            self.status = progress.props.status
            self.status_string = \
                packagekit.StatusEnum.to_string(progress.props.status)
        elif progress_type == packagekit.ProgressType.TRANSACTION_FLAGS:
            self.flags = progress.props.transaction_flags
        elif progress_type == packagekit.ProgressType.ROLE:
            self.role = progress.props.role
        elif progress_type == packagekit.ProgressType.CALLER_ACTIVE:
            self.caller_active = progress.props.caller_active
        elif progress_type == packagekit.ProgressType.DOWNLOAD_SIZE_REMAINING:
            self.download_size_remaining = \
                progress.props.download_size_remaining
        elif progress_type == packagekit.ProgressType.SPEED:
            self.speed = progress.props.speed
        else:
            print(('Unhandled packagekit progress callback - %s, %s',
                  progress, progress_type))


# Keep refs needed for callback to work
n = None
npkgs = None

def notify_pleaseinstall(notification=None, action=None, data=None):
    pkgs = data
    pkgsstr = " ".join(pkgs)
#    print pkgs
    print("info: button clicked, installing %s" % pkgsstr)
    if use_apt_daemon:
        installer = AptDaemonGUIClient(pkgs[0])
    else:
        installer = PackageKitInstaller(pkgs)
    installer.request_installation()
def notify(bus, vendor, device, pkgs):
    pkgstr = " ".join(pkgs)
    if 'usb' == bus:
        usbdb = isenkram.usb.usbdb()
        usbinfo = usbdb.product(vendor, device)
        usbdb = None
        if usbinfo['vendorname'] is None:
            usbinfo['vendorname']  = 'N/A'
        if usbinfo['productname'] is None:
            usbinfo['productname']  = 'N/A'

        title = "New %s device inserted" % bus
        text = "%s from vendor %s [%04x:%04x] supported by package(s) %s." \
            % (usbinfo['productname'], usbinfo['vendorname'],
               vendor, device, pkgstr)
    else:
        title = "New %s device inserted" % bus
        text = "Device [%04x:%04x] supported by package(s) %s." \
            % (vendor, device, pkgstr)

    print("info: " + text)

    # Initializite pynotify
    if not Notify.init("isenkramd"):
        return False
    global n
    global npkgs
    npkgs = pkgs
    n = Notify.Notification(summary=title, body=text)
    n.set_timeout(10000)
    n.add_action("clicked",
                 "Install program(s)",
                 notify_pleaseinstall, npkgs)
    n.show()
    return True

def is_pkg_installed(packagename):
    cmd = ["dpkg", "-l", packagename]
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    retval = False
    while True:
        retcode = p.poll()
        line = p.stdout.readline()
        if 0 == line.find(b"ii "):
            retval = True
        if(retcode is not None):
            break
    return retval

def devid2modalias(bus, vendor, device):
    target = "%s:v%04xp%04xd*" % (bus, vendor, device)
    modalias = None
    for filename in glob.iglob("/sys/bus/%s/devices/*/modalias" % bus):
        f = open(filename)
        line = f.readline().strip()
#        print line, target
        if fnmatch.fnmatch(line, target):
            modalias = line
#            print filename
        f.close()
    return modalias

def get_pkg_suggestions_aptmodaliases(modalias):
    print("info: checking appstream modaliases info")
    return isenkram.lookup.pkgs_handling_appstream_modaliases([modalias])

def get_pkg_suggestions_mymodaliases(modalias):
    print("info: checking my modaliases file (from svn)")
    return isenkram.lookup.pkgs_handling_extra_modaliases([modalias])

def get_pkg_suggestions(modalias):
    pkgs = []
#    discoverpkgs = get_pkg_suggestions_discover(bus, vendor, device)
#    pkgs.extend(discoverpkgs)
    aptpkgs = get_pkg_suggestions_aptmodaliases(modalias)
    pkgs.extend(aptpkgs)
    mypkgs = get_pkg_suggestions_mymodaliases(modalias)
    pkgs.extend(mypkgs)
    # Avoid duplicates in package list
    phash = {}
    for p in pkgs:
        phash[p] = 1
    pkgs = list(phash.keys())
    return sorted(pkgs)

def uevent_callback(client, action, device, user_data):
    modalias = device.get_property("MODALIAS")
    # Map loaded kernel modules to lkmodule:modulename "modalias"
    if ("add" == action and "module" == device.get_subsystem()):
        modalias = "lkmodule:%s" % (device.get_property("DEVPATH").split("/"))[2]
    if ("add" == action and modalias is not None):
        device_vendor = device.get_property("ID_VENDOR_ENC")
        device_model = device.get_property("ID_MODEL_ENC")
        print("uevent %s %s %s" % (device_vendor, device_model, modalias))
        bus = device.get_subsystem()
        if "usb" == bus:
            print("info: discovered USB device %s %s" % (device_vendor,
                                                         device_model))
            pkgs = get_pkg_suggestions(modalias)
#                print "Suggestions: ", pkgs
            newpkg = []
            alreadyinstalled = []
            for pkg in pkgs:
                if not is_pkg_installed(pkg):
                    newpkg.append(pkg)
                else:
                    alreadyinstalled.append(pkg)
            print("info: not proposing already installed package(s) %s" % \
                  ', '.join(alreadyinstalled))
            if 0 < len(newpkg):
                vendorid, deviceid, bcdevice = \
                    device.get_property("PRODUCT").split("/")
                notify(bus, int(vendorid, 16), int(deviceid, 16), newpkg)

def restart_daemon(monitor, file1, file2, evt_type):
    if (evt_type == Gio.FileMonitorEvent.CHANGED):
        scriptname = file1.get_path()
        os.execl(scriptname, scriptname)

def main():
    client = GUdev.Client(subsystems=[])
    client.connect("uevent", uevent_callback, None)

    # Restart daemon if the file on disk changes, to handle package upgrades
    scriptname = os.path.realpath(__file__)
    gfile = Gio.File.new_for_path(scriptname)
    monitor = gfile.monitor_file(Gio.FileMonitorFlags.NONE, None)
    monitor.connect("changed", restart_daemon)

    loop = GLib.MainLoop()
    print("info: ready to accept hardware events")
    loop.run()

if __name__ == '__main__':
    main()
