# Copyright (C) 2001-2005 by Intevation GmbH
# Authors:
# Jan-Oliver Wagner <jan@intevation.de>
# Bernhard Herzog <bh@intevation.de>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with Thuban for details.

"""
Thuban's application object.
"""

__version__ = "$Revision: 1.39 $"

import sys, os
import os.path

import traceback

from wxPython.wx import *

from Thuban.Lib.connector import Publisher
from Thuban.Lib.fileutil import get_application_dir

from Thuban import _
from Thuban.Model.session import create_empty_session
from Thuban.Model.save import save_session
from Thuban.Model.load import load_session, LoadCancelled
from Thuban.Model.messages import MAPS_CHANGED
from Thuban.Model.layer import RasterLayer
import Thuban.Model.resource

from extensionregistry import ext_registry

import view
import tree
import mainwindow
import dbdialog
import altpathdialog
import exceptiondialog

from messages import SESSION_REPLACED


class ThubanApplication(wxApp, Publisher):

    """
    Thuban's application class.

    All wxWindows programs have to have an instance of an application
    class derived from wxApp. In Thuban the application class holds
    references to the main window and the session.
    """

    def OnInit(self):
        sys.excepthook = self.ShowExceptionDialog

        # Initialize instance variables before trying to create any
        # windows.  Creating windows can start an event loop if
        # e.g. message boxes are popped up for some reason, and event
        # handlers, especially EVT_UPDATE_UI may want to access things
        # from the application.

        # Defaults for the directories used in file dialogs
        self.path={"data":".", "projection":".", "alt_path":""}

        self.session = None
        self.top = None
        self.create_session()

        # Create an optional splash screen and then the mainwindow
        self.splash = self.splash_screen()
        if self.splash is not None:
            self.splash.Show()
        self.read_startup_files()
        self.init_extensions()
        self.top = self.CreateMainWindow()
        # The session was alredy created above and we need to get the
        # map into the mainwindow.  maps_changed does that.
        self.maps_changed()
        self.SetTopWindow(self.top)
        if self.splash is None:
            self.ShowMainWindow()

        return True

    def OnExit(self):
        """Clean up code.

        Extend this in derived classes if needed.
        """
        self.session.Destroy()
        self.session = None
        Publisher.Destroy(self)

    def read_startup_files(self):
        """Read the startup files."""
        # for now the startup file is ~/.thuban/thubanstart.py
        dir = get_application_dir()
        if os.path.isdir(dir):
            sys.path.append(dir)
            try:
                import thubanstart
            except ImportError:
                tb = sys.exc_info()[2]
                try:
                    if tb.tb_next is not None:
                        # The ImportError exception was raised from
                        # inside the thubanstart module.
                        sys.stderr.write(_("Cannot import the thubanstart"
                                         " module\n"))
                        traceback.print_exc(None, sys.stderr)
                    else:
                        # There's no thubanstart module.
                        sys.stderr.write(_("No thubanstart module available\n"))
                finally:
                    # make sure we delete the traceback object,
                    # otherwise there's be circular references involving
                    # the current stack frame
                    del tb
            except:
                sys.stderr.write(_("Cannot import the thubanstart module\n"))
                traceback.print_exc(None, sys.stderr)
        else:
            # There's no .thuban directory
            sys.stderr.write(_("No ~/.thuban directory\n"))

    def init_extensions(self):
        """Call initialization callbacks for all registered extensions."""
        for ext in ext_registry:
            ext.init_ext()

    def splash_screen(self):
        """Create and return a splash screen.

        This method is called by OnInit to determine whether the
        application should have a splashscreen. If the application
        should display a splash screen override this method in a derived
        class and have it create and return the wxSplashScreen instance.
        The implementation of this method in the derived class should
        also arranged for ShowMainWindow to be called.

        The default implementation simply returns None so that no splash
        screen is shown and ShowMainWindow will be called automatically.
        """
        return None

    def ShowMainWindow(self):
        """Show the main window

        Normally this method is automatically called by OnInit to show
        the main window. However, if the splash_screen method has
        returned a splashscreen it is expected that the derived class
        also arranges for ShowMainWindow to be called at the appropriate
        time.
        """
        self.top.Show(True)

    def CreateMainWindow(self):
        """Create and return the main window for the application.

        Override this in subclasses to instantiate the Thuban mainwindow
        with different parameters or to use a different class for the
        main window.
        """
        msg = (_("This is the wxPython-based Graphical User Interface"
               " for exploring geographic data"))
        return mainwindow.MainWindow(NULL, -1, "Thuban", self, None,
                                     initial_message = msg,
                                     size = (600, 400))

    def Session(self):
        """Return the application's session object"""
        return self.session

    def SetSession(self, session):
        """Make session the new session.

        Issue SESSION_REPLACED after self.session has become the new
        session. After the session has been assigned call
        self.subscribe_session() with the new session and
        self.unsubscribe_session with the old one.
        """
        oldsession = self.session
        self.session = session
        self.subscribe_session(self.session)
        self.issue(SESSION_REPLACED)
        self.maps_changed()
        if oldsession is not None:
            self.unsubscribe_session(oldsession)
            oldsession.Destroy()

    def SetPath(self, group, filename):
        """Store the application's default path for file dialogs extracted
        from a given filename.
        """
        self.path[group] = os.path.dirname( filename )

    def Path(self, group):
        """Return the application's default path for file dialogs."""
        return self.path[group]

    def subscribe_session(self, session):
        """Subscribe to some of the sessions channels.

        Extend this method in derived classes if you need additional
        channels.
        """
        session.Subscribe(MAPS_CHANGED, self.maps_changed)

    def unsubscribe_session(self, session):
        """Unsubscribe from the sessions channels.

        Extend this method in derived classes if you subscribed to
        additional channels in subscribe_session().
        """
        session.Unsubscribe(MAPS_CHANGED, self.maps_changed)

    def create_session(self):
        """Create a default session.

        Override this method in derived classes to instantiate the
        session differently or to use a different session class. Don't
        subscribe to channels here yet. Do that in the
        subscribe_session() method.
        """
        self.SetSession(create_empty_session())

    def OpenSession(self, filename, db_connection_callback = None,
                                    shapefile_callback = None):
        """Open the session in the file named filename"""
        # Make sure we deal with an absolute pathname. Otherwise we can
        # get problems when saving because the saving code expects an
        # absolute directory name
        filename = os.path.abspath(filename)
        if db_connection_callback is None:
            db_connection_callback = self.run_db_param_dialog
        if shapefile_callback is None:
            shapefile_callback = self.run_alt_path_dialog
        try:
            session = load_session(filename,
                               db_connection_callback=db_connection_callback,
                               shapefile_callback=shapefile_callback)
        except LoadCancelled:
            return
        session.SetFilename(filename)
        session.UnsetModified()
        self.SetSession(session)

        for map in session.Maps():
            for layer in map.Layers():
                if isinstance(layer, RasterLayer) \
                    and not Thuban.Model.resource.has_gdal_support():
                    msg = _("The current session contains Image layers,\n"
                            "but the GDAL library is not available to "
                            "draw them.")
                    dlg = wx.wxMessageDialog(None,
                                             msg,
                                             _("Library not available"),
                                             wx.wxOK | wx.wxICON_INFORMATION)
                    print msg
                    dlg.ShowModal()
                    dlg.Destroy()
                    break

    def run_db_param_dialog(self, parameters, message):
        """Implementation of the db_connection_callback for loading sessions"""
        dlg = dbdialog.DBDialog(None, _("DB Connection Parameters"),
                                parameters, message)
        return dlg.RunDialog()

    # run_alt_path_dialog: Raise a dialog to ask for an alternative path
    # if the shapefile couldn't be found.
    # TODO: 
    #   - Store a list of already used alternative paths and return these 
    #     iteratively (using a generator) 
    #   - How do we interact with the user to tell him we used a different 
    #     shapefile (location), mode "check"? The current approach with the 
    #     file dialog is not that comfortable.
    #
    def run_alt_path_dialog(self, filename, mode = None, second_try = 0):
        """Implemetation of the shapefile_callback while loading sessions.
        
           This implements two modes:
           - search: Search for an alternative path. If available from a 
             list of alrady known paths, else interactivly by file dialog.
             Currently the "second_try" is important since else the user might
             be caught in a loop.
           - check: Ask the user for confirmation, if a path from list has
             been found successful.

           Returns:
           - fname: The full path to the (shape) file.
           - from_list: Flags if the path was taken from list or entered
             manually.
        """

        if mode == "search":
            if self.Path('alt_path') == "" or second_try:
                dlg = altpathdialog.AltPathFileDialog(filename)
                fname = dlg.RunDialog()
                if fname is not None:
                    self.SetPath('alt_path', fname)    
                from_list = 0
            else:
                fname = os.path.join(self.Path('alt_path'),
                                     os.path.basename(filename))
                from_list = 1
        elif mode == "check":
                dlg = altpathdialog.AltPathConfirmDialog(filename)
                fname = dlg.RunDialog()
                if fname is not None:
                    self.SetPath('alt_path', fname)
                from_list = 0
        else:
            fname = None
            from_list = 0
        return fname, from_list


    def SaveSession(self):
        save_session(self.session, self.session.filename)

    def maps_changed(self, *args):
        """Subscribed to the session's MAPS_CHANGED messages.

        Set the toplevel window's map to the map in the session. This is
        done by calling the window's SetMap method with the map as
        argument. If the session doesn't have any maps None is used
        instead.

        Currently Thuban can only really handle at most one map in a
        sessions so the first map in the session's list of maps as
        returned by the Maps method is used.
        """
        # The mainwindow may not have been created yet, so check whether
        # it has been created before calling any of its methods
        if self.top is not None:
            if self.session.HasMaps():
                self.top.SetMap(self.session.Maps()[0])
            else:
                self.top.SetMap(None)

    in_exception_dialog = 0 # flag: are we already inside the exception dialog?

    def ShowExceptionDialog(self, exc_type, exc_value, exc_traceback):
        """Show a message box with information about an exception.
    
        The parameters are the usual values describing an exception in
        Python, the exception type, the value and the traceback.
    
        This method can be used as a value for the sys.excepthook.
        """

        if self.in_exception_dialog:
            return
        self.in_exception_dialog = 1
        while wxIsBusy():
            wxEndBusyCursor() # reset the mouse cursor

        try:
            lines = traceback.format_exception(exc_type, exc_value,
                                            exc_traceback)
            message = _("An unhandled exception occurred:\n%s\n"
                        "(please report to"
                        " http://thuban.intevation.org/bugtracker.html)"
                        "\n\n%s") % (exc_value, "".join(lines))
            print message

            # We don't use an explicit parent here because this method might
            # be called in circumstances where the main window doesn't exist
            # anymore.
            exceptiondialog.run_exception_dialog(None, message)

        finally:
            self.in_exception_dialog = 0
            # delete the last exception info that python keeps in
            # sys.last_* because especially last_traceback keeps
            # indirect references to all objects bound to local
            # variables and this might prevent some object from being
            # collected early enough.
            sys.last_type = sys.last_value = sys.last_traceback = None

