"""Validate properties.

See https://www.w3.org/TR/CSS21/propidx.html and various CSS3 modules.

"""

from math import inf

from tinycss2 import parse_component_value_list
from tinycss2.color3 import parse_color

from .. import computed_values
from ..properties import KNOWN_PROPERTIES, ZERO_PIXELS, Dimension

from ..utils import (  # isort:skip
    InvalidValues, Pending, check_var_function, comma_separated_list,
    get_angle, get_content_list, get_content_list_token, get_custom_ident,
    get_image, get_keyword, get_length, get_resolution, get_single_keyword,
    get_url, parse_2d_position, parse_function, parse_position,
    remove_whitespace, single_keyword, single_token)

PREFIX = '-weasy-'
PROPRIETARY = set()
UNSTABLE = set()

# 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.
# For properties that take a single value, that value is returned by itself
# instead of a list.
PROPERTIES = {}


class PendingProperty(Pending):
    """Property with validation done when defining calculated values."""
    def validate(self, tokens, wanted_key):
        return validate_non_shorthand(tokens, self.name)[0][1]


# Validators

def property(property_name=None, proprietary=False, unstable=False,
             wants_base_url=False):
    """Decorator adding a function to the ``PROPERTIES``.

    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 proprietary:
        Proprietary (vendor-specific, non-standard) are prefixed: anchors can
        for example be set using ``-weasy-anchor: attr(id)``.
        See https://www.w3.org/TR/CSS/#proprietary
    :param unstable:
        Mark properties that are defined in specifications that didn't reach
        the Candidate Recommandation stage. They can be used both
        vendor-prefixed or unprefixed.
        See https://www.w3.org/TR/CSS/#unstable-syntax
    :param wants_base_url:
        The function takes the stylesheet’s base URL as an additional
        parameter.

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

        function.wants_base_url = wants_base_url
        PROPERTIES[name] = function
        if proprietary:
            PROPRIETARY.add(name)
        if unstable:
            UNSTABLE.add(name)
        return function
    return decorator


def validate_non_shorthand(tokens, name, base_url=None, required=False):
    """Validator for non-shorthand properties."""
    if name.startswith('--'):
        # TODO: validate content
        return ((name, tokens),)

    if not required and name not in KNOWN_PROPERTIES:
        raise InvalidValues('unknown property')

    if not required and name not in PROPERTIES:
        raise InvalidValues('property not supported yet')

    function = PROPERTIES[name]
    for token in tokens:
        if check_var_function(token):
            # Found CSS variable, return pending-substitution values.
            return ((name, PendingProperty(tokens, name)),)

    keyword = get_single_keyword(tokens)
    if keyword in ('initial', 'inherit'):
        value = keyword
    else:
        if function.wants_base_url:
            value = function(tokens, base_url)
        else:
            value = function(tokens)
        if value is None:
            raise InvalidValues
    return ((name, value),)


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


@property('background-color')
@property('border-top-color')
@property('border-right-color')
@property('border-bottom-color')
@property('border-left-color')
@property('column-rule-color', unstable=True)
@property('text-decoration-color')
@single_token
def other_colors(token):
    return parse_color(token)


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


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


@property()
@single_keyword
def empty_cells(keyword):
    """``empty-cells`` property validation."""
    return keyword in ('show', 'hide')


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


@property('background-image', wants_base_url=True)
@comma_separated_list
@single_token
def background_image(token, base_url):
    if get_keyword(token) == 'none':
        return 'none', None
    return get_image(token, base_url)


@property('list-style-image', wants_base_url=True)
@single_token
def list_style_image(token, base_url):
    """``list-style-image`` property validation."""
    if get_keyword(token) == 'none':
        return 'none', None
    parsed_url = get_url(token, base_url)
    if parsed_url:
        if parsed_url[0] == 'url' and parsed_url[1][0] == 'external':
            return 'url', parsed_url[1][1]


@property()
def transform_origin(tokens):
    """``transform-origin`` property validation."""
    if len(tokens) == 3:
        # Ignore third parameter as 3D transforms are ignored.
        tokens = tokens[:2]
    return parse_2d_position(tokens)


@property()
@comma_separated_list
def background_position(tokens):
    """``background-position`` property validation."""
    return parse_position(tokens)


@property()
@comma_separated_list
def object_position(tokens):
    """``object-position`` property validation."""
    return parse_position(tokens)


@property()
@comma_separated_list
def background_repeat(tokens):
    """``background-repeat`` property validation."""
    keywords = tuple(map(get_keyword, tokens))
    if keywords == ('repeat-x',):
        return ('repeat', 'no-repeat')
    if keywords == ('repeat-y',):
        return ('no-repeat', 'repeat')
    if keywords in (('no-repeat',), ('repeat',), ('space',), ('round',)):
        return keywords * 2
    if len(keywords) == 2 and all(
            k in ('no-repeat', 'repeat', 'space', 'round')
            for k in keywords):
        return keywords


@property()
@comma_separated_list
def background_size(tokens):
    """Validation for ``background-size``."""
    if len(tokens) == 1:
        token = tokens[0]
        keyword = get_keyword(token)
        if keyword in ('contain', 'cover'):
            return keyword
        if keyword == 'auto':
            return ('auto', 'auto')
        length = get_length(token, negative=False, percentage=True)
        if length:
            return (length, 'auto')
    elif len(tokens) == 2:
        values = []
        for token in tokens:
            length = get_length(token, negative=False, percentage=True)
            if length:
                values.append(length)
            elif get_keyword(token) == 'auto':
                values.append('auto')
        if len(values) == 2:
            return tuple(values)


@property('background-clip')
@property('background-origin')
@comma_separated_list
@single_keyword
def box(keyword):
    """Validation for the ``<box>`` type used in ``background-clip``
    and ``background-origin``."""
    return keyword in ('border-box', 'padding-box', 'content-box')


@property()
def border_spacing(tokens):
    """Validator for the `border-spacing` property."""
    lengths = [get_length(token, negative=False) for token in tokens]
    if all(lengths):
        if len(lengths) == 1:
            return (lengths[0], lengths[0])
        elif len(lengths) == 2:
            return tuple(lengths)


@property('border-top-right-radius')
@property('border-bottom-right-radius')
@property('border-bottom-left-radius')
@property('border-top-left-radius')
def border_corner_radius(tokens):
    """Validator for the `border-*-radius` properties."""
    lengths = [
        get_length(token, negative=False, percentage=True) for token in tokens]
    if all(lengths):
        if len(lengths) == 1:
            return (lengths[0], lengths[0])
        elif len(lengths) == 2:
            return tuple(lengths)


@property('border-top-style')
@property('border-right-style')
@property('border-left-style')
@property('border-bottom-style')
@property('column-rule-style', unstable=True)
@single_keyword
def border_style(keyword):
    """``border-*-style`` properties validation."""
    return keyword in ('none', 'hidden', 'dotted', 'dashed', 'double',
                       'inset', 'outset', 'groove', 'ridge', 'solid')


@property('break-before')
@property('break-after')
@single_keyword
def break_before_after(keyword):
    """``break-before`` and ``break-after`` properties validation."""
    return keyword in ('auto', 'avoid', 'avoid-page', 'page', 'left', 'right',
                       'recto', 'verso', 'avoid-column', 'column', 'always')


@property()
@single_keyword
def break_inside(keyword):
    """``break-inside`` property validation."""
    return keyword in ('auto', 'avoid', 'avoid-page', 'avoid-column')


@property()
@single_keyword
def box_decoration_break(keyword):
    """``box-decoration-break`` property validation."""
    return keyword in ('slice', 'clone')


@property()
@single_token
def block_ellipsis(token):
    """``box-ellipsis`` property validation."""
    if token.type == 'string':
        return ('string', token.value)
    else:
        keyword = get_keyword(token)
        if keyword in ('none', 'auto'):
            return keyword


@property('continue', unstable=True)
@single_keyword
def continue_(keyword):
    """``continue`` property validation."""
    return keyword in ('auto', 'discard')


@property(unstable=True)
@single_token
def max_lines(token):
    if token.type == 'number' and token.int_value is not None:
        if token.int_value >= 1:
            return token.int_value
    keyword = get_keyword(token)
    if keyword == 'none':
        return keyword


@property(unstable=True)
@single_keyword
def margin_break(keyword):
    """``margin-break`` property validation."""
    return keyword in ('auto', 'keep', 'discard')


@property(unstable=True)
@single_token
def page(token):
    """``page`` property validation."""
    if token.type == 'ident':
        return 'auto' if token.lower_value == 'auto' else token.value


@property('bleed-left', unstable=True)
@property('bleed-right', unstable=True)
@property('bleed-top', unstable=True)
@property('bleed-bottom', unstable=True)
@single_token
def bleed(token):
    """``bleed`` property validation."""
    keyword = get_keyword(token)
    if keyword == 'auto':
        return 'auto'
    else:
        return get_length(token)


@property(unstable=True)
def marks(tokens):
    """``marks`` property validation."""
    if len(tokens) == 2:
        keywords = tuple(get_keyword(token) for token in tokens)
        if 'crop' in keywords and 'cross' in keywords:
            return keywords
    elif len(tokens) == 1:
        keyword = get_keyword(tokens[0])
        if keyword in ('crop', 'cross'):
            return (keyword,)
        elif keyword == 'none':
            return ()


@property('outline-style')
@single_keyword
def outline_style(keyword):
    """``outline-style`` properties validation."""
    return keyword in ('none', 'dotted', 'dashed', 'double', 'inset',
                       'outset', 'groove', 'ridge', 'solid')


@property('border-top-width')
@property('border-right-width')
@property('border-left-width')
@property('border-bottom-width')
@property('column-rule-width', unstable=True)
@property('outline-width')
@single_token
def border_width(token):
    """Border, column rule and outline widths properties validation."""
    length = get_length(token, negative=False)
    if length:
        return length
    keyword = get_keyword(token)
    if keyword in ('thin', 'medium', 'thick'):
        return keyword


@property(wants_base_url=True)
@single_token
def border_image_source(token, base_url):
    if get_keyword(token) == 'none':
        return 'none', None
    return get_image(token, base_url)


@property()
def border_image_slice(tokens):
    values = []
    fill = False
    for i, token in enumerate(tokens):
        # Don't use get_length() because a dimension with a unit is disallowed.
        if token.type == 'percentage' and token.value >= 0:
            values.append(Dimension(token.value, '%'))
        elif token.type == 'number' and token.value >= 0:
            values.append(Dimension(token.value, None))
        elif get_keyword(token) == 'fill' and not fill and i in (0, len(tokens) - 1):
            fill = True
            values.append('fill')
        else:
            return

    if 1 <= len(values) - int(fill) <= 4:
        return tuple(values)


@property()
def border_image_width(tokens):
    values = []
    for token in tokens:
        if get_keyword(token) == 'auto':
            values.append('auto')
        elif token.type == 'number' and token.value >= 0:
            values.append(Dimension(token.value, None))
        else:
            if length := get_length(token, negative=False, percentage=True):
                values.append(length)
            else:
                return

    if 1 <= len(values) <= 4:
        return tuple(values)


@property()
def border_image_outset(tokens):
    values = []
    for token in tokens:
        if token.type == 'number' and token.value >= 0:
            values.append(Dimension(token.value, None))
        else:
            if length := get_length(token, negative=False):
                values.append(length)
            else:
                return

    if 1 <= len(values) <= 4:
        return tuple(values)


@property()
def border_image_repeat(tokens):
    if 1 <= len(tokens) <= 2:
        keywords = tuple(get_keyword(token) for token in tokens)
        if set(keywords) <= {'stretch', 'repeat', 'round', 'space'}:
            return keywords


@property(unstable=True)
@single_token
def column_width(token):
    """``column-width`` property validation."""
    length = get_length(token, negative=False)
    if length:
        return length
    keyword = get_keyword(token)
    if keyword == 'auto':
        return keyword


@property(unstable=True)
@single_keyword
def column_span(keyword):
    """``column-span`` property validation."""
    return keyword in ('all', 'none')


@property()
@single_keyword
def box_sizing(keyword):
    """Validation for the ``box-sizing`` property from css3-ui"""
    return keyword in ('padding-box', 'border-box', 'content-box')


@property()
@single_keyword
def caption_side(keyword):
    """``caption-side`` properties validation."""
    return keyword in ('top', 'bottom')


@property()
@single_keyword
def clear(keyword):
    """``clear`` property validation."""
    return keyword in ('left', 'right', 'both', 'none')


@property()
@single_token
def clip(token):
    """Validation for the ``clip`` property."""
    function = parse_function(token)
    if function:
        name, args = function
        if name == 'rect' and len(args) == 4:
            values = []
            for arg in args:
                if get_keyword(arg) == 'auto':
                    values.append('auto')
                else:
                    length = get_length(arg)
                    if length:
                        values.append(length)
            if len(values) == 4:
                return tuple(values)
    if get_keyword(token) == 'auto':
        return ()


@property(wants_base_url=True)
def content(tokens, base_url):
    """``content`` property validation."""
    # See https://www.w3.org/TR/css-content-3/#content-property
    tokens = list(tokens)
    parsed_tokens = []
    while tokens:
        if len(tokens) >= 2 and (
                tokens[1].type == 'literal' and tokens[1].value == ','):
            token, tokens = tokens[0], tokens[2:]
            parsed_token = (
                get_image(token, base_url) or get_url(token, base_url))
            if parsed_token:
                parsed_tokens.append(parsed_token)
            else:
                return
        else:
            break
    if len(tokens) == 0:
        return
    if len(tokens) >= 3 and tokens[-1].type == 'string' and (
            tokens[-2].type == 'literal' and tokens[-2].value == '/'):
        # Ignore text for speech
        tokens = tokens[:-2]
    keyword = get_single_keyword(tokens)
    if keyword in ('normal', 'none'):
        return (keyword,)
    return get_content_list(tokens, base_url)


@property()
def counter_increment(tokens):
    """``counter-increment`` property validation."""
    return counter(tokens, default_integer=1)


@property()
def counter_reset(tokens):
    """``counter-reset`` property validation."""
    return counter(tokens, default_integer=0)


@property()
def counter_set(tokens):
    """``counter-set`` property validation."""
    return counter(tokens, default_integer=0)


def counter(tokens, default_integer):
    """``counter-increment`` and ``counter-reset`` properties validation."""
    if get_single_keyword(tokens) == 'none':
        return ()
    tokens = iter(tokens)
    token = next(tokens, None)
    assert token, 'got an empty token list'
    results = []
    while token is not None:
        if token.type != 'ident':
            return  # expected a keyword here
        counter_name = token.value
        if counter_name in ('none', 'initial', 'inherit'):
            raise InvalidValues(f'Invalid counter name: {counter_name}')
        token = next(tokens, None)
        if token is not None and (
                token.type == 'number' and token.int_value is not None):
            # Found an integer. Use it and get the next token
            integer = token.int_value
            token = next(tokens, None)
        else:
            # Not an integer. Might be the next counter name.
            # Keep `token` for the next loop iteration.
            integer = default_integer
        results.append((counter_name, integer))
    return tuple(results)


@property('top')
@property('right')
@property('left')
@property('bottom')
@property('margin-top')
@property('margin-right')
@property('margin-bottom')
@property('margin-left')
@single_token
def lenght_precentage_or_auto(token):
    """``margin-*`` properties validation."""
    length = get_length(token, percentage=True)
    if length:
        return length
    if get_keyword(token) == 'auto':
        return 'auto'


@property('height')
@property('width')
@single_token
def width_height(token):
    """Validation for the ``width`` and ``height`` properties."""
    length = get_length(token, negative=False, percentage=True)
    if length:
        return length
    if get_keyword(token) == 'auto':
        return 'auto'


@property('column-gap', unstable=True)
@property('row-gap', unstable=True)
@single_token
def gap(token):
    """Validation for the ``column-gap`` and ``row-gap`` properties."""
    length = get_length(token, percentage=True, negative=False)
    if length:
        return length
    keyword = get_keyword(token)
    if keyword == 'normal':
        return keyword


@property(unstable=True)
@single_keyword
def column_fill(keyword):
    """``column-fill`` property validation."""
    return keyword in ('auto', 'balance')


@property()
@single_keyword
def direction(keyword):
    """``direction`` property validation."""
    return keyword in ('ltr', 'rtl')


@property()
def display(tokens):
    """``display`` property validation."""
    for token in tokens:
        if token.type != 'ident':
            return

    if len(tokens) == 1:
        value = tokens[0].value
        if value in (
                'none', 'table-caption', 'table-row-group', 'table-cell',
                'table-header-group', 'table-footer-group', 'table-row',
                'table-column-group', 'table-column'):
            return (value,)
        elif value in ('inline-table', 'inline-flex', 'inline-grid'):
            return tuple(value.split('-'))
        elif value == 'inline-block':
            return ('inline', 'flow-root')

    outside = inside = list_item = None
    for token in tokens:
        value = token.value
        if value in ('block', 'inline'):
            if outside:
                return
            outside = value
        elif value in ('flow', 'flow-root', 'table', 'flex', 'grid'):
            if inside:
                return
            inside = value
        elif value == 'list-item':
            if list_item:
                return
            list_item = value
        else:
            return

    outside = outside or 'block'
    inside = inside or 'flow'
    if list_item:
        if inside in ('flow', 'flow-root'):
            return (outside, inside, list_item)
    else:
        return (outside, inside)


@property('float')
@single_keyword
def float_(keyword):  # XXX do not hide the "float" builtin
    """``float`` property validation."""
    return keyword in ('left', 'right', 'footnote', 'none')


@property()
@comma_separated_list
def font_family(tokens):
    """``font-family`` property validation."""
    if len(tokens) == 1 and tokens[0].type == 'string':
        return tokens[0].value
    elif tokens and all(token.type == 'ident' for token in tokens):
        return ' '.join(token.value for token in tokens)


@property()
@single_keyword
def font_kerning(keyword):
    return keyword in ('auto', 'normal', 'none')


@property()
@single_token
def font_language_override(token):
    keyword = get_keyword(token)
    if keyword == 'normal':
        return keyword
    elif token.type == 'string':
        return token.value


@property()
def font_variant_ligatures(tokens):
    if len(tokens) == 1:
        keyword = get_keyword(tokens[0])
        if keyword in ('normal', 'none'):
            return keyword
    values = []
    couples = (
        ('common-ligatures', 'no-common-ligatures'),
        ('historical-ligatures', 'no-historical-ligatures'),
        ('discretionary-ligatures', 'no-discretionary-ligatures'),
        ('contextual', 'no-contextual'))
    all_values = []
    for couple in couples:
        all_values.extend(couple)
    for token in tokens:
        if token.type != 'ident':
            return None
        if token.value in all_values:
            concurrent_values = next(
                couple for couple in couples if token.value in couple)
            if any(value in values for value in concurrent_values):
                return None
            else:
                values.append(token.value)
        else:
            return None
    if values:
        return tuple(values)


@property()
@single_keyword
def font_variant_position(keyword):
    return keyword in ('normal', 'sub', 'super')


@property()
@single_keyword
def font_variant_caps(keyword):
    return keyword in (
        'normal', 'small-caps', 'all-small-caps', 'petite-caps',
        'all-petite-caps', 'unicase', 'titling-caps')


@property()
def font_variant_numeric(tokens):
    if len(tokens) == 1:
        keyword = get_keyword(tokens[0])
        if keyword == 'normal':
            return keyword
    values = []
    couples = (
        ('lining-nums', 'oldstyle-nums'),
        ('proportional-nums', 'tabular-nums'),
        ('diagonal-fractions', 'stacked-fractions'),
        ('ordinal',), ('slashed-zero',))
    all_values = []
    for couple in couples:
        all_values.extend(couple)
    for token in tokens:
        if token.type != 'ident':
            return None
        if token.value in all_values:
            concurrent_values = next(
                couple for couple in couples if token.value in couple)
            if any(value in values for value in concurrent_values):
                return None
            else:
                values.append(token.value)
        else:
            return None
    if values:
        return tuple(values)


@property()
def font_feature_settings(tokens):
    """``font-feature-settings`` property validation."""
    if len(tokens) == 1 and get_keyword(tokens[0]) == 'normal':
        return 'normal'

    @comma_separated_list
    def font_feature_settings_list(tokens):
        feature, value = None, None

        if len(tokens) == 2:
            tokens, token = tokens[:-1], tokens[-1]
            if token.type == 'ident':
                value = {'on': 1, 'off': 0}.get(token.value)
            elif (token.type == 'number' and
                    token.int_value is not None and token.int_value >= 0):
                value = token.int_value
        elif len(tokens) == 1:
            value = 1

        if len(tokens) == 1:
            token, = tokens
            if token.type == 'string' and len(token.value) == 4:
                if all(0x20 <= ord(letter) <= 0x7f for letter in token.value):
                    feature = token.value

        if feature is not None and value is not None:
            return feature, value

    return font_feature_settings_list(tokens)


@property()
@single_keyword
def font_variant_alternates(keyword):
    # TODO: support other values
    # See https://www.w3.org/TR/css-fonts-3/#font-variant-caps-prop
    return keyword in ('normal', 'historical-forms')


@property()
def font_variant_east_asian(tokens):
    if len(tokens) == 1:
        keyword = get_keyword(tokens[0])
        if keyword == 'normal':
            return keyword
    values = []
    couples = (
        ('jis78', 'jis83', 'jis90', 'jis04', 'simplified', 'traditional'),
        ('full-width', 'proportional-width'),
        ('ruby',))
    all_values = []
    for couple in couples:
        all_values.extend(couple)
    for token in tokens:
        if token.type != 'ident':
            return None
        if token.value in all_values:
            concurrent_values = next(
                couple for couple in couples if token.value in couple)
            if any(value in values for value in concurrent_values):
                return None
            else:
                values.append(token.value)
        else:
            return None
    if values:
        return tuple(values)


@property()
def font_variation_settings(tokens):
    """``font-variation-settings`` property validation."""
    if len(tokens) == 1 and get_keyword(tokens[0]) == 'normal':
        return 'normal'

    @comma_separated_list
    def font_variation_settings_list(tokens):
        if len(tokens) == 2:
            key, value = tokens
            if key.type == 'string' and value.type == 'number':
                return key.value, value.value

    return font_variation_settings_list(tokens)


@property()
@single_token
def font_size(token):
    """``font-size`` property validation."""
    length = get_length(token, negative=False, percentage=True)
    if length:
        return length
    font_size_keyword = get_keyword(token)
    if font_size_keyword in ('smaller', 'larger'):
        return font_size_keyword
    if font_size_keyword in computed_values.FONT_SIZE_KEYWORDS:
        return font_size_keyword


@property()
@single_keyword
def font_style(keyword):
    """``font-style`` property validation."""
    return keyword in ('normal', 'italic', 'oblique')


@property()
@single_keyword
def font_stretch(keyword):
    """Validation for the ``font-stretch`` property."""
    return keyword in (
        'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed',
        'normal',
        'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded')


@property()
@single_token
def font_weight(token):
    """``font-weight`` property validation."""
    keyword = get_keyword(token)
    if keyword in ('normal', 'bold', 'bolder', 'lighter'):
        return keyword
    if token.type == 'number' and token.int_value is not None:
        if token.int_value in (100, 200, 300, 400, 500, 600, 700, 800, 900):
            return token.int_value


@property()
@single_keyword
def object_fit(keyword):
    # TODO: Figure out what the spec means by "'scale-down' flag".
    #   As of this writing, neither Firefox nor chrome support
    #   anything other than a single keyword as is done here.
    return keyword in ('fill', 'contain', 'cover', 'none', 'scale-down')


@property(unstable=True)
@single_token
def image_resolution(token):
    # TODO: support 'snap' and 'from-image'
    return get_resolution(token)


@property('letter-spacing')
@property('word-spacing')
@single_token
def spacing(token):
    """Validation for ``letter-spacing`` and ``word-spacing``."""
    if get_keyword(token) == 'normal':
        return 'normal'
    length = get_length(token)
    if length:
        return length


@property()
@single_token
def line_height(token):
    """``line-height`` property validation."""
    if get_keyword(token) == 'normal':
        return 'normal'
    if token.type == 'number' and token.value >= 0:
        return Dimension(token.value, None)
    if token.type == 'percentage' and token.value >= 0:
        return Dimension(token.value, '%')
    elif token.type == 'dimension' and token.value >= 0:
        return get_length(token)


@property()
@single_keyword
def list_style_position(keyword):
    """``list-style-position`` property validation."""
    return keyword in ('inside', 'outside')


@property()
@single_token
def list_style_type(token):
    """``list-style-type`` property validation."""
    if token.type == 'ident':
        return token.value
    elif token.type == 'string':
        return ('string', token.value)
    elif token.type == 'function' and token.name == 'symbols':
        allowed_types = (
            'cyclic', 'numeric', 'alphabetic', 'symbolic', 'fixed')
        function_arguments = remove_whitespace(token.arguments)
        if len(function_arguments) >= 1:
            arguments = []
            if function_arguments[0].type == 'ident':
                if function_arguments[0].value in allowed_types:
                    index = 1
                    arguments.append(function_arguments[0].value)
                else:
                    return
            else:
                arguments.append('symbolic')
                index = 0
            if len(function_arguments) < index + 1:
                return
            for i in range(index, len(function_arguments)):
                if function_arguments[i].type != 'string':
                    return
                arguments.append(function_arguments[i].value)
            if arguments[0] in ('alphabetic', 'numeric'):
                if len(arguments) < 3:
                    return
            return ('symbols()', tuple(arguments))


@property('min-width')
@property('min-height')
@single_token
def min_width_height(token):
    """``min-width`` and ``min-height`` properties validation."""
    # See https://www.w3.org/TR/css-flexbox-1/#min-size-auto
    keyword = get_keyword(token)
    if keyword == 'auto':
        return keyword
    else:
        return length_or_precentage([token])


@property('padding-top')
@property('padding-right')
@property('padding-bottom')
@property('padding-left')
@single_token
def length_or_precentage(token):
    """``padding-*`` properties validation."""
    length = get_length(token, negative=False, percentage=True)
    if length:
        return length


@property('max-width')
@property('max-height')
@single_token
def max_width_height(token):
    """Validation for max-width and max-height"""
    length = get_length(token, negative=False, percentage=True)
    if length:
        return length
    if get_keyword(token) == 'none':
        return Dimension(inf, 'px')


@property()
@single_token
def opacity(token):
    """Validation for the ``opacity`` property."""
    if token.type == 'number':
        return min(1, max(0, token.value))
    if token.type == 'percentage':
        return min(1, max(0, token.value / 100))


@property()
@single_token
def z_index(token):
    """Validation for the ``z-index`` property."""
    if get_keyword(token) == 'auto':
        return 'auto'
    if token.type == 'number' and token.int_value is not None:
        return token.int_value


@property('orphans')
@property('widows')
@single_token
def orphans_widows(token):
    """Validation for the ``orphans`` and ``widows`` properties."""
    if token.type == 'number' and token.int_value is not None:
        value = token.int_value
        if value >= 1:
            return value


@property(unstable=True)
@single_token
def column_count(token):
    """Validation for the ``column-count`` property."""
    if token.type == 'number' and token.int_value is not None:
        value = token.int_value
        if value >= 1:
            return value
    if get_keyword(token) == 'auto':
        return 'auto'


@property()
@single_keyword
def overflow(keyword):
    """Validation for the ``overflow`` property."""
    return keyword in ('auto', 'visible', 'hidden', 'scroll')


@property()
@single_keyword
def text_overflow(keyword):
    """Validation for the ``text-overflow`` property."""
    return keyword in ('clip', 'ellipsis')


@property()
@single_token
def position(token):
    """``position`` property validation."""
    if token.type == 'function' and token.name == 'running':
        if len(token.arguments) == 1 and token.arguments[0].type == 'ident':
            return ('running()', token.arguments[0].value)
    keyword = get_single_keyword([token])
    if keyword in ('static', 'relative', 'absolute', 'fixed'):
        return keyword


@property()
def quotes(tokens):
    """``quotes`` property validation."""
    if len(tokens) == 1:
        if (keyword := get_keyword(tokens[0])) in ('auto', 'none'):
            return keyword
    if (tokens and len(tokens) % 2 == 0 and
            all(token.type == 'string' for token in tokens)):
        strings = tuple(token.value for token in tokens)
        # Separate open and close quotes.
        # eg.  ('«', '»', '“', '”')  -> (('«', '“'), ('»', '”'))
        return strings[::2], strings[1::2]


@property()
@single_keyword
def table_layout(keyword):
    """Validation for the ``table-layout`` property"""
    if keyword in ('fixed', 'auto'):
        return keyword


@property()
@single_keyword
def text_align_all(keyword):
    """``text-align-all`` property validation."""
    return keyword in ('left', 'right', 'center', 'justify', 'start', 'end')


@property()
@single_keyword
def text_align_last(keyword):
    """``text-align-last`` property validation."""
    return keyword in (
        'auto', 'left', 'right', 'center', 'justify', 'start', 'end')


@property()
def text_decoration_line(tokens):
    """``text-decoration-line`` property validation."""
    keywords = {get_keyword(token) for token in tokens}
    if keywords == {'none'}:
        return 'none'
    allowed_values = {'underline', 'overline', 'line-through', 'blink'}
    if len(tokens) == len(keywords) and keywords.issubset(allowed_values):
        return keywords


@property()
@single_keyword
def text_decoration_style(keyword):
    """``text-decoration-style`` property validation."""
    if keyword in ('solid', 'double', 'dotted', 'dashed', 'wavy'):
        return keyword


@property()
@single_token
def text_indent(token):
    """``text-indent`` property validation."""
    length = get_length(token, percentage=True)
    if length:
        return length


@property()
@single_keyword
def text_transform(keyword):
    """``text-align`` property validation."""
    return keyword in (
        'none', 'uppercase', 'lowercase', 'capitalize', 'full-width')


@property()
@single_token
def vertical_align(token):
    """Validation for the ``vertical-align`` property"""
    length = get_length(token, percentage=True)
    if length:
        return length
    keyword = get_keyword(token)
    if keyword in ('baseline', 'middle', 'sub', 'super',
                   'text-top', 'text-bottom', 'top', 'bottom'):
        return keyword


@property()
@single_keyword
def visibility(keyword):
    """``white-space`` property validation."""
    return keyword in ('visible', 'hidden', 'collapse')


@property()
@single_keyword
def white_space(keyword):
    """``white-space`` property validation."""
    return keyword in ('normal', 'pre', 'nowrap', 'pre-wrap', 'pre-line')


@property()
@single_keyword
def overflow_wrap(keyword):
    """``overflow-wrap`` property validation."""
    return keyword in ('anywhere', 'normal', 'break-word')


@property()
@single_keyword
def word_break(keyword):
    """``word-break`` property validation."""
    return keyword in ('normal', 'break-all')


@property()
@single_token
def flex_basis(token):
    """``flex-basis`` property validation."""
    basis = width_height([token])
    if basis is not None:
        return basis
    if get_keyword(token) == 'content':
        return 'content'


@property()
@single_keyword
def flex_direction(keyword):
    """``flex-direction`` property validation."""
    return keyword in ('row', 'row-reverse', 'column', 'column-reverse')


@property('flex-grow')
@property('flex-shrink')
@single_token
def flex_grow_shrink(token):
    if token.type == 'number':
        return token.value


def _inflexible_breadth(token):
    """Parse ``inflexible-breadth``."""
    keyword = get_keyword(token)
    if keyword in ('auto', 'min-content', 'max-content'):
        return keyword
    elif keyword:
        return
    length = get_length(token, negative=False, percentage=True)
    if length:
        return length


def _track_breadth(token):
    """Parse ``track-breadth``."""
    if token.type == 'dimension' and token.value >= 0 and token.unit == 'fr':
        return Dimension(token.value, token.unit)
    return _inflexible_breadth(token)


def _track_size(token):
    """Parse ``track-size``."""
    track_breadth = _track_breadth(token)
    if track_breadth:
        return track_breadth
    function = parse_function(token)
    if function:
        name, args = function
        if name == 'minmax':
            if len(args) == 2:
                inflexible_breadth = _inflexible_breadth(args[0])
                track_breadth = _track_breadth(args[1])
                if inflexible_breadth and track_breadth:
                    return ('minmax()', inflexible_breadth, track_breadth)
        elif name == 'fit-content':
            if len(args) == 1:
                length = get_length(args[0], negative=False, percentage=True)
                if length:
                    return ('fit-content()', length)


def _fixed_size(token):
    """Parse ``fixed-size``."""
    length = get_length(token, negative=False, percentage=True)
    if length:
        return length
    function = parse_function(token)
    if function:
        name, args = function
        if name == 'minmax' and len(args) == 2:
            length = get_length(args[0], negative=False, percentage=True)
            if length:
                track_breadth = _track_breadth(args[1])
                if track_breadth:
                    return ('minmax()', length, track_breadth)
            keyword = get_keyword(args[0])
            if keyword in ('min-content', 'max-content', 'auto') or length:
                fixed_breadth = get_length(
                    args[1], negative=False, percentage=True)
                if fixed_breadth:
                    return ('minmax()', length or keyword, fixed_breadth)


def _line_names(arg):
    """Parse ``line-names``."""
    return_line_names = []
    if arg.type == '[] block':
        for token in arg.content:
            if token.type == 'ident':
                return_line_names.append(token.value)
            elif token.type != 'whitespace':
                return
        return tuple(return_line_names)


@property('grid-auto-columns')
@property('grid-auto-rows')
def grid_auto(tokens):
    """``grid-auto-columns`` and ``grid-auto-rows`` properties validation."""
    return_tokens = []
    for token in tokens:
        track_size = _track_size(token)
        if track_size:
            return_tokens.append(track_size)
            continue
        return
    return tuple(return_tokens)


@property()
def grid_auto_flow(tokens):
    """``grid-auto-flow`` property validation."""
    if len(tokens) == 1:
        keyword = get_keyword(tokens[0])
        if keyword in ('row', 'column'):
            return (keyword,)
        elif keyword == 'dense':
            return (keyword, 'row')
    elif len(tokens) == 2:
        keywords = [get_keyword(token) for token in tokens]
        if 'dense' in keywords and ('row' in keywords or 'column' in keywords):
            return tuple(keywords)


@property('grid-template-columns')
@property('grid-template-rows')
def grid_template(tokens):
    """``grid-template-columns`` and ``grid-template-rows`` validation."""
    return_tokens = []
    if len(tokens) == 1 and get_keyword(tokens[0]) == 'none':
        return 'none'
    if get_keyword(tokens[0]) == 'subgrid':
        return_tokens.append('subgrid')
        subgrid_tokens = []
        for token in tokens[1:]:
            line_names = _line_names(token)
            if line_names is not None:
                subgrid_tokens.append(line_names)
                continue
            function = parse_function(token)
            if function:
                name, args = function
                if name == 'repeat' and len(args) >= 2:
                    if (args[0].type == 'number' and
                            args[0].is_integer and args[0].value >= 1):
                        number = args[0].int_value
                    elif get_keyword(args[0]) == 'auto-fill':
                        number = 'auto-fill'
                    else:
                        return
                    line_names_list = []
                    for arg in args[1:]:
                        line_names = _line_names(arg)
                        if line_names is not None:
                            line_names_list.append(line_names)
                    subgrid_tokens.append(
                        ('repeat()', number, tuple(line_names_list)))
                    continue
            return
        return_tokens.append(tuple(subgrid_tokens))
    else:
        includes_auto_repeat = False
        includes_track = False
        last_is_line_name = False
        for token in tokens:
            line_names = _line_names(token)
            if line_names is not None:
                if last_is_line_name:
                    return
                last_is_line_name = True
                return_tokens.append(line_names)
                continue
            fixed_size = _fixed_size(token)
            if fixed_size:
                if not last_is_line_name:
                    return_tokens.append(())
                last_is_line_name = False
                return_tokens.append(fixed_size)
                continue
            track_size = _track_size(token)
            if track_size:
                if not last_is_line_name:
                    return_tokens.append(())
                last_is_line_name = False
                return_tokens.append(track_size)
                includes_track = True
                continue
            function = parse_function(token)
            if function:
                name, args = function
                if name == 'repeat' and len(args) >= 2:
                    if (args[0].type == 'number' and
                            args[0].is_integer and args[0].value >= 1):
                        number = args[0].int_value
                    elif get_keyword(args[0]) in ('auto-fill', 'auto-fit'):
                        # auto-repeat
                        if includes_auto_repeat:
                            return
                        number = args[0].value
                        includes_auto_repeat = True
                    else:
                        return
                    names_and_sizes = []
                    repeat_last_is_line_name = False
                    for arg in args[1:]:
                        line_names = _line_names(arg)
                        if line_names is not None:
                            if repeat_last_is_line_name:
                                return
                            names_and_sizes.append(line_names)
                            repeat_last_is_line_name = True
                            continue
                        # fixed-repead
                        fixed_size = _fixed_size(arg)
                        if fixed_size:
                            if not repeat_last_is_line_name:
                                names_and_sizes.append(())
                            repeat_last_is_line_name = False
                            names_and_sizes.append(fixed_size)
                            continue
                        # track-repeat
                        track_size = _track_size(arg)
                        if track_size:
                            includes_track = True
                            if not repeat_last_is_line_name:
                                names_and_sizes.append(())
                            repeat_last_is_line_name = False
                            names_and_sizes.append(track_size)
                            continue
                        return
                    if not last_is_line_name:
                        return_tokens.append(())
                    last_is_line_name = False
                    if not repeat_last_is_line_name:
                        names_and_sizes.append(())
                    return_tokens.append(
                        ('repeat()', number, tuple(names_and_sizes)))
                    continue
            return
        if includes_auto_repeat and includes_track:
            return
        if not last_is_line_name:
            return_tokens.append(())
    return tuple(return_tokens)


@property()
def grid_template_areas(tokens):
    """``grid-template-areas`` property validation."""
    if len(tokens) == 1 and get_keyword(tokens[0]) == 'none':
        return 'none'
    grid_areas = []
    for token in tokens:
        if token.type != 'string':
            return
        component_values = parse_component_value_list(token.value)
        row = []
        last_is_dot = False
        for value in component_values:
            if value.type == 'ident':
                row.append(value.value)
                last_is_dot = False
            elif value.type == 'literal' and value.value == '.':
                if last_is_dot:
                    continue
                row.append(None)
                last_is_dot = True
            elif value.type == 'whitespace':
                last_is_dot = False
            else:
                return
        if not row:
            return
        grid_areas.append(tuple(row))
    # check row / column have the same sizes
    if len(set(len(row) for row in grid_areas)) == 1:
        # check areas are continuous rectangles
        coordinates = set()
        areas = set()
        for y, row in enumerate(grid_areas):
            for x, area in enumerate(row):
                if (x, y) in coordinates or area is None:
                    continue
                if area in areas:
                    return
                areas.add(area)
                coordinates.add((x, y))
                nx = x + 1
                for nx, narea in enumerate(row[x+1:], start=x+1):
                    if narea != area:
                        break
                    coordinates.add((nx, y))
                for ny, nrow in enumerate(grid_areas[y+1:], start=y+1):
                    if set(nrow[x:nx]) == {area}:
                        for nnx in range(x, nx):
                            coordinates.add((nnx, ny))
                    else:
                        break
        return tuple(grid_areas)


@property('grid-row-start')
@property('grid-row-end')
@property('grid-column-start')
@property('grid-column-end')
def grid_line(tokens):
    """``grid-[row|column]-[start—end]`` properties validation."""
    if len(tokens) == 1:
        token = tokens[0]
        if keyword := get_keyword(token):
            if keyword == 'auto':
                return keyword
            elif keyword != 'span':
                return (None, None, keyword)
        elif token.type == 'number' and token.is_integer and token.value:
            return (None, token.int_value, None)
        return
    number = ident = span = None
    for token in tokens:
        if keyword := get_keyword(token):
            if keyword == 'auto':
                return
            if keyword == 'span':
                if span is None:
                    span = 'span'
                    continue
            elif keyword and ident is None:
                ident = keyword
                continue
        elif token.type == 'number' and token.is_integer and token.value:
            if number is None:
                number = token.int_value
                continue
        return
    if span:
        if number and number < 0:
            return
        elif ident or number:
            return (span, number, ident)
    elif number:
        return (span, number, ident)


@property()
@single_keyword
def flex_wrap(keyword):
    """``flex-wrap`` property validation."""
    return keyword in ('nowrap', 'wrap', 'wrap-reverse')


@property()
def justify_content(tokens):
    """``justify-content`` property validation."""
    if len(tokens) == 1:
        keyword = get_keyword(tokens[0])
        if keyword in (
                'center', 'space-between', 'space-around', 'space-evenly',
                'stretch', 'normal', 'flex-start', 'flex-end',
                'start', 'end', 'left', 'right'):
            return (keyword,)
    elif len(tokens) == 2:
        keywords = tuple(get_keyword(token) for token in tokens)
        if keywords[0] in ('safe', 'unsafe'):
            if keywords[1] in (
                    'center', 'start', 'end', 'flex-start', 'flex-end', 'left',
                    'right'):
                return keywords


@property()
def justify_items(tokens):
    """``justify-items`` property validation."""
    if len(tokens) == 1:
        keyword = get_keyword(tokens[0])
        if keyword in (
                'normal', 'stretch', 'center', 'start', 'end', 'self-start',
                'self-end', 'flex-start', 'flex-end', 'left', 'right',
                'legacy'):
            return (keyword,)
        elif keyword == 'baseline':
            return ('first', keyword)
    elif len(tokens) == 2:
        keywords = tuple(get_keyword(token) for token in tokens)
        if keywords[0] in ('safe', 'unsafe'):
            if keywords[1] in (
                    'center', 'start', 'end', 'self-start', 'self-end',
                    'flex-start', 'flex-end', 'left', 'right'):
                return keywords
        elif 'baseline' in keywords:
            if 'first' in keywords or 'last' in keywords:
                return keywords
        elif 'legacy' in keywords:
            if set(keywords) & {'left', 'right', 'center'}:
                return keywords


@property()
def justify_self(tokens):
    """``justify-self`` property validation."""
    if len(tokens) == 1:
        keyword = get_keyword(tokens[0])
        if keyword in (
                'auto', 'normal', 'stretch', 'center', 'start', 'end',
                'self-start', 'self-end', 'flex-start', 'flex-end', 'left',
                'right'):
            return (keyword,)
        elif keyword == 'baseline':
            return ('first', keyword)
    elif len(tokens) == 2:
        keywords = tuple(get_keyword(token) for token in tokens)
        if keywords[0] in ('safe', 'unsafe'):
            if keywords[1] in (
                    'center', 'start', 'end', 'self-start', 'self-end',
                    'flex-start', 'flex-end', 'left', 'right'):
                return keywords
        elif 'baseline' in keywords:
            if 'first' in keywords or 'last' in keywords:
                return keywords


@property()
def align_items(tokens):
    """``align-items`` property validation."""
    if len(tokens) == 1:
        keyword = get_keyword(tokens[0])
        if keyword in (
                'normal', 'stretch', 'center', 'start', 'end', 'self-start',
                'self-end', 'flex-start', 'flex-end'):
            return (keyword,)
        elif keyword == 'baseline':
            return ('first', keyword)
    elif len(tokens) == 2:
        keywords = tuple(get_keyword(token) for token in tokens)
        if keywords[0] in ('safe', 'unsafe'):
            if keywords[1] in (
                    'center', 'start', 'end', 'self-start', 'self-end',
                    'flex-start', 'flex-end'):
                return keywords
        elif 'baseline' in keywords:
            if 'first' in keywords or 'last' in keywords:
                return keywords


@property()
def align_self(tokens):
    """``align-self`` property validation."""
    if len(tokens) == 1:
        keyword = get_keyword(tokens[0])
        if keyword in (
                'auto', 'normal', 'stretch', 'center', 'start', 'end',
                'self-start', 'self-end', 'flex-start', 'flex-end'):
            return (keyword,)
        elif keyword == 'baseline':
            return ('first', keyword)
    elif len(tokens) == 2:
        keywords = tuple(get_keyword(token) for token in tokens)
        if keywords[0] in ('safe', 'unsafe'):
            if keywords[1] in (
                    'center', 'start', 'end', 'self-start', 'self-end',
                    'flex-start', 'flex-end'):
                return keywords
        elif 'baseline' in keywords:
            if 'first' in keywords or 'last' in keywords:
                return keywords


@property()
def align_content(tokens):
    """``align-content`` property validation."""
    if len(tokens) == 1:
        keyword = get_keyword(tokens[0])
        if keyword in (
                'center', 'space-between', 'space-around', 'space-evenly',
                'stretch', 'normal', 'flex-start', 'flex-end',
                'start', 'end'):
            return (keyword,)
        elif keyword == 'baseline':
            return ('first', keyword)
    elif len(tokens) == 2:
        keywords = tuple(get_keyword(token) for token in tokens)
        if keywords[0] in ('safe', 'unsafe'):
            if keywords[1] in (
                    'center', 'start', 'end', 'flex-start', 'flex-end'):
                return keywords
        elif 'baseline' in keywords:
            if 'first' in keywords or 'last' in keywords:
                return keywords


@property()
@single_token
def order(token):
    if token.type == 'number' and token.int_value is not None:
        return token.int_value


@property(unstable=True)
@single_keyword
def image_rendering(keyword):
    """Validation for ``image-rendering``."""
    return keyword in ('auto', 'crisp-edges', 'pixelated')


@property(unstable=True)
def image_orientation(tokens):
    """Validation for ``image-orientation``."""
    keyword = get_single_keyword(tokens)
    if keyword in ('none', 'from-image'):
        return keyword
    angle, flip = None, None
    for token in tokens:
        keyword = get_keyword(token)
        if keyword == 'flip':
            if flip is not None:
                return
            flip = True
            continue
        if angle is None:
            angle = get_angle(token)
            if angle is not None:
                continue
        return
    angle = 0 if angle is None else angle
    flip = False if flip is None else flip
    return (angle, flip)


@property(unstable=True)
def size(tokens):
    """``size`` property validation.

    See https://www.w3.org/TR/css-page-3/#page-size-prop

    """
    lengths = [get_length(token, negative=False) for token in tokens]
    if all(lengths):
        if len(lengths) == 1:
            return (lengths[0], lengths[0])
        elif len(lengths) == 2:
            return tuple(lengths)

    keywords = [get_keyword(token) for token in tokens]
    if len(keywords) == 1:
        keyword = keywords[0]
        if keyword in computed_values.PAGE_SIZES:
            return computed_values.PAGE_SIZES[keyword]
        elif keyword in ('auto', 'portrait'):
            return computed_values.INITIAL_PAGE_SIZE
        elif keyword == 'landscape':
            return computed_values.INITIAL_PAGE_SIZE[::-1]

    if len(keywords) == 2:
        if keywords[0] in ('portrait', 'landscape'):
            orientation, page_size = keywords
        elif keywords[1] in ('portrait', 'landscape'):
            page_size, orientation = keywords
        else:
            page_size = None
        if page_size in computed_values.PAGE_SIZES:
            width_height = computed_values.PAGE_SIZES[page_size]
            if orientation == 'portrait':
                return width_height
            else:
                height, width = width_height
                return width, height


@property(proprietary=True)
@single_token
def anchor(token):
    """Validation for ``anchor``."""
    if get_keyword(token) == 'none':
        return 'none'
    function = parse_function(token)
    if function:
        name, args = function
        prototype = (name, [arg.type for arg in args])
        if prototype == ('attr', ['ident']):
            return ('attr()', args[0].value)


@property(proprietary=True, wants_base_url=True)
@single_token
def link(token, base_url):
    """Validation for ``link``."""
    if get_keyword(token) == 'none':
        return 'none'
    parsed_url = get_url(token, base_url)
    if parsed_url:
        return parsed_url
    function = parse_function(token)
    if function:
        name, args = function
        prototype = (name, [arg.type for arg in args])
        if prototype == ('attr', ['ident']):
            return ('attr()', args[0].value)


@property()
@single_token
def tab_size(token):
    """Validation for ``tab-size``.

    See https://www.w3.org/TR/css-text-3/#tab-size

    """
    if token.type == 'number' and token.int_value is not None:
        value = token.int_value
        if value >= 0:
            return value
    return get_length(token, negative=False)


@property(unstable=True)
@single_token
def hyphens(token):
    """Validation for ``hyphens``."""
    keyword = get_keyword(token)
    if keyword in ('none', 'manual', 'auto'):
        return keyword


@property(unstable=True)
@single_token
def hyphenate_character(token):
    """Validation for ``hyphenate-character``."""
    keyword = get_keyword(token)
    if keyword == 'auto':
        return '‐'
    elif token.type == 'string':
        return token.value


@property(unstable=True)
@single_token
def hyphenate_limit_zone(token):
    """Validation for ``hyphenate-limit-zone``."""
    return get_length(token, negative=False, percentage=True)


@property(unstable=True)
def hyphenate_limit_chars(tokens):
    """Validation for ``hyphenate-limit-chars``."""
    if len(tokens) == 1:
        token, = tokens
        keyword = get_keyword(token)
        if keyword == 'auto':
            return (5, 2, 2)
        elif token.type == 'number' and token.int_value is not None:
            return (token.int_value, 2, 2)
    elif len(tokens) == 2:
        total, left = tokens
        total_keyword = get_keyword(total)
        left_keyword = get_keyword(left)
        if total.type == 'number' and total.int_value is not None:
            if left.type == 'number' and left.int_value is not None:
                return (total.int_value, left.int_value, left.int_value)
            elif left_keyword == 'auto':
                return (total.value, 2, 2)
        elif total_keyword == 'auto':
            if left.type == 'number' and left.int_value is not None:
                return (5, left.int_value, left.int_value)
            elif left_keyword == 'auto':
                return (5, 2, 2)
    elif len(tokens) == 3:
        total, left, right = tokens
        if (
            (get_keyword(total) == 'auto' or
                (total.type == 'number' and total.int_value is not None)) and
            (get_keyword(left) == 'auto' or
                (left.type == 'number' and left.int_value is not None)) and
            (get_keyword(right) == 'auto' or
                (right.type == 'number' and right.int_value is not None))
        ):
            total = total.int_value if total.type == 'number' else 5
            left = left.int_value if left.type == 'number' else 2
            right = right.int_value if right.type == 'number' else 2
            return (total, left, right)


@property(proprietary=True)
@single_token
def lang(token):
    """Validation for ``lang``."""
    if get_keyword(token) == 'none':
        return 'none'
    function = parse_function(token)
    if function:
        name, args = function
        prototype = (name, [arg.type for arg in args])
        if prototype == ('attr', ['ident']):
            return ('attr()', args[0].value)
    elif token.type == 'string':
        return ('string', token.value)


@property(unstable=True, wants_base_url=True)
def bookmark_label(tokens, base_url):
    """Validation for ``bookmark-label``."""
    parsed_tokens = tuple(
        get_content_list_token(token, base_url) for token in tokens)
    if None not in parsed_tokens:
        return parsed_tokens


@property(unstable=True)
@single_token
def bookmark_level(token):
    """Validation for ``bookmark-level``."""
    if token.type == 'number' and token.int_value is not None:
        value = token.int_value
        if value >= 1:
            return value
    elif get_keyword(token) == 'none':
        return 'none'


@property(unstable=True)
@single_keyword
def bookmark_state(keyword):
    """Validation for ``bookmark-state``."""
    return keyword in ('open', 'closed')


@property(unstable=True)
@single_keyword
def footnote_display(keyword):
    """Validation for ``footnote-display``."""
    return keyword in ('block', 'inline', 'compact')


@property(unstable=True)
@single_keyword
def footnote_policy(keyword):
    """Validation for ``footnote-policy``."""
    return keyword in ('auto', 'line', 'block')


@property(unstable=True, wants_base_url=True)
@comma_separated_list
def string_set(tokens, base_url):
    """Validation for ``string-set``."""
    # Spec asks for strings after custom keywords, but we allow content-lists
    if len(tokens) >= 2:
        var_name = get_custom_ident(tokens[0])
        if var_name is None:
            return
        parsed_tokens = tuple(
            get_content_list_token(token, base_url) for token in tokens[1:])
        if None not in parsed_tokens:
            return (var_name, parsed_tokens)
    elif tokens and get_keyword(tokens[0]) == 'none':
        return 'none', ()


@property()
def transform(tokens):
    """Validation for ``transform``."""
    if get_single_keyword(tokens) == 'none':
        return ()
    else:
        transforms = []
        for token in tokens:
            function = parse_function(token)
            if not function:
                return
            name, args = function

            if len(args) == 1:
                angle = get_angle(args[0])
                length = get_length(args[0], percentage=True)
                if name == 'rotate' and angle is not None:
                    transforms.append((name, angle))
                elif name in ('skewx', 'skew') and angle is not None:
                    transforms.append(('skew', (angle, 0)))
                elif name == 'skewy' and angle is not None:
                    transforms.append(('skew', (0, angle)))
                elif name in ('translatex', 'translate') and length:
                    transforms.append(('translate', (length, ZERO_PIXELS)))
                elif name == 'translatey' and length:
                    transforms.append(('translate', (ZERO_PIXELS, length)))
                elif name == 'scalex' and args[0].type == 'number':
                    transforms.append(('scale', (args[0].value, 1)))
                elif name == 'scaley' and args[0].type == 'number':
                    transforms.append(('scale', (1, args[0].value)))
                elif name == 'scale' and args[0].type == 'number':
                    transforms.append(('scale', (args[0].value,) * 2))
                else:
                    return
            elif len(args) == 2:
                if name == 'scale' and all(a.type == 'number' for a in args):
                    transforms.append((name, tuple(arg.value for arg in args)))
                elif name == 'translate':
                    lengths = tuple(
                        get_length(token, percentage=True) for token in args)
                    if all(lengths):
                        transforms.append((name, lengths))
                    else:
                        return
                elif name == 'skew':
                    angles = tuple(get_angle(token) for token in args)
                    if all(angle is not None for angle in angles):
                        transforms.append((name, angles))
                    else:
                        return
                else:
                    return
            elif len(args) == 6 and name == 'matrix' and all(
                    a.type == 'number' for a in args):
                transforms.append((name, tuple(arg.value for arg in args)))
            else:
                return
        return tuple(transforms)


@property()
@single_token
def appearance(token):
    """``appearance`` property validation."""
    keyword = get_keyword(token)
    if keyword in ('none', 'auto'):
        return keyword
