"""
Parse four ``Accept*`` headers used in server-driven content negotiation.

The four headers are ``Accept``, ``Accept-Charset``, ``Accept-Encoding`` and
``Accept-Language``.
"""

from collections import namedtuple
import re
import textwrap
import warnings


# RFC 7230 Section 3.2.3 "Whitespace"
# OWS            = *( SP / HTAB )
#                ; optional whitespace
OWS_re = '[ \t]*'

# RFC 7230 Section 3.2.6 "Field Value Components":
# tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
#                / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
#                / DIGIT / ALPHA
tchar_re = r"[!#$%&'*+\-.^_`|~0-9A-Za-z]"

# token          = 1*tchar
token_re = tchar_re + '+'
token_compiled_re = re.compile('^' + token_re + '$')

# RFC 7231 Section 5.3.1 "Quality Values"
# qvalue = ( "0" [ "." 0*3DIGIT ] )
#        / ( "1" [ "." 0*3("0") ] )
qvalue_re = (
    r'(?:0(?:\.[0-9]{0,3})?)'
    '|'
    r'(?:1(?:\.0{0,3})?)'
)
# weight = OWS ";" OWS "q=" qvalue
weight_re = OWS_re + ';' + OWS_re + '[qQ]=(' + qvalue_re + ')'


def _item_n_weight_re(item_re):
    return '(' + item_re + ')(?:' + weight_re + ')?'


def _item_qvalue_pair_to_header_element(pair):
    item, qvalue = pair
    if qvalue == 1.0:
        element = item
    elif qvalue == 0.0:
        element = '{};q=0'.format(item)
    else:
        element = '{};q={}'.format(item, qvalue)
    return element


def _list_0_or_more__compiled_re(element_re):
    # RFC 7230 Section 7 "ABNF List Extension: #rule":
    # #element => [ ( "," / element ) *( OWS "," [ OWS element ] ) ]
    return re.compile(
        '^(?:$)|' +
        '(?:' +
        '(?:,|(?:' + element_re + '))' +
        '(?:' + OWS_re + ',(?:' + OWS_re + element_re + ')?)*' +
        ')$',
    )


def _list_1_or_more__compiled_re(element_re):
    # RFC 7230 Section 7 "ABNF List Extension: #rule":
    # 1#element => *( "," OWS ) element *( OWS "," [ OWS element ] )
    # and RFC 7230 Errata ID: 4169
    return re.compile(
        '^(?:,' + OWS_re + ')*' + element_re +
        '(?:' + OWS_re + ',(?:' + OWS_re + element_re + ')?)*$',
    )


class AcceptOffer(namedtuple('AcceptOffer', ['type', 'subtype', 'params'])):
    """
    A pre-parsed offer tuple represeting a value in the format
    ``type/subtype;param0=value0;param1=value1``.

    :ivar type: The media type's root category.
    :ivar subtype: The media type's subtype.
    :ivar params: A tuple of 2-tuples containing parameter names and values.

    """
    __slots__ = ()

    def __str__(self):
        """
        Return the properly quoted media type string.

        """
        value = self.type + '/' + self.subtype
        return Accept._form_media_range(value, self.params)


class Accept(object):
    """
    Represent an ``Accept`` header.

    Base class for :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, and
    :class:`AcceptInvalidHeader`.
    """

    # RFC 6838 describes syntax rules for media types that are different to
    # (and stricter than) those in RFC 7231, but if RFC 7231 intended us to
    # follow the rules in RFC 6838 for media ranges, it would not have
    # specified its own syntax rules for media ranges, so it appears we should
    # use the rules in RFC 7231 for now.

    # RFC 5234 Appendix B.1 "Core Rules":
    # VCHAR         =  %x21-7E
    #                       ; visible (printing) characters
    vchar_re = '\x21-\x7e'
    # RFC 7230 Section 3.2.6 "Field Value Components":
    # quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
    # qdtext        = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
    # obs-text      = %x80-FF
    # quoted-pair   = "\" ( HTAB / SP / VCHAR / obs-text )
    obs_text_re = '\x80-\xff'
    qdtext_re = '[\t \x21\x23-\x5b\\\x5d-\x7e' + obs_text_re + ']'
    # The '\\' between \x5b and \x5d is needed to escape \x5d (']')
    quoted_pair_re = r'\\' + '[\t ' + vchar_re + obs_text_re + ']'
    quoted_string_re = \
        '"(?:(?:' + qdtext_re + ')|(?:' + quoted_pair_re + '))*"'

    # RFC 7231 Section 3.1.1.1 "Media Type":
    # type       = token
    # subtype    = token
    # parameter  = token "=" ( token / quoted-string )
    type_re = token_re
    subtype_re = token_re
    parameter_re = token_re + '=' + \
        '(?:(?:' + token_re + ')|(?:' + quoted_string_re + '))'

    # Section 5.3.2 "Accept":
    # media-range    = ( "*/*"
    #                  / ( type "/" "*" )
    #                  / ( type "/" subtype )
    #                  ) *( OWS ";" OWS parameter )
    media_range_re = (
        '(' +
        '(?:' + type_re + '/' + subtype_re + ')' +
        # '*' is included through type_re and subtype_re, so this covers */*
        # and type/*
        ')' +
        '(' +
        '(?:' + OWS_re + ';' + OWS_re +
        '(?![qQ]=)' +  # media type parameter cannot be named "q"
        parameter_re + ')*' +
        ')'
    )
    # accept-params  = weight *( accept-ext )
    # accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ]
    accept_ext_re = (
        OWS_re + ';' + OWS_re + token_re + '(?:' +
        '=(?:' +
        '(?:' + token_re + ')|(?:' + quoted_string_re + ')' +
        ')' +
        ')?'
    )
    accept_params_re = weight_re + '((?:' + accept_ext_re + ')*)'

    media_range_n_accept_params_re = media_range_re + '(?:' + \
        accept_params_re + ')?'
    media_range_n_accept_params_compiled_re = re.compile(
        media_range_n_accept_params_re,
    )

    accept_compiled_re = _list_0_or_more__compiled_re(
        element_re=media_range_n_accept_params_re,
    )

    # For parsing repeated groups within the media type parameters and
    # extension parameters segments
    parameters_compiled_re = re.compile(
        OWS_re + ';' + OWS_re + '(' + token_re + ')=(' + token_re + '|' +
        quoted_string_re + ')',
    )
    accept_ext_compiled_re = re.compile(
        OWS_re + ';' + OWS_re + '(' + token_re + ')' +
        '(?:' +
        '=(' +
        '(?:' +
        '(?:' + token_re + ')|(?:' + quoted_string_re + ')' +
        ')' +
        ')' +
        ')?',
    )

    # For parsing the media types in the `offers` argument to
    # .acceptable_offers(), we re-use the media range regex for media types.
    # This is not intended to be a validation of the offers; its main purpose
    # is to extract the media type and any media type parameters.
    media_type_re = media_range_re
    media_type_compiled_re = re.compile('^' + media_type_re + '$')

    @classmethod
    def _escape_and_quote_parameter_value(cls, param_value):
        """
        Escape and quote parameter value where necessary.

        For media type and extension parameter values.
        """
        if param_value == '':
            param_value = '""'
        else:
            param_value = param_value.replace('\\', '\\\\').replace(
                '"', r'\"',
            )
            if not token_compiled_re.match(param_value):
                param_value = '"' + param_value + '"'
        return param_value

    @classmethod
    def _form_extension_params_segment(cls, extension_params):
        """
        Convert iterable of extension parameters to str segment for header.

        `extension_params` is an iterable where each item is either a parameter
        string or a (name, value) tuple.
        """
        extension_params_segment = ''
        for item in extension_params:
            try:
                extension_params_segment += (';' + item)
            except TypeError:
                param_name, param_value = item
                param_value = cls._escape_and_quote_parameter_value(
                    param_value=param_value,
                )
                extension_params_segment += (
                    ';' + param_name + '=' + param_value
                )
        return extension_params_segment

    @classmethod
    def _form_media_range(cls, type_subtype, media_type_params):
        """
        Combine `type_subtype` and `media_type_params` to form a media range.

        `type_subtype` is a ``str``, and `media_type_params` is an iterable of
        (parameter name, parameter value) tuples.
        """
        media_type_params_segment = ''
        for param_name, param_value in media_type_params:
            param_value = cls._escape_and_quote_parameter_value(
                param_value=param_value,
            )
            media_type_params_segment += (';' + param_name + '=' + param_value)
        return type_subtype + media_type_params_segment

    @classmethod
    def _iterable_to_header_element(cls, iterable):
        """
        Convert iterable of tuples into header element ``str``.

        Each tuple is expected to be in one of two forms: (media_range, qvalue,
        extension_params_segment), or (media_range, qvalue).
        """
        try:
            media_range, qvalue, extension_params_segment = iterable
        except ValueError:
            media_range, qvalue = iterable
            extension_params_segment = ''

        if qvalue == 1.0:
            if extension_params_segment:
                element = '{};q=1{}'.format(
                    media_range, extension_params_segment,
                )
            else:
                element = media_range
        elif qvalue == 0.0:
            element = '{};q=0{}'.format(media_range, extension_params_segment)
        else:
            element = '{};q={}{}'.format(
                media_range, qvalue, extension_params_segment,
            )
        return element

    @classmethod
    def _parse_media_type_params(cls, media_type_params_segment):
        """
        Parse media type parameters segment into list of (name, value) tuples.
        """
        media_type_params = cls.parameters_compiled_re.findall(
            media_type_params_segment,
        )
        for index, (name, value) in enumerate(media_type_params):
            if value.startswith('"') and value.endswith('"'):
                value = cls._process_quoted_string_token(token=value)
                media_type_params[index] = (name, value)
        return media_type_params

    @classmethod
    def _process_quoted_string_token(cls, token):
        """
        Return unescaped and unquoted value from quoted token.
        """
        # RFC 7230, section 3.2.6 "Field Value Components": "Recipients that
        # process the value of a quoted-string MUST handle a quoted-pair as if
        # it were replaced by the octet following the backslash."
        return re.sub(r'\\(?![\\])', '', token[1:-1]).replace('\\\\', '\\')

    @classmethod
    def _python_value_to_header_str(cls, value):
        """
        Convert Python value to header string for __add__/__radd__.
        """
        if isinstance(value, str):
            return value
        if hasattr(value, 'items'):
            if value == {}:
                value = []
            else:
                value_list = []
                for media_range, item in value.items():
                    # item is either (media range, (qvalue, extension
                    # parameters segment)), or (media range, qvalue) (supported
                    # for backward compatibility)
                    if isinstance(item, (float, int)):
                        value_list.append((media_range, item, ''))
                    else:
                        value_list.append((media_range, item[0], item[1]))
                value = sorted(
                    value_list,
                    key=lambda item: item[1],  # qvalue
                    reverse=True,
                )
        if isinstance(value, (tuple, list)):
            header_elements = []
            for item in value:
                if isinstance(item, (tuple, list)):
                    item = cls._iterable_to_header_element(iterable=item)
                header_elements.append(item)
            header_str = ', '.join(header_elements)
        else:
            header_str = str(value)
        return header_str

    @classmethod
    def parse(cls, value):
        """
        Parse an ``Accept`` header.

        :param value: (``str``) header value
        :return: If `value` is a valid ``Accept`` header, returns an iterator
                 of (*media_range*, *qvalue*, *media_type_params*,
                 *extension_params*) tuples, as parsed from the header from
                 left to right.

                 | *media_range* is the media range, including any media type
                   parameters. The media range is returned in a canonicalised
                   form (except the case of the characters are unchanged):
                   unnecessary spaces around the semicolons before media type
                   parameters are removed; the parameter values are returned in
                   a form where only the '``\\``' and '``"``' characters are
                   escaped, and the values are quoted with double quotes only
                   if they need to be quoted.

                 | *qvalue* is the quality value of the media range.

                 | *media_type_params* is the media type parameters, as a list
                   of (parameter name, value) tuples.

                 | *extension_params* is the extension parameters, as a list
                   where each item is either a parameter string or a (parameter
                   name, value) tuple.
        :raises ValueError: if `value` is an invalid header
        """
        # Check if header is valid
        # Using Python stdlib's `re` module, there is currently no way to check
        # the match *and* get all the groups using the same regex, so we have
        # to do this in steps using multiple regexes.
        if cls.accept_compiled_re.match(value) is None:
            raise ValueError('Invalid value for an Accept header.')
        def generator(value):
            for match in (
                cls.media_range_n_accept_params_compiled_re.finditer(value)
            ):
                groups = match.groups()

                type_subtype = groups[0]

                media_type_params = cls._parse_media_type_params(
                    media_type_params_segment=groups[1],
                )

                media_range = cls._form_media_range(
                    type_subtype=type_subtype,
                    media_type_params=media_type_params,
                )

                # qvalue (groups[2]) and extension_params (groups[3]) are both
                # None if neither qvalue or extension parameters are found in
                # the match.

                qvalue = groups[2]
                qvalue = float(qvalue) if qvalue else 1.0

                extension_params = groups[3]
                if extension_params:
                    extension_params = cls.accept_ext_compiled_re.findall(
                        extension_params,
                    )
                    for index, (token_key, token_value) in enumerate(
                        extension_params
                    ):
                        if token_value:
                            if (
                                token_value.startswith('"') and
                                token_value.endswith('"')
                            ):
                                token_value = cls._process_quoted_string_token(
                                    token=token_value,
                                )
                                extension_params[index] = (
                                    token_key, token_value,
                                )
                        else:
                            extension_params[index] = token_key
                else:
                    extension_params = []

                yield (
                    media_range, qvalue, media_type_params, extension_params,
                )
        return generator(value=value)

    @classmethod
    def parse_offer(cls, offer):
        """
        Parse an offer into its component parts.

        :param offer: A media type or range in the format
                      ``type/subtype[;params]``.
        :return: A named tuple containing ``(*type*, *subtype*, *params*)``.

                 | *params* is a list containing ``(*parameter name*, *value*)``
                   values.

        :raises ValueError: If the offer does not match the required format.

        """
        if isinstance(offer, AcceptOffer):
            return offer
        match = cls.media_type_compiled_re.match(offer)
        if not match:
            raise ValueError('Invalid value for an Accept offer.')

        groups = match.groups()
        offer_type, offer_subtype = groups[0].split('/')
        offer_params = cls._parse_media_type_params(
            media_type_params_segment=groups[1],
        )
        if offer_type == '*' or offer_subtype == '*':
            raise ValueError('Invalid value for an Accept offer.')
        return AcceptOffer(
            offer_type.lower(),
            offer_subtype.lower(),
            tuple((name.lower(), value) for name, value in offer_params),
        )

    @classmethod
    def _parse_and_normalize_offers(cls, offers):
        """
        Throw out any offers that do not match the media range ABNF.

        :return: A list of offers split into the format ``[offer_index,
                 parsed_offer]``.

        """
        parsed_offers = []
        for index, offer in enumerate(offers):
            try:
                parsed_offer = cls.parse_offer(offer)
            except ValueError:
                continue
            parsed_offers.append([index, parsed_offer])
        return parsed_offers


class AcceptValidHeader(Accept):
    """
    Represent a valid ``Accept`` header.

    A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.2
    <7231#section-5.3.2>`.

    This object should not be modified. To add to the header, we can use the
    addition operators (``+`` and ``+=``), which return a new object (see the
    docstring for :meth:`AcceptValidHeader.__add__`).
    """

    @property
    def header_value(self):
        """(``str`` or ``None``) The header value."""
        return self._header_value

    @property
    def parsed(self):
        """
        (``list`` or ``None``) Parsed form of the header.

        A list of (*media_range*, *qvalue*, *media_type_params*,
        *extension_params*) tuples, where

        *media_range* is the media range, including any media type parameters.
        The media range is returned in a canonicalised form (except the case of
        the characters are unchanged): unnecessary spaces around the semicolons
        before media type parameters are removed; the parameter values are
        returned in a form where only the '``\\``' and '``"``' characters are
        escaped, and the values are quoted with double quotes only if they need
        to be quoted.

        *qvalue* is the quality value of the media range.

        *media_type_params* is the media type parameters, as a list of
        (parameter name, value) tuples.

        *extension_params* is the extension parameters, as a list where each
        item is either a parameter string or a (parameter name, value) tuple.
        """
        return self._parsed

    def __init__(self, header_value):
        """
        Create an :class:`AcceptValidHeader` instance.

        :param header_value: (``str``) header value.
        :raises ValueError: if `header_value` is an invalid value for an
                            ``Accept`` header.
        """
        self._header_value = header_value
        self._parsed = list(self.parse(header_value))
        self._parsed_nonzero = [item for item in self.parsed if item[1]]
        # item[1] is the qvalue

    def copy(self):
        """
        Create a copy of the header object.

        """
        return self.__class__(self._header_value)

    def __add__(self, other):
        """
        Add to header, creating a new header object.

        `other` can be:

        * ``None``
        * a ``str`` header value
        * a ``dict``, with media ranges ``str``'s (including any media type
          parameters) as keys, and either qvalues ``float``'s or (*qvalues*,
          *extension_params*) tuples as values, where *extension_params* is a
          ``str`` of the extension parameters segment of the header element,
          starting with the first '``;``'
        * a ``tuple`` or ``list``, where each item is either a header element
          ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple``
          or ``list`` where *media_range* is a ``str`` of the media range
          including any media type parameters, and *extension_params* is a
          ``str`` of the extension parameters segment of the header element,
          starting with the first '``;``'
        * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or
          :class:`AcceptInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``

        If `other` is a valid header value or another
        :class:`AcceptValidHeader` instance, and the header value it represents
        is not `''`, then the two header values are joined with ``', '``, and a
        new :class:`AcceptValidHeader` instance with the new header value is
        returned.

        If `other` is a valid header value or another
        :class:`AcceptValidHeader` instance representing a header value of
        `''`; or if it is ``None`` or an :class:`AcceptNoHeader` instance; or
        if it is an invalid header value, or an :class:`AcceptInvalidHeader`
        instance, then a new :class:`AcceptValidHeader` instance with the same
        header value as ``self`` is returned.
        """
        if isinstance(other, AcceptValidHeader):
            if other.header_value == '':
                return self.__class__(header_value=self.header_value)
            else:
                return create_accept_header(
                    header_value=self.header_value + ', ' + other.header_value,
                )

        if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)):
            return self.__class__(header_value=self.header_value)

        return self._add_instance_and_non_accept_type(
            instance=self, other=other,
        )

    def __bool__(self):
        """
        Return whether ``self`` represents a valid ``Accept`` header.

        Return ``True`` if ``self`` represents a valid header, and ``False`` if
        it represents an invalid header, or the header not being in the
        request.

        For this class, it always returns ``True``.
        """
        return True
    __nonzero__ = __bool__  # Python 2

    def __contains__(self, offer):
        """
        Return ``bool`` indicating whether `offer` is acceptable.

        .. warning::

           The behavior of :meth:`AcceptValidHeader.__contains__` is currently
           being maintained for backward compatibility, but it will change in
           the future to better conform to the RFC.

        :param offer: (``str``) media type offer
        :return: (``bool``) Whether ``offer`` is acceptable according to the
                 header.

        This uses the old criterion of a match in
        :meth:`AcceptValidHeader._old_match`, which is not as specified in
        :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not
        correctly take into account media type parameters:

            >>> 'text/html;p=1' in AcceptValidHeader('text/html')
            False

        or media ranges with ``q=0`` in the header::

            >>> 'text/html' in AcceptValidHeader('text/*, text/html;q=0')
            True
            >>> 'text/html' in AcceptValidHeader('text/html;q=0, */*')
            True

        (See the docstring for :meth:`AcceptValidHeader._old_match` for other
        problems with the old criterion for matching.)
        """
        warnings.warn(
            'The behavior of AcceptValidHeader.__contains__ is '
            'currently being maintained for backward compatibility, but it '
            'will change in the future to better conform to the RFC.',
            DeprecationWarning,
        )
        for (
            media_range, quality, media_type_params, extension_params
        ) in self._parsed_nonzero:
            if self._old_match(media_range, offer):
                return True
        return False

    def __iter__(self):
        """
        Return all the ranges with non-0 qvalues, in order of preference.

        .. warning::

           The behavior of this method is currently maintained for backward
           compatibility, but will change in the future.

        :return: iterator of all the media ranges in the header with non-0
                 qvalues, in descending order of qvalue. If two ranges have the
                 same qvalue, they are returned in the order of their positions
                 in the header, from left to right.

        Please note that this is a simple filter for the ranges in the header
        with non-0 qvalues, and is not necessarily the same as what the client
        prefers, e.g. ``'audio/basic;q=0, */*'`` means 'everything but
        audio/basic', but ``list(instance)`` would return only ``['*/*']``.
        """
        warnings.warn(
            'The behavior of AcceptLanguageValidHeader.__iter__ is currently '
            'maintained for backward compatibility, but will change in the '
            'future.',
            DeprecationWarning,
        )

        for media_range, qvalue, media_type_params, extension_params in sorted(
            self._parsed_nonzero,
            key=lambda i: i[1],
            reverse=True
        ):
            yield media_range

    def __radd__(self, other):
        """
        Add to header, creating a new header object.

        See the docstring for :meth:`AcceptValidHeader.__add__`.
        """
        return self._add_instance_and_non_accept_type(
            instance=self, other=other, instance_on_the_right=True,
        )

    def __repr__(self):
        return '<{} ({!r})>'.format(self.__class__.__name__, str(self))

    def __str__(self):
        r"""
        Return a tidied up version of the header value.

        e.g. If ``self.header_value`` is ``r',,text/html ; p1="\"\1\"" ;
        q=0.50; e1=1 ;e2  ,  text/plain ,'``, ``str(instance)`` returns
        ``r'text/html;p1="\"1\"";q=0.5;e1=1;e2, text/plain'``.
        """
        # self.parsed tuples are in the form: (media_range, qvalue,
        # media_type_params, extension_params)
        # self._iterable_to_header_element() requires iterable to be in the
        # form: (media_range, qvalue, extension_params_segment).
        return ', '.join(
            self._iterable_to_header_element(
                iterable=(
                    tuple_[0],  # media_range
                    tuple_[1],  # qvalue
                    self._form_extension_params_segment(
                        extension_params=tuple_[3],  # extension_params
                    )
                ),
            ) for tuple_ in self.parsed
        )

    def _add_instance_and_non_accept_type(
        self, instance, other, instance_on_the_right=False,
    ):
        if not other:
            return self.__class__(header_value=instance.header_value)

        other_header_value = self._python_value_to_header_str(value=other)

        if other_header_value == '':
            # if ``other`` is an object whose type we don't recognise, and
            # str(other) returns ''
            return self.__class__(header_value=instance.header_value)

        try:
            self.parse(value=other_header_value)
        except ValueError:  # invalid header value
            return self.__class__(header_value=instance.header_value)

        new_header_value = (
            (other_header_value + ', ' + instance.header_value)
            if instance_on_the_right
            else (instance.header_value + ', ' + other_header_value)
        )
        return self.__class__(header_value=new_header_value)

    def _old_match(self, mask, offer):
        """
        Check if the offer is covered by the mask

        ``offer`` may contain wildcards to facilitate checking if a ``mask``
        would match a 'permissive' offer.

        Wildcard matching forces the match to take place against the type or
        subtype of the mask and offer (depending on where the wildcard matches)

        .. warning::

           This is maintained for backward compatibility, and will be
           deprecated in the future.

        This method was WebOb's old criterion for deciding whether a media type
        matches a media range, used in

        - :meth:`AcceptValidHeader.__contains__`
        - :meth:`AcceptValidHeader.best_match`
        - :meth:`AcceptValidHeader.quality`

        It allows offers of *, */*, type/*, */subtype and types with no
        subtypes, which are not media types as specified in :rfc:`RFC 7231,
        section 5.3.2 <7231#section-5.3.2>`. This is also undocumented in any
        of the public APIs that uses this method.
        """
        # Match if comparisons are the same or either is a complete wildcard
        if (mask.lower() == offer.lower() or
                '*/*' in (mask, offer) or
                '*' == offer):
            return True

        # Set mask type with wildcard subtype for malformed masks
        try:
            mask_type, mask_subtype = [x.lower() for x in mask.split('/')]
        except ValueError:
            mask_type = mask
            mask_subtype = '*'

        # Set offer type with wildcard subtype for malformed offers
        try:
            offer_type, offer_subtype = [x.lower() for x in offer.split('/')]
        except ValueError:
            offer_type = offer
            offer_subtype = '*'

        if mask_subtype == '*':
            # match on type only
            if offer_type == '*':
                return True
            else:
                return mask_type.lower() == offer_type.lower()

        if mask_type == '*':
            # match on subtype only
            if offer_subtype == '*':
                return True
            else:
                return mask_subtype.lower() == offer_subtype.lower()

        if offer_subtype == '*':
            # match on type only
            return mask_type.lower() == offer_type.lower()

        if offer_type == '*':
            # match on subtype only
            return mask_subtype.lower() == offer_subtype.lower()

        return offer.lower() == mask.lower()

    def accept_html(self):
        """
        Return ``True`` if any HTML-like type is accepted.

        The HTML-like types are 'text/html', 'application/xhtml+xml',
        'application/xml' and 'text/xml'.
        """
        return bool(
            self.acceptable_offers(
                offers=[
                    'text/html',
                    'application/xhtml+xml',
                    'application/xml',
                    'text/xml',
                ],
            )
        )
    accepts_html = property(fget=accept_html, doc=accept_html.__doc__)
    # note the plural

    def acceptable_offers(self, offers):
        """
        Return the offers that are acceptable according to the header.

        The offers are returned in descending order of preference, where
        preference is indicated by the qvalue of the media range in the header
        that best matches the offer.

        This uses the matching rules described in :rfc:`RFC 7231, section 5.3.2
        <7231#section-5.3.2>`.

        Any offers that cannot be parsed via
        :meth:`.Accept.parse_offer` will be ignored.

        :param offers: ``iterable`` of ``str`` media types (media types can
                       include media type parameters) or pre-parsed instances
                       of :class:`.AcceptOffer`.
        :return: A list of tuples of the form (media type, qvalue), in
                 descending order of qvalue. Where two offers have the same
                 qvalue, they are returned in the same order as their order in
                 `offers`.
        """
        parsed = self.parsed

        # RFC 7231, section 3.1.1.1 "Media Type":
        # "The type, subtype, and parameter name tokens are case-insensitive.
        # Parameter values might or might not be case-sensitive, depending on
        # the semantics of the parameter name."
        lowercased_ranges = [
            (
                media_range.partition(';')[0].lower(),
                qvalue,
                tuple(
                    (name.lower(), value)
                    for name, value in media_type_params
                ),
            )
            for media_range, qvalue, media_type_params, __ in
            parsed
        ]
        lowercased_offers_parsed = self._parse_and_normalize_offers(offers)

        acceptable_offers_n_quality_factors = {}
        for offer_index, parsed_offer in lowercased_offers_parsed:
            offer = offers[offer_index]
            offer_type, offer_subtype, offer_media_type_params = parsed_offer
            for (
                range_type_subtype, range_qvalue, range_media_type_params,
            ) in lowercased_ranges:
                range_type, range_subtype = range_type_subtype.split('/', 1)

                # The specificity values below are based on the list in the
                # example in RFC 7231 section 5.3.2 explaining how "media
                # ranges can be overridden by more specific media ranges or
                # specific media types". We assign specificity to the list
                # items in reverse order, so specificity 4, 3, 2, 1 correspond
                # to 1, 2, 3, 4 in the list, respectively (so that higher
                # specificity has higher precedence).
                if (
                    offer_type == range_type
                    and offer_subtype == range_subtype
                ):
                    if range_media_type_params == ():
                        # If offer_media_type_params == () the offer and the
                        # range match exactly, with neither having media type
                        # parameters.
                        # If offer_media_type_params is not (), the offer and
                        # the range are a match. See the table towards the end
                        # of RFC 7231 section 5.3.2, where the media type
                        # 'text/html;level=3' matches the range 'text/html' in
                        # the header.
                        # Both cases are a match with a specificity of 3.
                        specificity = 3
                    elif offer_media_type_params == range_media_type_params:
                        specificity = 4
                    else:  # pragma: no cover
                        # no cover because of
                        # https://bitbucket.org/ned/coveragepy/issues/254/incorrect-coverage-on-continue-statement
                        continue
                else:
                    if range_subtype == '*' and offer_type == range_type:
                        specificity = 2
                    elif range_type_subtype == '*/*':
                        specificity = 1
                    else:  # pragma: no cover
                        # no cover because of
                        # https://bitbucket.org/ned/coveragepy/issues/254/incorrect-coverage-on-continue-statement
                        continue
                try:
                    if specificity <= (
                        acceptable_offers_n_quality_factors[offer][2]
                    ):
                        continue
                except KeyError:
                    # the entry for the offer is not already in
                    # acceptable_offers_n_quality_factors
                    pass
                acceptable_offers_n_quality_factors[offer] = (
                    range_qvalue,  # qvalue of matched range
                    offer_index,
                    specificity,  # specifity of matched range
                )

        acceptable_offers_n_quality_factors = [
            # key is offer, value[0] is qvalue, value[1] is offer_index
            (key, value[0], value[1])
            for key, value in acceptable_offers_n_quality_factors.items()
            if value[0]  # != 0.0
            # We have to filter out the offers with qvalues of 0 here instead
            # of just skipping them early in the large ``for`` loop because
            # that would not work for e.g. when the header is 'text/html;q=0,
            # text/html' (which does not make sense, but is nonetheless valid),
            # and offers is ['text/html']
        ]
        # sort by offer_index, ascending
        acceptable_offers_n_quality_factors.sort(key=lambda tuple_: tuple_[2])
        # (stable) sort by qvalue, descending
        acceptable_offers_n_quality_factors.sort(
            key=lambda tuple_: tuple_[1], reverse=True,
        )
        # drop offer_index
        acceptable_offers_n_quality_factors = [
            (item[0], item[1]) for item in acceptable_offers_n_quality_factors
        ]
        return acceptable_offers_n_quality_factors
        # If a media range is repeated in the header (which would not make
        # sense, but would be valid according to the rules in the RFC), an
        # offer for which the media range is the most specific match would take
        # its qvalue from the first appearance of the range in the header.

    def best_match(self, offers, default_match=None):
        """
        Return the best match from the sequence of media type `offers`.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future.

           :meth:`AcceptValidHeader.best_match` uses its own algorithm (one not
           specified in :rfc:`RFC 7231 <7231>`) to determine what is a best
           match. The algorithm has many issues, and does not conform to
           :rfc:`RFC 7231 <7231>`.

        Each media type in `offers` is checked against each non-``q=0`` range
        in the header. If the two are a match according to WebOb's old
        criterion for a match, the quality value of the match is the qvalue of
        the media range from the header multiplied by the server quality value
        of the offer (if the server quality value is not supplied, it is 1).

        The offer in the match with the highest quality value is the best
        match. If there is more than one match with the highest qvalue, the
        match where the media range has a lower number of '*'s is the best
        match. If the two have the same number of '*'s, the one that shows up
        first in `offers` is the best match.

        :param offers: (iterable)

                       | Each item in the iterable may be a ``str`` media type,
                         or a (media type, server quality value) ``tuple`` or
                         ``list``. (The two may be mixed in the iterable.)

        :param default_match: (optional, any type) the value to be returned if
                              there is no match

        :return: (``str``, or the type of `default_match`)

                 | The offer that is the best match. If there is no match, the
                   value of `default_match` is returned.

        This uses the old criterion of a match in
        :meth:`AcceptValidHeader._old_match`, which is not as specified in
        :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not
        correctly take into account media type parameters:

            >>> instance = AcceptValidHeader('text/html')
            >>> instance.best_match(offers=['text/html;p=1']) is None
            True

        or media ranges with ``q=0`` in the header::

            >>> instance = AcceptValidHeader('text/*, text/html;q=0')
            >>> instance.best_match(offers=['text/html'])
            'text/html'

            >>> instance = AcceptValidHeader('text/html;q=0, */*')
            >>> instance.best_match(offers=['text/html'])
            'text/html'

        (See the docstring for :meth:`AcceptValidHeader._old_match` for other
        problems with the old criterion for matching.)

        Another issue is that this method considers the best matching range for
        an offer to be the matching range with the highest quality value,
        (where quality values are tied, the most specific media range is
        chosen); whereas :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`
        specifies that we should consider the best matching range for a media
        type offer to be the most specific matching range.::

            >>> instance = AcceptValidHeader('text/html;q=0.5, text/*')
            >>> instance.best_match(offers=['text/html', 'text/plain'])
            'text/html'
        """
        warnings.warn(
            'The behavior of AcceptValidHeader.best_match is currently being '
            'maintained for backward compatibility, but it will be deprecated'
            ' in the future, as it does not conform to the RFC.',
            DeprecationWarning,
        )
        best_quality = -1
        best_offer = default_match
        matched_by = '*/*'
        for offer in offers:
            if isinstance(offer, (tuple, list)):
                offer, server_quality = offer
            else:
                server_quality = 1
            for item in self._parsed_nonzero:
                mask = item[0]
                quality = item[1]
                possible_quality = server_quality * quality
                if possible_quality < best_quality:
                    continue
                elif possible_quality == best_quality:
                    # 'text/plain' overrides 'message/*' overrides '*/*'
                    # (if all match w/ the same q=)
                    if matched_by.count('*') <= mask.count('*'):
                        continue
                if self._old_match(mask, offer):
                    best_quality = possible_quality
                    best_offer = offer
                    matched_by = mask
        return best_offer

    def quality(self, offer):
        """
        Return quality value of given offer, or ``None`` if there is no match.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future.

        :param offer: (``str``) media type offer
        :return: (``float`` or ``None``)

                 | The highest quality value from the media range(s) that match
                   the `offer`, or ``None`` if there is no match.

        This uses the old criterion of a match in
        :meth:`AcceptValidHeader._old_match`, which is not as specified in
        :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`. It does not
        correctly take into account media type parameters:

            >>> instance = AcceptValidHeader('text/html')
            >>> instance.quality('text/html;p=1') is None
            True

        or media ranges with ``q=0`` in the header::

            >>> instance = AcceptValidHeader('text/*, text/html;q=0')
            >>> instance.quality('text/html')
            1.0
            >>> AcceptValidHeader('text/html;q=0, */*').quality('text/html')
            1.0

        (See the docstring for :meth:`AcceptValidHeader._old_match` for other
        problems with the old criterion for matching.)

        Another issue is that this method considers the best matching range for
        an offer to be the matching range with the highest quality value,
        whereas :rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>` specifies
        that we should consider the best matching range for a media type offer
        to be the most specific matching range.::

            >>> instance = AcceptValidHeader('text/html;q=0.5, text/*')
            >>> instance.quality('text/html')
            1.0
        """
        warnings.warn(
            'The behavior of AcceptValidHeader.quality is currently being '
            'maintained for backward compatibility, but it will be deprecated '
            'in the future, as it does not conform to the RFC.',
            DeprecationWarning,
        )
        bestq = 0
        for item in self.parsed:
            media_range = item[0]
            qvalue = item[1]
            if self._old_match(media_range, offer):
                bestq = max(bestq, qvalue)
        return bestq or None


class MIMEAccept(Accept):
    """
    Backwards compatibility shim for the new functionality provided by
    AcceptValidHeader, AcceptInvalidHeader, or AcceptNoHeader, that acts like
    the old MIMEAccept from WebOb version 1.7 or lower.

    This shim does use the newer Accept header parsing, which will mean your
    application may be less liberal in what Accept headers are correctly
    parsed. It is recommended that user agents be updated to send appropriate
    Accept headers that are valid according to rfc:`RFC 7231, section 5.3.2
    <7231#section-5.3.2>`

    .. deprecated:: 1.8

       Instead of directly creating the Accept object, please see:
       :func:`create_accept_header(header_value)
       <webob.acceptparse.create_accept_header>`, which will create the
       appropriate object.

       This shim has an extended deprecation period to allow for application
       developers to switch the to new API.

    """

    def __init__(self, header_value):
        warnings.warn(
            'The MIMEAccept class has been replaced by '
            'webob.acceptparse.create_accept_header. This compatibility shim '
            'will be deprecated in a future version of WebOb.',
            DeprecationWarning
        )
        self._accept = create_accept_header(header_value)
        if self._accept.parsed:
            self._parsed = [(media, q) for (media, q, _, _) in self._accept.parsed]
            self._parsed_nonzero = [(m, q) for (m, q) in self._parsed if q]
        else:
            self._parsed = []
            self._parsed_nonzero = []

    @staticmethod
    def parse(value):
        try:
            parsed_accepted = Accept.parse(value)

            for (media, q, _, _) in parsed_accepted:
                yield (media, q)
        except ValueError:
            pass

    def __repr__(self):
        return self._accept.__repr__()

    def __iter__(self):
        return self._accept.__iter__()

    def __str__(self):
        return self._accept.__str__()

    def __add__(self, other):
        if isinstance(other, self.__class__):
            return self.__class__(str(self._accept.__add__(other._accept)))
        else:
            return self.__class__(str(self._accept.__add__(other)))

    def __radd__(self, other):
        return self.__class__(str(self._accept.__radd__(other)))

    def __contains__(self, offer):
        return offer in self._accept

    def quality(self, offer):
        return self._accept.quality(offer)

    def best_match(self, offers, default_match=None):
        return self._accept.best_match(offers, default_match=default_match)

    def accept_html(self):
        return self._accept.accept_html()


class _AcceptInvalidOrNoHeader(Accept):
    """
    Represent when an ``Accept`` header is invalid or not in request.

    This is the base class for the behaviour that :class:`.AcceptInvalidHeader`
    and :class:`.AcceptNoHeader` have in common.

    :rfc:`7231` does not provide any guidance on what should happen if the
    ``Accept`` header has an invalid value. This implementation disregards the
    header when the header is invalid, so :class:`.AcceptInvalidHeader` and
    :class:`.AcceptNoHeader` have much behaviour in common.
    """

    def __bool__(self):
        """
        Return whether ``self`` represents a valid ``Accept`` header.

        Return ``True`` if ``self`` represents a valid header, and ``False`` if
        it represents an invalid header, or the header not being in the
        request.

        For this class, it always returns ``False``.
        """
        return False
    __nonzero__ = __bool__  # Python 2

    def __contains__(self, offer):
        """
        Return ``bool`` indicating whether `offer` is acceptable.

        .. warning::

           The behavior of ``.__contains__`` for the ``Accept`` classes is
           currently being maintained for backward compatibility, but it will
           change in the future to better conform to the RFC.

        :param offer: (``str``) media type offer
        :return: (``bool``) Whether ``offer`` is acceptable according to the
                 header.

        For this class, either there is no ``Accept`` header in the request, or
        the header is invalid, so any media type is acceptable, and this always
        returns ``True``.
        """
        warnings.warn(
            'The behavior of .__contains__ for the Accept classes is '
            'currently being maintained for backward compatibility, but it '
            'will change in the future to better conform to the RFC.',
            DeprecationWarning,
        )
        return True

    def __iter__(self):
        """
        Return all the ranges with non-0 qvalues, in order of preference.

        .. warning::

           The behavior of this method is currently maintained for backward
           compatibility, but will change in the future.

        :return: iterator of all the media ranges in the header with non-0
                 qvalues, in descending order of qvalue. If two ranges have the
                 same qvalue, they are returned in the order of their positions
                 in the header, from left to right.

        When there is no ``Accept`` header in the request or the header is
        invalid, there are no media ranges, so this always returns an empty
        iterator.
        """
        warnings.warn(
            'The behavior of AcceptValidHeader.__iter__ is currently '
            'maintained for backward compatibility, but will change in the '
            'future.',
            DeprecationWarning,
        )
        return iter(())

    def accept_html(self):
        """
        Return ``True`` if any HTML-like type is accepted.

        The HTML-like types are 'text/html', 'application/xhtml+xml',
        'application/xml' and 'text/xml'.

        When the header is invalid, or there is no `Accept` header in the
        request, all `offers` are considered acceptable, so this always returns
        ``True``.
        """
        return bool(
            self.acceptable_offers(
                offers=[
                    'text/html',
                    'application/xhtml+xml',
                    'application/xml',
                    'text/xml',
                ],
            )
        )
    accepts_html = property(fget=accept_html, doc=accept_html.__doc__)
    # note the plural

    def acceptable_offers(self, offers):
        """
        Return the offers that are acceptable according to the header.

        Any offers that cannot be parsed via
        :meth:`.Accept.parse_offer` will be ignored.

        :param offers: ``iterable`` of ``str`` media types (media types can
                       include media type parameters)
        :return: When the header is invalid, or there is no ``Accept`` header
                 in the request, all `offers` are considered acceptable, so
                 this method returns a list of (media type, qvalue) tuples
                 where each offer in `offers` is paired with the qvalue of 1.0,
                 in the same order as in `offers`.
        """
        return [
            (offers[offer_index], 1.0)
            for offer_index, _
            # avoid returning any offers that don't match the grammar so
            # that the return values here are consistent with what would be
            # returned in AcceptValidHeader
            in self._parse_and_normalize_offers(offers)
        ]

    def best_match(self, offers, default_match=None):
        """
        Return the best match from the sequence of language tag `offers`.

        This is the ``.best_match()`` method for when the header is invalid or
        not found in the request, corresponding to
        :meth:`AcceptValidHeader.best_match`.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future (see the documentation for
           :meth:`AcceptValidHeader.best_match`).

        When the header is invalid, or there is no `Accept` header in the
        request, all `offers` are considered acceptable, so the best match is
        the media type in `offers` with the highest server quality value (if
        the server quality value is not supplied for a media type, it is 1).

        If more than one media type in `offers` have the same highest server
        quality value, then the one that shows up first in `offers` is the best
        match.

        :param offers: (iterable)

                       | Each item in the iterable may be a ``str`` media type,
                         or a (media type, server quality value) ``tuple`` or
                         ``list``. (The two may be mixed in the iterable.)

        :param default_match: (optional, any type) the value to be returned if
                              `offers` is empty.

        :return: (``str``, or the type of `default_match`)

                 | The offer that has the highest server quality value.  If
                   `offers` is empty, the value of `default_match` is returned.
        """
        warnings.warn(
            'The behavior of .best_match for the Accept classes is currently '
            'being maintained for backward compatibility, but the method will'
            ' be deprecated in the future, as its behavior is not specified '
            'in (and currently does not conform to) RFC 7231.',
            DeprecationWarning,
        )
        best_quality = -1
        best_offer = default_match
        for offer in offers:
            if isinstance(offer, (list, tuple)):
                offer, quality = offer
            else:
                quality = 1
            if quality > best_quality:
                best_offer = offer
                best_quality = quality
        return best_offer

    def quality(self, offer):
        """
        Return quality value of given offer, or ``None`` if there is no match.

        This is the ``.quality()`` method for when the header is invalid or not
        found in the request, corresponding to
        :meth:`AcceptValidHeader.quality`.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future (see the documentation for
           :meth:`AcceptValidHeader.quality`).

        :param offer: (``str``) media type offer
        :return: (``float``) ``1.0``.

        When the ``Accept`` header is invalid or not in the request, all offers
        are equally acceptable, so 1.0 is always returned.
        """
        warnings.warn(
            'The behavior of .quality for the Accept classes is currently '
            'being maintained for backward compatibility, but the method will'
            ' be deprecated in the future, as its behavior does not conform to'
            'RFC 7231.',
            DeprecationWarning,
        )
        return 1.0


class AcceptNoHeader(_AcceptInvalidOrNoHeader):
    """
    Represent when there is no ``Accept`` header in the request.

    This object should not be modified. To add to the header, we can use the
    addition operators (``+`` and ``+=``), which return a new object (see the
    docstring for :meth:`AcceptNoHeader.__add__`).
    """

    @property
    def header_value(self):
        """
        (``str`` or ``None``) The header value.

        As there is no header in the request, this is ``None``.
        """
        return self._header_value

    @property
    def parsed(self):
        """
        (``list`` or ``None``) Parsed form of the header.

        As there is no header in the request, this is ``None``.
        """
        return self._parsed

    def __init__(self):
        """
        Create an :class:`AcceptNoHeader` instance.
        """
        self._header_value = None
        self._parsed = None
        self._parsed_nonzero = None

    def copy(self):
        """
        Create a copy of the header object.

        """
        return self.__class__()

    def __add__(self, other):
        """
        Add to header, creating a new header object.

        `other` can be:

        * ``None``
        * a ``str`` header value
        * a ``dict``, with media ranges ``str``'s (including any media type
          parameters) as keys, and either qvalues ``float``'s or (*qvalues*,
          *extension_params*) tuples as values, where *extension_params* is a
          ``str`` of the extension parameters segment of the header element,
          starting with the first '``;``'
        * a ``tuple`` or ``list``, where each item is either a header element
          ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple``
          or ``list`` where *media_range* is a ``str`` of the media range
          including any media type parameters, and *extension_params* is a
          ``str`` of the extension parameters segment of the header element,
          starting with the first '``;``'
        * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or
          :class:`AcceptInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``

        If `other` is a valid header value or an :class:`AcceptValidHeader`
        instance, a new :class:`AcceptValidHeader` instance with the valid
        header value is returned.

        If `other` is ``None``, an :class:`AcceptNoHeader` instance, an invalid
        header value, or an :class:`AcceptInvalidHeader` instance, a new
        :class:`AcceptNoHeader` instance is returned.
        """
        if isinstance(other, AcceptValidHeader):
            return AcceptValidHeader(header_value=other.header_value)

        if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)):
            return self.__class__()

        return self._add_instance_and_non_accept_type(
            instance=self, other=other,
        )

    def __radd__(self, other):
        """
        Add to header, creating a new header object.

        See the docstring for :meth:`AcceptNoHeader.__add__`.
        """
        return self.__add__(other=other)

    def __repr__(self):
        return '<{}>'.format(self.__class__.__name__)

    def __str__(self):
        """Return the ``str`` ``'<no header in request>'``."""
        return '<no header in request>'

    def _add_instance_and_non_accept_type(self, instance, other):
        if other is None:
            return self.__class__()

        other_header_value = self._python_value_to_header_str(value=other)

        try:
            return AcceptValidHeader(header_value=other_header_value)
        except ValueError:  # invalid header value
            return self.__class__()


class AcceptInvalidHeader(_AcceptInvalidOrNoHeader):
    """
    Represent an invalid ``Accept`` header.

    An invalid header is one that does not conform to
    :rfc:`7231#section-5.3.2`.

    :rfc:`7231` does not provide any guidance on what should happen if the
    ``Accept`` header has an invalid value. This implementation disregards the
    header, and treats it as if there is no ``Accept`` header in the request.

    This object should not be modified. To add to the header, we can use the
    addition operators (``+`` and ``+=``), which return a new object (see the
    docstring for :meth:`AcceptInvalidHeader.__add__`).
    """

    @property
    def header_value(self):
        """(``str`` or ``None``) The header value."""
        return self._header_value

    @property
    def parsed(self):
        """
        (``list`` or ``None``) Parsed form of the header.

        As the header is invalid and cannot be parsed, this is ``None``.
        """
        return self._parsed

    def __init__(self, header_value):
        """
        Create an :class:`AcceptInvalidHeader` instance.
        """
        self._header_value = header_value
        self._parsed = None
        self._parsed_nonzero = None

    def copy(self):
        """
        Create a copy of the header object.

        """
        return self.__class__(self._header_value)

    def __add__(self, other):
        """
        Add to header, creating a new header object.

        `other` can be:

        * ``None``
        * a ``str`` header value
        * a ``dict``, with media ranges ``str``'s (including any media type
          parameters) as keys, and either qvalues ``float``'s or (*qvalues*,
          *extension_params*) tuples as values, where *extension_params* is a
          ``str`` of the extension parameters segment of the header element,
          starting with the first '``;``'
        * a ``tuple`` or ``list``, where each item is either a header element
          ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple``
          or ``list`` where *media_range* is a ``str`` of the media range
          including any media type parameters, and *extension_params* is a
          ``str`` of the extension parameters segment of the header element,
          starting with the first '``;``'
        * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or
          :class:`AcceptInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``

        If `other` is a valid header value or an :class:`AcceptValidHeader`
        instance, then a new :class:`AcceptValidHeader` instance with the valid
        header value is returned.

        If `other` is ``None``, an :class:`AcceptNoHeader` instance, an invalid
        header value, or an :class:`AcceptInvalidHeader` instance, a new
        :class:`AcceptNoHeader` instance is returned.
        """
        if isinstance(other, AcceptValidHeader):
            return AcceptValidHeader(header_value=other.header_value)

        if isinstance(other, (AcceptNoHeader, AcceptInvalidHeader)):
            return AcceptNoHeader()

        return self._add_instance_and_non_accept_type(
            instance=self, other=other,
        )

    def __radd__(self, other):
        """
        Add to header, creating a new header object.

        See the docstring for :meth:`AcceptValidHeader.__add__`.
        """
        return self._add_instance_and_non_accept_type(
            instance=self, other=other, instance_on_the_right=True,
        )

    def __repr__(self):
        return '<{}>'.format(self.__class__.__name__)
        # We do not display the header_value, as it is untrusted input. The
        # header_value could always be easily obtained from the .header_value
        # property.

    def __str__(self):
        """Return the ``str`` ``'<invalid header value>'``."""
        return '<invalid header value>'

    def _add_instance_and_non_accept_type(
        self, instance, other, instance_on_the_right=False,
    ):
        if other is None:
            return AcceptNoHeader()

        other_header_value = self._python_value_to_header_str(value=other)

        try:
            return AcceptValidHeader(header_value=other_header_value)
        except ValueError:  # invalid header value
            return AcceptNoHeader()


def create_accept_header(header_value):
    """
    Create an object representing the ``Accept`` header in a request.

    :param header_value: (``str``) header value
    :return: If `header_value` is ``None``, an :class:`AcceptNoHeader`
             instance.

             | If `header_value` is a valid ``Accept`` header, an
               :class:`AcceptValidHeader` instance.

             | If `header_value` is an invalid ``Accept`` header, an
               :class:`AcceptInvalidHeader` instance.
    """
    if header_value is None:
        return AcceptNoHeader()
    if isinstance(header_value, Accept):
        return header_value.copy()
    try:
        return AcceptValidHeader(header_value=header_value)
    except ValueError:
        return AcceptInvalidHeader(header_value=header_value)


def accept_property():
    doc = """
        Property representing the ``Accept`` header.

        (:rfc:`RFC 7231, section 5.3.2 <7231#section-5.3.2>`)

        The header value in the request environ is parsed and a new object
        representing the header is created every time we *get* the value of the
        property. (*set* and *del* change the header value in the request
        environ, and do not involve parsing.)
    """

    ENVIRON_KEY = 'HTTP_ACCEPT'

    def fget(request):
        """Get an object representing the header in the request."""
        return create_accept_header(
            header_value=request.environ.get(ENVIRON_KEY)
        )

    def fset(request, value):
        """
        Set the corresponding key in the request environ.

        `value` can be:

        * ``None``
        * a ``str`` header value
        * a ``dict``, with media ranges ``str``'s (including any media type
          parameters) as keys, and either qvalues ``float``'s or (*qvalues*,
          *extension_params*) tuples as values, where *extension_params* is a
          ``str`` of the extension parameters segment of the header element,
          starting with the first '``;``'
        * a ``tuple`` or ``list``, where each item is either a header element
          ``str``, or a (*media_range*, *qvalue*, *extension_params*) ``tuple``
          or ``list`` where *media_range* is a ``str`` of the media range
          including any media type parameters, and *extension_params* is a
          ``str`` of the extension parameters segment of the header element,
          starting with the first '``;``'
        * an :class:`AcceptValidHeader`, :class:`AcceptNoHeader`, or
          :class:`AcceptInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``
        """
        if value is None or isinstance(value, AcceptNoHeader):
            fdel(request=request)
        else:
            if isinstance(value, (AcceptValidHeader, AcceptInvalidHeader)):
                header_value = value.header_value
            else:
                header_value = Accept._python_value_to_header_str(value=value)
            request.environ[ENVIRON_KEY] = header_value

    def fdel(request):
        """Delete the corresponding key from the request environ."""
        try:
            del request.environ[ENVIRON_KEY]
        except KeyError:
            pass

    return property(fget, fset, fdel, textwrap.dedent(doc))


class AcceptCharset(object):
    """
    Represent an ``Accept-Charset`` header.

    Base class for :class:`AcceptCharsetValidHeader`,
    :class:`AcceptCharsetNoHeader`, and :class:`AcceptCharsetInvalidHeader`.
    """

    # RFC 7231 Section 3.1.1.2 "Charset":
    # charset = token
    charset_re = token_re
    # RFC 7231 Section 5.3.3 "Accept-Charset":
    # Accept-Charset = 1#( ( charset / "*" ) [ weight ] )
    charset_n_weight_re = _item_n_weight_re(item_re=charset_re)
    charset_n_weight_compiled_re = re.compile(charset_n_weight_re)
    accept_charset_compiled_re = _list_1_or_more__compiled_re(
        element_re=charset_n_weight_re,
    )

    @classmethod
    def _python_value_to_header_str(cls, value):
        if isinstance(value, str):
            header_str = value
        else:
            if hasattr(value, 'items'):
                value = sorted(
                    value.items(),
                    key=lambda item: item[1],
                    reverse=True,
                )
            if isinstance(value, (tuple, list)):
                result = []
                for item in value:
                    if isinstance(item, (tuple, list)):
                        item = _item_qvalue_pair_to_header_element(pair=item)
                    result.append(item)
                header_str = ', '.join(result)
            else:
                header_str = str(value)
        return header_str

    @classmethod
    def parse(cls, value):
        """
        Parse an ``Accept-Charset`` header.

        :param value: (``str``) header value
        :return: If `value` is a valid ``Accept-Charset`` header, returns an
                 iterator of (charset, quality value) tuples, as parsed from
                 the header from left to right.
        :raises ValueError: if `value` is an invalid header
        """
        # Check if header is valid
        # Using Python stdlib's `re` module, there is currently no way to check
        # the match *and* get all the groups using the same regex, so we have
        # to use one regex to check the match, and another to get the groups.
        if cls.accept_charset_compiled_re.match(value) is None:
            raise ValueError('Invalid value for an Accept-Charset header.')
        def generator(value):
            for match in (cls.charset_n_weight_compiled_re.finditer(value)):
                charset = match.group(1)
                qvalue = match.group(2)
                qvalue = float(qvalue) if qvalue else 1.0
                yield (charset, qvalue)
        return generator(value=value)


class AcceptCharsetValidHeader(AcceptCharset):
    """
    Represent a valid ``Accept-Charset`` header.

    A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.3
    <7231#section-5.3.3>`.

    This object should not be modified. To add to the header, we can use the
    addition operators (``+`` and ``+=``), which return a new object (see the
    docstring for :meth:`AcceptCharsetValidHeader.__add__`).
    """

    @property
    def header_value(self):
        """(``str``) The header value."""
        return self._header_value

    @property
    def parsed(self):
        """
        (``list``) Parsed form of the header.

        A list of (charset, quality value) tuples.
        """
        return self._parsed

    def __init__(self, header_value):
        """
        Create an :class:`AcceptCharsetValidHeader` instance.

        :param header_value: (``str``) header value.
        :raises ValueError: if `header_value` is an invalid value for an
                            ``Accept-Charset`` header.
        """
        self._header_value = header_value
        self._parsed = list(self.parse(header_value))
        self._parsed_nonzero = [
            item for item in self.parsed if item[1]  # item[1] is the qvalue
        ]

    def copy(self):
        """
        Create a copy of the header object.

        """
        return self.__class__(self._header_value)

    def __add__(self, other):
        """
        Add to header, creating a new header object.

        `other` can be:

        * ``None``
        * a ``str`` header value
        * a ``dict``, where keys are charsets and values are qvalues
        * a ``tuple`` or ``list``, where each item is a charset ``str`` or a
          ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs
          can be mixed within the ``tuple`` or ``list``)
        * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`,
          or :class:`AcceptCharsetInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``

        If `other` is a valid header value or another
        :class:`AcceptCharsetValidHeader` instance, the two header values are
        joined with ``', '``, and a new :class:`AcceptCharsetValidHeader`
        instance with the new header value is returned.

        If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an
        invalid header value, or an :class:`AcceptCharsetInvalidHeader`
        instance, a new :class:`AcceptCharsetValidHeader` instance with the
        same header value as ``self`` is returned.
        """
        if isinstance(other, AcceptCharsetValidHeader):
            return create_accept_charset_header(
                header_value=self.header_value + ', ' + other.header_value,
            )

        if isinstance(
            other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader)
        ):
            return self.__class__(header_value=self.header_value)

        return self._add_instance_and_non_accept_charset_type(
            instance=self, other=other,
        )

    def __bool__(self):
        """
        Return whether ``self`` represents a valid ``Accept-Charset`` header.

        Return ``True`` if ``self`` represents a valid header, and ``False`` if
        it represents an invalid header, or the header not being in the
        request.

        For this class, it always returns ``True``.
        """
        return True
    __nonzero__ = __bool__  # Python 2

    def __contains__(self, offer):
        """
        Return ``bool`` indicating whether `offer` is acceptable.

        .. warning::

           The behavior of :meth:`AcceptCharsetValidHeader.__contains__` is
           currently being maintained for backward compatibility, but it will
           change in the future to better conform to the RFC.

        :param offer: (``str``) charset offer
        :return: (``bool``) Whether ``offer`` is acceptable according to the
                 header.

        This does not fully conform to :rfc:`RFC 7231, section 5.3.3
        <7231#section-5.3.3>`: it incorrect interprets ``*`` to mean 'match any
        charset in the header', rather than 'match any charset that is not
        mentioned elsewhere in the header'::

            >>> 'UTF-8' in AcceptCharsetValidHeader('UTF-8;q=0, *')
            True
        """
        warnings.warn(
            'The behavior of AcceptCharsetValidHeader.__contains__ is '
            'currently being maintained for backward compatibility, but it '
            'will change in the future to better conform to the RFC.',
            DeprecationWarning,
        )
        for mask, quality in self._parsed_nonzero:
            if self._old_match(mask, offer):
                return True
        return False

    def __iter__(self):
        """
        Return all the items with non-0 qvalues, in order of preference.

        .. warning::

           The behavior of this method is currently maintained for backward
           compatibility, but will change in the future.

        :return: iterator of all the items (charset or ``*``) in the header
                 with non-0 qvalues, in descending order of qvalue. If two
                 items have the same qvalue, they are returned in the order of
                 their positions in the header, from left to right.

        Please note that this is a simple filter for the items in the header
        with non-0 qvalues, and is not necessarily the same as what the client
        prefers, e.g. ``'utf-7;q=0, *'`` means 'everything but utf-7', but
        ``list(instance)`` would return only ``['*']``.
        """
        warnings.warn(
            'The behavior of AcceptCharsetValidHeader.__iter__ is currently '
            'maintained for backward compatibility, but will change in the '
            'future.',
            DeprecationWarning,
        )
        for m,q in sorted(
            self._parsed_nonzero,
            key=lambda i: i[1],
            reverse=True
        ):
            yield m

    def __radd__(self, other):
        """
        Add to header, creating a new header object.

        See the docstring for :meth:`AcceptCharsetValidHeader.__add__`.
        """
        return self._add_instance_and_non_accept_charset_type(
            instance=self, other=other, instance_on_the_right=True,
        )

    def __repr__(self):
        return '<{} ({!r})>'.format(self.__class__.__name__, str(self))

    def __str__(self):
        r"""
        Return a tidied up version of the header value.

        e.g. If the ``header_value`` is ``', \t,iso-8859-5;q=0.000 \t,
        utf-8;q=1.000, UTF-7, unicode-1-1;q=0.210  ,'``, ``str(instance)``
        returns ``'iso-8859-5;q=0, utf-8, UTF-7, unicode-1-1;q=0.21'``.
        """
        return ', '.join(
            _item_qvalue_pair_to_header_element(pair=tuple_)
            for tuple_ in self.parsed
        )

    def _add_instance_and_non_accept_charset_type(
        self, instance, other, instance_on_the_right=False,
    ):
        if not other:
            return self.__class__(header_value=instance.header_value)

        other_header_value = self._python_value_to_header_str(value=other)

        try:
            self.parse(value=other_header_value)
        except ValueError:  # invalid header value
            return self.__class__(header_value=instance.header_value)

        new_header_value = (
            (other_header_value + ', ' + instance.header_value)
            if instance_on_the_right
            else (instance.header_value + ', ' + other_header_value)
        )
        return self.__class__(header_value=new_header_value)

    def _old_match(self, mask, offer):
        """
        Return whether charset offer matches header item (charset or ``*``).

        .. warning::

           This is maintained for backward compatibility, and will be
           deprecated in the future.

        This method was WebOb's old criterion for deciding whether a charset
        matches a header item (charset or ``*``), used in

        - :meth:`AcceptCharsetValidHeader.__contains__`
        - :meth:`AcceptCharsetValidHeader.best_match`
        - :meth:`AcceptCharsetValidHeader.quality`

        It does not conform to :rfc:`RFC 7231, section 5.3.3
        <7231#section-5.3.3>` in that it does not interpret ``*`` values in the
        header correctly: ``*`` should only match charsets not mentioned
        elsewhere in the header.
        """
        return mask == '*' or offer.lower() == mask.lower()

    def acceptable_offers(self, offers):
        """
        Return the offers that are acceptable according to the header.

        The offers are returned in descending order of preference, where
        preference is indicated by the qvalue of the charset or ``*`` in the
        header matching the offer.

        This uses the matching rules described in :rfc:`RFC 7231, section 5.3.3
        <7231#section-5.3.3>`.

        :param offers: ``iterable`` of ``str`` charsets
        :return: A list of tuples of the form (charset, qvalue), in descending
                 order of qvalue. Where two offers have the same qvalue, they
                 are returned in the same order as their order in `offers`.
        """
        lowercased_parsed = [
            (charset.lower(), qvalue) for (charset, qvalue) in self.parsed
        ]
        lowercased_offers = [offer.lower() for offer in offers]

        not_acceptable_charsets = set()
        acceptable_charsets = dict()
        asterisk_qvalue = None

        for charset, qvalue in lowercased_parsed:
            if charset == '*':
                if asterisk_qvalue is None:
                    asterisk_qvalue = qvalue
            elif (
                charset not in acceptable_charsets and charset not in
                not_acceptable_charsets
                # if we have not already encountered this charset in the header
            ):
                if qvalue == 0.0:
                    not_acceptable_charsets.add(charset)
                else:
                    acceptable_charsets[charset] = qvalue
        acceptable_charsets = list(acceptable_charsets.items())
        # Sort acceptable_charsets by qvalue, descending order
        acceptable_charsets.sort(key=lambda tuple_: tuple_[1], reverse=True)

        filtered_offers = []
        for index, offer in enumerate(lowercased_offers):
            # If offer matches a non-* charset with q=0, it is filtered out
            if any((
                (offer == charset) for charset in not_acceptable_charsets
            )):
                continue

            matched_charset_qvalue = None
            for charset, qvalue in acceptable_charsets:
                if offer == charset:
                    matched_charset_qvalue = qvalue
                    break
            else:
                if asterisk_qvalue:
                    matched_charset_qvalue = asterisk_qvalue
            if matched_charset_qvalue is not None:  # if there was a match
                filtered_offers.append((
                    offers[index], matched_charset_qvalue, index
                ))

        # sort by position in `offers` argument, ascending
        filtered_offers.sort(key=lambda tuple_: tuple_[2])
        # When qvalues are tied, position in `offers` is the tiebreaker.

        # sort by qvalue, descending
        filtered_offers.sort(key=lambda tuple_: tuple_[1], reverse=True)

        return [(item[0], item[1]) for item in filtered_offers]
        # (offer, qvalue), dropping the position

    def best_match(self, offers, default_match=None):
        """
        Return the best match from the sequence of charset `offers`.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future.

           :meth:`AcceptCharsetValidHeader.best_match`  has many issues, and
           does not conform to :rfc:`RFC 7231 <7231>`.

        Each charset in `offers` is checked against each non-``q=0`` item
        (charset or ``*``) in the header. If the two are a match according to
        WebOb's old criterion for a match, the quality value of the match is
        the qvalue of the item from the header multiplied by the server quality
        value of the offer (if the server quality value is not supplied, it is
        1).

        The offer in the match with the highest quality value is the best
        match. If there is more than one match with the highest qvalue, the one
        that shows up first in `offers` is the best match.

        :param offers: (iterable)

                       | Each item in the iterable may be a ``str`` charset, or
                         a (charset, server quality value) ``tuple`` or
                         ``list``.  (The two may be mixed in the iterable.)

        :param default_match: (optional, any type) the value to be returned if
                              there is no match

        :return: (``str``, or the type of `default_match`)

                 | The offer that is the best match. If there is no match, the
                   value of `default_match` is returned.

        The algorithm behind this method was written for the ``Accept`` header
        rather than the ``Accept-Charset`` header. It uses the old criterion of
        a match in :meth:`AcceptCharsetValidHeader._old_match`, which does not
        conform to :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`, in that
        it does not interpret ``*`` values in the header correctly: ``*``
        should only match charsets not mentioned elsewhere in the header::

            >>> AcceptCharsetValidHeader('utf-8;q=0, *').best_match(['utf-8'])
            'utf-8'
        """
        warnings.warn(
            'The behavior of AcceptCharsetValidHeader.best_match is currently'
            ' being maintained for backward compatibility, but it will be '
            'deprecated in the future, as it does not conform to the RFC.',
            DeprecationWarning,
        )
        best_quality = -1
        best_offer = default_match
        matched_by = '*/*'
        for offer in offers:
            if isinstance(offer, (tuple, list)):
                offer, server_quality = offer
            else:
                server_quality = 1
            for mask, quality in self._parsed_nonzero:
                possible_quality = server_quality * quality
                if possible_quality < best_quality:
                    continue
                elif possible_quality == best_quality:
                    # 'text/plain' overrides 'message/*' overrides '*/*'
                    # (if all match w/ the same q=)
                    # [We can see that this was written for the Accept header,
                    # not the Accept-Charset header.]
                    if matched_by.count('*') <= mask.count('*'):
                        continue
                if self._old_match(mask, offer):
                    best_quality = possible_quality
                    best_offer = offer
                    matched_by = mask
        return best_offer

    def quality(self, offer):
        """
        Return quality value of given offer, or ``None`` if there is no match.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future.

        :param offer: (``str``) charset offer
        :return: (``float`` or ``None``)

                 | The quality value from the charset that matches the `offer`,
                   or ``None`` if there is no match.

        This uses the old criterion of a match in
        :meth:`AcceptCharsetValidHeader._old_match`, which does not conform to
        :rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`, in that it does
        not interpret ``*`` values in the header correctly: ``*`` should only
        match charsets not mentioned elsewhere in the header::

            >>> AcceptCharsetValidHeader('utf-8;q=0, *').quality('utf-8')
            1.0
            >>> AcceptCharsetValidHeader('utf-8;q=0.9, *').quality('utf-8')
            1.0
        """
        warnings.warn(
            'The behavior of AcceptCharsetValidHeader.quality is currently '
            'being maintained for backward compatibility, but it will be '
            'deprecated in the future, as it does not conform to the RFC.',
            DeprecationWarning,
        )
        bestq = 0
        for mask, q in self.parsed:
            if self._old_match(mask, offer):
                bestq = max(bestq, q)
        return bestq or None


class _AcceptCharsetInvalidOrNoHeader(AcceptCharset):
    """
    Represent when an ``Accept-Charset`` header is invalid or not in request.

    This is the base class for the behaviour that
    :class:`.AcceptCharsetInvalidHeader` and :class:`.AcceptCharsetNoHeader`
    have in common.

    :rfc:`7231` does not provide any guidance on what should happen if the
    ``Accept-Charset`` header has an invalid value. This implementation
    disregards the header when the header is invalid, so
    :class:`.AcceptCharsetInvalidHeader` and :class:`.AcceptCharsetNoHeader`
    have much behaviour in common.
    """

    def __bool__(self):
        """
        Return whether ``self`` represents a valid ``Accept-Charset`` header.

        Return ``True`` if ``self`` represents a valid header, and ``False`` if
        it represents an invalid header, or the header not being in the
        request.

        For this class, it always returns ``False``.
        """
        return False
    __nonzero__ = __bool__  # Python 2

    def __contains__(self, offer):
        """
        Return ``bool`` indicating whether `offer` is acceptable.

        .. warning::

           The behavior of ``.__contains__`` for the ``AcceptCharset`` classes
           is currently being maintained for backward compatibility, but it
           will change in the future to better conform to the RFC.

        :param offer: (``str``) charset offer
        :return: (``bool``) Whether ``offer`` is acceptable according to the
                 header.

        For this class, either there is no ``Accept-Charset`` header in the
        request, or the header is invalid, so any charset is acceptable, and
        this always returns ``True``.
        """
        warnings.warn(
            'The behavior of .__contains__ for the AcceptCharset classes is '
            'currently being maintained for backward compatibility, but it '
            'will change in the future to better conform to the RFC.',
            DeprecationWarning,
        )
        return True

    def __iter__(self):
        """
        Return all the items with non-0 qvalues, in order of preference.

        .. warning::

           The behavior of this method is currently maintained for backward
           compatibility, but will change in the future.

        :return: iterator of all the items (charset or ``*``) in the header
                 with non-0 qvalues, in descending order of qvalue. If two
                 items have the same qvalue, they are returned in the order of
                 their positions in the header, from left to right.

        When there is no ``Accept-Charset`` header in the request or the header
        is invalid, there are no items, and this always returns an empty
        iterator.
        """
        warnings.warn(
            'The behavior of AcceptCharsetValidHeader.__iter__ is currently '
            'maintained for backward compatibility, but will change in the '
            'future.',
            DeprecationWarning,
        )
        return iter(())

    def acceptable_offers(self, offers):
        """
        Return the offers that are acceptable according to the header.

        The offers are returned in descending order of preference, where
        preference is indicated by the qvalue of the charset or ``*`` in the
        header matching the offer.

        This uses the matching rules described in :rfc:`RFC 7231, section 5.3.3
        <7231#section-5.3.3>`.

        :param offers: ``iterable`` of ``str`` charsets
        :return: A list of tuples of the form (charset, qvalue), in descending
                 order of qvalue. Where two offers have the same qvalue, they
                 are returned in the same order as their order in `offers`.

                 | When the header is invalid or there is no ``Accept-Charset``
                   header in the request, all `offers` are considered
                   acceptable, so this method returns a list of (charset,
                   qvalue) tuples where each offer in `offers` is paired with
                   the qvalue of 1.0, in the same order as `offers`.
        """
        return [(offer, 1.0) for offer in offers]

    def best_match(self, offers, default_match=None):
        """
        Return the best match from the sequence of charset `offers`.

        This is the ``.best_match()`` method for when the header is invalid or
        not found in the request, corresponding to
        :meth:`AcceptCharsetValidHeader.best_match`.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future (see the documentation for
           :meth:`AcceptCharsetValidHeader.best_match`).

        When the header is invalid, or there is no `Accept-Charset` header in
        the request, all the charsets in `offers` are considered acceptable, so
        the best match is the charset in `offers` with the highest server
        quality value (if the server quality value is not supplied, it is 1).

        If more than one charsets in `offers` have the same highest server
        quality value, then the one that shows up first in `offers` is the best
        match.

        :param offers: (iterable)

                       | Each item in the iterable may be a ``str`` charset, or
                         a (charset, server quality value) ``tuple`` or
                         ``list``.  (The two may be mixed in the iterable.)

        :param default_match: (optional, any type) the value to be returned if
                              `offers` is empty.

        :return: (``str``, or the type of `default_match`)

                 | The charset that has the highest server quality value.  If
                   `offers` is empty, the value of `default_match` is returned.
        """
        warnings.warn(
            'The behavior of .best_match for the AcceptCharset classes is '
            'currently being maintained for backward compatibility, but the '
            'method will be deprecated in the future, as its behavior is not '
            'specified in (and currently does not conform to) RFC 7231.',
            DeprecationWarning,
        )
        best_quality = -1
        best_offer = default_match
        for offer in offers:
            if isinstance(offer, (list, tuple)):
                offer, quality = offer
            else:
                quality = 1
            if quality > best_quality:
                best_offer = offer
                best_quality = quality
        return best_offer

    def quality(self, offer):
        """
        Return quality value of given offer, or ``None`` if there is no match.

        This is the ``.quality()`` method for when the header is invalid or not
        found in the request, corresponding to
        :meth:`AcceptCharsetValidHeader.quality`.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future (see the documentation for
           :meth:`AcceptCharsetValidHeader.quality`).

        :param offer: (``str``) charset offer
        :return: (``float``) ``1.0``.

        When the ``Accept-Charset`` header is invalid or not in the request,
        all offers are equally acceptable, so 1.0 is always returned.
        """
        warnings.warn(
            'The behavior of .quality for the Accept-Charset classes is '
            'currently being maintained for backward compatibility, but the '
            'method will be deprecated in the future, as its behavior does not'
            ' conform to RFC 7231.',
            DeprecationWarning,
        )
        return 1.0


class AcceptCharsetNoHeader(_AcceptCharsetInvalidOrNoHeader):
    """
    Represent when there is no ``Accept-Charset`` header in the request.

    This object should not be modified. To add to the header, we can use the
    addition operators (``+`` and ``+=``), which return a new object (see the
    docstring for :meth:`AcceptCharsetNoHeader.__add__`).
    """

    @property
    def header_value(self):
        """
        (``str`` or ``None``) The header value.

        As there is no header in the request, this is ``None``.
        """
        return self._header_value

    @property
    def parsed(self):
        """
        (``list`` or ``None``) Parsed form of the header.

        As there is no header in the request, this is ``None``.
        """
        return self._parsed

    def __init__(self):
        """
        Create an :class:`AcceptCharsetNoHeader` instance.
        """
        self._header_value = None
        self._parsed = None
        self._parsed_nonzero = None

    def copy(self):
        """
        Create a copy of the header object.

        """
        return self.__class__()

    def __add__(self, other):
        """
        Add to header, creating a new header object.

        `other` can be:

        * ``None``
        * a ``str`` header value
        * a ``dict``, where keys are charsets and values are qvalues
        * a ``tuple`` or ``list``, where each item is a charset ``str`` or a
          ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs
          can be mixed within the ``tuple`` or ``list``)
        * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`,
          or :class:`AcceptCharsetInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``

        If `other` is a valid header value or an
        :class:`AcceptCharsetValidHeader` instance, a new
        :class:`AcceptCharsetValidHeader` instance with the valid header value
        is returned.

        If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an
        invalid header value, or an :class:`AcceptCharsetInvalidHeader`
        instance, a new :class:`AcceptCharsetNoHeader` instance is returned.
        """
        if isinstance(other, AcceptCharsetValidHeader):
            return AcceptCharsetValidHeader(header_value=other.header_value)

        if isinstance(
            other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader)
        ):
            return self.__class__()

        return self._add_instance_and_non_accept_charset_type(
            instance=self, other=other,
        )

    def __radd__(self, other):
        """
        Add to header, creating a new header object.

        See the docstring for :meth:`AcceptCharsetNoHeader.__add__`.
        """
        return self.__add__(other=other)

    def __repr__(self):
        return '<{}>'.format(self.__class__.__name__)

    def __str__(self):
        """Return the ``str`` ``'<no header in request>'``."""
        return '<no header in request>'

    def _add_instance_and_non_accept_charset_type(self, instance, other):
        if not other:
            return self.__class__()

        other_header_value = self._python_value_to_header_str(value=other)

        try:
            return AcceptCharsetValidHeader(header_value=other_header_value)
        except ValueError:  # invalid header value
            return self.__class__()


class AcceptCharsetInvalidHeader(_AcceptCharsetInvalidOrNoHeader):
    """
    Represent an invalid ``Accept-Charset`` header.

    An invalid header is one that does not conform to
    :rfc:`7231#section-5.3.3`. As specified in the RFC, an empty header is an
    invalid ``Accept-Charset`` header.

    :rfc:`7231` does not provide any guidance on what should happen if the
    ``Accept-Charset`` header has an invalid value. This implementation
    disregards the header, and treats it as if there is no ``Accept-Charset``
    header in the request.

    This object should not be modified. To add to the header, we can use the
    addition operators (``+`` and ``+=``), which return a new object (see the
    docstring for :meth:`AcceptCharsetInvalidHeader.__add__`).
    """

    @property
    def header_value(self):
        """(``str`` or ``None``) The header value."""
        return self._header_value

    @property
    def parsed(self):
        """
        (``list`` or ``None``) Parsed form of the header.

        As the header is invalid and cannot be parsed, this is ``None``.
        """
        return self._parsed

    def __init__(self, header_value):
        """
        Create an :class:`AcceptCharsetInvalidHeader` instance.
        """
        self._header_value = header_value
        self._parsed = None
        self._parsed_nonzero = None

    def copy(self):
        """
        Create a copy of the header object.

        """
        return self.__class__(self._header_value)

    def __add__(self, other):
        """
        Add to header, creating a new header object.

        `other` can be:

        * ``None``
        * a ``str`` header value
        * a ``dict``, where keys are charsets and values are qvalues
        * a ``tuple`` or ``list``, where each item is a charset ``str`` or a
          ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs
          can be mixed within the ``tuple`` or ``list``)
        * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`,
          or :class:`AcceptCharsetInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``

        If `other` is a valid header value or an
        :class:`AcceptCharsetValidHeader` instance, a new
        :class:`AcceptCharsetValidHeader` instance with the valid header value
        is returned.

        If `other` is ``None``, an :class:`AcceptCharsetNoHeader` instance, an
        invalid header value, or an :class:`AcceptCharsetInvalidHeader`
        instance, a new :class:`AcceptCharsetNoHeader` instance is returned.
        """
        if isinstance(other, AcceptCharsetValidHeader):
            return AcceptCharsetValidHeader(header_value=other.header_value)

        if isinstance(
            other, (AcceptCharsetNoHeader, AcceptCharsetInvalidHeader)
        ):
            return AcceptCharsetNoHeader()

        return self._add_instance_and_non_accept_charset_type(
            instance=self, other=other,
        )

    def __radd__(self, other):
        """
        Add to header, creating a new header object.

        See the docstring for :meth:`AcceptCharsetValidHeader.__add__`.
        """
        return self._add_instance_and_non_accept_charset_type(
            instance=self, other=other, instance_on_the_right=True,
        )

    def __repr__(self):
        return '<{}>'.format(self.__class__.__name__)
        # We do not display the header_value, as it is untrusted input. The
        # header_value could always be easily obtained from the .header_value
        # property.

    def __str__(self):
        """Return the ``str`` ``'<invalid header value>'``."""
        return '<invalid header value>'

    def _add_instance_and_non_accept_charset_type(
        self, instance, other, instance_on_the_right=False,
    ):
        if not other:
            return AcceptCharsetNoHeader()

        other_header_value = self._python_value_to_header_str(value=other)

        try:
            return AcceptCharsetValidHeader(header_value=other_header_value)
        except ValueError:  # invalid header value
            return AcceptCharsetNoHeader()


def create_accept_charset_header(header_value):
    """
    Create an object representing the ``Accept-Charset`` header in a request.

    :param header_value: (``str``) header value
    :return: If `header_value` is ``None``, an :class:`AcceptCharsetNoHeader`
             instance.

             | If `header_value` is a valid ``Accept-Charset`` header, an
               :class:`AcceptCharsetValidHeader` instance.

             | If `header_value` is an invalid ``Accept-Charset`` header, an
               :class:`AcceptCharsetInvalidHeader` instance.
    """
    if header_value is None:
        return AcceptCharsetNoHeader()
    if isinstance(header_value, AcceptCharset):
        return header_value.copy()
    try:
        return AcceptCharsetValidHeader(header_value=header_value)
    except ValueError:
        return AcceptCharsetInvalidHeader(header_value=header_value)


def accept_charset_property():
    doc = """
        Property representing the ``Accept-Charset`` header.

        (:rfc:`RFC 7231, section 5.3.3 <7231#section-5.3.3>`)

        The header value in the request environ is parsed and a new object
        representing the header is created every time we *get* the value of the
        property. (*set* and *del* change the header value in the request
        environ, and do not involve parsing.)
    """

    ENVIRON_KEY = 'HTTP_ACCEPT_CHARSET'

    def fget(request):
        """Get an object representing the header in the request."""
        return create_accept_charset_header(
            header_value=request.environ.get(ENVIRON_KEY)
        )

    def fset(request, value):
        """
        Set the corresponding key in the request environ.

        `value` can be:

        * ``None``
        * a ``str`` header value
        * a ``dict``, where keys are charsets and values are qvalues
        * a ``tuple`` or ``list``, where each item is a charset ``str`` or a
          ``tuple`` or ``list`` (charset, qvalue) pair (``str``'s and pairs
          can be mixed within the ``tuple`` or ``list``)
        * an :class:`AcceptCharsetValidHeader`, :class:`AcceptCharsetNoHeader`,
          or :class:`AcceptCharsetInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``
        """
        if value is None or isinstance(value, AcceptCharsetNoHeader):
            fdel(request=request)
        else:
            if isinstance(
                value, (AcceptCharsetValidHeader, AcceptCharsetInvalidHeader)
            ):
                header_value = value.header_value
            else:
                header_value = AcceptCharset._python_value_to_header_str(
                    value=value,
                )
            request.environ[ENVIRON_KEY] = header_value

    def fdel(request):
        """Delete the corresponding key from the request environ."""
        try:
            del request.environ[ENVIRON_KEY]
        except KeyError:
            pass

    return property(fget, fset, fdel, textwrap.dedent(doc))


class AcceptEncoding(object):
    """
    Represent an ``Accept-Encoding`` header.

    Base class for :class:`AcceptEncodingValidHeader`,
    :class:`AcceptEncodingNoHeader`, and :class:`AcceptEncodingInvalidHeader`.
    """

    # RFC 7231 Section 3.1.2.1 "Content Codings":
    # content-coding = token
    # Section 5.3.4 "Accept-Encoding":
    # Accept-Encoding  = #( codings [ weight ] )
    # codings          = content-coding / "identity" / "*"
    codings_re = token_re
    # "identity" (case-insensitive) and "*" are both already included in token
    # rule
    codings_n_weight_re = _item_n_weight_re(item_re=codings_re)
    codings_n_weight_compiled_re = re.compile(codings_n_weight_re)
    accept_encoding_compiled_re = _list_0_or_more__compiled_re(
        element_re=codings_n_weight_re,
    )

    @classmethod
    def _python_value_to_header_str(cls, value):
        if isinstance(value, str):
            header_str = value
        else:
            if hasattr(value, 'items'):
                value = sorted(
                    value.items(),
                    key=lambda item: item[1],
                    reverse=True,
                )
            if isinstance(value, (tuple, list)):
                result = []
                for item in value:
                    if isinstance(item, (tuple, list)):
                        item = _item_qvalue_pair_to_header_element(pair=item)
                    result.append(item)
                header_str = ', '.join(result)
            else:
                header_str = str(value)
        return header_str

    @classmethod
    def parse(cls, value):
        """
        Parse an ``Accept-Encoding`` header.

        :param value: (``str``) header value
        :return: If `value` is a valid ``Accept-Encoding`` header, returns an
                 iterator of (codings, quality value) tuples, as parsed from
                 the header from left to right.
        :raises ValueError: if `value` is an invalid header
        """
        # Check if header is valid
        # Using Python stdlib's `re` module, there is currently no way to check
        # the match *and* get all the groups using the same regex, so we have
        # to use one regex to check the match, and another to get the groups.
        if cls.accept_encoding_compiled_re.match(value) is None:
            raise ValueError('Invalid value for an Accept-Encoding header.')
        def generator(value):
            for match in (cls.codings_n_weight_compiled_re.finditer(value)):
                codings = match.group(1)
                qvalue = match.group(2)
                qvalue = float(qvalue) if qvalue else 1.0
                yield (codings, qvalue)
        return generator(value=value)


class AcceptEncodingValidHeader(AcceptEncoding):
    """
    Represent a valid ``Accept-Encoding`` header.

    A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.4
    <7231#section-5.3.4>`.

    This object should not be modified. To add to the header, we can use the
    addition operators (``+`` and ``+=``), which return a new object (see the
    docstring for :meth:`AcceptEncodingValidHeader.__add__`).
    """

    @property
    def header_value(self):
        """(``str`` or ``None``) The header value."""
        return self._header_value

    @property
    def parsed(self):
        """
        (``list`` or ``None``) Parsed form of the header.

        A list of (*codings*, *qvalue*) tuples, where

        *codings* (``str``) is a content-coding, the string "``identity``", or
        "``*``"; and

        *qvalue* (``float``) is the quality value of the codings.
        """
        return self._parsed

    def __init__(self, header_value):
        """
        Create an :class:`AcceptEncodingValidHeader` instance.

        :param header_value: (``str``) header value.
        :raises ValueError: if `header_value` is an invalid value for an
                            ``Accept-Encoding`` header.
        """
        self._header_value = header_value
        self._parsed = list(self.parse(header_value))
        self._parsed_nonzero = [item for item in self.parsed if item[1]]
        # item[1] is the qvalue

    def copy(self):
        """
        Create a copy of the header object.

        """
        return self.__class__(self._header_value)

    def __add__(self, other):
        """
        Add to header, creating a new header object.

        `other` can be:

        * ``None``
        * a ``str`` header value
        * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as
          keys, and qvalue ``float``'s as values
        * a ``tuple`` or ``list``, where each item is either a header element
          ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple``
          or ``list``
        * an :class:`AcceptEncodingValidHeader`,
          :class:`AcceptEncodingNoHeader`, or
          :class:`AcceptEncodingInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``

        If `other` is a valid header value or another
        :class:`AcceptEncodingValidHeader` instance, and the header value it
        represents is not ``''``, then the two header values are joined with
        ``', '``, and a new :class:`AcceptEncodingValidHeader` instance with
        the new header value is returned.

        If `other` is a valid header value or another
        :class:`AcceptEncodingValidHeader` instance representing a header value
        of ``''``; or if it is ``None`` or an :class:`AcceptEncodingNoHeader`
        instance; or if it is an invalid header value, or an
        :class:`AcceptEncodingInvalidHeader` instance, then a new
        :class:`AcceptEncodingValidHeader` instance with the same header value
        as ``self`` is returned.
        """
        if isinstance(other, AcceptEncodingValidHeader):
            if other.header_value == '':
                return self.__class__(header_value=self.header_value)
            else:
                return create_accept_encoding_header(
                    header_value=self.header_value + ', ' + other.header_value,
                )

        if isinstance(
            other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader)
        ):
            return self.__class__(header_value=self.header_value)

        return self._add_instance_and_non_accept_encoding_type(
            instance=self, other=other,
        )

    def __bool__(self):
        """
        Return whether ``self`` represents a valid ``Accept-Encoding`` header.

        Return ``True`` if ``self`` represents a valid header, and ``False`` if
        it represents an invalid header, or the header not being in the
        request.

        For this class, it always returns ``True``.
        """
        return True
    __nonzero__ = __bool__  # Python 2

    def __contains__(self, offer):
        """
        Return ``bool`` indicating whether `offer` is acceptable.

        .. warning::

           The behavior of :meth:`AcceptEncodingValidHeader.__contains__` is
           currently being maintained for backward compatibility, but it will
           change in the future to better conform to the RFC.

        :param offer: (``str``) a content-coding or ``identity`` offer
        :return: (``bool``) Whether ``offer`` is acceptable according to the
                 header.

        The behavior of this method does not fully conform to :rfc:`7231`.
        It does not correctly interpret ``*``::

            >>> 'gzip' in AcceptEncodingValidHeader('gzip;q=0, *')
            True

        and does not handle the ``identity`` token correctly::

            >>> 'identity' in AcceptEncodingValidHeader('gzip')
            False
        """
        warnings.warn(
            'The behavior of AcceptEncodingValidHeader.__contains__ is '
            'currently being maintained for backward compatibility, but it '
            'will change in the future to better conform to the RFC.',
            DeprecationWarning,
        )
        for mask, quality in self._parsed_nonzero:
            if self._old_match(mask, offer):
                return True

    def __iter__(self):
        """
        Return all the ranges with non-0 qvalues, in order of preference.

        .. warning::

           The behavior of this method is currently maintained for backward
           compatibility, but will change in the future.

        :return: iterator of all the (content-coding/``identity``/``*``) items
                 in the header with non-0 qvalues, in descending order of
                 qvalue. If two items have the same qvalue, they are returned
                 in the order of their positions in the header, from left to
                 right.

        Please note that this is a simple filter for the items in the header
        with non-0 qvalues, and is not necessarily the same as what the client
        prefers, e.g. ``'gzip;q=0, *'`` means 'everything but gzip', but
        ``list(instance)`` would return only ``['*']``.
        """
        warnings.warn(
            'The behavior of AcceptEncodingLanguageValidHeader.__iter__ is '
            'currently maintained for backward compatibility, but will change'
            ' in the future.',
            DeprecationWarning,
        )

        for m,q in sorted(
            self._parsed_nonzero,
            key=lambda i: i[1],
            reverse=True
        ):
            yield m

    def __radd__(self, other):
        """
        Add to header, creating a new header object.

        See the docstring for :meth:`AcceptEncodingValidHeader.__add__`.
        """
        return self._add_instance_and_non_accept_encoding_type(
            instance=self, other=other, instance_on_the_right=True,
        )

    def __repr__(self):
        return '<{} ({!r})>'.format(self.__class__.__name__, str(self))

    def __str__(self):
        r"""
        Return a tidied up version of the header value.

        e.g. If the ``header_value`` is ``",\t, a ;\t q=0.20 , b ,',"``,
        ``str(instance)`` returns ``"a;q=0.2, b, '"``.
        """
        return ', '.join(
            _item_qvalue_pair_to_header_element(pair=tuple_)
            for tuple_ in self.parsed
        )

    def _add_instance_and_non_accept_encoding_type(
        self, instance, other, instance_on_the_right=False,
    ):
        if not other:
            return self.__class__(header_value=instance.header_value)

        other_header_value = self._python_value_to_header_str(value=other)

        if other_header_value == '':
            # if ``other`` is an object whose type we don't recognise, and
            # str(other) returns ''
            return self.__class__(header_value=instance.header_value)

        try:
            self.parse(value=other_header_value)
        except ValueError:  # invalid header value
            return self.__class__(header_value=instance.header_value)

        new_header_value = (
            (other_header_value + ', ' + instance.header_value)
            if instance_on_the_right
            else (instance.header_value + ', ' + other_header_value)
        )
        return self.__class__(header_value=new_header_value)

    def _old_match(self, mask, offer):
        """
        Return whether content-coding offer matches codings header item.

        .. warning::

           This is maintained for backward compatibility, and will be
           deprecated in the future.

        This method was WebOb's old criterion for deciding whether a
        content-coding offer matches a header item (content-coding,
        ``identity`` or ``*``), used in

        - :meth:`AcceptCharsetValidHeader.__contains__`
        - :meth:`AcceptCharsetValidHeader.best_match`
        - :meth:`AcceptCharsetValidHeader.quality`

        It does not conform to :rfc:`RFC 7231, section 5.3.4
        <7231#section-5.3.4>` in that it does not interpret ``*`` values in the
        header correctly: ``*`` should only match content-codings not mentioned
        elsewhere in the header.
        """
        return mask == '*' or offer.lower() == mask.lower()

    def acceptable_offers(self, offers):
        """
        Return the offers that are acceptable according to the header.

        The offers are returned in descending order of preference, where
        preference is indicated by the qvalue of the item (content-coding,
        "identity" or "*") in the header that matches the offer.

        This uses the matching rules described in :rfc:`RFC 7231, section 5.3.4
        <7231#section-5.3.4>`.

        :param offers: ``iterable`` of ``str``s, where each ``str`` is a
                       content-coding or the string ``identity`` (the token
                       used to represent "no encoding")
        :return: A list of tuples of the form (content-coding or "identity",
                 qvalue), in descending order of qvalue. Where two offers have
                 the same qvalue, they are returned in the same order as their
                 order in `offers`.

        Use the string ``'identity'`` (without the quotes) in `offers` to
        indicate an offer with no content-coding. From the RFC: 'If the
        representation has no content-coding, then it is acceptable by default
        unless specifically excluded by the Accept-Encoding field stating
        either "identity;q=0" or "\\*;q=0" without a more specific entry for
        "identity".' The RFC does not specify the qvalue that should be
        assigned to the representation/offer with no content-coding; this
        implementation assigns it a qvalue of 1.0.
        """
        lowercased_parsed = [
            (codings.lower(), qvalue) for (codings, qvalue) in self.parsed
        ]
        lowercased_offers = [offer.lower() for offer in offers]

        not_acceptable_codingss = set()
        acceptable_codingss = dict()
        asterisk_qvalue = None

        for codings, qvalue in lowercased_parsed:
            if codings == '*':
                if asterisk_qvalue is None:
                    asterisk_qvalue = qvalue
            elif (
                codings not in acceptable_codingss and codings not in
                not_acceptable_codingss
                # if we have not already encountered this codings in the header
            ):
                if qvalue == 0.0:
                    not_acceptable_codingss.add(codings)
                else:
                    acceptable_codingss[codings] = qvalue
        acceptable_codingss = list(acceptable_codingss.items())
        # Sort acceptable_codingss by qvalue, descending order
        acceptable_codingss.sort(key=lambda tuple_: tuple_[1], reverse=True)

        filtered_offers = []
        for index, offer in enumerate(lowercased_offers):
            # If offer matches a non-* codings with q=0, it is filtered out
            if any((
                (offer == codings) for codings in not_acceptable_codingss
            )):
                continue

            matched_codings_qvalue = None
            for codings, qvalue in acceptable_codingss:
                if offer == codings:
                    matched_codings_qvalue = qvalue
                    break
            else:
                if asterisk_qvalue:
                    matched_codings_qvalue = asterisk_qvalue
                elif asterisk_qvalue != 0.0 and offer == 'identity':
                    matched_codings_qvalue = 1.0
            if matched_codings_qvalue is not None:  # if there was a match
                filtered_offers.append((
                    offers[index], matched_codings_qvalue, index
                ))

        # sort by position in `offers` argument, ascending
        filtered_offers.sort(key=lambda tuple_: tuple_[2])
        # When qvalues are tied, position in `offers` is the tiebreaker.

        # sort by qvalue, descending
        filtered_offers.sort(key=lambda tuple_: tuple_[1], reverse=True)

        return [(item[0], item[1]) for item in filtered_offers]
        # (offer, qvalue), dropping the position

    def best_match(self, offers, default_match=None):
        """
        Return the best match from the sequence of `offers`.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future.

           :meth:`AcceptEncodingValidHeader.best_match` uses its own algorithm
           (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a
           best match. The algorithm has many issues, and does not conform to
           the RFC.

        Each offer in `offers` is checked against each non-``q=0`` item
        (content-coding/``identity``/``*``) in the header. If the two are a
        match according to WebOb's old criterion for a match, the quality value
        of the match is the qvalue of the item from the header multiplied by
        the server quality value of the offer (if the server quality value is
        not supplied, it is 1).

        The offer in the match with the highest quality value is the best
        match. If there is more than one match with the highest qvalue, the one
        that shows up first in `offers` is the best match.

        :param offers: (iterable)

                       | Each item in the iterable may be a ``str`` *codings*,
                         or a (*codings*, server quality value) ``tuple`` or
                         ``list``, where *codings* is either a content-coding,
                         or the string ``identity`` (which represents *no
                         encoding*). ``str`` and ``tuple``/``list`` elements
                         may be mixed within the iterable.

        :param default_match: (optional, any type) the value to be returned if
                              there is no match

        :return: (``str``, or the type of `default_match`)

                 | The offer that is the best match. If there is no match, the
                   value of `default_match` is returned.

        This method does not conform to :rfc:`RFC 7231, section 5.3.4
        <7231#section-5.3.4>`, in that it does not correctly interpret ``*``::

            >>> AcceptEncodingValidHeader('gzip;q=0, *').best_match(['gzip'])
            'gzip'

        and does not handle the ``identity`` token correctly::

            >>> instance = AcceptEncodingValidHeader('gzip')
            >>> instance.best_match(['identity']) is None
            True
        """
        warnings.warn(
            'The behavior of AcceptEncodingValidHeader.best_match is '
            'currently being maintained for backward compatibility, but it '
            'will be deprecated in the future, as it does not conform to the'
            ' RFC.',
            DeprecationWarning,
        )
        best_quality = -1
        best_offer = default_match
        matched_by = '*/*'
        for offer in offers:
            if isinstance(offer, (tuple, list)):
                offer, server_quality = offer
            else:
                server_quality = 1
            for item in self._parsed_nonzero:
                mask = item[0]
                quality = item[1]
                possible_quality = server_quality * quality
                if possible_quality < best_quality:
                    continue
                elif possible_quality == best_quality:
                    # 'text/plain' overrides 'message/*' overrides '*/*'
                    # (if all match w/ the same q=)
                    # [We can see that this was written for the Accept header,
                    # not the Accept-Encoding header.]
                    if matched_by.count('*') <= mask.count('*'):
                        continue
                if self._old_match(mask, offer):
                    best_quality = possible_quality
                    best_offer = offer
                    matched_by = mask
        return best_offer

    def quality(self, offer):
        """
        Return quality value of given offer, or ``None`` if there is no match.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future.

        :param offer: (``str``) A content-coding, or ``identity``.
        :return: (``float`` or ``None``)

                 | The quality value from the header item
                   (content-coding/``identity``/``*``) that matches the
                   `offer`, or ``None`` if there is no match.

        The behavior of this method does not conform to :rfc:`RFC 7231, section
        5.3.4<7231#section-5.3.4>`, in that it does not correctly interpret
        ``*``::

            >>> AcceptEncodingValidHeader('gzip;q=0, *').quality('gzip')
            1.0

        and does not handle the ``identity`` token correctly::

            >>> AcceptEncodingValidHeader('gzip').quality('identity') is None
            True
        """
        warnings.warn(
            'The behavior of AcceptEncodingValidHeader.quality is currently '
            'being maintained for backward compatibility, but it will be '
            'deprecated in the future, as it does not conform to the RFC.',
            DeprecationWarning,
        )
        bestq = 0
        for mask, q in self.parsed:
            if self._old_match(mask, offer):
                bestq = max(bestq, q)
        return bestq or None


class _AcceptEncodingInvalidOrNoHeader(AcceptEncoding):
    """
    Represent when an ``Accept-Encoding`` header is invalid or not in request.

    This is the base class for the behaviour that
    :class:`.AcceptEncodingInvalidHeader` and :class:`.AcceptEncodingNoHeader`
    have in common.

    :rfc:`7231` does not provide any guidance on what should happen if the
    ``AcceptEncoding`` header has an invalid value. This implementation
    disregards the header when the header is invalid, so
    :class:`.AcceptEncodingInvalidHeader` and :class:`.AcceptEncodingNoHeader`
    have much behaviour in common.
    """

    def __bool__(self):
        """
        Return whether ``self`` represents a valid ``Accept-Encoding`` header.

        Return ``True`` if ``self`` represents a valid header, and ``False`` if
        it represents an invalid header, or the header not being in the
        request.

        For this class, it always returns ``False``.
        """
        return False
    __nonzero__ = __bool__  # Python 2

    def __contains__(self, offer):
        """
        Return ``bool`` indicating whether `offer` is acceptable.

        .. warning::

           The behavior of ``.__contains__`` for the ``Accept-Encoding``
           classes is currently being maintained for backward compatibility,
           but it will change in the future to better conform to the RFC.

        :param offer: (``str``) a content-coding or ``identity`` offer
        :return: (``bool``) Whether ``offer`` is acceptable according to the
                 header.

        For this class, either there is no ``Accept-Encoding`` header in the
        request, or the header is invalid, so any content-coding is acceptable,
        and this always returns ``True``.
        """
        warnings.warn(
            'The behavior of .__contains__ for the Accept-Encoding classes is '
            'currently being maintained for backward compatibility, but it '
            'will change in the future to better conform to the RFC.',
            DeprecationWarning,
        )
        return True

    def __iter__(self):
        """
        Return all the header items with non-0 qvalues, in order of preference.

        .. warning::

           The behavior of this method is currently maintained for backward
           compatibility, but will change in the future.

        :return: iterator of all the (content-coding/``identity``/``*``) items
                 in the header with non-0 qvalues, in descending order of
                 qvalue. If two items have the same qvalue, they are returned
                 in the order of their positions in the header, from left to
                 right.

        When there is no ``Accept-Encoding`` header in the request or the
        header is invalid, there are no items in the header, so this always
        returns an empty iterator.
        """
        warnings.warn(
            'The behavior of AcceptEncodingValidHeader.__iter__ is currently '
            'maintained for backward compatibility, but will change in the '
            'future.',
            DeprecationWarning,
        )
        return iter(())

    def acceptable_offers(self, offers):
        """
        Return the offers that are acceptable according to the header.

        :param offers: ``iterable`` of ``str``s, where each ``str`` is a
                       content-coding or the string ``identity`` (the token
                       used to represent "no encoding")
        :return: When the header is invalid, or there is no ``Accept-Encoding``
                 header in the request, all `offers` are considered acceptable,
                 so this method returns a list of (content-coding or
                 "identity", qvalue) tuples where each offer in `offers` is
                 paired with the qvalue of 1.0, in the same order as in
                 `offers`.
        """
        return [(offer, 1.0) for offer in offers]

    def best_match(self, offers, default_match=None):
        """
        Return the best match from the sequence of `offers`.

        This is the ``.best_match()`` method for when the header is invalid or
        not found in the request, corresponding to
        :meth:`AcceptEncodingValidHeader.best_match`.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future (see the documentation for
           :meth:`AcceptEncodingValidHeader.best_match`).

        When the header is invalid, or there is no `Accept-Encoding` header in
        the request, all `offers` are considered acceptable, so the best match
        is the offer in `offers` with the highest server quality value (if the
        server quality value is not supplied for a media type, it is 1).

        If more than one offer in `offers` have the same highest server quality
        value, then the one that shows up first in `offers` is the best match.

        :param offers: (iterable)

                       | Each item in the iterable may be a ``str`` *codings*,
                         or a (*codings*, server quality value) ``tuple`` or
                         ``list``, where *codings* is either a content-coding,
                         or the string ``identity`` (which represents *no
                         encoding*). ``str`` and ``tuple``/``list`` elements
                         may be mixed within the iterable.

        :param default_match: (optional, any type) the value to be returned if
                              `offers` is empty.

        :return: (``str``, or the type of `default_match`)

                 | The offer that has the highest server quality value. If
                   `offers` is empty, the value of `default_match` is returned.
        """
        warnings.warn(
            'The behavior of .best_match for the Accept-Encoding classes is '
            'currently being maintained for backward compatibility, but the '
            'method will be deprecated in the future, as its behavior is not '
            'specified in (and currently does not conform to) RFC 7231.',
            DeprecationWarning,
        )
        best_quality = -1
        best_offer = default_match
        for offer in offers:
            if isinstance(offer, (list, tuple)):
                offer, quality = offer
            else:
                quality = 1
            if quality > best_quality:
                best_offer = offer
                best_quality = quality
        return best_offer

    def quality(self, offer):
        """
        Return quality value of given offer, or ``None`` if there is no match.

        This is the ``.quality()`` method for when the header is invalid or not
        found in the request, corresponding to
        :meth:`AcceptEncodingValidHeader.quality`.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future (see the documentation for
           :meth:`AcceptEncodingValidHeader.quality`).

        :param offer: (``str``) A content-coding, or ``identity``.
        :return: (``float``) ``1.0``.

        When the ``Accept-Encoding`` header is invalid or not in the request,
        all offers are equally acceptable, so 1.0 is always returned.
        """
        warnings.warn(
            'The behavior of .quality for the Accept-Encoding classes is '
            'currently being maintained for backward compatibility, but the '
            'method will be deprecated in the future, as its behavior does '
            'not conform to RFC 7231.',
            DeprecationWarning,
        )
        return 1.0


class AcceptEncodingNoHeader(_AcceptEncodingInvalidOrNoHeader):
    """
    Represent when there is no ``Accept-Encoding`` header in the request.

    This object should not be modified. To add to the header, we can use the
    addition operators (``+`` and ``+=``), which return a new object (see the
    docstring for :meth:`AcceptEncodingNoHeader.__add__`).
    """

    @property
    def header_value(self):
        """
        (``str`` or ``None``) The header value.

        As there is no header in the request, this is ``None``.
        """
        return self._header_value

    @property
    def parsed(self):
        """
        (``list`` or ``None``) Parsed form of the header.

        As there is no header in the request, this is ``None``.
        """
        return self._parsed

    def __init__(self):
        """
        Create an :class:`AcceptEncodingNoHeader` instance.
        """
        self._header_value = None
        self._parsed = None
        self._parsed_nonzero = None

    def copy(self):
        """
        Create a copy of the header object.

        """
        return self.__class__()

    def __add__(self, other):
        """
        Add to header, creating a new header object.

        `other` can be:

        * ``None``
        * a ``str`` header value
        * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as
          keys, and qvalue ``float``'s as values
        * a ``tuple`` or ``list``, where each item is either a header element
          ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple``
          or ``list``
        * an :class:`AcceptEncodingValidHeader`,
          :class:`AcceptEncodingNoHeader`, or
          :class:`AcceptEncodingInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``

        If `other` is a valid header value or an
        :class:`AcceptEncodingValidHeader` instance, a new
        :class:`AcceptEncodingValidHeader` instance with the valid header value
        is returned.

        If `other` is ``None``, an :class:`AcceptEncodingNoHeader` instance, an
        invalid header value, or an :class:`AcceptEncodingInvalidHeader`
        instance, a new :class:`AcceptEncodingNoHeader` instance is returned.
        """
        if isinstance(other, AcceptEncodingValidHeader):
            return AcceptEncodingValidHeader(header_value=other.header_value)

        if isinstance(
            other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader)
        ):
            return self.__class__()

        return self._add_instance_and_non_accept_encoding_type(
            instance=self, other=other,
        )

    def __radd__(self, other):
        """
        Add to header, creating a new header object.

        See the docstring for :meth:`AcceptEncodingNoHeader.__add__`.
        """
        return self.__add__(other=other)

    def __repr__(self):
        return '<{}>'.format(self.__class__.__name__)

    def __str__(self):
        """Return the ``str`` ``'<no header in request>'``."""
        return '<no header in request>'

    def _add_instance_and_non_accept_encoding_type(self, instance, other):
        if other is None:
            return self.__class__()

        other_header_value = self._python_value_to_header_str(value=other)

        try:
            return AcceptEncodingValidHeader(header_value=other_header_value)
        except ValueError:  # invalid header value
            return self.__class__()


class AcceptEncodingInvalidHeader(_AcceptEncodingInvalidOrNoHeader):
    """
    Represent an invalid ``Accept-Encoding`` header.

    An invalid header is one that does not conform to
    :rfc:`7231#section-5.3.4`.

    :rfc:`7231` does not provide any guidance on what should happen if the
    ``Accept-Encoding`` header has an invalid value. This implementation
    disregards the header, and treats it as if there is no ``Accept-Encoding``
    header in the request.

    This object should not be modified. To add to the header, we can use the
    addition operators (``+`` and ``+=``), which return a new object (see the
    docstring for :meth:`AcceptEncodingInvalidHeader.__add__`).
    """

    @property
    def header_value(self):
        """(``str`` or ``None``) The header value."""
        return self._header_value

    @property
    def parsed(self):
        """
        (``list`` or ``None``) Parsed form of the header.

        As the header is invalid and cannot be parsed, this is ``None``.
        """
        return self._parsed

    def __init__(self, header_value):
        """
        Create an :class:`AcceptEncodingInvalidHeader` instance.
        """
        self._header_value = header_value
        self._parsed = None
        self._parsed_nonzero = None

    def copy(self):
        """
        Create a copy of the header object.

        """
        return self.__class__(self._header_value)

    def __add__(self, other):
        """
        Add to header, creating a new header object.

        `other` can be:

        * ``None``
        * a ``str`` header value
        * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as
          keys, and qvalue ``float``'s as values
        * a ``tuple`` or ``list``, where each item is either a header element
          ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple``
          or ``list``
        * an :class:`AcceptEncodingValidHeader`,
          :class:`AcceptEncodingNoHeader`, or
          :class:`AcceptEncodingInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``

        If `other` is a valid header value or an
        :class:`AcceptEncodingValidHeader` instance, then a new
        :class:`AcceptEncodingValidHeader` instance with the valid header value
        is returned.

        If `other` is ``None``, an :class:`AcceptEncodingNoHeader` instance, an
        invalid header value, or an :class:`AcceptEncodingInvalidHeader`
        instance, a new :class:`AcceptEncodingNoHeader` instance is returned.
        """
        if isinstance(other, AcceptEncodingValidHeader):
            return AcceptEncodingValidHeader(header_value=other.header_value)

        if isinstance(
            other, (AcceptEncodingNoHeader, AcceptEncodingInvalidHeader)
        ):
            return AcceptEncodingNoHeader()

        return self._add_instance_and_non_accept_encoding_type(
            instance=self, other=other,
        )

    def __radd__(self, other):
        """
        Add to header, creating a new header object.

        See the docstring for :meth:`AcceptEncodingValidHeader.__add__`.
        """
        return self._add_instance_and_non_accept_encoding_type(
            instance=self, other=other, instance_on_the_right=True,
        )

    def __repr__(self):
        return '<{}>'.format(self.__class__.__name__)
        # We do not display the header_value, as it is untrusted input. The
        # header_value could always be easily obtained from the .header_value
        # property.

    def __str__(self):
        """Return the ``str`` ``'<invalid header value>'``."""
        return '<invalid header value>'

    def _add_instance_and_non_accept_encoding_type(
        self, instance, other, instance_on_the_right=False,
    ):
        if other is None:
            return AcceptEncodingNoHeader()

        other_header_value = self._python_value_to_header_str(value=other)

        try:
            return AcceptEncodingValidHeader(header_value=other_header_value)
        except ValueError:  # invalid header value
            return AcceptEncodingNoHeader()


def create_accept_encoding_header(header_value):
    """
    Create an object representing the ``Accept-Encoding`` header in a request.

    :param header_value: (``str``) header value
    :return: If `header_value` is ``None``, an :class:`AcceptEncodingNoHeader`
             instance.

             | If `header_value` is a valid ``Accept-Encoding`` header, an
               :class:`AcceptEncodingValidHeader` instance.

             | If `header_value` is an invalid ``Accept-Encoding`` header, an
               :class:`AcceptEncodingInvalidHeader` instance.
    """
    if header_value is None:
        return AcceptEncodingNoHeader()
    if isinstance(header_value, AcceptEncoding):
        return header_value.copy()
    try:
        return AcceptEncodingValidHeader(header_value=header_value)
    except ValueError:
        return AcceptEncodingInvalidHeader(header_value=header_value)


def accept_encoding_property():
    doc = """
        Property representing the ``Accept-Encoding`` header.

        (:rfc:`RFC 7231, section 5.3.4 <7231#section-5.3.4>`)

        The header value in the request environ is parsed and a new object
        representing the header is created every time we *get* the value of the
        property. (*set* and *del* change the header value in the request
        environ, and do not involve parsing.)
    """

    ENVIRON_KEY = 'HTTP_ACCEPT_ENCODING'

    def fget(request):
        """Get an object representing the header in the request."""
        return create_accept_encoding_header(
            header_value=request.environ.get(ENVIRON_KEY)
        )

    def fset(request, value):
        """
        Set the corresponding key in the request environ.

        `value` can be:

        * ``None``
        * a ``str`` header value
        * a ``dict``, with content-coding, ``identity`` or ``*`` ``str``'s as
          keys, and qvalue ``float``'s as values
        * a ``tuple`` or ``list``, where each item is either a header element
          ``str``, or a (content-coding/``identity``/``*``, qvalue) ``tuple``
          or ``list``
        * an :class:`AcceptEncodingValidHeader`,
          :class:`AcceptEncodingNoHeader`, or
          :class:`AcceptEncodingInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``
        """
        if value is None or isinstance(value, AcceptEncodingNoHeader):
            fdel(request=request)
        else:
            if isinstance(
                value, (AcceptEncodingValidHeader, AcceptEncodingInvalidHeader)
            ):
                header_value = value.header_value
            else:
                header_value = AcceptEncoding._python_value_to_header_str(
                    value=value,
                )
            request.environ[ENVIRON_KEY] = header_value

    def fdel(request):
        """Delete the corresponding key from the request environ."""
        try:
            del request.environ[ENVIRON_KEY]
        except KeyError:
            pass

    return property(fget, fset, fdel, textwrap.dedent(doc))


class AcceptLanguage(object):
    """
    Represent an ``Accept-Language`` header.

    Base class for :class:`AcceptLanguageValidHeader`,
    :class:`AcceptLanguageNoHeader`, and :class:`AcceptLanguageInvalidHeader`.
    """

    # RFC 7231 Section 5.3.5 "Accept-Language":
    # Accept-Language = 1#( language-range [ weight ] )
    # language-range  =
    #           <language-range, see [RFC4647], Section 2.1>
    # RFC 4647 Section 2.1 "Basic Language Range":
    # language-range   = (1*8ALPHA *("-" 1*8alphanum)) / "*"
    # alphanum         = ALPHA / DIGIT
    lang_range_re = (
        r'\*|'
        '(?:'
        '[A-Za-z]{1,8}'
        '(?:-[A-Za-z0-9]{1,8})*'
        ')'
    )
    lang_range_n_weight_re = _item_n_weight_re(item_re=lang_range_re)
    lang_range_n_weight_compiled_re = re.compile(lang_range_n_weight_re)
    accept_language_compiled_re = _list_1_or_more__compiled_re(
        element_re=lang_range_n_weight_re,
    )

    @classmethod
    def _python_value_to_header_str(cls, value):
        if isinstance(value, str):
            header_str = value
        else:
            if hasattr(value, 'items'):
                value = sorted(
                    value.items(),
                    key=lambda item: item[1],
                    reverse=True,
                )
            if isinstance(value, (tuple, list)):
                result = []
                for element in value:
                    if isinstance(element, (tuple, list)):
                        element = _item_qvalue_pair_to_header_element(
                            pair=element
                        )
                    result.append(element)
                header_str = ', '.join(result)
            else:
                header_str = str(value)
        return header_str

    @classmethod
    def parse(cls, value):
        """
        Parse an ``Accept-Language`` header.

        :param value: (``str``) header value
        :return: If `value` is a valid ``Accept-Language`` header, returns an
                 iterator of (language range, quality value) tuples, as parsed
                 from the header from left to right.
        :raises ValueError: if `value` is an invalid header
        """
        # Check if header is valid
        # Using Python stdlib's `re` module, there is currently no way to check
        # the match *and* get all the groups using the same regex, so we have
        # to use one regex to check the match, and another to get the groups.
        if cls.accept_language_compiled_re.match(value) is None:
            raise ValueError('Invalid value for an Accept-Language header.')
        def generator(value):
            for match in (
                cls.lang_range_n_weight_compiled_re.finditer(value)
            ):
                lang_range = match.group(1)
                qvalue = match.group(2)
                qvalue = float(qvalue) if qvalue else 1.0
                yield (lang_range, qvalue)
        return generator(value=value)


class AcceptLanguageValidHeader(AcceptLanguage):
    """
    Represent a valid ``Accept-Language`` header.

    A valid header is one that conforms to :rfc:`RFC 7231, section 5.3.5
    <7231#section-5.3.5>`.

    We take the reference from the ``language-range`` syntax rule in :rfc:`RFC
    7231, section 5.3.5 <7231#section-5.3.5>` to :rfc:`RFC 4647, section 2.1
    <4647#section-2.1>` to mean that only basic language ranges (and not
    extended language ranges) are expected in the ``Accept-Language`` header.

    This object should not be modified. To add to the header, we can use the
    addition operators (``+`` and ``+=``), which return a new object (see the
    docstring for :meth:`AcceptLanguageValidHeader.__add__`).
    """

    def __init__(self, header_value):
        """
        Create an :class:`AcceptLanguageValidHeader` instance.

        :param header_value: (``str``) header value.
        :raises ValueError: if `header_value` is an invalid value for an
                            ``Accept-Language`` header.
        """
        self._header_value = header_value
        self._parsed = list(self.parse(header_value))
        self._parsed_nonzero = [item for item in self.parsed if item[1]]
        # item[1] is the qvalue

    def copy(self):
        """
        Create a copy of the header object.

        """
        return self.__class__(self._header_value)

    @property
    def header_value(self):
        """(``str`` or ``None``) The header value."""
        return self._header_value

    @property
    def parsed(self):
        """
        (``list`` or ``None``) Parsed form of the header.

        A list of (language range, quality value) tuples.
        """
        return self._parsed

    def __add__(self, other):
        """
        Add to header, creating a new header object.

        `other` can be:

        * ``None``
        * a ``str``
        * a ``dict``, with language ranges as keys and qvalues as values
        * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple``
          or ``list`` (language range, qvalue) pairs (``str``'s and pairs can
          be mixed within the ``tuple`` or ``list``)
        * an :class:`AcceptLanguageValidHeader`,
          :class:`AcceptLanguageNoHeader`, or
          :class:`AcceptLanguageInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``

        If `other` is a valid header value or another
        :class:`AcceptLanguageValidHeader` instance, the two header values are
        joined with ``', '``, and a new :class:`AcceptLanguageValidHeader`
        instance with the new header value is returned.

        If `other` is ``None``, an :class:`AcceptLanguageNoHeader` instance, an
        invalid header value, or an :class:`AcceptLanguageInvalidHeader`
        instance, a new :class:`AcceptLanguageValidHeader` instance with the
        same header value as ``self`` is returned.
        """
        if isinstance(other, AcceptLanguageValidHeader):
            return create_accept_language_header(
                header_value=self.header_value + ', ' + other.header_value,
            )

        if isinstance(
            other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader)
        ):
            return self.__class__(header_value=self.header_value)

        return self._add_instance_and_non_accept_language_type(
            instance=self, other=other,
        )

    def __nonzero__(self):
        """
        Return whether ``self`` represents a valid ``Accept-Language`` header.

        Return ``True`` if ``self`` represents a valid header, and ``False`` if
        it represents an invalid header, or the header not being in the
        request.

        For this class, it always returns ``True``.
        """
        return True
    __bool__ = __nonzero__  # Python 3

    def __contains__(self, offer):
        """
        Return ``bool`` indicating whether `offer` is acceptable.

        .. warning::

           The behavior of :meth:`AcceptLanguageValidHeader.__contains__` is
           currently being maintained for backward compatibility, but it will
           change in the future to better conform to the RFC.

           What is 'acceptable' depends on the needs of your application.
           :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` suggests three
           matching schemes from :rfc:`RFC 4647 <4647>`, two of which WebOb
           supports with :meth:`AcceptLanguageValidHeader.basic_filtering` and
           :meth:`AcceptLanguageValidHeader.lookup` (we interpret the RFC to
           mean that Extended Filtering cannot apply for the
           ``Accept-Language`` header, as the header only accepts basic
           language ranges.) If these are not suitable for the needs of your
           application, you may need to write your own matching using
           :attr:`AcceptLanguageValidHeader.parsed`.

        :param offer: (``str``) language tag offer
        :return: (``bool``) Whether ``offer`` is acceptable according to the
                 header.

        This uses the old criterion of a match in
        :meth:`AcceptLanguageValidHeader._old_match`, which does not conform to
        :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` or any of the
        matching schemes suggested there. It also does not properly take into
        account ranges with ``q=0`` in the header::

            >>> 'en-gb' in AcceptLanguageValidHeader('en, en-gb;q=0')
            True
            >>> 'en' in AcceptLanguageValidHeader('en;q=0, *')
            True

        (See the docstring for :meth:`AcceptLanguageValidHeader._old_match` for
        other problems with the old criterion for a match.)
        """
        warnings.warn(
            'The behavior of AcceptLanguageValidHeader.__contains__ is '
            'currently being maintained for backward compatibility, but it '
            'will change in the future to better conform to the RFC.',
            DeprecationWarning,
        )
        for mask, quality in self._parsed_nonzero:
            if self._old_match(mask, offer):
                return True
        return False

    def __iter__(self):
        """
        Return all the ranges with non-0 qvalues, in order of preference.

        .. warning::

           The behavior of this method is currently maintained for backward
           compatibility, but will change in the future.

        :return: iterator of all the language ranges in the header with non-0
                 qvalues, in descending order of qvalue. If two ranges have the
                 same qvalue, they are returned in the order of their positions
                 in the header, from left to right.

        Please note that this is a simple filter for the ranges in the header
        with non-0 qvalues, and is not necessarily the same as what the client
        prefers, e.g. ``'en-gb;q=0, *'`` means 'everything but British
        English', but ``list(instance)`` would return only ``['*']``.
        """
        warnings.warn(
            'The behavior of AcceptLanguageValidHeader.__iter__ is currently '
            'maintained for backward compatibility, but will change in the '
            'future.',
            DeprecationWarning,
        )

        for m, q in sorted(
            self._parsed_nonzero,
            key=lambda i: i[1],
            reverse=True
        ):
            yield m

    def __radd__(self, other):
        """
        Add to header, creating a new header object.

        See the docstring for :meth:`AcceptLanguageValidHeader.__add__`.
        """
        return self._add_instance_and_non_accept_language_type(
            instance=self, other=other, instance_on_the_right=True,
        )

    def __repr__(self):
        return '<{} ({!r})>'.format(self.__class__.__name__, str(self))

    def __str__(self):
        r"""
        Return a tidied up version of the header value.

        e.g. If the ``header_value`` is ``', \t,de;q=0.000 \t, es;q=1.000, zh,
        jp;q=0.210  ,'``, ``str(instance)`` returns ``'de;q=0, es, zh,
        jp;q=0.21'``.
        """
        return ', '.join(
            _item_qvalue_pair_to_header_element(pair=tuple_)
            for tuple_ in self.parsed
        )

    def _add_instance_and_non_accept_language_type(
        self, instance, other, instance_on_the_right=False,
    ):
        if not other:
            return self.__class__(header_value=instance.header_value)

        other_header_value = self._python_value_to_header_str(value=other)

        try:
            self.parse(value=other_header_value)
        except ValueError:  # invalid header value
            return self.__class__(header_value=instance.header_value)

        new_header_value = (
            (other_header_value + ', ' + instance.header_value)
            if instance_on_the_right
            else (instance.header_value + ', ' + other_header_value)
        )
        return self.__class__(header_value=new_header_value)

    def _old_match(self, mask, item):
        """
        Return whether a language tag matches a language range.

        .. warning::

           This is maintained for backward compatibility, and will be
           deprecated in the future.

        This method was WebOb's old criterion for deciding whether a language
        tag matches a language range, used in

        - :meth:`AcceptLanguageValidHeader.__contains__`
        - :meth:`AcceptLanguageValidHeader.best_match`
        - :meth:`AcceptLanguageValidHeader.quality`

        It does not conform to :rfc:`RFC 7231, section 5.3.5
        <7231#section-5.3.5>`, or any of the matching schemes suggested there.

        :param mask: (``str``)

                     | language range

        :param item: (``str``)

                     | language tag. Subtags in language tags are separated by
                       ``-`` (hyphen). If there are underscores (``_``) in this
                       argument, they will be converted to hyphens before
                       checking the match.

        :return: (``bool``) whether the tag in `item` matches the range in
                 `mask`.

        `mask` and `item` are a match if:

        - ``mask == *``.
        - ``mask == item``.
        - If the first subtag of `item` equals `mask`, or if the first subtag
          of `mask` equals `item`.
          This means that::

              >>> instance._old_match(mask='en-gb', item='en')
              True
              >>> instance._old_match(mask='en', item='en-gb')
              True

          Which is different from any of the matching schemes suggested in
          :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, in that none of
          those schemes match both more *and* less specific tags.

          However, this method appears to be only designed for language tags
          and ranges with at most two subtags. So with an `item`/language tag
          with more than two subtags like ``zh-Hans-CN``::

              >>> instance._old_match(mask='zh', item='zh-Hans-CN')
              True
              >>> instance._old_match(mask='zh-Hans', item='zh-Hans-CN')
              False

          From commit history, this does not appear to have been from a
          decision to match only the first subtag, but rather because only
          language ranges and tags with at most two subtags were expected.
        """
        item = item.replace('_', '-').lower()
        mask = mask.lower()
        return (mask == '*'
            or item == mask
            or item.split('-')[0] == mask
            or item == mask.split('-')[0]
        )

    def basic_filtering(self, language_tags):
        """
        Return the tags that match the header, using Basic Filtering.

        This is an implementation of the Basic Filtering matching scheme,
        suggested as a matching scheme for the ``Accept-Language`` header in
        :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, and defined in
        :rfc:`RFC 4647, section 3.3.1 <4647#section-3.3.1>`. It filters the
        tags in the `language_tags` argument and returns the ones that match
        the header according to the matching scheme.

        :param language_tags: (``iterable``) language tags
        :return: A list of tuples of the form (language tag, qvalue), in
                 descending order of qvalue. If two or more tags have the same
                 qvalue, they are returned in the same order as that in the
                 header of the ranges they matched. If the matched range is the
                 same for two or more tags (i.e. their matched ranges have the
                 same qvalue and the same position in the header), then they
                 are returned in the same order as that in the `language_tags`
                 argument. If `language_tags` is unordered, e.g. if it is a set
                 or a dict, then that order may not be reliable.

        For each tag in `language_tags`:

        1. If the tag matches a non-``*`` language range in the header with
           ``q=0`` (meaning "not acceptable", see :rfc:`RFC 7231, section 5.3.1
           <7231#section-5.3.1>`), the tag is filtered out.
        2. The non-``*`` language ranges in the header that do not have ``q=0``
           are considered in descending order of qvalue; where two or more
           language ranges have the same qvalue, they are considered in the
           order in which they appear in the header.
        3. A language range 'matches a particular language tag if, in a
           case-insensitive comparison, it exactly equals the tag, or if it
           exactly equals a prefix of the tag such that the first character
           following the prefix is "-".' (:rfc:`RFC 4647, section 3.3.1
           <4647#section-3.3.1>`)
        4. If the tag does not match any of the non-``*`` language ranges, and
           there is a ``*`` language range in the header, then if the ``*``
           language range has ``q=0``, the language tag is filtered out,
           otherwise the tag is considered a match.

        (If a range (``*`` or non-``*``) appears in the header more than once
        -- this would not make sense, but is nonetheless a valid header
        according to the RFC -- the first in the header is used for matching,
        and the others are ignored.)
        """
        # The Basic Filtering matching scheme as applied to the Accept-Language
        # header is very under-specified by RFCs 7231 and 4647. This
        # implementation combines the description of the matching scheme in RFC
        # 4647 and the rules of the Accept-Language header in RFC 7231 to
        # arrive at an algorithm for Basic Filtering as applied to the
        # Accept-Language header.

        lowercased_parsed = [
            (range_.lower(), qvalue) for (range_, qvalue) in self.parsed
        ]
        lowercased_tags = [tag.lower() for tag in language_tags]

        not_acceptable_ranges = set()
        acceptable_ranges = dict()
        asterisk_qvalue = None

        for position_in_header, (range_, qvalue) in enumerate(
            lowercased_parsed
        ):
            if range_ == '*':
                if asterisk_qvalue is None:
                    asterisk_qvalue = qvalue
                    asterisk_position = position_in_header
            elif (
                range_ not in acceptable_ranges and range_ not in
                not_acceptable_ranges
                # if we have not already encountered this range in the header
            ):
                if qvalue == 0.0:
                    not_acceptable_ranges.add(range_)
                else:
                    acceptable_ranges[range_] = (qvalue, position_in_header)
        acceptable_ranges = [
            (range_, qvalue, position_in_header)
            for range_, (qvalue, position_in_header)
            in acceptable_ranges.items()
        ]
        # Sort acceptable_ranges by position_in_header, ascending order
        acceptable_ranges.sort(key=lambda tuple_: tuple_[2])
        # Sort acceptable_ranges by qvalue, descending order
        acceptable_ranges.sort(key=lambda tuple_: tuple_[1], reverse=True)
        # Sort guaranteed to be stable with Python >= 2.2, so position in
        # header is tiebreaker when two ranges have the same qvalue

        def match(tag, range_):
            # RFC 4647, section 2.1: 'A language range matches a particular
            # language tag if, in a case-insensitive comparison, it exactly
            # equals the tag, or if it exactly equals a prefix of the tag such
            # that the first character following the prefix is "-".'
            return (tag == range_) or tag.startswith(range_ + '-')
            # We can assume here that the language tags are valid tags, so we
            # do not have to worry about them being malformed and ending with
            # '-'.

        filtered_tags = []
        for index, tag in enumerate(lowercased_tags):
            # If tag matches a non-* range with q=0, it is filtered out
            if any((
                match(tag=tag, range_=range_)
                for range_ in not_acceptable_ranges
            )):
                continue

            matched_range_qvalue = None
            for range_, qvalue, position_in_header in acceptable_ranges:
                # acceptable_ranges is in descending order of qvalue, and tied
                # ranges are in ascending order of position_in_header, so the
                # first range_ that matches the tag is the best match
                if match(tag=tag, range_=range_):
                    matched_range_qvalue = qvalue
                    matched_range_position = position_in_header
                    break
            else:
                if asterisk_qvalue:
                    # From RFC 4647, section 3.3.1: '...HTTP/1.1 [RFC2616]
                    # specifies that the range "*" matches only languages not
                    # matched by any other range within an "Accept-Language"
                    # header.' (Though RFC 2616 is obsolete, and there is no
                    # mention of the meaning of "*" in RFC 7231, as the
                    # ``language-range`` syntax rule in RFC 7231 section 5.3.1
                    # directs us to RFC 4647, we can only assume that the
                    # meaning of "*" in the Accept-Language header remains the
                    # same).
                    matched_range_qvalue = asterisk_qvalue
                    matched_range_position = asterisk_position
            if matched_range_qvalue is not None:  # if there was a match
                filtered_tags.append((
                    language_tags[index], matched_range_qvalue,
                    matched_range_position
                ))

        # sort by matched_range_position, ascending
        filtered_tags.sort(key=lambda tuple_: tuple_[2])
        # When qvalues are tied, matched range position in the header is the
        # tiebreaker.

        # sort by qvalue, descending
        filtered_tags.sort(key=lambda tuple_: tuple_[1], reverse=True)

        return [(item[0], item[1]) for item in filtered_tags]
        # (tag, qvalue), dropping the matched_range_position

        # We return a list of tuples with qvalues, instead of just a set or
        # a list of language tags, because
        # RFC 4647 section 3.3: "If the language priority list contains more
        # than one range, the content returned is typically ordered in
        # descending level of preference, but it MAY be unordered, according to
        # the needs of the application or protocol."
        # We return the filtered tags in order of preference, each paired with
        # the qvalue of the range that was their best match, as the ordering
        # and the qvalues may well be needed in some applications, and a simple
        # set or list of language tags can always be easily obtained from the
        # returned list if the qvalues are not required. One use for qvalues,
        # for example, would be to indicate that two tags are equally preferred
        # (same qvalue), which we would not be able to do easily with a set or
        # a list without e.g. making a member of the set or list a sequence.

    def best_match(self, offers, default_match=None):
        """
        Return the best match from the sequence of language tag `offers`.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future.

           :meth:`AcceptLanguageValidHeader.best_match` uses its own algorithm
           (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a
           best match. The algorithm has many issues, and does not conform to
           :rfc:`RFC 7231 <7231>`.

           :meth:`AcceptLanguageValidHeader.lookup` is a possible alternative
           for finding a best match -- it conforms to, and is suggested as a
           matching scheme for the ``Accept-Language`` header in, :rfc:`RFC
           7231, section 5.3.5 <7231#section-5.3.5>` -- but please be aware
           that there are differences in how it determines what is a best
           match. If that is not suitable for the needs of your application,
           you may need to write your own matching using
           :attr:`AcceptLanguageValidHeader.parsed`.

        Each language tag in `offers` is checked against each non-0 range in
        the header. If the two are a match according to WebOb's old criterion
        for a match, the quality value of the match is the qvalue of the
        language range from the header multiplied by the server quality value
        of the offer (if the server quality value is not supplied, it is 1).

        The offer in the match with the highest quality value is the best
        match. If there is more than one match with the highest qvalue, the
        match where the language range has a lower number of '*'s is the best
        match. If the two have the same number of '*'s, the one that shows up
        first in `offers` is the best match.

        :param offers: (iterable)

                       | Each item in the iterable may be a ``str`` language
                         tag, or a (language tag, server quality value)
                         ``tuple`` or ``list``. (The two may be mixed in the
                         iterable.)

        :param default_match: (optional, any type) the value to be returned if
                              there is no match

        :return: (``str``, or the type of `default_match`)

                 | The language tag that is the best match. If there is no
                   match, the value of `default_match` is returned.


        **Issues**:

        - Incorrect tiebreaking when quality values of two matches are the same
          (https://github.com/Pylons/webob/issues/256)::

              >>> header = AcceptLanguageValidHeader(
              ...     header_value='en-gb;q=1, en;q=0.8'
              ... )
              >>> header.best_match(offers=['en', 'en-GB'])
              'en'
              >>> header.best_match(offers=['en-GB', 'en'])
              'en-GB'

              >>> header = AcceptLanguageValidHeader(header_value='en-gb, en')
              >>> header.best_match(offers=['en', 'en-gb'])
              'en'
              >>> header.best_match(offers=['en-gb', 'en'])
              'en-gb'

        - Incorrect handling of ``q=0``::

              >>> header = AcceptLanguageValidHeader(header_value='en;q=0, *')
              >>> header.best_match(offers=['en'])
              'en'

              >>> header = AcceptLanguageValidHeader(header_value='fr, en;q=0')
              >>> header.best_match(offers=['en-gb'], default_match='en')
              'en'

        - Matching only takes into account the first subtag when matching a
          range with more specific or less specific tags::

              >>> header = AcceptLanguageValidHeader(header_value='zh')
              >>> header.best_match(offers=['zh-Hans-CN'])
              'zh-Hans-CN'
              >>> header = AcceptLanguageValidHeader(header_value='zh-Hans')
              >>> header.best_match(offers=['zh-Hans-CN'])
              >>> header.best_match(offers=['zh-Hans-CN']) is None
              True

              >>> header = AcceptLanguageValidHeader(header_value='zh-Hans-CN')
              >>> header.best_match(offers=['zh'])
              'zh'
              >>> header.best_match(offers=['zh-Hans'])
              >>> header.best_match(offers=['zh-Hans']) is None
              True

        """
        warnings.warn(
            'The behavior of AcceptLanguageValidHeader.best_match is '
            'currently being maintained for backward compatibility, but it '
            'will be deprecated in the future as it does not conform to the '
            'RFC.',
            DeprecationWarning,
        )
        best_quality = -1
        best_offer = default_match
        matched_by = '*/*'
        # [We can see that this was written for the ``Accept`` header and not
        # the ``Accept-Language`` header, as there are no '/'s in a valid
        # ``Accept-Language`` header.]
        for offer in offers:
            if isinstance(offer, (tuple, list)):
                offer, server_quality = offer
            else:
                server_quality = 1
            for mask, quality in self._parsed_nonzero:
                possible_quality = server_quality * quality
                if possible_quality < best_quality:
                    continue
                elif possible_quality == best_quality:
                    # 'text/plain' overrides 'message/*' overrides '*/*'
                    # (if all match w/ the same q=)
                    if matched_by.count('*') <= mask.count('*'):
                        continue
                    # [This tiebreaking was written for the `Accept` header. A
                    # basic language range in a valid ``Accept-Language``
                    # header can only be either '*' or a range with no '*' in
                    # it. This happens to work here, but is not sufficient as a
                    # tiebreaker.
                    #
                    # A best match here, given this algorithm uses
                    # self._old_match() which matches both more *and* less
                    # specific tags, should be the match where the absolute
                    # value of the difference between the subtag counts of
                    # `mask` and `offer` is the lowest.]
                if self._old_match(mask, offer):
                    best_quality = possible_quality
                    best_offer = offer
                    matched_by = mask
        return best_offer

    def lookup(
        self, language_tags, default_range=None, default_tag=None,
        default=None,
    ):
        """
        Return the language tag that best matches the header, using Lookup.

        This is an implementation of the Lookup matching scheme,
        suggested as a matching scheme for the ``Accept-Language`` header in
        :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`, and described in
        :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`.

        Each language range in the header is considered in turn, by descending
        order of qvalue; where qvalues are tied, ranges are considered from
        left to right.

        Each language range in the header represents the most specific tag that
        is an acceptable match: Lookup progressively truncates subtags from the
        end of the range until a matching language tag is found. An example is
        given in :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`, under
        "Example of a Lookup Fallback Pattern":

        ::

            Range to match: zh-Hant-CN-x-private1-private2
            1. zh-Hant-CN-x-private1-private2
            2. zh-Hant-CN-x-private1
            3. zh-Hant-CN
            4. zh-Hant
            5. zh
            6. (default)

        :param language_tags: (``iterable``) language tags

        :param default_range: (optional, ``None`` or ``str``)

                              | If Lookup finds no match using the ranges in
                                the header, and this argument is not None,
                                Lookup will next attempt to match the range in
                                this argument, using the same subtag
                                truncation.

                              | `default_range` cannot be '*', as '*' is
                                skipped in Lookup. See :ref:`note
                                <acceptparse-lookup-asterisk-note>`.

                              | This parameter corresponds to the functionality
                                described in :rfc:`RFC 4647, section 3.4.1
                                <4647#section-3.4.1>`, in the paragraph
                                starting with "One common way to provide for a
                                default is to allow a specific language range
                                to be set as the default..."

        :param default_tag: (optional, ``None`` or ``str``)

                            | At least one of `default_tag` or `default` must
                              be supplied as an argument to the method, to
                              define the defaulting behaviour.

                            | If Lookup finds no match using the ranges in the
                              header and `default_range`, this argument is not
                              ``None``, and it does not match any range in the
                              header with ``q=0`` (exactly, with no subtag
                              truncation), then this value is returned.

                            | This parameter corresponds to "return a
                              particular language tag designated for the
                              operation", one of the examples of "defaulting
                              behavior" described in :rfc:`RFC 4647, section
                              3.4.1 <4647#section-3.4.1>`.

        :param default: (optional, ``None`` or any type, including a callable)

                        | At least one of `default_tag` or `default` must be
                          supplied as an argument to the method, to define the
                          defaulting behaviour.

                        | If Lookup finds no match using the ranges in the
                          header and `default_range`, and `default_tag` is
                          ``None`` or not acceptable because it matches a
                          ``q=0`` range in the header, then Lookup will next
                          examine the `default` argument.

                        | If `default` is a callable, it will be called, and
                          the callable's return value will be returned.

                        | If `default` is not a callable, the value itself will
                          be returned.

                        | The difference between supplying a ``str`` to
                          `default_tag` and `default` is that `default_tag` is
                          checked against ``q=0`` ranges in the header to see
                          if it matches one of the ranges specified as not
                          acceptable, whereas a ``str`` for the `default`
                          argument is simply returned.

                        | This parameter corresponds to the "defaulting
                          behavior" described in :rfc:`RFC 4647, section 3.4.1
                          <4647#section-3.4.1>`

        :return: (``str``, ``None``, or any type)

                 | The best match according to the Lookup matching scheme, or a
                   return value from one of the default arguments.

        **Notes**:

        .. _acceptparse-lookup-asterisk-note:

        - Lookup's behaviour with '*' language ranges in the header may be
          surprising. From :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`:

              In the lookup scheme, this range does not convey enough
              information by itself to determine which language tag is most
              appropriate, since it matches everything.  If the language range
              "*" is followed by other language ranges, it is skipped.  If the
              language range "*" is the only one in the language priority list
              or if no other language range follows, the default value is
              computed and returned.

          So

          ::

              >>> header = AcceptLanguageValidHeader('de, zh, *')
              >>> header.lookup(language_tags=['ja', 'en'], default='default')
              'default'

        - Any tags in `language_tags` and `default_tag` and any tag matched
          during the subtag truncation search for `default_range`, that are an
          exact match for a non-``*`` range with ``q=0`` in the header, are
          considered not acceptable and ruled out.

        - If there is a ``*;q=0`` in the header, then `default_range` and
          `default_tag` have no effect, as ``*;q=0`` means that all languages
          not already matched by other ranges within the header are
          unacceptable.
        """
        if default_tag is None and default is None:
            raise TypeError(
                '`default_tag` and `default` arguments cannot both be None.'
            )

        # We need separate `default_tag` and `default` arguments because if we
        # only had the `default` argument, there would be no way to tell
        # whether a str is a language tag (in which case we have to check
        # whether it has been specified as not acceptable with a q=0 range in
        # the header) or not (in which case we can just return the value).

        if default_range == '*':
            raise ValueError('default_range cannot be *.')

        parsed = list(self.parsed)

        tags = language_tags
        not_acceptable_ranges = []
        acceptable_ranges = []

        asterisk_non0_found = False
        # Whether there is a '*' range in the header with q={not 0}

        asterisk_q0_found = False
        # Whether there is a '*' range in the header with q=0
        # While '*' is skipped in Lookup because it "does not convey enough
        # information by itself to determine which language tag is most
        # appropriate" (RFC 4647, section 3.4), '*;q=0' is clear in meaning:
        # languages not matched by any other range within the header are not
        # acceptable.

        for range_, qvalue in parsed:
            if qvalue == 0.0:
                if range_ == '*':  # *;q=0
                    asterisk_q0_found = True
                else:  # {non-* range};q=0
                    not_acceptable_ranges.append(range_.lower())
            elif not asterisk_q0_found and range_ == '*':  # *;q={not 0}
                asterisk_non0_found = True
                # if asterisk_q0_found, then it does not matter whether
                # asterisk_non0_found
            else:  # {non-* range};q={not 0}
                acceptable_ranges.append((range_, qvalue))
        # Sort acceptable_ranges by qvalue, descending order
        acceptable_ranges.sort(key=lambda tuple_: tuple_[1], reverse=True)
        # Sort guaranteed to be stable with Python >= 2.2, so position in
        # header is tiebreaker when two ranges have the same qvalue

        acceptable_ranges = [tuple_[0] for tuple_ in acceptable_ranges]
        lowered_tags = [tag.lower() for tag in tags]

        def best_match(range_):
            subtags = range_.split('-')
            while True:
                for index, tag in enumerate(lowered_tags):
                    if tag in not_acceptable_ranges:
                        continue
                        # We think a non-'*' range with q=0 represents only
                        # itself as a tag, and there should be no falling back
                        # with subtag truncation. For example, with
                        # 'en-gb;q=0', it should not mean 'en;q=0': the client
                        # is unlikely to expect that specifying 'en-gb' as not
                        # acceptable would mean that 'en' is also not
                        # acceptable. There is no guidance on this at all in
                        # the RFCs, so it is left to us to decide how it should
                        # work.

                    if tag == range_:
                        return tags[index]  # return the pre-lowered tag

                try:
                    subtag_before_this = subtags[-2]
                except IndexError:  # len(subtags) == 1
                    break
                # len(subtags) >= 2
                if len(subtag_before_this) == 1 and (
                    subtag_before_this.isdigit() or
                    subtag_before_this.isalpha()
                ):  # if subtag_before_this is a single-letter or -digit subtag
                    subtags.pop(-1)  # pop twice instead of once
                subtags.pop(-1)
                range_ = '-'.join(subtags)

        for range_ in acceptable_ranges:
            match = best_match(range_=range_.lower())
            if match is not None:
                return match

        if not asterisk_q0_found:
            if default_range is not None:
                lowered_default_range = default_range.lower()
                match = best_match(range_=lowered_default_range)
                if match is not None:
                    return match

            if default_tag is not None:
                lowered_default_tag = default_tag.lower()
                if lowered_default_tag not in not_acceptable_ranges:
                    return default_tag

        try:
            return default()
        except TypeError:  # default is not a callable
            return default

    def quality(self, offer):
        """
        Return quality value of given offer, or ``None`` if there is no match.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future.

           :meth:`AcceptLanguageValidHeader.quality` uses its own algorithm
           (one not specified in :rfc:`RFC 7231 <7231>`) to determine what is a
           best match. The algorithm has many issues, and does not conform to
           :rfc:`RFC 7231 <7231>`.

           What should be considered a match depends on the needs of your
           application (for example, should a language range in the header
           match a more specific language tag offer, or a less specific tag
           offer?) :rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>` suggests
           three matching schemes from :rfc:`RFC 4647 <4647>`, two of which
           WebOb supports with
           :meth:`AcceptLanguageValidHeader.basic_filtering` and
           :meth:`AcceptLanguageValidHeader.lookup` (we interpret the RFC to
           mean that Extended Filtering cannot apply for the
           ``Accept-Language`` header, as the header only accepts basic
           language ranges.) :meth:`AcceptLanguageValidHeader.basic_filtering`
           returns quality values with the matched language tags.
           :meth:`AcceptLanguageValidHeader.lookup` returns a language tag
           without the quality value, but the quality value is less likely to
           be useful when we are looking for a best match.

           If these are not suitable or sufficient for the needs of your
           application, you may need to write your own matching using
           :attr:`AcceptLanguageValidHeader.parsed`.

        :param offer: (``str``) language tag offer
        :return: (``float`` or ``None``)

                 | The highest quality value from the language range(s) that
                   match the `offer`, or ``None`` if there is no match.


        **Issues**:

        - Incorrect handling of ``q=0`` and ``*``::

              >>> header = AcceptLanguageValidHeader(header_value='en;q=0, *')
              >>> header.quality(offer='en')
              1.0

        - Matching only takes into account the first subtag when matching a
          range with more specific or less specific tags::

              >>> header = AcceptLanguageValidHeader(header_value='zh')
              >>> header.quality(offer='zh-Hans-CN')
              1.0
              >>> header = AcceptLanguageValidHeader(header_value='zh-Hans')
              >>> header.quality(offer='zh-Hans-CN')
              >>> header.quality(offer='zh-Hans-CN') is None
              True

              >>> header = AcceptLanguageValidHeader(header_value='zh-Hans-CN')
              >>> header.quality(offer='zh')
              1.0
              >>> header.quality(offer='zh-Hans')
              >>> header.quality(offer='zh-Hans') is None
              True

        """
        warnings.warn(
            'The behavior of AcceptLanguageValidHeader.quality is'
            'currently being maintained for backward compatibility, but it '
            'will be deprecated in the future as it does not conform to the '
            'RFC.',
            DeprecationWarning,
        )
        bestq = 0
        for mask, q in self.parsed:
            if self._old_match(mask, offer):
                bestq = max(bestq, q)
        return bestq or None


class _AcceptLanguageInvalidOrNoHeader(AcceptLanguage):
    """
    Represent when an ``Accept-Language`` header is invalid or not in request.

    This is the base class for the behaviour that
    :class:`.AcceptLanguageInvalidHeader` and :class:`.AcceptLanguageNoHeader`
    have in common.

    :rfc:`7231` does not provide any guidance on what should happen if the
    ``Accept-Language`` header has an invalid value. This implementation
    disregards the header when the header is invalid, so
    :class:`.AcceptLanguageInvalidHeader` and :class:`.AcceptLanguageNoHeader`
    have much behaviour in common.
    """

    def __nonzero__(self):
        """
        Return whether ``self`` represents a valid ``Accept-Language`` header.

        Return ``True`` if ``self`` represents a valid header, and ``False`` if
        it represents an invalid header, or the header not being in the
        request.

        For this class, it always returns ``False``.
        """
        return False
    __bool__ = __nonzero__  # Python 3

    def __contains__(self, offer):
        """
        Return ``bool`` indicating whether `offer` is acceptable.

        .. warning::

           The behavior of ``.__contains__`` for the ``AcceptLanguage`` classes
           is currently being maintained for backward compatibility, but it
           will change in the future to better conform to the RFC.

        :param offer: (``str``) language tag offer
        :return: (``bool``) Whether ``offer`` is acceptable according to the
                 header.

        For this class, either there is no ``Accept-Language`` header in the
        request, or the header is invalid, so any language tag is acceptable,
        and this always returns ``True``.
        """
        warnings.warn(
            'The behavior of .__contains__ for the AcceptLanguage classes is '
            'currently being maintained for backward compatibility, but it '
            'will change in the future to better conform to the RFC.',
            DeprecationWarning,
        )
        return True

    def __iter__(self):
        """
        Return all the ranges with non-0 qvalues, in order of preference.

        .. warning::

           The behavior of this method is currently maintained for backward
           compatibility, but will change in the future.

        :return: iterator of all the language ranges in the header with non-0
                 qvalues, in descending order of qvalue. If two ranges have the
                 same qvalue, they are returned in the order of their positions
                 in the header, from left to right.

        For this class, either there is no ``Accept-Language`` header in the
        request, or the header is invalid, so there are no language ranges, and
        this always returns an empty iterator.
        """
        warnings.warn(
            'The behavior of AcceptLanguageValidHeader.__iter__ is currently '
            'maintained for backward compatibility, but will change in the '
            'future.',
            DeprecationWarning,
        )
        return iter(())

    def basic_filtering(self, language_tags):
        """
        Return the tags that match the header, using Basic Filtering.

        :param language_tags: (``iterable``) language tags
        :return: A list of tuples of the form (language tag, qvalue), in
                 descending order of preference.

        When the header is invalid and when the header is not in the request,
        there are no matches, so this method always returns an empty list.
        """
        return []

    def best_match(self, offers, default_match=None):
        """
        Return the best match from the sequence of language tag `offers`.

        This is the ``.best_match()`` method for when the header is invalid or
        not found in the request, corresponding to
        :meth:`AcceptLanguageValidHeader.best_match`.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future (see the documentation for
           :meth:`AcceptLanguageValidHeader.best_match`).

        When the header is invalid, or there is no `Accept-Language` header in
        the request, any of the language tags in `offers` are considered
        acceptable, so the best match is the tag in `offers` with the highest
        server quality value (if the server quality value is not supplied, it
        is 1).

        If more than one language tags in `offers` have the same highest server
        quality value, then the one that shows up first in `offers` is the best
        match.

        :param offers: (iterable)

                       | Each item in the iterable may be a ``str`` language
                         tag, or a (language tag, server quality value)
                         ``tuple`` or ``list``. (The two may be mixed in the
                         iterable.)

        :param default_match: (optional, any type) the value to be returned if
                              `offers` is empty.

        :return: (``str``, or the type of `default_match`)

                 | The language tag that has the highest server quality value.
                   If `offers` is empty, the value of `default_match` is
                   returned.
        """
        warnings.warn(
            'The behavior of .best_match for the AcceptLanguage classes is '
            'currently being maintained for backward compatibility, but the '
            'method will be deprecated in the future, as its behavior is not '
            'specified in (and currently does not conform to) RFC 7231.',
            DeprecationWarning,
        )
        best_quality = -1
        best_offer = default_match
        for offer in offers:
            if isinstance(offer, (list, tuple)):
                offer, quality = offer
            else:
                quality = 1
            if quality > best_quality:
                best_offer = offer
                best_quality = quality
        return best_offer

    def lookup(
        self, language_tags=None, default_range=None, default_tag=None,
        default=None,
    ):
        """
        Return the language tag that best matches the header, using Lookup.

        When the header is invalid, or there is no ``Accept-Language`` header
        in the request, all language tags are considered acceptable, so it is
        as if the header is '*'. As specified for the Lookup matching scheme in
        :rfc:`RFC 4647, section 3.4 <4647#section-3.4>`, when the header is
        '*', the default value is to be computed and returned. So this method
        will ignore the `language_tags` and `default_range` arguments, and
        proceed to `default_tag`, then `default`.

        :param language_tags: (optional, any type)

                              | This argument is ignored, and is only used as a
                                placeholder so that the method signature
                                corresponds to that of
                                :meth:`AcceptLanguageValidHeader.lookup`.

        :param default_range: (optional, any type)

                              | This argument is ignored, and is only used as a
                                placeholder so that the method signature
                                corresponds to that of
                                :meth:`AcceptLanguageValidHeader.lookup`.

        :param default_tag: (optional, ``None`` or ``str``)

                            | At least one of `default_tag` or `default` must
                              be supplied as an argument to the method, to
                              define the defaulting behaviour.

                            | If this argument is not ``None``, then it is
                              returned.

                            | This parameter corresponds to "return a
                              particular language tag designated for the
                              operation", one of the examples of "defaulting
                              behavior" described in :rfc:`RFC 4647, section
                              3.4.1 <4647#section-3.4.1>`.

        :param default: (optional, ``None`` or any type, including a callable)

                        | At least one of `default_tag` or `default` must be
                          supplied as an argument to the method, to define the
                          defaulting behaviour.

                        | If `default_tag` is ``None``, then Lookup will next
                          examine the `default` argument.

                        | If `default` is a callable, it will be called, and
                          the callable's return value will be returned.

                        | If `default` is not a callable, the value itself will
                          be returned.

                        | This parameter corresponds to the "defaulting
                          behavior" described in :rfc:`RFC 4647, section 3.4.1
                          <4647#section-3.4.1>`

        :return: (``str``, or any type)

                 | the return value from `default_tag` or `default`.
        """
        if default_tag is None and default is None:
            raise TypeError(
                '`default_tag` and `default` arguments cannot both be None.'
            )

        if default_tag is not None:
            return default_tag

        try:
            return default()
        except TypeError:  # default is not a callable
            return default

    def quality(self, offer):
        """
        Return quality value of given offer, or ``None`` if there is no match.

        This is the ``.quality()`` method for when the header is invalid or not
        found in the request, corresponding to
        :meth:`AcceptLanguageValidHeader.quality`.

        .. warning::

           This is currently maintained for backward compatibility, and will be
           deprecated in the future (see the documentation for
           :meth:`AcceptLanguageValidHeader.quality`).

        :param offer: (``str``) language tag offer
        :return: (``float``) ``1.0``.

        When the ``Accept-Language`` header is invalid or not in the request,
        all offers are equally acceptable, so 1.0 is always returned.
        """
        warnings.warn(
            'The behavior of .quality for the AcceptLanguage classes is '
            'currently being maintained for backward compatibility, but the '
            'method will be deprecated in the future, as its behavior is not '
            'specified in (and currently does not conform to) RFC 7231.',
            DeprecationWarning,
        )
        return 1.0


class AcceptLanguageNoHeader(_AcceptLanguageInvalidOrNoHeader):
    """
    Represent when there is no ``Accept-Language`` header in the request.

    This object should not be modified. To add to the header, we can use the
    addition operators (``+`` and ``+=``), which return a new object (see the
    docstring for :meth:`AcceptLanguageNoHeader.__add__`).
    """

    def __init__(self):
        """
        Create an :class:`AcceptLanguageNoHeader` instance.
        """
        self._header_value = None
        self._parsed = None
        self._parsed_nonzero = None

    def copy(self):
        """
        Create a copy of the header object.

        """
        return self.__class__()

    @property
    def header_value(self):
        """
        (``str`` or ``None``) The header value.

        As there is no header in the request, this is ``None``.
        """
        return self._header_value

    @property
    def parsed(self):
        """
        (``list`` or ``None``) Parsed form of the header.

        As there is no header in the request, this is ``None``.
        """
        return self._parsed

    def __add__(self, other):
        """
        Add to header, creating a new header object.

        `other` can be:

        * ``None``
        * a ``str``
        * a ``dict``, with language ranges as keys and qvalues as values
        * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple``
          or ``list`` (language range, qvalue) pairs (``str``'s and pairs can be
          mixed within the ``tuple`` or ``list``)
        * an :class:`AcceptLanguageValidHeader`,
          :class:`AcceptLanguageNoHeader`, or
          :class:`AcceptLanguageInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``

        If `other` is a valid header value or an
        :class:`AcceptLanguageValidHeader` instance, a new
        :class:`AcceptLanguageValidHeader` instance with the valid header value
        is returned.

        If `other` is ``None``, an :class:`AcceptLanguageNoHeader` instance, an
        invalid header value, or an :class:`AcceptLanguageInvalidHeader`
        instance, a new :class:`AcceptLanguageNoHeader` instance is returned.
        """
        if isinstance(other, AcceptLanguageValidHeader):
            return AcceptLanguageValidHeader(header_value=other.header_value)

        if isinstance(
            other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader)
        ):
            return self.__class__()

        return self._add_instance_and_non_accept_language_type(
            instance=self, other=other,
        )

    def __radd__(self, other):
        """
        Add to header, creating a new header object.

        See the docstring for :meth:`AcceptLanguageNoHeader.__add__`.
        """
        return self.__add__(other=other)

    def __repr__(self):
        return '<{}>'.format(self.__class__.__name__)

    def __str__(self):
        """Return the ``str`` ``'<no header in request>'``."""
        return '<no header in request>'

    def _add_instance_and_non_accept_language_type(self, instance, other):
        if not other:
            return self.__class__()

        other_header_value = self._python_value_to_header_str(value=other)

        try:
            return AcceptLanguageValidHeader(header_value=other_header_value)
        except ValueError:  # invalid header value
            return self.__class__()


class AcceptLanguageInvalidHeader(_AcceptLanguageInvalidOrNoHeader):
    """
    Represent an invalid ``Accept-Language`` header.

    An invalid header is one that does not conform to
    :rfc:`7231#section-5.3.5`. As specified in the RFC, an empty header is an
    invalid ``Accept-Language`` header.

    :rfc:`7231` does not provide any guidance on what should happen if the
    ``Accept-Language`` header has an invalid value. This implementation
    disregards the header, and treats it as if there is no ``Accept-Language``
    header in the request.

    This object should not be modified. To add to the header, we can use the
    addition operators (``+`` and ``+=``), which return a new object (see the
    docstring for :meth:`AcceptLanguageInvalidHeader.__add__`).
    """

    def __init__(self, header_value):
        """
        Create an :class:`AcceptLanguageInvalidHeader` instance.
        """
        self._header_value = header_value
        self._parsed = None
        self._parsed_nonzero = None

    def copy(self):
        """
        Create a copy of the header object.

        """
        return self.__class__(self._header_value)

    @property
    def header_value(self):
        """(``str`` or ``None``) The header value."""
        return self._header_value

    @property
    def parsed(self):
        """
        (``list`` or ``None``) Parsed form of the header.

        As the header is invalid and cannot be parsed, this is ``None``.
        """
        return self._parsed

    def __add__(self, other):
        """
        Add to header, creating a new header object.

        `other` can be:

        * ``None``
        * a ``str``
        * a ``dict``, with language ranges as keys and qvalues as values
        * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple``
          or ``list`` (language range, qvalue) pairs (``str``'s and pairs can
          be mixed within the ``tuple`` or ``list``)
        * an :class:`AcceptLanguageValidHeader`,
          :class:`AcceptLanguageNoHeader`, or
          :class:`AcceptLanguageInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``

        If `other` is a valid header value or an
        :class:`AcceptLanguageValidHeader` instance, a new
        :class:`AcceptLanguageValidHeader` instance with the valid header value
        is returned.

        If `other` is ``None``, an :class:`AcceptLanguageNoHeader` instance, an
        invalid header value, or an :class:`AcceptLanguageInvalidHeader`
        instance, a new :class:`AcceptLanguageNoHeader` instance is returned.
        """
        if isinstance(other, AcceptLanguageValidHeader):
            return AcceptLanguageValidHeader(header_value=other.header_value)

        if isinstance(
            other, (AcceptLanguageNoHeader, AcceptLanguageInvalidHeader)
        ):
            return AcceptLanguageNoHeader()

        return self._add_instance_and_non_accept_language_type(
            instance=self, other=other,
        )

    def __radd__(self, other):
        """
        Add to header, creating a new header object.

        See the docstring for :meth:`AcceptLanguageValidHeader.__add__`.
        """
        return self._add_instance_and_non_accept_language_type(
            instance=self, other=other, instance_on_the_right=True,
        )

    def __repr__(self):
        return '<{}>'.format(self.__class__.__name__)
        # We do not display the header_value, as it is untrusted input. The
        # header_value could always be easily obtained from the .header_value
        # property.

    def __str__(self):
        """Return the ``str`` ``'<invalid header value>'``."""
        return '<invalid header value>'

    def _add_instance_and_non_accept_language_type(
        self, instance, other, instance_on_the_right=False,
    ):
        if not other:
            return AcceptLanguageNoHeader()

        other_header_value = self._python_value_to_header_str(value=other)

        try:
            return AcceptLanguageValidHeader(header_value=other_header_value)
        except ValueError:  # invalid header value
            return AcceptLanguageNoHeader()


def create_accept_language_header(header_value):
    """
    Create an object representing the ``Accept-Language`` header in a request.

    :param header_value: (``str``) header value
    :return: If `header_value` is ``None``, an :class:`AcceptLanguageNoHeader`
             instance.

             | If `header_value` is a valid ``Accept-Language`` header, an
               :class:`AcceptLanguageValidHeader` instance.

             | If `header_value` is an invalid ``Accept-Language`` header, an
               :class:`AcceptLanguageInvalidHeader` instance.
    """
    if header_value is None:
        return AcceptLanguageNoHeader()
    if isinstance(header_value, AcceptLanguage):
        return header_value.copy()
    try:
        return AcceptLanguageValidHeader(header_value=header_value)
    except ValueError:
        return AcceptLanguageInvalidHeader(header_value=header_value)


def accept_language_property():
    doc = """
        Property representing the ``Accept-Language`` header.

        (:rfc:`RFC 7231, section 5.3.5 <7231#section-5.3.5>`)

        The header value in the request environ is parsed and a new object
        representing the header is created every time we *get* the value of the
        property. (*set* and *del* change the header value in the request
        environ, and do not involve parsing.)
    """

    ENVIRON_KEY = 'HTTP_ACCEPT_LANGUAGE'

    def fget(request):
        """Get an object representing the header in the request."""
        return create_accept_language_header(
            header_value=request.environ.get(ENVIRON_KEY)
        )

    def fset(request, value):
        """
        Set the corresponding key in the request environ.

        `value` can be:

        * ``None``
        * a ``str``
        * a ``dict``, with language ranges as keys and qvalues as values
        * a ``tuple`` or ``list``, of language range ``str``'s or of ``tuple``
          or ``list`` (language range, qvalue) pairs (``str``'s and pairs can
          be mixed within the ``tuple`` or ``list``)
        * an :class:`AcceptLanguageValidHeader`,
          :class:`AcceptLanguageNoHeader`, or
          :class:`AcceptLanguageInvalidHeader` instance
        * object of any other type that returns a value for ``__str__``
        """
        if value is None or isinstance(value, AcceptLanguageNoHeader):
            fdel(request=request)
        else:
            if isinstance(
                value, (AcceptLanguageValidHeader, AcceptLanguageInvalidHeader)
            ):
                header_value = value.header_value
            else:
                header_value = AcceptLanguage._python_value_to_header_str(
                    value=value,
                )
            request.environ[ENVIRON_KEY] = header_value

    def fdel(request):
        """Delete the corresponding key from the request environ."""
        try:
            del request.environ[ENVIRON_KEY]
        except KeyError:
            pass

    return property(fget, fset, fdel, textwrap.dedent(doc))
