"""Error classes for CherryPy."""

import urllib
import urlparse


class Error(Exception):
    pass

class NotReady(Error):
    """A request was made before the app server has been started."""
    pass

class WrongConfigValue(Error):
    """ Happens when a config value can't be parsed, or is otherwise illegal. """
    pass

class RequestHandled(Exception):
    """Exception raised when no further request handling should occur."""
    pass

class InternalRedirect(Exception):
    """Exception raised when processing should be handled by a different path.
    
    If you supply 'params', it will be used to re-populate params.
    If 'params' is a dict, it will be used directly.
    If 'params' is a string, it will be converted to a dict using cgi.parse_qs.
    
    If you omit 'params', the params from the original request will
    remain in effect, including any POST parameters.
    """
    
    def __init__(self, path, params=None):
        import cherrypy
        import cgi
        request = cherrypy.request
        
        # Note that urljoin will "do the right thing" whether url is:
        #  1. a URL relative to root (e.g. "/dummy")
        #  2. a URL relative to the current path
        # Note that any querystring will be discarded.
        path = urlparse.urljoin(cherrypy.request.path, path)
        
        # Set a 'path' member attribute so that code which traps this
        # error can have access to it.
        self.path = path
        
        if params is not None:
            if isinstance(params, basestring):
                request.query_string = params
                request.queryString = request.query_string # Backward compatibility
                pm = cgi.parse_qs(params, keep_blank_values=True)
                for key, val in pm.items():
                    if len(val) == 1:
                        pm[key] = val[0]
                request.params = pm
            else:
                request.query_string = urllib.urlencode(params)
                request.queryString = request.query_string
                request.params = params.copy()
        
        Exception.__init__(self, path, params)



class HTTPRedirect(Exception):
    """Exception raised when the request should be redirected.
    
    The new URL must be passed as the first argument to the Exception, e.g.,
        cperror.HTTPRedirect(newUrl). Multiple URLs are allowed. If a URL
        is absolute, it will be used as-is. If it is relative, it is assumed
        to be relative to the current cherrypy.request.path.
    """
    
    def __init__(self, urls, status=None):
        import cherrypy
        
        if isinstance(urls, basestring):
            urls = [urls]
        
        abs_urls = []
        for url in urls:
            # Note that urljoin will "do the right thing" whether url is:
            #  1. a complete URL with host (e.g. "http://www.dummy.biz/test")
            #  2. a URL relative to root (e.g. "/dummy")
            #  3. a URL relative to the current path
            # Note that any querystring in browser_url will be discarded.
            url = urlparse.urljoin(cherrypy.request.browser_url, url)
            abs_urls.append(url)
        self.urls = abs_urls
        
        # RFC 2616 indicates a 301 response code fits our goal; however,
        # browser support for 301 is quite messy. Do 302 instead. See
        # http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html
        if status is None:
            if cherrypy.response.version >= "1.1":
                status = 303
            else:
                status = 302
        else:
            status = int(status)
            if status < 300 or status > 399:
                raise ValueError("status must be between 300 and 399.")
        
        self.status = status
        Exception.__init__(self, abs_urls, status)
    
    def set_response(self):
        import cherrypy
        response = cherrypy.response
        response.status = status = self.status
        
        if status in (300, 301, 302, 303, 307):
            response.headers['Content-Type'] = "text/html"
            
            # "The ... URI SHOULD be given by the Location field
            # in the response."
            response.headers['Location'] = self.urls[0]
            
            # "Unless the request method was HEAD, the entity of the response
            # SHOULD contain a short hypertext note with a hyperlink to the
            # new URI(s)."
            msg = {300: "This resource can be found at <a href='%s'>%s</a>.",
                   301: "This resource has permanently moved to <a href='%s'>%s</a>.",
                   302: "This resource resides temporarily at <a href='%s'>%s</a>.",
                   303: "This resource can be found at <a href='%s'>%s</a>.",
                   307: "This resource has moved temporarily to <a href='%s'>%s</a>.",
                   }[status]
            response.body = "<br />\n".join([msg % (u, u) for u in self.urls])
        elif status == 304:
            # Not Modified.
            # "The response MUST include the following header fields:
            # Date, unless its omission is required by section 14.18.1"
            # The "Date" header should have been set in Request.__init__
            
            # "...the response SHOULD NOT include other entity-headers.
            for key in ('Allow', 'Content-Encoding', 'Content-Language',
                        'Content-Length', 'Content-Location', 'Content-MD5',
                        'Content-Range', 'Content-Type', 'Expires',
                        'Last-Modified'):
                if key in response.headers:
                    del response.headers[key]
            
            # "The 304 response MUST NOT contain a message-body."
            response.body = None
        elif status == 305:
            # Use Proxy.
            # self.urls[0] should be the URI of the proxy.
            response.headers['Location'] = self.urls[0]
            response.body = None
        else:
            raise ValueError("The %s status code is unknown." % status)


class HTTPError(Error):
    """ Exception used to return an HTTP error code to the client.
        This exception will automatically set the response status and body.
        
        A custom message (a long description to display in the browser)
        can be provided in place of the default.
    """
    
    def __init__(self, status=500, message=None):
        self.status = status = int(status)
        if status < 400 or status > 599:
            raise ValueError("status must be between 400 and 599.")
        self.message = message
        Error.__init__(self, status, message)
    
    def set_response(self):
        import cherrypy
        handler = cherrypy._cputil.get_special_attribute("_cp_on_http_error", "_cpOnHTTPError")
        handler(self.status, self.message)


class NotFound(HTTPError):
    """ Happens when a URL couldn't be mapped to any class.method """
    
    def __init__(self, path=None):
        if path is None:
            import cherrypy
            path = cherrypy.request.path
        self.args = (path,)
        HTTPError.__init__(self, 404, "The path %s was not found." % repr(path))


class InternalError(HTTPError):
    """ Error that should never happen """
    
    def __init__(self, message=None):
        HTTPError.__init__(self, 500, message)

