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

"""
LocalMediaProvider component class
"""


__maintainer__ = 'Florian Boucault <florian@fluendo.com>'
__maintainer2__ = 'Philippe Normand <philippe@fluendo.com>'

from elisa.base_components.media_provider import MediaProvider, UriNotMonitorable
from elisa.base_components.media_provider import NotifyEvent
from elisa.core import common
from elisa.core.media_uri import MediaUri
from elisa.core.media_file import MediaFile
from elisa.core.utils import misc, sorting
from elisa.extern import path

from twisted.internet import reactor

try:
    from elisa.extern.coherence import inotify
except ImportError:
    inotify = None

import os
import shutil
import re

from twisted.internet import defer, task, threads


class LocalMedia(MediaProvider):
    """
    This class implements local filesystem support
    """

    hidden_fnmatch_pattern = '[!.]*'
    
    default_config = {'hidden_file_chars': '!.'}


    def __init__(self):
        MediaProvider.__init__(self)
        self._paths = {}

    def initialize(self):
        ret = super(LocalMedia, self).initialize()
        self._hidden_file_chars = self._get_option('hidden_file_chars')

        return ret

    def _get_option(self, name):
        try:
            return self.config[name]
        except KeyError:
            return self.default_config[name]

    def supported_uri_schemes__get(self):
        return { 'file': 0 }

    def get_media_type(self, uri):
        if os.path.isdir(uri.path):
            return defer.succeed({'file_type': 'directory', 'mime_type': ''})

        def get_metadata_done(metadata):
            self.debug("got metadata for %s: %s", uri, metadata)

            del metadata['uri']

            return metadata

        def get_metadata_failure(failure):
            self.debug("error getting media type for uri %s: %s", uri, failure)

            return failure

        req_metadata = {'uri': uri, 'file_type': None, 'mime_type': None}
        get_metadata_dfr = common.application.metadata_manager.get_metadata(req_metadata)
        get_metadata_dfr.addCallbacks(get_metadata_done, get_metadata_failure)

        return get_metadata_dfr

    def is_directory(self, uri):
        return defer.succeed(os.path.isdir(uri.path))
    
    def has_children_with_types(self, uri, media_types):
        if not os.path.isdir(uri.path):
            return defer.succeed(False)

        try:
            contents = os.listdir(uri.path)
        except OSError, error:
            self.info("Could not list directory %r: %s", path, error)
            return defer.succeed(False)
        
        if len(contents) == 0:
            return defer.succeed(False)

        media_manager = common.application.media_manager

        # we go over all the children until we find one that matches media_types
        has_children = False

        def get_media_type_done(metadata):
            if not media_types or metadata['file_type'] in media_types:
                global hash_children
                has_children = True

        def get_media_type_failure(failure):
            # consume it
            return None

        def children_iter():
            for child in contents:
                if has_children:
                    break

                if len(child) == 0 or \
                        child[0] in self._hidden_file_chars:
                    yield None
                    continue

                try:
                    child_uri = uri.join(child)
                except UnicodeDecodeError, e:
                    self.warning(e)
                    # skip this child
                    yield None
                    continue

                get_media_type_dfr = media_manager.get_media_type(child_uri)
                get_media_type_dfr.addCallbacks(get_media_type_done,
                        get_media_type_failure)

                yield get_media_type_dfr
        
        def children_iter_done(iterator):
            return has_children

        dfr = task.coiterate(children_iter())
        dfr.addCallback(children_iter_done)

        return dfr

    def get_direct_children(self, uri, list_of_children):
        self.info("Retrieving contents of %r", uri)
        
        if not os.path.isdir(uri.path):
            return defer.succeed(list_of_children)


        def filter_contents(contents):
            before = len(contents)
            # drop all non-uncode objects, because the sorting would break
            contents = filter(lambda x: isinstance(x, unicode), contents)
            if before > len(contents):
                self.warning("Found wrong encoded files in '%s'. Dropped them" %
                                uri.path)

            dfr = threads.deferToThread(sorting.natural_sort, contents)
            # sort directory contents alpha-numerically
            dfr.addCallback(natural_sort_done, contents)
            return dfr


        def natural_sort_done(result, contents):
            def children_iter():
                folders = []
                files = []

                for filename in contents:
                    if len(filename) == 0 or \
                            filename[0] in self._hidden_file_chars:
                        yield None
                        continue

                    file_path = os.path.join(uri.path, filename)
                    
                    parts = {'scheme': 'file', 'path': file_path}
                    file_uri = MediaUri(parts)
                    metadata = {}
                    try:
                        metadata['fs_mtime'] = os.stat(file_path).st_mtime
                    except OSError:
                        pass
                    
                    item = (file_uri, metadata)

                    if os.path.isdir(file_path):
                        folders.append(item)
                    else:
                        files.append(item)

                    yield None

                for lst in (folders, files):
                    for item in lst:
                        list_of_children.append(item)
                        yield None

            def children_iter_done(iterator):
                return list_of_children
            
            dfr = task.coiterate(children_iter())
            dfr.addCallback(children_iter_done)

            return dfr

        dfr = threads.deferToThread(os.listdir, uri.path)
        # filter the result
        dfr.addCallback(filter_contents)

        return dfr

    def open(self, uri, mode='r'):
        ret = None

        if os.path.isdir(uri.path):
            return defer.fail(Exception('open called on a directory?'))


        try:
            handle = open(uri.path, mode)
        except Exception, exc:
            self.info("Could not open %s : %s" % (uri, exc))
        else:
            ret = MediaFile(self, handle)
        
        return defer.succeed(ret)

    def monitor_uri(self, uri, callback, *extra_args):

        def inotify_cb(iwp, filename, mask, parameter):
            media_uri = MediaUri(unicode("file://%s/%s" % (iwp.path, filename)))
            event = self._inotify_event_type(mask)
            return callback(media_uri, {}, event,uri, *extra_args)

        if self.uri_is_monitorable(uri):
            try:
                inotify_instance = inotify.INotify()
            except SystemError, error:
                self.warning(error)
                raise UriNotMonitorable(uri)
            else:
                path = uri.path.encode('utf-8')

                self.info("Starting to monitor %r", path)
                try:
                    inotify_instance.watch(path, auto_add = False, \
                                           recursive=False, \
                                           mask = inotify.IN_CREATE \
                                           | inotify.IN_DELETE \
                                           | inotify.IN_ATTRIB \
                                           | inotify.IN_MOVED_TO \
                                           | inotify.IN_MOVED_FROM, \
                                           callbacks=(inotify_cb, None))
                except IOError, error:
                    raise UriNotMonitorable(uri)
        else:
            raise UriNotMonitorable(uri)

    def _inotify_event_type(self, mask):
        if mask & inotify.IN_CREATE or mask & inotify.IN_MOVED_TO:
            return NotifyEvent.ADDED
        elif mask & inotify.IN_DELETE or mask & inotify.IN_UNMOUNT or mask & inotify.IN_MOVED_FROM:
            return NotifyEvent.REMOVED
        elif mask & inotify.IN_ATTRIB:
            return NotifyEvent.MODIFIED
        else:
            # FIXME: exception should be raised
            inotify_instance = inotify.INotify()
            human = inotify_instance.flag_to_human(mask)
            self.info("Unexpected inotify event: %s" % human)
            return NotifyEvent.REMOVED


    def unmonitor_uri(self, uri):
        self.debug("Stopping %r monitoring", uri)
        inotify.INotify().ignore(uri.path)


    def uri_is_monitorable(self, uri):
        monitorable = False
        home = os.path.expanduser('~')
        if inotify != None:
            inotify_instance = inotify.INotify()
            try:
                inotify_instance.watch(home)
            except UriNotMonitorable:
                monitorable = False
            else:
                monitorable = True
                inotify_instance.ignore(home)
        return monitorable

    def uri_is_monitored(self, uri):
        return inotify and inotify.INotify().is_watched(uri.path)

    def copy(self, orig_uri, dest_uri, recursive=False):
        try:
            if recursive:
                shutil.copytree(orig_uri.path, dest_uri.path)
            else:
                shutil.copy(orig_uri.path, dest_uri.path)
        except Exception, e:
            raise e

    def move(self, orig_uri, dest_uri):
        try:
            shutil.move(orig_uri.path, dest_uri.path)
        except shutil.Error, e:
            raise e


    def delete(self, uri, recursive=False):
        try:
            if recursive:
                shutil.rmtree(uri.path)
            else:
                os.remove(uri.path)
        except shutil.Error, e:
            raise e

