# (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 urlparse
import urllib
from command import Command, BadCommand
from paste.deploy import loadapp, loadserver
from paste.wsgilib import raw_interactive

class RequestCommand(Command):

    min_args = 2
    usage = 'CONFIG_FILE URL [OPTIONS/ARGUMENTS]'
    takes_config_file = 1
    summary = "Run a request for the described application"
    description = """\
    This command makes an artifical request to a web application that
    uses a paste.deploy configuration file for the server and
    application.

    Use 'paster request config.ini /url' to request /url.  Use
    'paster post config.ini /url < data' to do a POST with the given
    request body.

    If the URL is relative (doesn't begin with /) it is interpreted as
    relative to /.command/.  The variable environ['paste.command_request']
    will be set to True in the request, so your application can distinguish
    these calls from normal requests.

    Note that you can pass options besides the options listed here; any unknown
    options will be passed to the application in environ['QUERY_STRING'].
    """
    
    parser = Command.standard_parser(quiet=True)
    parser.add_option('-n', '--app-name',
                      dest='app_name',
                      metavar='NAME',
                      help="Load the named application (default main)")
    parser.add_option('--config-var',
                      dest='config_vars',
                      metavar='NAME:VALUE',
                      action='append',
                      help="Variable to make available in the config for %()s substitution "
                      "(you can use this option multiple times)")
    parser.add_option('--header',
                      dest='headers',
                      metavar='NAME:VALUE',
                      action='append',
                      help="Header to add to request (you can use this option multiple times)")
    parser.add_option('--display-headers',
                      dest='display_headers',
                      action='store_true',
                      help='Display headers before the response body')

    ARG_OPTIONS = ['-n', '--app-name', '--config-var', '--header']
    OTHER_OPTIONS = ['--display-headers']
    
    ## FIXME: some kind of verbosity?
    ## FIXME: allow other methods than POST and GET?

    _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I)

    def command(self):
        vars = {}
        app_spec = self.args[0]
        url = self.args[1]
        url = urlparse.urljoin('/.command/', url)
        if self.options.config_vars:
            for item in self.option.config_vars:
                if ':' not in item:
                    raise BadCommand(
                        "Bad option, should be name:value : --config-var=%s" % item)
                name, value = item.split(':', 1)
                vars[name] = value
        headers = {}
        if self.options.headers:
            for item in self.options.headers:
                if ':' not in item:
                    raise BadCommand(
                        "Bad option, should be name:value : --header=%s" % item)
                name, value = item.split(':', 1)
                headers[name] = value.strip()
        if not self._scheme_re.search(app_spec):
            app_spec = 'config:'+app_spec
        if self.options.app_name:
            if '#' in app_spec:
                app_spec = app_spec.split('#', 1)[0]
            app_spec = app_spec + '#' + options.app_name
        app = loadapp(app_spec, relative_to=os.getcwd(), global_conf=vars)
        if self.command_name.lower() == 'post':
            request_method = 'POST'
        else:
            request_method = 'GET'
        qs = []
        for item in self.args[2:]:
            if '=' in item:
                item = urllib.quote(item.split('=', 1)[0]) + '=' + urllib.quote(item.split('=', 1)[1])
            else:
                item = urllib.quote(item)
            qs.append(item)
        qs = '&'.join(qs)
        
        environ = {
            'REQUEST_METHOD': request_method,
            ## FIXME: shouldn't be static (an option?):
            'CONTENT_TYPE': 'text/plain',
            'wsgi.run_once': True,
            'wsgi.multithread': False,
            'wsgi.multiprocess': False,
            'wsgi.errors': sys.stderr,
            'QUERY_STRING': qs,
            'HTTP_ACCEPT': 'text/plain;q=1.0, */*;q=0.1',
            'paste.command_request': True,
            }
        if request_method == 'POST':
            environ['wsgi.input'] = sys.stdin
            environ['CONTENT_LENGTH'] = '-1'
        for name, value in headers.items():
            if name.lower() == 'content-type':
                name = 'CONTENT_TYPE'
            else:
                name = 'HTTP_'+name.upper().replace('-', '_')
            environ[name] = value
            
        status, headers, output, errors = raw_interactive(app, url, **environ)
        assert not errors, "errors should be printed directly to sys.stderr"
        if self.options.display_headers:
            for name, value in headers:
                sys.stdout.write('%s: %s\n' % (name, value))
            sys.stdout.write('\n')
        sys.stdout.write(output)
        sys.stdout.flush()
        status_int = int(status.split()[0])
        if status_int != 200:
            return status_int

    def parse_args(self, args):
        if args == ['-h']:
            Command.parse_args(self, args)
            return
        # These are the arguments parsed normally:
        normal_args = []
        # And these are arguments passed to the URL:
        extra_args = []
        # This keeps track of whether we have the two required positional arguments:
        pos_args = 0
        while args:
            start = args[0]
            if not start.startswith('-'):
                if pos_args < 2:
                    pos_args += 1
                    normal_args.append(start)
                    args.pop(0)
                    continue
                else:
                    normal_args.append(start)
                    args.pop(0)
                    continue
            else:
                found = False
                for option in self.ARG_OPTIONS:
                    if start == option:
                        normal_args.append(start)
                        args.pop(0)
                        if not args:
                            raise BadCommand(
                                "Option %s takes an argument" % option)
                        normal_args.append(args.pop(0))
                        found = True
                        break
                    elif start.startswith(option+'='):
                        normal_args.append(start)
                        args.pop(0)
                        found = True
                        break
                if found:
                    continue
                if start in self.OTHER_OPTIONS:
                    normal_args.append(start)
                    args.pop(0)
                    continue
                extra_args.append(start)
                args.pop(0)
        Command.parse_args(self, normal_args)
        # Add the extra arguments back in:
        self.args = self.args + extra_args
            
