# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
import os
import re
import sys
import urllib
from ConfigParser import ConfigParser
import pkg_resources
from paste.deploy.util.fixtypeerror import fix_call

__all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig']

############################################################
## Utility functions
############################################################

def import_string(s):
    return pkg_resources.EntryPoint.parse("x="+s).load(False)

def _aslist(obj):
    """
    Turn object into a list; lists and tuples are left as-is, None
    becomes [], and everything else turns into a one-element list.
    """
    if obj is None:
        return []
    elif isinstance(obj, (list, tuple)):
        return obj
    else:
        return [obj]

def _flatten(lst):
    """
    Flatten a nested list.
    """
    if not isinstance(lst, (list, tuple)):
        return [lst]
    result = []
    for item in lst:
        result.extend(_flatten(item))
    return result

class NicerConfigParser(ConfigParser):

    def __init__(self, filename, *args, **kw):
        ConfigParser.__init__(self, *args, **kw)
        self.filename = filename

    def _interpolate(self, section, option, rawval, vars):
        try:
            return ConfigParser._interpolate(
                self, section, option, rawval, vars)
        except Exception, e:
            args = list(e.args)
            args[0] = 'Error in file %s, [%s] %s=%r: %s' % (
                self.filename, section, option, rawval, e)
            e.args = tuple(args)
            raise

############################################################
## Object types
############################################################

class _ObjectType(object):

    name = None
    egg_protocols = None
    config_prefixes = None

    def __init__(self):
        # Normalize these variables:
        self.egg_protocols = map(_aslist, _aslist(self.egg_protocols))
        self.config_prefixes = map(_aslist, _aslist(self.config_prefixes))

    def __repr__(self):
        return '<%s protocols=%r prefixes=%r>' % (
            self.name, self.egg_protocols, self.config_prefixes)

    def invoke(self, context):
        assert context.protocol in _flatten(self.egg_protocols)
        return fix_call(context.object,
                        context.global_conf, **context.local_conf)

class _App(_ObjectType):

    name = 'application'
    egg_protocols = ['paste.app_factory', 'paste.composite_factory',
                     'paste.composit_factory']
    config_prefixes = [['app', 'application'], ['composite', 'composit'],
                       'pipeline', 'filter-app']

    def invoke(self, context):
        if context.protocol in ('paste.composit_factory',
                                'paste.composite_factory'):
            return fix_call(context.object,
                            context.loader, context.global_conf,
                            **context.local_conf)
        elif context.protocol == 'paste.app_factory':
            return fix_call(context.object, context.global_conf, **context.local_conf)
        else:
            assert 0, "Protocol %r unknown" % context.protocol

APP = _App()

class _Filter(_ObjectType):
    name = 'filter'
    egg_protocols = [['paste.filter_factory', 'paste.filter_app_factory']]
    config_prefixes = ['filter']

    def invoke(self, context):
        if context.protocol == 'paste.filter_factory':
            return fix_call(context.object,
                            context.global_conf, **context.local_conf)
        elif context.protocol == 'paste.filter_app_factory':
            def filter_wrapper(wsgi_app):
                # This should be an object, so it has a nicer __repr__
                return fix_call(context.object,
                                wsgi_app, context.global_conf,
                                **context.local_conf)
            return filter_wrapper
        else:
            assert 0, "Protocol %r unknown" % context.protocol

FILTER = _Filter()

class _Server(_ObjectType):
    name = 'server'
    egg_protocols = [['paste.server_factory', 'paste.server_runner']]
    config_prefixes = ['server']

    def invoke(self, context):
        if context.protocol == 'paste.server_factory':
            return fix_call(context.object,
                            context.global_conf, **context.local_conf)
        elif context.protocol == 'paste.server_runner':
            def server_wrapper(wsgi_app):
                # This should be an object, so it has a nicer __repr__
                return fix_call(context.object,
                                wsgi_app, context.global_conf,
                                **context.local_conf)
            return server_wrapper
        else:
            assert 0, "Protocol %r unknown" % context.protocol

SERVER = _Server()

# Virtual type: (@@: There's clearly something crufty here;
# this probably could be more elegant)
class _PipeLine(_ObjectType):
    name = 'pipeline'

    def invoke(self, context):
        app = context.app_context.create()
        filters = [c.create() for c in context.filter_contexts]
        filters.reverse()
        for filter in filters:
            app = filter(app)
        return app

PIPELINE = _PipeLine()

class _FilterApp(_ObjectType):
    name = 'filter_app'

    def invoke(self, context):
        next_app = context.next_context.create()
        filter = context.filter_context.create()
        return filter(next_app)

FILTER_APP = _FilterApp()

class _FilterWith(_App):
    name = 'filtered_with'

    def invoke(self, context):
        filter = context.filter_context.create()
        filtered = context.next_context.create()
        if context.next_context.object_type is APP:
            return filter(filtered)
        else:
            # filtering a filter
            def composed(app):
                return filter(filtered(app))
            return composed

FILTER_WITH = _FilterWith()

############################################################
## Loaders
############################################################

def loadapp(uri, name=None, **kw):
    return loadobj(APP, uri, name=name, **kw)

def loadfilter(uri, name=None, **kw):
    return loadobj(FILTER, uri, name=name, **kw)

def loadserver(uri, name=None, **kw):
    return loadobj(SERVER, uri, name=name, **kw)

def appconfig(uri, name=None, relative_to=None, global_conf=None):
    context = loadcontext(APP, uri, name=name,
                          relative_to=relative_to,
                          global_conf=global_conf)
    return context.config()

_loaders = {}

def loadobj(object_type, uri, name=None, relative_to=None,
            global_conf=None):
    context = loadcontext(
        object_type, uri, name=name, relative_to=relative_to,
        global_conf=global_conf)
    return context.create()

def loadcontext(object_type, uri, name=None, relative_to=None,
                global_conf=None):
    if '#' in uri:
        if name is None:
            uri, name = uri.split('#', 1)
        else:
            # @@: Ignore fragment or error?
            uri = uri.split('#', 1)[0]
    if name is None:
        name = 'main'
    if ':' not in uri:
        raise LookupError("URI has no scheme: %r" % uri)
    scheme, path = uri.split(':', 1)
    scheme = scheme.lower()
    if scheme not in _loaders:
        raise LookupError(
            "URI scheme not known: %r (from %s)"
            % (scheme, ', '.join(_loaders.keys())))
    return _loaders[scheme](
        object_type,
        uri, path, name=name, relative_to=relative_to,
        global_conf=global_conf)

def _loadconfig(object_type, uri, path, name, relative_to,
                global_conf):
    # De-Windowsify the paths:
    path = path.replace('\\', '/')
    absolute_path = True
    if sys.platform == 'win32':
        _absolute_re = re.compile(r'^[a-zA-Z]:')
        if not _absolute_re.search(path):
            absolute_path = False
    else:
        if not path.startswith('/'):
            absolute_path = False
    if not absolute_path:
        if not relative_to:
            raise ValueError(
                "Cannot resolve relative uri %r; no context keyword "
                "argument given" % uri)
        relative_to = relative_to.replace('\\', '/')
        if relative_to.endswith('/'):
            path = relative_to + path
        else:
            path = relative_to + '/' + path
    if path.startswith('///'):
        path = path[2:]
    path = urllib.unquote(path)
    loader = ConfigLoader(path)
    if global_conf:
        loader.update_defaults(global_conf, overwrite=False)
    return loader.get_context(object_type, name, global_conf)

_loaders['config'] = _loadconfig

def _loadegg(object_type, uri, spec, name, relative_to,
             global_conf):
    loader = EggLoader(spec)
    return loader.get_context(object_type, name, global_conf)

_loaders['egg'] = _loadegg

############################################################
## Loaders
############################################################

class _Loader(object):

    def get_app(self, name=None, global_conf=None):
        return self.app_context(
            name=name, global_conf=global_conf).create()

    def get_filter(self, name=None, global_conf=None):
        return self.filter_context(
            name=name, global_conf=global_conf).create()

    def get_server(self, name=None, global_conf=None):
        return self.server_context(
            name=name, global_conf=global_conf).create()

    def app_context(self, name=None, global_conf=None):
        return self.get_context(
            APP, name=name, global_conf=global_conf)

    def filter_context(self, name=None, global_conf=None):
        return self.get_context(
            FILTER, name=name, global_conf=global_conf)

    def server_context(self, name=None, global_conf=None):
        return self.get_context(
            SERVER, name=name, global_conf=global_conf)

    _absolute_re = re.compile(r'^[a-zA-Z]+:')
    def absolute_name(self, name):
        """
        Returns true if the name includes a scheme
        """
        if name is None:
            return False
        return self._absolute_re.search(name)        

class ConfigLoader(_Loader):

    def __init__(self, filename):
        self.filename = filename = filename.strip()
        self.parser = NicerConfigParser(self.filename)
        # Don't lower-case keys:
        self.parser.optionxform = str
        # Stupid ConfigParser ignores files that aren't found, so
        # we have to add an extra check:
        if not os.path.exists(filename):
            raise OSError(
                "File %r not found" % filename)
        self.parser.read(filename)
        self.parser._defaults.setdefault(
            'here', os.path.dirname(os.path.abspath(filename)))
        self.parser._defaults.setdefault(
            '__file__', os.path.abspath(filename))

    def update_defaults(self, new_defaults, overwrite=True):
        for key, value in new_defaults.items():
            if not overwrite and key in self.parser._defaults:
                continue
            self.parser._defaults[key] = value

    def get_context(self, object_type, name=None, global_conf=None):
        if self.absolute_name(name):
            return loadcontext(object_type, name,
                               relative_to=os.path.dirname(self.filename),
                               global_conf=global_conf)
        section = self.find_config_section(
            object_type, name=name)
        if global_conf is None:
            global_conf = {}
        else:
            global_conf = global_conf.copy()
        defaults = self.parser.defaults()
        global_conf.update(defaults)
        local_conf = {}
        global_additions = {}
        get_from_globals = {}
        for option in self.parser.options(section):
            if option.startswith('set '):
                name = option[4:].strip()
                global_additions[name] = global_conf[name] = (
                    self.parser.get(section, option))
            elif option.startswith('get '):
                name = option[4:].strip()
                get_from_globals[name] = self.parser.get(section, option)
            else:
                if option in defaults:
                    # @@: It's a global option (?), so skip it
                    continue
                local_conf[option] = self.parser.get(section, option)
        for local_var, glob_var in get_from_globals.items():
            local_conf[local_var] = global_conf[glob_var]
        if object_type in (APP, FILTER) and 'filter-with' in local_conf:
            filter_with = local_conf.pop('filter-with')
        else:
            filter_with = None
        if 'require' in local_conf:
            for spec in local_conf['require'].split():
                pkg_resources.require(spec)
            del local_conf['require']
        if section.startswith('filter-app:'):
            context = self._filter_app_context(
                object_type, section, name=name,
                global_conf=global_conf, local_conf=local_conf,
                global_additions=global_additions)
        elif section.startswith('pipeline:'):
            context = self._pipeline_app_context(
                object_type, section, name=name,
                global_conf=global_conf, local_conf=local_conf,
                global_additions=global_additions)
        elif 'use' in local_conf:
            context = self._context_from_use(
                object_type, local_conf, global_conf, global_additions,
                section)
        else:
            context = self._context_from_explicit(
                object_type, local_conf, global_conf, global_additions,
                section)
        if filter_with is not None:
            filter_with_context = LoaderContext(
                obj=None,
                object_type=FILTER_WITH,
                protocol=None,
                global_conf=global_conf, local_conf=local_conf,
                loader=self)
            filter_with_context.filter_context = self.filter_context(
                name=filter_with, global_conf=global_conf)
            filter_with_context.next_context = context
            return filter_with_context
        return context

    def _context_from_use(self, object_type, local_conf, global_conf,
                          global_additions, section):
        use = local_conf.pop('use')
        context = self.get_context(
            object_type, name=use, global_conf=global_conf)
        context.global_conf.update(global_additions)
        context.local_conf.update(local_conf)
        # @@: Should loader be overwritten?
        context.loader = self
        return context

    def _context_from_explicit(self, object_type, local_conf, global_conf,
                               global_addition, section):
        possible = []
        for protocol_options in object_type.egg_protocols:
            for protocol in protocol_options:
                if protocol in local_conf:
                    possible.append((protocol, local_conf[protocol]))
                    break
        if len(possible) > 1:
            raise LookupError(
                "Multiple protocols given in section %r: %s"
                % (section, possible))
        if not possible:
            raise LookupError(
                "No loader given in section %r" % section)
        found_protocol, found_expr = possible[0]
        del local_conf[found_protocol]
        value = import_string(found_expr)
        context = LoaderContext(
            value, object_type, found_protocol,
            global_conf, local_conf, self)
        return context

    def _filter_app_context(self, object_type, section, name,
                            global_conf, local_conf, global_additions):
        if 'next' not in local_conf:
            raise LookupError(
                "The [%s] section in %s is missing a 'next' setting"
                % (section, self.filename))
        next_name = local_conf.pop('next')
        context = LoaderContext(None, FILTER_APP, None, global_conf,
                                local_conf, self)
        context.next_context = self.get_context(
            APP, next_name, global_conf)
        if 'use' in local_conf:
            context.filter_context = self._context_from_use(
                FILTER, local_conf, global_conf, global_additions,
                section)