import os
import sys
from twisted.web.server import NOT_DONE_YET
from twisted.web.client import readBody


def load_routes(location):
    routes = []
    if not os.path.isdir(location):
        raise Exception(
            'Cannot find specified configure directory: {0}', location)
    for site in sorted(os.listdir(location)):
        try:
            routes.append(load_site(os.path.join(location, site)))
        except Exception as e:
            sys.stderr.write('Unable to load {0}; skipping\n'.format(
                os.path.abspath(os.path.join(location, site))))
            sys.stderr.write('  {0}\n\n'.format(e))
    return RouteList(routes)


def try_load(filename, default=None):
    if not os.path.exists(filename):
        return default
    if not os.path.isfile(os.path.abspath(filename)):
        raise Exception(
            'Cannot load {0}: is not a file'.format(filename))
    with open(filename) as f:
        return f.read().strip()


def domain_for(request):
    return request.getHeader('host')


def load_site(loc):
    if not os.path.isdir(loc):
        raise Exception('Site spec not a directory: {0}'.format(loc))

    if (not os.path.exists(os.path.join(loc, 'domain')) and
            not os.path.exists(os.path.join(loc, 'path'))):
        raise Exception(
            'The site {0} does not specify a domain or a path'.format(loc))
    domain = try_load(os.path.join(loc, 'domain'), '*')
    path = try_load(os.path.join(loc, 'path'), '*').lstrip('/')
    mode = try_load(os.path.join(loc, 'mode'), 'http')
    if mode == 'http':
        dispatch = HTTPDispatch(
            host=try_load(os.path.join(loc, 'host'), 'localhost'),
            port=int(try_load(os.path.join(loc, 'port'), 80)))
    elif mode == 'melvil':
        dispatch = MelvilDispatch(
            loc=try_load(os.path.join(loc, 'conf'), '/dev/null'))
    elif mode == 'redir':
        dispatch = RedirDispatch(
            resp=int(try_load(os.path.join(loc, 'resp'), 303)),
            location=try_load(os.path.join(loc, 'location'),
                              'http://localhost/'))
    else:
        raise Exception('Unknown mode: {0}'.format(repr(mode)))
    return Route(domain=domain, path=path, dispatch=dispatch, disk_loc=loc)


def wildcard_match(spec, string):
    loc_stack = [(0, 0)]
    while loc_stack:
        (i, j) = loc_stack.pop()
        if i >= len(spec) and j >= len(string):
            return True
        elif i >= len(spec) or j >= len(string):
            continue
        elif spec[i] == string[j]:
            loc_stack.append((i + 1, j + 1))
        elif spec[i] == '*':
            loc_stack.append((i + 1, j + 1))
            loc_stack.append((i, j + 1))
        else:
            continue
    return False


class RouteList:
    '''
    An object which represents an ordered set of possible routing
    options.
    '''

    def __init__(self, routes):
        self.routes = routes

    def handle(self, request, agent):
        for route in self.routes:
            if route.matches(request):
                return route.handle(request, agent)
        return "unable to handle request"

    def __repr__(self):
        return 'RouteList([{0}])'.format(
            ', '.join(repr(r) for r in self.routes))

    def pretty(self, level=0):
        return ''.join(
            ' ' * level + r.pretty(level=level)
            for r in self.routes)


class Route:

    def __init__(self, domain, path, dispatch, disk_loc=''):
        self.domain = domain
        self.path = path
        self.dispatch = dispatch
        self.disk_loc = disk_loc

    def matches(self, request):
        return (wildcard_match(self.path, '/' + request.path) and
                wildcard_match(self.domain, domain_for(request)))

    def handle(self, request, agent):
        print 'handled by {0}'.format(self.disk_loc)
        return self.dispatch.handle(request, agent)

    def __repr__(self):
        return 'Route({0}, {1}, {2}, disk_loc={3})'.format(
            repr(self.domain),
            repr(self.path),
            self.dispatch,
            repr(self.disk_loc))

    def pretty(self, level=0):
        return '{0}/{1} => {2}'.format(self.domain,
                                       self.path,
                                       self.dispatch.pretty(level=level))


class Dispatch:

    def handle(self, request, agent):
        return '[no route matched your request]'

    def __repr__(self):
        return '{name}({args})'.format(
            name=self.__class__.__name__,
            args=', '.join('{0}={1}'.format(k, repr(v))
                           for (k, v) in self.__dict__.items()))


class HTTPDispatch(Dispatch):

    def __init__(self, host='localhost', port=80):
        self.host = host
        self.port = port

    def handle(self, request, agent):
        def callback(response):
            for (k, v) in response.headers.getAllRawHeaders():
                request.setHeader(k, v[0])
            r = readBody(response)
            r.addCallback(handle_body)
            return r

        def handle_body(body):
            request.write(body)
            request.finish()

        url = 'http://{0}:{1}{2}'.format(
            self.host, self.port, request.path)
        print url
        agent.request('GET', url, request.requestHeaders, None) \
             .addCallback(callback)
        return NOT_DONE_YET

    def pretty(self, level=0):
        return '{0}:{1}\n'.format(self.host, self.port)


class MelvilDispatch(Dispatch):
    '''
    Dispatch the route down to a different configuration directory.
    '''

    def __init__(self, loc):
        self.routes = load_routes(loc)

    def handle(self, request, agent):
        return self.routes.handle(request)

    def pretty(self, level=0):
        return '\n' + self.routes.pretty(level=level + 2)


class RedirDispatch(Dispatch):
    '''
    Use a redirect code (probably 303) to manually redirect to a
    different location.
    '''

    def __init__(self, resp=303, location='http://localhost/'):
        self.resp = resp
        self.location = location

    def handle(self, request, agent):
        request.setResponseCode(self.resp)
        request.setHeader('location', self.location)
        return ''

    def pretty(self, level=0):
        return 'redir({0})\n'.format(self.location)
