#!/usr/bin/python

import sys, os, getopt, threading, time
import ashd.proto, ashd.util

def usage(out):
    out.write("usage: ashd-wsgi [-hA] [-p MODPATH] [-l REQLIMIT] HANDLER-MODULE [ARGS...]\n")

reqlimit = 0
modwsgi_compat = False
opts, args = getopt.getopt(sys.argv[1:], "+hAp:l:")
for o, a in opts:
    if o == "-h":
        usage(sys.stdout)
        sys.exit(0)
    elif o == "-p":
        sys.path.insert(0, a)
    elif o == "-A":
        modwsgi_compat = True
    elif o == "-l":
        reqlimit = int(a)
if len(args) < 1:
    usage(sys.stderr)
    sys.exit(1)

try:
    handlermod = __import__(args[0], fromlist = ["dummy"])
except ImportError, exc:
    sys.stderr.write("ashd-wsgi: handler %s not found: %s\n" % (args[0], exc.message))
    sys.exit(1)
if not modwsgi_compat:
    if not hasattr(handlermod, "wmain"):
        sys.stderr.write("ashd-wsgi: handler %s has no `wmain' function\n" % args[0])
        sys.exit(1)
    handler = handlermod.wmain(*args[1:])
else:
    if not hasattr(handlermod, "application"):
        sys.stderr.write("ashd-wsgi: handler %s has no `application' object\n" % args[0])
        sys.exit(1)
    handler = handlermod.application

class closed(IOError):
    def __init__(self):
        super(closed, self).__init__("The client has closed the connection.")

cwd = os.getcwd()
def absolutify(path):
    if path[0] != '/':
        return os.path.join(cwd, path)
    return path

def unquoteurl(url):
    buf = ""
    i = 0
    while i < len(url):
        c = url[i]
        i += 1
        if c == '%':
            if len(url) >= i + 2:
                c = 0
                if '0' <= url[i] <= '9':
                    c |= (ord(url[i]) - ord('0')) << 4
                elif 'a' <= url[i] <= 'f':
                    c |= (ord(url[i]) - ord('a') + 10) << 4
                elif 'A' <= url[i] <= 'F':
                    c |= (ord(url[i]) - ord('A') + 10) << 4
                else:
                    raise ValueError("Illegal URL escape character")
                if '0' <= url[i + 1] <= '9':
                    c |= ord(url[i + 1]) - ord('0')
                elif 'a' <= url[i + 1] <= 'f':
                    c |= ord(url[i + 1]) - ord('a') + 10
                elif 'A' <= url[i + 1] <= 'F':
                    c |= ord(url[i + 1]) - ord('A') + 10
                else:
                    raise ValueError("Illegal URL escape character")
                buf += chr(c)
                i += 2
            else:
                raise ValueError("Incomplete URL escape character")
        else:
            buf += c
    return buf

def dowsgi(req):
    env = {}
    env["wsgi.version"] = 1, 0
    for key, val in req.headers:
        env["HTTP_" + key.upper().replace("-", "_")] = val
    env["SERVER_SOFTWARE"] = "ashd-wsgi/1"
    env["GATEWAY_INTERFACE"] = "CGI/1.1"
    env["SERVER_PROTOCOL"] = req.ver
    env["REQUEST_METHOD"] = req.method
    env["REQUEST_URI"] = req.url
    name = req.url
    p = name.find('?')
    if p >= 0:
        env["QUERY_STRING"] = name[p + 1:]
        name = name[:p]
    else:
        env["QUERY_STRING"] = ""
    if name[-len(req.rest):] == req.rest:
        # This is the same hack used in call*cgi.
        name = name[:-len(req.rest)]
    try:
        pi = unquoteurl(req.rest)
    except:
        pi = req.rest
    if name == '/':
        # This seems to be normal CGI behavior, but see callcgi.c for
        # details.
        pi = "/" + pi
        name = ""
    env["SCRIPT_NAME"] = name
    env["PATH_INFO"] = pi
    if "Host" in req: env["SERVER_NAME"] = req["Host"]
    if "X-Ash-Server-Port" in req: env["SERVER_PORT"] = req["X-Ash-Server-Port"]
    if "X-Ash-Protocol" in req and req["X-Ash-Protocol"] == "https": env["HTTPS"] = "on"
    if "X-Ash-Address" in req: env["REMOTE_ADDR"] = req["X-Ash-Address"]
    if "Content-Type" in req: env["CONTENT_TYPE"] = req["Content-Type"]
    if "Content-Length" in req: env["CONTENT_LENGTH"] = req["Content-Length"]
    if "X-Ash-File" in req: env["SCRIPT_FILENAME"] = absolutify(req["X-Ash-File"])
    if "X-Ash-Protocol" in req: env["wsgi.url_scheme"] = req["X-Ash-Protocol"]
    env["wsgi.input"] = req.sk
    env["wsgi.errors"] = sys.stderr
    env["wsgi.multithread"] = True
    env["wsgi.multiprocess"] = False
    env["wsgi.run_once"] = False

    resp = []
    respsent = []

    def flushreq():
        if not respsent:
            if not resp:
                raise Exception, "Trying to write data before starting response."
            status, headers = resp
            respsent[:] = [True]
            try:
                req.sk.write("HTTP/1.1 %s\n" % status)
                for nm, val in headers:
                    req.sk.write("%s: %s\n" % (nm, val))
                req.sk.write("\n")
            except IOError:
                raise closed()

    def write(data):
        if not data:
            return
        flushreq()
        try:
            req.sk.write(data)
            req.sk.flush()
        except IOError:
            raise closed()

    def startreq(status, headers, exc_info = None):
        if resp:
            if exc_info:                # Interesting, this...
                try:
                    if respsent:
                        raise exc_info[0], exc_info[1], exc_info[2]
                finally:
                    exc_info = None     # CPython GC bug?
            else:
                raise Exception, "Can only start responding once."
        resp[:] = status, headers
        return write

    respiter = handler(env, startreq)
    try:
        try:
            for data in respiter:
                write(data)
            if resp:
                flushreq()
        except closed:
            pass
    finally:
        if hasattr(respiter, "close"):
            respiter.close()

flightlock = threading.Condition()
inflight = 0

class reqthread(threading.Thread):
    def __init__(self, req):
        super(reqthread, self).__init__(name = "Request handler")
        self.req = req.dup()
    
    def run(self):
        global inflight
        try:
            flightlock.acquire()
            try:
                if reqlimit != 0:
                    start = time.time()
                    while inflight >= reqlimit:
                        flightlock.wait(10)
                        if time.time() - start > 10:
                            os.abort()
                inflight += 1
            finally:
                flightlock.release()
            try:
                dowsgi(self.req)
            finally:
                flightlock.acquire()
                try:
                    inflight -= 1
                    flightlock.notify()
                finally:
                    flightlock.release()
        finally:
            self.req.close()
    
def handle(req):
    reqthread(req).start()

ashd.util.serveloop(handle)
