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

"""
Module responsible for starting the Application
"""

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

import pkg_resources
import sys, time
import os
import datetime
import locale
import gobject
import platform


from elisa.core import __version__, version_info, config, log
from elisa.core.utils import exception_hook
from elisa.core.bus import bus
from elisa.core.utils import i18n
from elisa.core.utils import classinit
from elisa.core import thumbnailer
from elisa.core import media_uri
from elisa.core import common

from elisa.base_components.message import Message

from elisa.core import plugin_registry, config_upgrader
from elisa.core import input_manager, media_manager, service_manager
from elisa.core import metadata_manager
from elisa.core import interface_controller, player_registry

from twisted.internet import reactor, defer, threads
from twisted.web import client

from elisa.extern.translation import Translator

UPDATES_URL = "http://elisa.fluendo.com/updates/update.php?install_date=%(date)s&version=%(version)s"
CONFIG_DIR = os.path.join(os.path.expanduser('~'), ".elisa")
CONFIG_FILE = "elisa.conf"
TRANS_FILE = "data/translations.lst"

DEFAULT_CONFIG = """\
[general]
version = '%(version)s'
install_date = '%(install_date)s'
media_providers = ['daap:daap_media', 'youtube:youtube_media', 'shoutcast:shoutcast_media', 'fspot:fspot_media', 'coherence:upnp_media', 'ipod:ipod_media', 'base:local_media', 'media_db:elisa_media', 'gvfs:gnomevfs_media', 'audiocd:audiocd_media', 'flickr:flickr_media']
metadata_providers = ['media_db:db_metadata', 'gstreamer:gst_metadata_client', 'amazon:amazon_covers']
service_providers = ['updater:updater_service', 'media_db:media_scanner', 'gnome:gnome_screensaver_service', 'hal:hal_service', 'coherence:coherence_service', 'osso:osso_service']
player_engines = ['base:playbin_engine', 'audiocd:cdda_engine']
backends = ['backend1']
frontends = ['frontend1']

[backend1]
activity = 'raval:elisa_activity'
mvc_mappings = 'raval:data/raval_mvc_mappings.conf'
input_providers = ['lirc:lirc_input']

[frontend1]
backend = 'backend1'
theme = 'raval:tango_theme'
input_providers = ['pigment:pigment_input']

[xmlmenu:locations_builder]
locations = []
auto_locations = 1

[media_db:media_scanner]
enabled = '1'
fivemin_location_updates = []
hourly_location_updates = []
daily_location_updates = []
weekly_location_updates = []
unmonitored_locations = []

[media_db:db]
db_backend = 'sqlite'
database = 'elisa.db'

[lirc:lirc_input]
# filename of the LIRC config map to use
lirc_rc = 'streamzap.lirc'
delay = '4'
repeat = '1'

[coherence:coherence_service]
logmode = 'none'
controlpoint = 'yes'

[[plugins]]

[base:service_activity]
# a list of activites, which should beappear in the service_menu
service_activities = ['service:about_activity']

[base:local_media]
hidden_file_chars = '!.'

[player]
audiosink = 'autoaudiosink'

[theme_switcher:theme_switcher_activity]
# a list of themes 'plugin:component' like 'raval:tango_theme'
themes = ['raval:tango_theme', 'raval:poblenou_theme', 'raval:chris_theme']"""

class ComponentsLoadedMessage(Message):
    """
    Sent when all components have been instantiated
    """
                    
class Application(log.Loggable):
    """ Application is the entry point of Elisa. It groups all the necessary
    elements needed for Elisa to run. It is in charge of instantiating a
    Config and a PluginRegistry. Application also provides access to
    input events and data, and holds the user interfaces. It creates
    various managers (InputManager, MediaManager...),
    an InterfaceController and a DBBackend.

    @ivar plugin_registry:      loads and manages the plugins
    @type plugin_registry:      L{elisa.core.plugin_registry.PluginRegistry}
    @ivar config:               Application's configuration file, storing options
    @type config:               L{elisa.core.config.Config}
    @ivar bus:                  DOCME
    @type bus:                  L{elisa.core.bus.bus.Bus}
    @ivar translator:           DOCME
    @type translator:           L{elisa.extern.translator.Translator}
    @ivar metadata_manager:     DOCME
    @type metadata_manager:     L{elisa.core.metadata_manager.MetadataManager}
    @ivar service_manager:      DOCME
    @type service_manager:      L{elisa.core.service_manager.ServiceManager}
    @ivar player_registry:      DOCME
    @type player_registry:      L{elisa.core.player_registry.PlayerRegistry}
    @ivar interface_controller: DOCME
    @type interface_controller: L{elisa.core.interface_controller.InterfaceController}
    @ivar input_manager:        DOCME
    @type input_manager:        L{elisa.core.input_manager.InputManager}
    @ivar media_manager:        DOCME
    @type media_manager:        L{elisa.core.media_manager.MediaManager}
    @ivar thumbnailer:          DOCME
    @type thumbnailer:          L{elisa.core.thumbnailer.Thumbnailer}
    """

    # Allows property fget/fset/fdel/doc overriding
    __metaclass__ = classinit.ClassInitMeta
    __classinit__ = classinit.build_properties

    log_category = "application"

    def __init__(self, config_filename=None, show_tracebacks=False,
                                                            splash=None):
        """
        Application constructor. It:

         - loads global localization
         - loads the config file
         - loads the exception hook system
         - loads the profiler

        @param config_filename: the config filename to use. Can be absolute
                                or relative path
        @type config_filename:  string or None to use default config file
        """
        log.Loggable.__init__(self)
        self.debug("Creating")
        self._splash = splash

        self.show_tracebacks = show_tracebacks
        self.running = False
        self._plugin_registry = None

        self._config = None

        self._load_config(config_filename)
        self._load_exception_hook()
        self._load_profiler()
        self._compile_po_files()
        
        common.set_application(self)

        self._load_translator()
        
        self._plugin_registry = plugin_registry.PluginRegistry(self.config)
        self._plugin_registry.load_plugins()

        self._bus = bus.Bus()

        self._service_manager = service_manager.ServiceManager()
        self._metadata_manager = metadata_manager.MetadataManager()
        self._media_manager = media_manager.MediaManager(self.metadata_manager)
        self._input_manager = input_manager.InputManager()
        self._player_registry = player_registry.PlayerRegistry()
        self._interface_controller = interface_controller.InterfaceController()
        self._thumbnailer = thumbnailer.Thumbnailer()

    def _compile_po_files(self):
        if os.path.exists(TRANS_FILE):
            i18n.compile_translations_from_file(TRANS_FILE)

    def _load_translator(self):
        
        # check the locale is supported        
        try:
            locale.getpreferredencoding()
        except locale.Error, error:
            self.warning(error)
            self.warning("Falling back to system locale")
            #locale.setlocale(locale.LC_ALL, '')
            os.environ['LANG'] = 'C'
            
        self._translator = Translator()

    def _load_config(self, config_filename):
        if not config_filename:
            if not os.path.exists(CONFIG_DIR):
                try:
                    os.makedirs(CONFIG_DIR)
                except OSError, e:
                    self.warning("Could not create '%s': %s" % (CONFIG_DIR, e))
                    raise
                
            config_filename = os.path.join(CONFIG_DIR, CONFIG_FILE)

        self.info("Using config file: %r", config_filename)
        self._config_filename = config_filename
        today = datetime.date.today().isoformat()
        default_config = DEFAULT_CONFIG % {'version': __version__,
                                           'install_date': today}

        try:
            cfg = config.Config(config_filename, default_config=default_config)
        except config.ConfigError, error:
            self.warning(error)
            raise

        if not cfg.first_load:
            # ok we might have an old config format here
            upgrader = config_upgrader.ConfigUpgrader(cfg, default_config)
            cfg = upgrader.update_for(version_info)
            
        self._install_date = cfg.get_option('install_date', section='general',
                                            default=today)
        self._config = cfg

    def _load_profiler(self):
        """
        This imports profiling modules (TODO)
        """
        enable = int(self.config.get_option('enable_profiling', default='0'))
        if enable:
            # TODO: import profiling modules and set things ready
            pass

    def _check_updates(self):
        url = UPDATES_URL % {'date': self._install_date, 'version': __version__}
        dfr = client.getPage(url)

        def got_result(result):
            # TODO: notify user about new version
            pass
        
        dfr.addCallback(got_result)
        
    def _load_exception_hook(self):
        """ Override the default system exception hook with our own
        """
        # FIXME: make this configurable
        self.logdir = None 
        self.debug_level = int(self.show_tracebacks)

        sys.excepthook = self._excepthook
        # log twisted errors: Deactivated
        # from twisted.python import log
        # log.err = self.log_failure

    def _excepthook(self, *args):
        data = exception_hook.format_traceback(*args)
        path = exception_hook.write_to_logfile(data, self.logdir)
        self.warning("An Traceback occurred and got saved to %s" % path)
        self._after_hook(data)
    
    def _after_hook(self, data):
        if self.debug_level > 0:
            print data
            if self.debug_level == 2:
                try:
                    import pdb
                except ImportError:
                    print "pdb missing. debug shell not started"
                    return

                print "You are now in a debug shell. Application hold until" \
                      " you press 'c'!"
                pdb.set_trace()
    
    # Exception handling methods
    def log_traceback(self):
        """
        Log the traceback without stopping the process. This could ususally be
        used in parts, where you want to go on and log the exception.
        Example::

            try:
                component.initialize()
            except:
                # and log all the other exceptions
                path = application.log_traceback()
                self.warning("Initilize Component '%s' failed. Traceback saved at %s" % path)
            self.going_on()

        @return: path to the file, where the traceback got logged
        """
        data = exception_hook.format_traceback()
        path = exception_hook.write_to_logfile(data, self.logdir)
        self._after_hook(data)
        return path
    
    def log_failure(self, failure):
        """
        Log the twisted failure without re-raising the exception. Example in
        an errback::

            def errback(failure):
                path = application.log_failure(failure)
                self.warning("Connection refused. Full output at %s" % path)
                return
            
        @param failure: the failure to log
        @type failure:  L{twisted.python.failure.Failure}

        @return: path to the file, where the traceback got logged
        """
        data = exception_hook.format_failure(failure)
        path = exception_hook.write_to_logfile(data, self.logdir)
        self._after_hook(data)
        return path


    def plugin_registry__get(self):
        return self._plugin_registry

    def bus__get(self):
        return self._bus

    def translator__get(self):
        return self._translator

    def metadata_manager__get(self):
        return self._metadata_manager

    def service_manager__get(self):
        return self._service_manager


    def player_registry__get(self):
        return self._player_registry

    def interface_controller__get(self):
        return self._interface_controller

    def input_manager__get(self):
        return self._input_manager

    def media_manager__get(self):
        return self._media_manager

    def thumbnailer__get(self):
        return self._thumbnailer

    def config__get(self):
        return self._config

    def initialize(self):
        def initialize_managers_done(result, managers):
            # see if any of the managers failed
            failed = False
            first_failure = None
            initialized = []
            for i, item in enumerate(result):
                # item is (True, manager) if manager.initialize() succeeded,
                # otherwise (False, failure.Failure)
                res, obj = item
                manager = managers[i]
                if res == False:
                    if first_failure is None:
                        # we log everything and return the first failure to
                        # main()
                        first_failure = obj

                    self.warning("Manager %s failed to initialize: %s",
                                 manager, result)
                    failed = True
                else:
                    initialized.append(manager)

            if failed:
                deferreds = []
                # HMPF, clean initialized managers
                for manager in initialized:
                    deferreds.append(defer.maybeDeferred(manager.stop))

                dfr = defer.DeferredList(deferreds)
                # if a stop() call fails here we can't do anything, at least we
                # tried to clean
                dfr.addCallback(lambda result: defer.fail(first_failure))
                return dfr
            
            self._player_registry.initialize()
            # close the splash screen right before loading the context
            self._close_splash_screen()
            self._interface_controller.initialize()
            self.bus.send_message(ComponentsLoadedMessage())

        # call initialize() on all the managers, if any of the managers fail to
        # initialize, stop() the initialized ones and return a failure to the
        # caller; otherwise go on and initialize the player registry and the
        # interface controller and send the damn ComponentsLoaded Message
        managers = (self._service_manager, self._metadata_manager,
                    self._media_manager, self._input_manager)
        deferreds = []
        for manager in managers:
            self.debug("Initializing manager %s" % manager)
            deferreds.append(defer.maybeDeferred(manager.initialize))

        dfr = defer.DeferredList(deferreds)
        dfr.addCallback(initialize_managers_done, managers)
        return dfr

    def start(self):
        """ Execute the application. Start the Managers and the
        InterfaceController.
        """
        if hasattr(gobject, 'set_prgname') == True:
            gobject.set_prgname('elisa')
            
        threads.deferToThread(self._check_updates)
        
        self.running = True
        self.info("Starting")
        self.bus.start()
        self.input_manager.start()
        self.metadata_manager.start()
        self.media_manager.start(seconds=5)
        self.service_manager.start()
        self.interface_controller.start()

    def restart(self):
        return self.stop(restart=True)

    def stop(self, stop_reactor=True):
        """Stop the application.

        @param stop_reactor:    stop the reactor after stopping the application
        @type stop_reactor:     bool
        @rtype:                 L{twisted.internet.defer.Deferred}
        """
        self._close_splash_screen()

        def interface_controller_stopped(result):
            self.info("Stopping managers")
            self.player_registry.deinitialize()
            self.thumbnailer.stop()
            
            manager_deferreds = []
            for manager in (self.service_manager, self.metadata_manager,
                    self.media_manager, self.input_manager):
                manager_deferreds.append(defer.maybeDeferred(manager.stop))

            dfr = defer.DeferredList(manager_deferreds)
            dfr.addCallback(managers_stopped)
            return dfr

        def managers_stopped(managers):
            self.info("Stopping reactor")
            self.bus.stop()

            self.running = False

            if self.config:
                self.config.write()

            if stop_reactor and reactor.running:
                reactor.stop()

        if self.running:
            # stop the interface controller, then the player registry, the
            # thumbnailer and the managers
            self.info("Stopping interface controller")
            dfr = self.interface_controller.stop()
            dfr.addCallback(interface_controller_stopped)
            return dfr

        return defer.succeed(None)

    def _close_splash_screen(self):
        if self._splash != None:
            self._splash.destroy()


