# -*- 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.
#
# Authors: Alessandro Decina <alessandro@fluendo.com>,
# Guillaume Emont <guillaume@fluendo.com>

# FIXME: use CdromDisableDigitalPlayback() and friends to enable/disable cd/dvd
# autoplay, see if we can do the same for usb sticks/ipods

"""
WMDResource
"""

import os

import win32file
import win32api
import win32con
import winioctlcon
import winerror
from win32gui_struct import UnpackDEV_BROADCAST
import pywintypes

import struct
import gobject

from twisted.internet import threads
from elisa.core.utils import defer

from elisa.core.components.resource_provider import ResourceProvider
from elisa.core.utils.i18n import install_translation
from elisa.core import common

from elisa.plugins.base.messages.device import NewDeviceDetected, NewUnknownDevice,\
        DeviceRemoved
from elisa.plugins.base.models.device import DevicesModel, VolumeModel
from elisa.core.media_uri import MediaUri

from elisa.plugins.pigment.message import PigmentFrontendLoadedMessage

_ = install_translation('wmd')

MAX_DRIVES = 26

class WMDVolumeModel(VolumeModel):
    """
    Volume model Win32 platform specific.

    As of now it implements the 'eject' method.
    """
    def __init__(self, udi=None):
        super(WMDVolumeModel, self).__init__(udi)
        self.mount_point = None
        self.protocol = "file"

    def eject(self):
        #FIXME: this is blocking
        uri = MediaUri({'scheme': 'dvd',
                        'host': self.device,
                        'path': ''})

        fmt = ""

        size = struct.calcsize(fmt)
        name = '\\\\.\\' + uri[-2:-1] + ':'

        volume_handle = win32file.CreateFile(name, win32con.GENERIC_READ,
                                             win32con.FILE_SHARE_READ, None,
                                             win32con.OPEN_EXISTING, 0, 0)
        result = win32file.DeviceIoControl(volume_handle,
                                           winioctlcon.IOCTL_STORAGE_EJECT_MEDIA,
                                           None, size, None)
        win32file.CloseHandle(volume_handle)

        res = struct.unpack(fmt, result)
        return defer.succeed(None)

class WMDResource(ResourceProvider):
    """
    WMD resource provider. Provides volumes:// URI and sends messages on the
    bus when devices are plugged or unplugged (devices include CDs and DVDs).
    """
    supported_uri='^volumes://'

    def initialize(self):
        bus = common.application.bus
        bus.register(self._frontend_loaded, PigmentFrontendLoadedMessage)
        self._dfr_list = []
        self._signal_handler_id = None
        return super(WMDResource, self).initialize()

    def clean(self):
        self.debug('cleaning up')
        if self._signal_handler_id != None:
            gobject.disconnect(self._signal_handler_id)
            self._signal_handler_id = None

        dfr = super(WMDResource, self).clean()

        if self._dfr_list != []:
            dfr_list = defer.DeferredList(self._dfr_list)
            dfr.chainDeferred(dfr_list)

        return dfr

    def _frontend_loaded(self, message, frontend):
        frontend.windows_msg_handler.add_wndproc(win32con.WM_DEVICECHANGE,
                                                 self._on_device_change)

    def get(self, uri, context_model=None):
        """
        Simple method to retrieve volumes. You can access it with
        C{volumes://} and apply a filter parameter. If you specify it
        only these kind of volumes show up. Example::

        ::

          C{volumes://localhost/?filter=dvd,cdda}

        would only give you dvds and cddas. The three knows filters
        are: dvd, removable and cdda.

        The default is that all filters are applied (like
        filter=dvd,cdda,removable).

        In return you get a L{elisa.plugins.base.models.device.DevicesModel}
        """

        filters = uri.get_param('filter', 'dvd,cdda,removable').split(',')

        model = DevicesModel()
        return model, threads.deferToThread(self._get_impl, filters, model)

    def _get_impl(self, filters, model):
        mask = win32api.GetLogicalDrives()
        units = self._drive_units_from_mask(mask)

        for unit in units:
            volume_info = self._get_volume_info(unit)
            if volume_info == None:
                continue
            protocol = self._find_protocol(unit, volume_info)
            if self._filter(unit, protocol, filters):
                device_model = self._create_model_from_unit(unit, protocol, volume_info)
                model.devices.append(device_model)

        return model
             
    def _udi_from_unit(self, unit):
        # quite simplistic, but it works.
        return 'volume:' + unit

    def _create_model_from_unit(self, unit, protocol, volume_info):
        udi = self._udi_from_unit(unit)
        model = WMDVolumeModel(udi)
        model.device = unit
        model.mount_point = unit + '\\'
        model.protocol = protocol
        if protocol == 'ipod':
            model.name = 'iPod'
        else:
            model.name = volume_info[0]
        if model.name == '':
            model.name = _("Volume (%(name)s)") % {'name': unit}

        return model

    # Hackish way of finding whether a unit is an audio cd
    def _is_audio_cd(self, unit):
        path = unit + '\\'
        files = os.listdir(path)
        if files == []:
            return False
        return files[0].endswith('.cda')

    def _find_protocol(self, unit, volume_info):
        type = win32file.GetDriveType(unit)

        if type == win32con.DRIVE_CDROM:
            if volume_info[4] == "UDF" \
                    or os.path.exists(os.path.join(unit, '\\VIDEO_TS')):
                return 'dvd'
            elif self._is_audio_cd(unit):
                return 'cdda'
        elif os.path.exists(os.path.join(unit, '\\iPod_Control')):
            return 'ipod'

        return 'file'

    # We need that to know if a device can be removed (DeviceHotplug field),
    # since GetDriveType() returns DRIVE_FIXED for USB hard drives.
    def _get_hotplug_info(self, unit):
        """
        Get the hotplug info for the unit from its device driver.
        """

        # typedef struct _STORAGE_HOTPLUG_INFO {
        #   UINT Size;
        #   UCHAR MediaRemovable;
        #   UCHAR MediaHotplug;
        #   UCHAR DeviceHotplug;
        #   UCHAR WriteCacheEnableOverride;
        # } STORAGE_HOTPLUG_INFO; 
        hotplug_info_fmt = "IBBBB"

        result = \
            self._device_io_control(winioctlcon.IOCTL_STORAGE_GET_HOTPLUG_INFO,
                                    unit, hotplug_info_fmt)
        return {'MediaRemovable': bool(result[1]),
                'MediaHotplug': bool(result[2]),
                'DeviceHotplug': bool(result[3])}

    def _device_io_control(self, ioctl, unit, fmt):
        """
        Send a control code to the driver handling unit, getting a result in
        the format specified by C{fmt}.
        """

        size = struct.calcsize(fmt)
        name = '\\\\.\\' + unit

        volume_handle = win32file.CreateFile(name, 0,
                win32con.FILE_SHARE_READ| win32con.FILE_SHARE_WRITE,
                None, win32con.OPEN_EXISTING, 0, None)

        if volume_handle == win32file.INVALID_HANDLE_VALUE: #cannot open the drive
            return False

        result = win32file.DeviceIoControl(volume_handle, ioctl,
                                           None, size, None)
        win32file.CloseHandle(volume_handle)

        return struct.unpack(fmt, result)

    def _is_removable(self, unit):
        type = win32file.GetDriveType(unit)
        if type in (win32con.DRIVE_REMOVABLE, win32con.DRIVE_CDROM):
            return True

        # FIXME: This is a hack we use for mapped network drives because using
        # win32file.DeviceIoControl to determinate hotplug information for 
        # mapped drives returns error. Description on MSDN homepage 
        #(http://msdn.microsoft.com/en-us/library/aa363147(VS.85).aspx) states 
        # that DeviceIoControl "retrieve information about, a floppy disk drive,
        # hard disk drive, tape drive, or CD-ROM drive." One should look close
        # to this problem and probably redesign it to make it work.
        if type == win32con.DRIVE_REMOTE:
            return False

        hotplug_info = self._get_hotplug_info(unit)
        return hotplug_info['DeviceHotplug']

    def _filter(self, unit, protocol, filters):
        if protocol in filters:
            return True

        if 'removable' in filters:
            if protocol == 'file':
                return self._is_removable(unit)

        return False

    def _get_volume_info(self, unit):
        # for some reason, the volume name works better than just the unit 
        try:
            path = win32file.GetVolumeNameForVolumeMountPoint(unit + '\\')
        except pywintypes.error, exception:
            if exception[0] in (winerror.ERROR_INVALID_PARAMETER,
                                winerror.ERROR_PATH_NOT_FOUND):
                self.warning('GetVolumeNameForVolumeMountPoint(%s\\) '\
                        'returned %s, using %s\\ as volume name' \
                        % (unit, exception, unit))
                path = unit + '\\'
            else:
                raise
        try:
            return win32api.GetVolumeInformation(path)
        except pywintypes.error, exception:
            if exception[0] in (winerror.ERROR_NOT_READY,
                                winerror.ERROR_UNRECOGNIZED_VOLUME,
                                winerror.ERROR_INVALID_PARAMETER,
                                winerror.ERROR_INVALID_HANDLE):
                return None
            else:
                raise

    def _drive_units_from_mask(self, mask):
        test_mask = 1
        letter_num = ord('A')
        drives = []
        for i in xrange(MAX_DRIVES):
            if test_mask & mask:
                drives.append(chr(letter_num) + ':')
            test_mask <<= 1
            letter_num += 1
        return drives

    def _on_device_change(self, viewport, event):
        info = UnpackDEV_BROADCAST(event.lparam)
        if (event.wparam != win32con.DBT_DEVICEARRIVAL \
                and event.wparam != win32con.DBT_DEVICEREMOVECOMPLETE) \
            or info.devicetype != win32con.DBT_DEVTYP_VOLUME:
            return True

        self.debug('on device change')

        units = self._drive_units_from_mask(info.unitmask)

        if event.wparam == win32con.DBT_DEVICEARRIVAL:
            dfr = threads.deferToThread(self._send_new_devices, units)

            def send_messages(messages):
                for message in messages:
                    common.application.bus.send_message(message)

            dfr.addCallback(send_messages)

            def remove_deferred(result):
                self._dfr_list.remove(dfr)
                return result

            self._dfr_list.append(dfr)

            dfr.addBoth(remove_deferred)

        else: # DBT_DEVICEREMOVECOMPLETE
            self._send_removed_devices(units)
        return True

    def _send_new_devices(self, units):
        messages = []
        for unit in units:
            volume_info = self._get_volume_info(unit)
            if volume_info != None:
                protocol = self._find_protocol(unit, volume_info)
                model = self._create_model_from_unit(unit, protocol, volume_info)
                message = NewDeviceDetected(model.udi)
                message.model = model
            else:
                udi = self._udi_from_unit(unit)
                message = NewUnknownDevice(udi)

            messages.append(message)
        return messages

    def _send_removed_devices(self, units):
        for unit in units:
            udi = self._udi_from_unit(unit)
            message = DeviceRemoved(udi)
            common.application.bus.send_message(message)

def main():
    from elisa.core.media_uri import MediaUri
    from twisted.internet import glib2reactor
    glib2reactor.install()
    class Test(object):
        def component_created(self, component):
            def print_devices(devices):
                for device in devices.devices:
                    print device.__dict__
            devices, dfr = component.get(MediaUri('volumes://localhost?filter=removable,dvd,cdda'))
            dfr.addCallback(print_devices)

        def run(self):
            from twisted.internet import reactor

            dfr = WMDResource.create({})
            dfr.addCallback(self.component_created)
            reactor.run()

    class App(object):
        class Bus(object):
            def register(self, *args):
                pass
            def send_message(self, msg):
                if isinstance(msg, DeviceRemoved):
                    print "received a DeviceRemoved for udi=%s" % msg.udi
                elif isinstance(msg, NewDeviceDetected):
                    print "received a NewDeviceDetected with model:", msg.model.__dict__
                elif isinstance(msg, NewUnknownDevice):
                    print "received a NewUnknownDevice for udi=%s" % msg.udi
                else:
                    print "received a", type(msg)

        bus = Bus()

    common.application = App()
    Test().run()

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