// Copyright 2016 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'package:charcode/charcode.dart';
import 'package:meta/meta.dart';

import '../ast/css/value.dart';
import '../ast/selector.dart';
import '../util/character.dart';
import '../utils.dart';
import 'parser.dart';

/// Pseudo-class selectors that take unadorned selectors as arguments.
@internal
final selectorPseudoClasses = {
  "not",
  "is",
  "matches",
  "where",
  "current",
  "any",
  "has",
  "host",
  "host-context",
};

/// Pseudo-element selectors that take unadorned selectors as arguments.
@internal
final selectorPseudoElements = {"slotted"};

/// A parser for selectors.
///
/// This class is largely duplicated between here and [SelectorParser]. Most
/// changes here should be mirrored there and vice versa.
class SelectorParser extends Parser {
  /// Whether this parser allows the parent selector `&`.
  final bool _allowParent;

  /// Whether to parse the selector as plain CSS.
  final bool _plainCss;

  /// Creates a parser that parses CSS selectors.
  ///
  /// If [allowParent] is `false`, this will throw a [SassFormatException] if
  /// the selector includes the parent selector `&`.
  ///
  /// If [plainCss] is `true`, this will parse the selector as a plain CSS
  /// selector rather than a Sass selector.
  SelectorParser(
    super.contents, {
    super.url,
    super.interpolationMap,
    bool allowParent = true,
    bool plainCss = false,
  })  : _allowParent = allowParent,
        _plainCss = plainCss;

  SelectorList parse() {
    return wrapSpanFormatException(() {
      var selector = _selectorList();
      if (!scanner.isDone) scanner.error("expected selector.");
      return selector;
    });
  }

  ComplexSelector parseComplexSelector() {
    return wrapSpanFormatException(() {
      var complex = _complexSelector();
      if (!scanner.isDone) scanner.error("expected selector.");
      return complex;
    });
  }

  CompoundSelector parseCompoundSelector() {
    return wrapSpanFormatException(() {
      var compound = _compoundSelector();
      if (!scanner.isDone) scanner.error("expected selector.");
      return compound;
    });
  }

  SimpleSelector parseSimpleSelector() {
    return wrapSpanFormatException(() {
      var simple = _simpleSelector();
      if (!scanner.isDone) scanner.error("unexpected token.");
      return simple;
    });
  }

  /// Consumes a selector list.
  SelectorList _selectorList() {
    var start = scanner.state;
    var previousLine = scanner.line;
    var components = <ComplexSelector>[_complexSelector()];

    _whitespace();
    while (scanner.scanChar($comma)) {
      _whitespace();
      if (scanner.peekChar() == $comma) continue;
      if (scanner.isDone) break;

      var lineBreak = scanner.line != previousLine;
      if (lineBreak) previousLine = scanner.line;
      components.add(_complexSelector(lineBreak: lineBreak));
    }

    return SelectorList(components, spanFrom(start));
  }

  /// Consumes a complex selector.
  ///
  /// If [lineBreak] is `true`, that indicates that there was a line break
  /// before this selector.
  ComplexSelector _complexSelector({bool lineBreak = false}) {
    var start = scanner.state;

    var componentStart = scanner.state;
    CompoundSelector? lastCompound;
    var combinators = <CssValue<Combinator>>[];

    List<CssValue<Combinator>>? initialCombinators;
    var components = <ComplexSelectorComponent>[];

    loop:
    while (true) {
      _whitespace();

      switch (scanner.peekChar()) {
        case $plus:
          var combinatorStart = scanner.state;
          scanner.readChar();
          combinators.add(
            CssValue(Combinator.nextSibling, spanFrom(combinatorStart)),
          );

        case $gt:
          var combinatorStart = scanner.state;
          scanner.readChar();
          combinators.add(
            CssValue(Combinator.child, spanFrom(combinatorStart)),
          );

        case $tilde:
          var combinatorStart = scanner.state;
          scanner.readChar();
          combinators.add(
            CssValue(Combinator.followingSibling, spanFrom(combinatorStart)),
          );

        case null:
          break loop;

        case $lbracket ||
              $dot ||
              $hash ||
              $percent ||
              $colon ||
              $ampersand ||
              $asterisk ||
              $pipe:
        case _ when lookingAtIdentifier():
          if (lastCompound != null) {
            components.add(
              ComplexSelectorComponent(
                lastCompound,
                combinators,
                spanFrom(componentStart),
              ),
            );
          } else if (combinators.isNotEmpty) {
            assert(initialCombinators == null);
            initialCombinators = combinators;
            componentStart = scanner.state;
          }

          lastCompound = _compoundSelector();
          combinators = [];
          if (scanner.peekChar() == $ampersand) {
            scanner.error(
              '"&" may only used at the beginning of a compound selector.',
            );
          }

        case _:
          break loop;
      }
    }

    if (combinators.isNotEmpty && _plainCss) {
      scanner.error("expected selector.");
    } else if (lastCompound != null) {
      components.add(
        ComplexSelectorComponent(
          lastCompound,
          combinators,
          spanFrom(componentStart),
        ),
      );
    } else if (combinators.isNotEmpty) {
      initialCombinators = combinators;
    } else {
      scanner.error("expected selector.");
    }

    return ComplexSelector(
      initialCombinators ?? const [],
      components,
      spanFrom(start),
      lineBreak: lineBreak,
    );
  }

  /// Consumes a compound selector.
  CompoundSelector _compoundSelector() {
    var start = scanner.state;
    var components = <SimpleSelector>[_simpleSelector()];

    while (_isSimpleSelectorStart(scanner.peekChar())) {
      components.add(_simpleSelector(allowParent: _plainCss));
    }

    return CompoundSelector(components, spanFrom(start));
  }

  /// Consumes a simple selector.
  ///
  /// If [allowParent] is passed, it controls whether the parent selector `&` is
  /// allowed. Otherwise, it defaults to [_allowParent].
  SimpleSelector _simpleSelector({bool? allowParent}) {
    var start = scanner.state;
    allowParent ??= _allowParent;
    switch (scanner.peekChar()) {
      case $lbracket:
        return _attributeSelector();
      case $dot:
        return _classSelector();
      case $hash:
        return _idSelector();
      case $percent:
        var selector = _placeholderSelector();
        if (_plainCss) {
          error(
            "Placeholder selectors aren't allowed in plain CSS.",
            spanFrom(start),
          );
        }
        return selector;
      case $colon:
        return _pseudoSelector();
      case $ampersand:
        var selector = _parentSelector();
        if (!allowParent) {
          error(
            "Parent selectors aren't allowed here.",
            spanFrom(start),
          );
        }
        return selector;

      default:
        return _typeOrUniversalSelector();
    }
  }

  /// Consumes an attribute selector.
  AttributeSelector _attributeSelector() {
    var start = scanner.state;
    scanner.expectChar($lbracket);
    _whitespace();

    var name = _attributeName();

    _whitespace();
    if (scanner.scanChar($rbracket)) {
      return AttributeSelector(name, spanFrom(start));
    }

    var operator = _attributeOperator();
    _whitespace();

    var next = scanner.peekChar();
    var value = next == $single_quote || next == $double_quote
        ? string()
        : identifier();
    _whitespace();

    next = scanner.peekChar();
    var modifier = next != null && next.isAlphabetic
        ? String.fromCharCode(scanner.readChar())
        : null;

    scanner.expectChar($rbracket);
    return AttributeSelector.withOperator(
      name,
      operator,
      value,
      spanFrom(start),
      modifier: modifier,
    );
  }

  /// Consumes a qualified name as part of an attribute selector.
  QualifiedName _attributeName() {
    if (scanner.scanChar($asterisk)) {
      scanner.expectChar($pipe);
      return QualifiedName(identifier(), namespace: "*");
    }

    if (scanner.scanChar($pipe)) {
      return QualifiedName(identifier(), namespace: "");
    }

    var nameOrNamespace = identifier();
    if (scanner.peekChar() != $pipe || scanner.peekChar(1) == $equal) {
      return QualifiedName(nameOrNamespace);
    }

    scanner.readChar();
    return QualifiedName(identifier(), namespace: nameOrNamespace);
  }

  /// Consumes an attribute selector's operator.
  AttributeOperator _attributeOperator() {
    var start = scanner.position;
    switch (scanner.readChar()) {
      case $equal:
        return AttributeOperator.equal;

      case $tilde:
        scanner.expectChar($equal);
        return AttributeOperator.include;

      case $pipe:
        scanner.expectChar($equal);
        return AttributeOperator.dash;

      case $caret:
        scanner.expectChar($equal);
        return AttributeOperator.prefix;

      case $dollar:
        scanner.expectChar($equal);
        return AttributeOperator.suffix;

      case $asterisk:
        scanner.expectChar($equal);
        return AttributeOperator.substring;

      default:
        scanner.error('Expected "]".', position: start);
    }
  }

  /// Consumes a class selector.
  ClassSelector _classSelector() {
    var start = scanner.state;
    scanner.expectChar($dot);
    var name = identifier();
    return ClassSelector(name, spanFrom(start));
  }

  /// Consumes an ID selector.
  IDSelector _idSelector() {
    var start = scanner.state;
    scanner.expectChar($hash);
    var name = identifier();
    return IDSelector(name, spanFrom(start));
  }

  /// Consumes a placeholder selector.
  PlaceholderSelector _placeholderSelector() {
    var start = scanner.state;
    scanner.expectChar($percent);
    var name = identifier();
    return PlaceholderSelector(name, spanFrom(start));
  }

  /// Consumes a parent selector.
  ParentSelector _parentSelector() {
    var start = scanner.state;
    scanner.expectChar($ampersand);
    var suffix = lookingAtIdentifierBody() ? identifierBody() : null;
    if (_plainCss && suffix != null) {
      scanner.error(
        "Parent selectors can't have suffixes in plain CSS.",
        position: start.position,
        length: scanner.position - start.position,
      );
    }

    return ParentSelector(spanFrom(start), suffix: suffix);
  }

  /// Consumes a pseudo selector.
  PseudoSelector _pseudoSelector() {
    var start = scanner.state;
    scanner.expectChar($colon);
    var element = scanner.scanChar($colon);
    var name = identifier();

    if (!scanner.scanChar($lparen)) {
      return PseudoSelector(name, spanFrom(start), element: element);
    }
    _whitespace();

    var unvendored = unvendor(name);
    String? argument;
    SelectorList? selector;
    if (element) {
      if (selectorPseudoElements.contains(unvendored)) {
        selector = _selectorList();
      } else {
        argument = declarationValue(allowEmpty: true);
      }
    } else if (selectorPseudoClasses.contains(unvendored)) {
      selector = _selectorList();
    } else if (unvendored == "nth-child" || unvendored == "nth-last-child") {
      argument = _aNPlusB();
      _whitespace();
      if (scanner.peekChar(-1).isWhitespace && scanner.peekChar() != $rparen) {
        expectIdentifier("of");
        argument += " of";
        _whitespace();

        selector = _selectorList();
      }
    } else {
      argument = declarationValue(allowEmpty: true).trimRight();
    }
    scanner.expectChar($rparen);

    return PseudoSelector(
      name,
      spanFrom(start),
      element: element,
      argument: argument,
      selector: selector,
    );
  }

  /// Consumes an [`An+B` production][An+B] and returns its text.
  ///
  /// [An+B]: https://drafts.csswg.org/css-syntax-3/#anb-microsyntax
  String _aNPlusB() {
    var buffer = StringBuffer();
    switch (scanner.peekChar()) {
      case $e || $E:
        expectIdentifier("even");
        return "even";

      case $o || $O:
        expectIdentifier("odd");
        return "odd";

      case $plus || $minus:
        buffer.writeCharCode(scanner.readChar());
        break;
    }

    if (scanner.peekChar().isDigit) {
      do {
        buffer.writeCharCode(scanner.readChar());
      } while (scanner.peekChar().isDigit);
      _whitespace();
      if (!scanIdentChar($n)) return buffer.toString();
    } else {
      expectIdentChar($n);
    }
    buffer.writeCharCode($n);
    _whitespace();

    var next = scanner.peekChar();
    if (next != $plus && next != $minus) return buffer.toString();
    buffer.writeCharCode(scanner.readChar());
    _whitespace();

    if (!scanner.peekChar().isDigit) scanner.error("Expected a number.");
    do {
      buffer.writeCharCode(scanner.readChar());
    } while (scanner.peekChar().isDigit);
    return buffer.toString();
  }

  /// Consumes a type selector or a universal selector.
  ///
  /// These are combined because either one could start with `*`.
  SimpleSelector _typeOrUniversalSelector() {
    var start = scanner.state;
    if (scanner.scanChar($asterisk)) {
      if (!scanner.scanChar($pipe)) return UniversalSelector(spanFrom(start));
      return scanner.scanChar($asterisk)
          ? UniversalSelector(spanFrom(start), namespace: "*")
          : TypeSelector(
              QualifiedName(identifier(), namespace: "*"), spanFrom(start));
    } else if (scanner.scanChar($pipe)) {
      return scanner.scanChar($asterisk)
          ? UniversalSelector(spanFrom(start), namespace: "")
          : TypeSelector(
              QualifiedName(identifier(), namespace: ""), spanFrom(start));
    }

    var nameOrNamespace = identifier();
    if (!scanner.scanChar($pipe)) {
      return TypeSelector(QualifiedName(nameOrNamespace), spanFrom(start));
    } else if (scanner.scanChar($asterisk)) {
      return UniversalSelector(spanFrom(start), namespace: nameOrNamespace);
    } else {
      return TypeSelector(
          QualifiedName(identifier(), namespace: nameOrNamespace),
          spanFrom(start));
    }
  }

  // Returns whether [character] can start a simple selector in the middle of a
  // compound selector.
  bool _isSimpleSelectorStart(int? character) => switch (character) {
        $asterisk || $lbracket || $dot || $hash || $percent || $colon => true,
        $ampersand => _plainCss,
        _ => false,
      };

  /// The value of `consumeNewlines` is not relevant for this class.
  void _whitespace() {
    whitespace(consumeNewlines: true);
  }
}
