# -*- coding: UTF-8 -*-
#    This file is part of arena.
#
#    arena is free software: you can redistribute it and/or modify it
#    under the terms of the GNU General Public License as published by the
#    Free Software Foundation; either version 3 of the License or, at your
#    option, any later version.
#
#    arena is distributed in the hope that it will be useful, but
#    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABLITY
#    or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
#    License for more details.
#
#    You should have received a copy of the GNU General Public License along
#    with arena.  If not, see <http://www.gnu.org/licenses/>.

## @file
#  This file defines the basic content mediator for arena.  A content mediator
#  connects a content provider to a view that displays that content.

from Library.Application.BufferedCGIApplications import \
    ABufferedCGIApplication
from Library.Base.Events import ALoggingEventfulObject
from Library.UI.ContentProviders import AContentProvider
from Library.UI.Views import AView
from Library.Base.Events import AnEvent
from Library.IO.Logging.Logs import AMessageLog, AMessageWithTimeStamp
from Library.String.Utilities import String

import imp, os, sys, traceback, urllib

# The events generated by AContentMediator
cmEvents = {
  # Afer the object is initialized
  "init": None,
  # Before the object is deleted
  "delete": None,
  # On any error
  "error": None,
  # Before an item is logged
  "log": None,
  # Before a plugin is loaded
  "loadPlugin": None,
  # Before content is fetched
  "fetchContent": None,
  # Before a view is rendered
  "renderView": None
}

## The error message used when an invalid view or content provider is given to
#  the content mediator
cmInvalidProvider = "failed to generate content because an invalid {} " \
    "provider was provided to the {}"

## An error that may occur while mediating content.
class AContentMediatorError(AMessageWithTimeStamp):
    pass

# A content mediator, which connects a content provider to a view that displays
# that content.  This class does not implement AnApplication.run() and so should
# not be instantiated directly; rather, it serves to take care of the setup
# and execution of common tasks so that subclasses can focus simply on marrying
# content to presentation.
class AContentMediator(ABufferedCGIApplication):
    ## Constructor
    def __init__(Self, **params):
        ABufferedCGIApplication.__init__(Self, **params)
            
        # Construct the event generator
        params.update(Mediator=Self)
        Self._Events = ALoggingEventfulObject(**params)
        Self._options = Self._Events.parseParams(params)
        
    ## Initializer
    def init(Self):
        result = ABufferedCGIApplication.init(Self)
        
        if result:
            # Establish private attributes
            # The events that are generated by the content mediator
            Self._Events = None
            # The log that is used for error and debugging output
            Self._Log = None
            # The content provider for the mediator
            Self._ContentProvider = None
            # The view provider for the mediator
            Self._ViewProvider = None
            
        return result
        
    ## Destructor
    def __del__(Self):        
        if Self._ViewProvider is not None:
            del Self._ViewProvider
            
        if Self._ContentProvider is not None:
            del Self._ContentProvider
            
        if Self._Events is not None:
            del Self._Events
        
        if Self._Log is not None:
            del Self._Log
        
        ABufferedCGIApplication.__del__(Self)
        
    ## Construct or open a log file suitable for error and debugging output.
    #
    #  This method generates the \p openLog event.
    def openLog(Self, logFileName=None):
        Result = None
        
        Event = Self.generate("openLog", logFileName=logFileName)
        if Event.handled():
            Result = Event.result()
            del Event
            return Result
            
        del Event
            
        # Construct the log
        Result = AMessageLog(logFileName)
        
        return Result
        
    ## Generate the named event
    def generate(Self, eventName, **params):
        return Self._Events.generate(eventName, **params)
        
    ## Generate an error event.
    #
    #  If an event handler is specified for this event, it will receive an
    #  instance of AnEvent with a parameter named \p Error that refers to the
    #  exception that was raised because of the error.  The handler should set
    #  Event.handled() to \p True in order to prevent the exception from being
    #  raised; otherwise, if the event is not handled, it will be logged to the
    #  log associated with the class, and then an exception will be raised.
    #
    #  This routine generates the \p error event.
    def error(Self, Error):
        Self._Events.error(Error)

    ## Log the specified item.
    #
    #  This routine generates the \p log event.
    def log(Self, Item, freeItem=True):
        return Self._Events.log(Item, freeItem)
        
    ## Display an error that may occur while running the mediator
    def displayError(Self, Err):
        exc_type, exc_value, exc_traceback = sys.exc_info()            
        errlines = traceback.format_exception(exc_type, exc_value, 
            exc_traceback)
        errmsg = str(Err) + "<br />"
            
        trace = "<pre>"
        for line in errlines:
            trace += line + "<br />"
                
        trace += "</pre>"
         
        Self._Output.write(trace)  
        
    ## Load the specified plugin.
    #
    #  This method generates the \p loadPlugin event.
    #
    #  If \p identifier specifies the name of an identifier defined in the 
    #  plugin (a class, function, variable, etc.), then this method will return 
    #  a reference to that item; otherwise, it returns a reference to the 
    #  module itself.
    def loadPlugin(Self, pluginName, identifier=None):
        Result = None
        
        Event = Self.generate("loadPlugin", pluginName=pluginName,
            identifier=identifier)
        if Event.handled():
            Result = Event.result()
            del Event
            return Result
            
        del Event
        
        # Break the plugin name into its component parts
        pluginPath = String.explode(pluginName, list, ".")
        if len(pluginPath) == 0:
            return Result
        
        moduleFile = None
        modulePath = None
        moduleDescription = None
        PluginModule = None
        
        try:
            # Recurse through the module hierarchy
            for module in pluginPath:
                moduleFile, modulePath, moduleDescription = imp.find_module(
                    module, modulePath)
                PluginModule = imp.load_module(module, moduleFile, modulePath,
                    moduleDescription)
                    
                Result = PluginModule
                
                if moduleFile is None:    
                    modulePath = PluginModule.__path__
            
            # If an identifier was specified, then return a reference to it
            if identifier is not None:
                if identifier in Result.__dict__:
                    Result = Result.__dict__[identifier]
                    
                else:
                    Self.error(KeyError, identifier)
                    
            # Close the file
            moduleFile.close()
            moduleFile = None
        
        except Exception, Err:
            if moduleFile is not None:
                moduleFile.close()
                
            Self.error(Err)
            
        return Result
        
    ## Fetch content from the content provider.
    #
    #  This routine will attempt to call the class or function provided to the
    #  content mediator and will return the results of that call.
    #
    #  This routine generates the \p fetchContent event.
    def fetchContent(Self):
        result = tuple()
        
        Event = Self.generate("fetchContent")
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        if isinstance(Self._ContentProvider, AContentProvider):
            result = Self._ContentProvider.content()
            
        elif callable(Self._ContentProvider):
            result = Self._ContentProvider()
            
        else:
            Self.Error(AContentMediatorError(cmInvalidProvider, "content",
                Self.displayName()))
                
        return result
        
    ## Render content using the view provider.
    #
    #  This routine will attempt to call the class or function provided to the
    #  content mediator and will return the results of that call.
    #
    #  This routine generates the \p renderView event.
    def renderView(Self, content):
        result = ""
        
        Event = Self.generate("renderView", content=content)
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
        
        if isinstance(Self._ViewProvider, AView):
            result = Self._ViewProvider.render(content)
            
        elif callable(Self._ViewProvider):
            result = Self._ViewProvider(content)
            
        else:
            Self.error(AContentMediatorError(cmInvalidProvider, "view",
                Self.displayName()))
                
        return result        
        
    # Properties ---------------------------------------------------------------
    ## Set the handler for the specified event.
    #
    #  The \p newHandler parameter should refer to a callable object
    #  that can receive the event object.
    #
    #  This routine returns the previous handler for the specified event.
    def on(Self, eventName, newHandler):
        return Self._Events.on(eventName, newHandler)
        
    ## Set handlers for multiple events.
    #
    #  The parameters passed in \p events should follow the same format as those
    #  passed to the constructor for AnEventfulObject: the event to handle 
    #  should be specified by prefixing the event name with "on".
    #
    #  This method returns a dictionary object that contains the previous
    #  handler for each event.  Only those events in \ p events which are
    #  actually generated by the class will be registered; all other event names
    #  are discarded.
    def onMultiple(Self, **events):
        return Self._Events.onMultiple(**events)
        
    ## Retrieve the handler for the specified event.
    #
    #  If \p eventName refers to an event that is not generated by the class,
    #  this routine returns \p None.
    def handlerFor(Self, eventName):
        return Self._Events.handlerFor(eventName)
        
    ## Determine whether or not the object generates the named event.
    def generates(Self, eventName):
        return Self._Events.generates(eventName)
        
    ## Set or retrieve the content provider for the class.
    #
    #  If \p params is empty, this method simply returns the current content
    #  provider.  Alternatively, \p params may contain one of the following:
    #  - A single class instance, which should either be a reference to an 
    #    instance of AContentProvider (or its descendants); 
    #  - A single Python callable object that will return an iterable object 
    #    when it is called.
    #  - Two strings: the first should be the name of a plugin to load, 
    #    and the second should indicate the name of the class or function within
    #    that plugin to use as the content provider.  If a class name is 
    #    specified, the class will be instantiated and passed any keyword 
    #    parameters specified by \p keywordParams.  If a function is specified, 
    #    it is expected to return an iterable object when called.
    def ContentProvider(Self, *params, **keywordParams):
        PreviousProvider = Self._ContentProvider
        
        if len(params) > 0:
            # Set the new content provider
            NewProvider = params[0]
            
            # If two strings were passed, then attempt to load the specified
            # plugin and find the specified identifier
            if (len(params) > 1) and \
              (isinstance(NewProvider, basestring)):
                NewProvider = Self.loadPlugin(params[0], params[1])
                
                # If the new provider is a descendant of AContentProvider,
                # then instantiate it.
                if issubclass(NewProvider, AContentProvider):
                    # Include a reference to this class when instantiating
                    keywordParams.update(Mediator=Self)
                    NewProvider = NewProvider(**keywordParams)
                    
            Self._ContentProvider = NewProvider
            
        return PreviousProvider
        
    ## Set or retrieve the view provider for the class.
    #
    #  If \p params is empty, this method simply returns the current view
    #  provider.  Alternatively, \p params may contain one of the following:
    #  - A single class instance, which should either be a reference to an 
    #    instance of AView (or its descendants); 
    #  - A single Python callable object that accepts an iterable object, and
    #    that will return a string when it is called.
    #  - Two strings: the first should be the name of a plugin to load, 
    #    and the second should indicate the name of the class or function within
    #    that plugin to use as the view provider.  If a class name is specified,
    #    the class will be instantiated and passed any keyword parameters 
    #    specified by \p keywordParams.  If a function is specified, it is
    #    expected to return a string when called.
    def ViewProvider(Self, *params, **keywordParams):
        PreviousProvider = Self._ViewProvider
        
        if len(params) > 0:
            # Set the new content provider
            NewProvider = params[0]
            
            # If two strings were passed, then attempt to load the specified
            # plugin and find the specified identifier
            if (len(params) > 1) and \
              (isinstance(NewProvider, basestring)):
                NewProvider = Self.loadPlugin(params[0], params[1])
                
                # If the new provider is a descendant of AContentProvider,
                # then instantiate it.
                if issubclass(NewProvider, AView):
                    # Include a reference to this class when instantiating
                    keywordParams.update(Mediator=Self)
                    NewProvider = NewProvider(**keywordParams)
                    
            Self._ViewProvider = NewProvider
            
        return PreviousProvider     
