"""
This is the interface for interacting with the Group Web Service.
"""

from datetime import datetime
import json
import re
from urllib.parse import urlencode
from restclients_core.exceptions import DataFailureException
from uw_gws.dao import GWS_DAO
from uw_gws.models import (
    Group, CourseGroup, GroupReference, GroupEntity, GroupMember,
    GroupAffiliate)
from uw_gws.exceptions import InvalidGroupID

DAO = GWS_DAO()

class GWS(object):
    """
    The GWS object has methods for getting group information.
    """
    API = '/group_sws/v3'
    QTRS = {'win': 'winter', 'spr': 'spring', 'sum': 'summer', 'aut': 'autumn'}
    RE_GROUP_ID = re.compile(r'^[a-z0-9][\w\.-]+$', re.I)

    def __init__(self, config={}):
        self.actas = config['actas'] if 'actas' in config else None

    def search_groups(self, **kwargs):
        """
        Returns a list of restclients.GroupReference objects matching the
        passed parameters. Valid parameters are:
            name: parts_of_name
                name may include the wild-card (*) character.
            stem: group_stem
            member: member netid
            owner: admin netid
            instructor: instructor netid
                stem="course" will be set when this parameter is passed.
            student: student netid
                stem="course" will be set when this parameter is passed.
            affiliate: affiliate_name
            type: search_type
                Values are 'direct' to search for direct membership and
                'effective' to search for effective memberships. Default is
                direct membership.
            scope: search_scope
                Values are 'one' to limit results to one level of stem name
                and 'all' to return all groups.
        """
        kwargs = dict((k.lower(), kwargs[k].lower()) for k in kwargs)
        if 'type' in kwargs and (
                kwargs['type'] != 'direct' and kwargs['type'] != 'effective'):
            del(kwargs['type'])

        if 'scope' in kwargs and (
                kwargs['scope'] != 'one' and kwargs['scope'] != 'all'):
            del(kwargs['scope'])

        if "instructor" in kwargs or "student" in kwargs:
            kwargs["stem"] = "course"

        url = "{}/search?{}".format(self.API, urlencode(kwargs))

        data = self._get_resource(url)

        groups = []
        for datum in data.get('data', []):
            group = GroupReference()
            group.uwregid = datum.get('regid')
            group.display_name = datum.get('displayName')
            group.name = datum.get('id')
            group.url = datum.get('url')
            groups.append(group)

        return groups

    def get_group_by_id(self, group_id):
        """
        Returns a restclients.Group object for the group identified by the
        passed group ID.
        """
        self._valid_group_id(group_id)

        url = "{}/group/{}".format(self.API, group_id)

        data = self._get_resource(url)

        return self._group_from_json(data.get("data"))

    def create_group(self, group):
        """
        Creates a group from the passed restclients.Group object.
        """
        self._valid_group_id(group.id)

        body = {"data": group.json_data()}
        url = "{}/group/{}".format(self.API, group.name)

        data = self._put_resource(url, headers={}, body=body)

        return self._group_from_json(data.get("data"))

    def update_group(self, group):
        """
        Updates a group from the passed restclients.Group object.
        """
        self._valid_group_id(group.id)

        body = {"data": group.json_data()}
        headers = {"If-Match": "*"}
        url = "{}/group/{}".format(self.API, group.name)

        data = self._put_resource(url, headers, body)

        return self._group_from_json(data.get("data"))

    def delete_group(self, group_id):
        """
        Deletes the group identified by the passed group ID.
        """
        self._valid_group_id(group_id)

        url = "{}/group/{}".format(self.API, group_id)

        self._delete_resource(url)

        return True

    def get_members(self, group_id):
        """
        Returns a list of restclients.GroupMember objects for the group
        identified by the passed group ID.
        """
        self._valid_group_id(group_id)

        url = "{}/group/{}/member".format(self.API, group_id)

        data = self._get_resource(url)

        members = []
        for datum in data.get("data"):
            members.append(self._group_member_from_json(datum))
        return members

    def update_members(self, group_id, members):
        """
        Updates the membership of the group represented by the passed group id.
        Returns a list of members not found.
        """
        self._valid_group_id(group_id)

        body = {"data": [m.json_data() for m in members]}
        headers = {"If-Match": "*"}
        url = "{}/group/{}/member".format(self.API, group_id)

        data = self._put_resource(url, headers, body)

        errors = data.get("errors", [])
        if len(errors):
            return errors[0].get("notFound", [])
        return []

    def get_effective_members(self, group_id):
        """
        Returns a list of effective restclients.GroupMember objects for the
        group identified by the passed group ID.
        """
        self._valid_group_id(group_id)

        url = "{}/group/{}/effective_member".format(self.API, group_id)

        data = self._get_resource(url)

        members = []
        for datum in data.get("data"):
            members.append(self._group_member_from_json(datum))
        return members

    def get_effective_member_count(self, group_id):
        """
        Returns a count of effective members for the group identified by the
        passed group ID.
        """
        self._valid_group_id(group_id)

        url = "{}/group/{}/effective_member?view=count".format(self.API,
                                                               group_id)

        data = self._get_resource(url)

        count = data.get("data").get("count")
        return int(count)

    def is_effective_member(self, group_id, netid):
        """
        Returns True if the netid is in the group, False otherwise.
        """
        self._valid_group_id(group_id)

        # GWS doesn't accept EPPNs on effective member checks, for UW users
        netid = re.sub('@washington.edu', '', netid)

        url = "{}/group/{}/effective_member/{}".format(self.API,
                                                       group_id,
                                                       netid)

        try:
            data = self._get_resource(url)
            return True  # 200
        except DataFailureException as ex:
            if ex.status == 404:
                return False
            else:
                raise

    def _group_entity_from_json(self, data):
        return GroupEntity(name=data.get('id'),
                           type=data.get('type'),
                           display_name=data.get('name'))

    def _group_member_from_json(self, data):
        member = GroupMember(name=data.get('id'), type=data.get('type'))
        if data.get('mtype', None):
            member.mtype = data.get('mtype')
        if data.get('source', None):
            member.source = data.get('source')
        return member

    def _group_from_json(self, data):
        def _add_dt(timestamp):
            return datetime.fromtimestamp(float(timestamp)/1000.0)

        def _add_users(data, name, target):
            for item in data.get(name, []):
                target.append(self._group_entity_from_json(item))

        group_id = data.get('id')
        if re.match(r'^course_', group_id):
            course_data = data.get('course')
            group = CourseGroup()
            group.curriculum_abbr = course_data.get('curriculum')
            group.course_number = course_data.get('number')
            group.year = course_data.get('year')
            group.quarter = self.QTRS.get(course_data.get('quarter'))
            group.section_id = course_data.get('section')
            group.sln = course_data.get('sln')
            _add_users(course_data, 'instructors', group.instructors)
        else:
            group = Group()

        group.name = group_id
        group.uwregid = data.get('regid')
        group.display_name = data.get('displayName')
        group.description = data.get('description')
        group.contact = data.get('contact')
        group.authnfactor = data.get('authnfactor')
        group.classification = data.get('classification')
        group.dependson = data.get('dependson')

        try:
            group.last_modified = _add_dt(data.get('lastModified'))
        except (AttributeError, TypeError):
            pass

        try:
            group.membership_modified = _add_dt(data.get('lastMemberModified'))
        except (AttributeError, TypeError):
            pass

        _add_users(data, 'admins', group.admins)
        _add_users(data, 'updaters', group.updaters)
        _add_users(data, 'creators', group.creators)
        _add_users(data, 'readers', group.readers)
        _add_users(data, 'optins', group.optins)
        _add_users(data, 'optouts', group.optouts)

        for affl_data in data.get('affiliates'):
            affiliate = GroupAffiliate()
            affiliate.name = affl_data.get('name')
            affiliate.status = affl_data.get('status')
            affiliate.forward = affl_data.get('forward')
            _add_users(affl_data, 'sender', affiliate.senders)
            group.affiliates.append(affiliate)

        return group

    def _valid_group_id(self, group_id):
        if (group_id is None or not self.RE_GROUP_ID.match(group_id)):
            raise InvalidGroupID(group_id)

    def _get_resource(self, url, headers={}):
        headers["Accept"] = "application/json"
        if self.actas:
            headers["X-UW-Act-as"] = self.actas

        response = DAO.getURL(url, headers)

        if response.status != 200:
            raise DataFailureException(url, response.status, response.data)

        return json.loads(response.data)

    def _put_resource(self, url, headers={}, body={}):
        headers["Accept"] = "application/json"
        headers["Content-Type"] = "application/json"
        if self.actas:
            headers["X-UW-Act-as"] = self.actas

        response = DAO.putURL(url, headers, json.dumps(body))

        if response.status != 200:
            raise DataFailureException(url, response.status, response.data)

        return json.loads(response.data)

    def _delete_resource(self, url, headers={}):
        if self.actas:
            headers["X-UW-Act-as"] = self.actas

        response = DAO.deleteURL(url, headers)

        if response.status != 200:
            raise DataFailureException(url, response.status, response.data)


"""
http://docs.readthedocs.io/en/latest/getting_started.html#in-rst
pip install sphinx sphinx-autobuild
pip install sphinxcontrib-napoleon
pip install sphinx_rtd_theme

"""

def initialize_docs():
    from os.path import join
    import setup
    setupkw = setup.setupkw
    full_version = setup.parse_version()
    short_version = '.'.join(full_version.split('.')[0:2])

    doc_dpath = join('.', 'docs')

    exe = 'sphinx-apidoc'
    args = [
        exe,
        '--force',
        '--full',
        '--output-dir="{}"'.format(doc_dpath),
        '--doc-author="{}"'.format(setupkw['author']),
        '--doc-version="{}"'.format(short_version),
        '--doc-release="{}"'.format(full_version),
        '--maxdepth="8"',
        # '--ext-autodoc',
        # '--ext-ifconfig',
        # '--ext-githubpages',
        # '--ext-mathjax',
        setupkw['name'],
    ]
    cmdstr = ' '.join(args)

    import ubelt as ub
    result = ub.cmd(cmdstr, verbose=2)
    assert result['ret'] == 0

def modify_conf():
    """
    pip install redbaron
    """
    import redbaron
    import ubelt as ub
    conf_path = 'docs/conf.py'

    source = ub.readfrom(conf_path)
    red = redbaron.RedBaron(source)

    # Insert custom extensions
    extra_extensions = [
        '"sphinxcontrib.napoleon"'
    ]

    ext_node = red.find('name', value='extensions').parent
    ext_node.value.value.extend(extra_extensions)

    # Overwrite theme to read-the-docs
    theme_node = red.find('name', value='html_theme').parent
    theme_node.value.value = '"sphinx_rtd_theme"'

    ub.writeto(conf_path, red.dumps())


# encoding: utf-8
"""
Installation:
    pip install git+https://github.com/Erotemic/xdoctest.git

Developing:
    git clone https://github.com/Erotemic/xdoctest.git
    pip install -e xdoctest

Pypi:
    cd ~/code/xdoctest

    # Presetup
    pip install twine

    # First tag the source-code
    VERSION=$(python -c "import setup; print(setup.parse_version())")
    echo $VERSION
    git tag $VERSION -m "tarball tag $VERSION"
    git push --tags origin master

    # NEW API TO UPLOAD TO PYPI
    # https://packaging.python.org/tutorials/distributing-packages/

    # Build wheel or source distribution
    python setup.py bdist_wheel --universal

    # Use twine to upload. This will prompt for username and password
    twine upload --username erotemic --skip-existing dist/*

    # Check the url to make sure everything worked
    https://pypi.org/project/xdoctest/

    # ---------- OLD ----------------
    # Check the url to make sure everything worked
    https://pypi.python.org/pypi?:action=display&name=xdoctest

Notes:
    In case you need to delete a tag
    git push --delete origin tagname
    git tag --delete tagname
"""
from setuptools import setup
# from setuptools import find_packages

setupkw = dict(
    name='xdoctest',
    packages=['xdoctest', 'xdoctest.utils', 'xdoctest.docstr'],
    author='Jon Crall',
    author_email='erotemic@gmail.com',
    url='https://github.com/Erotemic/xdoctest',
    license='Apache 2',
)

def parse_version():
    """ Statically parse the version number from __init__.py """
    from os.path import dirname, join
    import ast
    modname = setupkw['name']
    init_fpath = join(dirname(__file__), modname, '__init__.py')
    with open(init_fpath) as file_:
        sourcecode = file_.read()
    pt = ast.parse(sourcecode)
    class VersionVisitor(ast.NodeVisitor):
        def visit_Assign(self, node):
            for target in node.targets:
                if target.id == '__version__':
                    self.version = node.value.s
    visitor = VersionVisitor()
    visitor.visit(pt)
    return visitor.version

def parse_requirements(fname='requirements.txt'):
    """
    Parse the package dependencies listed in a requirements file but strips
    specific versioning information.

    CommandLine:
        python -c "import setup; print(setup.parse_requirements())"
    """
    from os.path import dirname, join, exists
    import re
    require_fpath = join(dirname(__file__), fname)
    # This breaks on pip install, so check that it exists.
    if exists(require_fpath):
        with open(require_fpath, 'r') as f:
            packages = []
            for line in f.readlines():
                line = line.strip()
                if line and not line.startswith('#'):
                    if line.startswith('-e '):
                        package = line.split('#egg=')[1]
                        packages.append(package)
                    else:
                        pat = '|'.join(['>', '>=', '=='])
                        package = re.split(pat, line)[0]
                        packages.append(package)
            return packages
    return []

def parse_description():
    """
    Parse the description in the README file

    CommandLine:
        pandoc --from=markdown --to=rst --output=README.rst README.md
        python -c "import setup; print(setup.parse_description())"
    """
    from os.path import dirname, join, exists
    readme_fpath = join(dirname(__file__), 'README.rst')
    # This breaks on pip install, so check that it exists.
    if exists(readme_fpath):
        with open(readme_fpath, 'r') as f:
            text = f.read()
        return text
    return ''

version = parse_version()

if __name__ == '__main__':
    setup(
        version=version,
        description='A rewrite of the builtin doctest module',
        install_requires=parse_requirements('requirements.txt'),
        extras_require={
            'all': parse_requirements('optional-requirements.txt')
        },
        long_description=parse_description(),
        entry_points={
            # the pytest11 entry point makes the plugin available to pytest
            'pytest11': [
                'xdoctest = xdoctest.plugin',
            ],
            # the console_scripts entry point creates the xdoctest executable
            'console_scripts': [
                'xdoctest = xdoctest.__main__:main'
            ]
        },
        # custom PyPI classifier for pytest plugins
        classifiers=[
            'Framework :: Pytest',
            'Development Status :: 4 - Beta',
            'Intended Audience :: Developers',
            'Topic :: Software Development :: Libraries :: Python Modules',
            'Topic :: Utilities',
            # This should be interpreted as Apache License v2.0
            'License :: OSI Approved :: Apache Software License',
            # Supported Python versions
            'Programming Language :: Python :: 2.7',
            'Programming Language :: Python :: 3',
        ],
        **setupkw
    )


import six
import h5py
import numpy as np
import scipy.sparse as ss

FORMAT_DICT = {
    'csr': ss.csr_matrix,
    'csc': ss.csc_matrix,
}

def get_format_str(data):
    for format_str, format_class in six.viewitems(FORMAT_DICT):
        if isinstance(data, format_class):
            return format_str
    raise ValueError("Data type {} is not supported.".format(type(data)))

def get_format_class(format_str):
    try:
        format_class = FORMAT_DICT[format_str]
    except KeyError:
        raise ValueError("Format {} is not supported."
                         .format(format_str))
    return format_class

class Group(h5py.Group):
    """The HDF5 group that can detect and create sparse matrix.
    """

    def __getitem__(self, key):
        h5py_item = super(Group, self).__getitem__(key)
        if isinstance(h5py_item, h5py.Group):
            if 'h5sparse_format' in h5py_item.attrs:
                # detect the sparse matrix
                return Dataset(h5py_item)
            else:
                return Group(h5py_item.id)
        elif isinstance(h5py_item, h5py.Dataset):
            return h5py_item
        else:
            raise ValueError("Unexpected item type.")

    def create_dataset(self, name, shape=None, dtype=None, data=None,
                       sparse_format=None, indptr_dtype=np.int64, indices_dtype=np.int32,
                       **kwargs):
        """Create 3 datasets in a group to represent the sparse array.

        Parameters
        ----------
        sparse_format:
        """
        if isinstance(data, Dataset):
            assert sparse_format is None
            group = self.create_group(name)
            group.attrs['h5sparse_format'] = data.attrs['h5sparse_format']
            group.attrs['h5sparse_shape'] = data.attrs['h5sparse_shape']
            group.create_dataset('data', data=data.h5py_group['data'],
                                 dtype=dtype, **kwargs)
            group.create_dataset('indices', data=data.h5py_group['indices'],
                                 dtype=indices_dtype, **kwargs)
            group.create_dataset('indptr', data=data.h5py_group['indptr'],
                                 dtype=indptr_dtype, **kwargs)
        elif ss.issparse(data):
            if sparse_format is not None:
                format_class = get_format_class(sparse_format)
                data = format_class(data)
            group = self.create_group(name)
            group.attrs['h5sparse_format'] = get_format_str(data)
            group.attrs['h5sparse_shape'] = data.shape
            group.create_dataset('data', data=data.data, dtype=dtype, **kwargs)
            group.create_dataset('indices', data=data.indices, dtype=indices_dtype, **kwargs)
            group.create_dataset('indptr', data=data.indptr, dtype=indptr_dtype, **kwargs)
        elif data is None and sparse_format is not None:
            format_class = get_format_class(sparse_format)
            if dtype is None:
                dtype = np.float64
            if shape is None:
                shape = (0, 0)
            data = format_class(shape, dtype=dtype)
            group = self.create_group(name)
            group.attrs['h5sparse_format'] = get_format_str(data)
            group.attrs['h5sparse_shape'] = data.shape
            group.create_dataset('data', data=data.data, dtype=dtype, **kwargs)
            group.create_dataset('indices', data=data.indices, dtype=indices_dtype, **kwargs)
            group.create_dataset('indptr', data=data.indptr, dtype=indptr_dtype, **kwargs)
        else:
            # forward the arguments to h5py
            assert sparse_format is None
            return super(Group, self).create_dataset(
                name, data=data, shape=shape, dtype=dtype, **kwargs)
        return Dataset(group)

class File(h5py.File, Group):
    """The HDF5 file object that can detect and create sparse matrix.
    """
    pass

class Dataset(h5py.Group):
    """The HDF5 sparse matrix dataset.

    Parameters
    ----------
    h5py_group : h5py.Dataset
    """

    def __init__(self, h5py_group):
        super(Dataset, self).__init__(h5py_group.id)
        self.h5py_group = h5py_group
        self.shape = tuple(self.attrs['h5sparse_shape'])
        self.format_str = self.attrs['h5sparse_format']
        self.dtype = h5py_group['data'].dtype
        self.indptr_dtype = h5py_group['indptr'].dtype
        self.indices_dtype = h5py_group['indices'].dtype

    def __getitem__(self, key):
        if isinstance(key, slice):
            if key.step is not None:
                raise NotImplementedError("Index step is not supported.")
            start = key.start
            stop = key.stop
            if stop is not None and stop > 0:
                stop += 1
            if start is not None and start < 0:
                start -= 1
            indptr_slice = slice(start, stop)
            indptr = self.h5py_group['indptr'][indptr_slice]
            data = self.h5py_group['data'][indptr[0]:indptr[-1]]
            indices = self.h5py_group['indices'][indptr[0]:indptr[-1]]
            indptr -= indptr[0]
            if self.format_str == 'csr':
                shape = (indptr.size - 1, self.shape[1])
            elif self.format_str == 'csc':
                shape = (self.shape[0], indptr.size - 1)
            else:
                raise NotImplementedError("Slicing for format {} is not implemented."
                                          .format(self.format_str))
        elif isinstance(key, tuple) and key == ():
            data = self.h5py_group['data'][()]
            indices = self.h5py_group['indices'][()]
            indptr = self.h5py_group['indptr'][()]
            shape = self.shape
        else:
            raise NotImplementedError("Only support one slice as index.")

        format_class = get_format_class(self.attrs['h5sparse_format'])
        return format_class((data, indices, indptr), shape=shape)

    @property
    def value(self):
        return self[()]

    def append(self, sparse_matrix):
        if self.format_str != get_format_str(sparse_matrix):
            raise ValueError("Format not the same.")

        if self.format_str == 'csr':
            # data
            data = self.h5py_group['data']
            orig_data_size = data.shape[0]
            new_shape = (orig_data_size + sparse_matrix.data.shape[0],)
            data.resize(new_shape)
            data[orig_data_size:] = sparse_matrix.data

            # indptr
            indptr = self.h5py_group['indptr']
            orig_data_size = indptr.shape[0]
            append_offset = indptr[-1]
            new_shape = (orig_data_size + sparse_matrix.indptr.shape[0] - 1,)
            indptr.resize(new_shape)
            indptr[orig_data_size:] = (sparse_matrix.indptr[1:].astype(np.int64)
                                       + append_offset)

            # indices
            indices = self.h5py_group['indices']
            orig_data_size = indices.shape[0]
            new_shape = (orig_data_size + sparse_matrix.indices.shape[0],)
            indices.resize(new_shape)
            indices[orig_data_size:] = sparse_matrix.indices

            # shape
            self.shape = (
                self.shape[0] + sparse_matrix.shape[0],
                max(self.shape[1], sparse_matrix.shape[1]))
            self.attrs['h5sparse_shape'] = self.shape
        else:
            raise NotImplementedError("The append method for format {} is not implemented."
                                      .format(self.format_str))


"""
Contains a CLICommand for decrypting stdin to stdout.

Uses the following from :py:class:`swiftly.cli.context.CLIContext`:

==========  =====================
io_manager  For directing output.
==========  =====================
"""
"""
Copyright 2011-2013 Gregory Holt

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.
"""
import os

from swiftly.cli.command import CLICommand, ReturnCode
from swiftly.dencrypt import AES256CBC, aes_decrypt

def cli_decrypt(context, key):
    """
    Decrypts context.io_manager's stdin and sends that to
    context.io_manager's stdout.

    See :py:mod:`swiftly.cli.decrypt` for context usage information.

    See :py:class:`CLIDecrypt` for more information.
    """
    with context.io_manager.with_stdout() as stdout:
        with context.io_manager.with_stdin() as stdin:
            crypt_type = stdin.read(1)
            if crypt_type == AES256CBC:
                for chunk in aes_decrypt(key, stdin):
                    stdout.write(chunk)
                stdout.flush()
            else:
                raise ReturnCode(
                    'contents encrypted with unsupported type %r' % crypt_type)

class CLIDecrypt(CLICommand):
    """
    A CLICommand for decrypting stdin and sending that to stdout.

    See the output of ``swiftly help decrypt`` for more information.
    """

    def __init__(self, cli):
        super(CLIDecrypt, self).__init__(
            cli, 'decrypt', max_args=1, usage="""
Usage: %prog [main_options] decrypt [key]

For help on [main_options] run %prog with no args.

Decrypts standard input using the given [key] and sends that to standard
output. If the key is not provided on the command line or is a single dash "-",
it must be provided via a SWIFTLY_CRYPT_KEY environment variable.

This currently only supports AES 256 in CBC mode but other algorithms may be
offered in the future.
""".strip())

    def __call__(self, args):
        options, args, context = self.parse_args_and_create_context(args)
        key = args.pop(0) if args else None
        if not key or key == '-':
            key = os.environ.get('SWIFTLY_CRYPT_KEY')
        if not key:
            raise ReturnCode(
                'No key provided and no SWIFTLY_CRYPT_KEY in the environment.')
        return cli_decrypt(context, key)


"""
Concurrency API for Swiftly.

Copyright 2011-2013 Gregory Holt

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.
"""

__all__ = ['Concurrency']

import sys
from six.moves import queue

try:
    from eventlet import GreenPool, sleep, Timeout
except ImportError:
    GreenPool = None
    sleep = None
    Timeout = Exception

class Concurrency(object):
    """
    Convenience class to support concurrency, if Eventlet is
    available; otherwise it just performs at single concurrency.

    :param concurrency: The level of concurrency desired. Default: 10
    """

    def __init__(self, concurrency=10):
        self.concurrency = concurrency
        if self.concurrency and GreenPool:
            self._pool = GreenPool(self.concurrency)
        else:
            self._pool = None
        self._queue = queue.Queue()
        self._results = {}

    def _spawner(self, ident, func, *args, **kwargs):
        exc_type = exc_value = exc_tb = result = None
        try:
            result = func(*args, **kwargs)
        except (Exception, Timeout):
            exc_type, exc_value, exc_tb = sys.exc_info()
        self._queue.put((ident, (exc_type, exc_value, exc_tb, result)))

    def spawn(self, ident, func, *args, **kwargs):
        """
        Returns immediately to the caller and begins executing the
        func in the background. Use get_results and the ident given
        to retrieve the results of the func. If the func causes an
        exception, this exception will be caught and the
        sys.exc_info() will be returned via get_results.

        :param ident: An identifier to find the results of the func
            from get_results. This identifier can be anything unique
            to the Concurrency instance.
        :param func: The function to execute concurrently.
        :param args: The args to give the func.
        :param kwargs: The keyword args to the give the func.
        :returns: None
        """
        if self._pool:
            self._pool.spawn_n(self._spawner, ident, func, *args, **kwargs)
            sleep()
        else:
            self._spawner(ident, func, *args, **kwargs)

    def get_results(self):
        """
        Returns a dict of the results currently available. The keys
        are the ident values given with the calls to spawn. The
        values are tuples of (exc_type, exc_value, exc_tb, result)
        where:

        =========  ============================================
        exc_type   The type of any exception raised.
        exc_value  The actual exception if any was raised.
        exc_tb     The traceback if any exception was raised.
        result     If no exception was raised, this will be the
                   return value of the called function.
        =========  ============================================
        """
        try:
            while True:
                ident, value = self._queue.get(block=False)
                self._results[ident] = value
        except queue.Empty:
            pass
        return self._results

    def join(self):
        """
        Blocks until all currently pending functions have finished.
        """
        if self._pool:
            self._pool.waitall()


"""
Contains IOManager for managing access to input, output, error, and
debug file-like objects.
"""
"""
Copyright 2011-2013 Gregory Holt

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.
"""
import contextlib
import errno
import os
import sys

class IOManager(object):
    """
    Manages access to IO in ways that are mostly specific to
    :py:mod:`swiftly.cli` but might be generally useful.

    :param stdin: The file-like object to use for default stdin or
        sys.stdin if None.
    :param stdout: The file-like object to use for default stdout or
        sys.stdout if None.
    :param stderr: The file-like object to use for default stderr or
        sys.stderr if None.
    :param debug: The file-like object to use for default debug
        output or sys.stderr if None.
    :param stdin_root: The root path to use for requests for pathed
        stdin file-like objects.
    :param stdout_root: The root path to use for requests for pathed
        stdout file-like objects.
    :param stderr_root: The root path to use for requests for pathed
        stderr file-like objects.
    :param debug_root: The root path to use for requests for pathed
        debug file-like objects.
    :param stdin_sub_command: A shell line to pipe any stdin
        file-like objects through.
    :param stdout_sub_command: A shell line to pipe any stdout
        file-like objects through.
    :param stderr_sub_command: A shell line to pipe any stderr
        file-like objects through.
    :param debug_sub_command: A shell line to pipe any debug
        file-like objects through.
    :param subprocess_module: The subprocess module to use; for
        instance, you might use eventlet.green.subprocess instead of
        the standard subprocess module.
    :param verbose: A function to call with ``(msg)`` when waiting
        for subcommands to complete and for logging the subcommands'
        return codes.
    """

    def __init__(self, stdin=None, stdout=None, stderr=None, debug=None,
                 stdin_root=None, stdout_root=None, stderr_root=None,
                 debug_root=None, stdin_sub_command=None,
                 stdout_sub_command=None, stderr_sub_command=None,
                 debug_sub_command=None, subprocess_module=None, verbose=None):
        self.stdin = stdin or sys.stdin
        self.stdout = stdout or sys.stdout
        self.stderr = stderr or sys.stderr
        self.debug = debug or sys.stderr
        self.stdin_root = stdin_root
        self.stdout_root = stdout_root
        self.stderr_root = stderr_root
        self.debug_root = debug_root
        self.stdin_sub_command = stdin_sub_command
        self.stdout_sub_command = stdout_sub_command
        self.stderr_sub_command = stderr_sub_command
        self.debug_sub_command = debug_sub_command
        self.subprocess_module = subprocess_module
        if not self.subprocess_module:
            import subprocess
            self.subprocess_module = subprocess
        self.verbose = verbose

    def client_path_to_os_path(self, client_path):
        """
        Converts a client path into the operating system's path by
        replacing instances of '/' with os.path.sep.

        Note: If the client path contains any instances of
        os.path.sep already, they will be replaced with '-'.
        """
        if os.path.sep == '/':
            return client_path
        return client_path.replace(os.path.sep, '-').replace('/', os.path.sep)

    def os_path_to_client_path(self, os_path):
        """
        Converts an operating system path into a client path by
        replacing instances of os.path.sep with '/'.

        Note: If the client path contains any instances of '/'
        already, they will be replaced with '-'.
        """
        if os.path.sep == '/':
            return os_path
        return os_path.replace('/', '-').replace(os.path.sep, '/')

    def _get_path(self, root, os_path):
        path = None
        if root:
            if root.endswith(os.path.sep):
                if os_path:
                    path = os.path.join(root, os_path)
            else:
                path = root
        return path

    def _get_in_and_path(self, default, root, sub_command, os_path):
        inn = default
        path = self._get_path(root, os_path)
        if path:
            inn = open(path, 'rb')
        if sub_command:
            inn = self.subprocess_module.Popen(
                sub_command, shell=True, stdin=inn,
                stdout=self.subprocess_module.PIPE)
        return inn, path

    def _get_out_and_path(self, default, root, sub_command, os_path):
        out = default
        path = self._get_path(root, os_path)
        if path:
            dirname = os.path.dirname(path)
            if dirname:
                try:
                    os.makedirs(dirname)
                except OSError as err:
                    if err.errno != errno.EEXIST:
                        raise
            out = open(path, 'wb')
        if sub_command:
            out = self.subprocess_module.Popen(
                sub_command, shell=True, stdin=self.subprocess_module.PIPE,
                stdout=out)
        return out, path

    def get_stdin(self, os_path=None, skip_sub_command=False):
        """
        Returns a stdin-suitable file-like object based on the
        optional os_path and optionally skipping any configured
        sub-command.
        """
        sub_command = None if skip_sub_command else self.stdin_sub_command
        inn, path = self._get_in_and_path(
            self.stdin, self.stdin_root, sub_command, os_path)
        if hasattr(inn, 'stdout'):
            return inn.stdout
        return inn

    def get_stdout(self, os_path=None, skip_sub_command=False):
        """
        Returns a stdout-suitable file-like object based on the
        optional os_path and optionally skipping any configured
        sub-command.
        """
        sub_command = None if skip_sub_command else self.stdout_sub_command
        out, path = self._get_out_and_path(
            self.stdout, self.stdout_root, sub_command, os_path)
        if hasattr(out, 'stdin'):
            return out.stdin
        return out

    def get_stderr(self, os_path=None, skip_sub_command=False):
        """
        Returns a stderr-suitable file-like object based on the
        optional os_path and optionally skipping any configured
        sub-command.
        """
        sub_command = None if skip_sub_command else self.stderr_sub_command
        out, path = self._get_out_and_path(
            self.stderr, self.stderr_root, sub_command, os_path)
        if hasattr(out, 'stdin'):
            return out.stdin
        return out

    def get_debug(self, os_path=None, skip_sub_command=False):
        """
        Returns a debug-output-suitable file-like object based on the
        optional os_path and optionally skipping any configured
        sub-command.
        """
        sub_command = None if skip_sub_command else self.debug_sub_command
        out, path = self._get_out_and_path(
            self.debug, self.debug_root, sub_command, os_path)
        if hasattr(out, 'stdin'):
            return out.stdin
        return out

    def _wait(self, item, path):
        if hasattr(item, 'wait'):
            if self.verbose:
                msg = 'Waiting on sub-command'
                if path:
                    msg += ' to close %s' % path
                self.verbose(msg)
            exit_code = item.wait()
            if self.verbose:
                msg = 'Sub-command exited with %s' % exit_code
                if path:
                    msg += ' and closed %s' % path
                self.verbose(msg)

    def _close(self, item):
        if item not in (self.stdin, self.stdout, self.stderr, self.debug):
            if hasattr(item, 'close'):
                item.close()

    @contextlib.contextmanager
    def with_stdin(self, os_path=None, skip_sub_command=False,
                   disk_closed_callback=None):
        """
        A context manager yielding a stdin-suitable file-like object
        based on the optional os_path and optionally skipping any
        configured sub-command.

        :param os_path: Optional path to base the file-like object
            on.
        :param skip_sub_command: Set True to skip any configured
            sub-command filter.
        :param disk_closed_callback: If the backing of the file-like
            object is an actual file that will be closed,
            disk_closed_callback (if set) will be called with the
            on-disk path just after closing it.
        """
        sub_command = None if skip_sub_command else self.stdin_sub_command
        inn, path = self._get_in_and_path(
            self.stdin, self.stdin_root, sub_command, os_path)
        try:
            if hasattr(inn, 'stdout'):
                yield inn.stdout
            else:
                yield inn
        finally:
            if hasattr(inn, 'stdout'):
                self._close(inn.stdout)
            self._wait(inn, path)
            self._close(inn)
            if disk_closed_callback and path:
                disk_closed_callback(path)

    @contextlib.contextmanager
    def with_stdout(self, os_path=None, skip_sub_command=False,
                    disk_closed_callback=None):
        """
        A context manager yielding a stdout-suitable file-like object
        based on the optional os_path and optionally skipping any
        configured sub-command.

        :param os_path: Optional path to base the file-like object
            on.
        :param skip_sub_command: Set True to skip any configured
            sub-command filter.
        :param disk_closed_callback: If the backing of the file-like
            object is an actual file that will be closed,
            disk_closed_callback (if set) will be called with the
            on-disk path just after closing it.
        """
        sub_command = None if skip_sub_command else self.stdout_sub_command
        out, path = self._get_out_and_path(
            self.stdout, self.stdout_root, sub_command, os_path)
        try:
            if hasattr(out, 'stdin'):
                yield out.stdin
            else:
                yield out
        finally:
            if hasattr(out, 'stdin'):
                self._close(out.stdin)
            self._wait(out, path)
            self._close(out)
            if disk_closed_callback and path:
                disk_closed_callback(path)

    @contextlib.contextmanager
    def with_stderr(self, os_path=None, skip_sub_command=False,
                    disk_closed_callback=None):
        """
        A context manager yielding a stderr-suitable file-like object
        based on the optional os_path and optionally skipping any
        configured sub-command.

        :param os_path: Optional path to base the file-like object
            on.
        :param skip_sub_command: Set True to skip any configured
            sub-command filter.
        :param disk_closed_callback: If the backing of the file-like
            object is an actual file that will be closed,
            disk_closed_callback (if set) will be called with the
            on-disk path just after closing it.
        """
        sub_command = None if skip_sub_command else self.stderr_sub_command
        out, path = self._get_out_and_path(
            self.stderr, self.stderr_root, sub_command, os_path)
        try:
            if hasattr(out, 'stdin'):
                yield out.stdin
            else:
                yield out
        finally:
            if hasattr(out, 'stdin'):
                self._close(out.stdin)
            self._wait(out, path)
            self._close(out)
            if disk_closed_callback and path:
                disk_closed_callback(path)

    @contextlib.contextmanager
    def with_debug(self, os_path=None, skip_sub_command=False,
                   disk_closed_callback=None):
        """
        A context manager yielding a debug-output-suitable file-like
        object based on the optional os_path and optionally skipping
        any configured sub-command.

        :param os_path: Optional path to base the file-like object
            on.
        :param skip_sub_command: Set True to skip any configured
            sub-command filter.
        :param disk_closed_callback: If the backing of the file-like
            object is an actual file that will be closed,
            disk_closed_callback (if set) will be called with the
            on-disk path just after closing it.
        """
        sub_command = None if skip_sub_command else self.debug_sub_command
        out, path = self._get_out_and_path(
            self.debug, self.debug_root, sub_command, os_path)
        try:
            if hasattr(out, 'stdin'):
                yield out.stdin
            else:
                yield out
        finally:
            if hasattr(out, 'stdin'):
                self._close(out.stdin)
            self._wait(out, path)
            self._close(out)
            if disk_closed_callback and path:
                disk_closed_callback(path)


"""
Contains a CLICommand that can issue DELETE requests.

Uses the following from :py:class:`swiftly.cli.context.CLIContext`:

==============  =====================================================
cdn             True if the CDN Management URL should be used instead
                of the Storage URL.
client_manager  For connecting to Swift.
concurrency     The number of concurrent actions that can be
                performed.
headers         A dict of headers to send.
ignore_404      True if 404s should be silently ignored.
io_manager      For directing output.
query           A dict of query parameters to send.
==============  =====================================================
"""
"""
Copyright 2011-2013 Gregory Holt

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.
"""
import six
from swiftly.concurrency import Concurrency
from swiftly.cli.command import CLICommand, ReturnCode

def cli_empty_account(context, yes_empty_account=False, until_empty=False):
    """
    Deletes all objects and containers in the account.

    You must set yes_empty_account to True to verify you really want to
    do this.

    By default, this will perform one pass at deleting all objects and
    containers; so if objects revert to previous versions or if new
    objects or containers otherwise arise during the process, the
    account may not be empty once done.

    Set `until_empty` to True if you want multiple passes to keep trying
    to fully empty and delete the containers. Note until_empty=True
    could run forever if something else is making new items faster than
    they're being deleted.

    See :py:mod:`swiftly.cli.delete` for context usage information.

    See :py:class:`CLIDelete` for more information.
    """
    if not yes_empty_account:
        raise ReturnCode(
            'called cli_empty_account without setting yes_empty_account=True')
    marker = None
    while True:
        with context.client_manager.with_client() as client:
            status, reason, headers, contents = client.get_account(
                marker=marker, headers=context.headers, query=context.query,
                cdn=context.cdn)
        if status // 100 != 2:
            if status == 404 and context.ignore_404:
                return
            raise ReturnCode('listing account: %s %s' % (status, reason))
        if not contents:
            if until_empty and marker:
                marker = None
                continue
            break
        for item in contents:
            cli_delete(
                context, item['name'], context.headers, recursive=True)
        marker = item['name']

def cli_empty_container(context, path, until_empty=False):
    """
    Deletes all objects in the container.

    By default, this will perform one pass at deleting all objects in
    the container; so if objects revert to previous versions or if new
    objects otherwise arise during the process, the container may not be
    empty once done.

    Set `until_empty` to True if you want multiple passes to keep trying
    to fully empty the container. Note until_empty=True could run
    forever if something else is making new objects faster than they're
    being deleted.

    See :py:mod:`swiftly.cli.delete` for context usage information.

    See :py:class:`CLIDelete` for more information.
    """
    path = path.rstrip('/').decode('utf8')
    conc = Concurrency(context.concurrency)

    def check_conc():
        for (exc_type, exc_value, exc_tb, result) in \
                six.itervalues(conc.get_results()):
            if exc_value:
                with context.io_manager.with_stderr() as fp:
                    fp.write(str(exc_value))
                    fp.write('\n')
                    fp.flush()

    marker = None
    while True:
        with context.client_manager.with_client() as client:
            status, reason, headers, contents = client.get_container(
                path, marker=marker, headers=context.headers,
                query=context.query, cdn=context.cdn)
        if status // 100 != 2:
            if status == 404 and context.ignore_404:
                return
            raise ReturnCode(
                'listing container %r: %s %s' % (path, status, reason))
        if not contents:
            if until_empty and marker:
                marker = None
                continue
            break
        for item in contents:
            newpath = '%s/%s' % (path, item['name'])
            new_context = context.copy()
            new_context.ignore_404 = True
            check_conc()
            conc.spawn(newpath, cli_delete, new_context, newpath)
        marker = item['name']
        conc.join()
        check_conc()

def cli_delete(context, path, body=None, recursive=False,
               yes_empty_account=False, yes_delete_account=False,
               until_empty=False):
    """
    Deletes the item (account, container, or object) at the path.

    See :py:mod:`swiftly.cli.delete` for context usage information.

    See :py:class:`CLIDelete` for more information.

    :param context: The :py:class:`swiftly.cli.context.CLIContext` to
        use.
    :param path: The path of the item (acount, container, or object)
        to delete.
    :param body: The body to send with the DELETE request. Bodies are
        not normally sent with DELETE requests, but this can be
        useful with bulk deletes for instance.
    :param recursive: If True and the item is an account or
        container, deletes will be issued for any containing items as
        well. This does one pass at the deletion; so if objects revert
        to previous versions or if new objects otherwise arise during
        the process, the container(s) may not be empty once done. Set
        `until_empty` to True if you want multiple passes to keep trying
        to fully empty the containers.
    :param until_empty: If True and recursive is True, this will cause
        Swiftly to keep looping through the deletes until the containers
        are completely empty. Useful if you have object versioning
        turned on or otherwise have objects that seemingly reappear
        after being deleted. It could also run forever if you have
        something that's uploading objects at a faster rate than they
        are deleted.
    :param yes_empty_account: This must be set to True for
        verification when the item is an account and recursive is
        True.
    :param yes_delete_account: This must be set to True for
        verification when the item is an account and you really wish
        a delete to be issued for the account itself.
    """
    path = path.lstrip('/') if path else ''
    if not path:
        if yes_empty_account:
            cli_empty_account(
                context, yes_empty_account=yes_empty_account,
                until_empty=until_empty)
        if yes_delete_account:
            with context.client_manager.with_client() as client:
                status, reason, headers, contents = client.delete_account(
                    headers=context.headers, query=context.query,
                    cdn=context.cdn, body=body,
                    yes_i_mean_delete_the_account=yes_delete_account)
                if status // 100 != 2:
                    if status == 404 and context.ignore_404:
                        return
                    raise ReturnCode(
                        'deleting account: %s %s' % (status, reason))
    elif '/' not in path.rstrip('/'):
        path = path.rstrip('/')
        if recursive:
            cli_empty_container(context, path, until_empty=until_empty)
        with context.client_manager.with_client() as client:
            status, reason, headers, contents = client.delete_container(
                path, headers=context.headers,
                query=context.query, cdn=context.cdn, body=body)
            if status // 100 != 2:
                if status == 404 and context.ignore_404:
                    return
                raise ReturnCode(
                    'deleting container %r: %s %s' % (path, status, reason))
    else:
        with context.client_manager.with_client() as client:
            status, reason, headers, contents = client.delete_object(
                *path.split('/', 1), headers=context.headers,
                query=context.query, cdn=context.cdn, body=body)
            if status // 100 != 2:
                if status == 404 and context.ignore_404:
                    return
                raise ReturnCode(
                    'deleting object %r: %s %s' % (path, status, reason))

class CLIDelete(CLICommand):
    """
    A CLICommand that can issue DELETE requests.

    See the output of ``swiftly help delete`` for more information.
    """

    def __init__(self, cli):
        super(CLIDelete, self).__init__(
            cli, 'delete', max_args=1, usage="""
Usage: %prog [main_options] delete [options] [path]

For help on [main_options] run %prog with no args.

Issues a DELETE request of the [path] given.""".strip())
        self.option_parser.add_option(
            '-h', '-H', '--header', dest='header', action='append',
            metavar='HEADER:VALUE',
            help='Add a header to the request. This can be used multiple '
                 'times for multiple headers. Examples: '
                 '-hx-some-header:some-value -h "X-Some-Other-Header: Some '
                 'other value"')
        self.option_parser.add_option(
            '-q', '--query', dest='query', action='append',
            metavar='NAME[=VALUE]',
            help='Add a query parameter to the request. This can be used '
                 'multiple times for multiple query parameters. Example: '
                 '-qmultipart-manifest=get')
        self.option_parser.add_option(
            '-i', '--input', dest='input_', metavar='PATH',
            help='Indicates where to read the DELETE request body from; '
                 'use a dash (as in "-i -") to specify standard input since '
                 'DELETEs do not normally take input.')
        self.option_parser.add_option(
            '--recursive', dest='recursive', action='store_true',
            help='Normally a delete for a non-empty container will error with '
                 'a 409 Conflict; --recursive will first delete all objects '
                 'in a container and then delete the container itself. For an '
                 'account delete, all containers and objects will be deleted '
                 '(requires the --yes-i-mean-empty-the-account option). Note '
                 'that this will do just one pass at deletion, so if objects '
                 'revert to previous versions or somehow otherwise arise '
                 'after the deletion pass, the container or account may not '
                 'be full empty once done. See --until-empty for a '
                 'multiple-pass option.')
        self.option_parser.add_option(
            '--until-empty', dest='until_empty', action='store_true',
            help='If used with --recursive, multiple passes will be attempted '
                 'to empty all the containers of objects and the account of '
                 'all containers. Note that could run forever if there is '
                 'something else creating items faster than they are deleted.')
        self.option_parser.add_option(
            '--yes-i-mean-empty-the-account', dest='yes_empty_account',
            action='store_true',
            help='Required when issuing a delete directly on an account with '
                 'the --recursive option. This will delete all containers and '
                 'objects in the account without deleting the account itself, '
                 'leaving an empty account. THERE IS NO GOING BACK!')
        self.option_parser.add_option(
            '--yes-i-mean-delete-the-account', dest='yes_delete_account',
            action='store_true',
            help='Required when issuing a delete directly on an account. Some '
                 'Swift clusters do not support this. Those that do will mark '
                 'the account as deleted and immediately begin removing the '
                 'objects from the cluster in the backgound. THERE IS NO '
                 'GOING BACK!')
        self.option_parser.add_option(
            '--ignore-404', dest='ignore_404', action='store_true',
            help='Ignores 404 Not Found responses; the exit code will be 0 '
                 'instead of 1.')

    def __call__(self, args):
        options, args, context = self.parse_args_and_create_context(args)
        context.headers = self.options_list_to_lowered_dict(options.header)
        context.query = self.options_list_to_lowered_dict(options.query)
        context.ignore_404 = options.ignore_404
        path = args.pop(0).lstrip('/') if args else None
        body = None
        if options.input_:
            if options.input_ == '-':
                body = self.cli.context.io_manager.get_stdin()
            else:
                body = open(options.input_, 'rb')
        recursive = options.recursive
        until_empty = options.until_empty
        yes_empty_account = options.yes_empty_account
        yes_delete_account = options.yes_delete_account
        if not path:
            if not recursive:
                if not yes_delete_account:
                    raise ReturnCode("""
A delete directly on an account requires the --yes-i-mean-delete-the-account
option as well.

Some Swift clusters do not support this.

Those that do will mark the account as deleted and immediately begin removing
the objects from the cluster in the backgound.

THERE IS NO GOING BACK!""".strip())
            else:
                if not yes_empty_account:
                    raise ReturnCode("""
A delete --recursive directly on an account requires the
--yes-i-mean-empty-the-account option as well.

All containers and objects in the account will be deleted, leaving an empty
account.

THERE IS NO GOING BACK!""".strip())
        return cli_delete(
            context, path, body=body, recursive=recursive,
            yes_empty_account=yes_empty_account,
            yes_delete_account=yes_delete_account, until_empty=until_empty)


"""
Contains an extended optparse.OptionParser that works better with
command line applications that may call other command line
applications or use multiple parsers.
"""
"""
Copyright 2011-2013 Gregory Holt

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.
"""
import sys
import optparse

def _stdout_filed(func):
    """
    Instance method decorator to convert an optional file keyword
    argument into an actual value, whether it be a passed value, a
    value obtained from an io_manager, or sys.stdout.
    """
    def wrapper(self, file=None):
        if file:
            return func(self, file=file)
        elif self.io_manager:
            with self.io_manager.with_stdout() as stdout:
                return func(self, file=stdout)
        else:
            return func(self, file=sys.stdout)
    wrapper.__doc__ = func.__doc__
    return wrapper

def _stderr_filed(func):
    """
    Instance method decorator to convert an optional file keyword
    argument into an actual value, whether it be a passed value, a
    value obtained from an io_manager, or sys.stderr.
    """
    def wrapper(self, msg, file=None):
        if file:
            return func(self, msg, file=file)
        elif self.io_manager:
            with self.io_manager.with_stderr() as stderr:
                return func(self, msg, file=stderr)
        else:
            return func(self, msg, file=sys.stderr)
    wrapper.__doc__ = func.__doc__
    return wrapper

class OptionParser(optparse.OptionParser, object):
    """
    Extended optparse.OptionParser that supports an io_manager,
    raw_epilog output, and an error prefix. It also does not exit
    automatically on error and instead sets an instance bool of
    error_encountered.
    """

    def __init__(self, usage=None, option_list=None,
                 option_class=optparse.Option, version=None,
                 conflict_handler='error', description=None, formatter=None,
                 add_help_option=True, prog=None, epilog=None,
                 io_manager=None, raw_epilog='', error_prefix=''):
        super(OptionParser, self).__init__(
            usage, option_list, option_class, version, conflict_handler,
            description, formatter, False, prog, epilog)
        if add_help_option:
            self.add_option(
                '-?', '--help', dest='help', action='store_true',
                help='Shows this help text.')
        if version:
            self.remove_option('--version')
            self.add_option(
                '--version', dest='version', action='store_true',
                help='Shows the version of this tool.')
        self.io_manager = io_manager
        #: Output just after the standard print_help output, in it's raw form.
        #: This is different than epilog in that epilog is reformatted.
        self.raw_epilog = raw_epilog
        #: Output just before any error. This can be useful in identification
        #: when multiple parsers are in use.
        self.error_prefix = error_prefix
        #: True if an error was encountered while parsing.
        self.error_encountered = False

    @_stderr_filed
    def error(self, msg, file=None):
        """
        Outputs the error msg to the file if specified, or to the
        io_manager's stderr if available, or to sys.stderr.
        """
        self.error_encountered = True
        file.write(self.error_prefix)
        file.write(msg)
        file.write('\n')
        file.flush()

    def exit(self, status=0, msg=None):
        """
        Immediately exits Python with the given status (or 0) as the
        exit code and optionally outputs the msg using self.error.
        """
        if msg:
            self.error(msg)
        sys.exit(status)

    @_stdout_filed
    def print_help(self, file=None):
        """
        Outputs help information to the file if specified, or to the
        io_manager's stdout if available, or to sys.stdout.
        """
        optparse.OptionParser.print_help(self, file)
        if self.raw_epilog:
            file.write(self.raw_epilog)
        file.flush()

    @_stdout_filed
    def print_usage(self, file=None):
        """
        Outputs usage information to the file if specified, or to the
        io_manager's stdout if available, or to sys.stdout.
        """
        optparse.OptionParser.print_usage(self, file)
        file.flush()

    @_stdout_filed
    def print_version(self, file=None):
        """
        Outputs version information to the file if specified, or to
        the io_manager's stdout if available, or to sys.stdout.
        """
        optparse.OptionParser.print_version(self, file)
        file.flush()


"""
Contains the CLICommand class that can be subclassed to create new
Swiftly commands.
"""
"""
Copyright 2011-2013 Gregory Holt

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.
"""
from swiftly.cli.optionparser import OptionParser

class ReturnCode(Exception):
    """
    Raise this to indicate the desire to exit.

    :param text: The text to report as the reason for exiting.
        Default: None
    :param code: The return code to give back to the shell.
        Default: 1
    """

    def __init__(self, text=None, code=1):
        Exception.__init__(self, text)
        self.text = text
        self.code = code

class CLICommand(object):
    """
    Subclass this to create new Swiftly commands.

    Don't forget to add your new class to
    :py:attr:`swiftly.cli.cli.COMMANDS`.

    Your subclass will be created by :py:class:`swiftly.cli.cli.CLI`
    with the CLI instance as the only parameter. You should then call
    this superclass with that CLI instance, your command's name, and
    any other options desired.

    See the implemention for other commands like
    :py:class:`swiftly.cli.auth.CLIAuth` for good starting points.

    :param cli: The :py:class:`swiftly.cli.cli.CLI` instance.
    :param name: The name of the command.
    :param min_args: The minimum number of arguments required.
    :param max_args: The maximum number of arguments allowed.
    :param usage: The usage string for
        :py:class:`OptionParser`.
    """

    def __init__(self, cli, name, min_args=None, max_args=None, usage=None):
        self.cli = cli
        self.name = name
        self.min_args = min_args
        self.max_args = max_args
        if usage is None:
            usage = """
Usage: %%prog [main_options] %s

For help on [main_options] run %%prog with no args.

Executes the %s command.""".strip() % (name, name)
        self.option_parser = OptionParser(
            usage=usage, io_manager=self.cli.context.io_manager,
            error_prefix=name + ' command: ')

    def parse_args_and_create_context(self, args):
        """
        Helper method that will parse the args into options and
        remaining args as well as create an initial
        :py:class:`swiftly.cli.context.CLIContext`.

        The new context will be a copy of
        :py:attr:`swiftly.cli.cli.CLI.context` with the following
        attributes added:

        =======================  ===================================
        muted_account_headers    The headers to omit when outputting
                                 account headers.
        muted_container_headers  The headers to omit when outputting
                                 container headers.
        muted_object_headers     The headers to omit when outputting
                                 object headers.
        =======================  ===================================

        :returns: options, args, context
        """
        original_args = args
        try:
            options, args = self.option_parser.parse_args(args)
        except UnboundLocalError:
            # Happens sometimes with an error handler that doesn't raise its
            # own exception. We'll catch the error below with
            # error_encountered.
            pass
        if self.option_parser.error_encountered:
            if '-?' in original_args or '-h' in original_args or \
                    '--help' in original_args:
                self.option_parser.print_help()
            raise ReturnCode()
        if options.help:
            self.option_parser.print_help()
            raise ReturnCode()
        if self.min_args is not None and len(args) < self.min_args:
            raise ReturnCode(
                'requires at least %s args.' % self.min_args)
        if self.max_args is not None and len(args) > self.max_args:
            raise ReturnCode(
                'requires no more than %s args.' % self.max_args)
        context = self.cli.context.copy()
        context.muted_account_headers = [
            'accept-ranges', 'content-length', 'content-type', 'date']
        context.muted_container_headers = [
            'accept-ranges', 'content-length', 'content-type', 'date']
        context.muted_object_headers = ['accept-ranges', 'date']
        return options, args, context

    def options_list_to_lowered_dict(self, options_list):
        """
        Helper function that will convert an options list into a dict
        of key/values.

        This is used for the quite common -hheader:value and
        -qparameter=value command line options, like this::

            context.headers = self.options_list_to_lowered_dict(options.header)
            context.query = self.options_list_to_lowered_dict(options.query)

        For a full example, see
        :py:func:`swiftly.cli.get.CLIGet.__call__`.
        """
        result = {}
        if options_list:
            for key in options_list:
                key = key.lstrip()
                colon = key.find(':')
                if colon < 1:
                    colon = None
                equal = key.find('=')
                if equal < 1:
                    equal = None
                if colon and (not equal or colon < equal):
                    key, value = key.split(':', 1)
                elif equal:
                    key, value = key.split('=', 1)
                else:
                    value = ''
                result[key.lower()] = value.lstrip()
        return result