# -*- coding: utf-8 -*-
# Elisa - Home multimedia server
# Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Elisa with Fluendo's plugins.
#
# The GPL part of Elisa is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Elisa" in the root directory of this distribution package
# for details on that license.

"""
HalServiceProvider component class
"""


__maintainer__ = 'Philippe Normand <philippe@fluendo.com>'
__maintainer2__ = 'Benjamin Kampmann <benjamin@fluendo.com>'


from elisa.base_components.service_provider import ServiceProvider
from elisa.core.application import ComponentsLoadedMessage
from elisa.core import common
from elisa.core import component
import os, time, platform
from twisted.internet import threads

from elisa.extern.translation import gettexter, N_
T_ = gettexter('elisa-hal-service')

plugin_registry = common.application.plugin_registry
DeviceActionMessage = plugin_registry.get_component_class('base:device_action_message')

import dbus
if getattr(dbus, 'version', (0,0,0)) >= (0,41,0):
    import dbus.glib
from dbus.exceptions import DBusException

try:
    import gconf
except ImportError:
    gconf = None

class NotMountable(Exception):
    pass

def get_dbus_error_message(exception):
    """
    Retrieve the message of the DBus error. Usually something like
    'org.freedesktop....'

    @param exception: the DBus exception to analyze
    @type exception:  L{dbus.DBusException}
    @rtype:           string
    """
    if dbus.version < (0, 82, 0):
        msg = exception.message
    else:
        msg = "%s: %s" % (exception._dbus_error_name, exception.message)
    return msg


class HalService(ServiceProvider):
    """
    This class implements HAL support
    """

    def __init__(self):
        ServiceProvider.__init__(self)
        self._hotplugged_devices = {}

    def initialize(self):
        ServiceProvider.initialize(self)
        bus = common.application.bus
        bus.register(self._components_loaded, ComponentsLoadedMessage)
        bus.register(self._device_action, DeviceActionMessage)

        try:
            self.bus = dbus.SystemBus()
            self.ses = dbus.SessionBus()
        except DBusException, error:
            msg = "DBus is not running."
            raise component.InitializeFailure(self.name, msg)

        try:
            self.hal_manager = self.bus.get_object('org.freedesktop.Hal',
                                                   '/org/freedesktop/Hal/Manager')
        except DBusException, error:
            self.warning(error)
            msg = "HAL is not installed or might not be running."
            raise component.InitializeFailure(self.name, msg)
        
        self.hal_iface = dbus.Interface(self.hal_manager,
                                        'org.freedesktop.Hal.Manager')

    def _device_action(self, msg, sender):
        if msg.action == DeviceActionMessage.ActionType.EJECT:
            for udi, infos in self._hotplugged_devices.iteritems():
                (name, fstype, mount_point) = infos
                if fstype == msg.fstype and mount_point == msg.mount_point:
                    device = self.get_device_with_udi(udi)
                    self.eject_device(device)
                    break
                
    def _components_loaded(self, msg, sender):
        self.detect_coldplugged()
        self.hal_iface.connect_to_signal('DeviceAdded',
                                         self.device_added_callback)
        self.hal_iface.connect_to_signal('DeviceRemoved',
                                         self.device_removed_callback)

    def start(self):
        self.stop_volume_manager_monitoring()

    def stop(self):
        self.restore_volume_manager_monitoring()

    def detect_coldplugged(self):
        udis = self.hal_iface.FindDeviceByCapability('volume')

        for udi in udis:
            # get a hal object for the volume referenced by udi
            volume = self.get_device_with_udi(udi)
            parent_uri = volume.GetProperty('info.parent')
            parent = self.get_device_with_udi(parent_uri)

            if (parent.GetProperty('storage.removable') \
                or parent.GetProperty('storage.removable.media_available')) \
                and (parent.GetProperty('storage.hotpluggable') or
                     volume.GetProperty('volume.is_disc')):
                # if its parent is removable and hotpluggable or the volume
                # is a disc (DVD or CD) then the volume has been coldplugged
                self.device_added_callback(udi)
            
    def stop_volume_manager_monitoring(self):
        """
        Neutralize some of the volume_manager monitoring settings so
        that the user won't see rhythmbox pop up when an iPod is
        inserted (for example).
        """
        if gconf:
            client = gconf.client_get_default()
            path = '/desktop/gnome/volume_manager'
            autoplay_cda = client.get_bool('%s/autoplay_cda' % path)
            autoplay_dvd = client.get_bool('%s/autoplay_dvd' % path)
            autobrowse = client.get_bool('%s/autobrowse' % path)
            self.volume_manager_config = {'autoplay_cda': autoplay_cda,
                                          'autoplay_dvd': autoplay_dvd,
                                          'autobrowse': autobrowse}
            for prop in self.volume_manager_config.keys():
                client.set_bool('%s/%s' % (path, prop), False)

    def restore_volume_manager_monitoring(self):
        """
        Restore the volume_manager gconf settings
        """
        if gconf:
            client = gconf.client_get_default()
            path = '/desktop/gnome/volume_manager'
            for prop, value in self.volume_manager_config.iteritems():
                client.set_bool('%s/%s' % (path, prop), value)

    def get_device_with_udi(self, udi):
        obj = self.bus.get_object('org.freedesktop.Hal', udi)
        if obj != None:
            device = dbus.Interface(obj, 'org.freedesktop.Hal.Device')
            return device
        return None

    def parent_is_ipod(self, device):
        parent_udi = device.GetProperty(u'info.parent')
        parent = self.get_device_with_udi(parent_udi)
        if not parent.PropertyExists(u'portable_audio_player.type'):
            return False
        return parent.GetProperty(u'portable_audio_player.type').lower() == 'ipod'
        # Is it intelligent to try to get the portable_audio_player.output_formats
        # too for accessing in audio or video?

    def eject_device(self, device):
        name = device.GetProperty(u'info.product')
        self.debug("ejecting: %r", name)
        interface = 'org.freedesktop.Hal.Device.Storage'
        method = device.get_dbus_method('Eject', dbus_interface=interface)

        try:
            method([])
        except ValueError:
            # not exposed on HAL DBus API
            
            block_device = device.GetProperty(u'block.device')
            # TODO: find something more cross-platform
            threads.deferToThread(os.system, "eject %s" % block_device)
                

    def mount_device(self, device):
        ignore = self._get_property(device, u'volume.ignore', False)
        
        if ignore:
            self.debug("volume.ignore property set on %r, can't mount it",
                       device)
            return

        prop_name = u'org.freedesktop.Hal.Device.Volume.method_names'
        method_names = self._get_property(device, prop_name, [])
        
        if 'Mount' not in method_names:
            raise NotMountable()

        interface = 'org.freedesktop.Hal.Device.Volume'
        method = device.get_dbus_method('Mount', dbus_interface=interface)

        name = device.GetProperty(u'info.product')
        self.debug("mounting: %r ", name)
        
        try:
            # Let's HAL decide, what the mount point should be
            method('', device.GetProperty(u'volume.fstype'),[])
        except DBusException, exc:
            already_mounted = 'org.freedesktop.Hal.Device.Volume.AlreadyMounted'
            unavailable = 'org.freedesktop.Hal.Device.Volume.MountPointNotAvailable'
            permission_denied = 'org.freedesktop.Hal.Device.Volume.PermissionDenied'
            
            msg = get_dbus_error_message(exc)
            
            if msg.startswith(already_mounted):
                self.info("Already mounted")
            elif msg.startswith(permission_denied):
                idx = msg.index(permission_denied) + len(permission_denied) + 2
                device_error = msg[idx:]
                self.info("Permission denied: %s", device_error)
            elif msg.startswith(unavailable):
                return None
            else:
                raise

        mount_point = self._get_mount_point(device)
        return mount_point

    def _get_mount_point(self, device):
        mount_point = device.GetProperty(u'volume.mount_point')
        name = device.GetProperty(u'info.product')
        if not mount_point:
            if platform.system() == 'Linux':
                # this property is not supported by upstream HAL
                # and seems specific to Ubuntu distro as stated there:
                # http://lists.freedesktop.org/archives/hal/2007-April/008062.html
                # FIXME: this needs further investigation. linux.* properties
                #        should not be used.
                mount_point = self._get_property(device,
                                                 'linux.fstab.mountpoint','')
                
        self.debug("mount point of %r: %r", name, mount_point)
        return mount_point

    def _get_property(self, device, prop_name, default):
        value = default
        if device.PropertyExists(prop_name):
            value = device.GetProperty(prop_name)
        return value
        
    
    def device_added_callback(self, udi):
        mount_point_prop = u'volume.label'

        if udi not in self._hotplugged_devices:
            device = self.get_device_with_udi(udi)
            if device.QueryCapability("volume"):
                try:
                    name = str(device.GetProperty('info.product'))
                except dbus.DBusException:
                    name = ''

                if self.parent_is_ipod(device):
                    fstype = 'ipod'
                    try:
                        point = self.mount_device(device)
                        # we support audio-only iPod for now
                        self.new_volume_cb(udi, name, fstype, point,
                                           ['audio',], True, 'ipod')
                    except NotMountable, exception:
                        pass
                else:

                    parent_udi = device.GetProperty(u'info.parent')
                    parent = self.get_device_with_udi(parent_udi)
                    
                    removable = self._get_property(parent, u'storage.removable',
                                                   False)
                    
                    dvd = self._get_property(device, 'volume.disc.is_videodvd',
                                             False)
                    audio_cd = self._get_property(device,
                                                  'volume.disc.has_audio',
                                                  False)
                    disc_is_partition = self._get_property(device,
                                                           'volume.disc.is_partition',
                                                           False)
                    is_partition = self._get_property(device,
                                                      'volume.is_partition',
                                                      False)
                    is_partition = disc_is_partition or is_partition
                    
                    has_data = self._get_property(device,
                                                  'volume.disc.has_data', False)

                    self.debug("name: %r, dvd: %r, audio_cd: %r, partition: %r "\
                               "has_data: %r, removable: %r", name, dvd,
                               audio_cd, is_partition, has_data, removable)
                    if dvd:
                        if not name:
                            name = T_(N_('DVD'))
                        self.new_volume_cb(udi, name, 'dvd', '', ['video',],
                                           removable, 'dvd')
                    elif audio_cd:
                        if not name:
                            name = T_(N_('Audio CD'))
                        fstype = 'cdda'
                        # FIXME: We are currently not supporting different
                        # devices. So the name is not sent!
                        self.new_volume_cb(udi, name, fstype,'', ['audio',],
                                           removable, 'audiocd')
                    elif is_partition or has_data or removable:
                        fstype = 'file'
                        if not name:
                            name = T_(N_('File device'))
                        try:
                            point = self.mount_device(device)
                        except NotMountable, exc:
                            pass
                        else:
                            self.new_volume_cb(udi,name, fstype, point,
                                               ['audio', 'video', 'image'],
                                               removable, 'usb_storage')

    def device_removed_callback(self, uid):
        if uid in self._hotplugged_devices:
            name, fstype, mount_point = self._hotplugged_devices[uid]
            del self._hotplugged_devices[uid]
            if self.del_volume_cb:
                if mount_point:
                    self.del_volume_cb(name, fstype, mount_point)
                if fstype == 'cdda':
                    # FIXME: add support for AudioCD removal
                    self.del_volume_cb(T_(N_('Audio CD')),'cdda','')
                elif fstype == 'dvd':
                    self.del_volume_cb(T_(N_('DVD')), 'dvd', '')
                    
    def new_volume_cb(self, uid, name, fstype, mount_point, media_types=None,
                      removable=False, theme_icon=None):
        """
        Called when a new volume has been detected

        @param name:            Name of the device
        @type name:             string
        @param fstype:          Filesystem type
        @type fstype:           string
        @param mount_point:     Mount point
        @type mount_point:      string
        @keyword media_types:   media types that can be stored on the volume
        @type media_types:      list
        @keyword removable:     can the volume be removed?
        @type removable:        bool
        @keyword theme_icon:    the theme_icon the view should use to display
                                this volume
        @type theme_icon:       string
        """
        self._hotplugged_devices[uid] = ( name, fstype, mount_point )
        self.debug("New volume found %r at %r", name, mount_point)

        mount_point = "%s://%s" % (fstype, mount_point)
        msg = DeviceActionMessage(DeviceActionMessage.ActionType.LOCATION_ADDED,
                           name, fstype, mount_point, media_types,
                           removable, theme_icon)
        common.application.bus.send_message(msg)

    def del_volume_cb(self, name, fstype, mount_point):
        """
        Called when volume has been removed

        @param name:            Name of the device
        @type name:             string
        @param fstype:          Filesystem type
        @type fstype:           string
        @param mount_point:     Mount point
        @type mount_point:      string
        """
        self.debug("Volume unmounted %s" % name)
        mount_point = "%s://%s" % (fstype, mount_point)
        msg = DeviceActionMessage(DeviceActionMessage.ActionType.LOCATION_REMOVED,
                           name, fstype, mount_point)
        common.application.bus.send_message(msg)
