# -*- coding: utf-8 -*-

# Copyright 2009 Joe Wreschnig
#           2014 Ben Ockmore
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.

import collections

from mutagen import Metadata
from mutagen._util import dict_match, utf8
from mutagen.mp4 import MP4, MP4Tags, error, delete
from ._compat import PY2, text_type

__all__ = ["EasyMP4Tags", "EasyMP4", "delete", "error"]

class EasyMP4KeyError(error, KeyError, ValueError):
    pass

class EasyMP4Tags(collections.MutableMapping, Metadata):
    """A file with MPEG-4 iTunes metadata.

    Like Vorbis comments, EasyMP4Tags keys are case-insensitive ASCII
    strings, and values are a list of Unicode strings (and these lists
    are always of length 0 or 1).

    If you need access to the full MP4 metadata feature set, you should use
    MP4, not EasyMP4.
    """

    Set = {}
    Get = {}
    Delete = {}
    List = {}

    def __init__(self, *args, **kwargs):
        self.__mp4 = MP4Tags(*args, **kwargs)
        self.load = self.__mp4.load
        self.save = self.__mp4.save
        self.delete = self.__mp4.delete

    filename = property(lambda s: s.__mp4.filename,
                        lambda s, fn: setattr(s.__mp4, 'filename', fn))

    @classmethod
    def RegisterKey(cls, key,
                    getter=None, setter=None, deleter=None, lister=None):
        """Register a new key mapping.

        A key mapping is four functions, a getter, setter, deleter,
        and lister. The key may be either a string or a glob pattern.

        The getter, deleted, and lister receive an MP4Tags instance
        and the requested key name. The setter also receives the
        desired value, which will be a list of strings.

        The getter, setter, and deleter are used to implement __getitem__,
        __setitem__, and __delitem__.

        The lister is used to implement keys(). It should return a
        list of keys that are actually in the MP4 instance, provided
        by its associated getter.
        """
        key = key.lower()
        if getter is not None:
            cls.Get[key] = getter
        if setter is not None:
            cls.Set[key] = setter
        if deleter is not None:
            cls.Delete[key] = deleter
        if lister is not None:
            cls.List[key] = lister

    @classmethod
    def RegisterTextKey(cls, key, atomid):
        """Register a text key.

        If the key you need to register is a simple one-to-one mapping
        of MP4 atom name to EasyMP4Tags key, then you can use this
        function::

            EasyMP4Tags.RegisterTextKey("artist", b"\xa9ART")
        """
        def getter(tags, key):
            return tags[atomid]

        def setter(tags, key, value):
            tags[atomid] = value

        def deleter(tags, key):
            del(tags[atomid])

        cls.RegisterKey(key, getter, setter, deleter)

    @classmethod
    def RegisterIntKey(cls, key, atomid, min_value=0, max_value=2**16-1):
        """Register a scalar integer key.
        """

        def getter(tags, key):
            return list(map(text_type, tags[atomid]))

        def setter(tags, key, value):
            clamp = lambda x: min(max(min_value, x), max_value)
            tags[atomid] = [clamp(v) for v in map(int, value)]

        def deleter(tags, key):
            del(tags[atomid])

        cls.RegisterKey(key, getter, setter, deleter)

    @classmethod
    def RegisterIntPairKey(cls, key, atomid, min_value=0, max_value=2**16-1):
        def getter(tags, key):
            ret = []
            for (track, total) in tags[atomid]:
                if total:
                    ret.append(u"%d/%d" % (track, total))
                else:
                    ret.append(text_type(track))
            return ret

        def setter(tags, key, value):
            clamp = lambda x: int(min(max(min_value, x), max_value))
            data = []
            for v in value:
                try:
                    tracks, total = v.split("/")
                    tracks = clamp(int(tracks))
                    total = clamp(int(total))
                except (ValueError, TypeError):
                    tracks = clamp(int(v))
                    total = min_value
                data.append((tracks, total))
            tags[atomid] = data

        def deleter(tags, key):
            del(tags[atomid])

        cls.RegisterKey(key, getter, setter, deleter)

    @classmethod
    def RegisterFreeformKey(cls, key, name, mean=b"com.apple.iTunes"):
        """Register a text key.

        If the key you need to register is a simple one-to-one mapping
        of MP4 freeform atom (----) and name to EasyMP4Tags key, then
        you can use this function::

            EasyMP4Tags.RegisterFreeformKey(
                "musicbrainz_artistid", b"MusicBrainz Artist Id")
        """
        atomid = b"----:" + mean + b":" + name

        def getter(tags, key):
            return [s.decode("utf-8", "replace") for s in tags[atomid]]

        def setter(tags, key, value):
            tags[atomid] = [utf8(v) for v in value]

        def deleter(tags, key):
            del(tags[atomid])

        cls.RegisterKey(key, getter, setter, deleter)

    def __getitem__(self, key):
        key = key.lower()
        func = dict_match(self.Get, key)
        if func is not None:
            return func(self.__mp4, key)
        else:
            raise EasyMP4KeyError("%r is not a valid key" % key)

    def __setitem__(self, key, value):
        key = key.lower()

        if PY2:
            if isinstance(value, basestring):
                value = [value]
        else:
            if isinstance(value, text_type):
                value = [value]

        func = dict_match(self.Set, key)
        if func is not None:
            return func(self.__mp4, key, value)
        else:
            raise EasyMP4KeyError("%r is not a valid key" % key)

    def __delitem__(self, key):
        key = key.lower()
        func = dict_match(self.Delete, key)
        if func is not None:
            return func(self.__mp4, key)
        else:
            raise EasyMP4KeyError("%r is not a valid key" % key)

    def __iter__(self):
        keys = []
        for key in self.Get.keys():
            if key in self.List:
                keys.extend(self.List[key](self.__mp4, key))
            elif key in self:
                keys.append(key)
        return iter(keys)

    def __len__(self):
        keys = []
        for key in self.Get.keys():
            if key in self.List:
                keys.extend(self.List[key](self.__mp4, key))
            elif key in self:
                keys.append(key)
        return len(keys)

    def pprint(self):
        """Print tag key=value pairs."""
        strings = []
        for key in sorted(self.keys()):
            values = self[key]
            for value in values:
                strings.append("%s=%s" % (key, value))
        return "\n".join(strings)

for atomid, key in {
    b'\xa9nam': 'title',
    b'\xa9alb': 'album',
    b'\xa9ART': 'artist',
    b'aART': 'albumartist',
    b'\xa9day': 'date',
    b'\xa9cmt': 'comment',
    b'desc': 'description',
    b'\xa9grp': 'grouping',
    b'\xa9gen': 'genre',
    b'cprt': 'copyright',
    b'soal': 'albumsort',
    b'soaa': 'albumartistsort',
    b'soar': 'artistsort',
    b'sonm': 'titlesort',
    b'soco': 'composersort',
}.items():
    EasyMP4Tags.RegisterTextKey(key, atomid)

for name, key in {
    b'MusicBrainz Artist Id': 'musicbrainz_artistid',
    b'MusicBrainz Track Id': 'musicbrainz_trackid',
    b'MusicBrainz Album Id': 'musicbrainz_albumid',
    b'MusicBrainz Album Artist Id': 'musicbrainz_albumartistid',
    b'MusicIP PUID': 'musicip_puid',
    b'MusicBrainz Album Status': 'musicbrainz_albumstatus',
    b'MusicBrainz Album Type': 'musicbrainz_albumtype',
    b'MusicBrainz Release Country': 'releasecountry',
}.items():
    EasyMP4Tags.RegisterFreeformKey(key, name)

for name, key in {
    b"tmpo": "bpm",
}.items():
    EasyMP4Tags.RegisterIntKey(key, name)

for name, key in {
    b"trkn": "tracknumber",
    b"disk": "discnumber",
}.items():
    EasyMP4Tags.RegisterIntPairKey(key, name)

class EasyMP4(MP4):
    """Like :class:`MP4 <mutagen.mp4.MP4>`,
    but uses :class:`EasyMP4Tags` for tags.

    :ivar info: :class:`MP4Info <mutagen.mp4.MP4Info>`
    :ivar tags: :class:`EasyMP4Tags`
    """

    MP4Tags = EasyMP4Tags

    Get = EasyMP4Tags.Get
    Set = EasyMP4Tags.Set
    Delete = EasyMP4Tags.Delete
    List = EasyMP4Tags.List
    RegisterTextKey = EasyMP4Tags.RegisterTextKey
    RegisterKey = EasyMP4Tags.RegisterKey


"""
This module contains HTTP transports used for communicating with the
Route53 API endpoint.
"""

import time
import base64
import hmac
import hashlib
import requests
from route53.exceptions import Route53Error

class BaseTransport(object):
    """
    This serves as an interface for HTTP transports. It provides a really
    simple blueprint for what is involved in working with the Route53
    API.
    """

    def __init__(self, connection):
        """
        :param Route53Connection connection: The connection being used with
            the transport. The connection contains their AWS credentials
            and a few other settings.
        """

        self.connection = connection

    @property
    def endpoint(self):
        """
        :rtype: str
        :returns: The Route53 API endpoint to query against.
        """

        return self.connection._endpoint

    def _hmac_sign_string(self, string_to_sign):
        """
        Route53 uses AWS an HMAC-based authentication scheme, involving the
        signing of a date string with the user's secret access key. More details
        on the specifics can be found in their documentation_.

        .. documentation:: http://docs.amazonwebservices.com/Route53/latest/DeveloperGuide/RESTAuthentication.html

        This method is used to sign said time string, for use in the request
        headers.

        :param str string_to_sign: The time string to sign.
        :rtype: str
        :returns: An HMAC signed string.
        """

        # Just use SHA256, since we're all running modern versions
        # of Python (right?).
        new_hmac = hmac.new(
            self.connection._aws_secret_access_key.encode('utf-8'),
            digestmod=hashlib.sha256
        )
        new_hmac.update(string_to_sign.encode('utf-8'))
        # The HMAC digest str is done at this point.
        digest = new_hmac.digest()
        # Now we have to Base64 encode it, and we're done.
        return base64.b64encode(digest).decode('utf-8')

    def get_request_headers(self):
        """
        Determine the headers to send along with the request. These are
        pretty much the same for every request, with Route53.
        """

        date_header = time.asctime(time.gmtime())
        # We sign the time string above with the user's AWS secret access key
        # in order to authenticate our request.
        signing_key = self._hmac_sign_string(date_header)

        # Amazon's super fun auth token.
        auth_header = "AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=HmacSHA256,Signature=%s" % (
            self.connection._aws_access_key_id,
            signing_key,
        )

        return {
            'X-Amzn-Authorization': auth_header,
            'x-amz-date': date_header,
            'Host': 'route53.amazonaws.com',
        }

    def send_request(self, path, data, method):
        """
        All outbound requests go through this method. It defers to the
        transport's various HTTP method-specific methods.

        :param str path: The path to tack on to the endpoint URL for
            the query.
        :param data: The params to send along with the request.
        :type data: Either a dict or bytes, depending on the request type.
        :param str method: One of 'GET', 'POST', or 'DELETE'.

        :rtype: str
        :returns: The body of the response.
        """

        headers = self.get_request_headers()

        if method == 'GET':
            return self._send_get_request(path, data, headers)
        elif method == 'POST':
            return self._send_post_request(path, data, headers)
        elif method == 'DELETE':
            return self._send_delete_request(path, headers)
        else:
            raise Route53Error("Invalid request method: %s" % method)

    def _send_get_request(self, path, params, headers):
        """
        Transport sub-classes need to override this.

        Sends the GET request to the Route53 endpoint.

        :param str path: The path to tack on to the endpoint URL for
            the query.
        :param dict params: Key/value pairs to send.
        :param dict headers: A dict of headers to send with the request.
        :rtype: str
        :returns: The body of the response.
        """

        raise NotImplementedError

    def _send_post_request(self, path, data, headers):
        """
        Transport sub-classes need to override this.

        Sends the POST request to the Route53 endpoint.

        :param str path: The path to tack on to the endpoint URL for
            the query.
        :param data: Either a dict, or bytes.
        :type data: dict or bytes
        :param dict headers: A dict of headers to send with the request.
        :rtype: str
        :returns: The body of the response.
        """

        raise NotImplementedError

    def _send_delete_request(self, path, headers):
        """
        Transport sub-classes need to override this.

        Sends the DELETE request to the Route53 endpoint.

        :param str path: The path to tack on to the endpoint URL for
            the query.
        :param dict headers: A dict of headers to send with the request.
        :rtype: str
        :returns: The body of the response.
        """

        raise NotImplementedError

class RequestsTransport(BaseTransport):
    """
    A requests-based transport. More details may be found on the
    `requests webpage`_.

    .. _requests webpage: http://docs.python-requests.org/en/latest/
    """

    def _send_get_request(self, path, params, headers):
        """
        Sends the GET request to the Route53 endpoint.

        :param str path: The path to tack on to the endpoint URL for
            the query.
        :param dict params: Key/value pairs to send.
        :param dict headers: A dict of headers to send with the request.
        :rtype: str
        :returns: The body of the response.
        """

        r = requests.get(self.endpoint + path, params=params, headers=headers)
        r.raise_for_status()
        return r.text

    def _send_post_request(self, path, data, headers):
        """
        Sends the POST request to the Route53 endpoint.

        :param str path: The path to tack on to the endpoint URL for
            the query.
        :param data: Either a dict, or bytes.
        :type data: dict or bytes
        :param dict headers: A dict of headers to send with the request.
        :rtype: str
        :returns: The body of the response.
        """

        r = requests.post(self.endpoint + path, data=data, headers=headers)
        return r.text

    def _send_delete_request(self, path, headers):
        """
        Sends the DELETE request to the Route53 endpoint.

        :param str path: The path to tack on to the endpoint URL for
            the query.
        :param dict headers: A dict of headers to send with the request.
        :rtype: str
        :returns: The body of the response.
        """

        r = requests.delete(self.endpoint + path, headers=headers)
        return r.text


# -*- coding: utf-8 -*-

# Copyright 2005 Joe Wreschnig
#           2014 Ben Ockmore
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.

"""APEv2 reading and writing.

The APEv2 format is most commonly used with Musepack files, but is
also the format of choice for WavPack and other formats. Some MP3s
also have APEv2 tags, but this can cause problems with many MP3
decoders and taggers.

APEv2 tags, like Vorbis comments, are freeform key=value pairs. APEv2
keys can be any ASCII string with characters from 0x20 to 0x7E,
between 2 and 255 characters long.  Keys are case-sensitive, but
readers are recommended to be case insensitive, and it is forbidden to
multiple keys which differ only in case.  Keys are usually stored
title-cased (e.g. 'Artist' rather than 'artist').

APEv2 values are slightly more structured than Vorbis comments; values
are flagged as one of text, binary, or an external reference (usually
a URI).

Based off the format specification found at
http://wiki.hydrogenaudio.org/index.php?title=APEv2_specification.
"""

__all__ = ["APEv2", "APEv2File", "Open", "delete"]

import sys
import struct

from ._compat import cBytesIO, PY3, text_type, PY2, reraise, swap_to_string, long_
from mutagen import Metadata, FileType, StreamInfo
from mutagen._util import cdata, delete_bytes, total_ordering

import collections

def is_valid_apev2_key(key):
    if not isinstance(key, text_type):
        if PY3:
            raise TypeError("APEv2 key must be unicode")

        try:
            key = key.decode('ascii')
        except UnicodeDecodeError:
            return False

    #PY26 - Change to set literal syntax (since set is faster than list here)
    return ((2 <= len(key) <= 255) and (min(key) >= u' ') and
            (max(key) <= u'~') and (key not in [u"OggS", u"TAG", u"ID3", u"MP+"]))

# There are three different kinds of APE tag values.
# "0: Item contains text information coded in UTF-8
#  1: Item contains binary information
#  2: Item is a locator of external stored information [e.g. URL]
#  3: reserved"
TEXT, BINARY, EXTERNAL = range(3)

HAS_HEADER = long_(1) << 31
HAS_NO_FOOTER = long_(1) << 30
IS_HEADER = long_(1) << 29

class error(IOError):
    pass

class APENoHeaderError(error, ValueError):
    pass

class APEUnsupportedVersionError(error, ValueError):
    pass

class APEBadItemError(error, ValueError):
    pass

class _APEv2Data(object):
    # Store offsets of the important parts of the file.
    start = header = data = footer = end = None
    # Footer or header; seek here and read 32 to get version/size/items/flags
    metadata = None
    # Actual tag data
    tag = None

    version = None
    size = None
    items = None
    flags = 0

    # The tag is at the start rather than the end. A tag at both
    # the start and end of the file (i.e. the tag is the whole file)
    # is not considered to be at the start.
    is_at_start = False

    def __init__(self, fileobj):
        self.__find_metadata(fileobj)

        if self.header is None:
            self.metadata = self.footer
        elif self.footer is None:
            self.metadata = self.header
        else:
            self.metadata = max(self.header, self.footer)

        if self.metadata is None:
            return

        self.__fill_missing(fileobj)
        self.__fix_brokenness(fileobj)
        if self.data is not None:
            fileobj.seek(self.data)
            self.tag = fileobj.read(self.size)

    def __find_metadata(self, fileobj):
        # Try to find a header or footer.

        # Check for a simple footer.
        try:
            fileobj.seek(-32, 2)
        except IOError:
            fileobj.seek(0, 2)
            return
        if fileobj.read(8) == b"APETAGEX":
            fileobj.seek(-8, 1)
            self.footer = self.metadata = fileobj.tell()
            return

        # Check for an APEv2 tag followed by an ID3v1 tag at the end.
        try:
            fileobj.seek(-128, 2)
            if fileobj.read(3) == b"TAG":

                fileobj.seek(-35, 1)  # "TAG" + header length
                if fileobj.read(8) == b"APETAGEX":
                    fileobj.seek(-8, 1)
                    self.footer = fileobj.tell()
                    return

                # ID3v1 tag at the end, maybe preceded by Lyrics3v2.
                # (http://www.id3.org/lyrics3200.html)
                # (header length - "APETAGEX") - "LYRICS200"
                fileobj.seek(15, 1)
                if fileobj.read(9) == b'LYRICS200':
                    fileobj.seek(-15, 1)  # "LYRICS200" + size tag
                    try:
                        offset = int(fileobj.read(6))
                    except ValueError:
                        raise IOError

                    fileobj.seek(-32 - offset - 6, 1)
                    if fileobj.read(8) == b"APETAGEX":
                        fileobj.seek(-8, 1)
                        self.footer = fileobj.tell()
                        return

        except IOError:
            pass

        # Check for a tag at the start.
        fileobj.seek(0, 0)
        if fileobj.read(8) == b"APETAGEX":
            self.is_at_start = True
            self.header = 0

    def __fill_missing(self, fileobj):
        fileobj.seek(self.metadata + 8)
        self.version = fileobj.read(4)
        self.size = cdata.uint_le(fileobj.read(4))
        self.items = cdata.uint_le(fileobj.read(4))
        self.flags = cdata.uint_le(fileobj.read(4))

        if self.header is not None:
            self.data = self.header + 32
            # If we're reading the header, the size is the header
            # offset + the size, which includes the footer.
            self.end = self.data + self.size
            fileobj.seek(self.end - 32, 0)
            if fileobj.read(8) == b"APETAGEX":
                self.footer = self.end - 32
        elif self.footer is not None:
            self.end = self.footer + 32
            self.data = self.end - self.size
            if self.flags & HAS_HEADER:
                self.header = self.data - 32
            else:
                self.header = self.data
        else:
            raise APENoHeaderError("No APE tag found")

        # exclude the footer from size
        if self.footer is not None:
            self.size -= 32

    def __fix_brokenness(self, fileobj):
        # Fix broken tags written with PyMusepack.
        if self.header is not None:
            start = self.header
        else:
            start = self.data
        fileobj.seek(start)

        while start > 0:
            # Clean up broken writing from pre-Mutagen PyMusepack.
            # It didn't remove the first 24 bytes of header.
            try:
                fileobj.seek(-24, 1)
            except IOError:
                break
            else:
                if fileobj.read(8) == b"APETAGEX":
                    fileobj.seek(-8, 1)
                    start = fileobj.tell()
                else:
                    break
        self.start = start

class APEv2(collections.MutableMapping, Metadata):
    """A file with an APEv2 tag.

    ID3v1 tags are silently ignored and overwritten.
    """

    filename = None

    def __init__(self, *args, **kwargs):
        self.__casemap = {}
        self.__dict = {}
        super(APEv2, self).__init__(*args, **kwargs)
        # Internally all names are stored as lowercase, but the case
        # they were set with is remembered and used when saving.  This
        # is roughly in line with the standard, which says that keys
        # are case-sensitive but two keys differing only in case are
        # not allowed, and recommends case-insensitive
        # implementations.

    def pprint(self):
        """Return tag key=value pairs in a human-readable format."""

        items = sorted(self.items())
        return u"\n".join(u"%s=%s" % (k, v.pprint()) for k, v in items)

    def load(self, filename):
        """Load tags from a filename."""

        self.filename = filename
        fileobj = open(filename, "rb")
        try:
            data = _APEv2Data(fileobj)
        finally:
            fileobj.close()
        if data.tag:
            self.clear()
            self.__casemap.clear()
            self.__parse_tag(data.tag, data.items)
        else:
            raise APENoHeaderError("No APE tag found")

    def __parse_tag(self, tag, count):
        fileobj = cBytesIO(tag)

        for i in range(count):
            size_data = fileobj.read(4)
            # someone writes wrong item counts
            if not size_data:
                break
            size = cdata.uint_le(size_data)
            flags = cdata.uint_le(fileobj.read(4))

            # Bits 1 and 2 bits are flags, 0-3
            # Bit 0 is read/write flag, ignored
            kind = (flags & 6) >> 1
            if kind == 3:
                raise APEBadItemError("value type must be 0, 1, or 2")
            key = value = fileobj.read(1)
            while key[-1:] != b'\x00' and value:
                value = fileobj.read(1)
                key += value
            if key[-1:] == b"\x00":
                key = key[:-1]
            if PY3:
                try:
                    key = key.decode("ascii")
                except UnicodeError as err:
                    reraise(APEBadItemError, err, sys.exc_info()[2])
            value = fileobj.read(size)

            if kind == TEXT:
                value = APETextValue(value, kind)
            elif kind == BINARY:
                value = APEBinaryValue(value, kind)
            elif kind == EXTERNAL:
                value = APEExtValue(value, kind)

            self[key] = value

    def __getitem__(self, key):
        if not is_valid_apev2_key(key):
            raise KeyError("%r is not a valid APEv2 key" % key)
        if PY2:
            key = key.encode('ascii')

        return self.__dict[key.lower()]

    def __delitem__(self, key):
        if not is_valid_apev2_key(key):
            raise KeyError("%r is not a valid APEv2 key" % key)
        if PY2:
            key = key.encode('ascii')

        del(self.__casemap[key.lower()])
        del(self.__dict[key.lower()])

    def __setitem__(self, key, value):
        """'Magic' value setter.

        This function tries to guess at what kind of value you want to
        store. If you pass in a valid UTF-8 or Unicode string, it
        treats it as a text value. If you pass in a list, it treats it
        as a list of string/Unicode values.  If you pass in a string
        that is not valid UTF-8, it assumes it is a binary value.

        Python 3: all bytes will be assumed to be a byte value, even
        if they are valid utf-8.

        If you need to force a specific type of value (e.g. binary
        data that also happens to be valid UTF-8, or an external
        reference), use the APEValue factory and set the value to the
        result of that::

            from mutagen.apev2 import APEValue, EXTERNAL
            tag['Website'] = APEValue('http://example.org', EXTERNAL)
        """

        if not is_valid_apev2_key(key):
            raise KeyError("%r is not a valid APEv2 key" % key)

        if PY2:
            key = key.encode('ascii')

        if not isinstance(value, _APEValue):
            # let's guess at the content if we're not already a value...
            if isinstance(value, text_type):
                # unicode? we've got to be text.
                value = APEValue(value, TEXT)
            elif isinstance(value, list):
                items = []
                for v in value:
                    if not isinstance(v, text_type):
                        if PY3:
                            raise TypeError("item in list not str")
                        v = v.decode("utf-8")
                    items.append(v)

                # list? text.
                value = APEValue(u"\0".join(items), TEXT)
            else:
                if PY3:
                    value = APEValue(value, BINARY)
                else:
                    try:
                        value.decode("utf-8")
                    except UnicodeError:
                        # invalid UTF8 text, probably binary
                        value = APEValue(value, BINARY)
                    else:
                        # valid UTF8, probably text
                        value = APEValue(value, TEXT)
        self.__casemap[key.lower()] = key
        self.__dict[key.lower()] = value

    def __iter__(self):
        return iter(self.__casemap.get(key, key) for key in self.__dict.keys())

    def __len__(self):
        return len(self.__dict.keys())

    def save(self, filename=None):
        """Save changes to a file.

        If no filename is given, the one most recently loaded is used.

        Tags are always written at the end of the file, and include
        a header and a footer.
        """

        filename = filename or self.filename
        try:
            fileobj = open(filename, "r+b")
        except IOError:
            fileobj = open(filename, "w+b")
        data = _APEv2Data(fileobj)

        if data.is_at_start:
            delete_bytes(fileobj, data.end - data.start, data.start)
        elif data.start is not None:
            fileobj.seek(data.start)
            # Delete an ID3v1 tag if present, too.
            fileobj.truncate()
        fileobj.seek(0, 2)

        # "APE tags items should be sorted ascending by size... This is
        # not a MUST, but STRONGLY recommended. Actually the items should
        # be sorted by importance/byte, but this is not feasible."
        tags = sorted((v._internal(k) for k, v in self.items()), key=len)
        num_tags = len(tags)
        tags = b"".join(tags)

        header = bytearray(b"APETAGEX")
        # version, tag size, item count, flags
        header += struct.pack("<4I", 2000, len(tags) + 32, num_tags,
                              HAS_HEADER | IS_HEADER)
        header += b"\0" * 8
        fileobj.write(header)

        fileobj.write(tags)

        footer = bytearray(b"APETAGEX")
        footer += struct.pack("<4I", 2000, len(tags) + 32, num_tags,
                              HAS_HEADER)
        footer += b"\0" * 8

        fileobj.write(footer)
        fileobj.close()

    def delete(self, filename=None):
        """Remove tags from a file."""

        filename = filename or self.filename
        fileobj = open(filename, "r+b")
        try:
            data = _APEv2Data(fileobj)
            if data.start is not None and data.size is not None:
                delete_bytes(fileobj, data.end - data.start, data.start)
        finally:
            fileobj.close()
        self.clear()

Open = APEv2

def delete(filename):
    """Remove tags from a file."""

    try:
        APEv2(filename).delete()
    except APENoHeaderError:
        pass

def APEValue(value, kind):
    """APEv2 tag value factory.

    Use this if you need to specify the value's type manually.  Binary
    and text data are automatically detected by APEv2.__setitem__.
    """

    if kind in (TEXT, EXTERNAL):
        if not isinstance(value, text_type):
            # stricter with py3
            if PY3:
                raise TypeError("str only for text/external values")
        else:
            value = value.encode("utf-8")

    if kind == TEXT:
        return APETextValue(value, kind)
    elif kind == BINARY:
        return APEBinaryValue(value, kind)
    elif kind == EXTERNAL:
        return APEExtValue(value, kind)
    else:
        raise ValueError("kind must be TEXT, BINARY, or EXTERNAL")

@swap_to_string
@total_ordering
class _APEValue(object):
    def __init__(self, value, kind):
        if not isinstance(value, bytes):
            raise TypeError("value not bytes")
        self.kind = kind
        self.value = value

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

    def __bytes__(self):
        return self.value

    def __eq__(self, other):
        return bytes(self) == other

    def __lt__(self, other):
        return bytes(self) < other

    # Packed format for an item:
    # 4B: Value length
    # 4B: Value type
    # Key name
    # 1B: Null
    # Key value
    def _internal(self, key):
        if not isinstance(key, bytes):
            key = key.encode("utf-8")
        data = bytearray()
        data += struct.pack("<2I", len(self.value), self.kind << 1)
        data += key
        data += b"\0"
        data += self.value
        return bytes(data)

    def __repr__(self):
        return "%s(%r, %d)" % (type(self).__name__, self.value, self.kind)

@swap_to_string
@total_ordering
class _APEUtf8Value(_APEValue):

    def __str__(self):
        return self.value.decode("utf-8")

    def __eq__(self, other):
        return text_type(self) == other

    def __lt__(self, other):
        return text_type(self) < other

class APETextValue(_APEUtf8Value):
    """An APEv2 text value.

    Text values are Unicode/UTF-8 strings. They can be accessed like
    strings (with a null separating the values), or arrays of strings.
    """

    def __iter__(self):
        """Iterate over the strings of the value (not the characters)"""

        return iter(text_type(self).split(u"\0"))

    def __getitem__(self, index):
        return text_type(self).split(u"\0")[index]

    def __len__(self):
        return self.value.count(b"\0") + 1

    __hash__ = _APEValue.__hash__

    def __setitem__(self, index, value):
        if not isinstance(value, text_type):
            if PY3:
                raise TypeError("value not str")
            value = value.decode("utf-8")

        values = list(self)
        values[index] = value
        self.value = (u"\0".join(values)).encode('utf-8')

    def pprint(self):
        return u" / ".join(self)

class APEBinaryValue(_APEValue):
    """An APEv2 binary value."""

    def pprint(self):
        return u"[%d bytes]" % len(self)

class APEExtValue(_APEUtf8Value):
    """An APEv2 external value.

    External values are usually URI or IRI strings.
    """

    def pprint(self):
        return u"[External] %s" % text_type(self)

class APEv2File(FileType):
    class _Info(StreamInfo):
        length = 0
        bitrate = 0

        def __init__(self, fileobj):
            pass

        @staticmethod
        def pprint():
            return u"Unknown format with APEv2 tag."

    def load(self, filename):
        self.filename = filename
        self.info = self._Info(open(filename, "rb"))
        try:
            self.tags = APEv2(filename)
        except error:
            self.tags = None

    def add_tags(self):
        if self.tags is None:
            self.tags = APEv2()
        else:
            raise ValueError("%r already has tags: %r" % (self, self.tags))

    @staticmethod
    def score(filename, fileobj, header):
        try:
            fileobj.seek(-160, 2)
        except IOError:
            fileobj.seek(0)
        footer = fileobj.read()
        filename = filename.lower()
        return ((b"APETAGEX" in footer) - header.startswith(b"ID3"))


from lxml import etree
from route53 import xml_parsers, xml_generators
from route53.exceptions import Route53Error
from route53.transport import RequestsTransport
#from route53.util import prettyprint_xml
from route53.xml_parsers.common_change_info import parse_change_info

class Route53Connection(object):
    """
    Instances of this class are instantiated by the top-level
    :py:func:`route53.connect` function, and serve as a high level gateway
    to the Route 53 API. The majority of your interaction with these
    instances will probably be creating, deleting, and retrieving
    :py:class:`HostedZone <route53.hosted_zone.HostedZone>` instances.

    .. warning:: Do not instantiate instances of this class yourself.
    """

    endpoint_version = '2012-02-29'
    """The date-based API version. Mostly visible for your reference."""

    def __init__(self, aws_access_key_id, aws_secret_access_key):
        """
        :param str aws_access_key_id: An account's access key ID.
        :param str aws_secret_access_key: An account's secret access key.
        """

        self._endpoint = 'https://route53.amazonaws.com/%s/' % self.endpoint_version
        self._xml_namespace = 'https://route53.amazonaws.com/doc/%s/' % self.endpoint_version
        self._aws_access_key_id = aws_access_key_id
        self._aws_secret_access_key = aws_secret_access_key
        self._transport = RequestsTransport(self)

    def _send_request(self, path, data, method):
        """
        Uses the HTTP transport to query the Route53 API. Runs the response
        through lxml's parser, before we hand it off for further picking
        apart by our call-specific parsers.

        :param str path: The RESTful path to tack on to the :py:attr:`endpoint`.
        :param data: The params to send along with the request.
        :type data: Either a dict or bytes, depending on the request type.
        :param str method: One of 'GET', 'POST', or 'DELETE'.
        :rtype: lxml.etree._Element
        :returns: An lxml Element root.
        """

        response_body = self._transport.send_request(path, data, method)
        root = etree.fromstring(response_body)
        #print(prettyprint_xml(root))
        return root

    def _do_autopaginating_api_call(self, path, params, method, parser_func,
        next_marker_xpath, next_marker_param_name,
        next_type_xpath=None, parser_kwargs=None):
        """
        Given an API method, the arguments passed to it, and a function to
        hand parsing off to, loop through the record sets in the API call
        until all records have been yielded.

        :param str method: The API method on the endpoint.
        :param dict params: The kwargs from the top-level API method.
        :param callable parser_func: A callable that is used for parsing the
            output from the API call.
        :param str next_marker_param_name: The XPath to the marker tag that
            will determine whether we continue paginating.
        :param str next_marker_param_name: The parameter name to manipulate
            in the request data to bring up the next page on the next
            request loop.
        :keyword str next_type_xpath: For the
            py:meth:`list_resource_record_sets_by_zone_id` method, there's
            an additional paginator token. Specifying this XPath looks for it.
        :keyword dict parser_kwargs: Optional dict of additional kwargs to pass
            on to the parser function.
        :rtype: generator
        :returns: Returns a generator that may be returned by the top-level
            API method.
        """

        if not parser_kwargs:
            parser_kwargs = {}

        # We loop indefinitely since we have no idea how many "pages" of
        # results we're going to have to go through.
        while True:
            # An lxml Element node.
            root = self._send_request(path, params, method)

            # Individually yield HostedZone instances after parsing/instantiating.
            for record in parser_func(root, connection=self, **parser_kwargs):
                yield record

            # This will determine at what offset we start the next query.
            next_marker = root.find(next_marker_xpath)
            if next_marker is None:
                # If the NextMarker tag is absent, we know we've hit the
                # last page.
                break

            # if NextMarker is present, we'll adjust our API request params
            # and query again for the next page.
            params[next_marker_param_name] = next_marker.text

            if next_type_xpath:
                # This is a _list_resource_record_sets_by_zone_id call. Look
                # for the given tag via XPath and adjust our type arg for
                # the next request. Without specifying this, we loop
                # infinitely.
                next_type = root.find(next_type_xpath)
                params['type'] = next_type.text

    def list_hosted_zones(self, page_chunks=100):
        """
        List all hosted zones associated with this connection's account. Since
        this method returns a generator, you can pull as many or as few
        entries as you'd like, without having to query and receive every
        hosted zone you may have.

        :keyword int page_chunks: This API call is "paginated" behind-the-scenes
            in order to break up large result sets. This number determines
            the maximum number of
            :py:class:`HostedZone <route53.hosted_zone.HostedZone>`
            instances to retrieve per request. The default is fine for almost
            everyone.

        :rtype: generator
        :returns: A generator of :py:class:`HostedZone <route53.hosted_zone.HostedZone>`
            instances.
        """

        return  self._do_autopaginating_api_call(
            path='hostedzone',
            params={'maxitems': page_chunks},
            method='GET',
            parser_func=xml_parsers.list_hosted_zones_parser,
            next_marker_xpath="./{*}NextMarker",
            next_marker_param_name="marker",
        )

    def create_hosted_zone(self, name, caller_reference=None, comment=None):
        """
        Creates and returns a new hosted zone. Once a hosted zone is created,
        its details can't be changed.

        :param str name: The name of the hosted zone to create.
        :keyword str caller_reference: A unique string that identifies the
            request and that allows failed create_hosted_zone requests to be
            retried without the risk of executing the operation twice. If no
            value is given, we'll generate a Type 4 UUID for you.
        :keyword str comment: An optional comment to attach to the zone.
        :rtype: tuple
        :returns: A tuple in the form of ``(hosted_zone, change_info)``.
            The ``hosted_zone`` variable contains a
            :py:class:`HostedZone <route53.hosted_zone.HostedZone>`
            instance matching the newly created zone, and ``change_info``
            is a dict with some details about the API request.
        """

        body = xml_generators.create_hosted_zone_writer(
            connection=self,
            name=name,
            caller_reference=caller_reference,
            comment=comment
        )

        root = self._send_request(
            path='hostedzone',
            data=body,
            method='POST',
        )

        return xml_parsers.created_hosted_zone_parser(
            root=root,
            connection=self
        )

    def get_hosted_zone_by_id(self, id):
        """
        Retrieves a hosted zone, by hosted zone ID (not name).

        :param str id: The hosted zone's ID (a short hash string).
        :rtype: :py:class:`HostedZone <route53.hosted_zone.HostedZone>`
        :returns: An :py:class:`HostedZone <route53.hosted_zone.HostedZone>`
            instance representing the requested hosted zone.
        """

        root = self._send_request(
            path='hostedzone/%s' % id,
            data={},
            method='GET',
        )

        return xml_parsers.get_hosted_zone_by_id_parser(
            root=root,
            connection=self,
        )

    def delete_hosted_zone_by_id(self, id):
        """
        Deletes a hosted zone, by hosted zone ID (not name).

        .. tip:: For most cases, we recommend deleting hosted zones via a
            :py:class:`HostedZone <route53.hosted_zone.HostedZone>`
            instance's
            :py:meth:`HostedZone.delete <route53.hosted_zone.HostedZone.delete>`
            method, but this saves an HTTP request if you already know the zone's ID.

        .. note:: Unlike
            :py:meth:`HostedZone.delete <route53.hosted_zone.HostedZone.delete>`,
            this method has no optional ``force`` kwarg.

        :param str id: The hosted zone's ID (a short hash string).
        :rtype: dict
        :returns: A dict of change info, which contains some details about
            the request.
        """

        root = self._send_request(
            path='hostedzone/%s' % id,
            data={},
            method='DELETE',
        )

        return xml_parsers.delete_hosted_zone_by_id_parser(
            root=root,
            connection=self,
        )

    def _list_resource_record_sets_by_zone_id(self, id, rrset_type=None,
                                             identifier=None, name=None,
                                             page_chunks=100):
        """
        Lists a hosted zone's resource record sets by Zone ID, if you
        already know it.

        .. tip:: For most cases, we recommend going through a
            :py:class:`HostedZone <route53.hosted_zone.HostedZone>`
            instance's
            :py:meth:`HostedZone.record_sets <route53.hosted_zone.HostedZone.record_sets>`
            property, but this saves an HTTP request if you already know the
            zone's ID.

        :param str id: The ID of the zone whose record sets we're listing.
        :keyword str rrset_type: The type of resource record set to begin the
            record listing from.
        :keyword str identifier: Weighted and latency resource record sets
            only: If results were truncated for a given DNS name and type,
            the value of SetIdentifier for the next resource record set
            that has the current DNS name and type.
        :keyword str name: Not really sure what this does.
        :keyword int page_chunks: This API call is paginated behind-the-scenes
            by this many ResourceRecordSet instances. The default should be
            fine for just about everybody, aside from those with tons of RRS.

        :rtype: generator
        :returns: A generator of ResourceRecordSet instances.
        """

        params = {
            'name': name,
            'type': rrset_type,
            'identifier': identifier,
            'maxitems': page_chunks,
        }

        return  self._do_autopaginating_api_call(
            path='hostedzone/%s/rrset' % id,
            params=params,
            method='GET',
            parser_func=xml_parsers.list_resource_record_sets_by_zone_id_parser,
            parser_kwargs={'zone_id': id},
            next_marker_xpath="./{*}NextRecordName",
            next_marker_param_name="name",
            next_type_xpath="./{*}NextRecordType"
        )

    def _change_resource_record_sets(self, change_set, comment=None):
        """
        Given a ChangeSet, POST it to the Route53 API.

        .. note:: You probably shouldn't be using this method directly,
            as there are convenience methods on the ResourceRecordSet
            sub-classes.

        :param change_set.ChangeSet change_set: The ChangeSet object to create
            the XML doc from.
        :keyword str comment: An optional comment to go along with the request.
        :rtype: dict
        :returns: A dict of change info, which contains some details about
            the request.
        """

        body = xml_generators.change_resource_record_set_writer(
            connection=self,
            change_set=change_set,
            comment=comment
        )

        root = self._send_request(
            path='hostedzone/%s/rrset' % change_set.hosted_zone_id,
            data=body,
            method='POST',
        )

        #print(prettyprint_xml(root))

        e_change_info = root.find('./{*}ChangeInfo')
        if e_change_info is None:
            error = root.find('./{*}Error').find('./{*}Message').text
            raise Route53Error(error)
        return parse_change_info(e_change_info)


from ctypes import *
from bacon.core import lib
from bacon import native
import bacon

BlendFlags = native.BlendFlags

if native._mock_native:
    def push_transform():
        pass
else:
    push_transform = lib.PushTransform
push_transform.__doc__ = '''Save the current graphics transform by pushing it on the transform stack.  It can be restored by
calling :func:`pop_transform`.
'''

if native._mock_native:
    def pop_transform():
        pass
else:
    pop_transform = lib.PopTransform
pop_transform.__doc__ = '''Restore a previously saved transform by popping it off the transform stack.
'''

if native._mock_native:
    def translate(x, y):
        pass
else:
    translate = lib.Translate
translate.__doc__ = '''Translate the current graphics transform by ``(x, y)`` units.
'''

if native._mock_native:
    def scale(sx, sy):
        pass
else:
    scale = lib.Scale
scale.__doc__ = '''Scale the current graphics transform by multiplying through ``(sx, sy)``.
'''

if native._mock_native:
    def rotate(radians):
        pass
else:
    rotate = lib.Rotate
rotate.__doc__ = '''Rotate the current graphics transform by ``radians`` counter-clockwise.
'''

def set_transform(matrix):
    '''Replace the current graphics transform with the given 4x4 matrix.  For example, to replace
    the transform with a translation by ``(x, y)``::

        bacon.set_transform([1, 0, 0, x,
                             0, 1, 0, y,
                             0, 0, 1, 0,
                             0, 0, 0, 1])

    :param matrix: a 4x4 matrix in column major order, represented as a flat 16 element sequence.
    '''
    lib.SetTransform((c_float * 16)(*matrix))

if native._mock_native:
    def push_color():
        pass
else:
    push_color = lib.PushColor
push_color.__doc__ = '''Save the current graphics color by pushing it on the color stack.  It can be restored with :func:`pop_color`.

The color stack is cleared at the beginning of each frame, and the default color reset to white.
'''

if native._mock_native:
    def pop_color():
        pass
else:
    pop_color = lib.PopColor
pop_color.__doc__ = '''Restore a previously saved graphics color by popping it off the color stack.
'''

if native._mock_native:
    def set_color(r, g, b, a):
        pass
else:
    set_color = lib.SetColor
set_color.__doc__ = '''Set the current graphics color to the given RGBA values.  Typically each component has a value between
0.0 and 1.0, however out-of-range values are permitted and may be used for special effects with an
appropriate shader.

The color is reset to white at the beginning of each frame.
'''

if native._mock_native:
    def multiply_color(r, g, b, a):
        pass
else:
    multiply_color = lib.MultiplyColor
multiply_color.__doc__ = '''multiply_color(r, g, b, a)

Multiplies the current graphics color component-wise by the given RGBA values.
'''

if native._mock_native:
    def clear(r, g, b, a):
        pass
else:
    clear = lib.Clear
clear.__doc__ = '''Clear the current target to the given RGBA color.  Each color component must be in the range 0.0 to 1.0.
You should clear the window at the beginning of each frame.  Failure to do so may cause visual
artifacts and/or poor performance on some platforms.
'''

_target_stack = [None]

def push_target(target):
    '''Begin rendering to the given image or window.  All subsequent drawing will be to the given target.
    Use :func:`pop_target` to resume rendering to the previous target.

    The target stack is automatically cleared and returned to the window at the beginning of each frame.

    :param image: the :class:`Image` or :class:`Window` to render to
    '''
    _target_stack.append(target)
    _set_target(target)

def pop_target():
    '''Remove the current target from the stack and resume rendering on the one that was active
    before :func:`push_target` was called.
    '''
    _target_stack.pop()
    _set_target(_target_stack[-1])

def get_target():
    '''Get the current target being rendered to.

    :return: :class:`Image` or :class:`Window`'''
    return _target_stack[-1] or bacon.window

def _set_target(target):
    try:
        lib.SetFrameBuffer(target._handle, target._content_scale)
    except AttributeError:
        lib.SetFrameBuffer(0, bacon.window._content_scale)

if native._mock_native:
    def set_viewport(x, y, width, height):
        pass
else:
    set_viewport = lib.SetViewport
set_viewport.__doc__ = '''Set the current viewport in screen-space.  This affects both the GPU viewport, which provides a screen-space clip,
and the projection matrix, which is constructed from the viewport coordinates automatically.

Viewport coordinates are specified in pixel-space with (0, 0) at the upper-left corner.

The viewport is reset to the window dimensions at the beginning of each frame, and when switching targets.
'''

def set_shader(shader):
    '''Set the current graphics shader.  All subsequent drawing commands will be rendered with this shader.
    To reset to the default shader, pass ``None`` as the argument.

    The shader is automatically reset to the default shader at the beginning of each frame.

    :param shader: a :class:`Shader` to render with
    '''
    lib.SetShader(shader._handle if shader else 0)

if native._mock_native:
    def set_blending(src_blend, dest_blend):
        pass
else:
    set_blending = lib.SetBlending
set_blending.__doc__ = '''Set the current graphics blend mode.  All subsequent drawing commands will be rendered with this blend mode.

The default blend mode is ``(BlendFlags.one, BlendFlags.one_minus_src_alpha)``, which is appropriate for
premultiplied alpha.

:param src_blend: a value from the :class:`BlendFlags` enumeration, specifying the blend contribution from the source fragment
:param dest_blend: a value from the :class:`BlendFlags` enumeration, specifying the blend contribution from the destination fragment
'''

def draw_image(image, x1, y1, x2 = None, y2 = None):
    '''Draw an image.

    The image's top-left corner is drawn at ``(x1, y1)``, and its lower-left at ``(x2, y2)``.  If ``x2`` and ``y2`` are omitted, they
    are calculated to render the image at its native resoultion.

    Note that images can be flipped and scaled by providing alternative values for ``x2`` and ``y2``.

    :param image: an :class:`Image` to draw
    '''
    if x2 is None:
        x2 = x1 + image.width
    if y2 is None:
        y2 = y1 + image.height
    lib.DrawImage(image._handle, x1, y1, x2, y2)

def draw_image_region(image, x1, y1, x2, y2,
                      ix1, iy1, ix2, iy2):
    '''Draw a rectangular region of an image.

    The part of the image contained by the rectangle in texel-space by the coordinates ``(ix1, iy1)`` to ``(ix2, iy2)`` is
    drawn at coordinates ``(x1, y1)`` to ``(x2, y2)``.  All coordinates have the origin ``(0, 0)`` at the upper-left corner.

    For example, to draw the left half of a ``100x100`` image at coordinates ``(x, y)``::

        bacon.draw_image_region(image, x, y, x + 50, y + 100,
                                0, 0, 50, 100)

    :param image: an :class:`Image` to draw
    '''
    lib.DrawImageRegion(image._handle, x1, y1, x2, y2, ix1, iy1, ix2, iy2)

if native._mock_native:
    def draw_line(x1, y1, x2, y2):
        pass
else:
    draw_line = lib.DrawLine
draw_line.__doc__ = '''Draw a line from coordinates ``(x1, y1)`` to ``(x2, y2)``.

No texture is applied.
'''

if native._mock_native:
    def draw_rect(x1, y1, x2, y2):
        pass
else:
    draw_rect = lib.DrawRect
draw_rect.__doc__ = '''Draw a rectangle bounding coordinates ``(x1, y1)`` to ``(x2, y2)``.

No texture is applied.
'''

if native._mock_native:
    def fill_rect(x1, y1, x2, y2):
        pass
else:
    fill_rect = lib.FillRect
fill_rect.__doc__ = '''Fill a rectangle bounding coordinates ``(x1, y1)`` to ``(x2, y2)``.

No texture is applied.
'''


# -*- coding: utf-8 -*-

# Copyright 2006 Joe Wreschnig
#           2014 Ben Ockmore
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.

"""Read and write Ogg bitstreams and pages.

This module reads and writes a subset of the Ogg bitstream format
version 0. It does *not* read or write Ogg Vorbis files! For that,
you should use mutagen.oggvorbis.

This implementation is based on the RFC 3533 standard found at
http://www.xiph.org/ogg/doc/rfc3533.txt.
"""

import struct
import sys
import zlib

from mutagen import FileType
from mutagen._util import cdata, insert_bytes, delete_bytes
from ._compat import cBytesIO, reraise, chr_

class error(IOError):
    """Ogg stream parsing errors."""

    pass

class OggPage(object):
    """A single Ogg page (not necessarily a single encoded packet).

    A page is a header of 26 bytes, followed by the length of the
    data, followed by the data.

    The constructor is givin a file-like object pointing to the start
    of an Ogg page. After the constructor is finished it is pointing
    to the start of the next page.

    Attributes:

    * version -- stream structure version (currently always 0)
    * position -- absolute stream position (default -1)
    * serial -- logical stream serial number (default 0)
    * sequence -- page sequence number within logical stream (default 0)
    * offset -- offset this page was read from (default None)
    * complete -- if the last packet on this page is complete (default True)
    * packets -- list of raw packet data (default [])

    Note that if 'complete' is false, the next page's 'continued'
    property must be true (so set both when constructing pages).

    If a file-like object is supplied to the constructor, the above
    attributes will be filled in based on it.
    """

    version = 0
    __type_flags = 0
    position = 0
    serial = 0
    sequence = 0
    offset = None
    complete = True

    def __init__(self, fileobj=None):
        self.packets = []

        if fileobj is None:
            return

        self.offset = fileobj.tell()

        header = fileobj.read(27)
        if len(header) == 0:
            raise EOFError

        try:
            (oggs, self.version, self.__type_flags, self.position,
             self.serial, self.sequence, crc, segments) = struct.unpack(
                 "<4sBBqIIiB", header)
        except struct.error:
            raise error("unable to read full header; got %r" % header)

        if oggs != b"OggS":
            raise error("read %r, expected %r, at 0x%x" % (
                oggs, b"OggS", fileobj.tell() - 27))

        if self.version != 0:
            raise error("version %r unsupported" % self.version)

        total = 0
        lacings = []
        lacing_bytes = fileobj.read(segments)
        if len(lacing_bytes) != segments:
            raise error("unable to read %r lacing bytes" % segments)
        for c in bytearray(lacing_bytes):
            total += c
            if c < 255:
                lacings.append(total)
                total = 0
        if total:
            lacings.append(total)
            self.complete = False

        self.packets = [fileobj.read(l) for l in lacings]
        if list(map(len, self.packets)) != lacings:
            raise error("unable to read full data")

    def __eq__(self, other):
        """Two Ogg pages are the same if they write the same data."""
        try:
            return (self.write() == other.write())
        except AttributeError:
            return False

    __hash__ = object.__hash__

    def __repr__(self):
        attrs = ['version', 'position', 'serial', 'sequence', 'offset',
                 'complete', 'continued', 'first', 'last']
        values = ["%s=%r" % (attr, getattr(self, attr)) for attr in attrs]
        return "<%s %s, %d bytes in %d packets>" % (
            type(self).__name__, " ".join(values), sum(map(len, self.packets)),
            len(self.packets))

    def write(self):
        """Return a string encoding of the page header and data.

        A ValueError is raised if the data is too big to fit in a
        single page.
        """

        data = [
            struct.pack("<4sBBqIIi", b"OggS", self.version, self.__type_flags,
                        self.position, self.serial, self.sequence, 0)
        ]

        lacing_data = []
        for datum in self.packets:
            quot, rem = divmod(len(datum), 255)
            lacing_data.append(b"\xff" * quot + chr_(rem))
        lacing_data = b"".join(lacing_data)
        if not self.complete and lacing_data.endswith(b"\x00"):
            lacing_data = lacing_data[:-1]
        data.append(chr_(len(lacing_data)))
        data.append(lacing_data)
        data.extend(self.packets)
        data = b"".join(data)

        # Python's CRC is swapped relative to Ogg's needs.
        # crc32 returns uint prior to py2.6 on some platforms, so force uint
        crc = (~zlib.crc32(data.translate(cdata.bitswap), -1)) & 0xffffffff
        # Although we're using to_uint_be, this actually makes the CRC
        # a proper le integer, since Python's CRC is byteswapped.
        crc = cdata.to_uint_be(crc).translate(cdata.bitswap)
        data = data[:22] + crc + data[26:]
        return data

    @property
    def size(self):
        """Total frame size."""

        header_size = 27 # Initial header size
        for datum in self.packets:
            quot, rem = divmod(len(datum), 255)
            header_size += quot + 1
        if not self.complete and rem == 0:
            # Packet contains a multiple of 255 bytes and is not
            # terminated, so we don't have a \x00 at the end.
            header_size -= 1
        header_size += sum(map(len, self.packets))
        return header_size

    def __set_flag(self, bit, val):
        mask = 1 << bit
        if val:
            self.__type_flags |= mask
        else:
            self.__type_flags &= ~mask

    continued = property(
        lambda self: cdata.test_bit(self.__type_flags, 0),
        lambda self, v: self.__set_flag(0, v),
        doc="The first packet is continued from the previous page.")

    first = property(
        lambda self: cdata.test_bit(self.__type_flags, 1),
        lambda self, v: self.__set_flag(1, v),
        doc="This is the first page of a logical bitstream.")

    last = property(
        lambda self: cdata.test_bit(self.__type_flags, 2),
        lambda self, v: self.__set_flag(2, v),
        doc="This is the last page of a logical bitstream.")

    @staticmethod
    def renumber(fileobj, serial, start):
        """Renumber pages belonging to a specified logical stream.

        fileobj must be opened with mode r+b or w+b.

        Starting at page number 'start', renumber all pages belonging
        to logical stream 'serial'. Other pages will be ignored.

        fileobj must point to the start of a valid Ogg page; any
        occuring after it and part of the specified logical stream
        will be numbered. No adjustment will be made to the data in
        the pages nor the granule position; only the page number, and
        so also the CRC.

        If an error occurs (e.g. non-Ogg data is found), fileobj will
        be left pointing to the place in the stream the error occured,
        but the invalid data will be left intact (since this function
        does not change the total file size).
        """

        number = start
        while True:
            try:
                page = OggPage(fileobj)
            except EOFError:
                break
            else:
                if page.serial != serial:
                    # Wrong stream, skip this page.
                    continue
                # Changing the number can't change the page size,
                # so seeking back based on the current size is safe.
                fileobj.seek(-page.size, 1)
            page.sequence = number
            fileobj.write(page.write())
            fileobj.seek(page.offset + page.size, 0)
            number += 1

    @staticmethod
    def to_packets(pages, strict=False):
        """Construct a list of packet data from a list of Ogg pages.

        If strict is true, the first page must start a new packet,
        and the last page must end the last packet.
        """

        serial = pages[0].serial
        sequence = pages[0].sequence
        packets = []

        if strict:
            if pages[0].continued:
                raise ValueError("first packet is continued")
            if not pages[-1].complete:
                raise ValueError("last packet does not complete")
        elif pages and pages[0].continued:
            packets.append([b""])

        for page in pages:
            if serial != page.serial:
                raise ValueError("invalid serial number in %r" % page)
            elif sequence != page.sequence:
                raise ValueError("bad sequence number in %r" % page)
            else:
                sequence += 1

            if page.continued:
                packets[-1].append(page.packets[0])
            else:
                packets.append([page.packets[0]])
            packets.extend([p] for p in page.packets[1:])

        return [b"".join(p) for p in packets]

    @staticmethod
    def from_packets(packets, sequence=0, default_size=4096,
                     wiggle_room=2048):
        """Construct a list of Ogg pages from a list of packet data.

        The algorithm will generate pages of approximately
        default_size in size (rounded down to the nearest multiple of
        255). However, it will also allow pages to increase to
        approximately default_size + wiggle_room if allowing the
        wiggle room would finish a packet (only one packet will be
        finished in this way per page; if the next packet would fit
        into the wiggle room, it still starts on a new page).

        This method reduces packet fragmentation when packet sizes are
        slightly larger than the default page size, while still
        ensuring most pages are of the average size.

        Pages are numbered started at 'sequence'; other information is
        uninitialized.
        """

        chunk_size = (default_size // 255) * 255

        pages = []

        page = OggPage()
        page.sequence = sequence

        for packet in packets:
            page.packets.append(b"")
            while packet:
                data, packet = packet[:chunk_size], packet[chunk_size:]
                if page.size < default_size and len(page.packets) < 255:
                    page.packets[-1] += data
                else:
                    # If we've put any packet data into this page yet,
                    # we need to mark it incomplete. However, we can
                    # also have just started this packet on an already
                    # full page, in which case, just start the new
                    # page with this packet.
                    if page.packets[-1]:
                        page.complete = False
                        if len(page.packets) == 1:
                            page.position = -1
                    else:
                        page.packets.pop(-1)
                    pages.append(page)
                    page = OggPage()
                    page.continued = not pages[-1].complete
                    page.sequence = pages[-1].sequence + 1
                    page.packets.append(data)

                if len(packet) < wiggle_room:
                    page.packets[-1] += packet
                    packet = b""

        if page.packets:
            pages.append(page)

        return pages

    @classmethod
    def replace(cls, fileobj, old_pages, new_pages):
        """Replace old_pages with new_pages within fileobj.

        old_pages must have come from reading fileobj originally.
        new_pages are assumed to have the 'same' data as old_pages,
        and so the serial and sequence numbers will be copied, as will
        the flags for the first and last pages.

        fileobj will be resized and pages renumbered as necessary. As
        such, it must be opened r+b or w+b.
        """

        # Number the new pages starting from the first old page.
        first = old_pages[0].sequence
        for page, seq in zip(new_pages, range(first, first + len(new_pages))):
            page.sequence = seq
            page.serial = old_pages[0].serial

        new_pages[0].first = old_pages[0].first
        new_pages[0].last = old_pages[0].last
        new_pages[0].continued = old_pages[0].continued

        new_pages[-1].first = old_pages[-1].first
        new_pages[-1].last = old_pages[-1].last
        new_pages[-1].complete = old_pages[-1].complete
        if not new_pages[-1].complete and len(new_pages[-1].packets) == 1:
            new_pages[-1].position = -1

        new_data = b''.join(cls.write(p) for p in new_pages)

        # Make room in the file for the new data.
        delta = len(new_data)
        fileobj.seek(old_pages[0].offset, 0)
        insert_bytes(fileobj, delta, old_pages[0].offset)
        fileobj.seek(old_pages[0].offset, 0)
        fileobj.write(new_data)
        new_data_end = old_pages[0].offset + delta

        # Go through the old pages and delete them. Since we shifted
        # the data down the file, we need to adjust their offsets. We
        # also need to go backwards, so we don't adjust the deltas of
        # the other pages.
        old_pages.reverse()
        for old_page in old_pages:
            adj_offset = old_page.offset + delta
            delete_bytes(fileobj, old_page.size, adj_offset)

        # Finally, if there's any discrepency in length, we need to
        # renumber the pages for the logical stream.
        if len(old_pages) != len(new_pages):
            fileobj.seek(new_data_end, 0)
            serial = new_pages[-1].serial
            sequence = new_pages[-1].sequence + 1
            cls.renumber(fileobj, serial, sequence)

    @staticmethod
    def find_last(fileobj, serial):
        """Find the last page of the stream 'serial'.

        If the file is not multiplexed this function is fast. If it is,
        it must read the whole the stream.

        This finds the last page in the actual file object, or the last
        page in the stream (with eos set), whichever comes first.
        """

        # For non-muxed streams, look at the last page.
        try:
            fileobj.seek(-256*256, 2)
        except IOError:
            # The file is less than 64k in length.
            fileobj.seek(0)
        data = fileobj.read()
        try:
            index = data.rindex(b"OggS")
        except ValueError:
            raise error("unable to find final Ogg header")
        bytesobj = cBytesIO(data[index:])
        best_page = None
        try:
            page = OggPage(bytesobj)
        except error:
            pass
        else:
            if page.serial == serial:
                if page.last:
                    return page
                else:
                    best_page = page
            else:
                best_page = None

        # The stream is muxed, so use the slow way.
        fileobj.seek(0)
        try:
            page = OggPage(fileobj)
            while not page.last:
                page = OggPage(fileobj)
                while page.serial != serial:
                    page = OggPage(fileobj)
                best_page = page
            return page
        except error:
            return best_page
        except EOFError:
            return best_page

class OggFileType(FileType):
    """An generic Ogg file."""

    _Info = None
    _Tags = None
    _Error = None
    _mimes = ["application/ogg", "application/x-ogg"]

    def load(self, filename):
        """Load file information from a filename."""

        self.filename = filename
        fileobj = open(filename, "rb")
        try:
            try:
                self.info = self._Info(fileobj)
                self.tags = self._Tags(fileobj, self.info)
                self.info._post_tags(fileobj)
            except error as e:
                reraise(self._Error, e, sys.exc_info()[2])
            except EOFError:
                raise self._Error("no appropriate stream found")
        finally:
            fileobj.close()

    def delete(self, filename=None):
        """Remove tags from a file.

        If no filename is given, the one most recently loaded is used.
        """

        if filename is None:
            filename = self.filename

        self.tags.clear()
        fileobj = open(filename, "rb+")
        try:
            try:
                self.tags._inject(fileobj)
            except error as e:
                reraise(self._Error, e, sys.exc_info()[2])
            except EOFError:
                raise self._Error("no appropriate stream found")
        finally:
            fileobj.close()

    def save(self, filename=None):
        """Save a tag to a file.

        If no filename is given, the one most recently loaded is used.
        """

        if filename is None:
            filename = self.filename
        fileobj = open(filename, "rb+")
        try:
            try:
                self.tags._inject(fileobj)
            except error as e:
                reraise(self._Error, e, sys.exc_info()[2])
            except EOFError:
                raise self._Error("no appropriate stream found")
        finally:
            fileobj.close()


# -*- coding: utf-8 -*-

# Copyright 2006 Joe Wreschnig
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.

"""Read and write Ogg Theora comments.

This module handles Theora files wrapped in an Ogg bitstream. The
first Theora stream found is used.

Based on the specification at http://theora.org/doc/Theora_I_spec.pdf.
"""

__all__ = ["OggTheora", "Open", "delete"]

import struct

from mutagen import StreamInfo
from mutagen._vorbis import VCommentDict
from mutagen._util import cdata
from mutagen.ogg import OggPage, OggFileType, error as OggError

class error(OggError):
    pass

class OggTheoraHeaderError(error):
    pass

class OggTheoraInfo(StreamInfo):
    """Ogg Theora stream information.

    Attributes:

    * length - file length in seconds, as a float
    * fps - video frames per second, as a float
    """

    length = 0

    def __init__(self, fileobj):
        page = OggPage(fileobj)
        while not page.packets[0].startswith(b"\x80theora"):
            page = OggPage(fileobj)
        if not page.first:
            raise OggTheoraHeaderError(
                "page has ID header, but doesn't start a stream")
        data = page.packets[0]
        vmaj, vmin = struct.unpack("2B", data[7:9])
        if (vmaj, vmin) != (3, 2):
            raise OggTheoraHeaderError(
                "found Theora version %d.%d != 3.2" % (vmaj, vmin))
        fps_num, fps_den = struct.unpack(">2I", data[22:30])
        self.fps = fps_num / float(fps_den)
        self.bitrate = cdata.uint_be(b"\x00" + data[37:40])
        self.granule_shift = (cdata.ushort_be(data[40:42]) >> 5) & 0x1F
        self.serial = page.serial

    def _post_tags(self, fileobj):
        page = OggPage.find_last(fileobj, self.serial)
        position = page.position
        mask = (1 << self.granule_shift) - 1
        frames = (position >> self.granule_shift) + (position & mask)
        self.length = frames / float(self.fps)

    def pprint(self):
        return "Ogg Theora, %.2f seconds, %d bps" % (self.length, self.bitrate)

class OggTheoraCommentDict(VCommentDict):
    """Theora comments embedded in an Ogg bitstream."""

    def __init__(self, fileobj, info):
        pages = []
        complete = False
        while not complete:
            page = OggPage(fileobj)
            if page.serial == info.serial:
                pages.append(page)
                complete = page.complete or (len(page.packets) > 1)
        data = OggPage.to_packets(pages)[0][7:]
        super(OggTheoraCommentDict, self).__init__(data + b"\x01")

    def _inject(self, fileobj):
        """Write tag data into the Theora comment packet/page."""

        fileobj.seek(0)
        page = OggPage(fileobj)
        while not page.packets[0].startswith(b"\x81theora"):
            page = OggPage(fileobj)

        old_pages = [page]
        while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1):
            page = OggPage(fileobj)
            if page.serial == old_pages[0].serial:
                old_pages.append(page)

        packets = OggPage.to_packets(old_pages, strict=False)

        packets[0] = b"\x81theora" + self.write(framing=False)

        new_pages = OggPage.from_packets(packets, old_pages[0].sequence)
        OggPage.replace(fileobj, old_pages, new_pages)

class OggTheora(OggFileType):
    """An Ogg Theora file."""

    _Info = OggTheoraInfo
    _Tags = OggTheoraCommentDict
    _Error = OggTheoraHeaderError
    _mimes = ["video/x-theora"]

    @staticmethod
    def score(filename, fileobj, header):
        return (header.startswith(b"OggS") *
                ((b"\x80theora" in header) + (b"\x81theora" in header)))

Open = OggTheora

def delete(filename):
    """Remove tags from a file."""

    OggTheora(filename).delete()


from route53.xml_parsers.common_change_info import parse_change_info
from route53.xml_parsers.common_hosted_zone import parse_hosted_zone, parse_delegation_set

def created_hosted_zone_parser(root, connection):
    """
    Parses the API responses for the
    :py:meth:`route53.connection.Route53Connection.create_hosted_zone` method.

    :param lxml.etree._Element root: The root node of the etree parsed
        response from the API.
    :param Route53Connection connection: The connection instance used to
        query the API.
    :rtype: HostedZone
    :returns: The newly created HostedZone.
    """

    zone = root.find('./{*}HostedZone')
    # This pops out a HostedZone instance.
    hosted_zone = parse_hosted_zone(zone, connection)

    # Now we'll fill in the nameservers.
    e_delegation_set = root.find('./{*}DelegationSet')
    # Modifies the HostedZone in place.
    parse_delegation_set(hosted_zone, e_delegation_set)

    # With each CreateHostedZone request, there's some details about the
    # request's ID, status, and submission time. We'll return this in a tuple
    # just for the sake of completeness.
    e_change_info = root.find('./{*}ChangeInfo')
    # Translate the ChangeInfo values to a dict.
    change_info = parse_change_info(e_change_info)

    return hosted_zone, change_info


#  Content (c) 2002, 2004, 2006-2009, 2012
#    David Turner <david@freetype.org>
#
#  This file contains routines used to parse the content of documentation
#  comment blocks and build more structured objects out of them.
#

from sources import *
from utils import *
import string, re

# this regular expression is used to detect code sequences. these
# are simply code fragments embedded in '{' and '}' like in:
#
#  {
#    x = y + z;
#    if ( zookoo == 2 )
#    {
#      foobar();
#    }
#  }
#
# note that indentation of the starting and ending accolades must be
# exactly the same. the code sequence can contain accolades at greater
# indentation
#
re_code_start = re.compile( r"(\s*){\s*$" )
re_code_end   = re.compile( r"(\s*)}\s*$" )

# this regular expression is used to isolate identifiers from
# other text
#
re_identifier = re.compile( r'((?:\w|-)*)' )

# we collect macros ending in `_H'; while outputting the object data, we use
# this info together with the object's file location to emit the appropriate
# header file macro and name before the object itself
#
re_header_macro = re.compile( r'^#define\s{1,}(\w{1,}_H)\s{1,}<(.*)>' )

#############################################################################
#
# The DocCode class is used to store source code lines.
#
#   'self.lines' contains a set of source code lines that will be dumped as
#   HTML in a <PRE> tag.
#
#   The object is filled line by line by the parser; it strips the leading
#   "margin" space from each input line before storing it in 'self.lines'.
#
class  DocCode:

    def  __init__( self, margin, lines ):
        self.lines = []
        self.words = None

        # remove margin spaces
        for l in lines:
            if string.strip( l[:margin] ) == "":
                l = l[margin:]
            self.lines.append( l )

    def  dump( self, prefix = "", width = 60 ):
        lines = self.dump_lines( 0, width )
        for l in lines:
            print prefix + l

    def  dump_lines( self, margin = 0, width = 60 ):
        result = []
        for l in self.lines:
            result.append( " " * margin + l )
        return result

#############################################################################
#
# The DocPara class is used to store "normal" text paragraph.
#
#   'self.words' contains the list of words that make up the paragraph
#
class  DocPara:

    def  __init__( self, lines ):
        self.lines = None
        self.words = []
        for l in lines:
            l = string.strip( l )
            self.words.extend( string.split( l ) )

    def  dump( self, prefix = "", width = 60 ):
        lines = self.dump_lines( 0, width )
        for l in lines:
            print prefix + l

    def  dump_lines( self, margin = 0, width = 60 ):
        cur    = ""  # current line
        col    = 0   # current width
        result = []

        for word in self.words:
            ln = len( word )
            if col > 0:
                ln = ln + 1

            if col + ln > width:
                result.append( " " * margin + cur )
                cur = word
                col = len( word )
            else:
                if col > 0:
                    cur = cur + " "
                cur = cur + word
                col = col + ln

        if col > 0:
            result.append( " " * margin + cur )

        return result

#############################################################################
#
#  The DocField class is used to store a list containing either DocPara or
#  DocCode objects. Each DocField also has an optional "name" which is used
#  when the object corresponds to a field or value definition
#
class  DocField:

    def  __init__( self, name, lines ):
        self.name  = name  # can be None for normal paragraphs/sources
        self.items = []    # list of items

        mode_none  = 0     # start parsing mode
        mode_code  = 1     # parsing code sequences
        mode_para  = 3     # parsing normal paragraph

        margin     = -1    # current code sequence indentation
        cur_lines  = []

        # now analyze the markup lines to see if they contain paragraphs,
        # code sequences or fields definitions
        #
        start = 0
        mode  = mode_none

        for l in lines:
            # are we parsing a code sequence ?
            if mode == mode_code:
                m = re_code_end.match( l )
                if m and len( m.group( 1 ) ) <= margin:
                    # that's it, we finished the code sequence
                    code = DocCode( 0, cur_lines )
                    self.items.append( code )
                    margin    = -1
                    cur_lines = []
                    mode      = mode_none
                else:
                    # nope, continue the code sequence
                    cur_lines.append( l[margin:] )
            else:
                # start of code sequence ?
                m = re_code_start.match( l )
                if m:
                    # save current lines
                    if cur_lines:
                        para = DocPara( cur_lines )
                        self.items.append( para )
                        cur_lines = []

                    # switch to code extraction mode
                    margin = len( m.group( 1 ) )
                    mode   = mode_code
                else:
                    if not string.split( l ) and cur_lines:
                        # if the line is empty, we end the current paragraph,
                        # if any
                        para = DocPara( cur_lines )
                        self.items.append( para )
                        cur_lines = []
                    else:
                        # otherwise, simply add the line to the current
                        # paragraph
                        cur_lines.append( l )

        if mode == mode_code:
            # unexpected end of code sequence
            code = DocCode( margin, cur_lines )
            self.items.append( code )
        elif cur_lines:
            para = DocPara( cur_lines )
            self.items.append( para )

    def  dump( self, prefix = "" ):
        if self.field:
            print prefix + self.field + " ::"
            prefix = prefix + "----"

        first = 1
        for p in self.items:
            if not first:
                print ""
            p.dump( prefix )
            first = 0

    def  dump_lines( self, margin = 0, width = 60 ):
        result = []
        nl     = None

        for p in self.items:
            if nl:
                result.append( "" )

            result.extend( p.dump_lines( margin, width ) )
            nl = 1

        return result

# this regular expression is used to detect field definitions
#
re_field = re.compile( r"\s*(\w*|\w(\w|\.)*\w)\s*::" )

class  DocMarkup:

    def  __init__( self, tag, lines ):
        self.tag    = string.lower( tag )
        self.fields = []

        cur_lines = []
        field     = None
        mode      = 0

        for l in lines:
            m = re_field.match( l )
            if m:
                # we detected the start of a new field definition

                # first, save the current one
                if cur_lines:
                    f = DocField( field, cur_lines )
                    self.fields.append( f )
                    cur_lines = []
                    field     = None

                field     = m.group( 1 )   # record field name
                ln        = len( m.group( 0 ) )
                l         = " " * ln + l[ln:]
                cur_lines = [l]
            else:
                cur_lines.append( l )

        if field or cur_lines:
            f = DocField( field, cur_lines )
            self.fields.append( f )

    def  get_name( self ):
        try:
            return self.fields[0].items[0].words[0]
        except:
            return None

    def  get_start( self ):
        try:
            result = ""
            for word in self.fields[0].items[0].words:
                result = result + " " + word
            return result[1:]
        except:
            return "ERROR"

    def  dump( self, margin ):
        print " " * margin + "<" + self.tag + ">"
        for f in self.fields:
            f.dump( "  " )
        print " " * margin + "</" + self.tag + ">"

class  DocChapter:

    def  __init__( self, block ):
        self.block    = block
        self.sections = []
        if block:
            self.name  = block.name
            self.title = block.get_markup_words( "title" )
            self.order = block.get_markup_words( "sections" )
        else:
            self.name  = "Other"
            self.title = string.split( "Miscellaneous" )
            self.order = []

class  DocSection:

    def  __init__( self, name = "Other" ):
        self.name        = name
        self.blocks      = {}
        self.block_names = []  # ordered block names in section
        self.defs        = []
        self.abstract    = ""
        self.description = ""
        self.order       = []
        self.title       = "ERROR"
        self.chapter     = None

    def  add_def( self, block ):
        self.defs.append( block )

    def  add_block( self, block ):
        self.block_names.append( block.name )
        self.blocks[block.name] = block

    def  process( self ):
        # look up one block that contains a valid section description
        for block in self.defs:
            title = block.get_markup_text( "title" )
            if title:
                self.title       = title
                self.abstract    = block.get_markup_words( "abstract" )
                self.description = block.get_markup_items( "description" )
                self.order       = block.get_markup_words( "order" )
                return

    def  reorder( self ):
        self.block_names = sort_order_list( self.block_names, self.order )

class  ContentProcessor:

    def  __init__( self ):
        """initialize a block content processor"""
        self.reset()

        self.sections = {}    # dictionary of documentation sections
        self.section  = None  # current documentation section

        self.chapters = []    # list of chapters

        self.headers  = {}    # dictionary of header macros

    def  set_section( self, section_name ):
        """set current section during parsing"""
        if not self.sections.has_key( section_name ):
            section = DocSection( section_name )
            self.sections[section_name] = section
            self.section                = section
        else:
            self.section = self.sections[section_name]

    def  add_chapter( self, block ):
        chapter = DocChapter( block )
        self.chapters.append( chapter )

    def  reset( self ):
        """reset the content processor for a new block"""
        self.markups      = []
        self.markup       = None
        self.markup_lines = []

    def  add_markup( self ):
        """add a new markup section"""
        if self.markup and self.markup_lines:

            # get rid of last line of markup if it's empty
            marks = self.markup_lines
            if len( marks ) > 0 and not string.strip( marks[-1] ):
                self.markup_lines = marks[:-1]

            m = DocMarkup( self.markup, self.markup_lines )

            self.markups.append( m )

            self.markup       = None
            self.markup_lines = []

    def  process_content( self, content ):
        """process a block content and return a list of DocMarkup objects
           corresponding to it"""
        markup       = None
        markup_lines = []
        first        = 1

        for line in content:
            found = None
            for t in re_markup_tags:
                m = t.match( line )
                if m:
                    found  = string.lower( m.group( 1 ) )
                    prefix = len( m.group( 0 ) )
                    line   = " " * prefix + line[prefix:]   # remove markup from line
                    break

            # is it the start of a new markup section ?
            if found:
                first = 0
                self.add_markup()  # add current markup content
                self.markup = found
                if len( string.strip( line ) ) > 0:
                    self.markup_lines.append( line )
            elif first == 0:
                self.markup_lines.append( line )

        self.add_markup()

        return self.markups

    def  parse_sources( self, source_processor ):
        blocks = source_processor.blocks
        count  = len( blocks )

        for n in range( count ):
            source = blocks[n]
            if source.content:
                # this is a documentation comment, we need to catch
                # all following normal blocks in the "follow" list
                #
                follow = []
                m = n + 1
                while m < count and not blocks[m].content:
                    follow.append( blocks[m] )
                    m = m + 1

                doc_block = DocBlock( source, follow, self )

    def  finish( self ):
        # process all sections to extract their abstract, description
        # and ordered list of items
        #
        for sec in self.sections.values():
            sec.process()

        # process chapters to check that all sections are correctly
        # listed there
        for chap in self.chapters:
            for sec in chap.order:
                if self.sections.has_key( sec ):
                    section = self.sections[sec]
                    section.chapter = chap
                    section.reorder()
                    chap.sections.append( section )
                else:
                    sys.stderr.write( "WARNING: chapter '" +          \
                        chap.name + "' in " + chap.block.location() + \
                        " lists unknown section '" + sec + "'\n" )

        # check that all sections are in a chapter
        #
        others = []
        for sec in self.sections.values():
            if not sec.chapter:
                others.append( sec )

        # create a new special chapter for all remaining sections
        # when necessary
        #
        if others:
            chap = DocChapter( None )
            chap.sections = others
            self.chapters.append( chap )

class  DocBlock:

    def  __init__( self, source, follow, processor ):
        processor.reset()

        self.source  = source
        self.code    = []
        self.type    = "ERRTYPE"
        self.name    = "ERRNAME"
        self.section = processor.section
        self.markups = processor.process_content( source.content )

        # compute block type from first markup tag
        try:
            self.type = self.markups[0].tag
        except:
            pass

        # compute block name from first markup paragraph
        try:
            markup = self.markups[0]
            para   = markup.fields[0].items[0]
            name   = para.words[0]
            m = re_identifier.match( name )
            if m:
                name = m.group( 1 )
            self.name = name
        except:
            pass

        if self.type == "section":
            # detect new section starts
            processor.set_section( self.name )
            processor.section.add_def( self )
        elif self.type == "chapter":
            # detect new chapter
            processor.add_chapter( self )
        else:
            processor.section.add_block( self )

        # now, compute the source lines relevant to this documentation
        # block. We keep normal comments in for obvious reasons (??)
        source = []
        for b in follow:
            if b.format:
                break
            for l in b.lines:
                # collect header macro definitions
                m = re_header_macro.match( l )
                if m:
                    processor.headers[m.group( 2 )] = m.group( 1 );

                # we use "/* */" as a separator
                if re_source_sep.match( l ):
                    break
                source.append( l )

        # now strip the leading and trailing empty lines from the sources
        start = 0
        end   = len( source ) - 1

        while start < end and not string.strip( source[start] ):
            start = start + 1

        while start < end and not string.strip( source[end] ):
            end = end - 1

        if start == end and not string.strip( source[start] ):
            self.code = []
        else:
            self.code = source[start:end + 1]

    def  location( self ):
        return self.source.location()

    def  get_markup( self, tag_name ):
        """return the DocMarkup corresponding to a given tag in a block"""
        for m in self.markups:
            if m.tag == string.lower( tag_name ):
                return m
        return None

    def  get_markup_name( self, tag_name ):
        """return the name of a given primary markup in a block"""
        try:
            m = self.get_markup( tag_name )
            return m.get_name()
        except:
            return None

    def  get_markup_words( self, tag_name ):
        try:
            m = self.get_markup( tag_name )
            return m.fields[0].items[0].words
        except:
            return []

    def  get_markup_text( self, tag_name ):
        result = self.get_markup_words( tag_name )
        return string.join( result )

    def  get_markup_items( self, tag_name ):
        try:
            m = self.get_markup( tag_name )
            return m.fields[0].items
        except:
            return None

# eof


import uuid
from io import BytesIO
from lxml import etree

def create_hosted_zone_writer(connection, name, caller_reference, comment):
    """
    Forms an XML string that we'll send to Route53 in order to create
    a new hosted zone.

    :param Route53Connection connection: The connection instance used to
        query the API.
    :param str name: The name of the hosted zone to create.
    """

    if not caller_reference:
        caller_reference = str(uuid.uuid4())

    e_root = etree.Element(
        "CreateHostedZoneRequest",
        xmlns=connection._xml_namespace
    )

    e_name = etree.SubElement(e_root, "Name")
    e_name.text = name

    e_caller_reference = etree.SubElement(e_root, "CallerReference")
    e_caller_reference.text = caller_reference

    if comment:
        e_config = etree.SubElement(e_root, "HostedZoneConfig")
        e_comment = etree.SubElement(e_config, "Comment")
        e_comment.text = comment

    e_tree = etree.ElementTree(element=e_root)

    fobj = BytesIO()
    # This writes bytes.
    e_tree.write(fobj, xml_declaration=True, encoding='utf-8', method="xml")
    return fobj.getvalue().decode('utf-8')