# -*- 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 customizes ADataSourceContentProvider to work with the default 
#  setup for arena.

from Library.UI.ContentFromDataSource import ADataSourceContentProvider, \
    dcpDataSourceError
from Library.Base.Events import AnEventfulObject
from Library.UI.ContentProviders import AContentProviderError
from Local.Configuration import Site

from Plugins.Session.Arena.Sessions import AnArenaSession
from Plugins.User.Arena.Users import AnArenaUser
from Plugins.User.Subscriptions import Right

from Library.String.Utilities import String
from Library.IO.StringStreams import AStringStream

## Events generated by AnArenaContentProvider
achEvents = {
    ## Before the session begins
    "beginSession": None,
    ## Before the user is loaded
    "fetchUser": None
}

## The session variable used to track the last channel displayed
achSessionVariable = "/channel/last"
## The error message used when the user is not subscribed to the current channel
achSubscriptionError = ("the user \"{}\" belongs to no group that has the "
                        "right to access \"{}\"")

## An arena content provider error
class AnArenaContentProviderError(AContentProviderError):
    pass

## This clas represents a channel record for arena.
class AnArenaContentProvider(ADataSourceContentProvider):
    ## Constructor.
    #
    #  The constructor expects that \p params will contain the following values:
    #  - \p Channel -- A reference to the AChannel instance that specified that
    #                  this class should be constructed.
    #
    #  The following optional items may also be provided in \p params:
    #  - \p Session -- A reference to an ASession instance that will be used
    #                  by the class.  If this value is not provided, or if it
    #                  is \p None, the class will construct its own session
    #                  handler.
    #  - \p User -- A reference to an AUser instance that will be used by
    #               the class.  If this value is not provided, or if it 
    #               is \p None, the class will construct its own user handler.
    def __init__(Self, fieldsValues=None, **params):
        ADataSourceContentProvider.__init__(Self, **params)
            
        if "Channel" in Self._params:
            Self._Channel = Self._params["Channel"]
            del Self._params["Channel"]
            
        if "Session" in Self._params:
            Self._Session = Self._params["Session"]
            del Self._params["Session"]
            
        if "User" in Self._params:
            Self._User = Self._params["User"]
            del Self._params["User"]
            
        Self._Request = Self._Mediator.Request
        
    ## Initializer
    def init(Self):
        result = ADataSourceContentProvider.init(Self)
        if result:
            # Establish private attributes
            # The channel instance that specified that this class should be
            # constructed
            Self._Channel = None
            # The HTTP request that prompted all of this
            Self._Request = None  
            # The session for which the channel is loaded
            Self._Session = None
            # The user for which the channel is loaded
            Self._User = None  
            
            # Update the events generated by the class
            Self._events.update(achEvents)      
            
        return result
        
    ## Destructor
    def __del__(Self):
        if Self._Session is not None:
            del Self._Session
            
        if Self._Channel is not None:
            del Self._Channel
            
        if Self._User is not None:
            del Self._User
            
        if Self._Request is not None:
            del Self._Request
            
        ADataSourceContentProvider.__del__(Self)
        
    ## Begin an arena session
    def beginSession(Self):
        Event = Self.generate("beginSession")
        if Event.handled():
            del Event
            return
            
        del Event
        
        Self._Session = AnArenaSession(
            Log = Self._Log,
            DataSource = Self._DataSource,
            ContentProvider = Self
        )
        
        Self._Session.open()
        
    ## Fetch the user specified by the session
    def fetchUser(Self):
        Event = Self.generate("fetchUser")
        if Event.handled():
            del Event
            return
            
        del Event
        
        Self._User = AnArenaUser(None,
            Log = Self._Log,
            DataSource = Self._DataSource,
            ContentProvider = Self
        )
        
        Self._User.fetch(Self._Session.userName())
                
    ## Fetch the content specified by the channel
    def fetchContent(Self):
        result = (None,)
        
        Event = Self.generate("fetchContent")
        if Event.handled():
            result = Event.result()
            del Event
            return result
            
        del Event
            
        try:
            # Set up a protected environment in which to execute Python code.
            # This environment is used to execute the Python code contained in
            # the 'source' and 'filter' meta attributes.
            variables = {
                "Request": Self._Request,
                "Channel": Self._Channel,
                "Site": Site,
                "Mediator": Self._Mediator,
                "DataSource": Self._DataSource,
                "Session": Self._Session,
                "User": Self._User,
                "Self": Self,
                "String": String,
                "source": "",
                "fields": [],
                "filter": [],
                "sort": [],
                "limit": -1,
                "offset": 0,
                "fullText": False
            }
                    
            # If a sort order was specified in the URI request, then use it
            if "sort" in Self._Request:
                variables["sort"] = \
                    String.explode(Self._Request["sort"].value)
                    
            # If a limit was specified in the URI request, then use it
            if "limit" in Self._Request:
                variables["limit"] = String.toLong(Self._Request["limit"].value)
            
            # If an offset was specified in the URI request, then use it
            if "offset" in Self._Request:
                variables["offset"] = \
                    String.toLong(Self._Request["offset"].value)    
                        
            # The "onFetchContent" attribute is expected to be Python code that
            # determines the category from which to retrieve content and the
            # filter criteria for that content.
            if "onFetchContent" in Self._Channel["meta"]:
                scriptlet = Self._Channel["meta"]["onFetchContent"] + "\n"
                # Correct for Python 2.6's lack of universal newline handling
                scriptlet = scriptlet.replace("\r", "\n")
                bytecode = compile(scriptlet, "<string>", "exec")
                
                exec(bytecode, variables)
                
        except Exception,Err:
            Self.error(AnArenaContentProviderError(dcpDataSourceError,
                Self._Channel.name(), Self._DataSource.name(),
                str(Err)))                 

        # Copy certain variables back to the client request, even if they
        # were not originally defined as part of that request.  This allows
        # views to have access to the values as well.
        Self._Request["sort"] = String.implode(variables["sort"])
        Self._Request["limit"] = variables["limit"]
        Self._Request["offset"] = variables["offset"]

        # Only return posts for a channel that specifies that it needs them
        if len(variables["source"]) == 0:
            return result
            
        # Fetch the content
        result = Self._DataSource.fetch(variables["source"],
            fieldNames = variables["fields"],
            criteria = variables["filter"],
            sort = variables["sort"],
            limit = variables["limit"],
            offset = variables["offset"],
            fullText = variables["fullText"])
                
        del variables
        
        return result

    # Properties ---------------------------------------------------------------
    ## Fetch content for the channel
    def content(Self):
        # Begin the arena session
        if Self._Session is None:
            Self.beginSession()
        
        # Load the user specified by the session
        if Self._User is None:
            Self.fetchUser()
            
        # Determine whether the user is subscribed to the current channel
        if Self._User.has(Right.toRead, Self._Channel.name()) == False:
            Self.error(AnArenaContentProviderError(achSubscriptionError,
                Self._User.name(), Self._Channel.name()))              
        
        # Fetch the content
        return Self.fetchContent()
        
    ## Retrieve the channel that specified this class should be constructed
    def Channel(Self):
        return Self._Channel
        
    ## Retrieve the session manager used by this class
    def Session(Self):
        return Self._Session
        
    ## Retrieve the user record used by this class
    def User(Self):
        return Self._User
        
    ## Return the name of the channel that constructed this class
    def name(Self):
        return Self._Channel.name()
        
## This class extends AnArenaContentProvider to allow post content to be parsed 
#  before it is returned.
class AnArenaParsedContentProvider(AnArenaContentProvider):
    ## Constructor
    def __init__(Self, **params):
        AnArenaContentProvider.__init__(Self, **params)
        
        Self._contentParameters.update({
            "Mediator": Self._Mediator,
            "Channel": Self._Channel
        })
        
    ## Initializer
    def init(Self):
        result = AnArenaContentProvider.init(Self)
        
        if result:
            # Establish private attributes
            # The content handler to use for content
            Self._ContentHandler = None
            # Parameters to pass to the content handler
            Self._contentParameters = {}
            # The content retrieved from the data source
            Self._content = None
            # The iterator over the content retrieved from the data source
            Self._iter = None
            
        return result     
        
    def next(Self):
        if Self._iter is not None:
            nextItem = Self._iter.next()
            
            if nextItem is not None:                
                return Self._ContentHandler(nextItem, 
                    **Self._contentParameters)    
        
        raise StopIteration
        
    # Properties ---------------------------------------------------------------
    ## Fetch content for the channel.
    def content(Self):
        # Load the content handler plugin, if one is specified
        if "contentHandler" in Self._Channel["meta"]:
            contentType = \
                String.explode(Self._Channel["meta"]["contentHandler"])
            
            contentParameters = {}
            if "contentParameters" in Self._Channel["meta"]:
                contentParameters.update(
                    String.explode(Self._Channel["meta"]["contentParameters"],
                        dict
                    )
                )
            
            Self.ContentHandler(*contentType, **contentParameters)
            
        Self._content = AnArenaContentProvider.content(Self)
        Self._iter = iter(Self._content)
        
        # Return an iterator over the content that will allow us to load
        # our content handler for each content item retrieved by the view.
        return Self
        
    ## Retrieve or set the content handler used by the content provider.
    #  If \p params is empty, this method simply returns the current content
    #  handler.  Alternatively, \p params may contain one of the following:
    #  - A single class instance, which should either be a reference to an 
    #    instance of APost (or its descendants); 
    #  - Two strings: the first should be the name of a plugin to load, 
    #    and the second should indicate the name of a class within that plugin 
    #    to use as the content handler.  The class will be instantiated for each 
    #    content element (or post) needed while the view is being rendered.
    #    Any \p keywordParams specified will be passed to the content handler
    #    when it is constructed.
    def ContentHandler(Self, *params, **keywordParams):
        PreviousHandler = Self._ContentHandler
        
        if len(params) > 0:
            # Set the new data source
            NewHandler = 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(NewHandler, basestring)):
                NewHandler = Self._Mediator.loadPlugin(params[0], params[1])
            
            Self._ContentHandler = NewHandler
            Self._contentParameters.update(**keywordParams)

        return PreviousHandler
    
    ## Return an iterator object.
    #
    #  This method allows the instance of AnArenaParsedContentProvider to
    #  pretend that it is the content returned by the data source; this allows
    #  it to parse each record as it is retrieved for display by the view.    
    def __iter__(Self):
        return Self
        
    ## Return the "length" of the content -- that is, how many records there 
    #  are.
    def __len__(Self):
        return len(Self._content)
