"""Provides a publisher object that behaves like the Quixote 1 Publisher.
Specifically, arbitrary namespaces may be exported and the HTTPRequest
object is passed as the first argument to exported functions.  Also,
the _q_lookup(), _q_resolve(), and _q_access() methods work as they did
in Quixote 1.
"""

import sys
import re
import types
import warnings
from quixote import errors, get_request, redirect
from quixote.publish import Publisher as _Publisher
from quixote.directory import Directory
from quixote.html import htmltext


class Publisher(_Publisher):
    """
    Instance attributes:
      namespace_stack : [ module | instance | class ]
    """

    def __init__(self, root_namespace, config=None):
        from quixote.config import Config
        if type(root_namespace) is types.StringType:
            root_namespace = _get_module(root_namespace)
        self.namespace_stack = [root_namespace]
        if config is None:
            config = Config()
        directory = RootDirectory(root_namespace, self.namespace_stack)
        _Publisher.__init__(self, directory, config=config)

    def debug(self, msg):
        self.log(msg)

    def get_namespace_stack(self):
        """get_namespace_stack() ->  [ module | instance | class ]
        """
        return self.namespace_stack


class RootDirectory(Directory):
    def __init__(self, root_namespace, namespace_stack):
        self.root_namespace = root_namespace
        self.namespace_stack = namespace_stack

    def _q_traverse(self, path):
        # Initialize the publisher's namespace_stack
        del self.namespace_stack[:]

        request = get_request()

        # Traverse package to a (hopefully-) callable object
        object = _traverse_url(self.root_namespace, path, request,
                               self.namespace_stack)

        # None means no output -- traverse_url() just issued a redirect.
        if object is None:
            return None

        # Anything else must be either a string...
        if isstring(object):
            output = object

        # ...or a callable.
        elif callable(object):
            output = object(request)
            if output is None:
                raise RuntimeError, 'callable %s returned None' % repr(object)

        # Uh-oh: 'object' is neither a string nor a callable.
        else:
            raise RuntimeError(
                "object is neither callable nor a string: %s" % repr(object))

        return output


def _get_module(name):
    """Get a module object by name."""
    __import__(name)
    module = sys.modules[name]
    return module


_slash_pat = re.compile("//*")

def _traverse_url(root_namespace, path_components, request, namespace_stack):
    """(root_namespace : any, path_components : [string],
        request : HTTPRequest, namespace_stack : list) -> (object : any)

    Perform traversal based on the provided path, starting at the root
    object.  It returns the script name and path info values for
    the arrived-at object, along with the object itself and
    a list of the namespaces traversed to get there.

    It's expected that the final object is something callable like a
    function or a method; intermediate objects along the way will
    usually be packages or modules.

    To prevent crackers from writing URLs that traverse private
    objects, every package, module, or object along the way must have
    a _q_exports attribute containing a list of publicly visible
    names.  Not having a _q_exports attribute is an error, though
    having _q_exports be an empty list is OK.  If a component of the path
    isn't in _q_exports, that also produces an error.

    Modifies the namespace_stack as it traverses the url, so that
    any exceptions encountered along the way can be handled by the
    nearest handler.
    """

    path = '/' + '/'.join(path_components)

    # If someone accesses a Quixote driver script without a trailing
    # slash, we'll wind up here with an empty path.  This won't
    # work; relative references in the page generated by the root
    # namespace's _q_index() will be off.  Fix it by redirecting the
    # user to the right URL; when the client follows the redirect,
    # we'll wind up here again with path == '/'.
    if not path:
        return redirect(request.environ['SCRIPT_NAME'] + '/' , permanent=1)

    # Traverse starting at the root
    object = root_namespace
    namespace_stack.append(object)

    # Loop over the components of the path
    for component in path_components:
        if component == "":
            # "/q/foo/" == "/q/foo/_q_index"
            component = "_q_index"
        object = _get_component(object, component, request, namespace_stack)

    if not (isstring(object) or callable(object)):
        # We went through all the components of the path and ended up at
        # something which isn't callable, like a module or an instance
        # without a __call__ method.
        if path[-1] != '/':
            if not request.form:
                # This is for the convenience of users who type in paths.
                # Repair the path and redirect.  This should not happen for
                # URLs within the site.
                return redirect(request.get_path() + "/", permanent=1)

            else:
                # Automatic redirects disabled or there is form data.  If
                # there is form data then the programmer is using the
                # wrong path.  A redirect won't work if the form data came
                # from a POST anyhow.
                raise errors.TraversalError(
                    "object is neither callable nor string "
                    "(missing trailing slash?)",
                    private_msg=repr(object),
                    path=path)
        else:
            raise errors.TraversalError(
                "object is neither callable nor string",
                private_msg=repr(object),
                path=path)

    return object


def _get_component(container, component, request, namespace_stack):
    """Get one component of a path from a namespace.
    """
    # First security check: if the container doesn't even have an
    # _q_exports list, fail now: all Quixote-traversable namespaces
    # (modules, packages, instances) must have an export list!
    if not hasattr(container, '_q_exports'):
        raise errors.TraversalError(
                    private_msg="%r has no _q_exports list" % container)

    # Second security check: call _q_access function if it's present.
    if hasattr(container, '_q_access'):
        # will raise AccessError if access failed
        container._q_access(request)

    # Third security check: make sure the current name component
    # is in the export list or is '_q_index'.  If neither
    # condition is true, check for a _q_lookup() and call it.
    # '_q_lookup()' translates an arbitrary string into an object
    # that we continue traversing.  (This is very handy; it lets
    # you put user-space objects into your URL-space, eliminating
    # the need for digging ID strings out of a query, or checking
    # PATHINFO after Quixote's done with it.  But it is a
    # compromise to security: it opens up the traversal algorithm
    # to arbitrary names not listed in _q_exports!)  If
    # _q_lookup() doesn't exist or is None, a TraversalError is
    # raised.

    # Check if component is in _q_exports.  The elements in
    # _q_exports can be strings or 2-tuples mapping external names
    # to internal names.
    if component in container._q_exports or component == '_q_index':
        internal_name = component
    else:
        # check for an explicit external to internal mapping
        for value in container._q_exports:
            if type(value) is types.TupleType:
                if value[0] == component:
                    internal_name = value[1]
                    break
        else:
            internal_name = None

    if internal_name is None:
        # Component is not in exports list.
        object = None
        if hasattr(container, "_q_lookup"):
            object = container._q_lookup(request, component)
        elif hasattr(container, "_q_getname"):
            warnings.warn("_q_getname() on %s used; should "
                          "be replaced by _q_lookup()" % type(container))
            object = container._q_getname(request, component)
        if object is None:
            raise errors.TraversalError(
                private_msg="object %r has no attribute %r" % (
                                                    container,
                                                    component))

    # From here on, you can assume that the internal_name is not None
    elif hasattr(container, internal_name):
        # attribute is in _q_exports and exists
        object = getattr(container, internal_name)

    elif internal_name == '_q_index':
        if hasattr(container, "_q_lookup"):
            object = container._q_lookup(request, "")
        else:
            raise errors.AccessError(
                private_msg=("_q_index not found in %r" % container))

    elif hasattr(container, "_q_resolve"):
        object = container._q_resolve(internal_name)
        if object is None:
            raise RuntimeError, ("component listed in _q_exports, "
                                 "but not returned by _q_resolve(%r)"
                                 % internal_name)
        else:
            # Set the object, so _q_resolve won't need to be called again.
            setattr(container, internal_name, object)

    elif type(container) is types.ModuleType:
        # try importing it as a sub-module.  If we get an ImportError
        # here we don't catch it.  It means that something that
        # doesn't exist was exported or an exception was raised from
        # deeper in the code.
        mod_name = container.__name__ + '.' + internal_name
        object = _get_module(mod_name)

    else:
        # a non-existent attribute is in _q_exports,
        # and the container is not a module.  Give up.
        raise errors.TraversalError(
                private_msg=("%r in _q_exports list, "
                             "but not found in %r" % (component,
                                                      container)))

    namespace_stack.append(object)
    return object


def isstring(x):
    return isinstance(x, (str, unicode, htmltext))
