# coding: utf8
"""
    weasyprint.css.validation
    -------------------------

    Expand shorthands and validate property values.
    See http://www.w3.org/TR/CSS21/propidx.html and various CSS3 modules.

    :copyright: Copyright 2011-2014 Simon Sapin and contributors, see AUTHORS.
    :license: BSD, see LICENSE for details.

"""

from __future__ import division, unicode_literals

import functools
import math

from tinycss.color3 import parse_color
from tinycss.parsing import split_on_comma, remove_whitespace

from ..logger import LOGGER
from ..formatting_structure import counters
from ..compat import urljoin, unquote
from ..urls import url_is_absolute, iri_to_uri
from ..images import LinearGradient, RadialGradient
from .properties import (INITIAL_VALUES, KNOWN_PROPERTIES, NOT_PRINT_MEDIA,
                         Dimension)
from . import computed_values

# TODO: unit-test these validators


# get the sets of keys
LENGTH_UNITS = set(computed_values.LENGTHS_TO_PIXELS) | set(['ex', 'em', 'ch'])


# keyword -> (open, insert)
CONTENT_QUOTE_KEYWORDS = {
    'open-quote': (True, True),
    'close-quote': (False, True),
    'no-open-quote': (True, False),
    'no-close-quote': (False, False),
}

ZERO_PERCENT = Dimension(0, '%')
FIFTY_PERCENT = Dimension(50, '%')
HUNDRED_PERCENT = Dimension(100, '%')
BACKGROUND_POSITION_PERCENTAGES = {
    'top': ZERO_PERCENT,
    'left': ZERO_PERCENT,
    'center': FIFTY_PERCENT,
    'bottom': HUNDRED_PERCENT,
    'right': HUNDRED_PERCENT,
}


# yes/no validators for non-shorthand properties
# Maps property names to functions taking a property name and a value list,
# returning a value or None for invalid.
# Also transform values: keyword and URLs are returned as strings.
# For properties that take a single value, that va
lue is returned by itself
# instead of a list.
VALIDATORS = {}

EXPANDERS = {}

PREFIXED = set()
UNPREFIXED = set()
PREFIX = '-weasy-'


class InvalidValues(ValueError):
    """Invalid or unsupported values for a known CSS property
."""


# Validators

def validator(property_name=None, prefixed=False, unprefixed=False,
              wants_base_url=False):
    """Decorator adding a function to the ``VALIDATORS``.

    The name of the property covered by the decorated function is set to
    ``property_name`` if given, or is inferred from the function name
    (replacing underscores by hyphens).

    :param prefixed:
        Vendor-specific (non-standard) and experimental properties are
        prefixed: stylesheets need to use eg. ``-weasy-bookmark-level: 2``
        instead of ``bookmark-level: 2``.
        See http://wiki.csswg.org/spec/vendor-prefixes
    :param unprefixed:
        Mark properties that used to be prefixed. When used with the prefix,
        they will be ignored be give a specific warning.
    :param wants_base_url:
        The function takes the stylesheet’s base URL as an additional
        parameter.

    """
    assert not (prefixed and unprefixed)

    def decorator(function):
        """Add ``function`` to the ``VALIDATORS``."""
        if property_name is None:
            name = function.__name__.replace('_', '-')
        else:
            name = property_name
        assert name in KNOWN_PROPERTIES, name
        assert name not in VALIDATORS, name

        function.wants_base_url = wants_base_url
        VALIDATORS[name] = function
        if prefixed:
            PREFIXED.add(name)
        if unprefixed:
            UNPREFIXED.add(name)
        return function
    return decorator


def get_keyword(token):
    """If ``value`` is a keyword, return its name.

    Otherwise return ``None``.

    """
    if token.type == 'IDENT':
        return token.value.lower()


def get_single_keyword(tokens):
    """If ``values`` is a 1-element list of keywords, return its name.

    Otherwise return ``None``.

    """
    if len(tokens) == 1:
        token = tokens[0]
        if token.type == 'IDENT':
            return token.value.lower()


def single_keyword(function):
    """Decorator for validators that only accept a single keyword."""
    @functools.wraps(function)
    def keyword_validator(tokens):
        """Wrap a validator to call get_single_keyword on tokens."""
        keyword = get_single_keyword(tokens)
        if function(keyword):
            return keyword
    return keyword_validator


def single_token(function):
    """Decorator for validators that only accept a single token."""
    @functools.wraps(function)
    def single_token_validator(tokens, *args):
        """Validate a property whose token is single."""
        if len(tokens) == 1:
            return function(tokens[0], *args)
    single_token_validator.__func__ = function
    return single_token_validator


def comma_separated_list(function):
    """Decorator for validators that accept a comma separated list."""
    @functools.wraps(function)
    def wrapper(tokens, *args):
        results = []
        for part in split_on_comma(tokens):
            result = function(remove_whitespace(part), *args)
            if result is None:
                return None
            results.append(result)
        return results
    wrapper.single_value = function
    return wrapper


def get_length(token, negative=True, percentage=False):
    if (token.unit in LENGTH_UNITS or (percentage and token.unit == '%')
            or (token.type in ('INTEGER', 'NUMBER')
                and token.value == 0)) and (negative or token.value >= 0):
        return Dimension(token.value, token.unit)


# http://dev.w3.org/csswg/css3-values/#angles
# 1<unit> is this many radians.
ANGLE_TO_RADIANS = {
    'rad': 1,
    'turn': 2 * math.pi,
    'deg': math.pi / 180,
    'grad': math.pi / 200,
}


def get_angle(token):
    """Return the value in radians of an <angle> token, or None."""
    factor = ANGLE_TO_RADIANS.get(token.unit)
    if factor is not None:
        return token.value * factor


# http://dev.w3.org/csswg/css-values/#resolution
RESOLUTION_TO_DPPX = {
    'dppx': 1,
    'dpi': 1 / computed_values.LENGTHS_TO_PIXELS['in'],
    'dpcm': 1 / computed_values.LENGTHS_TO_PIXELS['cm'],
}


def get_resolution(token):
    """Return the value in dppx of a <resolution> token, or None."""
    factor = RESOLUTION_TO_DPPX.get(token.unit)
    if factor is not None:
        return token.value * factor


def safe_urljoin(base_url, url):
    if url_is_absolute(url):
        return iri_to_uri(url)
    elif base_url:
        return iri_to_uri(urljoin(base_url, url))
    else:
        raise InvalidValues(
            'Relative URI reference without a base URI: %r' % url)


@validator()
@comma_separated_list
@single_keyword
def background_attachment(keyword):
    """``background-attachment`` property validation."""
    return keyword in ('scroll', 'fixed', 'local')


@validator('background-color')
@validator('border-top-color')
@validator('border-right-color')
@validator('border-bottom-color')
@validator('border-left-color')
@single_token
def other_colors(token):
    return parse_color(token)


@validator()
@single_token
def outline_color(token):
    if get_keyword(token) == 'invert':
        return 'currentColor'
    else:
        return parse_color(token)


@validator()
@single_keyword
def border_collapse(keyword):
    return keyword in ('separate', 'collapse')


@validator('color')
@single_token
def color(token):
    """``*-color`` and ``color`` properties validation."""
    result = parse_color(token)
    if result == 'currentColor':
        return 'inherit'
    else:
        return result


@validator('background-image', wants_base_url=True)
@comma_separated_list
@single_token
def background_image(token, base_url):
    if token.type != 'FUNCTION':
        return image_url([token], base_url)
    arguments = split_on_comma(t for t in token.content if t.type != 'S')
    name = token.function_name.lower()
    if name in ('linear-gradient', 'repeating-linear-gradient'):
        direction, color_stops = parse_linear_gradient_parameters(arguments)
        if color_stops:
            return 'linear-gradient', LinearGradient(
                [parse_color_stop(stop) for stop in color_stops],
                direction, 'repeating' in name)
  