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

import copy
import logging
import re

import webob.dec
import webob.exc


LOG = logging.getLogger('aversion')

SLASH_RE = re.compile('/+')


def quoted_split(string, sep, quotes='"'):
    """
    Split a string on the given separation character, but respecting
    double-quoted sections of the string.  Returns an iterator.

    :param string: The string to split.
    :param sep: The character separating sections of the string.
    :param quotes: A string specifying all legal quote characters.

    :returns: An iterator which will iterate over each element of the
              string separated by the designated separator.
    """

    # Initialize the algorithm
    start = None
    escape = False
    quote = False

    # Walk through the string
    for i, c in enumerate(string):
        # Save the start index
        if start is None:
            start = i

        # Handle escape sequences
        if escape:
            escape = False

        # Handle quoted strings
        elif quote:
            if c == '\\':
                escape = True
            elif c == quote:
                quote = False

        # Handle the separator
        elif c == sep:
            yield string[start:i]
            start = None

        # Handle quotes
        elif c in quotes:
            quote = c

    # Yield the last part
    if start is not None:
        yield string[start:]


def unquote(quoted):
    """
    Unquotes a value, as drawn from a header.

    Note: This does not use the real unquoting algorithm, but what
    browsers are actually using for quoting.  Internet Explorer (and
    probably some other browsers) fails to apply the proper quoting
    algorithm.  Thus, the algorithm used is simply to remove the
    quotes.

    :param quoted: The quoted string.

    :returns: The string with the quoting removed.
    """

    if quoted[:1] == '"' and quoted[-1:] == '"':
        return quoted[1:-1]
    return quoted


def parse_ctype(ctype):
    """
    Parse a content type.

    :param ctype: The content type, with corresponding parameters.

    :returns: A tuple of the content type and a dictionary containing
              the content type parameters.  The content type will
              additionally be available in the dictionary as the '_'
              key.
    """

    result_ctype = None
    result = {}
    for part in quoted_split(ctype, ';'):
        # Extract the content type first
        if result_ctype is None:
            result_ctype = part
            result['_'] = part
            continue

        # OK, we have a 'key' or 'key=value' to handle; figure it
        # out...
        equal = part.find('=')
        if equal > 0 and part.find('"', 0, equal) < 0:
            result[part[:equal]] = unquote(part[equal + 1:])
        else:
            # If equal > 0 but it's preceded by a ", it's seriously
            # messed up, but go ahead and be liberal...
            result[part] = True

    # If we failed to parse a content type out, return an empty
    # content type
    if result_ctype is None:
        result_ctype = ''

    return result_ctype, result


def _match_mask(mask, ctype):
    """
    Determine if a content type mask matches a given content type.

    :param mask: The content type mask, taken from the Accept
                 header.
    :param ctype: The content type to match to the mask.
    """

    # Handle the simple cases first
    if '*' not in mask:
        return ctype == mask
    elif mask == '*/*':
        return True
    elif not mask.endswith('/*'):
        return False

    mask_major = mask[:-2]
    ctype_major = ctype.split('/', 1)[0]
    return ctype_major == mask_major


def best_match(requested, allowed):
    """
    Determine the best content type to use for the request.

    :param ctypes: A list of the available content types.

    :returns: A tuple of the best match content type and the
              parameters for that content type.
    """

    requested = [parse_ctype(ctype) for ctype in quoted_split(requested, ',')]

    best_q = -1
    best_ctype = ''
    best_params = {}
    best_match = '*/*'

    # Walk the list of content types
    for ctype in allowed:
        # Compare to the accept list
        for ctype_mask, params in requested:
            try:
                q = float(params.get('q', 1.0))
            except ValueError:
                # Bad quality value
                continue

            if q < best_q:
                # Not any better
                continue
            elif best_q == q:
                # Base on the best match
                if best_match.count('*') <= ctype_mask.count('*'):
                    continue

            # OK, see if we have a match
            if _match_mask(ctype_mask, ctype):
                best_q = q