# -*- 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.


"""
Media data access support
"""


__maintainer__ = 'Philippe Normand <philippe@fluendo.com>'


from elisa.core.component import Component
from elisa.extern import enum
from twisted.internet import defer

class UriNotMonitorable(Exception):

    def __init__(self, uri):
        self.uri = uri

    def __str__(self):
        return str(self.uri)

# ADDED:    guess what, a new directory or file was added
# REMOVED:  guess what, a directory or file was removed
# MODIFIED: guess what, a directory or file was updated
NotifyEvent = enum.Enum('ADDED', 'REMOVED', 'MODIFIED')

media_types = ('directory', 'audio', 'video', 'image')

class MediaProvider(Component):
    """
    Medias are all accessible using URIs. MediaProvider components are
    responsible to support one or many URI schemes.

    MediaProviders are able to parse media trees of supported URI(s)
    scheme(s), they can also provide limited information about parsed
    files and directories.

    MediaProviders can optionnally monitor media locations and notify
    registered components of data updates (new/updated/deleted
    files/directories).

    TODO:

      - blocking_{copy,move,delete} ?

    @cvar scannable_uri_schemes: DOCME
    @type scannable_uri_schemes: list
    @cvar supported_uri_schemes: DOCME
    @type supported_uri_schemes: dict mapping string to int

    """

    def scannable_uri_schemes__get(self):
        """
        Retrieve the URI schemes that can be scanned by the
        media_scanner. Since media scanning can be an heavy and long
        task the MediaProvider developer can choose to make the
        media_scanner skip URIs with scheme not appearing in returned
        list.

        By default the return value of this method corresponds to the
        keys of L{supported_uri_schemes__get} return value.

        # FIXME: this should be documented in the class docstring as a class
        # variable
        """
        return self.supported_uri_schemes.keys()

    def supported_uri_schemes__get(self):
        """
        Retrieve the URI schemes supported by the provider, for each
        scheme there's a priority. Higher priority == 0 means the
        provider will always be used to read data from a given scheme.

        This function is called by the MediaManager to know which
        media provider it has to use to access a specified URI. You
        should return a dict containing the uri scheme (such as
        'file', 'cdda', ...) and its priority between 0 to 255 (0
        being the topmost priority).  The prority is used by the
        MediaManager to know which media provider it should use in
        case there are more than one who support the desired uri
        scheme. You might have for example a component which supports
        more than one scheme, but the support of one of them is not
        very efficient compared to what it could be. In this case you
        could modify its priority to tell the MediaManager that
        another component should be used instead of it to access this
        scheme.

        example: { 'file': 0, 'smb': 10 }

        # FIXME: this should be documented in the class docstring as a class
        # variable
        """
        return {}

    def get_real_uri(self, uri):
        """
        Returns the original uri (acesable) from a virtual
        uri representation.

        @param uri:     the URI to validate
        @type uri:      L{elisa.core.media_uri.MediaUri}
        @rtype:         L{elisa.core.media_uri.MediaUri}
        """
        return uri


    def get_media_type(self, uri):
        """
        Same as L{blocking_get_media_type} but without blocking (in
        theory). This method by default triggers a succeeded callback
        on a Twisted deferred, using L{blocking_get_media_type} result.
        
        @param uri: the URI to analyze
        @type uri:  L{elisa.core.media_uri.MediaUri}
        @rtype: L{twisted.internet.defer.Deferred}
        """
        return defer.succeed(self._blocking_get_media_type(uri))

    def _blocking_get_media_type(self, uri):
        """
        Try to guess the maximum information from the media located
        at given uri by looking at eventual file extension. Will
        return something like::

          {'file_type': string (values: one of media_provider.media_types)
           'mime_type': string (example: 'audio/mpeg' for .mp3 uris. can be
                               empty string if unguessable)
           }

        'file_type' and 'mime_type' can be empty strings if it failed
        recognizing them.

        @param uri: the URI to analyze
        @type uri:  L{elisa.core.media_uri.MediaUri}
        @rtype:     dict
        """
        raise NotImplementedError
    
    def is_directory(self, uri):
        """
        Same as L{_blocking_is_directory} but without blocking (in
        theory). This method by default triggers a succeeded callback
        on a Twisted deferred, using L{_blocking_is_directory} result.
        
        @param uri: the URI to analyze
        @type uri:  L{elisa.core.media_uri.MediaUri}
        @rtype:     L{twisted.internet.defer.Deferred}
        """
        return defer.succeed(self._blocking_is_directory(uri))
    
    def _blocking_is_directory(self, uri):
        """
        return True if a directory

        @param uri: the URI to analyze
        @type uri:  L{elisa.core.media_uri.MediaUri}
        @rtype:     bool
        """
        raise NotImplementedError

    def has_children_with_types(self, uri, media_types):
        """
        Same as L{_blocking_has_children_with_types} but without blocking (in
        theory). This method by default triggers a succeeded callback
        on a Twisted deferred, using L{_blocking_has_children_with_types} result.
        
        @param uri:         the URI to scan
        @type uri:          L{elisa.core.media_uri.MediaUri}
        @param media_types: the media_types to look for on the directory
        @type media_types:  list of strings
        @rtype:             L{twisted.internet.defer.Deferred}
        """
        return defer.succeed(self._blocking_has_children_with_types(uri,
                                                                   media_types))
    
    def _blocking_has_children_with_types(self, uri, media_types):
        """
        Detect whether the given uri has children for given media
        types which can be one of media_provider.media_types.
        Implies the URI is a directory as well.

        @param uri:         the URI to scan
        @type uri:          L{elisa.core.media_uri.MediaUri}
        @param media_types: the media_types to look for on the directory
        @type media_types:  list of strings
        @rtype:             bool
        """
        raise NotImplementedError

    def get_direct_children(self, uri, children_with_info):
        """
        Same as L{_blocking_get_direct_children} but without blocking (in
        theory). This method by default triggers a succeeded callback
        on a Twisted deferred, using L{_blocking_get_direct_children} result.
        
        @param uri:                the URI to analyze
        @type uri:                 L{elisa.core.media_uri.MediaUri}
        @param children_with_info: List where the children will be appended
        @type children_with_info:  list
        @rtype:                    L{twisted.internet.defer.Deferred}
        """
        return defer.succeed(self._blocking_get_direct_children(uri,
                                                               children_with_info))
    
    def _blocking_get_direct_children(self, uri, children_with_info):
        """
        Scan the data located at given uri and return informations
        about its children.  Fills children_with_info.

        Typemap of filled result::

          [
             (uri : media_uri.MediaUri,
              additional info: dict),
            ...
          ]

        If you supply additional info, they should be stored in a
        L{elisa.core.observers.dict.DictObservable} instead of a
        normal dictionnary. Valid keys::

          ['default_image', 'artist', 'album', 'song', 'song_artist',
           'song_album']
        

        @param uri:                the URI to analyze
        @type uri:                 L{elisa.core.media_uri.MediaUri}
        @param children_with_info: List where the children will be appended
        @type children_with_info:  list
        @rtype:                    list
        """
        return children_with_info

    def open(self, uri, mode=None):
        """
        Same as L{_blocking_open} but without blocking (in
        theory). This method by default triggers a succeeded callback
        on a Twisted deferred, using L{_blocking_open} result.

        @note: It's not allowed to open directories, it's up to the
        developer to check that the URI to open doesn't represent a
        directory.

        @param uri:     the URI to open
        @type uri:      L{elisa.core.media_uri.MediaUri}
        @keyword mode:  how to open the file -- see manual of builtin open()
        @type mode:     string or None
        @rtype:         L{twisted.internet.defer.Deferred}
        """
        return defer.succeed(self._blocking_open(uri, mode=mode))
    
    def _blocking_open(self, uri, mode=None):
        """
        Open an uri and return MediaFile.

        @note: It's not allowed to open directories, it's up to the
        developer to check that the URI to open doesn't represent a
        directory.


        @param uri:     the URI to open
        @type uri:      L{elisa.core.media_uri.MediaUri}
        @keyword mode:  how to open the file -- see manual of builtin open()
        @type mode:     string or None
        @rtype:         L{elisa.core.media_file.MediaFile}
        """
        raise NotImplementedError

    def close(self, media_file):
        """
        Same as L{_blocking_close} but without blocking (in
        theory). This method by default triggers a succeeded callback
        on a Twisted deferred, using L{_blocking_close} result.

        @param media_file: the file to close
        @type media_file:  L{elisa.core.media_file.MediaFile}
        @rtype:            L{twisted.internet.defer.Deferred}
        """
        return defer.succeed(self._blocking_close(media_file))
        
    def _blocking_close(self, media_file):
        """
        Close a MediaFile

        @param media_file: the file to close
        @type media_file:  L{elisa.core.media_file.MediaFile}
        """
        if media_file:
            media_file.descriptor.close()

    def seek(self, media_file, offset, whence=0):
        """
        Same as L{_blocking_seek} but without blocking (in
        theory). This method by default triggers a succeeded callback
        on a Twisted deferred, using L{_blocking_seek} result.

        @param media_file: the file to seek in
        @type media_file:  L{elisa.core.media_file.MediaFile}
        @param offset:     how many bytes to seek
        @type offset:      int
        @keyword whence:   from where to seek
        @type whence:      int (default=0)
        @rtype:            L{twisted.internet.defer.Deferred}
        """
        return defer.succeed(self._blocking_seek(media_file, offset,
                                                whence=whence))

    def _blocking_seek(self, media_file, offset, whence=0):
        """
        Seek data in a MediaFile

        @param media_file: the file to seek in
        @type media_file:  L{elisa.core.media_file.MediaFile}
        @param offset:     how many bytes to seek
        @type offset:      int
        @keyword whence:   from where to seek
        @type whence:      int (default=0)
        """
        if media_file:
            media_file.descriptor.seek(offset, whence)

    def read(self, media_file, size=-1):
        """
        Same as L{_blocking_seek} but without blocking (in
        theory). This method by default triggers a succeeded callback
        on a Twisted deferred, using L{_blocking_seek} result.

        @param media_file: the file to read data from
        @type media_file:  L{elisa.core.media_file.MediaFile}
        @keyword size:     how many data we should try to read
        @type size:        int (default: -1 == all)
        @rtype:            L{twisted.internet.defer.Deferred}
        """
        return defer.succeed(self._blocking_read(media_file, size=size))

    def _blocking_read(self, media_file, size=-1):
        """
        Read data from a MediaFile

        @param media_file: the file to read data from
        @type media_file:  L{elisa.core.media_file.MediaFile}
        @keyword size:     how many data we should try to read
        @type size:        int (default: -1 == all)
        @rtype:            string
        """
        data = ''
        if media_file:
            if size == -1:
                data = media_file.descriptor.read()
            else:
                data = media_file.descriptor.read(size)
        return data

    def next_location(self, uri, root=None):
        """
        Same as L{_blocking_next_location} but without blocking (in
        theory). This method by default triggers a succeeded callback
        on a Twisted deferred, using L{_blocking_next_location} result.
        
        @param uri:  the URI representing the file or directory from
                     where to move on
        @type uri:   L{elisa.core.media_uri.MediaUri}
        @param root: root URI
        @type root:  L{elisa.core.media_uri.MediaUri}
        @rtype:      L{twisted.internet.defer.Deferred}
        """
        return defer.succeed(self._blocking_next_location(uri, root=root))
    
    def _blocking_next_location(self, uri, root=None):
        """
        Return the uri just next to given uri.

        @param uri:  the URI representing the file or directory from
                     where to move on
        @type uri:   L{elisa.core.media_uri.MediaUri}
        @param root: root URI
        @type root:  L{elisa.core.media_uri.MediaUri}
        @rtype:      L{elisa.core.media_uri.MediaUri}
        """
        raise NotImplementedError

    def previous_location(self, uri):
        """
        Same as L{_blocking_previous_location} but without blocking (in
        theory). This method by default triggers a succeeded callback
        on a Twisted deferred, using L{_blocking_previous_location} result.
        
        @param uri: the URI representing the file or directory prior
                    to uri
        @type uri:  L{elisa.core.media_uri.MediaUri}
        @rtype:     L{twisted.internet.defer.Deferred}
        """
        return defer.succeed(self._blocking_next_location(uri, root=root))

    def _blocking_previous_location(self, uri):
        """
        Return the uri found before given uri

        @param uri: the URI representing the file or directory prior to uri
        @type uri:  L{elisa.core.media_uri.MediaUri}
        @rtype:     L{elisa.core.media_uri.MediaUri}
        """
        raise NotImplementedError

    def monitor_uri(self, uri, callback):
        """
        Start monitoring given uri for modification and call a
        function in case of any change happening on `uri` Raises
        UriNotMonitorable(uri) if uri can't be monitored

        @param uri:      URI representing the file or directory to monitor
        @type uri:       L{elisa.core.media_uri.MediaUri}
        @param callback: a callable taking the event that occured and the uri
                         of the file on which the event applies to
                         prototype: callable(uri, metadata, event)
                         type uri:   L{elisa.core.media_uri.MediaUri}
                         type metadata: dict
                         type event: L{elisa.base_components.media_provider.NotifyEvent}

        @raise UriNotMonitorable : if the uri cannot be monitored
        """
        raise NotImplementedError

    def unmonitor_uri(self, uri):
        """
        Stop monitoring given uri.

        @param uri: the URI representing the file or directory to monitor
        @type uri:  L{elisa.core.media_uri.MediaUri}
        """
        raise NotImplementedError

    def uri_is_monitorable(self, uri):
        """
        Check if the uri is monitorable for modification

        @param uri: the URI representing the file or directory for
                    which we would like to know if it is monitorable or not
        @type uri:  L{elisa.core.media_uri.MediaUri}
        @rtype:     bool
        """
        return False

    def uri_is_monitored(self, uri):
        """
        Check if the uri is currently monitored for modification

        @param uri: the URI representing the file or directory for
                    which we would like to know if it is currently
                    monitored or not
        @type uri:  L{elisa.core.media_uri.MediaUri}
        @rtype:     bool
        """
        raise NotImplementedError

    def copy(self, orig_uri, dest_uri, recursive=False):
        """
        Copy one location to another. If both URIs represent a
        directory and recursive flag is set to True I will recursively
        copy the directory to the destination URI.

        @param orig_uri:  the URI to copy, can represent either a directory or
                          a file
        @type orig_uri:   L{elisa.core.media_uri.MediaUri}
        @param dest_uri:  the destination URI, can represent either a directory
                          or a file
        @type dest_uri:   L{elisa.core.media_uri.MediaUri}
        @param recursive: if orig_uri represents a directory, should I copy it
                          recursively to dest_uri?
        @type recursive:  bool
        @rtype:           L{twisted.internet.defer.Deferred}
        """
        raise NotImplementedError

    def move(self, orig_uri, dest_uri):
        """
        Move data located at given URI to another URI. If orig_uri
        represents a directory it will recusively be moved to
        dest_uri. In the case where orig_uri is a directory, dest_uri
        can't be a file.

        @param orig_uri: the URI to move, can represent either a directory or
                         a file
        @type orig_uri:  L{elisa.core.media_uri.MediaUri}
        @param dest_uri: the destination URI, can represent either a directory
                         or a file
        @type dest_uri:  L{elisa.core.media_uri.MediaUri}
        @rtype:          L{twisted.internet.defer.Deferred}
        """
        raise NotImplementedError

    def delete(self, uri, recursive=False):
        """
        Delete a resource located at given URI. If that URI represents
        a directory and recursive flag is set to True I will
        recursively remove the directory.

        @param uri:       the URI representing the file or directory for
                          which we would like to know if it is currently
                          monitored or not
        @type uri:        L{elisa.core.media_uri.MediaUri}
        @param recursive: if orig_uri represents a directory, should I copy it
                          recursively to dest_uri?
        @type recursive:  bool
        @rtype:           L{twisted.internet.defer.Deferred}
        """
        raise NotImplementedError
