// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:math' as math;

import 'package:source_span/source_span.dart';

import 'src/messages.dart';
import 'src/preprocessor_options.dart';
import 'visitor.dart';

export 'src/messages.dart' show Message, MessageLevel;
export 'src/preprocessor_options.dart';

part 'src/analyzer.dart';
part 'src/polyfill.dart';
part 'src/property.dart';
part 'src/token.dart';
part 'src/token_kind.dart';
part 'src/tokenizer.dart';
part 'src/tokenizer_base.dart';

enum ClauseType {
  none,
  conjunction,
  disjunction,
}

/// Used for parser lookup ahead (used for nested selectors Less support).
class ParserState extends TokenizerState {
  final Token peekToken;
  final Token? previousToken;

  ParserState(this.peekToken, this.previousToken, Tokenizer tokenizer)
      : super(tokenizer);
}

void _createMessages({List<Message>? errors, PreprocessorOptions? options}) {
  errors ??= [];
  options ??= const PreprocessorOptions(useColors: false, inputFile: 'memory');

  messages = Messages(options: options, printHandler: errors.add);
}

/// CSS checked mode enabled.
bool get isChecked => messages.options.checked;

// TODO(terry): Remove nested name parameter.
/// Parse and analyze the CSS file.
StyleSheet compile(Object input,
    {List<Message>? errors,
    PreprocessorOptions? options,
    bool nested = true,
    bool polyfill = false,
    List<StyleSheet>? includes}) {
  includes ??= [];

  var source = _inputAsString(input);

  _createMessages(errors: errors, options: options);

  var file = SourceFile.fromString(source);

  var tree = _Parser(file, source).parse();

  analyze([tree], errors: errors, options: options);

  if (polyfill) {
    var processCss = PolyFill(messages);
    processCss.process(tree, includes: includes);
  }

  return tree;
}

/// Analyze the CSS file.
void analyze(List<StyleSheet> styleSheets,
    {List<Message>? errors, PreprocessorOptions? options}) {
  _createMessages(errors: errors, options: options);
  Analyzer(styleSheets, messages).run();
}

/// Parse the [input] CSS stylesheet into a tree.
///
/// The [input] can be a [String], or [List]`<int>` of bytes and returns a
/// [StyleSheet] AST. The optional [errors] list will collect any error
/// encountered.
StyleSheet parse(
  Object input, {
  List<Message>? errors,
  PreprocessorOptions? options,
}) {
  var source = _inputAsString(input);

  _createMessages(errors: errors, options: options);

  var file = SourceFile.fromString(source);
  return _Parser(file, source).parse();
}

/// Parse the [input] CSS selector into a tree. The [input] can be a [String],
/// or [List]`<int>` of bytes and returns a [StyleSheet] AST.  The optional
/// [errors] list will contain each error/warning as a [Message].
// TODO(jmesserly): should rename "parseSelector" and return Selector
StyleSheet selector(Object input, {List<Message>? errors}) {
  var source = _inputAsString(input);

  _createMessages(errors: errors);

  var file = SourceFile.fromString(source);
  return (_Parser(file, source)..tokenizer.inSelector = true).parseSelector();
}

SelectorGroup? parseSelectorGroup(Object input, {List<Message>? errors}) {
  var source = _inputAsString(input);

  _createMessages(errors: errors);

  var file = SourceFile.fromString(source);
  return (_Parser(file, source)
        // TODO(jmesserly): this fix should be applied to the parser. It's
        // tricky because by the time the flag is set one token has already
        // been fetched.
        ..tokenizer.inSelector = true)
      .processSelectorGroup();
}

String _inputAsString(Object input) {
  String source;

  if (input is String) {
    source = input;
  } else if (input is List) {
    // TODO(terry): The parse function needs an "encoding" argument and will
    //              default to whatever encoding CSS defaults to.
    //
    // Here's some info about CSS encodings:
    // http://www.w3.org/International/questions/qa-css-charset.en.php
    //
    // As JMesserly suggests it will probably need a "pre-parser" html5lib
    // (encoding_parser.dart) that interprets the bytes as ASCII and scans for
    // @charset. But for now an "encoding" argument would work.  Often the
    // HTTP header will indicate the correct encoding.
    //
    // See encoding helpers at: package:html5lib/lib/src/char_encodings.dart
    // These helpers can decode in different formats given an encoding name
    // (mostly unicode, ascii, windows-1252 which is html5 default encoding).
    source = String.fromCharCodes(input as List<int>);
  } else {
    // TODO(terry): Support RandomAccessFile using console.
    throw ArgumentError("'source' must be a String or "
        'List<int> (of bytes). RandomAccessFile not supported from this '
        'simple interface');
  }

  return source;
}

// TODO(terry): Consider removing this class when all usages can be eliminated
//               or replaced with compile API.
/// Public parsing interface for csslib.
class Parser {
  final _Parser _parser;

  // TODO(jmesserly): having file and text is redundant.
  // TODO(rnystrom): baseUrl isn't used. Remove from API.
  Parser(SourceFile file, String text, {int start = 0, String? baseUrl})
      : _parser = _Parser(file, text, start: start);

  StyleSheet parse() => _parser.parse();
}

// CSS2.1 pseudo-elements which were defined with a single ':'.
const _legacyPseudoElements = <String>{
  'after',
  'before',
  'first-letter',
  'first-line',
};

/// A simple recursive descent parser for CSS.
class _Parser {
  final Tokenizer tokenizer;

  /// File containing the source being parsed, used to report errors with
  /// source-span locations.
  final SourceFile file;

  Token? _previousToken;
  late Token _peekToken;

  _Parser(this.file, String text, {int start = 0})
      : tokenizer = Tokenizer(file, text, true, start) {
    _peekToken = tokenizer.next();
  }

  /// Main entry point for parsing an entire CSS file.
  StyleSheet parse() {
    var productions = <TreeNode>[];

    var start = _peekToken.span;
    while (!_maybeEat(TokenKind.END_OF_FILE) && !_peekKind(TokenKind.RBRACE)) {
      // TODO(terry): Need to handle charset.
      final rule = processRule();
      if (rule != null) {
        productions.add(rule);
      } else {
        break;
      }
    }

    checkEndOfFile();

    return StyleSheet(productions, _makeSpan(start));
  }

  /// Main entry point for parsing a simple selector sequence.
  StyleSheet parseSelector() {
    var productions = <TreeNode>[];

    var start = _peekToken.span;
    while (!_maybeEat(TokenKind.END_OF_FILE) && !_peekKind(TokenKind.RBRACE)) {
      var selector = processSelector();
      if (selector != null) {
        productions.add(selector);
      } else {
        break; // Prevent infinite loop if we can't parse something.
      }
    }

    checkEndOfFile();

    return StyleSheet.selector(productions, _makeSpan(start));
  }

  /// Generate an error if [file] has not been completely consumed.
  void checkEndOfFile() {
    if (!(_peekKind(TokenKind.END_OF_FILE) ||
        _peekKind(TokenKind.INCOMPLETE_COMMENT))) {
      _error('premature end of file unknown CSS', _peekToken.span);
    }
  }

  /// Guard to break out of parser when an unexpected end of file is found.
  // TODO(jimhug): Failure to call this method can lead to infinite parser
  //   loops.  Consider embracing exceptions for more errors to reduce
  //   the danger here.
  bool isPrematureEndOfFile() {
    if (_maybeEat(TokenKind.END_OF_FILE)) {
      _error('unexpected end of file', _peekToken.span);
      return true;
    } else {
      return false;
    }
  }

  ///////////////////////////////////////////////////////////////////
  // Basic support methods
  ///////////////////////////////////////////////////////////////////
  int _peek() => _peekToken.kind;

  Token _next({bool unicodeRange = false}) {
    final next = _previousToken = _peekToken;
    _peekToken = tokenizer.next(unicodeRange: unicodeRange);
    return next;
  }

  bool _peekKind(int kind) => _peekToken.kind == kind;

  // Is the next token a legal identifier?  This includes pseudo-keywords.
  bool _peekIdentifier() => TokenKind.isIdentifier(_peekToken.kind);

  /// Marks the parser/tokenizer look ahead to support Less nested selectors.
  ParserState get _mark => ParserState(_peekToken, _previousToken, tokenizer);

  /// Restores the parser/tokenizer state to state remembered by _mark.
  void _restore(ParserState markedData) {
    tokenizer.restore(markedData);
    _peekToken = markedData.peekToken;
    _previousToken = markedData.previousToken;
  }

  bool _maybeEat(int kind, {bool unicodeRange = false}) {
    if (_peekToken.kind == kind) {
      _previousToken = _peekToken;
      _peekToken = tokenizer.next(unicodeRange: unicodeRange);
      return true;
    } else {
      return false;
    }
  }

  void _eat(int kind, {bool unicodeRange = false}) {
    if (!_maybeEat(kind, unicodeRange: unicodeRange)) {
      _errorExpected(TokenKind.kindToString(kind));
    }
  }

  void _errorExpected(String expected) {
    var tok = _next();
    String message;
    try {
      message = 'expected $expected, but found $tok';
    } catch (e) {
      message = 'parsing error expected $expected';
    }
    _error(message, tok.span);
  }

  void _error(String message, SourceSpan? location) {
    location ??= _peekToken.span;
    messages.error(message, location);
  }

  void _warning(String message, SourceSpan? location) {
    location ??= _peekToken.span;
    messages.warning(message, location);
  }

  FileSpan _makeSpan(FileSpan start) {
    // TODO(terry): there are places where we are creating spans before we eat
    // the tokens, so using _previousToken is not always valid.
    // TODO(nweiz): use < rather than compareTo when SourceSpan supports it.
    if (_previousToken == null || _previousToken!.span.compareTo(start) < 0) {
      return start;
    }
    return start.expand(_previousToken!.span);
  }

  ///////////////////////////////////////////////////////////////////
  // Top level productions
  ///////////////////////////////////////////////////////////////////

  /// The media_query_list production below replaces the media_list production
  /// from CSS2 the new grammar is:
  ///
  ///     media_query_list
  ///      : S* [media_query [ ',' S* media_query ]* ]?
  ///     media_query
  ///      : [ONLY | NOT]? S* media_type S* [ AND S* expression ]*
  ///      | expression [ AND S* expression ]*
  ///     media_type
  ///      : IDENT
  ///     expression
  ///      : '(' S* media_feature S* [ ':' S* expr ]? ')' S*
  ///     media_feature
  ///      : IDENT
  List<MediaQuery> processMediaQueryList() {
    var mediaQueries = <MediaQuery>[];

    do {
      var mediaQuery = processMediaQuery();
      if (mediaQuery != null) {
        mediaQueries.add(mediaQuery);
      } else {
        break;
      }
    } while (_maybeEat(TokenKind.COMMA));

    return mediaQueries;
  }

  MediaQuery? processMediaQuery() {
    // Grammar: [ONLY | NOT]? S* media_type S*
    //          [ AND S* MediaExpr ]* | MediaExpr [ AND S* MediaExpr ]*

    var start = _peekToken.span;

    // Is it a unary media operator?
    var op = _peekToken.text;
    var opLen = op.length;
    var unaryOp = TokenKind.matchMediaOperator(op, 0, opLen);
    if (unaryOp != -1) {
      if (isChecked) {
        if (unaryOp != TokenKind.MEDIA_OP_NOT ||
            unaryOp != TokenKind.MEDIA_OP_ONLY) {
          _warning('Only the unary operators NOT and ONLY allowed',
              _makeSpan(start));
        }
      }
      _next();
      start = _peekToken.span;
    }

    Identifier? type;
    // Get the media type.
    if (_peekIdentifier()) type = identifier();

    var exprs = <MediaExpression>[];

    while (true) {
      // Parse AND if query has a media_type or previous expression.
      var andOp = exprs.isNotEmpty || type != null;
      if (andOp) {
        op = _peekToken.text;
        opLen = op.length;
        if (TokenKind.matchMediaOperator(op, 0, opLen) !=
            TokenKind.MEDIA_OP_AND) {
          break;
        }
        _next();
      }

      var expr = processMediaExpression(andOp);
      if (expr == null) break;

      exprs.add(expr);
    }

    if (unaryOp != -1 || type != null || exprs.isNotEmpty) {
      return MediaQuery(unaryOp, type, exprs, _makeSpan(start));
    }
    return null;
  }

  MediaExpression? processMediaExpression([bool andOperator = false]) {
    var start = _peekToken.span;

    // Grammar: '(' S* media_feature S* [ ':' S* expr ]? ')' S*
    if (_maybeEat(TokenKind.LPAREN)) {
      if (_peekIdentifier()) {
        var feature = identifier(); // Media feature.
        var exprs = _maybeEat(TokenKind.COLON)
            ? processExpr()
            : Expressions(_makeSpan(_peekToken.span));
        if (_maybeEat(TokenKind.RPAREN)) {
          return MediaExpression(andOperator, feature, exprs, _makeSpan(start));
        } else if (isChecked) {
          _warning(
              'Missing parenthesis around media expression', _makeSpan(start));
          return null;
        }
      } else if (isChecked) {
        _warning('Missing media feature in media expression', _makeSpan(start));
      }
    }
    return null;
  }

  /// Directive grammar:
  ///
  ///     import:             '@import' [string | URI] media_list?
  ///     media:              '@media' media_query_list '{' ruleset '}'
  ///     page:               '@page' [':' IDENT]? '{' declarations '}'
  ///     stylet:             '@stylet' IDENT '{' ruleset '}'
  ///     media_query_list:   IDENT [',' IDENT]
  ///     keyframes:          '@-webkit-keyframes ...' (see grammar below).
  ///     font_face:          '@font-face' '{' declarations '}'
  ///     namespace:          '@namespace name url("xmlns")
  ///     host:               '@host '{' ruleset '}'
  ///     mixin:              '@mixin name [(args,...)] '{' declarations/ruleset '}'
  ///     include:            '@include name [(@arg,@arg1)]
  ///                         '@include name [(@arg...)]
  ///     content:            '@content'
  ///     -moz-document:      '@-moz-document' [ <url> | url-prefix(<string>) |
  ///                             domain(<string>) | regexp(<string) ]# '{'
  ///                           declarations
  ///                         '}'
  ///     supports:           '@supports' supports_condition group_rule_body
  Directive? processDirective() {
    var start = _peekToken.span;

    var tokId = processVariableOrDirective();
    if (tokId is VarDefinitionDirective) return tokId;
    final tokenId = tokId as int;
    switch (tokenId) {
      case TokenKind.DIRECTIVE_IMPORT:
        _next();

        // @import "uri_string" or @import url("uri_string") are identical; only
        // a url can follow an @import.
        String? importStr;
        if (_peekIdentifier()) {
          var func = processFunction(identifier());
          if (func is UriTerm) {
            importStr = func.text;
          }
        } else {
          importStr = processQuotedString(false);
        }

        // Any medias?
        var medias = processMediaQueryList();

        if (importStr == null) {
          _error('missing import string', _peekToken.span);
        }

        return ImportDirective(importStr!.trim(), medias, _makeSpan(start));

      case TokenKind.DIRECTIVE_MEDIA:
        _next();

        // Any medias?
        var media = processMediaQueryList();

        var rules = <TreeNode>[];
        if (_maybeEat(TokenKind.LBRACE)) {
          while (!_maybeEat(TokenKind.END_OF_FILE)) {
            final rule = processRule();
            if (rule == null) break;
            rules.add(rule);
          }

          if (!_maybeEat(TokenKind.RBRACE)) {
            _error('expected } after ruleset for @media', _peekToken.span);
          }
        } else {
          _error('expected { after media before ruleset', _peekToken.span);
        }
        return MediaDirective(media, rules, _makeSpan(start));

      case TokenKind.DIRECTIVE_HOST:
        _next();

        var rules = <TreeNode>[];
        if (_maybeEat(TokenKind.LBRACE)) {
          while (!_maybeEat(TokenKind.END_OF_FILE)) {
            final rule = processRule();
            if (rule == null) break;
            rules.add(rule);
          }

          if (!_maybeEat(TokenKind.RBRACE)) {
            _error('expected } after ruleset for @host', _peekToken.span);
          }
        } else {
          _error('expected { after host before ruleset', _peekToken.span);
        }
        return HostDirective(rules, _makeSpan(start));

      case TokenKind.DIRECTIVE_PAGE:
        // @page S* IDENT? pseudo_page?
        //      S* '{' S*
        //      [ declaration | margin ]?
        //      [ ';' S* [ declaration | margin ]? ]* '}' S*
        //
        // pseudo_page :
        //      ':' [ "left" | "right" | "first" ]
        //
        // margin :
        //      margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
        //
        // margin_sym : @top-left-corner, @top-left, @bottom-left, etc.
        //
        // See http://www.w3.org/TR/css3-page/#CSS21
        _next();

        // Page name
        Identifier? name;
        if (_peekIdentifier()) {
          name = identifier();
        }

        // Any pseudo page?
        Identifier? pseudoPage;
        if (_maybeEat(TokenKind.COLON)) {
          if (_peekIdentifier()) {
            pseudoPage = identifier();
            // TODO(terry): Normalize pseudoPage to lowercase.
            if (isChecked &&
                !(pseudoPage.name == 'left' ||
                    pseudoPage.name == 'right' ||
                    pseudoPage.name == 'first')) {
              _warning(
                  'Pseudo page must be left, top or first', pseudoPage.span);
              return null;
            }
          }
        }

        var pseudoName = pseudoPage is Identifier ? pseudoPage.name : '';
        var ident = name is Identifier ? name.name : '';
        return PageDirective(
            ident, pseudoName, processMarginsDeclarations(), _makeSpan(start));

      case TokenKind.DIRECTIVE_CHARSET:
        // @charset S* STRING S* ';'
        _next();

        var charEncoding = processQuotedString(false);
        return CharsetDirective(charEncoding, _makeSpan(start));

      // TODO(terry): Workaround Dart2js bug continue not implemented in switch
      //              see https://code.google.com/p/dart/issues/detail?id=8270
      /*
      case TokenKind.DIRECTIVE_MS_KEYFRAMES:
        // TODO(terry): For now only IE 10 (are base level) supports @keyframes,
        // -moz- has only been optional since Oct 2012 release of Firefox, not
        // all versions of webkit support @keyframes and opera doesn't yet
        // support w/o -o- prefix.  Add more warnings for other prefixes when
        // they become optional.
        if (isChecked) {
          _warning('@-ms-keyframes should be @keyframes', _makeSpan(start));
        }
        continue keyframeDirective;

      keyframeDirective:
      */
      case TokenKind.DIRECTIVE_KEYFRAMES:
      case TokenKind.DIRECTIVE_WEB_KIT_KEYFRAMES:
      case TokenKind.DIRECTIVE_MOZ_KEYFRAMES:
      case TokenKind.DIRECTIVE_O_KEYFRAMES:
      // TODO(terry): Remove workaround when bug 8270 is fixed.
      case TokenKind.DIRECTIVE_MS_KEYFRAMES:
        if (tokenId == TokenKind.DIRECTIVE_MS_KEYFRAMES && isChecked) {
          _warning('@-ms-keyframes should be @keyframes', _makeSpan(start));
        }
        // TODO(terry): End of workaround.

        // Key frames grammar:
        //
        //     @[browser]? keyframes [IDENT|STRING] '{' keyframes-blocks '}';
        //
        //     browser: [-webkit-, -moz-, -ms-, -o-]
        //
        //     keyframes-blocks:
        //       [keyframe-selectors '{' declarations '}']* ;
        //
        //     keyframe-selectors:
        //       ['from'|'to'|PERCENTAGE] [',' ['from'|'to'|PERCENTAGE] ]* ;
        _next();

        Identifier? name;
        if (_peekIdentifier()) {
          name = identifier();
        }

        _eat(TokenKind.LBRACE);

        var keyframe = KeyFrameDirective(tokenId, name, _makeSpan(start));

        do {
          var selectors = Expressions(_makeSpan(start));

          do {
            var term = processTerm() as Expression;

            // TODO(terry): Only allow from, to and PERCENTAGE ...

            selectors.add(term);
          } while (_maybeEat(TokenKind.COMMA));

          keyframe.add(KeyFrameBlock(
              selectors, processDeclarations(), _makeSpan(start)));
        } while (!_maybeEat(TokenKind.RBRACE) && !isPrematureEndOfFile());

        return keyframe;

      case TokenKind.DIRECTIVE_FONTFACE:
        _next();
        return FontFaceDirective(processDeclarations(), _makeSpan(start));

      case TokenKind.DIRECTIVE_STYLET:
        // Stylet grammar:
        //
        //     @stylet IDENT '{'
        //       ruleset
        //     '}'
        _next();

        dynamic name;
        if (_peekIdentifier()) {
          name = identifier();
        }

        _eat(TokenKind.LBRACE);

        var productions = <TreeNode>[];

        start = _peekToken.span;
        while (!_maybeEat(TokenKind.END_OF_FILE)) {
          final rule = processRule();
          if (rule == null) {
            break;
          }
          productions.add(rule);
        }

        _eat(TokenKind.RBRACE);

        return StyletDirective(name as String, productions, _makeSpan(start));

      case TokenKind.DIRECTIVE_NAMESPACE:
        // Namespace grammar:
        //
        // @namespace S* [namespace_prefix S*]? [STRING|URI] S* ';' S*
        // namespace_prefix : IDENT
        _next();

        Identifier? prefix;
        if (_peekIdentifier()) {
          prefix = identifier();
        }

        // The namespace URI can be either a quoted string url("uri_string")
        // are identical.
        String? namespaceUri;
        if (_peekIdentifier()) {
          var func = processFunction(identifier());
          if (func is UriTerm) {
            namespaceUri = func.text;
          }
        } else {
          if (prefix != null && prefix.name == 'url') {
            var func = processFunction(prefix);
            if (func is UriTerm) {
              // @namespace url("");
              namespaceUri = func.text;
              prefix = null;
            }
          } else {
            namespaceUri = processQuotedString(false);
          }
        }

        return NamespaceDirective(
            prefix?.name ?? '', namespaceUri, _makeSpan(start));

      case TokenKind.DIRECTIVE_MIXIN:
        return processMixin();

      case TokenKind.DIRECTIVE_INCLUDE:
        return processInclude(_makeSpan(start));
      case TokenKind.DIRECTIVE_CONTENT:
        // TODO(terry): TBD
        _warning('@content not implemented.', _makeSpan(start));
        return null;
      case TokenKind.DIRECTIVE_MOZ_DOCUMENT:
        return processDocumentDirective();
      case TokenKind.DIRECTIVE_SUPPORTS:
        return processSupportsDirective();
      case TokenKind.DIRECTIVE_VIEWPORT:
      case TokenKind.DIRECTIVE_MS_VIEWPORT:
        return processViewportDirective();
    }
    return null;
  }

  /// Parse the mixin beginning token offset [start]. Returns a
  /// [MixinDefinition] node.
  ///
  /// Mixin grammar:
  ///
  ///     @mixin IDENT [(args,...)] '{'
  ///       [ruleset | property | directive]*
  ///     '}'
  MixinDefinition? processMixin() {
    _next();

    var name = identifier();

    var params = <TreeNode>[];
    // Any parameters?
    if (_maybeEat(TokenKind.LPAREN)) {
      var mustHaveParam = false;
      var keepGoing = true;
      while (keepGoing) {
        var varDef = processVariableOrDirective(mixinParameter: true);
        if (varDef is VarDefinitionDirective || varDef is VarDefinition) {
          params.add(varDef as TreeNode);
        } else if (mustHaveParam) {
          _warning('Expecting parameter', _makeSpan(_peekToken.span));
          keepGoing = false;
        }
        if (_maybeEat(TokenKind.COMMA)) {
          mustHaveParam = true;
          continue;
        }
        keepGoing = !_maybeEat(TokenKind.RPAREN);
      }
    }

    _eat(TokenKind.LBRACE);

    var productions = <TreeNode>[];
    MixinDefinition? mixinDirective;

    var start = _peekToken.span;
    while (!_maybeEat(TokenKind.END_OF_FILE)) {
      var directive = processDirective();
      if (directive != null) {
        productions.add(directive);
        continue;
      }

      var declGroup = processDeclarations(checkBrace: false);
      if (declGroup.declarations.any((decl) =>
          decl is Declaration && decl is! IncludeMixinAtDeclaration)) {
        var newDecls = <Declaration>[];
        for (var include in productions) {
          // If declGroup has items that are declarations then we assume
          // this mixin is a declaration mixin not a top-level mixin.
          if (include is IncludeDirective) {
            newDecls.add(IncludeMixinAtDeclaration(include, include.span));
          } else {
            _warning('Error mixing of top-level vs declarations mixins',
                _makeSpan(include.span as FileSpan));
          }
        }
        declGroup.declarations.insertAll(0, newDecls);
        productions = [];
      } else {
        // Declarations are just @includes make it a list of productions
        // not a declaration group (anything else is a ruleset).  Make it a
        // list of productions, not a declaration group.
        for (var decl in declGroup.declarations) {
          productions
              .add(decl is IncludeMixinAtDeclaration ? decl.include : decl);
        }
        declGroup.declarations.clear();
      }

      if (declGroup.declarations.isNotEmpty) {
        if (productions.isEmpty) {
          mixinDirective = MixinDeclarationDirective(
              name.name, params, false, declGroup, _makeSpan(start));
          break;
        } else {
          for (var decl in declGroup.declarations) {
            productions
                .add(decl is IncludeMixinAtDeclaration ? decl.include : decl);
          }
        }
      } else {
        mixinDirective = MixinRulesetDirective(
            name.name, params, false, productions, _makeSpan(start));
        break;
      }
    }

    if (productions.isNotEmpty) {
      mixinDirective = MixinRulesetDirective(
          name.name, params, false, productions, _makeSpan(start));
    }

    _eat(TokenKind.RBRACE);

    return mixinDirective;
  }

  /// Returns a VarDefinitionDirective or VarDefinition if a varaible otherwise
  /// return the token id of a directive or -1 if neither.
  dynamic // VarDefinitionDirective | VarDefinition | int
      processVariableOrDirective({bool mixinParameter = false}) {
    var start = _peekToken.span;

    var tokId = _peek();
    // Handle case for @ directive (where there's a whitespace between the @
    // sign and the directive name.  Technically, it's not valid grammar but
    // a number of CSS tests test for whitespace between @ and name.
    if (tokId == TokenKind.AT) {
      _next();
      tokId = _peek();
      if (_peekIdentifier()) {
        // Is it a directive?
        var directive = _peekToken.text;
        var directiveLen = directive.length;
        tokId = TokenKind.matchDirectives(directive, 0, directiveLen);
        if (tokId == -1) {
          tokId = TokenKind.matchMarginDirectives(directive, 0, directiveLen);
        }
      }

      if (tokId == -1) {
        if (messages.options.lessSupport) {
          // Less compatibility:
          //    @name: value;      =>    var-name: value;       (VarDefinition)
          //    property: @name;   =>    property: var(name);   (VarUsage)
          Identifier? name;
          if (_peekIdentifier()) {
            name = identifier();
          }

          Expressions? exprs;
          if (mixinParameter && _maybeEat(TokenKind.COLON)) {
            exprs = processExpr();
          } else if (!mixinParameter) {
            _eat(TokenKind.COLON);
            exprs = processExpr();
          }

          var span = _makeSpan(start);
          return VarDefinitionDirective(VarDefinition(name, exprs, span), span);
        } else if (isChecked) {
          _error('unexpected directive @$_peekToken', _peekToken.span);
        }
      }
    } else if (mixinParameter && _peekToken.kind == TokenKind.VAR_DEFINITION) {
      _next();
      Identifier? definedName;
      if (_peekIdentifier()) definedName = identifier();

      Expressions? exprs;
      if (_maybeEat(TokenKind.COLON)) {
        exprs = processExpr();
      }

      return VarDefinition(definedName, exprs, _makeSpan(start));
    }

    return tokId;
  }

  IncludeDirective processInclude(SourceSpan span, {bool eatSemiColon = true}) {
    // Stylet grammar:
    //
    //     @include IDENT [(args,...)];
    _next();

    Identifier? name;
    if (_peekIdentifier()) {
      name = identifier();
    }

    var params = <List<Expression>>[];

    // Any parameters?  Parameters can be multiple terms per argument e.g.,
    // 3px solid yellow, green is two parameters:
    //    1. 3px solid yellow
    //    2. green
    // the first has 3 terms and the second has 1 term.
    if (_maybeEat(TokenKind.LPAREN)) {
      var terms = <Expression>[];
      dynamic expr;
      var keepGoing = true;
      while (keepGoing && (expr = processTerm()) != null) {
        // VarUsage is returned as a list
        terms.add((expr is List ? expr[0] : expr) as Expression);
        keepGoing = !_peekKind(TokenKind.RPAREN);
        if (keepGoing) {
          if (_maybeEat(TokenKind.COMMA)) {
            params.add(terms);
            terms = [];
          }
        }
      }
      params.add(terms);
      _maybeEat(TokenKind.RPAREN);
    }

    if (eatSemiColon) {
      _eat(TokenKind.SEMICOLON);
    }

    return IncludeDirective(name!.name, params, span);
  }

  DocumentDirective processDocumentDirective() {
    var start = _peekToken.span;
    _next(); // '@-moz-document'
    var functions = <LiteralTerm>[];
    do {
      LiteralTerm function;

      // Consume function token: IDENT '('
      var ident = identifier();
      _eat(TokenKind.LPAREN);

      // Consume function arguments.
      if (ident.name == 'url-prefix' || ident.name == 'domain') {
        // @-moz-document allows the 'url-prefix' and 'domain' functions to
        // omit quotations around their argument, contrary to the standard
        // in which they must be strings. To support this we consume a
        // string with optional quotation marks, then reapply quotation
        // marks so they're present in the emitted CSS.
        var argumentStart = _peekToken.span;
        var value = processQuotedString(true);
        // Don't quote the argument if it's empty. '@-moz-document url-prefix()'
        // is a common pattern used for browser detection.
        var argument = value.isNotEmpty ? '"$value"' : '';
        var argumentSpan = _makeSpan(argumentStart);

        _eat(TokenKind.RPAREN);

        var arguments = Expressions(_makeSpan(argumentSpan))
          ..add(LiteralTerm(argument, argument, argumentSpan));
        function = FunctionTerm(ident.name, ident.name, arguments,
            _makeSpan(ident.span as FileSpan));
      } else {
        function = processFunction(ident) as LiteralTerm;
      }

      functions.add(function);
    } while (_maybeEat(TokenKind.COMMA));

    _eat(TokenKind.LBRACE);
    var groupRuleBody = processGroupRuleBody();
    _eat(TokenKind.RBRACE);
    return DocumentDirective(functions, groupRuleBody, _makeSpan(start));
  }

  SupportsDirective processSupportsDirective() {
    var start = _peekToken.span;
    _next(); // '@supports'
    var condition = processSupportsCondition();
    _eat(TokenKind.LBRACE);
    var groupRuleBody = processGroupRuleBody();
    _eat(TokenKind.RBRACE);
    return SupportsDirective(condition, groupRuleBody, _makeSpan(start));
  }

  SupportsCondition? processSupportsCondition() {
    if (_peekKind(TokenKind.IDENTIFIER)) {
      return processSupportsNegation();
    }

    var start = _peekToken.span;
    var conditions = <SupportsConditionInParens>[];
    var clauseType = ClauseType.none;

    while (true) {
      conditions.add(processSupportsConditionInParens());

      ClauseType type;
      var text = _peekToken.text.toLowerCase();

      if (text == 'and') {
        type = ClauseType.conjunction;
      } else if (text == 'or') {
        type = ClauseType.disjunction;
      } else {
        break; // Done parsing clause.
      }

      if (clauseType == ClauseType.none) {
        clauseType = type; // First operand and operator of clause.
      } else if (clauseType != type) {
        _error("Operators can't be mixed without a layer of parentheses",
            _peekToken.span);
        break;
      }

      _next(); // Consume operator.
    }

    if (clauseType == ClauseType.conjunction) {
      return SupportsConjunction(conditions, _makeSpan(start));
    } else if (clauseType == ClauseType.disjunction) {
      return SupportsDisjunction(conditions, _makeSpan(start));
    } else {
      return conditions.first;
    }
  }

  SupportsNegation? processSupportsNegation() {
    var start = _peekToken.span;
    var text = _peekToken.text.toLowerCase();
    if (text != 'not') return null;
    _next(); // 'not'
    var condition = processSupportsConditionInParens();
    return SupportsNegation(condition, _makeSpan(start));
  }

  SupportsConditionInParens processSupportsConditionInParens() {
    var start = _peekToken.span;
    _eat(TokenKind.LPAREN);
    // Try to parse a condition.
    var condition = processSupportsCondition();
    if (condition != null) {
      _eat(TokenKind.RPAREN);
      return SupportsConditionInParens.nested(condition, _makeSpan(start));
    }
    // Otherwise, parse a declaration.
    var declaration = processDeclaration([]);
    _eat(TokenKind.RPAREN);
    return SupportsConditionInParens(declaration, _makeSpan(start));
  }

  ViewportDirective processViewportDirective() {
    var start = _peekToken.span;
    var name = _next().text;
    var declarations = processDeclarations();
    return ViewportDirective(name, declarations, _makeSpan(start));
  }

  TreeNode? processRule([SelectorGroup? selectorGroup]) {
    if (selectorGroup == null) {
      final directive = processDirective();
      if (directive != null) {
        _maybeEat(TokenKind.SEMICOLON);
        return directive;
      }
      selectorGroup = processSelectorGroup();
    }
    if (selectorGroup != null) {
      return RuleSet(selectorGroup, processDeclarations(), selectorGroup.span);
    }
    return null;
  }

  List<TreeNode> processGroupRuleBody() {
    var nodes = <TreeNode>[];
    while (!(_peekKind(TokenKind.RBRACE) || _peekKind(TokenKind.END_OF_FILE))) {
      var rule = processRule();
      if (rule != null) {
        nodes.add(rule);
        continue;
      }
      break;
    }
    return nodes;
  }

  /// Look ahead to see if what should be a declaration is really a selector.
  /// If it's a selector than it's a nested selector.  This support's Less'
  /// nested selector syntax (requires a look ahead). E.g.,
  ///
  ///     div {
  ///       width : 20px;
  ///       span {
  ///         color: red;
  ///       }
  ///     }
  ///
  /// Two tag name selectors div and span equivalent to:
  ///
  ///     div {
  ///       width: 20px;
  ///     }
  ///     div span {
  ///       color: red;
  ///     }
  ///
  /// Return `null` if no selector or [SelectorGroup] if a selector was parsed.
  SelectorGroup? _nestedSelector() {
    var oldMessages = messages;
    _createMessages();

    var markedData = _mark;

    // Look a head do we have a nested selector instead of a declaration?
    var selGroup = processSelectorGroup();

    var nestedSelector = selGroup != null &&
        _peekKind(TokenKind.LBRACE) &&
        messages.messages.isEmpty;

    if (!nestedSelector) {
      // Not a selector so restore the world.
      _restore(markedData);
      messages = oldMessages;
      return null;
    } else {
      // Remember any messages from look ahead.
      oldMessages.mergeMessages(messages);
      messages = oldMessages;
      return selGroup;
    }
  }

  DeclarationGroup processDeclarations({bool checkBrace = true}) {
    var start = _peekToken.span;

    if (checkBrace) _eat(TokenKind.LBRACE);

    var decls = <TreeNode>[];
    var dartStyles =
        <DartStyleExpression>[]; // List of latest styles exposed to Dart.

    do {
      var selectorGroup = _nestedSelector();
      while (selectorGroup != null) {
        // Nested selector so process as a ruleset.
        var ruleset = processRule(selectorGroup)!;
        decls.add(ruleset);
        selectorGroup = _nestedSelector();
      }

      var decl = processDeclaration(dartStyles);
      if (decl != null) {
        if (decl.hasDartStyle) {
          var newDartStyle = decl.dartStyle!;

          // Replace or add latest Dart style.
          var replaced = false;
          for (var i = 0; i < dartStyles.length; i++) {
            var dartStyle = dartStyles[i];
            if (dartStyle.isSame(newDartStyle)) {
              dartStyles[i] = newDartStyle;
              replaced = true;
              break;
            }
          }
          if (!replaced) {
            dartStyles.add(newDartStyle);
          }
        }
        decls.add(decl);
      }
    } while (_maybeEat(TokenKind.SEMICOLON));

    if (checkBrace) _eat(TokenKind.RBRACE);

    // Fixup declaration to only have dartStyle that are live for this set of
    // declarations.
    for (var decl in decls) {
      if (decl is Declaration) {
        if (decl.hasDartStyle && !dartStyles.contains(decl.dartStyle)) {
          // Dart style not live, ignore these styles in this Declarations.
          decl.dartStyle = null;
        }
      }
    }

    return DeclarationGroup(decls, _makeSpan(start));
  }

  List<DeclarationGroup> processMarginsDeclarations() {
    var groups = <DeclarationGroup>[];

    var start = _peekToken.span;

    _eat(TokenKind.LBRACE);

    var decls = <Declaration>[];
    var dartStyles =
        <DartStyleExpression>[]; // List of latest styles exposed to Dart.

    do {
      switch (_peek()) {
        case TokenKind.MARGIN_DIRECTIVE_TOPLEFTCORNER:
        case TokenKind.MARGIN_DIRECTIVE_TOPLEFT:
        case TokenKind.MARGIN_DIRECTIVE_TOPCENTER:
        case TokenKind.MARGIN_DIRECTIVE_TOPRIGHT:
        case TokenKind.MARGIN_DIRECTIVE_TOPRIGHTCORNER:
        case TokenKind.MARGIN_DIRECTIVE_BOTTOMLEFTCORNER:
        case TokenKind.MARGIN_DIRECTIVE_BOTTOMLEFT:
        case TokenKind.MARGIN_DIRECTIVE_BOTTOMCENTER:
        case TokenKind.MARGIN_DIRECTIVE_BOTTOMRIGHT:
        case TokenKind.MARGIN_DIRECTIVE_BOTTOMRIGHTCORNER:
        case TokenKind.MARGIN_DIRECTIVE_LEFTTOP:
        case TokenKind.MARGIN_DIRECTIVE_LEFTMIDDLE:
        case TokenKind.MARGIN_DIRECTIVE_LEFTBOTTOM:
        case TokenKind.MARGIN_DIRECTIVE_RIGHTTOP:
        case TokenKind.MARGIN_DIRECTIVE_RIGHTMIDDLE:
        case TokenKind.MARGIN_DIRECTIVE_RIGHTBOTTOM:
          // Margin syms processed.
          //   margin :
          //      margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
          //
          //      margin_sym : @top-left-corner, @top-left, @bottom-left, etc.
          var marginSym = _peek();

          _next();

          var declGroup = processDeclarations();
          groups.add(
              MarginGroup(marginSym, declGroup.declarations, _makeSpan(start)));
          break;
        default:
          var decl = processDeclaration(dartStyles);
          if (decl != null) {
            if (decl.hasDartStyle) {
              var newDartStyle = decl.dartStyle!;

              // Replace or add latest Dart style.
              var replaced = false;
              for (var i = 0; i < dartStyles.length; i++) {
                var dartStyle = dartStyles[i];
                if (dartStyle.isSame(newDartStyle)) {
                  dartStyles[i] = newDartStyle;
                  replaced = true;
                  break;
                }
              }
              if (!replaced) {
                dartStyles.add(newDartStyle);
              }
            }
            decls.add(decl);
          }
          _maybeEat(TokenKind.SEMICOLON);
          break;
      }
    } while (!_maybeEat(TokenKind.RBRACE) && !isPrematureEndOfFile());

    // Fixup declaration to only have dartStyle that are live for this set of
    // declarations.
    for (var decl in decls) {
      if (decl.hasDartStyle && !dartStyles.contains(decl.dartStyle)) {
        // Dart style not live, ignore these styles in this Declarations.
        decl.dartStyle = null;
      }
    }

    if (decls.isNotEmpty) {
      groups.add(DeclarationGroup(decls, _makeSpan(start)));
    }

    return groups;
  }

  SelectorGroup? processSelectorGroup() {
    var selectors = <Selector>[];
    var start = _peekToken.span;

    tokenizer.inSelector = true;
    do {
      var selector = processSelector();
      if (selector != null) {
        selectors.add(selector);
      }
    } while (_maybeEat(TokenKind.COMMA));
    tokenizer.inSelector = false;

    if (selectors.isNotEmpty) {
      return SelectorGroup(selectors, _makeSpan(start));
    }
    return null;
  }

  /// Return list of selectors
  Selector? processSelector() {
    var simpleSequences = <SimpleSelectorSequence>[];
    var start = _peekToken.span;
    while (true) {
      // First item is never descendant make sure it's COMBINATOR_NONE.
      var selectorItem = simpleSelectorSequence(simpleSequences.isEmpty);
      if (selectorItem != null) {
        simpleSequences.add(selectorItem);
      } else {
        break;
      }
    }

    if (simpleSequences.isEmpty) return null;

    return Selector(simpleSequences, _makeSpan(start));
  }

  /// Same as [processSelector] but reports an error for each combinator.
  ///
  /// This is a quick fix for parsing `<compound-selectors>` until the parser
  /// supports Selector Level 4 grammar:
  /// https://drafts.csswg.org/selectors-4/#typedef-compound-selector
  Selector? processCompoundSelector() {
    var selector = processSelector();
    if (selector != null) {
      for (var sequence in selector.simpleSelectorSequences) {
        if (!sequence.isCombinatorNone) {
          _error('compound selector can not contain combinator', sequence.span);
        }
      }
    }
    return selector;
  }

  SimpleSelectorSequence? simpleSelectorSequence(bool forceCombinatorNone) {
    var start = _peekToken.span;
    var combinatorType = TokenKind.COMBINATOR_NONE;
    var thisOperator = false;

    switch (_peek()) {
      case TokenKind.PLUS:
        _eat(TokenKind.PLUS);
        combinatorType = TokenKind.COMBINATOR_PLUS;
        break;
      case TokenKind.GREATER:
        _eat(TokenKind.GREATER);
        combinatorType = TokenKind.COMBINATOR_GREATER;
        break;
      case TokenKind.TILDE:
        _eat(TokenKind.TILDE);
        combinatorType = TokenKind.COMBINATOR_TILDE;
        break;
      case TokenKind.AMPERSAND:
        _eat(TokenKind.AMPERSAND);
        thisOperator = true;
        break;
    }

    // Check if WHITESPACE existed between tokens if so we're descendent.
    if (combinatorType == TokenKind.COMBINATOR_NONE && !forceCombinatorNone) {
      if (_previousToken != null && _previousToken!.end != _peekToken.start) {
        combinatorType = TokenKind.COMBINATOR_DESCENDANT;
      }
    }

    var span = _makeSpan(start);
    var simpleSel = thisOperator
        ? ElementSelector(ThisOperator(span), span)
        : simpleSelector();
    if (simpleSel == null &&
        (combinatorType == TokenKind.COMBINATOR_PLUS ||
            combinatorType == TokenKind.COMBINATOR_GREATER ||
            combinatorType == TokenKind.COMBINATOR_TILDE)) {
      // For "+ &", "~ &" or "> &" a selector sequence with no name is needed
      // so that the & will have a combinator too.  This is needed to
      // disambiguate selector expressions:
      //    .foo&:hover     combinator before & is NONE
      //    .foo &          combinator before & is DESCDENDANT
      //    .foo > &        combinator before & is GREATER
      simpleSel = ElementSelector(Identifier('', span), span);
    }
    if (simpleSel != null) {
      return SimpleSelectorSequence(simpleSel, span, combinatorType);
    }
    return null;
  }

  /// Simple selector grammar:
  ///
  ///     simple_selector_sequence
  ///        : [ type_selector | universal ]
  ///          [ HASH | class | attrib | pseudo | negation ]*
  ///        | [ HASH | class | attrib | pseudo | negation ]+
  ///     type_selector
  ///        : [ namespace_prefix ]? element_name
  ///     namespace_prefix
  ///        : [ IDENT | '*' ]? '|'
  ///     element_name
  ///        : IDENT
  ///     universal
  ///        : [ namespace_prefix ]? '*'
  ///     class
  ///        : '.' IDENT
  SimpleSelector? simpleSelector() {
    // TODO(terry): Natalie makes a good point parsing of namespace and element
    //              are essentially the same (asterisk or identifier) other
    //              than the error message for element.  Should consolidate the
    //              code.
    // TODO(terry): Need to handle attribute namespace too.
    dynamic first;
    var start = _peekToken.span;
    switch (_peek()) {
      case TokenKind.ASTERISK:
        // Mark as universal namespace.
        var tok = _next();
        first = Wildcard(_makeSpan(tok.span));
        break;
      case TokenKind.IDENTIFIER:
        first = identifier();
        break;
      default:
        // Expecting simple selector.
        // TODO(terry): Could be a synthesized token like value, etc.
        if (TokenKind.isKindIdentifier(_peek())) {
          first = identifier();
        } else if (_peekKind(TokenKind.SEMICOLON)) {
          // Can't be a selector if we found a semi-colon.
          return null;
        }
        break;
    }

    if (_maybeEat(TokenKind.NAMESPACE)) {
      TreeNode? element;
      switch (_peek()) {
        case TokenKind.ASTERISK:
          // Mark as universal element
          var tok = _next();
          element = Wildcard(_makeSpan(tok.span));
          break;
        case TokenKind.IDENTIFIER:
          element = identifier();
          break;
        default:
          _error('expected element name or universal(*), but found $_peekToken',
              _peekToken.span);
          break;
      }

      return NamespaceSelector(
          first, ElementSelector(element, element!.span!), _makeSpan(start));
    } else if (first != null) {
      return ElementSelector(first, _makeSpan(start));
    } else {
      // Check for HASH | class | attrib | pseudo | negation
      return simpleSelectorTail();
    }
  }

  bool _anyWhiteSpaceBeforePeekToken(int kind) {
    if (_previousToken != null && _previousToken!.kind == kind) {
      // If end of previous token isn't same as the start of peek token then
      // there's something between these tokens probably whitespace.
      return _previousToken!.end != _peekToken.start;
    }

    return false;
  }

  /// type_selector | universal | HASH | class | attrib | pseudo
  SimpleSelector? simpleSelectorTail() {
    // Check for HASH | class | attrib | pseudo | negation
    var start = _peekToken.span;
    switch (_peek()) {
      case TokenKind.HASH:
        _eat(TokenKind.HASH);

        if (_anyWhiteSpaceBeforePeekToken(TokenKind.HASH)) {
          _error('Not a valid ID selector expected #id', _makeSpan(start));
          return null;
        }
        return IdSelector(identifier(), _makeSpan(start));
      case TokenKind.DOT:
        _eat(TokenKind.DOT);

        if (_anyWhiteSpaceBeforePeekToken(TokenKind.DOT)) {
          _error('Not a valid class selector expected .className',
              _makeSpan(start));
          return null;
        }
        return ClassSelector(identifier(), _makeSpan(start));
      case TokenKind.COLON:
        // :pseudo-class ::pseudo-element
        return processPseudoSelector(start);
      case TokenKind.LBRACK:
        return processAttribute();
      case TokenKind.DOUBLE:
        _error('name must start with a alpha character, but found a number',
            _peekToken.span);
        _next();
        break;
    }
    return null;
  }

  SimpleSelector? processPseudoSelector(FileSpan start) {
    // :pseudo-class ::pseudo-element
    // TODO(terry): '::' should be token.
    _eat(TokenKind.COLON);
    var pseudoElement = _maybeEat(TokenKind.COLON);

    // TODO(terry): If no identifier specified consider optimizing out the
    //              : or :: and making this a normal selector.  For now,
    //              create an empty pseudoName.
    Identifier pseudoName;
    if (_peekIdentifier()) {
      pseudoName = identifier();
    } else {
      return null;
    }
    var name = pseudoName.name.toLowerCase();

    // Functional pseudo?
    if (_peekToken.kind == TokenKind.LPAREN) {
      if (!pseudoElement && name == 'not') {
        _eat(TokenKind.LPAREN);

        // Negation :   ':NOT(' S* negation_arg S* ')'
        var negArg = simpleSelector();

        _eat(TokenKind.RPAREN);
        return NegationSelector(negArg, _makeSpan(start));
      } else if (!pseudoElement &&
          (name == 'host' ||
              name == 'host-context' ||
              name == 'global-context' ||
              name == '-acx-global-context')) {
        _eat(TokenKind.LPAREN);
        var selector = processCompoundSelector();
        if (selector == null) {
          _errorExpected('a selector argument');
          return null;
        }
        _eat(TokenKind.RPAREN);
        var span = _makeSpan(start);
        return PseudoClassFunctionSelector(pseudoName, selector, span);
      } else {
        // Special parsing for expressions in pseudo functions.  Minus is used
        // as operator not identifier.
        // TODO(jmesserly): we need to flip this before we eat the "(" as the
        // next token will be fetched when we do that. I think we should try to
        // refactor so we don't need this boolean; it seems fragile.
        tokenizer.inSelectorExpression = true;
        _eat(TokenKind.LPAREN);

        // Handle function expression.
        var span = _makeSpan(start);
        var expr = processSelectorExpression();

        tokenizer.inSelectorExpression = false;

        // Used during selector look-a-head if not a SelectorExpression is
        // bad.
        if (expr is SelectorExpression) {
          _eat(TokenKind.RPAREN);
          return pseudoElement
              ? PseudoElementFunctionSelector(pseudoName, expr, span)
              : PseudoClassFunctionSelector(pseudoName, expr, span);
        } else {
          _errorExpected('CSS expression');
          return null;
        }
      }
    }

    // Treat CSS2.1 pseudo-elements defined with pseudo class syntax as pseudo-
    // elements for backwards compatibility.
    return pseudoElement || _legacyPseudoElements.contains(name)
        ? PseudoElementSelector(pseudoName, _makeSpan(start),
            isLegacy: !pseudoElement)
        : PseudoClassSelector(pseudoName, _makeSpan(start));
  }

  /// In CSS3, the expressions are identifiers, strings, or of the form "an+b".
  ///
  ///     : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
  ///
  ///     num               [0-9]+|[0-9]*\.[0-9]+
  ///     PLUS              '+'
  ///     DIMENSION         {num}{ident}
  ///     NUMBER            {num}
  TreeNode /* SelectorExpression | LiteralTerm */ processSelectorExpression() {
    var start = _peekToken.span;

    var expressions = <Expression>[];

    Token? termToken;
    dynamic value;

    var keepParsing = true;
    while (keepParsing) {
      switch (_peek()) {
        case TokenKind.PLUS:
          start = _peekToken.span;
          termToken = _next();
          expressions.add(OperatorPlus(_makeSpan(start)));
          break;
        case TokenKind.MINUS:
          start = _peekToken.span;
          termToken = _next();
          expressions.add(OperatorMinus(_makeSpan(start)));
          break;
        case TokenKind.INTEGER:
          termToken = _next();
          value = int.parse(termToken.text);
          break;
        case TokenKind.DOUBLE:
          termToken = _next();
          value = double.parse(termToken.text);
          break;
        case TokenKind.SINGLE_QUOTE:
          value = processQuotedString(false);
          value = "'${_escapeString(value as String, single: true)}'";
          return LiteralTerm(value, value, _makeSpan(start));
        case TokenKind.DOUBLE_QUOTE:
          value = processQuotedString(false);
          value = '"${_escapeString(value as String)}"';
          return LiteralTerm(value, value, _makeSpan(start));
        case TokenKind.IDENTIFIER:
          value = identifier(); // Snarf up the ident we'll remap, maybe.
          break;
        default:
          keepParsing = false;
      }

      if (keepParsing && value != null) {
        var unitTerm =
            processDimension(termToken, value as Object, _makeSpan(start));
        expressions.add(unitTerm);

        value = null;
      }
    }

    return SelectorExpression(expressions, _makeSpan(start));
  }

  // Attribute grammar:
  //
  // attributes :
  //   '[' S* IDENT S* [ ATTRIB_MATCHES S* [ IDENT | STRING ] S* ]? ']'
  //
  // ATTRIB_MATCHES :
  //   [ '=' | INCLUDES | DASHMATCH | PREFIXMATCH | SUFFIXMATCH | SUBSTRMATCH ]
  //
  // INCLUDES:         '~='
  //
  // DASHMATCH:        '|='
  //
  // PREFIXMATCH:      '^='
  //
  // SUFFIXMATCH:      '$='
  //
  // SUBSTRMATCH:      '*='
  AttributeSelector? processAttribute() {
    var start = _peekToken.span;

    if (_maybeEat(TokenKind.LBRACK)) {
      var attrName = identifier();

      int op;
      switch (_peek()) {
        case TokenKind.EQUALS:
        case TokenKind.INCLUDES: // ~=
        case TokenKind.DASH_MATCH: // |=
        case TokenKind.PREFIX_MATCH: // ^=
        case TokenKind.SUFFIX_MATCH: // $=
        case TokenKind.SUBSTRING_MATCH: // *=
          op = _peek();
          _next();
          break;
        default:
          op = TokenKind.NO_MATCH;
      }

      dynamic value;
      if (op != TokenKind.NO_MATCH) {
        // Operator hit so we require a value too.
        if (_peekIdentifier()) {
          value = identifier();
        } else {
          value = processQuotedString(false);
        }

        if (value == null) {
          _error('expected attribute value string or ident', _peekToken.span);
        }
      }

      _eat(TokenKind.RBRACK);

      return AttributeSelector(attrName, op, value, _makeSpan(start));
    }
    return null;
  }

  //  Declaration grammar:
  //
  //  declaration:  property ':' expr prio?
  //
  //  property:  IDENT [or IE hacks]
  //  prio:      !important
  //  expr:      (see processExpr)
  //
  // Here are the ugly IE hacks we need to support:
  //   property: expr prio? \9; - IE8 and below property, /9 before semi-colon
  //   *IDENT                   - IE7 or below
  //   _IDENT                   - IE6 property (automatically a valid ident)
  Declaration? processDeclaration(List<DartStyleExpression> dartStyles) {
    Declaration? decl;

    var start = _peekToken.span;

    // IE7 hack of * before property name if so the property is IE7 or below.
    var ie7 = _peekKind(TokenKind.ASTERISK);
    if (ie7) {
      _next();
    }

    // IDENT ':' expr '!important'?
    if (TokenKind.isIdentifier(_peekToken.kind)) {
      var propertyIdent = identifier();

      var ieFilterProperty = propertyIdent.name.toLowerCase() == 'filter';

      _eat(TokenKind.COLON);

      var exprs = processExpr(ieFilterProperty);

      var dartComposite = _styleForDart(propertyIdent, exprs, dartStyles);

      // Handle !important (prio)
      var importantPriority = _maybeEat(TokenKind.IMPORTANT);

      decl = Declaration(propertyIdent, exprs, dartComposite, _makeSpan(start),
          important: importantPriority, ie7: ie7);
    } else if (_peekToken.kind == TokenKind.VAR_DEFINITION) {
      _next();
      Identifier? definedName;
      if (_peekIdentifier()) definedName = identifier();

      _eat(TokenKind.COLON);

      var exprs = processExpr();

      decl = VarDefinition(definedName, exprs, _makeSpan(start));
    } else if (_peekToken.kind == TokenKind.DIRECTIVE_INCLUDE) {
      // @include mixinName in the declaration area.
      var span = _makeSpan(start);
      var include = processInclude(span, eatSemiColon: false);
      decl = IncludeMixinAtDeclaration(include, span);
    } else if (_peekToken.kind == TokenKind.DIRECTIVE_EXTEND) {
      var simpleSequences = <TreeNode>[];

      _next();
      var span = _makeSpan(start);
      var selector = simpleSelector();
      if (selector == null) {
        _warning('@extends expecting simple selector name', span);
      } else {
        simpleSequences.add(selector);
      }
      if (_peekKind(TokenKind.COLON)) {
        var pseudoSelector = processPseudoSelector(_peekToken.span);
        if (pseudoSelector is PseudoElementSelector ||
            pseudoSelector is PseudoClassSelector) {
          simpleSequences.add(pseudoSelector!);
        } else {
          _warning('not a valid selector', span);
        }
      }
      decl = ExtendDeclaration(simpleSequences, span);
    }

    return decl;
  }

  /// List of styles exposed to the Dart UI framework.
  static const int _fontPartFont = 0;
  static const int _fontPartVariant = 1;
  static const int _fontPartWeight = 2;
  static const int _fontPartSize = 3;
  static const int _fontPartFamily = 4;
  static const int _fontPartStyle = 5;
  static const int _marginPartMargin = 6;
  static const int _marginPartLeft = 7;
  static const int _marginPartTop = 8;
  static const int _marginPartRight = 9;
  static const int _marginPartBottom = 10;
  static const int _lineHeightPart = 11;
  static const int _borderPartBorder = 12;
  static const int _borderPartLeft = 13;
  static const int _borderPartTop = 14;
  static const int _borderPartRight = 15;
  static const int _borderPartBottom = 16;
  static const int _borderPartWidth = 17;
  static const int _borderPartLeftWidth = 18;
  static const int _borderPartTopWidth = 19;
  static const int _borderPartRightWidth = 20;
  static const int _borderPartBottomWidth = 21;
  static const int _heightPart = 22;
  static const int _widthPart = 23;
  static const int _paddingPartPadding = 24;
  static const int _paddingPartLeft = 25;
  static const int _paddingPartTop = 26;
  static const int _paddingPartRight = 27;
  static const int _paddingPartBottom = 28;

  static const Map<String, int> _stylesToDart = {
    'font': _fontPartFont,
    'font-family': _fontPartFamily,
    'font-size': _fontPartSize,
    'font-style': _fontPartStyle,
    'font-variant': _fontPartVariant,
    'font-weight': _fontPartWeight,
    'line-height': _lineHeightPart,
    'margin': _marginPartMargin,
    'margin-left': _marginPartLeft,
    'margin-right': _marginPartRight,
    'margin-top': _marginPartTop,
    'margin-bottom': _marginPartBottom,
    'border': _borderPartBorder,
    'border-left': _borderPartLeft,
    'border-right': _borderPartRight,
    'border-top': _borderPartTop,
    'border-bottom': _borderPartBottom,
    'border-width': _borderPartWidth,
    'border-left-width': _borderPartLeftWidth,
    'border-top-width': _borderPartTopWidth,
    'border-right-width': _borderPartRightWidth,
    'border-bottom-width': _borderPartBottomWidth,
    'height': _heightPart,
    'width': _widthPart,
    'padding': _paddingPartPadding,
    'padding-left': _paddingPartLeft,
    'padding-top': _paddingPartTop,
    'padding-right': _paddingPartRight,
    'padding-bottom': _paddingPartBottom
  };

  static const Map<String, int> _nameToFontWeight = {
    'bold': FontWeight.bold,
    'normal': FontWeight.normal
  };

  static int? _findStyle(String styleName) => _stylesToDart[styleName];

  DartStyleExpression? _styleForDart(Identifier property, Expressions exprs,
      List<DartStyleExpression> dartStyles) {
    var styleType = _findStyle(property.name.toLowerCase());
    if (styleType != null) {
      return buildDartStyleNode(styleType, exprs, dartStyles);
    }
    return null;
  }

  FontExpression _mergeFontStyles(
      FontExpression fontExpr, List<DartStyleExpression> dartStyles) {
    // Merge all font styles for this class selector.
    for (var dartStyle in dartStyles) {
      if (dartStyle.isFont) {
        fontExpr = FontExpression.merge(dartStyle as FontExpression, fontExpr);
      }
    }

    return fontExpr;
  }

  DartStyleExpression? buildDartStyleNode(
      int styleType, Expressions exprs, List<DartStyleExpression> dartStyles) {
    switch (styleType) {
      // Properties in order:
      //
      //     font-style font-variant font-weight font-size/line-height font-family
      //
      // The font-size and font-family values are required. If other values are
      // missing; a default, if it exist, will be used.
      case _fontPartFont:
        var processor = ExpressionsProcessor(exprs);
        return _mergeFontStyles(processor.processFont(), dartStyles);
      case _fontPartFamily:
        var processor = ExpressionsProcessor(exprs);

        try {
          return _mergeFontStyles(processor.processFontFamily(), dartStyles);
        } catch (fontException) {
          _error('$fontException', _peekToken.span);
        }
        break;
      case _fontPartSize:
        var processor = ExpressionsProcessor(exprs);
        return _mergeFontStyles(processor.processFontSize(), dartStyles);
      case _fontPartStyle:
        // Possible style values:
        //   normal [default]
        //   italic
        //   oblique
        //   inherit

        // TODO(terry): TBD
        break;
      case _fontPartVariant:
        // Possible variant values:
        //   normal  [default]
        //   small-caps
        //   inherit

        // TODO(terry): TBD
        break;
      case _fontPartWeight:
        // Possible weight values:
        //   normal [default]
        //   bold
        //   bolder
        //   lighter
        //   100 - 900
        //   inherit

        // TODO(terry): Only 'normal', 'bold', or values of 100-900 supported
        //              need to handle bolder, lighter, and inherit.  See
        //              https://github.com/dart-lang/csslib/issues/1
        var expr = exprs.expressions[0];
        if (expr is NumberTerm) {
          var fontExpr = FontExpression(expr.span, weight: expr.value as int?);
          return _mergeFontStyles(fontExpr, dartStyles);
        } else if (expr is LiteralTerm) {
          var weight = _nameToFontWeight[expr.value.toString()];
          if (weight != null) {
            var fontExpr = FontExpression(expr.span, weight: weight);
            return _mergeFontStyles(fontExpr, dartStyles);
          }
        }
        break;
      case _lineHeightPart:
        if (exprs.expressions.length == 1) {
          var expr = exprs.expressions[0];
          if (expr is UnitTerm) {
            var unitTerm = expr;
            // TODO(terry): Need to handle other units and LiteralTerm normal
            //              See https://github.com/dart-lang/csslib/issues/2.
            if (unitTerm.unit == TokenKind.UNIT_LENGTH_PX ||
                unitTerm.unit == TokenKind.UNIT_LENGTH_PT) {
              var fontExpr = FontExpression(expr.span,
                  lineHeight: LineHeight(expr.value as num, inPixels: true));
              return _mergeFontStyles(fontExpr, dartStyles);
            } else if (isChecked) {
              _warning('Unexpected unit for line-height', expr.span);
            }
          } else if (expr is NumberTerm) {
            var fontExpr = FontExpression(expr.span,
                lineHeight: LineHeight(expr.value as num, inPixels: false));
            return _mergeFontStyles(fontExpr, dartStyles);
          } else if (isChecked) {
            _warning('Unexpected value for line-height', expr.span);
          }
        }
        break;
      case _marginPartMargin:
        return MarginExpression.boxEdge(exprs.span, processFourNums(exprs));
      case _borderPartBorder:
        for (var expr in exprs.expressions) {
          var v = marginValue(expr);
          if (v != null) {
            final box = BoxEdge.uniform(v);
            return BorderExpression.boxEdge(exprs.span, box);
          }
        }
        break;
      case _borderPartWidth:
        var v = marginValue(exprs.expressions[0]);
        if (v != null) {
          final box = BoxEdge.uniform(v);
          return BorderExpression.boxEdge(exprs.span, box);
        }
        break;
      case _paddingPartPadding:
        return PaddingExpression.boxEdge(exprs.span, processFourNums(exprs));
      case _marginPartLeft:
      case _marginPartTop:
      case _marginPartRight:
      case _marginPartBottom:
      case _borderPartLeft:
      case _borderPartTop:
      case _borderPartRight:
      case _borderPartBottom:
      case _borderPartLeftWidth:
      case _borderPartTopWidth:
      case _borderPartRightWidth:
      case _borderPartBottomWidth:
      case _heightPart:
      case _widthPart:
      case _paddingPartLeft:
      case _paddingPartTop:
      case _paddingPartRight:
      case _paddingPartBottom:
        if (exprs.expressions.isNotEmpty) {
          return processOneNumber(exprs, styleType);
        }
        break;
    }
    return null;
  }

  // TODO(terry): Look at handling width of thin, thick, etc. any none numbers
  //              to convert to a number.
  DartStyleExpression? processOneNumber(Expressions exprs, int part) {
    var value = marginValue(exprs.expressions[0]);
    if (value != null) {
      return switch (part) {
        _marginPartLeft => MarginExpression(exprs.span, left: value),
        _marginPartTop => MarginExpression(exprs.span, top: value),
        _marginPartRight => MarginExpression(exprs.span, right: value),
        _marginPartBottom => MarginExpression(exprs.span, bottom: value),
        _borderPartLeft ||
        _borderPartLeftWidth =>
          BorderExpression(exprs.span, left: value),
        _borderPartTop ||
        _borderPartTopWidth =>
          BorderExpression(exprs.span, top: value),
        _borderPartRight ||
        _borderPartRightWidth =>
          BorderExpression(exprs.span, right: value),
        _borderPartBottom ||
        _borderPartBottomWidth =>
          BorderExpression(exprs.span, bottom: value),
        _heightPart => HeightExpression(exprs.span, value),
        _widthPart => WidthExpression(exprs.span, value),
        _paddingPartLeft => PaddingExpression(exprs.span, left: value),
        _paddingPartTop => PaddingExpression(exprs.span, top: value),
        _paddingPartRight => PaddingExpression(exprs.span, right: value),
        _paddingPartBottom => PaddingExpression(exprs.span, bottom: value),
        _ => null
      };
    }
    return null;
  }

  /// Margins are of the format:
  ///
  /// * top,right,bottom,left      (4 parameters)
  /// * top,right/left, bottom     (3 parameters)
  /// * top/bottom,right/left      (2 parameters)
  /// * top/right/bottom/left      (1 parameter)
  ///
  /// The values of the margins can be a unit or unitless or auto.
  BoxEdge? processFourNums(Expressions exprs) {
    num? top;
    num? right;
    num? bottom;
    num? left;

    var totalExprs = exprs.expressions.length;
    switch (totalExprs) {
      case 1:
        top = marginValue(exprs.expressions[0]);
        right = top;
        bottom = top;
        left = top;
        break;
      case 2:
        top = marginValue(exprs.expressions[0]);
        bottom = top;
        right = marginValue(exprs.expressions[1]);
        left = right;
        break;
      case 3:
        top = marginValue(exprs.expressions[0]);
        right = marginValue(exprs.expressions[1]);
        left = right;
        bottom = marginValue(exprs.expressions[2]);
        break;
      case 4:
        top = marginValue(exprs.expressions[0]);
        right = marginValue(exprs.expressions[1]);
        bottom = marginValue(exprs.expressions[2]);
        left = marginValue(exprs.expressions[3]);
        break;
      default:
        return null;
    }

    return BoxEdge.clockwiseFromTop(top, right, bottom, left);
  }

  // TODO(terry): Need to handle auto.
  num? marginValue(Expression exprTerm) {
    if (exprTerm is UnitTerm) {
      return exprTerm.value as num;
    } else if (exprTerm is NumberTerm) {
      return exprTerm.value as num;
    }
    return null;
  }

  //  Expression grammar:
  //
  //  expression:   term [ operator? term]*
  //
  //  operator:     '/' | ','
  //  term:         (see processTerm)
  Expressions processExpr([bool ieFilter = false]) {
    var start = _peekToken.span;
    var expressions = Expressions(_makeSpan(start));

    var keepGoing = true;
    dynamic expr;
    while (keepGoing && (expr = processTerm(ieFilter)) != null) {
      Expression? op;

      var opStart = _peekToken.span;

      switch (_peek()) {
        case TokenKind.SLASH:
          op = OperatorSlash(_makeSpan(opStart));
          break;
        case TokenKind.COMMA:
          op = OperatorComma(_makeSpan(opStart));
          break;
        case TokenKind.BACKSLASH:
          // Backslash outside of string; detected IE8 or older signaled by \9
          // at end of an expression.
          var ie8Start = _peekToken.span;

          _next();
          if (_peekKind(TokenKind.INTEGER)) {
            var numToken = _next();
            var value = int.parse(numToken.text);
            if (value == 9) {
              op = IE8Term(_makeSpan(ie8Start));
            } else if (isChecked) {
              _warning(
                  '\$value is not valid in an expression', _makeSpan(start));
            }
          }
          break;
      }

      if (expr != null) {
        if (expr is List<Expression>) {
          for (var exprItem in expr) {
            expressions.add(exprItem);
          }
        } else {
          expressions.add(expr as Expression);
        }
      } else {
        keepGoing = false;
      }

      if (op != null) {
        expressions.add(op);
        if (op is IE8Term) {
          keepGoing = false;
        } else {
          _next();
        }
      }
    }

    return expressions;
  }

  // ignore: constant_identifier_names
  static const int MAX_UNICODE = 0x10FFFF;

  // Term grammar:
  //
  //     term:
  //       unary_operator?
  //       [ term_value ]
  //       | STRING S* | IDENT S* | URI S* | UNICODERANGE S* | hexcolor
  //
  //     term_value:
  //       NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | ANGLE S* |
  //       TIME S* | FREQ S* | function
  //
  //     NUMBER:       {num}
  //     PERCENTAGE:   {num}%
  //     LENGTH:       {num}['px' | 'cm' | 'mm' | 'in' | 'pt' | 'pc']
  //     EMS:          {num}'em'
  //     EXS:          {num}'ex'
  //     ANGLE:        {num}['deg' | 'rad' | 'grad']
  //     TIME:         {num}['ms' | 's']
  //     FREQ:         {num}['hz' | 'khz']
  //     function:     IDENT '(' expr ')'
  //
  dynamic /* Expression | List<Expression> | ... */ processTerm(
      [bool ieFilter = false]) {
    var start = _peekToken.span;
    var unary = '';

    dynamic processIdentifier() {
      var nameValue = identifier(); // Snarf up the ident we'll remap, maybe.

      if (!ieFilter && _maybeEat(TokenKind.LPAREN)) {
        var calc = processCalc(nameValue);
        if (calc != null) return calc;
        // FUNCTION
        return processFunction(nameValue);
      }
      if (ieFilter) {
        if (_maybeEat(TokenKind.COLON) &&
            nameValue.name.toLowerCase() == 'progid') {
          // IE filter:progid:
          return processIEFilter(start);
        } else {
          // Handle filter:<name> where name is any filter e.g., alpha,
          // chroma, Wave, blur, etc.
          return processIEFilter(start);
        }
      }

      // TODO(terry): Need to have a list of known identifiers today only
      //              'from' is special.
      if (nameValue.name == 'from') {
        return LiteralTerm(nameValue, nameValue.name, _makeSpan(start));
      }

      // What kind of identifier is it, named color?
      var colorEntry = TokenKind.matchColorName(nameValue.name);
      if (colorEntry == null) {
        if (isChecked) {
          var propName = nameValue.name;
          var errMsg = TokenKind.isPredefinedName(propName)
              ? 'Improper use of property value $propName'
              : 'Unknown property value $propName';
          _warning(errMsg, _makeSpan(start));
        }
        return LiteralTerm(nameValue, nameValue.name, _makeSpan(start));
      }

      // Yes, process the color as an RGB value.
      var rgbColor =
          TokenKind.decimalToHex(TokenKind.colorValue(colorEntry), 6);
      return _parseHex(rgbColor, _makeSpan(start));
    }

    switch (_peek()) {
      case TokenKind.HASH:
        _eat(TokenKind.HASH);
        if (!_anyWhiteSpaceBeforePeekToken(TokenKind.HASH)) {
          String? hexText;
          if (_peekKind(TokenKind.INTEGER)) {
            var hexText1 = _peekToken.text;
            _next();
            // Append identifier only if there's no delimiting whitespace.
            if (_peekIdentifier() && _previousToken!.end == _peekToken.start) {
              hexText = '$hexText1${identifier().name}';
            } else {
              hexText = hexText1;
            }
          } else if (_peekIdentifier()) {
            hexText = identifier().name;
          }
          if (hexText != null) {
            return _parseHex(hexText, _makeSpan(start));
          }
        }

        if (isChecked) {
          _warning('Expected hex number', _makeSpan(start));
        }
        // Construct the bad hex value with a #<space>number.
        return _parseHex(
            ' ${(processTerm() as LiteralTerm).text}', _makeSpan(start));
      case TokenKind.INTEGER:
        var t = _next();
        var value = int.parse('$unary${t.text}');
        return processDimension(t, value, _makeSpan(start));
      case TokenKind.DOUBLE:
        var t = _next();
        var value = double.parse('$unary${t.text}');
        return processDimension(t, value, _makeSpan(start));
      case TokenKind.SINGLE_QUOTE:
        var value = processQuotedString(false);
        value = "'${_escapeString(value, single: true)}'";
        return LiteralTerm(value, value, _makeSpan(start));
      case TokenKind.DOUBLE_QUOTE:
        var value = processQuotedString(false);
        value = '"${_escapeString(value)}"';
        return LiteralTerm(value, value, _makeSpan(start));
      case TokenKind.LPAREN:
        _next();

        var group = GroupTerm(_makeSpan(start));

        dynamic /* Expression | List<Expression> | ... */ term;
        do {
          term = processTerm();
          if (term != null && term is LiteralTerm) {
            group.add(term);
          }
        } while (term != null &&
            !_maybeEat(TokenKind.RPAREN) &&
            !isPrematureEndOfFile());

        return group;
      case TokenKind.LBRACK:
        _next();

        var term = processTerm() as LiteralTerm;
        if (term is! NumberTerm) {
          _error('Expecting a positive number', _makeSpan(start));
        }

        _eat(TokenKind.RBRACK);

        return ItemTerm(term.value, term.text, _makeSpan(start));
      case TokenKind.IDENTIFIER:
        return processIdentifier();
      case TokenKind.UNICODE_RANGE:
        String? first;
        String? second;
        int firstNumber;
        int secondNumber;
        _eat(TokenKind.UNICODE_RANGE, unicodeRange: true);
        if (_maybeEat(TokenKind.HEX_INTEGER, unicodeRange: true)) {
          first = _previousToken!.text;
          firstNumber = int.parse('0x$first');
          if (firstNumber > MAX_UNICODE) {
            _error('unicode range must be less than 10FFFF', _makeSpan(start));
          }
          if (_maybeEat(TokenKind.MINUS, unicodeRange: true)) {
            if (_maybeEat(TokenKind.HEX_INTEGER, unicodeRange: true)) {
              second = _previousToken!.text;
              secondNumber = int.parse('0x$second');
              if (secondNumber > MAX_UNICODE) {
                _error(
                    'unicode range must be less than 10FFFF', _makeSpan(start));
              }
              if (firstNumber > secondNumber) {
                _error('unicode first range can not be greater than last',
                    _makeSpan(start));
              }
            }
          }
        } else if (_maybeEat(TokenKind.HEX_RANGE, unicodeRange: true)) {
          first = _previousToken!.text;
        }

        return UnicodeRangeTerm(first, second, _makeSpan(start));
      case TokenKind.AT:
        if (messages.options.lessSupport) {
          _next();

          var expr = processExpr();
          if (isChecked && expr.expressions.length > 1) {
            _error('only @name for Less syntax', _peekToken.span);
          }

          var param = expr.expressions[0];
          var varUsage =
              VarUsage((param as LiteralTerm).text, [], _makeSpan(start));
          expr.expressions[0] = varUsage;
          return expr.expressions;
        }
        break;
      default:
        // For tokens that we don't match above, but that are identifier like
        // ('PT', 'PX', ...) handle them like identifiers.
        if (TokenKind.isKindIdentifier(_peek())) {
          return processIdentifier();
        } else {
          return null;
        }
    }
  }

  /// Process all dimension units.
  LiteralTerm processDimension(Token? t, Object value, FileSpan span) {
    LiteralTerm term;
    var unitType = _peek();

    switch (unitType) {
      case TokenKind.UNIT_EM:
        span = span.expand(_next().span);
        term = EmTerm(value, t!.text, span);
        break;
      case TokenKind.UNIT_EX:
        span = span.expand(_next().span);
        term = ExTerm(value, t!.text, span);
        break;
      case TokenKind.UNIT_LENGTH_PX:
      case TokenKind.UNIT_LENGTH_CM:
      case TokenKind.UNIT_LENGTH_MM:
      case TokenKind.UNIT_LENGTH_IN:
      case TokenKind.UNIT_LENGTH_PT:
      case TokenKind.UNIT_LENGTH_PC:
        span = span.expand(_next().span);
        term = LengthTerm(value, t!.text, span, unitType);
        break;
      case TokenKind.UNIT_ANGLE_DEG:
      case TokenKind.UNIT_ANGLE_RAD:
      case TokenKind.UNIT_ANGLE_GRAD:
      case TokenKind.UNIT_ANGLE_TURN:
        span = span.expand(_next().span);
        term = AngleTerm(value, t!.text, span, unitType);
        break;
      case TokenKind.UNIT_TIME_MS:
      case TokenKind.UNIT_TIME_S:
        span = span.expand(_next().span);
        term = TimeTerm(value, t!.text, span, unitType);
        break;
      case TokenKind.UNIT_FREQ_HZ:
      case TokenKind.UNIT_FREQ_KHZ:
        span = span.expand(_next().span);
        term = FreqTerm(value, t!.text, span, unitType);
        break;
      case TokenKind.PERCENT:
        span = span.expand(_next().span);
        term = PercentageTerm(value, t!.text, span);
        break;
      case TokenKind.UNIT_FRACTION:
        span = span.expand(_next().span);
        term = FractionTerm(value, t!.text, span);
        break;
      case TokenKind.UNIT_RESOLUTION_DPI:
      case TokenKind.UNIT_RESOLUTION_DPCM:
      case TokenKind.UNIT_RESOLUTION_DPPX:
        span = span.expand(_next().span);
        term = ResolutionTerm(value, t!.text, span, unitType);
        break;
      case TokenKind.UNIT_CH:
        span = span.expand(_next().span);
        term = ChTerm(value, t!.text, span, unitType);
        break;
      case TokenKind.UNIT_REM:
        span = span.expand(_next().span);
        term = RemTerm(value, t!.text, span, unitType);
        break;
      case TokenKind.UNIT_VIEWPORT_VW:
      case TokenKind.UNIT_VIEWPORT_VH:
      case TokenKind.UNIT_VIEWPORT_VMIN:
      case TokenKind.UNIT_VIEWPORT_VMAX:
        span = span.expand(_next().span);
        term = ViewportTerm(value, t!.text, span, unitType);
        break;
      case TokenKind.UNIT_LH:
      case TokenKind.UNIT_RLH:
        span = span.expand(_next().span);
        term = LineHeightTerm(value, t!.text, span, unitType);
        break;
      default:
        if (value is Identifier) {
          term = LiteralTerm(value, value.name, span);
        } else {
          term = NumberTerm(value, t!.text, span);
        }
    }

    return term;
  }

  String processQuotedString([bool urlString = false]) {
    var start = _peekToken.span;

    // URI term sucks up everything inside of quotes(' or ") or between parens
    var stopToken = urlString ? TokenKind.RPAREN : -1;

    // Note: disable skipping whitespace tokens inside a string.
    // TODO(jmesserly): the layering here feels wrong.
    var inString = tokenizer._inString;
    tokenizer._inString = false;

    switch (_peek()) {
      case TokenKind.SINGLE_QUOTE:
        stopToken = TokenKind.SINGLE_QUOTE;
        _next(); // Skip the SINGLE_QUOTE.
        start = _peekToken.span;
        break;
      case TokenKind.DOUBLE_QUOTE:
        stopToken = TokenKind.DOUBLE_QUOTE;
        _next(); // Skip the DOUBLE_QUOTE.
        start = _peekToken.span;
        break;
      default:
        if (urlString) {
          if (_peek() == TokenKind.LPAREN) {
            _next(); // Skip the LPAREN.
            start = _peekToken.span;
          }
          stopToken = TokenKind.RPAREN;
        } else {
          _error('unexpected string', _makeSpan(start));
        }
        break;
    }

    // Gobble up everything until we hit our stop token.
    var stringValue = StringBuffer();
    while (_peek() != stopToken && _peek() != TokenKind.END_OF_FILE) {
      stringValue.write(_next().text);
    }

    tokenizer._inString = inString;

    // All characters between quotes is the string.
    if (stopToken != TokenKind.RPAREN) {
      _next(); // Skip the SINGLE_QUOTE or DOUBLE_QUOTE;
    }

    return stringValue.toString();
  }

  // TODO(terry): Should probably understand IE's non-standard filter syntax to
  //              fully support calc, var(), etc.
  /// IE's filter property breaks CSS value parsing.  IE's format can be:
  ///
  ///    filter: progid:DXImageTransform.MS.gradient(Type=0, Color='#9d8b83');
  ///
  /// We'll just parse everything after the 'progid:' look for the left paren
  /// then parse to the right paren ignoring everything in between.
  dynamic processIEFilter(FileSpan startAfterProgidColon) {
    // Support non-functional filters (i.e. filter: FlipH)
    var kind = _peek();
    if (kind == TokenKind.SEMICOLON || kind == TokenKind.RBRACE) {
      var tok = tokenizer.makeIEFilter(
          startAfterProgidColon.start.offset, _peekToken.start);
      return LiteralTerm(tok.text, tok.text, tok.span);
    }

    var parens = 0;
    while (_peek() != TokenKind.END_OF_FILE) {
      switch (_peek()) {
        case TokenKind.LPAREN:
          _eat(TokenKind.LPAREN);
          parens++;
          break;
        case TokenKind.RPAREN:
          _eat(TokenKind.RPAREN);
          if (--parens == 0) {
            var tok = tokenizer.makeIEFilter(
                startAfterProgidColon.start.offset, _peekToken.start);
            return LiteralTerm(tok.text, tok.text, tok.span);
          }
          break;
        default:
          _eat(_peek());
      }
    }
  }

  // TODO(terry): Hack to gobble up the calc expression as a string looking
  //              for the matching RPAREN the expression is not parsed into the
  //              AST.
  //
  // grammar should be:
  //
  //     <calc()> = calc( <calc-sum> )
  //     <calc-sum> = <calc-product> [ [ '+' | '-' ] <calc-product> ]*
  //     <calc-product> = <calc-value> [ '*' <calc-value> | '/' <number> ]*
  //     <calc-value> = <number> | <dimension> | <percentage> | ( <calc-sum> )
  //
  String processCalcExpression() {
    var inString = tokenizer._inString;
    tokenizer._inString = false;

    // Gobble up everything until we hit our stop token.
    var stringValue = StringBuffer();
    var left = 1;
    var matchingParens = false;
    while (_peek() != TokenKind.END_OF_FILE && !matchingParens) {
      var token = _peek();
      if (token == TokenKind.LPAREN) {
        left++;
      } else if (token == TokenKind.RPAREN) {
        left--;
      }

      matchingParens = left == 0;
      if (!matchingParens) stringValue.write(_next().text);
    }

    if (!matchingParens) {
      _error('problem parsing function expected ), ', _peekToken.span);
    }

    tokenizer._inString = inString;

    return stringValue.toString();
  }

  CalcTerm? processCalc(Identifier func) {
    var start = _peekToken.span;

    var name = func.name;
    if (const {'calc', '-webkit-calc', '-moz-calc', 'min', 'max', 'clamp'}
        .contains(name)) {
      // TODO(terry): Implement expression parsing properly.
      var expression = processCalcExpression();
      var calcExpr = LiteralTerm(expression, expression, _makeSpan(start));

      if (!_maybeEat(TokenKind.RPAREN)) {
        _error('problem parsing function expected ), ', _peekToken.span);
      }

      return CalcTerm(name, name, calcExpr, _makeSpan(start));
    }

    return null;
  }

  //  Function grammar:
  //
  //  function:     IDENT '(' expr ')'
  //
  TreeNode /* LiteralTerm | Expression */ processFunction(Identifier func) {
    var start = _peekToken.span;
    var name = func.name;

    switch (name) {
      case 'url':
        // URI term sucks up everything inside of quotes(' or ") or between
        // parens.
        var urlParam = processQuotedString(true);

        // TODO(terry): Better error message and checking for mismatched quotes.
        if (_peek() == TokenKind.END_OF_FILE) {
          _error('problem parsing URI', _peekToken.span);
        }

        if (_peek() == TokenKind.RPAREN) {
          _next();
        }

        return UriTerm(urlParam, _makeSpan(start));
      case 'var':
        // TODO(terry): Consider handling var in IE specific filter/progid.
        //              This will require parsing entire IE specific syntax
        //              e.g. `param = value` or `progid:com_id`, etc.
        //              for example:
        //
        //    var-blur: Blur(Add = 0, Direction = 225, Strength = 10);
        //    var-gradient: progid:DXImageTransform.Microsoft.gradient"
        //      (GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
        var expr = processExpr();
        if (!_maybeEat(TokenKind.RPAREN)) {
          _error('problem parsing var expected ), ', _peekToken.span);
        }
        if (isChecked &&
            expr.expressions.whereType<OperatorComma>().length > 1) {
          _error('too many parameters to var()', _peekToken.span);
        }

        var paramName = (expr.expressions[0] as LiteralTerm).text;

        // [0] - var name, [1] - OperatorComma, [2] - default value.
        var defaultValues = expr.expressions.length >= 3
            ? expr.expressions.sublist(2)
            : <Expression>[];
        return VarUsage(paramName, defaultValues, _makeSpan(start));
      default:
        var expr = processExpr();
        if (!_maybeEat(TokenKind.RPAREN)) {
          _error('problem parsing function expected ), ', _peekToken.span);
        }

        return FunctionTerm(name, name, expr, _makeSpan(start));
    }
  }

  Identifier identifier() {
    var tok = _next();

    if (!TokenKind.isIdentifier(tok.kind) &&
        !TokenKind.isKindIdentifier(tok.kind)) {
      if (isChecked) {
        _warning('expected identifier, but found $tok', tok.span);
      }
      return Identifier('', _makeSpan(tok.span));
    }

    return Identifier(tok.text, _makeSpan(tok.span));
  }

  // TODO(terry): Move this to base <= 36 and into shared code.
  static int _hexDigit(int c) {
    if (c >= 48 /*0*/ && c <= 57 /*9*/) {
      return c - 48;
    } else if (c >= 97 /*a*/ && c <= 102 /*f*/) {
      return c - 87;
    } else if (c >= 65 /*A*/ && c <= 70 /*F*/) {
      return c - 55;
    } else {
      return -1;
    }
  }

  HexColorTerm _parseHex(String hexText, SourceSpan span) {
    var hexValue = 0;

    for (var i = 0; i < hexText.length; i++) {
      var digit = _hexDigit(hexText.codeUnitAt(i));
      if (digit < 0) {
        _warning('Bad hex number', span);
        return HexColorTerm(BAD_HEX_VALUE(), hexText, span);
      }
      hexValue = (hexValue << 4) + digit;
    }

    // Make 3 character hex value #RRGGBB => #RGB iff:
    // high/low nibble of RR is the same, high/low nibble of GG is the same and
    // high/low nibble of BB is the same.
    if (hexText.length == 6 &&
        hexText[0] == hexText[1] &&
        hexText[2] == hexText[3] &&
        hexText[4] == hexText[5]) {
      hexText = '${hexText[0]}${hexText[2]}${hexText[4]}';
    } else if (hexText.length == 4 &&
        hexText[0] == hexText[1] &&
        hexText[2] == hexText[3]) {
      hexText = '${hexText[0]}${hexText[2]}';
    } else if (hexText.length == 2 && hexText[0] == hexText[1]) {
      hexText = hexText[0];
    }
    return HexColorTerm(hexValue, hexText, span);
  }
}

class ExpressionsProcessor {
  final Expressions _exprs;
  int _index = 0;

  ExpressionsProcessor(this._exprs);

  // TODO(terry): Only handles ##px unit.
  FontExpression processFontSize() {
    // font-size[/line-height]
    //
    // Possible size values:
    // * xx-small
    // * small
    // * medium [default]
    // * large
    // * x-large
    // * xx-large
    // * smaller
    // * larger
    // * ##length in px, pt, etc.
    // * ##%, percent of parent elem's font-size
    // * inherit
    LengthTerm? size;
    LineHeight? lineHt;
    var nextIsLineHeight = false;
    for (; _index < _exprs.expressions.length; _index++) {
      var expr = _exprs.expressions[_index];
      if (size == null && expr is LengthTerm) {
        // font-size part.
        size = expr;
      } else if (size != null) {
        if (expr is OperatorSlash) {
          // LineHeight could follow?
          nextIsLineHeight = true;
        } else if (nextIsLineHeight && expr is LengthTerm) {
          assert(expr.unit == TokenKind.UNIT_LENGTH_PX);
          lineHt = LineHeight(expr.value as num, inPixels: true);
          nextIsLineHeight = false;
          _index++;
          break;
        } else {
          break;
        }
      } else {
        break;
      }
    }

    return FontExpression(_exprs.span, size: size, lineHeight: lineHt);
  }

  FontExpression processFontFamily() {
    var family = <String>[];

    // Possible family values:
    // * font-family: arial, Times new roman ,Lucida Sans Unicode,Courier;
    // * font-family: "Times New Roman", arial, Lucida Sans Unicode, Courier;
    var moreFamilies = false;

    for (; _index < _exprs.expressions.length; _index++) {
      var expr = _exprs.expressions[_index];
      if (expr is LiteralTerm) {
        if (family.isEmpty || moreFamilies) {
          // It's font-family now.
          family.add(expr.toString());
          moreFamilies = false;
        } else if (isChecked) {
          messages.warning('Only font-family can be a list', _exprs.span);
        }
      } else if (expr is OperatorComma && family.isNotEmpty) {
        moreFamilies = true;
      } else {
        break;
      }
    }

    return FontExpression(_exprs.span, family: family);
  }

  FontExpression processFont() {
    // Process all parts of the font expression.
    FontExpression? fontSize;
    FontExpression? fontFamily;
    for (; _index < _exprs.expressions.length; _index++) {
      // Order is font-size font-family
      fontSize ??= processFontSize();
      fontFamily ??= processFontFamily();
      // TODO(terry): Handle font-weight, font-style, and font-variant. See
      //              https://github.com/dart-lang/csslib/issues/3
      //              https://github.com/dart-lang/csslib/issues/4
      //              https://github.com/dart-lang/csslib/issues/5
    }

    return FontExpression(_exprs.span,
        size: fontSize?.font.size,
        lineHeight: fontSize?.font.lineHeight,
        family: fontFamily?.font.family);
  }
}

/// Escapes [text] for use in a CSS string.
/// [single] specifies single quote `'` vs double quote `"`.
String _escapeString(String text, {bool single = false}) {
  StringBuffer? result;

  for (var i = 0; i < text.length; i++) {
    var code = text.codeUnitAt(i);
    String? replace;
    switch (code) {
      case 34 /*'"'*/ :
        if (!single) replace = r'\"';
        break;
      case 39 /*"'"*/ :
        if (single) replace = r"\'";
        break;
    }

    if (replace != null && result == null) {
      result = StringBuffer(text.substring(0, i));
    }

    if (result != null) result.write(replace ?? text[i]);
  }

  return result == null ? text : result.toString();
}
