# -*- test-case-name: xmantissa.test.test_webapp -*-

"""
This module is the basis for Mantissa-based web applications.  It provides
several basic pluggable application-level features, most notably Powerup-based
integration of the extensible hierarchical navigation system defined in
xmantissa.webnav
"""

import os, sha

from zope.interface import implements

from twisted.python.filepath import FilePath

from epsilon.structlike import record

from axiom.item import Item, declareLegacyItem
from axiom.attributes import text, integer, reference
from axiom.userbase import getAccountNames
from axiom import upgrade
from axiom.dependency import dependsOn

from nevow.rend import Page, NotFound
from nevow import livepage, athena
from nevow.inevow import IRequest, IResource, IQ
from nevow import tags as t
from nevow import url, static

from xmantissa.publicweb import CustomizedPublicPage
from xmantissa.website import PrefixURLMixin, JUST_SLASH, WebSite
from xmantissa.webtheme import getInstalledThemes, getAllThemes
from xmantissa.webnav import getTabs, getSelectedTab, setTabURLs
from xmantissa._webidgen import genkey, storeIDToWebID, webIDToStoreID
from xmantissa.fragmentutils import dictFillSlots
from xmantissa.offering import getInstalledOfferings

from xmantissa.ixmantissa import INavigableFragment, INavigableElement,\
    ISiteRootPlugin, IWebTranslator, IStaticShellContent

from xmantissa.webgestalt import AuthenticationApplication
from xmantissa.prefs import PreferenceAggregator, DefaultPreferenceCollection
from xmantissa.search import SearchAggregator

def _reorderForPreference(themeList, preferredThemeName):
    """
    Re-order the input themeList according to the preferred theme.

    Returns None.
    """
    for t in themeList:
        if preferredThemeName == t.themeName:
            themeList.remove(t)
            themeList.insert(0,t)
            return

class _WebIDFormatException(TypeError):
    """
    An inbound web ID was not formatted as expected.
    """

class NavMixin(object):

    fragmentName = 'main'
    username = None
    searchPattern = None

    def __init__(self, webapp, pageComponents):
        self.webapp = webapp
        self.pageComponents = pageComponents
        setTabURLs(self.pageComponents.navigation, webapp)

    def getDocFactory(self, fragmentName, default=None):
        return self.webapp.getDocFactory(fragmentName, default)

    def render_content(self, ctx, data):
        raise NotImplementedError("implement render_context in subclasses")

    def render_appNavigation(self, ctx, data):
        selectedTab = getSelectedTab(self.pageComponents.navigation,
                                     url.URL.fromContext(ctx))

        getp = IQ(self.docFactory).onePattern

        for tab in self.pageComponents.navigation:
            if tab == selectedTab or selectedTab in tab.children:
                p = 'selected-app-tab'
                contentp = 'selected-tab-contents'
            else:
                p = 'app-tab'
                contentp = 'tab-contents'

            yield dictFillSlots(getp(p),
                    {'name': tab.name,
                     'tab-contents': getp(contentp).fillSlots('href', tab.linkURL)})

    def render_navigation(self, ctx, data):
        getp = IQ(self.docFactory).onePattern

        def fillSlots(tabs):
            for tab in tabs:
                if tab.children:
                    kids = getp('subtabs').fillSlots('kids', fillSlots(tab.children))
                else:
                    kids = ''

                yield dictFillSlots(getp('tab'), dict(href=tab.linkURL,
                                                      name=tab.name,
                                                      kids=kids))

        return fillSlots(self.pageComponents.navigation)

    def render_title(self, ctx, data):
        return ctx.tag[self.__class__.__name__]

    def render_search(self, ctx, data):
        searchAggregator = self.pageComponents.searchAggregator

        if searchAggregator is None or not searchAggregator.providers():
            return ''
        return IQ(self.docFactory).patternGenerator("search")()

    def render_searchFormAction(self, ctx, data):
        searchAggregator = self.pageComponents.searchAggregator

        if searchAggregator is None or not searchAggregator.providers():
            action = ''
        else:
            action = self.webapp.linkTo(searchAggregator.storeID)

        return ctx.tag.fillSlots('form-action', action)

    def render_username(self, ctx, data):
        if self.username is None:
            for (user, domain) in getAccountNames(self.webapp.store):
                self.username = '%s@%s' % (user, domain)
                break
            else:
                self.username = 'nobody@noplace'
        return ctx.tag[self.username]

    def data_settingsLink(self, ctx, data):
        return self.webapp.linkTo(self.pageComponents.settings.storeID)

    def render_head(self, ctx, data):
        return ctx.tag

    def render_header(self, ctx, data):
        staticShellContent = self.pageComponents.staticShellContent
        if staticShellContent is None:
            return ctx.tag
        header = staticShellContent.getHeader()
        if header is not None:
            return ctx.tag[header]
        else:
            return ctx.tag

    def _getVersions(self):
        versions = []
        for (name, offering) in getInstalledOfferings(self.webapp.store.parent).iteritems():
            if offering.version is not None:
                v = offering.version
                versions.append(str(v).replace(v.package, name))
        return ' '.join(versions)

    def render_footer(self, ctx, data):
        footer = [self._getVersions()]
        staticShellContent = self.pageComponents.staticShellContent
        if staticShellContent is not None:
            f = staticShellContent.getFooter()
            if f is not None:
                footer.append(f)
        return ctx.tag[footer]

INSPECTROFY = os.environ.get('MANTISSA_DEV')

class FragmentWrapperMixin:
    def __init__(self, fragment, pageComponents):
        self.fragment = fragment
        fragment.page = self
        self.pageComponents = pageComponents

    def beforeRender(self, ctx):
        return getattr(self.fragment, 'beforeRender', lambda x: None)(ctx)

    def render_introspectionWidget(self, ctx, data):
        "Until we have eliminated everything but GenericAthenaLivePage"
        if INSPECTROFY:
            return ctx.tag['No debugging on crap-ass bad pages']
        else:
            return ''

    def render_head(self, ctx, data):
        req = IRequest(ctx)

        userStore = self.webapp.store
        siteStore = userStore.parent
        website = IResource(siteStore)

        l = self.pageComponents.themes
        _reorderForPreference(l, self.webapp.preferredTheme)
        extras = []
        for theme in l:
            extra = theme.head(req, website)
            if extra is not None:
                extras.append(extra)
        extra = self.fragment.head()
        if extra is not None:
            extras.append(extra)
        return ctx.tag[extras]

    def render_title(self, ctx, data):
        title = getattr(self.fragment, 'title', None)
        if not title:
            title = 'Divmod'
        return ctx.tag[title]

    def render_content(self, ctx, data):
        return ctx.tag[self.fragment]

class GenericNavigationPage(FragmentWrapperMixin, Page, NavMixin):
    def __init__(self, webapp, fragment, pageComponents):
        Page.__init__(self, docFactory=webapp.getDocFactory('shell'))
        NavMixin.__init__(self, webapp, pageComponents)
        FragmentWrapperMixin.__init__(self, fragment, pageComponents)


class GenericNavigationLivePage(FragmentWrapperMixin, livepage.LivePage, NavMixin):
    def __init__(self, webapp, fragment, pageComponents):
        livepage.LivePage.__init__(self, docFactory=webapp.getDocFactory('shell'))
        NavMixin.__init__(self, webapp, pageComponents)
        FragmentWrapperMixin.__init__(self, fragment, pageComponents)

    # XXX TODO: support live nav, live fragments somehow
    def render_head(self, ctx, data):
        ctx.tag[t.invisible(render=t.directive("liveglue"))]
        return FragmentWrapperMixin.render_head(self, ctx, data)

    def goingLive(self, ctx, client):
        getattr(self.fragment, 'goingLive', lambda x, y: None)(ctx, client)

    def locateHandler(self, ctx, path, name):
        handler = getattr(self.fragment, 'locateHandler', None)

        if handler is None:
            return getattr(self.fragment, 'handle_' + name)
        else:
            return handler(ctx, path, name)



_moduleToHash = {}
_hashToFile = {}
class GenericNavigationAthenaPage(athena.LivePage, FragmentWrapperMixin, NavMixin):
    def __init__(self, webapp, fragment, pageComponents):

        userStore = webapp.store
        siteStore = userStore.parent
        self.website = IResource(siteStore)

        athena.LivePage.__init__(
            self,
            getattr(fragment, 'iface', None),
            fragment,
            jsModuleRoot=None,
            transportRoot=url.root.child('live'),
            docFactory=webapp.getDocFactory('shell'))
        NavMixin.__init__(self, webapp, pageComponents)
        FragmentWrapperMixin.__init__(self, fragment, pageComponents)


    def _setJSModuleRoot(self, ctx):
        req = IRequest(ctx)
        hostname = req.getHeader('host')
        root = self.website.encryptedRoot(hostname)
        if root is None:
            root = url.URL.fromString('/')
        self.jsModuleRoot = root.child('private').child('jsmodule')


    def renderHTTP(self, ctx):
        """
        Capture the value of the C{Host} header for this request so that we can
        generate URLs with it later on during the page render.
        """
        self._setJSModuleRoot(ctx)
        return super(GenericNavigationAthenaPage, self).renderHTTP(ctx)


    def render_head(self, ctx, data):
        ctx.tag[t.invisible(render=t.directive("liveglue"))]
        return FragmentWrapperMixin.render_head(self, ctx, data)


    def render_introspectionWidget(self, ctx, data):
        if INSPECTROFY:
            f = athena.IntrospectionFragment()
            f.setFragmentParent(self)
            return ctx.tag[f]
        else:
            return ''


    def getJSModuleURL(self, moduleName):
        """
        Retrieve an URL at which the given module can be found.

        The default L{athena.LivePage} behavior is overridden here to give each
        module a permanent, unique, totally cachable URL based on its named and
        its contents.  This lets browser skip any requests for this module
        after the first as long as it hasn't changed, and forces it to
        re-request it as soon as it changes.
        """
        fp = FilePath(self.jsModules.mapping[moduleName])
        lastHash, lastTime = _moduleToHash.get(moduleName, (None, 0))
        if lastTime != fp.getmtime():
            thisHash = sha.new(fp.open().read()).hexdigest()
            _moduleToHash[moduleName] = (fp.getmtime(), thisHash)
            _hashToFile[thisHash] = fp.path
        else:
            thisHash = lastHash

        return self.jsModuleRoot.child(thisHash).child(moduleName)


    def locateChild(self, ctx, segments):
        res = NotFound

        if hasattr(self.fragment, 'locateChild'):
            res = self.fragment.locateChild(ctx, segments)

        if res is NotFound:
            try:
                self.webapp.fromWebID(segments[0])
            except TypeError:
                pass
            else:
                res = (self.webapp.createResource(), segments)

        if res is NotFound:
            res = super(GenericNavigationAthenaPage, self).locateChild(ctx, segments)

        return res



class HashedJSModuleNames(athena.JSModules):
    """
    An Athena module-serving resource which handles hashed names instead of
    regular module names.
    """
    def resourceFactory(self, fileName):
        """
        Retrieve an L{inevow.IResource} to render the contents of the given
        file.

        Override the default behavior to return a resource which can be cached
        for a long dang time.
        """
        return static.Data(
            file(fileName).read(),
            'text/javascript',
            expires=(60 * 60 * 24 * 365 * 5))



class PrivateRootPage(Page, NavMixin):
    addSlash = True

    def __init__(self, webapp, pageComponents):
        self.webapp = webapp
        Page.__init__(self, docFactory=webapp.getDocFactory('shell'))
        NavMixin.__init__(self, webapp, pageComponents)

    def child_(self, ctx):
        navigation = self.pageComponents.navigation
        if not navigation:
            return self
        # /private/XXXX ->
        click = self.webapp.linkTo(navigation[0].storeID)
        return url.URL.fromContext(ctx).click(click)

    def render_content(self, ctx, data):
        return """
        You have no default root page set, and no navigation plugins installed.  I
        don't know what to do.
        """

    def render_title(self, ctx, data):
        return ctx.tag['Private Root Page (You Should Not See This)']

    def child_jsmodule(self, ctx):
        """
        Retrieve a resource with JavaScript modules as child resources.
        """
        return HashedJSModuleNames(_hashToFile)

    def childFactory(self, ctx, name):
        try:
            o = self.webapp.fromWebID(name)
        except _WebIDFormatException:
            return None
        if o is None:
            return None
        res = IResource(o, None)
        if res is not None:
            return res
        fragment = INavigableFragment(o, None)
        if fragment is None:
            return None
        if fragment.fragmentName is not None:
            fragDocFactory = self.webapp.getDocFactory(fragment.fragmentName, None)
            if fragDocFactory is not None:
                fragment.docFactory = fragDocFactory
        if fragment.docFactory is None:
            raise RuntimeError("%r (fragment name %r) has no docFactory" % (fragment, fragment.fragmentName))

        if isinstance(fragment, (athena.LiveFragment, athena.LiveElement)):
            pageClass = GenericNavigationAthenaPage
        else:
            pageClass = {False: GenericNavigationPage,
                         True: GenericNavigationLivePage}.get(fragment.live)
        return pageClass(self.webapp, fragment, self.pageComponents)


class _PageComponents(record('navigation searchAggregator staticShellContent settings themes')):
    """
    I encapsulate various plugin objects that have some say
    in determining the available functionality on a given page
    """
    pass

class PrivateApplication(Item, PrefixURLMixin):
    """
    This is the root of a private, navigable web application.  It is designed
    to be installed on avatar stores after installing WebSite.

    To plug into it, install powerups of the type INavigableElement on the
    user's store.  Their tabs will be retrieved and items that are part of
    those powerups will be linked to; provide adapters for said items to either
    INavigableFragment or IResource.

    Note: IResource adapters should be used sparingly, for example, for
    specialized web resources which are not 'nodes' within the application; for
    example, that need to set a custom content/type or that should not display
    any navigation elements because they will be displayed only within IFRAME
    nodes.  Do _NOT_ use IResource adapters to provide a customized
    look-and-feel; instead use mantissa themes.  (XXX document webtheme.py more
    thoroughly)

    @ivar preferredTheme: A C{unicode} string naming the preferred theme for
    this application.  Templates and suchlike will be looked up for this theme
    first.

    @ivar hitCount: Number of page loads of this application.

    @ivar privateKey: A random integer used to deterministically but
    unpredictably perturb link generation to avoid being the target of XSS
    attacks.

    @ivar privateIndexPage: A reference to the Item whose IResource or
    INavigableFragment adapter will be displayed on login and upon viewing the
    'root' page, /private/.
    """

    implements(ISiteRootPlugin, IWebTranslator)

    powerupInterfaces = (IWebTranslator,)

    typeName = 'private_web_application'
    schemaVersion = 3


    preferredTheme = text()
    hitCount = integer(default=0)
    privateKey = integer(defaultFactory=genkey)

    website = dependsOn(WebSite)

    customizedPublicPage = dependsOn(CustomizedPublicPage)
    authenticationApplication = dependsOn(AuthenticationApplication)
    preferenceAggregator = dependsOn(PreferenceAggregator)
    defaultPreferenceCollection = dependsOn(DefaultPreferenceCollection)
    searchAggregator = dependsOn(SearchAggregator)

    #XXX Nothing ever uses this
    privateIndexPage = reference()

    prefixURL = 'private'

    sessioned = True
    sessionless = False

    def getPageComponents(self):
        navigation = getTabs(self.store.powerupsFor(INavigableElement))

        staticShellContent = IStaticShellContent(self.store, None)

        return _PageComponents(navigation,
                               self.searchAggregator,
                               staticShellContent,
                               self.store.findFirst(PreferenceAggregator),
                               getInstalledThemes(self.store.parent))

    def createResource(self):
        return PrivateRootPage(self, self.getPageComponents())

    # ISiteRootPlugin
    def resourceFactory(self, segments):
        if segments == JUST_SLASH:
            return self.createResource(), JUST_SLASH
        else:
            return super(PrivateApplication, self).resourceFactory(segments)


    # IWebTranslator
    def linkTo(self, obj):
        # currently obj must be a storeID, but other types might come eventually
        return '/%s/%s' % (self.prefixURL, storeIDToWebID(self.privateKey, obj))

    def linkToWithActiveTab(self, childItem, parentItem):
        """
        Return a URL which will point to the web facet of C{childItem},
        with the selected nav tab being the one that represents C{parentItem}
        """
        return self.linkTo(parentItem.storeID) + '/' + self.toWebID(childItem)

    def linkFrom(self, webid):
        return webIDToStoreID(self.privateKey, webid)

    def fromWebID(self, webID):
        storeID = self.linkFrom(webID)
        if storeID is None:
            # This is not a very good interface, but I don't want to change the
            # calling code right now as I'm neither confident in its test
            # coverage nor looking to go on a test-writing rampage through this
            # code for a minor fix.
            raise _WebIDFormatException("%r didn't look like a webID" % (webID,))
        webitem = self.store.getItemByID(storeID, None)
        return webitem

    def toWebID(self, item):
        return storeIDToWebID(self.privateKey, item.storeID)


    def getDocFactory(self, fragmentName, default=None):
        l = getAllThemes()
        _reorderForPreference(l, self.preferredTheme)
        for t in l:
            fact = t.getDocFactory(fragmentName, None)
            if fact is not None:
                return fact
        return default

declareLegacyItem(PrivateApplication.typeName, 2, dict(
    installedOn = reference(),
    preferredTheme = text(),
    hitCount = integer(default=0),
    privateKey = integer(),
    privateIndexPage = reference(),
    ))

def upgradePrivateApplication1To2(oldApp):
    newApp = oldApp.upgradeVersion(
        'private_web_application', 1, 2,
        installedOn=oldApp.installedOn,
        preferredTheme=oldApp.preferredTheme,
        hitCount=oldApp.hitCount,
        privateKey=oldApp.privateKey,
        privateIndexPage=oldApp.privateIndexPage)
    newApp.store.powerup(newApp.store.findOrCreate(
        CustomizedPublicPage), ISiteRootPlugin, -257)
    return newApp

upgrade.registerUpgrader(upgradePrivateApplication1To2, 'private_web_application', 1, 2)

def _upgradePrivateApplication2to3(old):
    pa = old.upgradeVersion(PrivateApplication.typeName, 2, 3,
        preferredTheme=old.preferredTheme,
        hitCount=old.hitCount,
        privateKey=old.privateKey,
        privateIndexPage=old.privateIndexPage)
    pa.customizedPublicPage = old.store.findOrCreate(CustomizedPublicPage)
    pa.authenticationApplication = old.store.findOrCreate(AuthenticationApplication)
    pa.preferenceAggregator = old.store.findOrCreate(PreferenceAggregator)
    pa.defaultPreferenceCollection = old.store.findOrCreate(DefaultPreferenceCollection)
    pa.searchAggregator = old.store.findOrCreate(SearchAggregator)
    pa.website = old.store.findOrCreate(WebSite)

upgrade.registerUpgrader(_upgradePrivateApplication2to3, PrivateApplication.typeName, 2, 3)


syntax highlighted by Code2HTML, v. 0.9.1