#!/usr/bin/python3

import sys, os, getopt, threading, logging, time, locale, collections
import ashd.proto, ashd.util, ashd.perf, ashd.serve
try:
    import pdm.srv
except:
    pdm = None

def usage(out):
    out.write("usage: ashd-wsgi3 [-hAL] [-m PDM-SPEC] [-p MODPATH] [-l REQLIMIT] HANDLER-MODULE [ARGS...]\n")

reqlimit = 0
modwsgi_compat = False
setlog = True
opts, args = getopt.getopt(sys.argv[1:], "+hALp:l:m:")
for o, a in opts:
    if o == "-h":
        usage(sys.stdout)
        sys.exit(0)
    elif o == "-p":
        sys.path.insert(0, a)
    elif o == "-L":
        setlog = False
    elif o == "-A":
        modwsgi_compat = True
    elif o == "-l":
        reqlimit = int(a)
    elif o == "-m":
        if pdm is not None:
            pdm.srv.listen(a)
if len(args) < 1:
    usage(sys.stderr)
    sys.exit(1)
if setlog:
    logging.basicConfig(format="ashd-wsgi3(%(name)s): %(levelname)s: %(message)s")
log = logging.getLogger("ashd-wsgi3")

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

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 mkenv(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_PROTOCOL", "wsgi.url_scheme"),
                     ("HTTP_X_ASH_SERVER_ADDRESS", "SERVER_ADDR"), ("HTTP_X_ASH_SERVER_PORT", "SERVER_PORT"),
                     ("HTTP_X_ASH_ADDRESS", "REMOTE_ADDR"), ("HTTP_X_ASH_PORT", "REMOTE_PORT"),
                     ("HTTP_CONTENT_TYPE", "CONTENT_TYPE"), ("HTTP_CONTENT_LENGTH", "CONTENT_LENGTH")]:
        if src in env: env[tgt] = env[src]
    for key in ["HTTP_CONTENT_TYPE", "HTTP_CONTENT_LENGTH"]:
        # The CGI specification does not strictly require this, but
        # many actualy programs and libraries seem to.
        if key in env: del env[key]
    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
    return env

if reqlimit != 0:
    guard = ashd.serve.abortlimiter(reqlimit).call
else:
    guard = lambda fun: fun()

def recode(thing):
    if isinstance(thing, collections.ByteString):
        return thing
    else:
        return str(thing).encode("latin-1")

class reqthread(ashd.serve.wsgithread):
    def __init__(self, req):
        super().__init__()
        self.req = req.dup()

    def handlewsgi(self):
        return handler(self.env, self.startreq)

    def writehead(self, status, headers):
        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:
            self.req.sk.write(buf)
        except IOError:
            raise ashd.serve.closed()

    def writedata(self, data):
        try:
            self.req.sk.write(data)
            self.req.sk.flush()
        except IOError:
            raise ashd.serve.closed()

    def handle(self):
        self.env = mkenv(self.req)
        with ashd.perf.request(self.env) as reqevent:
            super().handle()
            if self.status:
                reqevent.response([self.status, self.headers])
    
    def run(self):
        try:
            guard(super().run)
        finally:
            self.req.close()
            sys.stderr.flush()
    
def handle(req):
    reqthread(req).start()

ashd.util.serveloop(handle)
