"""Miscellaneous decorators and other helpers for running a CherryPy
server"""

import cherrypy
import sys
import os
import decorator
import simplejson as json

# Helper to parse parameters into booleans
def bool_param(s):
    """Return a bool indicating whether parameter 's' was True or False,
    supporting a few different types for 's'."""
    try:
        ss = s.lower()
        if ss in [ "0", "false", "f", "no", "n" ]:
            return False
        if ss in [ "1", "true", "t", "yes", "y" ]:
            return True
    except Exception:
        return bool(s)
    raise cherrypy.HTTPError("400 Bad Request",
                             "can't parse parameter: " + ss)

# Decorators
def chunked_response(func):
    """Decorator to enable chunked responses."""
    # Set this to False to get better tracebacks from some requests
    # (/stream/extract, /stream/intervals).
    func._cp_config = { 'response.stream': True }
    return func

def response_type(content_type):
    """Return a decorator-generating function that sets the
    response type to the specified string."""
    def wrapper(func, *args, **kwargs):
        cherrypy.response.headers['Content-Type'] = content_type
        return func(*args, **kwargs)
    return decorator.decorator(wrapper)

def exception_to_httperror(*expected):
    """Return a decorator-generating function that catches expected
    errors and throws a HTTPError describing it instead.

        @exception_to_httperror(NilmDBError, ValueError)
        def foo():
            pass
    """
    def wrapper(func, *args, **kwargs):
        exc_info = None
        try:
            return func(*args, **kwargs)
        except expected:
            # Re-raise it, but maintain the original traceback
            exc_info = sys.exc_info()
            new_exc = cherrypy.HTTPError("400 Bad Request", str(exc_info[1]))
            raise new_exc.with_traceback(exc_info[2])
        finally:
            del exc_info
    # We need to preserve the function's argspecs for CherryPy to
    # handle argument errors correctly.  Decorator.decorator takes
    # care of that.
    return decorator.decorator(wrapper)

# Custom CherryPy tools

def CORS_allow(methods):
    """This does several things:

    Handles CORS preflight requests.
    Adds Allow: header to all requests.
    Raise 405 if request.method not in method.

    It is similar to cherrypy.tools.allow, with the CORS stuff added.

    Add this to CherryPy with:
    cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow)
    """
    request = cherrypy.request.headers
    response = cherrypy.response.headers

    if not isinstance(methods, (tuple, list)): # pragma: no cover
        methods = [ methods ]
    methods = [ m.upper() for m in methods if m ]
    if not methods: # pragma: no cover
        methods = [ 'GET', 'HEAD' ]
    elif 'GET' in methods and 'HEAD' not in methods: # pragma: no cover
        methods.append('HEAD')
    response['Allow'] = ', '.join(methods)

    # Allow all origins
    if 'Origin' in request:
        response['Access-Control-Allow-Origin'] = request['Origin']

    # If it's a CORS request, send response.
    request_method = request.get("Access-Control-Request-Method", None)
    request_headers = request.get("Access-Control-Request-Headers", None)
    if (cherrypy.request.method == "OPTIONS" and
        request_method and request_headers):
        response['Access-Control-Allow-Headers'] = request_headers
        response['Access-Control-Allow-Methods'] = ', '.join(methods)
        # Try to stop further processing and return a 200 OK
        cherrypy.response.status = "200 OK"
        cherrypy.response.body = b""
        cherrypy.request.handler = lambda: ""
        return

    # Reject methods that were not explicitly allowed
    if cherrypy.request.method not in methods:
        raise cherrypy.HTTPError(405)


# Helper for json_in tool to process JSON data into normal request
# parameters.
def json_to_request_params(body):
    cherrypy.lib.jsontools.json_processor(body)
    if not isinstance(cherrypy.request.json, dict):
        raise cherrypy.HTTPError(415)
    cherrypy.request.params.update(cherrypy.request.json)

# Used as an "error_page.default" handler
def json_error_page(status, message, traceback, version,
                    force_traceback = False):
    """Return a custom error page in JSON so the client can parse it"""
    errordata = { "status" : status,
                  "message" : message,
                  "traceback" : traceback }
    # Don't send a traceback if the error was 400-499 (client's fault)
    code = int(status.split()[0])
    if not force_traceback:
        if code >= 400 and code <= 499:
            errordata["traceback"] = ""
    # Override the response type, which was previously set to text/html
    cherrypy.serving.response.headers['Content-Type'] = (
        "application/json;charset=utf-8" )
    # Undo the HTML escaping that cherrypy's get_error_page function applies
    # (cherrypy issue 1135)
    for k, v in errordata.items():
        v = v.replace("&lt;","<")
        v = v.replace("&gt;",">")
        v = v.replace("&amp;","&")
        errordata[k] = v
    return json.dumps(errordata, separators=(',',':'))

# Start/stop CherryPy standalone server
def cherrypy_start(blocking = False, event = False, embedded = False):
    """Start the CherryPy server, handling errors and signals
    somewhat gracefully."""

    if not embedded: # pragma: no cover
        # Handle signals nicely
        if hasattr(cherrypy.engine, "signal_handler"):
            cherrypy.engine.signal_handler.subscribe()
        if hasattr(cherrypy.engine, "console_control_handler"):
            cherrypy.engine.console_control_handler.subscribe()

    # Cherrypy stupidly calls os._exit(70) when it can't bind the
    # port.  At least try to print a reasonable error and continue
    # in this case, rather than just dying silently (as we would
    # otherwise do in embedded mode)
    real_exit = os._exit
    def fake_exit(code): # pragma: no cover
        if code == os.EX_SOFTWARE:
            fprintf(sys.stderr, "error: CherryPy called os._exit!\n")
        else:
            real_exit(code)
    os._exit = fake_exit
    cherrypy.engine.start()
    os._exit = real_exit

    # Signal that the engine has started successfully
    if event is not None:
        event.set()

    if blocking:
        try:
            cherrypy.engine.wait(cherrypy.engine.states.EXITING,
                                 interval = 0.1, channel = 'main')
        except (KeyboardInterrupt, IOError): # pragma: no cover
            cherrypy.engine.log('Keyboard Interrupt: shutting down bus')
            cherrypy.engine.exit()
        except SystemExit: # pragma: no cover
            cherrypy.engine.log('SystemExit raised: shutting down bus')
            cherrypy.engine.exit()
            raise

# Stop CherryPy server
def cherrypy_stop():
    cherrypy.engine.exit()
