#!/usr/bin/env python

# Copyright 2012 Paul Durivage <pauldurivage at gmail dot com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

#
# Please see https://github.com/angstwad/cf for the latest version and documentation.
#

import argparse
import ConfigParser
import os
import sys

try:
    import cloudfiles
    import cloudfiles.errors
except ImportError as e:
    print "cf requires the python-cloudfiles module -- please install this module."
    sys.exit(1)

__author__ = 'Paul Durivage <paul durivage at gmail dot com>'
__version__ = '0.20'

def do_rax_connection(creds):
    """ Authentication handler
    param: creds as a dict in the form of {'user': value, 'apikey': value}
    Returns cloudfiles.connection object
    """
    try:
        # Returns connection object
        return cloudfiles.get_connection(creds['user'], creds['apikey'])
    except cloudfiles.errors.AuthenticationFailed as e:
        print "Authentication to the Cloud Files API has failed: " + e.message
    except cloudfiles.errors.AuthenticationError as e:
        print "Authentication to the Cloud Files API has failed: there was an unspecified error."
        print e.message
    except:
        raise


def get_container_objects(selected_container):
    """ Handles retrieval of a container's file objects
    param: selected_container as cloudfiles.container object
    Returns: cloudfiles.storage_objects obj
    """
    return selected_container.get_objects()

def get_container(cf_conn, requested_container):
    """ Grabs a container based on its name and returns it
    param: cf_conn as cloudfiles.connection obj
    param: requested_container as string
    returns: cloudfiles.container object
    """
    containers = cf_conn.get_all_containers()
    for (index, item) in enumerate(containers):
        if str(item).strip() == requested_container:
            return containers[index]
    # If the container doesn't exist
    raise cloudfiles.errors.NoSuchContainer("Container not found: %s" % requested_container)

def cont_list(cf_conn):
    """ List containers
    param: cf_conn as cloudfiles.connection obj
    """
    for container in cf_conn.get_all_containers():
        print container
    print '\n' + repr(cf_conn.get_all_containers())

def cont_create(cf_conn, container_name):
    """ Container creation handler;
    param: cf_conn as cloudfiles.connection obj
    param: container_name as string
    """
    new_container = None
    try:
        new_container = cf_conn.create_container(container_name, error_on_existing=True)
    except cloudfiles.errors.ContainerExists as e:
        print "Container '%s' already exists." % e.message
    if isinstance(new_container, cloudfiles.Container):
        print "Container created successfully."

def cont_delete(cf_conn, container_name):
    """ Container deletion handler: handles only empty containers ATM
    param: cf_conn as cloudfiles.connection obj
    param: container_name as string
    """
    try:
        cf_conn.delete_container(container_name)
    except cloudfiles.errors.ContainerNotEmpty as e:
        print "Could not delete '%s': Container Not Empty" % e.container_name
    except:
        raise
    else:
        print "%s deleted successfully." % container_name

def cont_action(args, creds):
    """ Actions for requests on remote containers
    param: args as a parsed args obj
    param: creds as a dict
    """
    cf_conn = do_rax_connection(creds)
    if args.create:
        cont_create(cf_conn, args.container)
    elif args.delete:
        cont_delete(cf_conn, args.container)
    else:
        pass

def obj_list(objects):
    """ Object list in a remote container;
    param: objects as cloudfiles.container
    """
    for object in objects:
        print object.name
    print '\n' + repr(objects)

def obj_delete(selected_file, selected_container):
    """ Object deletion on a remote container
    param: file as list of strings
    param: selected_container as cloudfiles.container obj
    """
    try:
        for file in selected_file:
            selected_container.delete_object(file)
            print "Deleted '%s'." % file
    except cloudfiles.errors.ResponseError as e:
        print "Error %s, code: %s." % (e.reason, e.status)
    except:
        raise

def obj_action(args, creds):
    """ Actions for requests on specific objects in remote containers
    param: args as a parsed args obj
    param: creds as a dict
    """
    cf_conn = do_rax_connection(creds)
    try:
        selected_container = get_container(cf_conn, args.container)
    except cloudfiles.errors.NoSuchContainer as e:
        print e.message
        sys.exit(1)
    except:
        raise
    # Delete obj
    if args.delete:
        obj_delete(args.file, selected_container)

def list_action(args, creds):
    """ Actions for any "list" request
    param: args as a parsed args obj
    param: creds as a dict
    """
    cf_conn = do_rax_connection(creds)
    # List container objects
    if args.container != "all":
        try:
            selected_container = get_container(cf_conn, args.container)
        except cloudfiles.errors.NoSuchContainer as e:
            print e.message
            sys.exit(1)
        except:
            raise
        objects = get_container_objects(selected_container)
        # List objs in container
        obj_list(objects)
    # Just list containers
    elif args.container == "all":
        cont_list(cf_conn)
    else:
        raise cloudfiles.errors.NoSuchContainer('No container found')

def file_progress(*args):
    # TODO: Make this work better
    num = str(int( ( float(args[0]) / float(args[1]) ) * 100))
    sys.stdout.write(num)
    sys.stdout.write('\b'*len(num)), sys.stdout.flush()

def put_file(container, file):
    """ Create a remote object and put a local file into that object
    param: container as cloudfiles.container obj
    file as a string (a path)
    """
    container.create_object(os.path.basename(file)).load_from_filename(file, callback=file_progress)

def get_file(container, dir, file):
    """ Get actions to download from CF
    param: container as cloudfiles.container obj
    param: dir as a string (a path to put the file)
    param: file as a string
    """
    path_to_file = str(dir + file)
    container.get_object(file).save_to_filename(path_to_file, callback=file_progress)

def put_action(args, creds):
    """ Execute file putting actions into CF
    param: args as a parsed args obj
    param: creds as a dict
    """
    # Boilerplate go-connect cf_conn stuff, then grab our container obj
    cf_conn = do_rax_connection(creds)
    select_container = get_container(cf_conn, args.container)
    # Put files, plus a little verbosity to let people know we're doing something
    for file in args.file:
        f_basename = os.path.basename(file)
        sys.stdout.write("Putting %s\t\t " % f_basename)
        sys.stdout.write('%\b\b\b\b'), sys.stdout.flush()
        put_file(select_container, file)
        sys.stdout.write("\b\b\b.......Done!\n"), sys.stdout.flush()

def get_action(args, creds):
    """ File getting actions from CF
    param: args as a parsed args obj
    param: creds as a dict
    """
    # Make a proper file path with a trailing /.  Unix-y.
    if args.dest is None:
        dir  = os.getcwd() + '/'
    elif args.dest is not None:
        dir = os.path.abspath(args.dest) + '/'
    # Boilerplate go-connect cf_conn stuff, then grab our container obj
    cf_conn = do_rax_connection(creds)
    selected_container = get_container(cf_conn, args.container)
    # Get files, plus a little verbosity to let people know we're doing something
    for file in args.file:
        sys.stdout.write("Getting %s\t\t " % file)
        sys.stdout.write('%\b\b\b\b'), sys.stdout.flush()
        get_file(selected_container, dir, file)
        sys.stdout.write("\b\b\b.......Done!\n"), sys.stdout.flush()

def cred_args_specified(args, creds):
    # TODO: What to do if credentials are specified
    pass

def test_creds_action(args, creds):
    #  TODO: requesting a connection check against rax
    pass

def arg_parser():
    parser = argparse.ArgumentParser(description='cf - A command line clent to Rackspace Cloud Files', prog='cf')
    subparser = parser.add_subparsers()

    # Arguments for actions on containers
    container_parser = subparser.add_parser('cont', help='container actions')
    cont_group =  container_parser.add_mutually_exclusive_group(required=True)
    cont_group.add_argument('-D', '--delete', action='store_true', help='delete container')
    cont_group.add_argument('-C', '--create', action='store_true', help='create container')
    container_parser.add_argument('container', help='container on which to perform action')
    container_parser.set_defaults(func=cont_action)

    # Object arguments
    obj_parser = subparser.add_parser('obj', help='object (file) actions')
    obj_group = obj_parser.add_mutually_exclusive_group(required=True)
    obj_group.add_argument('-d', '--delete', action='store_true', help='delete file')
    obj_parser.add_argument('container', help='container name')
    obj_parser.add_argument('file', nargs='+', help='file(s) or object(s) to perform actions on')
    obj_parser.set_defaults(func=obj_action)
    # TODO: implement get-URI-if-pub args

    # List action arguments
    list_parser = subparser.add_parser('list', help='list actions')
    list_parser.add_argument('container', metavar="[container] or 'all'",
        help="container name, or 'all' to list all containers.")
    list_parser.set_defaults(func=list_action)

    # Put-file arguments
    put_parser = subparser.add_parser('put', help='put actions')
    put_parser.add_argument('container', action='store', help='container to put file(s) into')
    put_parser.add_argument('file', nargs='+', help='file(s) to put')
    put_parser.set_defaults(func=put_action)

    # Get-file arguments
    get_parser = subparser.add_parser('get', help='get actions')
    get_parser.add_argument('container', action='store', help='container to get file(s) from')
    get_parser.add_argument('file', nargs='+', help='file(s) to get')
    get_parser.add_argument('-d', '--dest', help='local destination of file(s)')
    get_parser.set_defaults(func=get_action)

    # Test Credentials
    test_parser = subparser.add_parser('test', help='test login credentials')
    test_parser.add_argument('--credentials', action='store_true', default=True, help='Test login credentials (default action)')
    test_parser.set_defaults(func=test_creds_action)

    # Credentails arguments
    creds_group = parser.add_argument_group(title="Login credentials arguments")
    creds_group.add_argument('--user', help='Username to use with RAX CF API')
    creds_group.add_argument('--apikey', help='API key to use against RAX CF API')
#    creds_group.set_defaults(func=cred_args_specified)  # TODO: Test this

    # Optional Arguments
    parser.add_argument('-V', '--version', action='version', version="cf "+ __version__,
        help="Print the version and exit")

    # Return the parsed args
    return parser.parse_args()

def config_parser():
        # Define the config file as $HOME/.cf
        cfgfile = os.environ['HOME'] + '/.cf'

        # If the config file doesn't exist
        if not os.path.exists(cfgfile):
            print "No default config exists at ~/.cf: Creating one"
            config = ConfigParser.RawConfigParser()
            config.add_section('API_Info')
            config.set('API_Info', 'username', 'someuser')
            config.set('API_Info', 'apikey', 'd1774cf48bec77a0a489a4b124c3a6478876b610')
            with open(cfgfile, 'a+') as configfile:
                config.write(configfile)
            print "Please edit the configuration at ~/.cf and rerun this program."
        # If the config file exists, read it
        elif os.path.exists(cfgfile):
            config = ConfigParser.RawConfigParser()
            config.read(cfgfile)
            username = config.get('API_Info', 'username')
            apikey = config.get('API_Info', 'apikey')
            return {'user': username, 'apikey': apikey}
        # Otherwise...
        else:
            print ("""Cannot create or read a configuration file.  Please ensure the
             current user has read/write permissions to %s""", cfgfile)
            sys.exit(1)

def check_config():
    # TODO: Check for env variables, cli args, or config file
    pass

def main():
    creds = config_parser()
    args = arg_parser()
    args.func(args, creds)

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print "\nKeyboard interrupt; cancelling operation."
        sys.exit(-1)