from __future__ import unicode_literals

from django.forms.fields import MultiValueField, CharField, ImageField

from .widgets import (
    SizedImageCenterpointClickDjangoAdminWidget, SizedImageCenterpointClickBootstrap3Widget,
    VersatileImagePPOIClickWidget
)

class SizedImageCenterpointMixIn(object):

    def compress(self, data_list):
        return tuple(data_list)

class VersatileImageFormField(ImageField):

    def to_python(self, data):
        """Ensure data is prepped properly before handing off to ImageField."""
        if data is not None:
            if hasattr(data, 'open'):
                data.open()
            return super(VersatileImageFormField, self).to_python(data)

class VersatileImagePPOIClickField(SizedImageCenterpointMixIn, MultiValueField):

    widget = VersatileImagePPOIClickWidget

    def __init__(self, *args, **kwargs):
        max_length = kwargs.pop('max_length', None)
        del max_length
        fields = (
            VersatileImageFormField(label='Image'),
            CharField(required=False)
        )
        super(VersatileImagePPOIClickField, self).__init__(
            tuple(fields), *args, **kwargs
        )

    def bound_data(self, data, initial):
        to_return = data
        if data[0] is None:
            to_return = initial
        return to_return

class SizedImageCenterpointClickDjangoAdminField(VersatileImagePPOIClickField):

    widget = SizedImageCenterpointClickDjangoAdminWidget
    # Need to remove `None` and `u''` so required fields will work
    # TODO: Better validation handling
    empty_values = [[], (), {}]

class SizedImageCenterpointClickBootstrap3Field(SizedImageCenterpointClickDjangoAdminField):

    widget = SizedImageCenterpointClickBootstrap3Widget


"""Fields."""
from __future__ import unicode_literals

import os

from django import VERSION as DJANGO_VERSION
from django.contrib.admin.widgets import AdminFileWidget
from django.db.models.fields import CharField
from django.db.models.fields.files import ImageField
from django.utils.translation import ugettext_lazy as _

from .files import VersatileImageFieldFile, VersatileImageFileDescriptor
from .forms import SizedImageCenterpointClickDjangoAdminField
from .placeholder import OnStoragePlaceholderImage
from .settings import VERSATILEIMAGEFIELD_PLACEHOLDER_DIRNAME
from .validators import validate_ppoi

class Creator(object):
    """Provides a way to set the attribute on the model."""

    def __init__(self, field):
        self.field = field

    def __get__(self, obj, objtype=None):
        return obj.__dict__[self.field.name]

    def __set__(self, obj, value):
        obj.__dict__[self.field.name] = self.field.to_python(value)

class VersatileImageField(ImageField):
    """Extends ImageField."""

    attr_class = VersatileImageFieldFile
    descriptor_class = VersatileImageFileDescriptor
    description = _('Versatile Image Field')

    def __init__(self, verbose_name=None, name=None, width_field=None,
                 height_field=None, ppoi_field=None, placeholder_image=None,
                 **kwargs):
        """Initialize an instance."""
        self.ppoi_field = ppoi_field
        super(VersatileImageField, self).__init__(
            verbose_name, name, width_field, height_field, **kwargs
        )
        self.placeholder_image = placeholder_image
        self.placeholder_image_name = None

    def process_placeholder_image(self):
        """
        Process the field's placeholder image.

        Ensures the placeholder image has been saved to the same storage class
        as the field in a top level folder with a name specified by
        settings.VERSATILEIMAGEFIELD_SETTINGS['placeholder_directory_name']

        This should be called by the VersatileImageFileDescriptor __get__.
        If self.placeholder_image_name is already set it just returns right away.
        """
        if self.placeholder_image_name:
            return

        placeholder_image_name = None
        placeholder_image = self.placeholder_image
        if placeholder_image:
            if isinstance(placeholder_image, OnStoragePlaceholderImage):
                name = placeholder_image.path
            else:
                name = placeholder_image.image_data.name
            placeholder_image_name = os.path.join(
                VERSATILEIMAGEFIELD_PLACEHOLDER_DIRNAME, name
            )
            if not self.storage.exists(placeholder_image_name):
                self.storage.save(
                    placeholder_image_name,
                    placeholder_image.image_data
                )
        self.placeholder_image_name = placeholder_image_name

    def pre_save(self, model_instance, add):
        """Return field's value just before saving."""
        file = super(VersatileImageField, self).pre_save(model_instance, add)
        self.update_ppoi_field(model_instance)
        return file

    def update_ppoi_field(self, instance, *args, **kwargs):
        """
        Update field's ppoi field, if defined.

        This method is hooked up this field's pre_save method to update
        the ppoi immediately before the model instance (`instance`)
        it is associated with is saved.

        This field's ppoi can be forced to update with force=True,
        which is how VersatileImageField.pre_save calls this method.
        """
        # Nothing to update if the field doesn't have have a ppoi
        # dimension field.
        if not self.ppoi_field:
            return

        # getattr will call the VersatileImageFileDescriptor's __get__ method,
        # which coerces the assigned value into an instance of
        # self.attr_class(VersatileImageFieldFile in this case).
        file = getattr(instance, self.attname)

        # file should be an instance of VersatileImageFieldFile or should be
        # None.
        ppoi = None
        if file and not isinstance(file, tuple):
            if hasattr(file, 'ppoi'):
                ppoi = file.ppoi

        # Update the ppoi field.
        if self.ppoi_field:
            setattr(instance, self.ppoi_field, ppoi)

    def save_form_data(self, instance, data):
        """
        Handle data sent from MultiValueField forms that set ppoi values.

        `instance`: The model instance that is being altered via a form
        `data`: The data sent from the form to this field which can be either:
        * `None`: This is unset data from an optional field
        * A two-position tuple: (image_form_data, ppoi_data)
            * `image_form-data` options:
                * `None` the file for this field is unchanged
                * `False` unassign the file form the field
            * `ppoi_data` data structure:
                * `%(x_coordinate)sx%(y_coordinate)s': The ppoi data to
                  assign to the unchanged file

        """
        to_assign = data
        if data and isinstance(data, tuple):
            # This value is coming from a MultiValueField
            if data[0] is None:
                # This means the file hasn't changed but we need to
                # update the ppoi
                current_field = getattr(instance, self.name)
                if data[1]:
                    current_field.ppoi = data[1]
                to_assign = current_field
            elif data[0] is False:
                # This means the 'Clear' checkbox was checked so we
                # need to empty the field
                to_assign = ''
            else:
                # This means there is a new upload so we need to unpack
                # the tuple and assign the first position to the field
                # attribute
                to_assign = data[0]
        super(VersatileImageField, self).save_form_data(instance, to_assign)

    def formfield(self, **kwargs):
        """Return a formfield."""
        # This is a fairly standard way to set up some defaults
        # while letting the caller override them.
        defaults = {}
        if self.ppoi_field:
            defaults['form_class'] = SizedImageCenterpointClickDjangoAdminField
        if kwargs.get('widget') is AdminFileWidget:
            # Ensuring default admin widget is skipped (in favor of using
            # SizedImageCenterpointClickDjangoAdminField's default widget as
            # the default widget choice for use in the admin).
            # This is for two reasons:
            # 1. To prevent 'typical' admin users (those who want to use
            #    the PPOI 'click' widget by default) from having to
            #    specify a formfield_overrides for each ModelAdmin class
            #    used by each model that has a VersatileImageField.
            # 2. If a VersatileImageField does not have a ppoi_field specified
            #    it will 'fall back' to a ClearableFileInput anyways.
            # If admin users do, in fact, want to force use of the
            # AdminFileWidget they can simply subclass AdminFileWidget and
            # specify it in their ModelAdmin.formfield_overrides (though,
            # if that's the case, why are they using VersatileImageField in
            # the first place?)
            del kwargs['widget']
        defaults.update(kwargs)
        return super(VersatileImageField, self).formfield(**defaults)

class PPOIField(CharField):

    def __init__(self, *args, **kwargs):
        if 'default' not in kwargs:
            kwargs['default'] = '0.5x0.5'
        kwargs['default'] = self.get_prep_value(
            value=validate_ppoi(
                kwargs['default'],
                return_converted_tuple=True
            )
        )
        if 'max_length' not in kwargs:
            kwargs['max_length'] = 20
        # Forcing editable = False since PPOI values are set directly on
        # VersatileImageField.
        kwargs['editable'] = False
        super(PPOIField, self).__init__(*args, **kwargs)
        self.validators.append(validate_ppoi)

    def contribute_to_class(self, cls, name, **kwargs):
        super(PPOIField, self).contribute_to_class(cls, name, **kwargs)
        setattr(cls, self.name, Creator(self))

    def from_db_value(self, value, *args, **kwargs):
        return self.to_python(value)

    def to_python(self, value):
        if value is None:
            value = '0.5x0.5'
        to_return = validate_ppoi(
            value, return_converted_tuple=True
        )
        return to_return

    def get_prep_value(self, value):
        if isinstance(value, tuple):
            value = 'x'.join(str(num) for num in value)
        return value

    def value_to_string(self, obj):
        """Prepare field for serialization."""
        if DJANGO_VERSION > (1, 9):
            value = self.value_from_object(obj)
        else:
            value = self._get_val_from_obj(obj)
        return self.get_prep_value(value)

__all__ = ['VersatileImageField', 'PPOIField']


from __future__ import unicode_literals

from functools import reduce
import logging
from sys import stdout

from django.db.models import Model
from django.db.models.query import QuerySet
from django.utils import six

from .utils import (
    get_rendition_key_set,
    get_url_from_image_key,
    validate_versatileimagefield_sizekey_list
)

logger = logging.getLogger(__name__)

def cli_progress_bar(start, end, bar_length=50):
    """
    Prints out a Yum-style progress bar (via sys.stdout.write).
    `start`: The 'current' value of the progress bar.
    `end`: The '100%' value of the progress bar.
    `bar_length`: The size of the overall progress bar.

    Example output with start=20, end=100, bar_length=50:
    [###########----------------------------------------] 20/100 (100%)

    Intended to be used in a loop. Example:
    end = 100
    for i in range(end):
        cli_progress_bar(i, end)

    Based on an implementation found here:
        http://stackoverflow.com/a/13685020/1149774
    """
    percent = float(start) / end
    hashes = '#' * int(round(percent * bar_length))
    spaces = '-' * (bar_length - len(hashes))
    stdout.write(
        "\r[{0}] {1}/{2} ({3}%)".format(
            hashes + spaces,
            start,
            end,
            int(round(percent * 100))
        )
    )
    stdout.flush()

class VersatileImageFieldWarmer(object):
    """
    A class for creating sets of images from a VersatileImageField
    """

    def __init__(self, instance_or_queryset,
                 rendition_key_set, image_attr, verbose=False):
        """
        Arguments:
        `instance_or_queryset`: A django model instance or QuerySet
        `rendition_key_set`: Either a string that corresponds to a key on
                        settings.VERSATILEIMAGEFIELD_RENDITION_KEY_SETS
                        or an iterable
                        of 2-tuples, both strings:
            [0]: The 'name' of the image size.
            [1]: A VersatileImageField 'size_key'.
            Example: [
                ('large', 'url'),
                ('medium', 'crop__400x400'),
                ('small', 'thumbnail__100x100')
            ]
        `image_attr`: A dot-notated path to a VersatileImageField on
                      `instance_or_queryset`
        `verbose`: bool signifying whether a progress bar should be printed
                   to sys.stdout
        """
        if isinstance(instance_or_queryset, Model):
            queryset = instance_or_queryset.__class__._default_manager.filter(
                pk=instance_or_queryset.pk
            )
        elif isinstance(instance_or_queryset, QuerySet):
            queryset = instance_or_queryset
        else:
            raise ValueError(
                "Only django model instances or QuerySets can be processed by "
                "{}".format(self.__class__.__name__)
            )
        self.queryset = queryset
        if isinstance(rendition_key_set, six.string_types):
            rendition_key_set = get_rendition_key_set(rendition_key_set)
        self.size_key_list = [
            size_key
            for key, size_key in validate_versatileimagefield_sizekey_list(
                rendition_key_set
            )
        ]
        self.image_attr = image_attr
        self.verbose = verbose

    @staticmethod
    def _prewarm_versatileimagefield(size_key, versatileimagefieldfile):
        """
        Returns a 2-tuple:
        0: bool signifying whether the image was successfully pre-warmed
        1: The url of the successfully created image OR the path on storage of
           the image that was not able to be successfully created.

        Arguments:
        `size_key_list`: A list of VersatileImageField size keys. Examples:
            * 'crop__800x450'
            * 'thumbnail__800x800'
        `versatileimagefieldfile`: A VersatileImageFieldFile instance
        """
        versatileimagefieldfile.create_on_demand = True
        try:
            url = get_url_from_image_key(versatileimagefieldfile, size_key)
        except Exception:
            success = False
            url_or_filepath = versatileimagefieldfile.name
            logger.exception('Thumbnail generation failed',
                             extra={'path': url_or_filepath})
        else:
            success = True
            url_or_filepath = url
        return (success, url_or_filepath)

    def warm(self):
        """
        Returns a 2-tuple:
        [0]: Number of images successfully pre-warmed
        [1]: A list of paths on the storage class associated with the
             VersatileImageField field being processed by `self` of
             files that could not be successfully seeded.
        """
        num_images_pre_warmed = 0
        failed_to_create_image_path_list = []
        total = self.queryset.count() * len(self.size_key_list)
        for a, instance in enumerate(self.queryset, start=1):
            for b, size_key in enumerate(self.size_key_list, start=1):
                success, url_or_filepath = self._prewarm_versatileimagefield(
                    size_key,
                    reduce(getattr, self.image_attr.split("."), instance)
                )
                if success is True:
                    num_images_pre_warmed += 1
                    if self.verbose:
                        cli_progress_bar(num_images_pre_warmed, total)
                else:
                    failed_to_create_image_path_list.append(url_or_filepath)

                if a * b == total:
                    stdout.write('\n')

        stdout.flush()
        return (num_images_pre_warmed, failed_to_create_image_path_list)


"""Registry."""
from __future__ import unicode_literals

import copy

from .datastructures import FilteredImage, SizedImage

class AlreadyRegistered(Exception):
    """Already registered exception."""

    pass

class InvalidSizedImageSubclass(Exception):
    """Ivalid sized image subclass exception."""

    pass

class InvalidFilteredImageSubclass(Exception):
    """Invalid filtered image subclass exception."""

    pass

class NotRegistered(Exception):
    """Not registered sizer/filter exception."""

    pass

class UnallowedSizerName(Exception):
    """Unallowed sizer name exception."""

    pass

class UnallowedFilterName(Exception):
    """Unallowed filter name exception."""

    pass

class VersatileImageFieldRegistry(object):
    """
    A VersatileImageFieldRegistry object.

    Allows new SizedImage & FilteredImage subclasses to be dynamically added
    to all SizedImageFileField instances at runtime. New SizedImage subclasses
    are registered with the register_sizer method. New ProcessedImage
    subclasses are registered with the register_filter method.
    """

    unallowed_sizer_names = (
        'build_filters_and_sizers',
        'chunks',
        'close',
        'closed',
        'create_on_demand',
        'delete',
        'encoding',
        'field',
        'file',
        'fileno',
        'filters',
        'flush',
        'get_filtered_root_folder',
        'get_sized_root_folder',
        'get_filtered_sized_root_folder',
        'delete_matching_files_from_storage',
        'delete_filtered_images',
        'delete_sized_images',
        'delete_filtered_sized_images',
        'delete_all_created_images',
        'height',
        'instance',
        'isatty',
        'multiple_chunks',
        'name',
        'newlines',
        'open',
        'path',
        'ppoi',
        'read',
        'readinto',
        'readline',
        'readlines',
        'save',
        'seek',
        'size',
        'softspace',
        'storage',
        'tell',
        'truncate',
        'url',
        'validate_ppoi',
        'width',
        'write',
        'writelines',
        'xreadlines'
    )

    def __init__(self, name='versatileimage_registry'):
        """Initialize a registry."""
        self._sizedimage_registry = {}  # attr_name -> sizedimage_cls
        self._filter_registry = {}  # attr_name -> filter_cls
        self.name = name

    def register_sizer(self, attr_name, sizedimage_cls):
        """
        Register a new SizedImage subclass (`sizedimage_cls`).

        To be used via the attribute (`attr_name`).
        """
        if attr_name.startswith(
            '_'
        ) or attr_name in self.unallowed_sizer_names:
            raise UnallowedSizerName(
                "`%s` is an unallowed Sizer name. Sizer names cannot begin "
                "with an underscore or be named any of the "
                "following: %s." % (
                    attr_name,
                    ', '.join([
                        name
                        for name in self.unallowed_sizer_names
                    ])
                )
            )
        if not issubclass(sizedimage_cls, SizedImage):
            raise InvalidSizedImageSubclass(
                'Only subclasses of versatileimagefield.datastructures.'
                'SizedImage may be registered with register_sizer'
            )

        if attr_name in self._sizedimage_registry:
            raise AlreadyRegistered(
                'A SizedImage class is already registered to the `%s` '
                'attribute. If you would like to override this attribute, '
                'use the unregister method' % attr_name
            )
        else:
            self._sizedimage_registry[attr_name] = sizedimage_cls

    def unregister_sizer(self, attr_name):
        """
        Unregister the SizedImage subclass currently assigned to `attr_name`.

        If a SizedImage subclass isn't already registered to `attr_name`
        NotRegistered will raise.
        """
        if attr_name not in self._sizedimage_registry:
            raise NotRegistered(
                'No SizedImage subclass is registered to %s' % attr_name
            )
        else:
            del self._sizedimage_registry[attr_name]

    def register_filter(self, attr_name, filterimage_cls):
        """
        Register a new FilteredImage subclass (`filterimage_cls`).

        To be used via the attribute (filters.`attr_name`)
        """
        if attr_name.startswith('_'):
            raise UnallowedFilterName(
                '`%s` is an unallowed Filter name. Filter names cannot begin '
                'with an underscore.' % attr_name
            )
        if not issubclass(filterimage_cls, FilteredImage):
            raise InvalidFilteredImageSubclass(
                'Only subclasses of FilteredImage may be registered as '
                'filters with VersatileImageFieldRegistry'
            )

        if attr_name in self._filter_registry:
            raise AlreadyRegistered(
                'A ProcessedImageMixIn class is already registered to the `%s`'
                ' attribute. If you would like to override this attribute, '
                'use the unregister method' % attr_name
            )
        else:
            self._filter_registry[attr_name] = filterimage_cls

    def unregister_filter(self, attr_name):
        """
        Unregister the FilteredImage subclass currently assigned to attr_name.

        If a FilteredImage subclass isn't already registered to filters.
        `attr_name` NotRegistered will raise.
        """
        if attr_name not in self._filter_registry:
            raise NotRegistered(
                'No FilteredImage subclass is registered to %s' % attr_name
            )
        else:
            del self._filter_registry[attr_name]

versatileimagefield_registry = VersatileImageFieldRegistry()

def autodiscover():
    """
    Discover versatileimagefield.py modules.

    Iterate over django.apps.get_app_configs() and discover
    versatileimagefield.py modules.
    """
    from importlib import import_module
    from django.apps import apps
    from django.utils.module_loading import module_has_submodule

    for app_config in apps.get_app_configs():
        # Attempt to import the app's module.

        try:
            before_import_sizedimage_registry = copy.copy(
                versatileimagefield_registry._sizedimage_registry
            )
            before_import_filter_registry = copy.copy(
                versatileimagefield_registry._filter_registry
            )
            import_module('%s.versatileimagefield' % app_config.name)
        except Exception:
            # Reset the versatileimagefield_registry to the state before the
            # last import as this import will have to reoccur on the next
            # request and this could raise NotRegistered and AlreadyRegistered
            # exceptions (see django ticket #8245).
            versatileimagefield_registry._sizedimage_registry = \
                before_import_sizedimage_registry
            versatileimagefield_registry._filter_registry = \
                before_import_filter_registry

            # Decide whether to bubble up this error. If the app just
            # doesn't have the module in question, we can ignore the error
            # attempting to import it, otherwise we want it to bubble up.
            if module_has_submodule(app_config.module, 'versatileimagefield'):
                raise


"""versatileimagefield Field mixins."""
from __future__ import unicode_literals

import os
import re

from django.utils.six import iteritems

from .datastructures import FilterLibrary
from .registry import autodiscover, versatileimagefield_registry
from .settings import (
    cache,
    VERSATILEIMAGEFIELD_CREATE_ON_DEMAND,
    VERSATILEIMAGEFIELD_SIZED_DIRNAME,
    VERSATILEIMAGEFIELD_FILTERED_DIRNAME
)
from .validators import validate_ppoi

autodiscover()

filter_regex_snippet = r'__({registered_filters})__'.format(
    registered_filters='|'.join([
        key
        for key, filter_cls in iteritems(
            versatileimagefield_registry._filter_registry
        )
    ])
)
sizer_regex_snippet = r'-({registered_sizers})-(\d+)x(\d+)(?:-\d+)?'.format(
    registered_sizers='|'.join([
        sizer_cls.get_filename_key_regex()
        for key, sizer_cls in iteritems(
            versatileimagefield_registry._sizedimage_registry
        )
    ])
)
filter_regex = re.compile(filter_regex_snippet + '$')
sizer_regex = re.compile(sizer_regex_snippet + '$')
filter_and_sizer_regex = re.compile(
    filter_regex_snippet + sizer_regex_snippet + '$'
)

class VersatileImageMixIn(object):
    """A mix-in that provides the filtering/sizing API."""

    def __init__(self, *args, **kwargs):
        """Construct PPOI and create_on_demand."""
        self._create_on_demand = VERSATILEIMAGEFIELD_CREATE_ON_DEMAND
        super(VersatileImageMixIn, self).__init__(*args, **kwargs)
        # Setting initial ppoi
        if self.field.ppoi_field:
            instance_ppoi_value = getattr(
                self.instance,
                self.field.ppoi_field,
                (0.5, 0.5)
            )
            self.ppoi = instance_ppoi_value
        else:
            self.ppoi = (0.5, 0.5)

    @property
    def url(self):
        """
        Return the appropriate URL.

        URL is constructed based on these field conditions:
            * If empty (not `self.name`) and a placeholder is defined, the
              URL to the placeholder is returned.
            * Otherwise, defaults to vanilla ImageFieldFile behavior.
        """
        if not self.name and self.field.placeholder_image_name:
            return self.storage.url(self.field.placeholder_image_name)

        return super(VersatileImageMixIn, self).url

    @property
    def create_on_demand(self):
        """create_on_demand getter."""
        return self._create_on_demand

    @create_on_demand.setter
    def create_on_demand(self, value):
        if not isinstance(value, bool):
            raise ValueError(
                "`create_on_demand` must be a boolean"
            )
        else:
            self._create_on_demand = value
            self.build_filters_and_sizers(self.ppoi, value)

    @property
    def ppoi(self):
        """Primary Point of Interest (ppoi) getter."""
        return self._ppoi_value

    @ppoi.setter
    def ppoi(self, value):
        """Primary Point of Interest (ppoi) setter."""
        ppoi = validate_ppoi(
            value,
            return_converted_tuple=True
        )
        if ppoi is not False:
            self._ppoi_value = ppoi
            self.build_filters_and_sizers(ppoi, self.create_on_demand)

    def build_filters_and_sizers(self, ppoi_value, create_on_demand):
        """Build the filters and sizers for a field."""
        name = self.name
        if not name and self.field.placeholder_image_name:
            name = self.field.placeholder_image_name
        self.filters = FilterLibrary(
            name,
            self.storage,
            versatileimagefield_registry,
            ppoi_value,
            create_on_demand
        )
        for (
            attr_name,
            sizedimage_cls
        ) in iteritems(versatileimagefield_registry._sizedimage_registry):
            setattr(
                self,
                attr_name,
                sizedimage_cls(
                    path_to_image=name,
                    storage=self.storage,
                    create_on_demand=create_on_demand,
                    ppoi=ppoi_value
                )
            )

    def get_filtered_root_folder(self):
        """Return the location where filtered images are stored."""
        folder, filename = os.path.split(self.name)
        return os.path.join(folder, VERSATILEIMAGEFIELD_FILTERED_DIRNAME, '')

    def get_sized_root_folder(self):
        """Return the location where sized images are stored."""
        folder, filename = os.path.split(self.name)
        return os.path.join(VERSATILEIMAGEFIELD_SIZED_DIRNAME, folder, '')

    def get_filtered_sized_root_folder(self):
        """Return the location where filtered + sized images are stored."""
        sized_root_folder = self.get_sized_root_folder()
        return os.path.join(
            sized_root_folder,
            VERSATILEIMAGEFIELD_FILTERED_DIRNAME
        )

    def delete_matching_files_from_storage(self, root_folder, regex):
        """
        Delete files in `root_folder` which match `regex` before file ext.

        Example values:
            * root_folder = 'foo/'
            * self.name = 'bar.jpg'
            * regex = re.compile('-baz')

            Result:
                * foo/bar-baz.jpg <- Deleted
                * foo/bar-biz.jpg <- Not deleted
        """
        if not self.name:   # pragma: no cover
            return
        try:
            directory_list, file_list = self.storage.listdir(root_folder)
        except OSError:   # pragma: no cover
            pass
        else:
            folder, filename = os.path.split(self.name)
            basename, ext = os.path.splitext(filename)
            for f in file_list:
                if not f.startswith(basename) or not f.endswith(ext):   # pragma: no cover
                    continue
                tag = f[len(basename):-len(ext)]
                assert f == basename + tag + ext
                if regex.match(tag) is not None:
                    file_location = os.path.join(root_folder, f)
                    self.storage.delete(file_location)
                    cache.delete(
                        self.storage.url(file_location)
                    )
                    print(
                        "Deleted {file} (created from: {original})".format(
                            file=os.path.join(root_folder, f),
                            original=self.name
                        )
                    )

    def delete_filtered_images(self):
        """Delete all filtered images created from `self.name`."""
        self.delete_matching_files_from_storage(
            self.get_filtered_root_folder(),
            filter_regex
        )

    def delete_sized_images(self):
        """Delete all sized images created from `self.name`."""
        self.delete_matching_files_from_storage(
            self.get_sized_root_folder(),
            sizer_regex
        )

    def delete_filtered_sized_images(self):
        """Delete all filtered sized images created from `self.name`."""
        self.delete_matching_files_from_storage(
            self.get_filtered_sized_root_folder(),
            filter_and_sizer_regex
        )

    def delete_all_created_images(self):
        """Delete all images created from `self.name`."""
        self.delete_filtered_images()
        self.delete_sized_images()
        self.delete_filtered_sized_images()


from __future__ import unicode_literals

from django.core.exceptions import ValidationError

INVALID_CENTERPOINT_ERROR_MESSAGE = (
    "%s is in invalid ppoi. A valid "
    "ppoi must provide two coordinates, one for the x axis and one "
    "for the y, where both values are between 0 and 1. You may pass it as "
    "either a two-position tuple like this: (0.5,0.5) or as a string where "
    "the two values are separated by an 'x' like this: '0.5x0.5'."
)

def validate_ppoi_tuple(value):
    """
    Validates that a tuple (`value`)...
    ...has a len of exactly 2
    ...both values are floats/ints that are greater-than-or-equal-to 0
       AND less-than-or-equal-to 1
    """
    valid = True
    while valid is True:
        if len(value) == 2 and isinstance(value, tuple):
            for x in value:
                if x >= 0 and x <= 1:
                    pass
                else:
                    valid = False
            break
        else:
            valid = False
    return valid

def validate_ppoi(value, return_converted_tuple=False):
    """
    Converts, validates and optionally returns a string with formatting:
    '%(x_axis)dx%(y_axis)d' into a two position tuple.

    If a tuple is passed to `value` it is also validated.

    Both x_axis and y_axis must be floats or ints greater
    than 0 and less than 1.
    """

    valid_ppoi = True
    to_return = None
    if isinstance(value, tuple):
        valid_ppoi = validate_ppoi_tuple(value)
        if valid_ppoi:
            to_return = value
    else:
        tup = tuple()
        try:
            string_split = [
                float(segment.strip())
                for segment in value.split('x')
                if float(segment.strip()) >= 0 and float(segment.strip()) <= 1
            ]
        except Exception:
            valid_ppoi = False
        else:
            tup = tuple(string_split)

        valid_ppoi = validate_ppoi_tuple(tup)

        if valid_ppoi:
            to_return = tup
    if not valid_ppoi:
        raise ValidationError(
            message=INVALID_CENTERPOINT_ERROR_MESSAGE % str(value),
            code='invalid_ppoi'
        )
    else:
        if to_return and return_converted_tuple is True:
            return to_return

__all__ = ['validate_ppoi_tuple', 'validate_ppoi']


"""Base datastructures for manipulated images."""
from __future__ import unicode_literals

from PIL import Image

from django.core.files.uploadedfile import InMemoryUploadedFile

from ..settings import QUAL, VERSATILEIMAGEFIELD_PROGRESSIVE_JPEG
from ..utils import get_image_metadata_from_file_ext

EXIF_ORIENTATION_KEY = 274

class ProcessedImage(object):
    """
    A base class for processing/saving different renditions of an image.

    Constructor arguments:
        * `path_to_image`: A path to a file within `storage`
        * `storage`: A django storage class
        * `create_on_demand`: A bool signifying whether new images should be
                              created on-demand.

    Subclasses must define the `process_image` method. see
    versatileimagefield.datastructures.filteredimage.FilteredImage and
    versatileimagefield.datastructures.sizedimage.SizedImage
    for examples.

    Includes a preprocessing API based on image format/file type. See
    the `preprocess` method for more specific information.
    """

    name = None
    url = None

    def __init__(self, path_to_image, storage, create_on_demand,
                 placeholder_image=None):
        """Construct a ProcessedImage."""
        self.path_to_image = path_to_image
        self.storage = storage
        self.create_on_demand = create_on_demand
        self.placeholder_image = placeholder_image

    def process_image(self, image, image_format, **kwargs):
        """
        Ensure NotImplemented is raised if not overloaded by subclasses.

        Arguments:
            * `image`: a PIL Image instance
            * `image_format`: str, a valid PIL format (i.e. 'JPEG' or 'GIF')

        Returns a BytesIO representation of the resized image.

        Subclasses MUST implement this method.
        """
        raise NotImplementedError(
            'Subclasses MUST provide a `process_image` method.'
        )

    def preprocess(self, image, image_format):
        """
        Preprocess an image.

        An API hook for image pre-processing. Calls any image format specific
        pre-processors (if defined). I.E. If `image_format` is 'JPEG', this
        method will look for a method named `preprocess_JPEG`, if found
        `image` will be passed to it.

        Arguments:
            * `image`: a PIL Image instance
            * `image_format`: str, a valid PIL format (i.e. 'JPEG' or 'GIF')

        Subclasses should return a 2-tuple:
            * [0]: A PIL Image instance.
            * [1]: A dictionary of additional keyword arguments to be used
                   when the instance is saved. If no additional keyword
                   arguments, return an empty dict ({}).
        """
        save_kwargs = {'format': image_format}

        # Ensuring image is properly rotated
        if hasattr(image, '_getexif'):
            exif_datadict = image._getexif()  # returns None if no EXIF data
            if exif_datadict is not None:
                exif = dict(exif_datadict.items())
                orientation = exif.get(EXIF_ORIENTATION_KEY, None)
                if orientation == 3:
                    image = image.transpose(Image.ROTATE_180)
                elif orientation == 6:
                    image = image.transpose(Image.ROTATE_270)
                elif orientation == 8:
                    image = image.transpose(Image.ROTATE_90)

        # Ensure any embedded ICC profile is preserved
        save_kwargs['icc_profile'] = image.info.get('icc_profile')

        if hasattr(self, 'preprocess_%s' % image_format):
            image, addl_save_kwargs = getattr(
                self,
                'preprocess_%s' % image_format
            )(image=image)
            save_kwargs.update(addl_save_kwargs)

        return image, save_kwargs

    def preprocess_GIF(self, image, **kwargs):
        """
        Receive a PIL Image instance of a GIF and return 2-tuple.

        Args:
            * [0]: Original Image instance (passed to `image`)
            * [1]: Dict with a transparency key (to GIF transparency layer)
        """
        if 'transparency' in image.info:
            save_kwargs = {'transparency': image.info['transparency']}
        else:
            save_kwargs = {}
        return (image, save_kwargs)

    def preprocess_JPEG(self, image, **kwargs):
        """
        Receive a PIL Image instance of a JPEG and returns 2-tuple.

        Args:
            * [0]: Image instance, converted to RGB
            * [1]: Dict with a quality key (mapped to the value of `QUAL` as
                   defined by the `VERSATILEIMAGEFIELD_JPEG_RESIZE_QUALITY`
                   setting)
        """
        save_kwargs = {
            'progressive': VERSATILEIMAGEFIELD_PROGRESSIVE_JPEG,
            'quality': QUAL
        }
        if image.mode != 'RGB':
            image = image.convert('RGB')
        return (image, save_kwargs)

    def retrieve_image(self, path_to_image):
        """Return a PIL Image instance stored at `path_to_image`."""
        image = self.storage.open(path_to_image, 'rb')
        file_ext = path_to_image.rsplit('.')[-1]
        image_format, mime_type = get_image_metadata_from_file_ext(file_ext)

        return (
            Image.open(image),
            file_ext,
            image_format,
            mime_type
        )

    def save_image(self, imagefile, save_path, file_ext, mime_type):
        """
        Save an image to self.storage at `save_path`.

        Arguments:
            `imagefile`: Raw image data, typically a BytesIO instance.
            `save_path`: The path within self.storage where the image should
                         be saved.
            `file_ext`: The file extension of the image-to-be-saved.
            `mime_type`: A valid image mime type (as found in
                         versatileimagefield.utils)
        """
        file_to_save = InMemoryUploadedFile(
            imagefile,
            None,
            'foo.%s' % file_ext,
            mime_type,
            imagefile.tell(),
            None
        )
        file_to_save.seek(0)
        self.storage.save(save_path, file_to_save)


"""Datastructures for sizing images."""
from __future__ import unicode_literals

from django.conf import settings
from django.utils.encoding import python_2_unicode_compatible
from ..settings import (
    cache,
    VERSATILEIMAGEFIELD_CACHE_LENGTH
)
from ..utils import get_resized_path
from .base import ProcessedImage
from .mixins import DeleteAndClearCacheMixIn

class MalformedSizedImageKey(Exception):
    """An Exception for improperly constructured sized image keys."""

    pass

@python_2_unicode_compatible
class SizedImageInstance(DeleteAndClearCacheMixIn):
    """A simple class for images created by SizedImage."""

    def __init__(self, name, url, storage):
        """Construct a SizedImageInstance."""
        self.name = name
        self.url = url
        self.storage = storage

    def __str__(self):
        """Return the string representation."""
        return self.url

class SizedImage(ProcessedImage, dict):
    """
    A dict subclass that exposes an image sizing API via key access.

    Subclasses must implement a `process_image` method.

    See versatileimagefield.versatileimagefield.CroppedImage and
    versatileimagefield.versatileimagefield.ThumbnailImage for subclass
    examples.
    """

    def __init__(self, path_to_image, storage, create_on_demand, ppoi=None):
        """Construct a SizedImage."""
        super(SizedImage, self).__init__(
            path_to_image, storage, create_on_demand
        )
        self.ppoi = ppoi
        try:
            key = self.get_filename_key()
        except AttributeError:
            raise NotImplementedError(
                'SizedImage subclasses must define a '
                '`filename_key` attribute or override the '
                '`get_filename_key` method.'
            )
        else:
            del key

    def ppoi_as_str(self):
        """Return PPOI value as a string."""
        return "%s__%s" % (
            str(self.ppoi[0]).replace('.', '-'),
            str(self.ppoi[1]).replace('.', '-')
        )

    def get_filename_key(self):
        """Return a string used to identify the resized image."""
        return self.filename_key

    @classmethod
    def get_filename_key_regex(cls):
        """Return the filename key regex."""
        try:
            return cls.filename_key_regex
        except AttributeError:
            try:
                return cls.filename_key
            except AttributeError:   # pragma: no cover
                raise NotImplementedError(
                    'SizedImage subclasses must define a '
                    '`filename_key_regex` attribute or a '
                    '`filename_key` attribute or override the '
                    '`get_filename_key_regex` class method.'
                )

    def __setitem__(self, key, value):
        """Ensure attribute assignment is disabled."""
        raise NotImplementedError(
            '%s instances do not allow key'
            ' assignment.' % self.__class__.__name__
        )

    def __getitem__(self, key):
        """
        Return a URL to an image sized according to key.

        Arguments:
            * `key`: A string in the following format
                     '[width-in-pixels]x[height-in-pixels]'
                     Example: '400x400'
        """
        try:
            width, height = [int(i) for i in key.split('x')]
        except (KeyError, ValueError):
            raise MalformedSizedImageKey(
                "%s keys must be in the following format: "
                "'`width`x`height`' where both `width` and `height` are "
                "integers." % self.__class__.__name__
            )

        if not self.path_to_image and getattr(
            settings, 'VERSATILEIMAGEFIELD_USE_PLACEHOLDIT', False
        ):
            resized_url = "http://placehold.it/%dx%d" % (width, height)
            resized_storage_path = resized_url
        else:
            resized_storage_path = get_resized_path(
                path_to_image=self.path_to_image,
                width=width,
                height=height,
                filename_key=self.get_filename_key(),
                storage=self.storage
            )

            try:
                resized_url = self.storage.url(resized_storage_path)
            except Exception:
                resized_url = None

            if self.create_on_demand is True:
                if cache.get(resized_url) and resized_url is not None:
                    # The sized path exists in the cache so the image already
                    # exists. So we `pass` to skip directly to the return
                    # statement
                    pass
                else:
                    if resized_storage_path and not self.storage.exists(
                        resized_storage_path
                    ):
                        self.create_resized_image(
                            path_to_image=self.path_to_image,
                            save_path_on_storage=resized_storage_path,
                            width=width,
                            height=height
                        )

                        resized_url = self.storage.url(resized_storage_path)

                    # Setting a super-long cache for a resized image (30 Days)
                    cache.set(resized_url, 1, VERSATILEIMAGEFIELD_CACHE_LENGTH)
        return SizedImageInstance(
            name=resized_storage_path,
            url=resized_url,
            storage=self.storage
        )

    def process_image(self, image, image_format, save_kwargs,
                      width, height):
        """
        Process a SizedImage.

        Arguments:
            * `image`: a PIL Image instance
            * `image_format`: A valid image mime type (e.g. 'image/jpeg')
            * `save_kwargs`: A dict of any keyword arguments needed during
                             save that are provided by the preprocessing API.
            * `width`: value in pixels (as int) representing the intended width
            * `height`: value in pixels (as int) representing the intended
                        height

        Returns a BytesIO representation of the resized image.

        Subclasses MUST implement this method.
        """
        raise NotImplementedError(
            'Subclasses MUST provide a `process_image` method.'
        )

    def create_resized_image(self, path_to_image, save_path_on_storage,
                             width, height):
        """
        Create a resized image.

        `path_to_image`: The path to the image with the media directory to
                         resize. If `None`, the
                         VERSATILEIMAGEFIELD_PLACEHOLDER_IMAGE will be used.
        `save_path_on_storage`: Where on self.storage to save the resized image
        `width`: Width of resized image (int)
        `height`: Desired height of resized image (int)
        `filename_key`: A string that will be used in the sized image filename
                        to signify what operation was done to it.
                        Examples: 'crop' or 'scale'
        """
        image, file_ext, image_format, mime_type = self.retrieve_image(
            path_to_image
        )

        image, save_kwargs = self.preprocess(image, image_format)

        imagefile = self.process_image(
            image=image,
            image_format=image_format,
            save_kwargs=save_kwargs,
            width=width,
            height=height
        )
        self.save_image(imagefile, save_path_on_storage, file_ext, mime_type)


from __future__ import unicode_literals

import django
from django.forms.widgets import ClearableFileInput, HiddenInput, MultiWidget, Select
from django.template.loader import render_to_string
from django.utils.safestring import mark_safe

CENTERPOINT_CHOICES = (
    ('0.0x0.0', 'Top Left'),
    ('0.0x0.5', 'Top Center'),
    ('0.0x1.0', 'Top Right'),
    ('0.5x0.0', 'Middle Left'),
    ('0.5x0.5', 'Middle Center'),
    ('0.5x1.0', 'Middle Right'),
    ('1.0x0.0', 'Bottom Left'),
    ('1.0x0.5', 'Bottom Center'),
    ('1.0x1.0', 'Bottom Right'),
)

class ClearableFileInputWithImagePreview(ClearableFileInput):

    has_template_widget_rendering = django.VERSION >= (1, 11)
    template_name = 'versatileimagefield/forms/widgets/versatile_image.html'

    def get_hidden_field_id(self, name):
        i = name.rindex('_')
        return "id_%s_%d" % (name[:i], int(name[i + 1:]) + 1)

    def image_preview_id(self, name):
        """Given the name of the image preview tag, return the HTML id for it."""
        return name + '_imagepreview'

    def get_ppoi_id(self, name):
        """Given the name of the primary point of interest tag, return the HTML id for it."""
        return name + '_ppoi'

    def get_point_stage_id(self, name):
        return name + '_point-stage'

    def render(self, name, value, attrs=None, renderer=None):
        """
        Render the widget as an HTML string.

        Overridden here to support Django < 1.11.
        """
        if self.has_template_widget_rendering:
            return super(ClearableFileInputWithImagePreview, self).render(
                name, value, attrs=attrs, renderer=renderer
            )
        else:
            context = self.get_context(name, value, attrs)
            return render_to_string(self.template_name, context)

    def get_sized_url(self, value):
        """Do not fail completely on invalid images"""
        try:
            # Ensuring admin preview thumbnails are created and available
            value.create_on_demand = True
            return value.thumbnail['300x300']
        except Exception:
            # Do not be overly specific with exceptions; we'd rather show no
            # thumbnail than crash when showing the widget.
            return None

    def get_context(self, name, value, attrs):
        """Get the context to render this widget with."""
        if self.has_template_widget_rendering:
            context = super(ClearableFileInputWithImagePreview, self).get_context(name, value, attrs)
        else:
            # Build the context manually.
            context = {}
            context['widget'] = {
                'name': name,
                'is_hidden': self.is_hidden,
                'required': self.is_required,
                'value': self._format_value(value),
                'attrs': self.build_attrs(self.attrs, attrs),
                'template_name': self.template_name,
                'type': self.input_type,
            }

        # It seems Django 1.11's ClearableFileInput doesn't add everything to the 'widget' key, so we can't use it
        # in MultiWidget. Add it manually here.
        checkbox_name = self.clear_checkbox_name(name)
        checkbox_id = self.clear_checkbox_id(checkbox_name)

        context['widget'].update({
            'checkbox_name': checkbox_name,
            'checkbox_id': checkbox_id,
            'is_initial': self.is_initial(value),
            'input_text': self.input_text,
            'initial_text': self.initial_text,
            'clear_checkbox_label': self.clear_checkbox_label,
        })

        if value and hasattr(value, "url"):
            context['widget'].update({
                'hidden_field_id': self.get_hidden_field_id(name),
                'point_stage_id': self.get_point_stage_id(name),
                'ppoi_id': self.get_ppoi_id(name),
                'sized_url': self.get_sized_url(value),
                'image_preview_id': self.image_preview_id(name),
            })

        return context

    def build_attrs(self, base_attrs, extra_attrs=None):
        """Build an attribute dictionary."""
        attrs = base_attrs.copy()
        if extra_attrs is not None:
            attrs.update(extra_attrs)
        return attrs

class SizedImageCenterpointWidgetMixIn(object):

    def decompress(self, value):
        return [value, 'x'.join(str(num) for num in value.ppoi)] if value else [None, None]

class VersatileImagePPOISelectWidget(SizedImageCenterpointWidgetMixIn, MultiWidget):

    def __init__(self, widgets=None, attrs=None):
        widgets = [
            ClearableFileInput(attrs=None),
            Select(attrs=attrs, choices=CENTERPOINT_CHOICES)
        ]
        super(VersatileImagePPOISelectWidget, self).__init__(widgets, attrs)

class VersatileImagePPOIClickWidget(SizedImageCenterpointWidgetMixIn, MultiWidget):

    def __init__(self, widgets=None, attrs=None, image_preview_template=None):
        widgets = (
            ClearableFileInputWithImagePreview(attrs={'class': 'file-chooser'}),
            HiddenInput(attrs={'class': 'ppoi-input'})
        )
        super(VersatileImagePPOIClickWidget, self).__init__(widgets, attrs)

    class Media:
        css = {
            'all': ('versatileimagefield/css/versatileimagefield.css',),
        }
        js = ('versatileimagefield/js/versatileimagefield.js',)

    def render(self, name, value, attrs=None, renderer=None):
        rendered = super(VersatileImagePPOIClickWidget, self).render(name, value, attrs=attrs)
        return mark_safe('
{}
'.format(rendered))

class SizedImageCenterpointClickDjangoAdminWidget(VersatileImagePPOIClickWidget):

    class Media:
        css = {
            'all': ('versatileimagefield/css/versatileimagefield-djangoadmin.css',),
        }

class Bootstrap3ClearableFileInputWithImagePreview(ClearableFileInputWithImagePreview):
    """A Bootstrap 3 version of the clearable file input with image preview."""

    template_name = 'versatileimagefield/forms/widgets/versatile_image_bootstrap.html'

class SizedImageCenterpointClickBootstrap3Widget(VersatileImagePPOIClickWidget):

    def __init__(self, widgets=None, attrs=None):
        widgets = (
            Bootstrap3ClearableFileInputWithImagePreview(attrs={'class': 'file-chooser'}),
            HiddenInput(attrs={'class': 'ppoi-input'})
        )
        super(VersatileImagePPOIClickWidget, self).__init__(widgets, attrs)

    class Media:
        css = {
            'all': ('versatileimagefield/css/versatileimagefield-bootstrap3.css',),
        }


from __future__ import unicode_literals

from functools import reduce

import os

from django.core.exceptions import ImproperlyConfigured

from .settings import (
    IMAGE_SETS,
    QUAL,
    VERSATILEIMAGEFIELD_POST_PROCESSOR,
    VERSATILEIMAGEFIELD_SIZED_DIRNAME,
    VERSATILEIMAGEFIELD_FILTERED_DIRNAME
)

# PIL-supported file formats as found here:
# https://infohost.nmt.edu/tcc/help/pubs/pil/formats.html
# (PIL Identifier, mime type)
BMP = ('BMP', 'image/bmp')
DCX = ('DCX', 'image/dcx')
EPS = ('eps', 'image/eps')
GIF = ('GIF', 'image/gif')
JPEG = ('JPEG', 'image/jpeg')
PCD = ('PCD', 'image/pcd')
PCX = ('PCX', 'image/pcx')
PDF = ('PDF', 'application/pdf')
PNG = ('PNG', 'image/png')
PPM = ('PPM', 'image/x-ppm')
PSD = ('PSD', 'image/psd')
TIFF = ('TIFF', 'image/tiff')
XBM = ('XBM', 'image/x-xbitmap')
XPM = ('XPM', 'image/x-xpm')

# Mapping file extensions to PIL types/mime types
FILE_EXTENSION_MAP = {
    'png': PNG,
    'jpe': JPEG,
    'jpeg': JPEG,
    'jpg': JPEG,
    'gif': GIF,
    'bmp': BMP,
    'dib': BMP,
    'dcx': DCX,
    'eps': EPS,
    'ps': EPS,
    'pcd': PCD,
    'pcx': PCX,
    'pdf': PDF,
    'pbm': PPM,
    'pgm': PPM,
    'ppm': PPM,
    'psd': PSD,
    'tif': TIFF,
    'tiff': TIFF,
    'xbm': XBM,
    'xpm': XPM
}

class InvalidSizeKeySet(Exception):
    pass

class InvalidSizeKey(Exception):
    pass

def post_process_image_key(image_key):
    """Apply the processor function associated with settings.VER"""
    if VERSATILEIMAGEFIELD_POST_PROCESSOR is None:
        return image_key
    else:
        return VERSATILEIMAGEFIELD_POST_PROCESSOR(image_key)

def get_resized_filename(filename, width, height, filename_key):
    """
    Return the 'resized filename' (according to `width`, `height` and
    `filename_key`) in the following format:
    `filename`-`filename_key`-`width`x`height`.ext
    """
    try:
        image_name, ext = filename.rsplit('.', 1)
    except ValueError:
        image_name = filename
        ext = 'jpg'

    resized_template = "%(filename_key)s-%(width)dx%(height)d"
    if ext.lower() in ['jpg', 'jpeg']:
        resized_template = resized_template + "-%(quality)d"

    resized_key = resized_template % ({
        'filename_key': filename_key,
        'width': width,
        'height': height,
        'quality': QUAL
    })

    return "%(image_name)s-%(image_key)s.%(ext)s" % ({
        'image_name': image_name,
        'image_key': post_process_image_key(resized_key),
        'ext': ext
    })

def get_resized_path(path_to_image, width, height,
                     filename_key, storage):
    """
    Return a `path_to_image` location on `storage` as dictated by `width`, `height`
    and `filename_key`
    """
    containing_folder, filename = os.path.split(path_to_image)

    resized_filename = get_resized_filename(
        filename,
        width,
        height,
        filename_key
    )

    joined_path = os.path.join(*[
        VERSATILEIMAGEFIELD_SIZED_DIRNAME,
        containing_folder,
        resized_filename
    ]).replace(' ', '')  # Removing spaces so this path is memcached friendly

    return joined_path

def get_filtered_filename(filename, filename_key):
    """
    Return the 'filtered filename' (according to `filename_key`)
    in the following format:
    `filename`__`filename_key`__.ext
    """
    try:
        image_name, ext = filename.rsplit('.', 1)
    except ValueError:
        image_name = filename
        ext = 'jpg'
    return "%(image_name)s__%(filename_key)s__.%(ext)s" % ({
        'image_name': image_name,
        'filename_key': filename_key,
        'ext': ext
    })

def get_filtered_path(path_to_image, filename_key, storage):
    """
    Return the 'filtered path'
    """
    containing_folder, filename = os.path.split(path_to_image)

    filtered_filename = get_filtered_filename(filename, filename_key)
    path_to_return = os.path.join(*[
        containing_folder,
        VERSATILEIMAGEFIELD_FILTERED_DIRNAME,
        filtered_filename
    ])
    # Removing spaces so this path is memcached key friendly
    path_to_return = path_to_return.replace(' ', '')
    return path_to_return

def get_image_metadata_from_file_ext(file_ext):
    """
    Receive a valid image file format and returns a 2-tuple of two strings:
        [0]: Image format (i.e. 'jpg', 'gif' or 'png')
        [1]: InMemoryUploadedFile-friendly save format (i.e. 'image/jpeg')
    image_format, in_memory_file_type
    """
    return FILE_EXTENSION_MAP.get(file_ext, JPEG)

def validate_versatileimagefield_sizekey_list(sizes):
    """
    Validate a list of size keys.

    `sizes`: An iterable of 2-tuples, both strings. Example:
    [
        ('large', 'url'),
        ('medium', 'crop__400x400'),
        ('small', 'thumbnail__100x100')
    ]
    """
    try:
        for key, size_key in sizes:
            size_key_split = size_key.split('__')
            if size_key_split[-1] != 'url' and (
                'x' not in size_key_split[-1]
            ):
                raise InvalidSizeKey(
                    "{0} is an invalid size. All sizes must be either "
                    "'url' or made up of at least two segments separated "
                    "by double underscores. Examples: 'crop__400x400', "
                    "filters__invert__url".format(size_key)
                )
    except ValueError:
        raise InvalidSizeKeySet(
            '{} is an invalid size key set. Size key sets must be an '
            'iterable of 2-tuples'.format(str(sizes))
        )
    return list(set(sizes))

def get_url_from_image_key(image_instance, image_key):
    """Build a URL from `image_key`."""
    img_key_split = image_key.split('__')
    if 'x' in img_key_split[-1]:
        size_key = img_key_split.pop(-1)
    else:
        size_key = None
    img_url = reduce(getattr, img_key_split, image_instance)
    if size_key:
        img_url = img_url[size_key].url
    return img_url

def build_versatileimagefield_url_set(image_instance, size_set, request=None):
    """
    Return a dictionary of urls corresponding to size_set
    - `image_instance`: A VersatileImageFieldFile
    - `size_set`: An iterable of 2-tuples, both strings. Example:
        [
            ('large', 'url'),
            ('medium', 'crop__400x400'),
            ('small', 'thumbnail__100x100')
        ]

        The above would lead to the following response:
        {
            'large': 'http://some.url/image.jpg',
            'medium': 'http://some.url/__sized__/image-crop-400x400.jpg',
            'small': 'http://some.url/__sized__/image-thumbnail-100x100.jpg',
        }
    - `request`:
    """
    size_set = validate_versatileimagefield_sizekey_list(size_set)
    to_return = {}
    if image_instance or image_instance.field.placeholder_image:
        for key, image_key in size_set:
            img_url = get_url_from_image_key(image_instance, image_key)
            if request is not None:
                img_url = request.build_absolute_uri(img_url)
            to_return[key] = img_url
    return to_return

def get_rendition_key_set(key):
    """
    Retrieve a validated and prepped Rendition Key Set from
    settings.VERSATILEIMAGEFIELD_RENDITION_KEY_SETS
    """
    try:
        rendition_key_set = IMAGE_SETS[key]
    except KeyError:
        raise ImproperlyConfigured(
            "No Rendition Key Set exists at "
            "settings.VERSATILEIMAGEFIELD_RENDITION_KEY_SETS['{}']".format(key)
        )
    else:
        return validate_versatileimagefield_sizekey_list(rendition_key_set)


"""Defines functions converting raw instructions into textual form."""
from __future__ import print_function, absolute_import, division, unicode_literals

import itertools

from .opcodes import INSN_LEAVE_BLOCK, INSN_ENTER_BLOCK
from .decode import decode_bytecode
from .wasmtypes import VAL_TYPE_I32, VAL_TYPE_I64, VAL_TYPE_F32, VAL_TYPE_F64

def format_instruction(insn):
    """
    Takes a raw `Instruction` and translates it into a human readable text
    representation. As of writing, the text representation for WASM is not yet
    standardized, so we just emit some generic format.
    """
    text = insn.op.mnemonic

    if not insn.imm:
        return text

    return text + ' ' + ', '.join([
        getattr(insn.op.imm_struct, x.name).to_string(
            getattr(insn.imm, x.name)
        )
        for x in insn.op.imm_struct._meta.fields
    ])

_lang_type_str_mapping = {
    VAL_TYPE_I32: 'i32',
    VAL_TYPE_I64: 'i64',
    VAL_TYPE_F32: 'f32',
    VAL_TYPE_F64: 'f64',
}

def format_lang_type(lang_type):
    """Takes a value type `int`, returning its string representation."""
    try:
        return _lang_type_str_mapping[lang_type]
    except KeyError:
        raise ValueError('Bad value for value type ({})'.format(lang_type))

def format_function(
    func_body,
    func_type=None,
    indent=2,
    format_locals=True,
):
    """
    Takes a `FunctionBody` and optionally a `FunctionType`, yielding the string
    representation of the function line by line. The function type is required
    for formatting function parameter and return value information.
    """
    if func_type is None:
        yield 'func'
    else:
        param_section = ' (param {})'.format(' '.join(
            map(format_lang_type, func_type.param_types)
        )) if func_type.param_types else ''
        result_section = ' (result {})'.format(
            format_lang_type(func_type.return_type)
        ) if func_type.return_type else ''
        yield 'func' + param_section + result_section

    if format_locals and func_body.locals:
        yield '(locals {})'.format(' '.join(itertools.chain.from_iterable(
            itertools.repeat(format_lang_type(x.type), x.count)
            for x in func_body.locals
        )))

    level = 1
    for cur_insn in decode_bytecode(func_body.code):
        if cur_insn.op.flags & INSN_LEAVE_BLOCK:
            level -= 1
        yield ' ' * (level * indent) + format_instruction(cur_insn)
        if cur_insn.op.flags & INSN_ENTER_BLOCK:
            level += 1


"""Provides functions for decoding WASM modules and bytecode."""
from __future__ import print_function, absolute_import, division, unicode_literals

from collections import namedtuple
from .modtypes import ModuleHeader, Section, SEC_UNK, SEC_NAME, NameSubSection
from .opcodes import OPCODE_MAP
from .compat import byte2int

Instruction = namedtuple('Instruction', 'op imm len')
ModuleFragment = namedtuple('ModuleFragment', 'type data')

def decode_bytecode(bytecode):
    """Decodes raw bytecode, yielding `Instruction`s."""
    bytecode_wnd = memoryview(bytecode)
    while bytecode_wnd:
        opcode_id = byte2int(bytecode_wnd[0])
        opcode = OPCODE_MAP[opcode_id]

        if opcode.imm_struct is not None:
            offs, imm, _ = opcode.imm_struct.from_raw(None, bytecode_wnd[1:])
        else:
            imm = None
            offs = 0

        insn_len = 1 + offs
        yield Instruction(opcode, imm, insn_len)
        bytecode_wnd = bytecode_wnd[insn_len:]

def decode_module(module, decode_name_subsections=False):
    """Decodes raw WASM modules, yielding `ModuleFragment`s."""
    module_wnd = memoryview(module)

    # Read & yield module header.
    hdr = ModuleHeader()
    hdr_len, hdr_data, _ = hdr.from_raw(None, module_wnd)
    yield ModuleFragment(hdr, hdr_data)
    module_wnd = module_wnd[hdr_len:]

    # Read & yield sections.
    while module_wnd:
        sec = Section()
        sec_len, sec_data, _ = sec.from_raw(None, module_wnd)

        # If requested, decode name subsections when encountered.
        if (
            decode_name_subsections and
            sec_data.id == SEC_UNK and
            sec_data.name == SEC_NAME
        ):
            sec_wnd = sec_data.payload
            while sec_wnd:
                subsec = NameSubSection()
                subsec_len, subsec_data, _ = subsec.from_raw(None, sec_wnd)
                yield ModuleFragment(subsec, subsec_data)
                sec_wnd = sec_wnd[subsec_len:]
        else:
            yield ModuleFragment(sec, sec_data)

        module_wnd = module_wnd[sec_len:]


"""Defines compatibility quirks for Python 2.7."""
from __future__ import print_function, absolute_import, division, unicode_literals

import sys
import functools
import logging
import warnings

def add_metaclass(metaclass):
    """
    Class decorator for creating a class with a metaclass.
    Borrowed from `six` module.
    """
    @functools.wraps(metaclass)
    def wrapper(cls):
        orig_vars = cls.__dict__.copy()
        slots = orig_vars.get('__slots__')
        if slots is not None:
            if isinstance(slots, str):
                slots = [slots]
            for slots_var in slots:
                orig_vars.pop(slots_var)
        orig_vars.pop('__dict__', None)
        orig_vars.pop('__weakref__', None)
        return metaclass(cls.__name__, cls.__bases__, orig_vars)
    return wrapper

def indent(text, prefix, predicate=None):
    """Adds 'prefix' to the beginning of selected lines in 'text'.

    If 'predicate' is provided, 'prefix' will only be added to the lines
    where 'predicate(line)' is True. If 'predicate' is not provided,
    it will default to adding 'prefix' to all non-empty lines that do not
    consist solely of whitespace characters.

    Borrowed from Py3 `textwrap` module.
    """
    if predicate is None:
        def predicate(line):
            return line.strip()

    def prefixed_lines():
        for line in text.splitlines(True):
            yield (prefix + line if predicate(line) else line)
    return ''.join(prefixed_lines())

def deprecated_func(func):
    """Deprecates a function, printing a warning on the first usage."""

    # We use a mutable container here to work around Py2's lack of
    # the `nonlocal` keyword.
    first_usage = [True]

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if first_usage[0]:
            warnings.warn(
                "Call to deprecated function {}.".format(func.__name__),
                DeprecationWarning,
            )
            first_usage[0] = False
        return func(*args, **kwargs)

    return wrapper

if sys.version_info[0] >= 3:
    def byte2int(x):
        return x

elif sys.version_info[0] == 2:
    def byte2int(x):
        return ord(x) if type(x) == str else x

else:
    raise Exception("Unsupported Python version")


import asyncio
import logging
from collections import defaultdict
from collections import deque
import re
import fnmatch
from .ami_protocol import AMIProtocol
from . import actions
from . import utils

class Manager:
    """Main object:

    .. code-block:: python

        >>> manager = Manager(
        ...    host='127.0.0.1',
        ...    port=5038,
        ...    ssl=False,
        ...    encoding='utf8')
    """

    defaults = dict(
        host='127.0.0.1',
        port=5038,
        events='on',
        ssl=False,
        encoding='utf8',
        ping_delay=10,
        protocol_factory=AMIProtocol,
        save_stream=None,
        loop=None,
        forgetable_actions=('ping', 'login'),
    )

    def __init__(self, **config):
        self.config = dict(self.defaults, **config)
        self.loop = self.config['loop']
        self.log = config.get('log', logging.getLogger(__name__))
        self.callbacks = defaultdict(list)
        self.protocol = None
        self.patterns = []
        self.save_stream = self.config.get('save_stream')
        self.authenticated = False
        self.authenticated_future = None
        self.awaiting_actions = deque()
        self.forgetable_actions = self.config['forgetable_actions']
        self.pinger = None
        self.ping_delay = int(self.config['ping_delay'])
        self._connected = False
        self.register_event('FullyBooted', self.send_awaiting_actions)

    def connection_made(self, f):
        if getattr(self, 'protocol', None):
            self.protocol.close()
        try:
            transport, protocol = f.result()
        except OSError:  # pragma: no cover
            if self._connected:
                self.log.exception('Not able to connect')
                self._connected = False
            else:
                self.log.warning('Not able to reconnect')
            self.loop.call_later(2, self.connect)
        else:
            self._connected = True
            self.log.debug('Manager connected')
            self.protocol = protocol
            self.protocol.queue = deque()
            self.protocol.factory = self
            self.protocol.log = self.log
            self.protocol.config = self.config
            self.protocol.encoding = self.encoding = self.config['encoding']
            self.responses = self.protocol.responses = {}
            if 'username' in self.config:
                self.authenticated = False
                self.authenticated_future = self.send_action({
                    'Action': 'Login',
                    'Username': self.config['username'],
                    'Secret': self.config['secret'],
                    'Events': self.config['events']})
                self.authenticated_future.add_done_callback(self.login)
            else:
                self.log.debug('username not in config file')
            self.pinger = self.loop.call_later(self.ping_delay, self.ping)

    def login(self, future):
        self.authenticated_future = None
        resp = future.result()
        self.authenticated = bool(resp.success)
        if self.pinger is not None:
            self.pinger.cancel()
        self.pinger = self.loop.call_later(self.ping_delay, self.ping)
        return self.authenticated

    def ping(self):  # pragma: no cover
        self.pinger = self.loop.call_later(self.ping_delay, self.ping)
        self.protocol.send({'Action': 'Ping'})

    @asyncio.coroutine
    def send_awaiting_actions(self, *_):
        self.log.info('Sending awaiting actions')
        while self.awaiting_actions:
            action = self.awaiting_actions.popleft()
            if action['action'].lower() not in self.forgetable_actions:
                if not action.future.done():
                    self.send_action(action, as_list=action.as_list)

    def send_action(self, action, as_list=None, **kwargs):
        """Send an :class:`~panoramisk.actions.Action` to the server:

        :param action: an Action or dict with action name and parameters to
                       send
        :type action: Action or dict or Command
        :param as_list: If True, the action Future will retrieve all responses
        :type as_list: boolean
        :return: a Future that will receive the response
        :rtype: asyncio.Future

        :Example:

        To retrieve answer in a coroutine::

            manager = Manager()
            resp = yield from manager.send_action({'Action': 'Status'})

        With a callback::

            manager = Manager()
            future = manager.send_action({'Action': 'Status'})
            future.add_done_callback(handle_status_response)

        See https://wiki.asterisk.org/wiki/display/AST/AMI+Actions for
        more information on actions
        """
        action.update(kwargs)
        return self.protocol.send(action, as_list=as_list)

    def send_command(self, command, as_list=False):
        """Send a :class:`~panoramisk.actions.Command` to the server::

            manager = Manager()
            resp = manager.send_command('http show status')

        Return a response :class:`~panoramisk.message.Message`.
        See https://wiki.asterisk.org/wiki/display/AST/ManagerAction_Command
        """
        action = actions.Action({'Command': command, 'Action': 'Command'},
                                as_list=as_list)
        return self.send_action(action)

    def send_agi_command(self, channel, command, as_list=False):
        """Send a :class:`~panoramisk.actions.Command` to the server:

        :param channel: Channel name where to launch command.
               Ex: 'SIP/000000-00000a53'
        :type channel: String
        :param command: command to launch. Ex: 'GET VARIABLE async_agi_server'
        :type command: String
        :param as_list: If True, the action Future will retrieve all responses
        :type as_list: boolean
        :return: a Future that will receive the response
        :rtype: asyncio.Future

        :Example:

        ::

            manager = Manager()
            resp = manager.send_agi_command('SIP/000000-00000a53',
                                            'GET VARIABLE async_agi_server')

        Return a response :class:`~panoramisk.message.Message`.
        See https://wiki.asterisk.org/wiki/display/AST/Asterisk+11+ManagerAction_AGI
        """
        action = actions.Command({'Action': 'AGI',
                                  'Channel': channel,
                                  'Command': command},
                                 as_list=as_list)
        return self.send_action(action)

    def connect(self):
        """connect to the server"""
        if self.loop is None:  # pragma: no cover
            self.loop = asyncio.get_event_loop()
        t = asyncio.Task(
            self.loop.create_connection(
                self.config['protocol_factory'],
                self.config['host'], self.config['port'],
                ssl=self.config['ssl']),
            loop=self.loop)
        t.add_done_callback(self.connection_made)
        return t

    def register_event(self, pattern, callback=None):
        """register an event. See :class:`~panoramisk.message.Message`:

        .. code-block:: python

            >>> def callback(manager, event):
            ...     print(manager, event)
            >>> manager = Manager()
            >>> manager.register_event('Meetme*', callback)
            <function callback at 0x...>

        You can also use the manager as a decorator:

        .. code-block:: python

            >>> manager = Manager()
            >>> @manager.register_event('Meetme*')
            ... def callback(manager, event):
            ...     print(manager, event)
        """
        def _register_event(callback):
            if not self.callbacks[pattern]:
                self.patterns.append((pattern,
                                     re.compile(fnmatch.translate(pattern))))
            self.callbacks[pattern].append(callback)
            return callback
        if callback is not None:
            return _register_event(callback)
        else:
            return _register_event

    def dispatch(self, event):
        matches = []
        event.manager = self
        for pattern, regexp in self.patterns:
            match = regexp.match(event.event)
            if match is not None:
                matches.append(pattern)
                for callback in self.callbacks[pattern]:
                    ret = callback(self, event)
                    if (asyncio.iscoroutine(ret) or
                            isinstance(ret, asyncio.Future)):
                        asyncio.ensure_future(ret, loop=self.loop)
        return matches

    def close(self):
        """Close the connection"""
        if self.pinger:
            self.pinger.cancel()
            self.pinger = None
        if getattr(self, 'protocol', None):
            self.protocol.close()

    def connection_lost(self, exc):
        self._connected = False
        self.log.error('Connection lost')
        if self.pinger:
            self.pinger.cancel()
            self.pinger = None
        self.log.info('Try to connect again in 2 seconds')
        self.loop.call_later(2, self.connect)

    @classmethod
    def from_config(cls, filename_or_fd, section='asterisk', **kwargs):
        config = utils.config(filename_or_fd, section=section)
        config.update(kwargs)
        return cls(**config)


import logging
import asyncio
from collections import OrderedDict
from .utils import parse_agi_result

log = logging.getLogger(__name__)

class Request:
    def __init__(self, app, headers, reader, writer, encoding='utf-8'):
        self.app = app
        self.headers = headers
        self.reader = reader
        self.writer = writer
        self.encoding = encoding

    @asyncio.coroutine
    def send_command(self, command):
        """Send a command for FastAGI request:

        :param command: Command to launch on FastAGI request. Ex: 'EXEC StartMusicOnHolds'
        :type command: String

        :Example:

        ::

            @asyncio.coroutine
            def call_waiting(request):
                print(['AGI variables:', request.headers])
                yield from request.send_command('ANSWER')
                yield from request.send_command('EXEC StartMusicOnHold')
                yield from request.send_command('EXEC Wait 10')

        """
        command += '\n'
        self.writer.write(command.encode(self.encoding))
        yield from self.writer.drain()

        agi_result = yield from self._read_result()
        # If Asterisk returns `100 Trying...`, wait for next the response.
        while agi_result.get('status_code') == 100:
            agi_result = yield from self._read_result()

        # when we got AGIUsageError the following line contains some indication
        if 'error' in agi_result and agi_result['error'] == 'AGIUsageError':
            buff_usage_error = yield from self.reader.readline()
            agi_result['msg'] += buff_usage_error.decode(self.encoding)

        return agi_result

    @asyncio.coroutine
    def _read_result(self):
        """Parse read a response from the AGI and parse it.

        :return dict: The AGI response parsed into a dict.
        """
        response = yield from self.reader.readline()
        return parse_agi_result(response.decode(self.encoding)[:-1])

class Application(dict):
    """Main object:

    .. code-block:: python

        >>> fa_app = Application()
    """

    buf_size = 100

    def __init__(self, default_encoding='utf-8', loop=None):
        super(Application, self).__init__()
        self.default_encoding = default_encoding
        if loop is None:
            loop = asyncio.get_event_loop()
        self.loop = loop
        self._route = OrderedDict()

    def add_route(self, path, endpoint):
        """Add a route for FastAGI requests:

        :param path: URI to answer. Ex: 'calls/start'
        :type path: String
        :param endpoint: command to launch. Ex: start
        :type endpoint: callable

        :Example:

        ::

            @asyncio.coroutine
            def start(request):
                print('Receive a FastAGI request')
                print(['AGI variables:', request.headers])

            fa_app = Application()
            fa_app.add_route('calls/start', start)

        """
        assert callable(endpoint), endpoint
        if path in self._route:
            raise ValueError('A route already exists.')
        if not asyncio.iscoroutinefunction(endpoint):
            endpoint = asyncio.coroutine(endpoint)
        self._route[path] = endpoint

    def del_route(self, path):
        """Delete a route for FastAGI requests:

        :param path: URI to answer. Ex: 'calls/start'
        :type path: String

        :Example:

        ::

            @asyncio.coroutine
            def start(request):
                print('Receive a FastAGI request')
                print(['AGI variables:', request.headers])

            fa_app = Application()
            fa_app.add_route('calls/start', start)
            fa_app.del_route('calls/start')

        """
        if path not in self._route:
            raise ValueError('This route doesn\'t exist.')
        del(self._route[path])

    @asyncio.coroutine
    def handler(self, reader, writer):
        """AsyncIO coroutine handler to launch socket listening.

        :Example:

        ::

            @asyncio.coroutine
            def start(request):
                print('Receive a FastAGI request')
                print(['AGI variables:', request.headers])

            fa_app = Application()
            fa_app.add_route('calls/start', start)
            coro = asyncio.start_server(fa_app.handler, '0.0.0.0', 4574)
            server = loop.run_until_complete(coro)

        See https://docs.python.org/3/library/asyncio-stream.html
        """
        buffer = b''
        while b'\n\n' not in buffer:
            buffer += yield from reader.read(self.buf_size)
        lines = buffer[:-2].decode(self.default_encoding).split('\n')
        headers = OrderedDict([
            line.split(': ', 1) for line in lines if ': ' in line
        ])

        agi_network_script = headers.get('agi_network_script')
        log.info('Received FastAGI request from %r for "%s" route',
                 writer.get_extra_info('peername'), agi_network_script)
        log.debug("Asterisk Headers: %r", headers)

        if agi_network_script is not None:
            route = self._route.get(agi_network_script)
            if route is not None:
                request = Request(app=self,
                                  headers=headers,
                                  reader=reader, writer=writer,
                                  encoding=self.default_encoding)
                try:
                    yield from route(request)
                except BaseException:
                    log.exception(
                        'An exception has been raised for the request "%s"',
                        agi_network_script
                    )
            else:
                log.error('No route for the request "%s"', agi_network_script)
        else:
            log.error('No agi_network_script header for the request')
        log.debug("Closing client socket")
        writer.close()


import re
import uuid

try:
    from collections.abc import MutableMapping
except ImportError:
    from collections import MutableMapping

from configparser import ConfigParser

EOL = '\r\n'

re_code = re.compile(r'(?P<code>^\d*)\s*(?P<response>.*)')
re_kv = re.compile(r'(?P<key>\w+)=(?P<value>[^\s]+)\s*(?:\((?P<data>.*)\))*')

def parse_agi_result(line):
    """Parse AGI results using Regular expression.

    AGI Result examples::

        100 result=0 Trying...

        200 result=0

        200 result=-1

        200 result=132456

        200 result= (timeout)

        510 Invalid or unknown command

        520-Invalid command syntax. Proper usage follows:
        int() argument must be a string, a bytes-like object or a number, not
        'NoneType'

        HANGUP

    """
    # print("--------------\n", line)
    if line == 'HANGUP':
        return {'error': 'AGIResultHangup',
                'msg': 'User hungup during execution'}

    kwargs = dict(code=0, response="", line=line)
    m = re_code.search(line)
    try:
        kwargs.update(m.groupdict())
    except AttributeError:
        # None has no attribute groupdict
        pass
    return agi_code_check(**kwargs)

def agi_code_check(code=None, response=None, line=None):
    """
    Check the AGI code and return a dict to help on error handling.
    """
    code = int(code)
    response = response or ""
    result = {'status_code': code, 'result': ('', ''), 'msg': ''}
    if code == 100:
        result['msg'] = line
    elif code == 200:
        for key, value, data in re_kv.findall(response):
            result[key] = (value, data)
            # If user hangs up... we get 'hangup' in the data
            if data == 'hangup':
                return {
                    'error': 'AGIResultHangup',
                    'msg': 'User hungup during execution'}
            elif key == 'result' and value == '-1':
                return {
                    'error': 'AGIAppError',
                    'msg': 'Error executing application, or hangup'}
    elif code == 510:
        result['error'] = 'AGIInvalidCommand'
    elif code == 520:
        # AGI Usage error
        result['error'] = 'AGIUsageError'
        result['msg'] = line
    else:
        # Unhandled code or undefined response
        result['error'] = 'AGIUnknownError'
        result['msg'] = line
    return result

class IdGenerator:
    """Generate some uuid for actions:

    .. code-block:: python

        >>> g = IdGenerator('mycounter')

    ..
        >>> IdGenerator.reset(uid='an_uuid4')

    It increments the counter at each calls:

    .. code-block:: python

        >>> print(g())
        mycounter/an_uuid4/1/1
        >>> print(g())
        mycounter/an_uuid4/1/2
    """

    instances = []

    def __init__(self, prefix):
        self.instances.append(self)
        self.prefix = prefix
        self.uid = str(uuid.uuid4())
        self.generator = self.get_generator()

    def get_generator(self):
        i = 0
        max_val = 10000
        while True:
            yield "%s/%s/%d/%d" % (self.prefix,
                                   self.uid, (i // max_val) + 1,
                                   (i % max_val) + 1)
            i += 1

    @classmethod
    def reset(cls, uid=None):
        """Mostly used for unit testing. Allow to use a static uuid and reset
        all counter"""
        for instance in cls.instances:
            if uid:
                instance.uid = uid
            instance.generator = instance.get_generator()

    def get_instances(self):
        """Mostly used for debugging"""
        return ["<%s prefix:%s (uid:%s)>" % (self.__class__.__name__,
                                             i.prefix, self.uid)
                for i in self.instances]

    def __call__(self):
        return next(self.generator)

    def __repr__(self):
        return "<%s prefix:%s (uid:%s)>" % (self.__class__.__name__, self.prefix, self.uid)

class CaseInsensitiveDict(MutableMapping):
    """
    A case-insensitive ``dict``-like object.

    Implements all methods and operations of ``collections.MutableMapping``.

    All keys are expected to be strings. The structure remembers the
    case of the last key to be set, and ``iter(instance)``,
    ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
    will contain case-sensitive keys. However, querying and contains
    testing is case insensitive:

    .. code-block:: python

        cid = CaseInsensitiveDict()
        cid['Action'] = 'SIPnotify'
        cid['aCTION'] == 'SIPnotify'  # True
        list(cid) == ['Action']  # True

    For example, ``event['actionid']`` will return the
    value of a ``'ActionID'`` response event, regardless
    of how the event name was originally stored.
    """
    def __init__(self, data=None, **kwargs):
        self._store = dict()
        self.update(data or {}, **kwargs)

    def __setitem__(self, key, value):
        # Use the lowercased key for lookups, but store the actual
        # key alongside the value.
        self._store[key.lower()] = (key, value)

    def __contains__(self, key):
        return key.lower() in self._store

    def __getattr__(self, attr):
        return self.get(attr, '')

    def __getitem__(self, key):
        return self._store[key.lower()][1]

    def __delitem__(self, key):
        raise NotImplementedError()

    def __iter__(self):
        return (key for key, value in self._store.values())

    def __len__(self):
        return len(self._store)

    def __repr__(self):
        return str(dict(self.items()))

def config(filename_or_fd, section='asterisk'):
    config = ConfigParser()
    if hasattr(filename_or_fd, 'read'):
        if hasattr(config, 'read_file'):
            config.read_file(filename_or_fd)
        else:
            config.readfp(filename_or_fd)
    else:
        config.read(filename_or_fd)
    return dict(config.items(section))


from . import utils
from urllib.parse import unquote

class Message(utils.CaseInsensitiveDict):
    """Handle both Responses and Events with the same api:

    ..
        >>> resp = Message({'Response': 'Follows'}, 'Response body')
        >>> event = Message({'Event': 'MeetmeEnd', 'Meetme': '4242'})

    Responses:

    .. code-block:: python

        >>> bool(resp.success)
        True
        >>> resp
        <Message Response='Follows' content='Response body'>
        >>> print(resp.content)
        Response body
        >>> for line in resp.iter_lines():
        ...     print(resp.content)
        Response body

    Events:

    .. code-block:: python

        >>> print(event['meetme'])
        4242
        >>> print(event.meetme)
        4242
        >>> event.unknown_header
        ''

    """

    quoted_keys = ['result']
    success_responses = ['Success', 'Follows', 'Goodbye']

    def __init__(self, headers, content=''):
        super(Message, self).__init__(headers, content=content)
        self.manager = None

    @property
    def id(self):
        if 'commandid' in self:
            return self['commandid']
        elif 'actionid' in self:
            return self['actionid']
        return None

    @property
    def action_id(self):
        if 'actionid' in self:
            return self['actionid']
        return None

    @property
    def success(self):
        """return True if a response status is Success or Follows:

        .. code-block:: python

            >>> resp = Message({'Response': 'Success'})
            >>> print(resp.success)
            True
            >>> resp['Response'] = 'Failed'
            >>> resp.success
            False
        """
        if 'event' in self:
            return True
        if self.response in self.success_responses:
            return True
        return False

    def __repr__(self):
        message = ' '.join(['%s=%r' % i for i in sorted(self.items())])
        return '<Message {0}>'.format(message)

    def iter_lines(self):
        """Iter over response body"""
        for line in self.content.split('\n'):
            yield line

    def parsed_result(self):
        """Get parsed result of AGI command"""
        if 'Result' in self:
            return utils.parse_agi_result(self['Result'])
        else:
            raise ValueError('No result in %r' % self)

    def getdict(self, key):
        """Convert a multi values header to a case-insensitive dict:

        .. code-block:: python

            >>> resp = Message({
            ...     'Response': 'Success',
            ...     'ChanVariable': [
            ...         'FROM_DID=', 'SIPURI=sip:42@10.10.10.1:4242'],
            ... })
            >>> print(resp.chanvariable)
            ['FROM_DID=', 'SIPURI=sip:42@10.10.10.1:4242']
            >>> value = resp.getdict('chanvariable')
            >>> print(value['sipuri'])
            sip:42@10.10.10.1:4242
        """
        values = self.get(key, None)
        if not isinstance(values, list):
            raise TypeError("{0} must be a list. got {1}".format(key, values))
        result = utils.CaseInsensitiveDict()
        for item in values:
            k, v = item.split('=', 1)
            result[k] = v
        return result

    @classmethod
    def from_line(cls, line):
        mlines = line.split(utils.EOL)
        headers = {}
        content = ''
        has_body = ('Response: Follows', 'Response: Fail')
        if mlines[0].startswith(has_body):
            content = mlines.pop()
            while not content and mlines:
                content = mlines.pop()
        for mline in mlines:
            if ': ' in mline:
                k, v = mline.split(': ', 1)
                if k.lower() in cls.quoted_keys:
                    v = unquote(v).strip()
                if k in headers:
                    o = headers.setdefault(k, [])
                    if not isinstance(o, list):
                        o = [o]
                    o.append(v)
                    headers[k] = o
                else:
                    headers[k] = v
        if 'Event' in headers or 'Response' in headers:
            return cls(headers, content)


# Extracts information from a project that has a distutils setup.py file.
import os
import sys
import logging

from copy import copy
from distutils import core

class FakeContext(object):

    def __init__(self, path):
        self._path = path

    def __enter__(self):
        self._old_path = os.path.abspath(os.curdir)
        if self._old_path in sys.path:
            sys.path.remove(self._old_path)
        os.chdir(self._path)

        if self._path not in sys.path:
            sys.path.insert(0, self._path)
            self._path_appended = True
        else:
            self._path_appended = False

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self._path_appended:
            sys.path.remove(self._path)
        sys.path.append(self._old_path)

        os.chdir(self._old_path)

class SetupMonkey(object):

    used_setuptools = False

    def distutils_setup_replacement(self, **kw):
        self._distutils_setup(**kw)

    def setuptools_setup_replacement(self, **kw):
        self.used_setuptools = True
        self._setuptools_setup(**kw)

    def get_data(self):
        return self._kw

    def __enter__(self):
        import distutils.core
        self._distutils_setup = distutils.core.setup
        distutils.core.setup = self.distutils_setup_replacement

        try:
            import setuptools
            self._setuptools_setup = setuptools.setup
            setuptools.setup = self.setuptools_setup_replacement
        except ImportError:
            self._setuptools_setup = None

        self._kw = {}
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        import distutils.core
        distutils.core.setup = self._distutils_setup
        if self._setuptools_setup is not None:
            import setuptools
            setuptools.setup = self._setuptools_setup

# This is a version of distutils run_setup() that doesn't give
# up just because Setuptools throws errors if you try to exec it.
def run_setup(script_name, script_args=None, stop_after="run"):
    """Run a setup script in a somewhat controlled environment, and
    return the Distribution instance that drives things.  This is useful
    if you need to find out the distribution meta-data (passed as
    keyword args from 'script' to 'setup()', or the contents of the
    config files or command-line.

    'script_name' is a file that will be run with 'execfile()';
    'sys.argv[0]' will be replaced with 'script' for the duration of the
    call.  'script_args' is a list of strings; if supplied,
    'sys.argv[1:]' will be replaced by 'script_args' for the duration of
    the call.

    'stop_after' tells 'setup()' when to stop processing; possible
    values:
      init
        stop after the Distribution instance has been created and
        populated with the keyword arguments to 'setup()'
      config
        stop after config files have been parsed (and their data
        stored in the Distribution instance)
      commandline
        stop after the command-line ('sys.argv[1:]' or 'script_args')
        have been parsed (and the data stored in the Distribution)
      run [default]
        stop after all commands have been run (the same as if 'setup()'
        had been called in the usual way

    Returns the Distribution instance, which provides all information
    used to drive the Distutils.
    """
    if stop_after not in ('init', 'config', 'commandline', 'run'):
        raise ValueError("invalid value for 'stop_after': %r" % stop_after)

    core._setup_stop_after = stop_after

    save_argv = sys.argv
    glocals = copy(globals())
    glocals['__file__'] = script_name
    glocals['__name__'] = "__main__"
    try:
        try:
            sys.argv[0] = script_name
            if script_args is not None:
                sys.argv[1:] = script_args
            f = open(script_name)
            try:
                exec(f.read(), glocals, glocals)
            finally:
                f.close()
        finally:
            sys.argv = save_argv
            core._setup_stop_after = None
    except Exception:
        logging.warn("Exception when running setup.", exc_info=True)

    if core._setup_distribution is None:
        raise RuntimeError(
            "'distutils.core.setup()' was never called -- "
            "perhaps '%s' is not a Distutils setup script?" %
            script_name)

    # I wonder if the setup script's namespace -- g and l -- would be of
    # any interest to callers?
    return core._setup_distribution

def get_data(path):
    """
    Returns data from a package directory.
    'path' should be an absolute path.
    """
    # Run the imported setup to get the metadata.
    with FakeContext(path):
        with SetupMonkey() as sm:
            try:
                distro = run_setup('setup.py', stop_after='config')

                metadata = {'_setuptools': sm.used_setuptools}

                for k, v in distro.metadata.__dict__.items():
                    if k[0] == '_' or not v:
                        continue
                    if all(not x for x in v):
                        continue
                    metadata[k] = v

                if sm.used_setuptools:
                    for extras in ['cmdclass', 'zip_safe', 'test_suite']:
                        v = getattr(distro, extras, None)
                        if v is not None and v not in ([], {}):
                            metadata[extras] = v

            except ImportError as e:
                # Either there is no setup py, or it's broken.
                logging.exception(e)
                metadata = {}

    return metadata