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



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


"""
MediaManager & MediaProvider notes and ideas


- The MediaManager can ask the MediaProvider component if a URI is cacheable.
  The MediaManager should have a totally abstract view on the type of data it
  is managing, the MediaProvider implements all data specific cases.

- When a media access is requested through the MediaManager, it is up to the
  MediaProvider to implement this operation as a singleton or simultaneous
  data access. i.e : you could open two files on two samba servers at the
  same time.

- That media request needs to provide non-blocking and blocking calls.
  Blocking I/O operations called from the main thread should raise an
  exception, in an effort to keep the entire program to block (the
  intention is to have Elisa as plugin-weakness safe as possible)

- The MediaProviders should have functions to provide meta data to the
  MediaManager. The data structure of the meta data should be
  predefined. **NOTE**: This isn't specific to MediaProviders,
  IMHO. Metadata retrieval should be implemented using GStreamer
  and/or a third-party library (like TagLib). MediaProviders provide
  URI data support, but they shouldn't be aware of the kind of
  multimedia files they provide access to. This is the job of the
  underlying multimedia framework. -- Philippe
  - We do it now with the usage of a dictionary containing metadata

- The MediaManager is portable by design. The MediaProviders can be
  system-dependent or requiring external applications running.

- The MediaManager's caching system could have parallel lists of contents
  to cache depending on the resource type. MediaProvider could provide
  a way for the MediaManager to know if the resource is likely to take
  long to retrieve or not. For example, if there is only one to-cache list,
  if the first 10 elements are HTTP resources and the next are from the
  local filesystem, the local filesystem resources could be processed in
  parralel to provide faster access to them. **NOTE**: How do you define
  "long to retrieve"? For me every task should be long by
  definition. -- Philippe
  **NOTE2**: then no parralelism. Although you can be pretty sure a local
  disk access will really be faster than a HTTP request, so it could come
  handy - although it's some kind of specials case - cause it might give the
  user a better responsivity.

- The caching system should be priority-list oriented. Priorities should
  be assigned to Medias to cache, and a request to MediaManager cached
  media should be able to interrupt current caching and process the
  request first.

- The MediaManager could count accesses to data. Those statistics could be
  considered as metadata, and could be retrieved by Components.

- The Components could add metadata to the Medias (like tags).
  However it will be needed first to ensure that the data given by the
  Component cannot cause a database corruption. **NOTE**: This
  metadata would be stored both on db and in files? -- Philippe
  **NOTE2**: DB for sure, and maybe in files if the MediaProvider
  supports it ?

- It should be possible to place a restriction on the Cache database
  file size.  That would be necessary in the case of Elisa running on
  a system more aimed at retrieving data from networks, that has a
  low-capacity hard drive. **NOTE**: What would we do when db file
  size exceeds the limit? drop some db records? never add anything
  more in the db? :) The box we have in mind will have a CF card, we
  found out that even with huge media libraries the db size doesn't
  exceeds 20mb. This also depends on how the db is designed. -- Philippe

  **NOTE2**: This is in a way related to fault-tolerancy (CF card or not).
  The behaviour should be IMHO to stop caching -- Colin

- The MediaManager should have different monitor location list
  depending on the time the user wants it to check for updates of
  these locations (one time, 5 minutes, 30min, hourly, daily, ...)

  There should also be an unmonitored location list so that the user can
  explicitly tell the MediaManager not to do caching on certain locations.

  The MediaManager will also ask the MediaProvider components if it can do
  monitoring on the medias that are relevant to a certain MediaProvider
  (like INotify for Local FS support).

  The MediaProviders can indeed add locations to monitor (HAL plugin
  for example). **NOTE**: I don't understand that example. The HAL
  plugin doesn't provide a media_provider, does it? -- Philippe
  **NOTE2** I was thinking about the case MediaProvider get signaled
  by the HAL plugin (which is not a MP) that a new location is available
  -- Colin

- MediaProviders should be able to provide source elements for
  GStreamer. This can become handy for URIs not supported (yet) by
  GStreamer but supported by MediaProviders who have access to the
  data (example: DAAP uris)

"""

import threading, os
from twisted.internet import reactor, defer

from elisa.core import common, manager
from elisa.core.utils import classinit
from elisa.base_components import media_provider
from elisa.core import media_uri

class MediaProviderNotFound(Exception):

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

    def __str__(self):
        return "No MediaProvider found for %r URI" % self.uri

class MediaManager(manager.Manager):
    """
    Provides access to files through the use of MediaProvider
    components which allows to handle different file I/O protocols
    using URIs.  It also handles caching in a database and the
    monitoring of files via the L{elisa.core.media_scanner.MediaScanner}.

    Database caching can be disabled:

     - explicitely in the application's config
     - if any dependency (pysqlite for instance) is unmet

    In a such case, the media_scanner instance variable is set to None.

    @ivar media_scanner:   the component which scans media sources and keep
                           the database up to date
    @type media_scanner:   L{elisa.core.media_scanner.MediaScanner}
    """

    def __init__(self, metadata_manager):
        """ Initialize media_providers instance variable and try to
        load the media_scanner.

        @param metadata_manager: The MetadataManager to use to create the MediaScanner
        @type metadata_manager: L{elisa.core.metadata_manager.MetadataManager}
        """
        manager.Manager.__init__(self)
        self._metadata_manager = metadata_manager
        self._providers_by_scheme = {}

    def start(self, seconds=0, resume_scan=False):
        """
        Load all enabled MediaProvider components using the
        PluginRegistry and eventually start the media_scanner

        @keyword seconds:     time in seconds to wait before starting the scanner
        @type seconds:        int
        @keyword resume_scan: should the media_scanner resume interrupted
                              scan at startup?
        @type resume_scan:    bool
        """
        
    def stop(self):
        """
        Stop the media_scanner if it's running and clean all
        registered media_providers.
        """
        self.info('Stopping')
        
        return manager.Manager.stop(self)

    def register_component(self, component):
        try:
            manager.Manager.register_component(self, component)
            schemes = component.supported_uri_schemes
            for scheme, index in schemes.iteritems():
                if scheme not in self._providers_by_scheme:
                    self._providers_by_scheme[scheme] = [component,]
                else:
                    self._providers_by_scheme[scheme].insert(index, component)
        except manager.AlreadyRegistered:
            pass
            #FIXME: needs to be raised?

    def unregister_component(self, component):
        for scheme in self._providers_by_scheme.keys():
            m_providers = self._providers_by_scheme[scheme]
            if component in m_providers:
                m_providers.remove(component)
            self._providers_by_scheme[scheme] = m_providers
        try:
            manager.Manager.unregister_component(self, component)
        except CannotUnregister:
            pass
            #FIXME: needs to be raised?

    def get_metadata(self, metadata, low_priority=False):
        """just a proxy to the metadata_manager.get_metadata"""
        return self._metadata_manager.get_metadata(metadata, low_priority=False)

    def _get_media_provider(self, uri):
        """
        Retrieve the MediaProvider supporting the scheme of the given
        URI. If multiple MediaProviders support it, the first one is
        chosen.

        @param uri: The location of the source
        @type uri:  L{elisa.core.media_uri.MediaUri}
        @rtype:     L{elisa.base_components.media_provider.MediaProvider}
        """
        provider = None
        providers = self._providers_by_scheme.get(uri.scheme)

        if providers:
            provider = providers[0]
            self.debug("Using %r media_provider to access %s://", provider.name,
                       uri.scheme)
        else:
            raise MediaProviderNotFound(uri)
        return provider

    def _proxy(self, method_name, fallback_result, uri, *args, **kw):
        result = fallback_result
        provider = self._get_media_provider(uri)
        if provider:
            real_args = (uri,) + args + (kw,)
            self.debug("Calling %s.%s%r", provider.name, method_name,
                       real_args)
            result = getattr(provider, method_name)(uri, *args, **kw)
        return result

    def is_scannable(self, uri):
        provider = self._get_media_provider(uri)
        scannable = provider and uri.scheme in provider.scannable_uri_schemes
        return scannable

    def _guess_source_for_uri(self, uri):
        source = None
        source_uri = uri

        while True:
            u_uri = unicode(source_uri)
            if u_uri[-1] == '/':
                u_uri = u_uri[:-1]

            source = self.get_source_for_uri(u_uri)
            if source:
                break
            
            source = None
            parent = source_uri.parent
            if parent == source_uri:
                break

            source_uri = parent

        return source

    def 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)
           }


        @param uri: the URI to analyze
        @type uri:  L{elisa.core.media_uri.MediaUri}
        @rtype:     L{twisted.internet.defer.Deferred}
        """

        return self._proxy('get_media_type', {}, uri)

    def blocking_get_media_type(self, uri):
        return self._proxy('_blocking_get_media_type', {}, uri)

    def 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
        """
        return self._proxy('is_directory', False, uri)

    def blocking_is_directory(self, uri):
        return self._proxy('_blocking_is_directory', False, uri)

    def 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:             L{twisted.internet.defer.Deferred}
        """
        return self._proxy('has_children_with_types', False, uri, media_types)

    def blocking_has_children_with_types(self, uri, media_types):
        return self._proxy('_blocking_has_children_with_types',
                           False, uri, media_types)

    def get_direct_children(self, uri, children_with_info):
        """
        Scan the data located at given uri and return a deferred.
        Fills children_with_info. Defferred is called when the
        gathering is finished with children_with_info as parameter

        Typemap of filled result:

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

        @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:                         twisted.internet.deferred
        """
        return self._proxy('get_direct_children', None, uri, children_with_info)

    def blocking_get_direct_children(self, children_with_info):
        return self._proxy('_blocking_get_direct_children', None, uri,
                           children_with_info)

    def open(self, uri, mode='r'):
        """
        Open an uri and return MediaFile file if the block keyword
        parameter is True. Else we return a deferred which will be
        trigerred when the media_file has been successfully opened.

        @param uri:     the URI to open
        @type uri:      L{elisa.core.media_uri.MediaUri}
        @param mode:    how to open the file -- see manual of builtin open()
        @type mode:     string or None
        @rtype:         L{elisa.core.media_file.MediaFile}
        """
        return self._proxy('open', None, uri, mode=mode)


    def blocking_open(self, uri, mode='r'):
        return self._proxy('_blocking_open', None, uri, mode=mode)

    def next_location(self, uri, root=None):
        """
        Return the uri just next to given uri and record it to history

        @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}
        """
        result = None
        method_name = 'next_location'
        if root:
            provider = self._get_media_provider(root)
        else:
            provider = self._get_media_provider(uri)
        if provider:
            result = getattr(provider, method_name)(uri, root=root)

            def got_location(location):
                self.debug("Next of %r: %r", uri, location)
                return location

            result.addCallback(got_location)
        return result

    def blocking_next_location(self, uri, root=None):
        location = None
        method_name = '_blocking_next_location'
        if root:
            provider = self._get_media_provider(root)
        else:
            provider = self._get_media_provider(uri)
        if provider:
            location = getattr(provider, method_name)(uri, root=root)
            self.debug("Next of %r: %r", uri, location)
        return location

    def 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}
        """
        return self._proxy('previous_location', None, uri)

    def blocking_previous_location(self, uri):
        return self._proxy('_blocking_previous_location', None, uri)

    def monitor_uri(self, uri, callback, *extra_args):
        """
        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 extra_args: extra positional arguments to pass to the callback
        @type extra_args: tuple
        @param callback: a callable taking the event that occured and the uri
                         of the file on which the event applies to
                         prototype: callable(uri, event)
                         type uri:   L{elisa.core.media_uri.MediaUri}
                         type event: L{elisa.base_components.media_provider.NotifyEvent}
        """

        return self._proxy('monitor_uri', None, uri, callback, *extra_args)

    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}
        """
        return self._proxy('unmonitor_uri', None, uri)

    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 self._proxy('uri_is_monitorable', False, uri)

    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
        """
        return self._proxy('uri_is_monitored', False, uri)

    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:           bool
        """
        return self._proxy('copy', False, orig_uri, dest_uri,
                           recursive=recursive)

    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:          bool
        """
        return self._proxy('move', False, orig_uri, dest_uri)

    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:           bool
        """
        return self._proxy('delete', False, uri, recursive=recursive)

    def get_real_uri(self, uri):
        """
        Returns the original uri (reachable) 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 self._proxy('get_real_uri', None, uri)
