"""

Data structures for the CSS abstract syntax tree.

"""


from webencodings import ascii_lower

from .serializer import _serialize_to, serialize_identifier, serialize_name


class Node:
    """Every node type inherits from this class,
    which is never instantiated directly.

    .. attribute:: type

        Each child class has a :attr:`type` class attribute
        with a unique string value.
        This allows checking for the node type with code like:

        .. code-block:: python

            if node.type == 'whitespace':

        instead of the more verbose:

        .. code-block:: python

            from tinycss2.ast import WhitespaceToken
            if isinstance(node, WhitespaceToken):

    Every node also has these attributes and methods,
    which are not repeated for brevity:

    .. attribute:: source_line

        The line number of the start of the node in the CSS source.
        Starts at 1.

    .. attribute:: source_column

        The column number within :attr:`source_line` of the start of the node
        in the CSS source.
        Starts at 1.

    .. automethod:: serialize

    """
    __slots__ = ['source_column', 'source_line']

    def __init__(self, source_line, source_column):
        self.source_line = source_line
        self.source_column = source_column

    def __repr__(self):
        return self.repr_format.format(self=self)

    def serialize(self):
        """Serialize this node to CSS syntax and return a Unicode string."""
        chunks = []
        self._serialize_to(chunks.append)
        return ''.join(chunks)

    def _serialize_to(self, write):
        """Serialize this node to CSS syntax, writing chunks as Unicode string
        by calling the provided :obj:`write` callback.

        """
        raise NotImplementedError  # pragma: no cover


class ParseError(Node):
    """A syntax error of some sort. May occur anywhere in the tree.

    Syntax errors are not fatal in the parser
    to allow for different error handling behaviors.
    For example, an error in a Selector list makes the whole rule invalid,
    but an error in a Media Query list only replaces one comma-separated query
    with ``not all``.

    .. autoattribute:: type

    .. attribute:: kind

        Machine-readable string indicating the type of error.
        Example: ``'bad-url'``.

    .. attribute:: message

        Human-readable explanation of the error, as a string.
        Could be translated, expanded to include details, etc.

    """
    __slots__ = ['kind', 'message']
    type = 'error'
    repr_format = '<{self.__class__.__name__} {self.kind}>'

    def __init__(self, line, column, kind, message):
        Node.__init__(self, line, column)
        self.kind = kind
        self.message = message

    def _serialize_to(self, write):
        if self.kind == 'bad-string':
            write('"[bad string]\n')
        elif self.kind == 'bad-url':
            write('url([bad url])')
        elif self.kind in ')]}':
            write(self.kind)
        elif self.kind in ('eof-in-string', 'eof-in-url'):
            pass
        else:  # pragma: no cover
            raise TypeError('Can not serialize %r' % self)


class Comment(Node):
    """A CSS comment.

    Comments can be ignored by passing ``skip_comments=True``
    to functions such as :func:`~tinycss2.parse_component_value_list`.

    .. autoattribute:: type

    .. attribute:: value

        The content of the comment, between ``/*`` and ``*/``, as a string.

    """
    __slots__ = ['value']
    type = 'comment'
    repr_format = '<{self.__class__.__name__} {self.value}>'

    def __init__(self, line, column, value):
        Node.__init__(self, line, column)
        self.value = value

    def _serialize_to(self, write):
        write('/*')
        write(self.value)
        write('*/')


class WhitespaceToken(Node):
    """A :diagram:`whitespace-token`.

    .. autoattribute:: type

    .. attribute:: value

        The whitespace sequence, as a string, as in the original CSS source.


    """
    __slots__ = ['value']
    type = 'whitespace'
    repr_format = '<{self.__class__.__name__}>'

    def __init__(self, line, column, value):
        Node.__init__(self, line, column)
        self.value = value

    def _serialize_to(self, write):
        write(self.value)


class LiteralToken(Node):
    r"""Token that represents one or more characters as in the CSS source.

    .. autoattribute:: type

    .. attribute:: value

        A string of one to four characters.

    Instances compare equal to their :attr:`value`,
    so that these are equivalent:

    .. code-block:: python

        if node == ';':
        if node.type == 'literal' and node.value == ';':

    This regroups what `the specification`_ defines as separate token types:

    .. _the specification: https://drafts.csswg.org/css-syntax-3/

    * *<colon-token>* ``:``
    * *<semicolon-token>* ``;``
    * *<comma-token>* ``,``
    * *<cdc-token>* ``-->``
    * *<cdo-token>* ``<!--``
    * *<include-match-token>* ``~=``
    * *<dash-match-token>* ``|=``
    * *<prefix-match-token>* ``^=``
    * *<suffix-match-token>* ``$=``
    * *<substring-match-token>* ``*=``
    * *<column-token>* ``||``
    * *<delim-token>* (a single ASCII character not part of any another token)

    """
    __slots__ = ['value']
    type = 'literal'
    repr_format = '<{self.__class__.__name__} {self.value}>'

    def __init__(self, line, column, value):
        Node.__init__(self, line, column)
        self.value = value

    def __eq__(self, other):
        return self.value == other or self is other

    def __ne__(self, other):
        return not self == other

    def _serialize_to(self, write):
        write(self.value)


class IdentToken(Node):
    """An :diagram:`ident-token`.

    .. autoattribute:: type

    .. attribute:: value

        The unescaped value, as a Unicode string.

    .. attribute:: lower_value

        Same as :attr:`value` but normalized to *ASCII lower case*,
        see :func:`~webencodings.ascii_lower`.
        This is the value to use when comparing to a CSS keyword.

    """
    __slots__ = ['lower_value', 'value']
    type = 'ident'
    repr_format = '<{self.__class__.__name__} {self.value}>'

    def __init__(self, line, column, value):
        Node.__init__(self, line, column)
        self.value = value
        try:
            self.lower_value = ascii_lower(value)
        except UnicodeEncodeError:
            self.lower_value = value

    def _serialize_to(self, write):
        write(serialize_identifier(self.value))


class AtKeywordToken(Node):
    """An :diagram:`at-keyword-token`.

    .. code-block:: text

        '@' <value>

    .. autoattribute:: type

    .. attribute:: value

        The unescaped value, as a Unicode string, without the preceding ``@``.

    .. attribute:: lower_value

        Same as :attr:`value` but normalized to *ASCII lower case*,
        see :func:`~webencodings.ascii_lower`.
        This is the value to use when comparing to a CSS at-keyword.

        .. code-block:: python

            if node.type == 'at-keyword' and node.lower_value == 'import':

    """
    __slots__ = ['lower_value', 'value']
    type = 'at-keyword'
    repr_format = '<{self.__class__.__name__} @{self.value}>'

    def __init__(self, line, column, value):
        Node.__init__(self, line, column)
        self.value = value
        try:
            self.lower_value = ascii_lower(value)
        except UnicodeEncodeError:
            self.lower_value = value

    def _serialize_to(self, write):
        write('@')
        write(serialize_identifier(self.value))


class HashToken(Node):
    r"""A :diagram:`hash-token`.

    .. code-block:: text

        '#' <value>

    .. autoattribute:: type

    .. attribute:: value

        The unescaped value, as a Unicode string, without the preceding ``#``.

    .. attribute:: is_identifier

        A boolean, true if the CSS source for this token
        was ``#`` followed by a valid identifier.
        (Only such hash tokens are valid ID selectors.)

    """
    __slots__ = ['is_identifier', 'value']
    type = 'hash'
    repr_format = '<{self.__class__.__name__} #{self.value}>'

    def __init__(self, line, column, value, is_identifier):
        Node.__init__(self, line, column)
        self.value = value
        self.is_identifier = is_identifier

    def _serialize_to(self, write):
        write('#')
        if self.is_identifier:
            write(serialize_identifier(self.value))
        else:
            write(serialize_name(self.value))


class StringToken(Node):
    """A :diagram:`string-token`.

    .. code-block:: text

        '"' <value> '"'

    .. autoattribute:: type

    .. attribute:: value

        The unescaped value, as a Unicode string, without the quotes.

    """
    __slots__ = ['representation', 'value']
    type = 'string'
    repr_format = '<{self.__class__.__name__} {self.representation}>'

    def __init__(self, line, column, value, representation):
        Node.__init__(self, line, column)
        self.value = value
        self.representation = representation

    def _serialize_to(self, write):
        write(self.representation)


class URLToken(Node):
    """An :diagram:`url-token`.

    .. code-block:: text

        'url(' <value> ')'

    .. autoattribute:: type

    .. attribute:: value

        The unescaped URL, as a Unicode string, without the ``url(`` and ``)``
        markers.

    """
    __slots__ = ['representation', 'value']
    type = 'url'
    repr_format = '<{self.__class__.__name__} {self.representation}>'

    def __init__(self, line, column, value, representation):
        Node.__init__(self, line, column)
        self.value = value
        self.representation = representation

    def _serialize_to(self, write):
        write(self.representation)


class UnicodeRangeToken(Node):
    """A :diagram:`unicode-range-token`.

    .. autoattribute:: type

    .. attribute:: start

        The start of the range, as an integer between 0 and 1114111.

    .. attribute:: end

        The end of the range, as an integer between 0 and 1114111.
        Same as :attr:`start` if the source only specified one value.

    """
    __slots__ = ['end', 'start']
    type = 'unicode-range'
    repr_format = '<{self.__class__.__name__} {self.start} {self.end}>'

    def __init__(self, line, column, start, end):
        Node.__init__(self, line, column)
        self.start = start
        self.end = end

    def _serialize_to(self, write):
        if self.end == self.start:
            write('U+%X' % self.start)
        else:
            write('U+%X-%X' % (self.start, self.end))


class NumberToken(Node):
    """A :diagram:`number-token`.

    .. autoattribute:: type

    .. attribute:: value

        The numeric value as a :class:`float`.

    .. attribute:: int_value

        The numeric value as an :class:`int`
        if :attr:`is_integer` is true, :obj:`None` otherwise.

    .. attribute:: is_integer

        Whether the token was syntactically an integer, as a boolean.

    .. attribute:: representation

        The CSS representation of the value, as a Unicode string.

    """
    __slots__ = ['int_value', 'is_integer', 'representation', 'value']
    type = 'number'
    repr_format = '<{self.__class__.__name__} {self.representation}>'

    def __init__(self, line, column, value, int_value, representation):
        Node.__init__(self, line, column)
        self.value = value
        self.int_value = int_value
        self.is_integer = int_value is not None
        self.representation = representation

    def _serialize_to(self, write):
        write(self.representation)


class PercentageToken(Node):
    """A :diagram:`percentage-token`.

    .. code-block:: text

        <representation> '%'

    .. autoattribute:: type

    .. attribute:: value

        The value numeric as a :class:`float`.

    .. attribute:: int_value

        The numeric value as an :class:`int`
        if the token was syntactically an integer,
        or :obj:`None`.

    .. attribute:: is_integer

        Whether the token’s value was syntactically an integer, as a boolean.

    .. attribute:: representation

        The CSS representation of the value without the unit,
        as a Unicode string.

    """
    __slots__ = ['int_value', 'is_integer', 'representation', 'value']
    type = 'percentage'
    repr_format = '<{self.__class__.__name__} {self.representation}%>'

    def __init__(self, line, column, value, int_value, representation):
        Node.__init__(self, line, column)
        self.value = value
        self.int_value = int_value
        self.is_integer = int_value is not None
        self.representation = representation

    def _serialize_to(self, write):
        write(self.representation)
        write('%')


class DimensionToken(Node):
    """A :diagram:`dimension-token`.

    .. code-block:: text

        <representation> <unit>

    .. autoattribute:: type

    .. attribute:: value

        The value numeric as a :class:`float`.

    .. attribute:: int_value

        The numeric value as an :class:`int`
        if the token was syntactically an integer,
        or :obj:`None`.

    .. attribute:: is_integer

        Whether the token’s value was syntactically an integer, as a boolean.

    .. attribute:: representation

        The CSS representation of the value without the unit,
        as a Unicode string.

    .. attribute:: unit

        The unescaped unit, as a Unicode string.

    .. attribute:: lower_unit

        Same as :attr:`unit` but normalized to *ASCII lower case*,
        see :func:`~webencodings.ascii_lower`.
        This is the value to use when comparing to a CSS unit.

        .. code-block:: python

            if node.type == 'dimension' and node.lower_unit == 'px':

    """
    __slots__ = [
        'int_value',
        'is_integer',
        'lower_unit',
        'representation',
        'unit',
        'value',
    ]
    type = 'dimension'
    repr_format = ('<{self.__class__.__name__} '
                   '{self.representation}{self.unit}>')

    def __init__(self, line, column, value, int_value, representation, unit):
        Node.__init__(self, line, column)
        self.value = value
        self.int_value = int_value
        self.is_integer = int_value is not None
        self.representation = representation
        self.unit = unit
        try:
            self.lower_unit = ascii_lower(unit)
        except UnicodeEncodeError:
            self.lower_unit = unit

    def _serialize_to(self, write):
        write(self.representation)
        # Disambiguate with scientific notation
        unit = self.unit
        if unit in ('e', 'E') or unit.startswith(('e-', 'E-')):
            write('\\65 ')
            write(serialize_name(unit[1:]))
        else:
            write(serialize_identifier(unit))


class ParenthesesBlock(Node):
    """A :diagram:`()-block`.

    .. code-block:: text

        '(' <content> ')'

    .. autoattribute:: type

    .. attribute:: content

        The content of the block, as list of :term:`component values`.
        The ``(`` and ``)`` markers themselves are not represented in the list.

    """
    __slots__ = ['content']
    type = '() block'
    repr_format = '<{self.__class__.__name__} ( … )>'

    def __init__(self, line, column, content):
        Node.__init__(self, line, column)
        self.content = content

    def _serialize_to(self, write):
        write('(')
        _serialize_to(self.content, write)
        write(')')


class SquareBracketsBlock(Node):
    """A :diagram:`[]-block`.

    .. code-block:: text

        '[' <content> ']'

    .. autoattribute:: type

    .. attribute:: content

        The content of the block, as list of :term:`component values`.
        The ``[`` and ``]`` markers themselves are not represented in the list.

    """
    __slots__ = ['content']
    type = '[] block'
    repr_format = '<{self.__class__.__name__} [ … ]>'

    def __init__(self, line, column, content):
        Node.__init__(self, line, column)
        self.content = content

    def _serialize_to(self, write):
        write('[')
        _serialize_to(self.content, write)
        write(']')


class CurlyBracketsBlock(Node):
    """A :diagram:`{}-block`.

    .. code-block:: text

        '{' <content> '}'

    .. autoattribute:: type

    .. attribute:: content

        The content of the block, as list of :term:`component values`.
        The ``[`` and ``]`` markers themselves are not represented in the list.

    """
    __slots__ = ['content']
    type = '{} block'
    repr_format = '<{self.__class__.__name__} {{ … }}>'

    def __init__(self, line, column, content):
        Node.__init__(self, line, column)
        self.content = content

    def _serialize_to(self, write):
        write('{')
        _serialize_to(self.content, write)
        write('}')


class FunctionBlock(Node):
    """A :diagram:`function-block`.

    .. code-block:: text

        <name> '(' <arguments> ')'

    .. autoattribute:: type

    .. attribute:: name

        The unescaped name of the function, as a Unicode string.

    .. attribute:: lower_name

        Same as :attr:`name` but normalized to *ASCII lower case*,
        see :func:`~webencodings.ascii_lower`.
        This is the value to use when comparing to a CSS function name.

    .. attribute:: arguments

        The arguments of the function, as list of :term:`component values`.
        The ``(`` and ``)`` markers themselves are not represented in the list.
        Commas are not special, but represented as :obj:`LiteralToken` objects
        in the list.

    """
    __slots__ = ['arguments', 'lower_name', 'name']
    type = 'function'
    repr_format = '<{self.__class__.__name__} {self.name}( … )>'

    def __init__(self, line, column, name, arguments):
        Node.__init__(self, line, column)
        self.name = name
        try:
            self.lower_name = ascii_lower(name)
        except UnicodeEncodeError:
            self.lower_name = name
        self.arguments = arguments

    def _serialize_to(self, write):
        write(serialize_identifier(self.name))
        write('(')
        _serialize_to(self.arguments, write)
        function = self
        while isinstance(function, FunctionBlock) and function.arguments:
            eof_in_string = (
                isinstance(function.arguments[-1], ParseError) and
                function.arguments[-1].kind == 'eof-in-string')
            if eof_in_string:
                return
            function = function.arguments[-1]
        write(')')


class Declaration(Node):
    """A (property or descriptor) :diagram:`declaration`.

    .. code-block:: text

        <name> ':' <value>
        <name> ':' <value> '!important'

    .. autoattribute:: type

    .. attribute:: name

        The unescaped name, as a Unicode string.

    .. attribute:: lower_name

        Same as :attr:`name` but normalized to *ASCII lower case*,
        see :func:`~webencodings.ascii_lower`.
        This is the value to use when comparing to
        a CSS property or descriptor name.

        .. code-block:: python

            if node.type == 'declaration' and node.lower_name == 'color':

    .. attribute:: value

        The declaration value as a list of :term:`component values`:
        anything between ``:`` and
        the end of the declaration, or ``!important``.

    .. attribute:: important

        A boolean, true if the declaration had an ``!important`` marker.
        It is up to the consumer to reject declarations that do not accept
        this flag, such as non-property descriptor declarations.

    """
    __slots__ = ['important', 'lower_name', 'name', 'value']
    type = 'declaration'
    repr_format = '<{self.__class__.__name__} {self.name}: …>'

    def __init__(self, line, column, name, lower_name, value, important):
        Node.__init__(self, line, column)
        self.name = name
        self.lower_name = lower_name
        self.value = value
        self.important = important

    def _serialize_to(self, write):
        write(serialize_identifier(self.name))
        write(':')
        _serialize_to(self.value, write)
        if self.important:
            write('!important')


class QualifiedRule(Node):
    """A :diagram:`qualified rule`.

    .. code-block:: text

        <prelude> '{' <content> '}'

    The interpretation of qualified rules depend on their context.
    At the top-level of a stylesheet
    or in a conditional rule such as ``@media``,
    they are **style rules** where the :attr:`prelude` is Selectors list
    and the :attr:`content` is a list of property declarations.

    .. autoattribute:: type

    .. attribute:: prelude

        The rule’s prelude, the part before the {} block,
        as a list of :term:`component values`.

    .. attribute:: content

        The rule’s content, the part inside the {} block,
        as a list of :term:`component values`.

    """
    __slots__ = ['content', 'prelude']
    type = 'qualified-rule'
    repr_format = ('<{self.__class__.__name__} '
                   '… {{ … }}>')

    def __init__(self, line, column, prelude, content):
        Node.__init__(self, line, column)
        self.prelude = prelude
        self.content = content

    def _serialize_to(self, write):
        _serialize_to(self.prelude, write)
        write('{')
        _serialize_to(self.content, write)
        write('}')


class AtRule(Node):
    """An :diagram:`at-rule`.

    .. code-block:: text

        @<at_keyword> <prelude> '{' <content> '}'
        @<at_keyword> <prelude> ';'

    The interpretation of at-rules depend on their at-keyword
    as well as their context.
    Most types of at-rules (ie. at-keyword values)
    are only allowed in some context,
    and must either end with a {} block or a semicolon.

    .. autoattribute:: type

    .. attribute:: at_keyword

        The unescaped value of the rule’s at-keyword,
        without the ``@`` symbol, as a Unicode string.

    .. attribute:: lower_at_keyword

        Same as :attr:`at_keyword` but normalized to *ASCII lower case*,
        see :func:`~webencodings.ascii_lower`.
        This is the value to use when comparing to a CSS at-keyword.

        .. code-block:: python

            if node.type == 'at-rule' and node.lower_at_keyword == 'import':

    .. attribute:: prelude

        The rule’s prelude, the part before the {} block or semicolon,
        as a list of :term:`component values`.

    .. attribute:: content

        The rule’s content, if any.
        The block’s content as a list of :term:`component values`
        for at-rules with a {} block,
        or :obj:`None` for at-rules ending with a semicolon.

    """
    __slots__ = ['at_keyword', 'content', 'lower_at_keyword', 'prelude']
    type = 'at-rule'
    repr_format = ('<{self.__class__.__name__} '
                   '@{self.at_keyword} … {{ … }}>')

    def __init__(self, line, column,
                 at_keyword, lower_at_keyword, prelude, content):
        Node.__init__(self, line, column)
        self.at_keyword = at_keyword
        self.lower_at_keyword = lower_at_keyword
        self.prelude = prelude
        self.content = content

    def _serialize_to(self, write):
        write('@')
        write(serialize_identifier(self.at_keyword))
        _serialize_to(self.prelude, write)
        if self.content is None:
            write(';')
        else:
            write('{')
            _serialize_to(self.content, write)
            write('}')
