# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-2009 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 Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.
#
# Author: Guillaume Emont <guillaume@fluendo.com>
"""
Helping classes to handle HAL in Elisa.

@var  protocol_tests: The list of ProtocolTest instances to use.
@type protocol_tests: C{list}
"""

import os
import dbus

from elisa.core.utils import defer
from elisa.core.utils.defer import Deferred
from elisa.plugins.base.models.device import DeviceModel, VolumeModel
from elisa.plugins.hal.dbus_helper import call_async_dbus_method as dbus_call
from elisa.plugins.hal.dbus_helper import ElisaDBusException, dbus_exceptions

from twisted.internet import reactor

class HALDeviceModel(DeviceModel):
    """
    HAL based implementation of DeviceModel.

    As of now it implements the 'eject' method.

    @ivar hal_device:  HAL device abstraction corresponding to this device model
    @type hal_device:  L{elisa.plugins.hal.hal.HALDevice}
    """
    def __init__(self, udi=None, hal_device=None):
        """
        @param hal_device: HAL device abstraction corresponding to this device
                           model
        @type hal_device:  L{elisa.plugins.hal.hal.HALDevice}
        """
        super(HALDeviceModel, self).__init__(udi)
        self.hal_device = hal_device

    def eject(self):
        return self.hal_device.eject()

class HALVolumeModel(VolumeModel):
    """
    HAL based implementation of VolumeModel.

    Does not provide any functionality for now, only have a reference to the
    underlying HALDevice in .device.
    """
    def __init__(self, udi=None, hal_device=None):
        """
        @param hal_device: HAL device abstraction corresponding to this device
                           model
        @type hal_device:  L{elisa.plugins.hal.hal.HALDevice}
        """
        super(HALVolumeModel, self).__init__(udi)
        self.hal_device = hal_device

class HALException(ElisaDBusException):
    pass

class HALPermissionDenied(HALException):
    dbus_name = 'org.freedesktop.Hal.PermissionDenied'
    pass

class HALNoSuchDevice(HALException):
    dbus_name = 'org.freedesktop.Hal.NoSuchDevice'
    pass

for exception in (HALPermissionDenied, HALNoSuchDevice):
    dbus_exceptions[exception.dbus_name] = exception

class ProtocolTest(object):
    """
    Class to test if a HALDevice can be considered to be of a protocol.

    @ivar protocol: the protocol for which this instance tests
    @type protocol: C{str}
    @ivar hal_properties: properties to test
    @type hal_properties: C{tuple}
    @ivar ModelClass: the model type associated with the protocol
    @type ModelClass: C{classobj}
    """
    def __init__(self, protocol, hal_properties, model_class=HALDeviceModel):
        self.protocol = protocol
        if type(hal_properties) == str:
            self.hal_properties = (hal_properties,)
        else:
            self.hal_properties = hal_properties
        self.ModelClass = model_class

    def has_protocol(self, hal_device):
        """
        Returns whether L{hal_device} represents a device of
        self.protocol.
        """
        for prop in self.hal_properties:
            if hal_device.properties.get(prop, False):
                return True
        return False

class IpodProtocolTest(ProtocolTest):
    """
    Specific ProtocolTest for the ipod protocol.
    """

    def __init__(self):
        parent = super(IpodProtocolTest, self)
        parent.__init__('ipod', None, HALVolumeModel)

    def has_protocol(self, hal_device):
        protocols_key = 'portable_audio_player.access_method.protocols'
        protocols = hal_device.parent_properties.get(protocols_key, ())
        if 'ipod' in protocols:
            return True
        return False

# These must be ordered from most specific to most generic
protocol_tests = [
        IpodProtocolTest(),
        ProtocolTest('cdda', 'volume.disc.has_audio'),
        ProtocolTest('dvd',  'volume.disc.is_videodvd'),
        ProtocolTest('file', ('volume.disc.is_partition',
                              'volume.is_partition',
                              'volume.disc.has_data',
                              'block.is_volume'),
                     HALVolumeModel),
]


class HALDevice(object):
    """
    Represents an easy wrapper for a HAL device, its properties and parent
    properties.

    @ivar udi: The unique device identifier
    @type udi: C{str}
    @ivar interface: HAL interface for this device
    @type interface: C{dbus.proxies.Interface} -> C{org.Freedesktop.Hal.Device}
    @ivar properties: The HAL properties of this device
    @type properties: C{dict}
    @ivar parent_properties: The HAL properties of the parent device of this device
    @type parent_properties: C{dict}
    @ivar protocol_test: the protocol test that matched for this device
    @type protocol_test: ProtocolTest
    """

    def __init__(self, udi, bus):
        """
        Don't use this directly, use create() instead, or your device will
        be incomplete.
        """
        self.udi = str(udi)
        self.interface = self._get_interface_from_udi(udi, bus)
        self.volume_interface = self._get_volume_interface_from_udi(udi, bus)
        self.properties = None
        self.parent_properties = None
        self.protocol_test = None
        
    @classmethod
    def create(klass, udi, bus):
        """
        Returns a deferred that will return a new HALDevice instance.
        """
        device = klass(udi, bus)

        def on_properties(properties):
            device.properties = properties
            parent_udi = properties['info.parent']
            parent_interface = klass._get_interface_from_udi(parent_udi, bus)
            return dbus_call(parent_interface.GetAllProperties)

        def on_parent_properties(parent_properties):
            device.parent_properties = parent_properties
            device.protocol_test = device._find_protocol_test()
            return device

        dfr = dbus_call(device.interface.GetAllProperties)
        dfr.addCallback(on_properties)
        dfr.addCallback(on_parent_properties)

        return dfr

    def needs_mounting(self):
        return self.properties.get('volume.is_mounted', None) == False

    def wait_for_mount(self, timeout):
        """
        Returns a deferred that will be fired when a mount_point is set, or in
        timeout seconds, whichever happens first. This should not be called
        more than once in the lifetime of a HALDevice.
        """
        dfr = Deferred()

        property_modified_conn = None
        firing = False

        def fire_anyway():
            if property_modified_conn != None:
                property_modified_conn.remove()
            if not firing:
                dfr.callback(None)

        delayed_call = reactor.callLater(timeout, fire_anyway)

        def update_mount_point(mount_point):
            self.properties['volume.mount_point'] = mount_point

        def property_modified_cb(num_changes, modified_props):
            for (property, added, removed) in modified_props:
                if property == 'volume.mount_point':
                    firing = True
                    if delayed_call.active():
                        delayed_call.cancel()
                    get_prop_dfr = dbus_call(self.interface.GetProperty,
                                             'volume.mount_point')
                    get_prop_dfr.addCallback(update_mount_point)
                    get_prop_dfr.chainDeferred(dfr)

        property_modified_conn = \
            self.interface.connect_to_signal('PropertyModified',
                                             property_modified_cb)
        
        return dfr

    def is_ignored(self):
        return self.properties.get('volume.ignore', False)

    def is_removable(self):
        if self.protocol_test.protocol in ('dvd', 'cdda'):
            return False

        return self.parent_properties.get('storage.removable', False) \
            or self.parent_properties.get('storage.hotpluggable', False)

    def filter(self, filters):
        if self.is_ignored():
            return False

        if not self.protocol_test:
            # no protocol_test matched, unknown protocol
            return False

        if self.protocol_test.protocol in filters:
            return True

        if 'removable' in filters:
            return self.is_removable()

        return False

    def create_model(self):
        model = self.protocol_test.ModelClass(self.udi, hal_device=self)
        model.protocol = self.protocol_test.protocol

        if self.properties.has_key('block.device'):
            model.device = unicode(self.properties['block.device'])

        name = self.properties.get('volume.label', '')
        if name == '':
            name = self.properties.get('info.product')
        model.name = unicode(name)

        if isinstance(model, VolumeModel):
            mount_point = self.properties.get('volume.mount_point', None)
            if mount_point:
                model.mount_point = str(mount_point)

        return model

    def is_known_device(self):
        return self.protocol_test != None

    def _find_protocol_test(self):
        for protocol_test in protocol_tests:
            if protocol_test.has_protocol(self):
                return protocol_test
        return None

    @classmethod
    def _get_interface_from_udi(klass, udi, bus):
        obj = bus.get_object('org.freedesktop.Hal', udi)
        return dbus.Interface(obj, 'org.freedesktop.Hal.Device')

    @classmethod
    def _get_volume_interface_from_udi(klass, udi, bus):
        obj = bus.get_object('org.freedesktop.Hal', udi)
        return dbus.Interface(obj, 'org.freedesktop.Hal.Device.Volume')

    def eject(self):
        """
        Eject the associated HAL device.

        Return a deferred fired when ejection is complete.

        @rtype:  L{elisa.core.utils.defer.Deferred}
        """
        # FIXME: Calling the 'Eject' method on the org.freedesktop.Hal.Device.Volume
        # interface is the right way to eject the device but for now it requires
        # more investigation to get it working.
        #get_prop_dfr = dbus_call(self.volume_interface.Eject,[''])
        #FIXME: this is blocking

        os.system("eject " + self.properties.get('volume.mount_point', None))
        return defer.succeed(None)
