#!/usr/bin/python3

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

def usage(out):
    out.write("usage: ashd-wsgi3 [-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 as exc:
    sys.stderr.write("ashd-wsgi3: handler %s not found: %s\n" % (args[0], exc.args[0]))
    sys.exit(1)
if not modwsgi_compat:
    if not hasattr(handlermod, "wmain"):
        sys.stderr.write("ashd-wsgi3: 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-wsgi3: handler %s has no `application' object\n" % args[0])
        sys.exit(1)
    handler = handlermod.application

class closed(IOError):
    def __init__(self):
        super().__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 = bytearray()
    i = 0
    while i < len(url):
        c = url[i]
        i += 1
        if c == ord(b'%'):
            if len(url) >= i + 2:
                c = 0
                if ord(b'0') <= url[i] <= ord(b'9'):
                    c |= (url[i] - ord(b'0')) << 4
                elif ord(b'a') <= url[i] <= ord(b'f'):
                    c |= (url[i] - ord(b'a') + 10) << 4
                elif ord(b'A') <= url[i] <= ord(b'F'):
                    c |= (url[i] - ord(b'A') + 10) << 4
                else:
                    raise ValueError("Illegal URL escape character")
                if ord(b'0') <= url[i + 1] <= ord(b'9'):
                    c |= url[i + 1] - ord('0')
                elif ord(b'a') <= url[i + 1] <= ord(b'f'):
                    c |= url[i + 1] - ord(b'a') + 10
                elif ord(b'A') <= url[i + 1] <= ord(b'F'):
                    c |= url[i + 1] - ord(b'A') + 10
                else:
                    raise ValueError("Illegal URL escape character")
                buf.append(c)
                i += 2
            else:
                raise ValueError("Incomplete URL escape character")
        else:
            buf.append(c)
    return buf

def dowsgi(req):
    env = {}
    env["wsgi.version"] = 1, 0
    for key, val in req.headers:
        env["HTTP_" + key.upper().replace(b"-", b"_").decode("latin-1")] = val.decode("latin-1")
    env["SERVER_SOFTWARE"] = "ashd-wsgi/1"
    env["GATEWAY_INTERFACE"] = "CGI/1.1"
    env["SERVER_PROTOCOL"] = req.ver.decode("latin-1")
    env["REQUEST_METHOD"] = req.method.decode("latin-1")
    try:
        rawpi = unquoteurl(req.rest)
    except:
        rawpi = req.rest
    try:
        name, rest, pi = (v.decode("utf-8") for v in (req.url, req.rest, rawpi))
        env["wsgi.uri_encoding"] = "utf-8"
    except UnicodeError as exc:
        name, rest, pi = (v.decode("latin-1") for v in (req.url, req.rest, rawpi))
        env["wsgi.uri_encoding"] = "latin-1"
    env["REQUEST_URI"] = name
    p = name.find('?')
    if p >= 0:
        env["QUERY_STRING"] = name[p + 1:]
        name = name[:p]
    else:
        env["QUERY_STRING"] = ""
    if name[-len(rest):] == rest:
        # This is the same hack used in call*cgi.
        name = name[:-len(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
    for src, tgt in [("HTTP_HOST", "SERVER_NAME"), ("HTTP_X_ASH_SERVER_PORT", "SERVER_PORT"),
                     ("HTTP_X_ASH_ADDRESS", "REMOTE_ADDR"), ("HTTP_CONTENT_TYPE", "CONTENT_TYPE"),
                     ("HTTP_CONTENT_LENGTH", "CONTENT_LENGTH"), ("HTTP_X_ASH_PROTOCOL", "wsgi.url_scheme")]:
        if src in env: env[tgt] = env[src]
    if "X-Ash-Protocol" in req and req["X-Ash-Protocol"] == b"https": env["HTTPS"] = "on"
    if "X-Ash-File" in req: env["SCRIPT_FILENAME"] = absolutify(req["X-Ash-File"].decode(locale.getpreferredencoding()))
    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 recode(thing):
        if isinstance(thing, collections.ByteString):
            return thing
        else:
            return str(thing).encode("latin-1")

    def flushreq():
        if not respsent:
            if not resp:
                raise Exception("Trying to write data before starting response.")
            status, headers = resp
            respsent[:] = [True]
            buf = bytearray()
            buf += b"HTTP/1.1 " + recode(status) + b"\n"
            for nm, val in headers:
                buf += recode(nm) + b": " + recode(val) + b"\n"
            buf += b"\n"
            try:
                req.sk.write(buf)
            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[1]
                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().__init__(name = "Request handler")
        self.req = req.dup()
    
    def run(self):
        global inflight
        try:
            with flightlock:
                if reqlimit != 0:
                    start = time.time()
                    while inflight >= reqlimit:
                        flightlock.wait(10)
                        if time.time() - start > 10:
                            os.abort()
                inflight += 1
            try:
                dowsgi(self.req)
            finally:
                with flightlock:
                    inflight -= 1
                    flightlock.notify()
        finally:
            self.req.close()
            sys.stderr.flush()
    
def handle(req):
    reqthread(req).start()

ashd.util.serveloop(handle)
