# -*- 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__ = 'Benjamin Kampmann <benjamin@fluendo.com>'

from menu_entry_builder import MenuEntryBuilder

from elisa.core.observers.list import ListObserver, ListObservable
from elisa.core.utils import misc
from elisa.core import common
from elisa.core.media_manager import MediaProviderNotFound
from elisa.core.media_uri import MediaUri, ParseException

from elisa.extern.coherence.et import parse_xml

from twisted.internet import defer

import platform, os, re

# remove old observers
from weakref import WeakKeyDictionary

plugin_registry = common.application.plugin_registry
MediaLocationMessage = plugin_registry.get_component_class('base:media_location_message')
InternetLocationMessage = plugin_registry.get_component_class('base:internet_location_message')
DeviceActionMessage = plugin_registry.get_component_class('base:device_action_message')
LocalNetworkLocationMessage = plugin_registry.get_component_class('base:local_network_location_message')
ForeignApplicationMessage = plugin_registry.get_component_class('base:foreign_application_message')
LocationsListMessage = plugin_registry.get_component_class('xmlmenu:locations_list_message')

class FilteredLocationObserver(ListObserver):

    def __init__(self, model, handle_call, icon=None, config=None):
        ListObserver.__init__(self)
        self._model = model
        self._children = model.children
        self._handle_call = handle_call
        self.location_types = set()
        self.media_types = set()
        self.uri_schemes = set()
        self._icon = icon
        self._config = config
        self._location_map = {}

    def inserted(self, elements, position):
        for location in elements:
            score = 0
            if len(self.media_types) == 0 or \
                self.media_types.intersection(set(location.media_types)):
                score += 1

            if len(self.location_types) == 0 or \
                location.location_type in self.location_types:
                score += 1

            if len(self.uri_schemes) == 0 or \
                location.uri.scheme in self.uri_schemes:
                score += 1

            if score == 3:
                # FIXME: a bit stupid, or?
                uri = location.uri

                c_e = parse_xml("<MenuEntry type='uri_node'>" \
                        "<Label>%s</Label>\n"\
                        "<URI>%s</URI>\n" \
                        "</MenuEntry>" %
                    (uri.label, uri)).getroot()

                if location.theme_icon:
                    c_e.insert(1, parse_xml('<Icon>%s</Icon>' %
                                            location.theme_icon).getroot())
                elif self._icon != None:
                    c_e.insert(1, self._icon)

                if self._config != None:
                    c_e.insert(2, self._config)

                # ugly hack to have the children set correctly
                self._model.children = self._children
                dfr = self._handle_call(self._model, c_e)
                dfr.addCallback(self._models_added, location)

    def removed(self, elements, position):

        for element in elements:
            element_id = id(element)

            if self._location_map.has_key(element_id):
                for model in self._location_map[element_id]:
                    try:
                        self._children.remove(model) 
                    except ValueError:
                        # The item is not in the list anymore. whatsoever,
                        # that is also what we wanted ;)
                        pass

                del self._location_map[element_id]

    def _models_added(self, models, location):
        location_id = id(location)

        if not self._location_map.has_key(location_id):
            self._location_map[location_id] = []

        for model in models:
            self._location_map[location_id].append(model)



class Location(object):
    """
    each Location has some parameters
    """
    def __init__(self):
        self.media_types = []
        self.location_type = ''
        self.uri = None
        self.theme_icon = None

class BusLocationNotifier(ListObserver):
    """
    Post LOCATION_ADDED messages on the bus
    """

    def __init__(self):
        super(BusLocationNotifier, self).__init__()
        self.bus = common.application.bus

    def inserted(self, elements, position):
        for element in elements:
            uri = element.uri
            message_type = MediaLocationMessage.ActionType.LOCATION_ADDED
            message = MediaLocationMessage(message_type, uri.path,
                    uri.scheme, uri.path)
            self.bus.send_message(message, self)

    def removed(self, elements, position):
        for element in elements:
            uri = element.uri
            message_type = MediaLocationMessage.ActionType.LOCATION_REMOVED
            message = MediaLocationMessage(message_type, uri.path,
                    uri.scheme, uri.path)
            self.bus.send_message(message, self)

class LocationsBuilder(MenuEntryBuilder):
    """
    This class can handle a special MenuEntry-XML-Tag and create
    MenuNodeModels according to the data it gets.
    """

    """
    The XDG directories mapped to the elisa internal media type
    """
    xdg_media_types = {
                    'XDG_MUSIC_DIR' : ['audio'],
                    'XDG_PICTURES_DIR' : ['image'],
                    'XDG_VIDEOS_DIR' : ['video']
                 }
    
    """
    The different bus_messages mapped to the location_type
    """
    location_map = {DeviceActionMessage : 'device',
                    ForeignApplicationMessage : 'app',
                    InternetLocationMessage : 'internet',
                    LocalNetworkLocationMessage : 'network',
                    MediaLocationMessage : 'local'
                    }

    default_config =  { 'auto_locations' : 1,
                        'locations' : []}


    def initialize(self):
        self._locations = ListObservable()
        # deactivated
        #self._bus_location_notifier = BusLocationNotifier()
        #self._locations.add_observer(self._bus_location_notifier)
        self._observers = WeakKeyDictionary()

        # FIXME: this is because the controller is cutting the reference to
        # the children but this components needs it to return it. it builds a
        # static menu
        self._places = WeakKeyDictionary()

        bus = common.application.bus
        bus.send_message(LocationsListMessage(self._locations),
                            sender=self)
        bus.register(self._got_bus_message, MediaLocationMessage)

        self._load_media_locations()
        #return defer.succeed(True)

    def menu_entry_identifiers__get(self):
        return ['locations']

    def loadmore(self, model):
        return defer.succeed(self._places[model])

    def unload(self, model):
        pass

    def build_menu_entry(self, real_parent, node):
        parent_node = node.find('ParentNode')
        show_on_empty = False
        if parent_node != None:
            plugin_registry = common.application.plugin_registry

            # FIXME: this should become deferred base soon
            parent = plugin_registry.create_component('base:menu_node_model')
            parent.children = plugin_registry.create_component('base:list_model')

            self._set_icon(parent, parent_node.find('Icon'))

            parent.text = self._make_label(parent_node.find('Label'))
            if parent_node.get('show-on-empty', 'False').upper() == 'TRUE':
                show_on_empty = True
            self.model_configs[parent] = self.model_configs[real_parent]
        else:
            parent = real_parent

        if self._observers.has_key(parent.children):
            return defer.succeed([])

        lObserver = FilteredLocationObserver(parent,
                                    self.activity.handle_menu_entry,
                                    icon=node.find('Icon'),
                                    config=node.find('Configuration')
                                            )

        for filter in node.findall('Filter'):
            filter_type = filter.get('type', None)
            if filter_type == None:
                self.warning("Ignoring Filter without type: %s" % filter)
            elif filter_type == 'media_type':
                lObserver.media_types.add(filter.text)
            elif filter_type == 'location_type':
                lObserver.location_types.add(filter.text)
            elif filter_type == 'uri_scheme':
                lObserver.uri_schemes.add(filter.text)

        lObserver.inserted(self._locations, 0)
        self._locations.add_observer(lObserver)
        self._observers[parent.children] = lObserver
        self._places[parent] = parent.children
        if real_parent != parent:
            if len(parent.children) != 0 or show_on_empty:
                parent.has_children = True
                parent.activity = self
                parent.children.activity = self
                real_parent.children.append(parent)

        return defer.succeed([])

    def _load_xdg_env(self):
        xdg_ok = True
        config_home = os.path.expanduser(os.getenv('XDG_CONFIG_HOME',
                                                   '~/.config'))
        user_dirs_file = os.path.join(config_home, 'user-dirs.dirs')

        # Lazily install XDG user-dirs files
        if not os.path.exists(user_dirs_file):
            self.debug("Executing xdg-user-dirs-update")
            retval = os.system('xdg-user-dirs-update')
            if retval != 0:
                self.debug("xdg-user-dirs-update program not found")
                xdg_ok = False
                
        if xdg_ok:
            variables = filter(lambda i: i.startswith('XDG'),
                               os.environ.keys())
            # TODO: simplify this if possible, with the xdg python package
            for xdg in self.xdg_media_types.keys():
                if xdg not in variables:
                    self.debug('XDG variables not found in os.environ, loading %r',
                               user_dirs_file)
                    key_val_re = re.compile('(.*)="(.*)"')
                    for line in open(user_dirs_file).readlines():
                        match = key_val_re.search(line)
                        if match and not match.groups()[0].startswith('#'):
                            var, value = match.groups()
                            self.debug('Found XDG variable %s, injecting in os.environ',
                                       var)
                            value = misc.env_var_expand(value)
                            os.environ[var] = value
                else:
                    self.debug("XDG variables %r already in os.environ",
                               variables)
                
    def _load_windows_env(self):
        from mswin32 import tools
        d = tools.get_multimedia_dir()
        
        os.environ['XDG_MUSIC_DIR'] = d['My Music'].encode('utf8')
        os.environ['XDG_PICTURES_DIR'] = d['My Pictures'].encode('utf8')
        os.environ['XDG_VIDEOS_DIR'] = d['My Video'].encode('utf8')

    def _add_location(self, uri, media, location_type='local',
                      theme_icon=None, removeable=True):
        for location in self._locations:
            if location.uri == uri:
                return

        bus = common.application.bus

        message_type = MediaLocationMessage.ActionType.LOCATION_ADDED

        if location_type == 'internet':
            msg = InternetLocationMessage
        elif location_type == 'network':
            msg = LocalNetworkLocationMessage
        else:
            msg = MediaLocationMessage
        message = msg(message_type, uri.label, uri.scheme, unicode(uri),
                                                media, removeable, theme_icon)
        bus.send_message(message, sender=self)
        
    def _load_media_locations(self):            

        # Load media locations configured by the user
        self._load_user_media_locations()

        # Optionally load user's default media locations
        self._load_default_media_locations()
        
        self.debug("Loaded Locations: %s", self._locations)

    def _load_user_media_locations(self):
        for location in self.config.get('locations', []):
            try:
                uri = MediaUri(location)
            except (TypeError, ParseException):
                self.warning('Location %s is no valid MediaUri' % location)
                continue

            # FIXME: remove this hack!
            media = ['audio', 'video', 'image']
            loca_dict = self.config.get(location, {})

            if loca_dict.has_key('only_media'):
                media = loca_dict['only_media']

            if loca_dict.has_key('label'):
                uri.label = loca_dict['label']

            location_type = 'local'
            # FIXME: a scheme-based location-type getting from the
            # media_provider would be really useful
            if loca_dict.has_key('location_type'):
                location_type = loca_dict['location_type'] 

            self._add_location(uri, media, location_type, None, False)

    def _load_default_media_locations(self):
        if bool(self.config.get('auto_locations', 1)):
            if platform.system() == 'Windows':
                self._load_windows_env()
            else:
                self._load_xdg_env()

            for key, media in self.xdg_media_types.iteritems():
                location = os.environ.get(key)
                if not location:
                    self.debug('%s not found in enviroment' % key)
                    continue

                location_uri = MediaUri({'scheme': 'file', 'path': location})

                # Simplified XDG support, disabled, hackish
                """
                media_locations = [ l.uri for l in self._locations
                                    if media in l.media ]
                if len(media_locations) == 0:
                    # only one directory to display, skip to its
                    # contents directly
                    media_manager = common.application.media_manager

                    def got_media_type(media_type, child_uri, media):
                        file_type = media_type['file_type']
                        can_add = False
                        if file_type == 'directory':
                            theme_icon = None
                            can_add = True
                        elif file_type == media:
                            if file_type == 'audio':
                                theme_icon = '%s_file_icon' % file_type
                            else:
                                theme_icon = child_uri
                            can_add = True
                            
                        if can_add:
                            self._add_location(child_uri, media,
                                               theme_icon=theme_icon)
                    
                    def got_children(children, media):
                        for (child_uri, metadata) in children:
                            d = media_manager.get_media_type(child_uri)
                            d.addCallback(got_media_type, child_uri, media)

                    dfr = media_manager.get_direct_children(location_uri, [])
                    dfr.addCallback(got_children, media)
                else:
                    self._add_location(location_uri, media)
                """
                self._add_location(location_uri, media)
        else:
            self.info("Auto locations lookup disabled")

    def _got_bus_message(self, message, sender):

        if not self.location_map.has_key(type(message)):
            self.warning("Unkown location of %s" % message)
            return

        ActionType = MediaLocationMessage.ActionType

        if message.action == ActionType.LOCATION_ADDED:
            # TODO: make use of can_eject variable
            can_eject = message.removable
            location = Location()

            location.location_type = self.location_map[type(message)]

            location.uri = MediaUri(message.mount_point)

            for old_location in self._locations:
                if unicode(location.uri) == unicode(old_location.uri):
                    # not twice please!
                    return

            location.uri.label = message.name
            location.media_types = message.media_types
            location.theme_icon = message.theme_icon

            if not message.removable:

                if not self.config.has_key('locations'):
                    self.config['locations'] = []

                byte_uri = str(location.uri)
                
                # if the uri is not yet saved, save it
                if not byte_uri in self.config['locations']:
                    self.config['locations'].append(byte_uri)

                    if not self.config.has_key(byte_uri):
                        self.config[byte_uri] = {}

                    self.config[byte_uri]['label'] = message.name
                    self.config[byte_uri]['only_media'] = message.media_types
                
            self._locations.append(location)
    
        elif message.action == ActionType.LOCATION_REMOVED:
            for location in self._locations:
                if unicode(location.uri) == unicode(message.mount_point):
                    self._locations.remove(location)
