#!/usr/bin/env python

#
# Guernsey REST client package, based on the Java Jersey client.
# Copyright (c) 2011 Simon Johnston (simon@johnstonshome.org)
# See LICENSE.txt included in this distribution or more details.
#

import guernsey
from guernsey import Client
from guernsey.filters import *
import atexit, logging, os.path, pprint, re, readline, StringIO
import subprocess, sys, threading, urlparse

# ------------------------------------------------------------
# Setup standard logging, this is used to support debug mode.
# ------------------------------------------------------------

FORMAT = '%(asctime)-15s %(module)s.%(funcName)s %(lineno)d [%(levelname)s] - %(message)s'
logging.basicConfig(format=FORMAT)
logger = logging.getLogger('guernsey')

# ------------------------------------------------------------
# Standard Filter list.
# ------------------------------------------------------------

filters = {'Logging': LoggingFilter('guernsey'), 'Gzip': GzipContentEncodingFilter(), 'MD5': ContentMd5Filter()}

# ------------------------------------------------------------
# Command-handling functions.
# ------------------------------------------------------------

commands = {}

class AsyncCommandThread(threading.Thread):
    def __init__(self, command, resource, args):
        self.command = command
        self.resource = resource
        self.args = args
        threading.Thread.__init__(self)

    def run(self):
        self.command(self.resource, self.args)

def add_command(cmd, name=None):
    if name is None:
        name = cmd.__name__
    commands[name] = cmd

def command_completer(text, index):
    try:
        return [cmd for cmd in commands.iterkeys() if cmd.startswith(text)][index]
    except IndexError:
        return None

# ------------------------------------------------------------
# Shell commands themselves and support functions.
# ------------------------------------------------------------

def substitute(string, environment):
    value = ''
    last = 0
    variables = re.compile('\$[a-zA-Z\-]+')
    results = variables.finditer(string)
    for result in results:
        (start, end) = result.span()
        value = value + string[last:start]
        last = end
        var = string[start+1:end]
        val = environment.get(var, '')
        value = value + val
    value = value + string[last:]
    return value

def get_input_details(args, method):
    filename = None
    content_type = None
    split = args.split(' ')
    if len(split) == 0 or (len(split) == 1 and split[0] == ''):
        pass
    elif len(split) == 1 and split[0] != '':
        if split[0].startswith('@'):
            filename = split[0][1:]
        else:
            content_type = split[0]
    elif len(split) == 2:
        if split[0].startswith('@'):
            filename = split[0][1:]
            content_type = split[1]
        else:
            print 'Try %s [@filename] [content-type]' % method
            return None
    else:
        print 'Try %s [@filename] [content-type]' % method
        return None
    return (filename, content_type)

def get_data(filename):
    data = None
    if filename is None:
        temp = StringIO.StringIO()
        blanks = 0
        while blanks < 2:
            line = raw_input()
            if line == '' and blanks == 0:
                temp.write(line + '\n')
                blanks = blanks + 1
            elif line == '':
                blanks = blanks + 1
            else:
                temp.write(line + '\n')
        data = temp.getvalue()
    else:
        fp = None
        try:
            fp = open(filename, 'rt')
            data = fp.read()
        except:
            logging.debug('Could not open file for reading: ' + filename)
        finally:
            if not fp is None:
                fp.close()
    return data

def cd(resource, path):
    """ cd path
        Resolve path against the current working resource and change
        URL.
    """
    try:
        return resource.path(path)
    except:
        print 'Invalid URL path: ' + path
add_command(cd)

def pwd(resource, args):
    """ pwd
        Print current target URL.
    """
    print resource.url
    return resource
add_command(pwd)

def post(resource, args):
    """ post [@filename] [content-type]
        Post content to the resource, if you specify the file name
        it's content will be read, otherwise all content entered
        manually until two blank lines will be sent. Note that the
        second parameter will set the Content-Type header and will
        override any value you specified with the ``set`` command.
    """
    input_details = get_input_details(args, 'post')
    if not input_details is None:
        (filename, content_type) = input_details
        entity = get_data(filename)
        if entity == None:
            print 'Unknown file, or IO error'
        else:
            if not content_type is None:
                resource.type(content_type)
            resource.entity(entity)
            response = resource.post()
            print str(response.status) + ' ' + response.reason_phrase
            if not response.location is None:
                print 'New resource location: ' + response.location
    return resource
add_command(post)

def put(resource, args):
    """ post [@filename] [content-type]
        Put content to the resource, if you specify the file name
        it's content will be read, otherwise all content entered
        manually until two blank lines will be sent. Note that the
        second parameter will set the Content-Type header and will
        override any value you specified with the ``set`` command.
    """
    input_details = get_input_details(args, 'put')
    if not input_details is None:
        (filename, content_type) = input_details
        entity = get_data(filename)
        if entity == None:
            print 'Unknown file, or IO error'
        else:
            if not content_type is None:
                resource.type(content_type)
            resource.entity(entity)
            response = resource.put()
            print str(response.status) + ' ' + response.reason_phrase
    return resource
add_command(put)

def get(resource, args):
    """ get [outputfile]
        Retrieve the current URL entity; if specified the returned
        entity will be written to ``outputfile``..
    """
    response = resource.get()
    print str(response.status) + ' ' + response.reason_phrase
    if response.status< 300 and not response.entity is None:
        if args == '':
            if not response.parsed_entity is None:
                pprint.pprint(response.parsed_entity)
            else:
                print response.entity
        else:
            fp = None
            try:
                fp = open(args, 'wb')
                fp.write(response.entity)
            except:
                logging.debug('Could not open file for writing: ' + filename)
            finally:
                if not fp is None:
                    fp.close()
    return resource
add_command(get)

def head(resource, args):
    """ head
        Retrieve the current resource headers only.
    """
    response = resource.head()
    print str(response.status) + ' ' + response.reason_phrase
    pprint.pprint(response.headers)
    return resource
add_command(head)

def delete(resource, args):
    """ delete
        Delete the current resource.
    """
    response = resource.delete()
    print str(response.status) + ' ' + response.reason_phrase
    return resource
add_command(delete)

def debug(resource, args):
    """ debug [On|Off]
        Either display the current debug status, or set it.
    """
    if args == '':
        if logger.isEnabledFor(logging.DEBUG):
            print 'debug is On'
        else:
            print 'debug is Off'
    elif args == 'On':
        logger.setLevel(logging.DEBUG)
    elif args == 'Off':
        logger.setLevel(logging.WARN)
    else:
        print 'Try: debug [On|Off]'
    return resource
add_command(debug)

def options(resource, args):
    """ options
        Retrieve the options for current resource.
    """
    response = resource.options()
    print str(response.status) + ' ' + response.reason_phrase
    pprint.pprint(response.headers)
    if not response.entity is None:
        print response.entity
    return resource
add_command(options)

def filter(resource, args):
    """ filter [name=On|Off]
        Either display the current status of system filters, or set the
        state of a specific filter.
    """
    if args == '':
        for filter in sorted(filters.keys()):
            if resource.is_filter_present(filters[filter]):
                print '%s is On' % filter
            else:
                print '%s is Off' % filter
    else:
        equals = args.find('=')
        if equals > 0:
            name = args[:equals].strip()
            if name in filters.keys():
                value = args[equals+1:].strip()
                if value == 'On':
                    resource.add_filter(filters[name])
                elif value == 'Off':
                    resource.remove_filter(filters[name])
                else:
                    print 'Must specify either On or Off'
            else:
                print 'Unknown filter: ' + name
        else:
            print 'Try filter name=On|Off'
    return resource
add_command(filter)

def query(resource, args):
    """ query [query_params]
        Either display the current query parameters, or set new query
        parameters.
    """
    if args == '':
        parsed = urlparse.urlparse(resource.url)
        print parsed.query
    else:
        params = args.split('&')
        param_list = [s.split('=') for s in params]
        try:
            resource = resource.query_params(dict(param_list))
        except:
            print 'Badly formatted query string: ' + args
    return resource
add_command(query)

def set(resource, args):
    """ set [key=value]
        Either display the current request settings, or define a new request
        setting by providing an HTTP request header key and value.
    """
    if args == '':
        for k in sorted(resource.headers.iterkeys()):
            print '%s=%s' % (k, resource.headers[k])
    else:
        equals = args.find('=')
        if equals > 0:
            value = substitute(args[equals+1:].strip(), resource.headers)
            if value == '':
                print 'May not set empty values'
            else:
                resource.headers[args[:equals].strip()] = value
    return resource
add_command(set)

def help(resource, args):
    """ help
        Display help on built-in shell commands.
    """
    for cmd in sorted(commands.iterkeys()):
        text = commands[cmd].__doc__
        if text is None:
            print ' ' + cmd
        else:
            print text
    return resource
add_command(help)

def exec_command(resource, args):
    """ ! [command]
        Execute a command in a sub-shell.
    """
    subprocess.call(args, shell=True)
    return resource
add_command(exec_command, "!")

def exit(resource, args):
    """ exit
        Exit the shell.
    """
    return resource
add_command(exit)

# ------------------------------------------------------------
# The following are only shell related functions.
# ------------------------------------------------------------

def configure_readline():
    histfile = os.path.join(os.path.expanduser("~"), ".resh_hist")
    try:
        readline.set_completer(command_completer)
        readline.parse_and_bind("tab: complete")
        readline.parse_and_bind ("bind ^I rl_complete") # OS X hack
        readline.read_history_file(histfile)
        atexit.register(readline.write_history_file, histfile)
    except IOError:
        pass

def parse_command_line(args):
    client = Client.create()
    if len(args) < 1:
        print 'Usage: resh URL'
        sys.exit(1)
    return client.resource(args[0])
 
def parse_rcfile(resource):
    rcfile = os.path.join(os.path.expanduser("~"), ".reshrc")
    if os.path.exists(rcfile):
        file = open(rcfile, 'rt')
        for line in file:
            parse_line(resource, line)
        file.close()

def parse_line(resource, line):
    line = line.strip()
    async = False
    if line.endswith('&'):
        line = line[:line.find('&')].strip()
        async = True
    if line != '':
        space = line.find(' ')
        if space > 0:
            cmd = line[:space]
            line = line[space+1:]
        else:
            cmd = line
            line = ''
        if commands.has_key(cmd):
            if async:
                thread = AsyncCommandThread(commands[cmd], resource.clone(), line)
                thread.start()
            else:
                resource = commands[cmd](resource, line)
        else:
            print 'Unknown command: ' + cmd
    return resource

def run_loop(resource):
    while True:
        try:
            input = raw_input('> ')
            if input.strip() == 'exit':
                break
            resource = parse_line(resource, input)
        except EOFError:
            break

if __name__ == '__main__':
    configure_readline()
    resource = parse_command_line(sys.argv[1:])
    parse_rcfile(resource)
    logger.info('Shell starting up')
    logger.info('Using Guernsey module: ' + guernsey.__file__)
    run_loop(resource)
