"""
    weasyprint.css
    --------------

    This module takes care of steps 3 and 4 of “CSS 2.1 processing model”:
    Retrieve stylesheets associated with a document and annotate every element
    with a value for every CSS property.

    http://www.w3.org/TR/CSS21/intro.html#processing-model

    This module does this in more than two steps. The
    :func:`get_all_computed_styles` function does everything, but it is itsef
    based on other functions in this module.

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

"""

from collections import namedtuple
from logging import DEBUG, WARNING

import cssselect2
import tinycss2
import tinycss2.nth

from .. import CSS
from ..logger import LOGGER, PROGRESS_LOGGER
from ..urls import URLFetchingError, get_url_attribute, url_join
from . import computed_values, media_queries
from .properties import INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES
from .utils import remove_whitespace
from .validation import preprocess_declarations
from .validation.descriptors import preprocess_descriptors

# Reject anything not in here:
PSEUDO_ELEMENTS = (
    None, 'before', 'after', 'marker', 'first-line', 'first-letter')


PageType = namedtuple('PageType', ['side', 'blank', 'first', 'index', 'name'])


class StyleFor:
    """Convenience function to get the computed styles for an element."""
    def __init__(self, html, sheets, presentational_hints, target_collector):
        # keys: (element, pseudo_element_type)
        #    element: an ElementTree Element or the '@page' string
        #    pseudo_element_type: a string such as 'first' (for @page) or
        #        'after', or None for normal elements
        # values: dicts of
        #     keys: property name as a string
        #     values: (values, weight)
        #         values: a PropertyValue-like object
        #         weight: values with a greater weight take precedence, see
        #             http://www.w3.org/TR/CSS21/cascade.html#cascading-order
        self._cascaded_styles = cascaded_styles = {}

        # keys: (element, pseudo_element_type), like cascaded_styles
        # values: style dict objects:
        #     keys: property name as a string
        #     values: a PropertyValue-like object
        self._computed_styles = {}

        self._sheets = sheets

        PROGRESS_LOGGER.info('Step 3 - Applying CSS')
        for specificity, attributes in find_style_attributes(
                html.etree_element, presentational_hints, html.base_url):
            element, declarations, base_url = attributes
            style = cascaded_styles.setdefault((element, None), {})
            for name, values, importance in preprocess_declarations(
                    base_url, declarations):
                precedence = declaration_precedence('author', importance)
                weight = (precedence, specificity)
                old_weight = style.get(name, (None, None))[1]
                if old_weight is None or old_weight <= weight:
                    style[name] = values, weight

        # First, add declarations and set computed styles for "real" elements
        # *in tree order*. Tree order is important so that parents have
        # computed styles before their children, for inheritance.

        # Iterate on all elements, even if there is no cascaded style for them.
        for element in html.wrapper_element.iter_subtree():
            for sheet, origin, sheet_specificity in sheets:
                # Add declarations for matched elements
                for selector in sheet.matcher.match(element):
                    specificity, order, pseudo_type, declarations = selector
                    specificity = sheet_specificity or specificity
                    style = cascaded_styles.setdefault(
                        (element.etree_element, pseudo_type), {})
                    for name, values, importance in declarations:
                        precedence = declaration_precedence(origin, importance)
                        weight = (precedence, specificity)
                        old_weight = style.get(name, (None, None))[1]
                        if old_weight is None or old_weight <= weight:
                            style[name] = values, weight
            parent = element.parent.etree_element if element.parent else None
            self.set_computed_styles(
                element.etree_element, root=html.etree_element, parent=parent,
                base_url=html.base_url, target_collector=target_collector)

        # Then computed styles for pseudo elements, in any order.
        # Pseudo-elements inherit from their associated element so they come
        # last. Do them in a second pass as there is no easy way to iterate
        # on the pseudo-elements for a given element with the current structure
        # of cascaded_styles. (Keys are (element, pseudo_type) tuples.)

        # Only iterate on pseudo-elements that have cascaded styles. (Others
        # might as well not exist.)
        for element, pseudo_type in cascaded_styles:
            if pseudo_type and not isinstance(element, PageType):
                self.set_computed_styles(
                    element, pseudo_type=pseudo_type,
                    # The pseudo-element inherits from the element.
                    root=html.etree_element, parent=element,
                    base_url=html.base_url, target_collector=target_collector)

        # Clear the cascaded styles, we don't need them anymore. Keep the
        # dictionary, it is used later for page margins.
        self._cascaded_styles.clear()

    def __call__(self, element, pseudo_type=None):
        style = self._computed_styles.get((element, pseudo_type))

        if style:
            if 'table' in style['display']:
                if (style['display'] in ('table', 'inline-table') and
                        style['border_collapse'] == 'collapse'):
                    # Padding do not apply
                    for side in ['top', 'bottom', 'left', 'right']:
                        style['padding_' + side] = computed_values.ZERO_PIXELS
                if (style['display'].startswith('table-') and
                        style['display'] != 'table-caption'):
                    # Margins do not apply
                    for side in ['top', 'bottom', 'left', 'right']:
                        style['margin_' + side] = computed_values.ZERO_PIXELS

        return style

    def set_computed_styles(self, element, parent, root=None, pseudo_type=None,
                            base_url=None, target_collector=None):
        """Set the computed values of styles to ``element``.

        Take the properties left by ``apply_style_rule`` on an element or
        pseudo-element and assign computed values with respect to the cascade,
        declaration priority (ie. ``!important``) and selector specificity.

        """
        cascaded_styles = self.get_cascaded_styles()
        computed_styles = self.get_computed_styles()
        if element == root and pseudo_type is None:
            assert parent is None
            parent_style = None
            root_style = {
                # When specified on the font-size property of the root element,
                # the rem units refer to the property’s initial value.
                'font_size': INITIAL_VALUES['font_size'],
            }
        else:
            assert parent is not None
            parent_style = computed_styles[parent, None]
            root_style = computed_styles[root, None]

        cascaded = cascaded_styles.get((element, pseudo_type), {})
        computed_styles[element, pseudo_type] = computed_from_cascaded(
            element, cascaded, parent_style, pseudo_type, root_style, base_url,
            target_collector)

        # The style of marker is deleted when display is different from
        # list-item.
        if pseudo_type is None:
            for pseudo in (None, 'before', 'after'):
                pseudo_style = cascaded_styles.get((element, pseudo), {})
                if 'display' in pseudo_style:
                    if pseudo_style['display'][0] == 'list-item':
                        break
            else:
                if (element, 'marker') in cascaded_styles:
                    del cascaded_styles[element, 'marker']

    def add_page_declarations(self, page_type):
        for sheet, origin, sheet_specificity in self._sheets:
            for _rule, selector_list, declarations in sheet.page_rules:
                for selector in selector_list:
                    specificity, pseudo_type, selector_page_type = selector
                    if self._page_type_match(selector_page_type, page_type):
                        specificity = sheet_specificity or specificity
                        style = self._cascaded_styles.setdefault(
                            (page_type, pseudo_type), {})
                        for name, values, importance in declarations:
                            precedence = declaration_precedence(
                                origin, importance)
                            weight = (precedence, specificity)
                            old_weight = style.get(name, (None, None))[1]
                            if old_weight is None or old_weight <= weight:
                                style[name] = values, weight

    def get_cascaded_styles(self):
        return self._cascaded_styles

    def get_computed_styles(self):
        return self._computed_styles

    @staticmethod
    def _page_type_match(selector_page_type, page_type):
        if selector_page_type.side not in (None, page_type.side):
            return False
        if selector_page_type.blank not in (None, page_type.blank):
            return False
        if selector_page_type.first not in (None, page_type.first):
            return False
        if selector_page_type.name not in (None, page_type.name):
            return False
        if selector_page_type.index is not None:
            a, b, group = selector_page_type.index
            # TODO: handle group
            if a:
                if (page_type.index + 1 - b) % a:
                    return False
            else:
                if page_type.index + 1 != b:
                    return False
        return True


def get_child_text(element):
    """Return the text directly in the element, not descendants."""
    content = [element.text] if element.text else []
    for child in element:
        if child.tail:
            content.append(child.tail)
    return ''.join(content)


def find_stylesheets(wrapper_element, device_media_type, url_fetcher, base_url,
                     font_config, page_rules):
    """Yield the stylesheets in ``element_tree``.

    The output order is the same as the source order.

    """
    from ..html import element_has_link_type  # Work around circular imports.

    for wrapper in wrapper_element.query_all('style', 'link'):
        element = wrapper.etree_element
        mime_type = element.get('type', 'text/css').split(';', 1)[0].strip()
        # Only keep 'type/subtype' from 'type/subtype ; param1; param2'.
        if mime_type != 'text/css':
            continue
        media_attr = element.get('media', '').strip() or 'all'
        media = [media_type.strip() for media_type in media_attr.split(',')]
        if not media_queries.evaluate_media_query(media, device_media_type):
            continue
        if element.tag == 'style':
            # Content is text that is directly in the <style> element, not its
            # descendants
            content = get_child_text(element)
            # ElementTree should give us either unicode or ASCII-only
            # bytestrings, so we don't need `encoding` here.
            css = CSS(
                string=content, base_url=base_url,
                url_fetcher=url_fetcher, media_type=device_media_type,
                font_config=font_config, page_rules=page_rules)
            yield css
        elif element.tag == 'link' and element.get('href'):
            if not element_has_link_type(element, 'stylesheet') or \
                    element_has_link_type(element, 'alternate'):
                continue
            href = get_url_attribute(element, 'href', base_url)
            if href is not None:
                try:
                    yield CSS(
                        url=href, url_fetcher=url_fetcher,
                        _check_mime_type=True, media_type=device_media_type,
                        font_config=font_config, page_rules=page_rules)
                except URLFetchingError as exc:
                    LOGGER.error(
                        'Failed to load stylesheet at %s : %s', href, exc)


def find_style_attributes(tree, presentational_hints=False, base_url=None):
    """Yield ``specificity, (element, declaration, base_url)`` rules.

    Rules from "style" attribute are returned with specificity
    ``(1, 0, 0)``.

    If ``presentational_hints`` is ``True``, rules from presentational hints
    are returned with specificity ``(0, 0, 0)``.

    """
    def check_style_attribute(element, style_attribute):
        declarations = tinycss2.parse_declaration_list(style_attribute)
        return element, declarations, base_url

    for element in tree.iter():
        specificity = (1, 0, 0)
        style_attribute = element.get('style')
        if style_attribute:
            yield specificity, check_style_attribute(element, style_attribute)
        if not presentational_hints:
            continue
        specificity = (0, 0, 0)
        if element.tag == 'body':
            # TODO: we should check the container frame element
            for part, position in (
                    ('height', 'top'), ('height', 'bottom'),
                    ('width', 'left'), ('width', 'right')):
                style_attribute = None
                for prop in ('margin%s' % part, '%smargin' % position):
                    if element.get(prop):
                        style_attribute = 'margin-%s:%spx' % (
                            position, element.get(prop))
                        break
                if style_attribute:
                    yield specificity, check_style_attribute(
                        element, style_attribute)
            if element.get('background'):
                style_attribute = 'background-image:url(%s)' % (
                    element.get('background'))
                yield specificity, check_style_attribute(
                    element, style_attribute)
            if element.get('bgcolor'):
                style_attribute = 'background-color:%s' % (
                    element.get('bgcolor'))
                yield specificity, check_style_attribute(
                    element, style_attribute)
            if element.get('text'):
                style_attribute = 'color:%s' % element.get('text')
                yield specificity, check_style_attribute(
                    element, style_attribute)
            # TODO: we should support link, vlink, alink
        elif element.tag == 'center':
            yield specificity, check_style_attribute(
                element, 'text-align:center')
        elif element.tag == 'div':
            align = element.get('align', '').lower()
            if align == 'middle':
                yield specificity, check_style_attribute(
                    element, 'text-align:center')
            elif align in ('center', 'left', 'right', 'justify'):
                yield specificity, check_style_attribute(
                    element, 'text-align:%s' % align)
        elif element.tag == 'font':
            if element.get('color'):
                yield specificity, check_style_attribute(
                    element, 'color:%s' % element.get('color'))
            if element.get('face'):
                yield specificity, check_style_attribute(
                    element, 'font-family:%s' % element.get('face'))
            if element.get('size'):
                size = element.get('size').strip()
                relative_plus = size.startswith('+')
                relative_minus = size.startswith('-')
                if relative_plus or relative_minus:
                    size = size[1:].strip()
                try:
                    size = int(size)
                except ValueError:
                    LOGGER.warning('Invalid value for size: %s', size)
                else:
                    font_sizes = {
                        1: 'x-small',
                        2: 'small',
                        3: 'medium',
                        4: 'large',
                        5: 'x-large',
                        6: 'xx-large',
                        7: '48px',  # 1.5 * xx-large
                    }
                    if relative_plus:
                        size += 3
                    elif relative_minus:
                        size -= 3
                    size = max(1, min(7, size))
                    yield specificity, check_style_attribute(
                        element, 'font-size:%s' % font_sizes[size])
        elif element.tag == 'table':
            # TODO: we should support cellpadding
            if element.get('cellspacing'):
                yield specificity, check_style_attribute(
                    element,
                    'border-spacing:%spx' % element.get('cellspacing'))
            if element.get('cellpadding'):
                cellpadding = element.get('cellpadding')
                if cellpadding.isdigit():
                    cellpadding += 'px'
                # TODO: don't match subtables cells
                for subelement in element.iter():
                    if subelement.tag in ('td', 'th'):
                        yield specificity, check_style_attribute(
                            subelement,
                            'padding-left:%s;padding-right:%s;'
                            'padding-top:%s;padding-bottom:%s;' % (
                                4 * (cellpadding,)))
            if element.get('hspace'):
                hspace = element.get('hspace')
                if hspace.isdigit():
                    hspace += 'px'
                yield specificity, check_style_attribute(
                    element,
                    'margin-left:%s;margin-right:%s' % (hspace, hspace))
            if element.get('vspace'):
                vspace = element.get('vspace')
                if vspace.isdigit():
                    vspace += 'px'
                yield specificity, check_style_attribute(
                    element,
                    'margin-top:%s;margin-bottom:%s' % (vspace, vspace))
            if element.get('width'):
                style_attribute = 'width:%s' % element.get('width')
                if element.get('width').isdigit():
                    style_attribute += 'px'
                yield specificity, check_style_attribute(
                    element, style_attribute)
            if element.get('height'):
                style_attribute = 'height:%s' % element.get('height')
                if element.get('height').isdigit():
                    style_attribute += 'px'
                yield specificity, check_style_attribute(
                    element, style_attribute)
            if element.get('background'):
                style_attribute = 'background-image:url(%s)' % (
                    element.get('background'))
                yield specificity, check_style_attribute(
                    element, style_attribute)
            if element.get('bgcolor'):
                style_attribute = 'background-color:%s' % (
                    element.get('bgcolor'))
                yield specificity, check_style_attribute(
                    element, style_attribute)
            if element.get('bordercolor'):
                style_attribute = 'border-color:%s' % (
                    element.get('bordercolor'))
                yield specificity, check_style_attribute(
                    element, style_attribute)
            if element.get('border'):
                style_attribute = 'border-width:%spx' % (
                    element.get('border'))
                yield specificity, check_style_attribute(
                    element, style_attribute)
        elif element.tag in ('tr', 'td', 'th', 'thead', 'tbody', 'tfoot'):
            align = element.get('align', '').lower()
            # TODO: we should align descendants too
            if align == 'middle':
                yield specificity, check_style_attribute(
                    element, 'text-align:center')
            elif align in ('center', 'left', 'right', 'justify'):
                yield specificity, check_style_attribute(
                    element, 'text-align:%s' % align)
            if element.get('background'):
                style_attribute = 'background-image:url(%s)' % (
                    element.get('background'))
                yield specificity, check_style_attribute(
                    element, style_attribute)
            if element.get('bgcolor'):
                style_attribute = 'background-color:%s' % (
                    element.get('bgcolor'))
                yield specificity, check_style_attribute(
                    element, style_attribute)
            if element.tag in ('tr', 'td', 'th'):
                if element.get('height'):
                    style_attribute = 'height:%s' % element.get('height')
                    if element.get('height').isdigit():
                        style_attribute += 'px'
                    yield specificity, check_style_attribute(
                        element, style_attribute)
                if element.tag in ('td', 'th'):
                    if element.get('width'):
                        style_attribute = 'width:%s' % element.get('width')
                        if element.get('width').isdigit():
                            style_attribute += 'px'
                        yield specificity, check_style_attribute(
                            element, style_attribute)
        elif element.tag == 'caption':
            align = element.get('align', '').lower()
            # TODO: we should align descendants too
            if align == 'middle':
                yield specificity, check_style_attribute(
                    element, 'text-align:center')
            elif align in ('center', 'left', 'right', 'justify'):
                yield specificity, check_style_attribute(
                    element, 'text-align:%s' % align)
        elif element.tag == 'col':
            if element.get('width'):
                style_attribute = 'width:%s' % element.get('width')
                if element.get('width').isdigit():
                    style_attribute += 'px'
                yield specificity, check_style_attribute(
                    element, style_attribute)
        elif element.tag == 'hr':
            size = 0
            if element.get('size'):
                try:
                    size = int(element.get('size'))
                except ValueError:
                    LOGGER.warning('Invalid value for size: %s', size)
            if (element.get('color'), element.get('noshade')) != (None, None):
                if size >= 1:
                    yield specificity, check_style_attribute(
                        element, 'border-width:%spx' % (size / 2))
            elif size == 1:
                yield specificity, check_style_attribute(
                    element, 'border-bottom-width:0')
            elif size > 1:
                yield specificity, check_style_attribute(
                    element, 'height:%spx' % (size - 2))
            if element.get('width'):
                style_attribute = 'width:%s' % element.get('width')
                if element.get('width').isdigit():
                    style_attribute += 'px'
                yield specificity, check_style_attribute(
                    element, style_attribute)
            if element.get('color'):
                yield specificity, check_style_attribute(
                    element, 'color:%s' % element.get('color'))
        elif element.tag in (
                'iframe', 'applet', 'embed', 'img', 'input', 'object'):
            if (element.tag != 'input' or
                    element.get('type', '').lower() == 'image'):
                align = element.get('align', '').lower()
                if align in ('middle', 'center'):
                    # TODO: middle and center values are wrong
                    yield specificity, check_style_attribute(
                        element, 'vertical-align:middle')
                if element.get('hspace'):
                    hspace = element.get('hspace')
                    if hspace.isdigit():
                        hspace += 'px'
                    yield specificity, check_style_attribute(
                        element,
                        'margin-left:%s;margin-right:%s' % (hspace, hspace))
                if element.get('vspace'):
                    vspace = element.get('vspace')
                    if vspace.isdigit():
                        vspace += 'px'
                    yield specificity, check_style_attribute(
                        element,
                        'margin-top:%s;margin-bottom:%s' % (vspace, vspace))
                # TODO: img seems to be excluded for width and height, but a
                # lot of W3C tests rely on this attribute being applied to img
                if element.get('width'):
                    style_attribute = 'width:%s' % element.get('width')
                    if element.get('width').isdigit():
                        style_attribute += 'px'
                    yield specificity, check_style_attribute(
                        element, style_attribute)
                if element.get('height'):
                    style_attribute = 'height:%s' % element.get('height')
                    if element.get('height').isdigit():
                        style_attribute += 'px'
                    yield specificity, check_style_attribute(
                        element, style_attribute)
                if element.tag in ('img', 'object', 'input'):
                    if element.get('border'):
                        yield specificity, check_style_attribute(
                            element,
                            'border-width:%spx;border-style:solid' %
                            element.get('border'))
        elif element.tag == 'ol':
            # From https://www.w3.org/TR/css-lists-3/
            if element.get('start'):
                yield specificity, check_style_attribute(
                    element,
                    'counter-reset:list-item %s;'
                    'counter-increment:list-item -1' % element.get('start'))
        elif element.tag == 'ul':
            # From https://www.w3.org/TR/css-lists-3/
            if element.get('value'):
                yield specificity, check_style_attribute(
                    element,
                    'counter-reset:list-item %s;'
                    'counter-increment:none' % element.get('value'))


def declaration_precedence(origin, importance):
    """Return the precedence for a declaration.

    Precedence values have no meaning unless compared to each other.

    Acceptable values for ``origin`` are the strings ``'author'``, ``'user'``
    and ``'user agent'``.

    """
    # See http://www.w3.org/TR/CSS21/cascade.html#cascading-order
    if origin == 'user agent':
        return 1
    elif origin == 'user' and not importance:
        return 2
    elif origin == 'author' and not importance:
        return 3
    elif origin == 'author':  # and importance
        return 4
    else:
        assert origin == 'user'  # and importance
        return 5


def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None,
                           root_style=None, base_url=None,
                           target_collector=None):
    """Get a dict of computed style mixed from parent and cascaded styles."""
    if not cascaded and parent_style is not None:
        # Fast path for anonymous boxes:
        # no cascaded style, only implicitly initial or inherited values.
        computed = dict(INITIAL_VALUES)
        for name in parent_style:
            if name in INHERITED or name.startswith('__'):
                computed[name] = parent_style[name]
        # page is not inherited but taken from the ancestor if 'auto'
        computed['page'] = parent_style['page']
        # border-*-style is none, so border-width computes to zero.
        # Other than that, properties that would need computing are
        # border-*-color, but they do not apply.
        computed['border_top_width'] = 0
        computed['border_bottom_width'] = 0
        computed['border_left_width'] = 0
        computed['border_right_width'] = 0
        computed['outline_width'] = 0
        return computed

    # Handle inheritance and initial values
    specified = {}
    computed = {}

    if parent_style:
        for name in parent_style:
            if name.startswith('__'):
                computed[name] = specified[name] = parent_style[name]
    for name in cascaded:
        if name.startswith('__'):
            computed[name] = specified[name] = cascaded[name][0]

    for name, initial in INITIAL_VALUES.items():
        if name in cascaded:
            value, _precedence = cascaded[name]
            keyword = value
        else:
            if name in INHERITED:
                keyword = 'inherit'
            else:
                keyword = 'initial'

        if keyword == 'inherit' and parent_style is None:
            # On the root element, 'inherit' from initial values
            keyword = 'initial'

        if keyword == 'initial':
            value = initial
            if name not in INITIAL_NOT_COMPUTED:
                # The value is the same as when computed
                computed[name] = value
        elif keyword == 'inherit':
            value = parent_style[name]
            # Values in parent_style are already computed.
            computed[name] = value

        specified[name] = value

    if specified['page'] == 'auto':
        # The page property does not inherit. However, if the page value on
        # an element is auto, then its used value is the value specified on
        # its nearest ancestor with a non-auto value. When specified on the
        # root element, the used value for auto is the empty string.
        computed['page'] = specified['page'] = (
            '' if parent_style is None else parent_style['page'])

    return computed_values.compute(
        element, pseudo_type, specified, computed, parent_style, root_style,
        base_url, target_collector)


def parse_page_selectors(rule):
    """Parse a page selector rule.

    Return a list of page data if the rule is correctly parsed. Page data are a
    dict containing:

    - 'side' ('left', 'right' or None),
    - 'blank' (True or None),
    - 'first' (True or None),
    - 'index' (page number or None),
    - 'name' (page name string or None), and
    - 'specificity' (list of numbers).

    Return ``None` if something went wrong while parsing the rule.

    """
    # See https://drafts.csswg.org/css-page-3/#syntax-page-selector

    tokens = list(remove_whitespace(rule.prelude))
    page_data = []

    # TODO: Specificity is probably wrong, should clean and test that.
    if not tokens:
        page_data.append({
            'side': None, 'blank': None, 'first': None, 'index': None,
            'name': None, 'specificity': [0, 0, 0]})
        return page_data

    while tokens:
        types = {
            'side': None, 'blank': None, 'first': None, 'index': None,
            'name': None, 'specificity': [0, 0, 0]}

        if tokens[0].type == 'ident':
            token = tokens.pop(0)
            types['name'] = token.value
            types['specificity'][0] = 1

        if len(tokens) == 1:
            return None
        elif not tokens:
            page_data.append(types)
            return page_data

        while tokens:
            literal = tokens.pop(0)
            if literal.type != 'literal':
                return None

            if literal.value == ':':
                if not tokens:
                    return None

                if tokens[0].type == 'ident':
                    ident = tokens.pop(0)
                    pseudo_class = ident.lower_value

                    if pseudo_class in ('left', 'right'):
                        if types['side'] and types['side'] != pseudo_class:
                            return None
                        types['side'] = pseudo_class
                        types['specificity'][2] += 1
                        continue

                    elif pseudo_class in ('blank', 'first'):
                        types[pseudo_class] = True
                        types['specificity'][1] += 1
                        continue

                elif tokens[0].type == 'function':
                    function = tokens.pop(0)
                    if function.name != 'nth':
                        return None
                    for i, argument in enumerate(function.arguments):
                        if argument.type == 'ident' and argument.value == 'of':
                            nth = function.arguments[:i - 1]
                            group = function.arguments[i:]
                            break
                    else:
                        nth = function.arguments
                        group = None

                    nth_values = tinycss2.nth.parse_nth(nth)
                    if nth_values is None:
                        return None

                    if group is not None:
                        group = [
                            token for token in group
                            if token.type not in (
                                'comment', 'whitespacespace')]
                        if len(group) != 1:
                            return None
                        group, = group
                        if group.type != 'ident':
                            return None
                        group = group.value

                        # TODO: handle page groups
                        return None

                    types['index'] = (*nth_values, group)
                    # TODO: specificity is not specified yet
                    # https://github.com/w3c/csswg-drafts/issues/3524
                    types['specificity'][1] += 1
                    continue

                return None
            elif literal.value == ',':
                if tokens and any(types['specificity']):
                    break
                else:
                    return None

        page_data.append(types)

    return page_data


def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules,
                          url_fetcher, matcher, page_rules, fonts,
                          font_config, ignore_imports=False):
    """Do the work that can be done early on stylesheet, before they are
    in a document.

    """
    for rule in stylesheet_rules:
        if getattr(rule, 'content', None) is None and (
                rule.type != 'at-rule' or rule.lower_at_keyword != 'import'):
            continue

        if rule.type == 'qualified-rule':
            declarations = list(preprocess_declarations(
                base_url, tinycss2.parse_declaration_list(rule.content)))
            if declarations:
                logger_level = WARNING
                try:
                    selectors = cssselect2.compile_selector_list(rule.prelude)
                    for selector in selectors:
                        matcher.add_selector(selector, declarations)
                        if selector.pseudo_element not in PSEUDO_ELEMENTS:
                            if selector.pseudo_element.startswith('-'):
                                logger_level = DEBUG
                                raise cssselect2.SelectorError(
                                    'ignored prefixed pseudo-element: %s'
                                    % selector.pseudo_element)
                            else:
                                raise cssselect2.SelectorError(
                                    'unknown pseudo-element: %s'
                                    % selector.pseudo_element)
                    ignore_imports = True
                except cssselect2.SelectorError as exc:
                    LOGGER.log(
                        logger_level,
                        "Invalid or unsupported selector '%s', %s",
                        tinycss2.serialize(rule.prelude), exc)
                    continue
            else:
                ignore_imports = True

        elif rule.type == 'at-rule' and rule.lower_at_keyword == 'import':
            if ignore_imports:
                LOGGER.warning('@import rule "%s" not at the beginning of the '
                               'the whole rule was ignored at %s:%s.',
                               tinycss2.serialize(rule.prelude),
                               rule.source_line, rule.source_column)
                continue

            tokens = remove_whitespace(rule.prelude)
            if tokens and tokens[0].type in ('url', 'string'):
                url = tokens[0].value
            else:
                continue
            media = media_queries.parse_media_query(tokens[1:])
            if media is None:
                LOGGER.warning('Invalid media type "%s" '
                               'the whole @import rule was ignored at %s:%s.',
                               tinycss2.serialize(rule.prelude),
                               rule.source_line, rule.source_column)
                continue
            if not media_queries.evaluate_media_query(
                    media, device_media_type):
                continue
            url = url_join(
                base_url, url, allow_relative=False,
                context='@import at %s:%s',
                context_args=(rule.source_line, rule.source_column))
            if url is not None:
                try:
                    CSS(
                        url=url, url_fetcher=url_fetcher,
                        media_type=device_media_type, font_config=font_config,
                        matcher=matcher, page_rules=page_rules)
                except URLFetchingError as exc:
                    LOGGER.error(
                        'Failed to load stylesheet at %s : %s', url, exc)

        elif rule.type == 'at-rule' and rule.lower_at_keyword == 'media':
            media = media_queries.parse_media_query(rule.prelude)
            if media is None:
                LOGGER.warning('Invalid media type "%s" '
                               'the whole @media rule was ignored at %s:%s.',
                               tinycss2.serialize(rule.prelude),
                               rule.source_line, rule.source_column)
                continue
            ignore_imports = True
            if not media_queries.evaluate_media_query(
                    media, device_media_type):
                continue
            content_rules = tinycss2.parse_rule_list(rule.content)
            preprocess_stylesheet(
                device_media_type, base_url, content_rules, url_fetcher,
                matcher, page_rules, fonts, font_config, ignore_imports=True)

        elif rule.type == 'at-rule' and rule.lower_at_keyword == 'page':
            data = parse_page_selectors(rule)

            if data is None:
                LOGGER.warning(
                    'Unsupported @page selector "%s", '
                    'the whole @page rule was ignored at %s:%s.',
                    tinycss2.serialize(rule.prelude),
                    rule.source_line, rule.source_column)
                continue

            ignore_imports = True
            for page_type in data:
                specificity = page_type.pop('specificity')
                page_type = PageType(**page_type)
                content = tinycss2.parse_declaration_list(rule.content)
                declarations = list(preprocess_declarations(base_url, content))

                if declarations:
                    selector_list = [(specificity, None, page_type)]
                    page_rules.append((rule, selector_list, declarations))

                for margin_rule in content:
                    if margin_rule.type != 'at-rule' or (
                            margin_rule.content is None):
                        continue
                    declarations = list(preprocess_declarations(
                        base_url,
                        tinycss2.parse_declaration_list(margin_rule.content)))
                    if declarations:
                        selector_list = [(
                            specificity, '@' + margin_rule.lower_at_keyword,
                            page_type)]
                        page_rules.append(
                            (margin_rule, selector_list, declarations))

        elif rule.type == 'at-rule' and rule.lower_at_keyword == 'font-face':
            ignore_imports = True
            content = tinycss2.parse_declaration_list(rule.content)
            rule_descriptors = dict(preprocess_descriptors(base_url, content))
            for key in ('src', 'font_family'):
                if key not in rule_descriptors:
                    LOGGER.warning(
                        "Missing %s descriptor in '@font-face' rule at %s:%s",
                        key.replace('_', '-'),
                        rule.source_line, rule.source_column)
                    break
            else:
                if font_config is not None:
                    font_filename = font_config.add_font_face(
                        rule_descriptors, url_fetcher)
                    if font_filename:
                        fonts.append(font_filename)


def get_all_computed_styles(html, user_stylesheets=None,
                            presentational_hints=False, font_config=None,
                            page_rules=None, target_collector=None):
    """Compute all the computed styles of all elements in ``html`` document.

    Do everything from finding author stylesheets to parsing and applying them.

    Return a ``style_for`` function that takes an element and an optional
    pseudo-element type, and return a style dict object.

    """
    # List stylesheets. Order here is not important ('origin' is).
    sheets = []
    for sheet in (html._ua_stylesheets() or []):
        sheets.append((sheet, 'user agent', None))
    if presentational_hints:
        for sheet in (html._ph_stylesheets() or []):
            sheets.append((sheet, 'author', (0, 0, 0)))
    for sheet in find_stylesheets(
            html.wrapper_element, html.media_type, html.url_fetcher,
            html.base_url, font_config, page_rules):
        sheets.append((sheet, 'author', None))
    for sheet in (user_stylesheets or []):
        sheets.append((sheet, 'user', None))

    return StyleFor(html, sheets, presentational_hints, target_collector)
