// 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 '../ast/sass.dart';
import '../deprecation.dart';
import '../interpolation_buffer.dart';
import '../util/character.dart';
import 'stylesheet.dart';

/// A parser for the CSS-compatible syntax.
class ScssParser extends StylesheetParser {
  bool get indented => false;
  int get currentIndentation => 0;

  ScssParser(super.contents, {super.url, super.parseSelectors});

  Interpolation styleRuleSelector() => almostAnyValue();

  void expectStatementSeparator([String? name]) {
    _whitespaceWithoutComments();
    if (scanner.isDone) return;
    if (scanner.peekChar() case $semicolon || $rbrace) return;
    scanner.expectChar($semicolon);
  }

  bool atEndOfStatement() {
    var next = scanner.peekChar();
    return next == null ||
        next == $semicolon ||
        next == $rbrace ||
        next == $lbrace;
  }

  bool lookingAtChildren() => scanner.peekChar() == $lbrace;

  bool scanElse(int ifIndentation) {
    var start = scanner.state;
    _whitespace();
    var beforeAt = scanner.state;
    if (scanner.scanChar($at)) {
      if (scanIdentifier('else', caseSensitive: true)) return true;
      if (scanIdentifier('elseif', caseSensitive: true)) {
        warnings.add((
          deprecation: Deprecation.elseif,
          message:
              '@elseif is deprecated and will not be supported in future Sass '
              'versions.\n'
              '\n'
              'Recommendation: @else if',
          span: spanFrom(beforeAt),
        ));
        scanner.position -= 2;
        return true;
      }
    }
    scanner.state = start;
    return false;
  }

  List<Statement> children(Statement child()) {
    scanner.expectChar($lbrace);
    _whitespaceWithoutComments();
    var children = <Statement>[];
    while (true) {
      switch (scanner.peekChar()) {
        case $dollar:
          children.add(variableDeclarationWithoutNamespace());

        case $slash:
          switch (scanner.peekChar(1)) {
            case $slash:
              children.add(_silentComment());
              _whitespaceWithoutComments();
            case $asterisk:
              children.add(_loudComment());
              _whitespaceWithoutComments();
            default:
              children.add(child());
          }

        case $semicolon:
          scanner.readChar();
          _whitespaceWithoutComments();

        case $rbrace:
          scanner.expectChar($rbrace);
          return children;

        default:
          children.add(child());
      }
    }
  }

  List<Statement> statements(Statement? statement()) {
    var statements = <Statement>[];
    _whitespaceWithoutComments();
    while (!scanner.isDone) {
      switch (scanner.peekChar()) {
        case $dollar:
          statements.add(variableDeclarationWithoutNamespace());

        case $slash:
          switch (scanner.peekChar(1)) {
            case $slash:
              statements.add(_silentComment());
              _whitespaceWithoutComments();
            case $asterisk:
              statements.add(_loudComment());
              _whitespaceWithoutComments();
            default:
              if (statement() case var child?) statements.add(child);
          }

        case $semicolon:
          scanner.readChar();
          _whitespaceWithoutComments();

        default:
          if (statement() case var child?) statements.add(child);
      }
    }
    return statements;
  }

  /// Consumes a statement-level silent comment block.
  SilentComment _silentComment() {
    var start = scanner.state;
    scanner.expect("//");

    do {
      while (!scanner.isDone && !scanner.readChar().isNewline) {}
      if (scanner.isDone) break;
      spaces();
    } while (scanner.scan("//"));

    if (plainCss) {
      error(
        "Silent comments aren't allowed in plain CSS.",
        spanFrom(start),
      );
    }

    return lastSilentComment = SilentComment(
      scanner.substring(start.position),
      spanFrom(start),
    );
  }

  /// Consumes a statement-level loud comment block.
  LoudComment _loudComment() {
    var start = scanner.state;
    scanner.expect("/*");
    var buffer = InterpolationBuffer()..write("/*");
    loop:
    while (true) {
      switch (scanner.peekChar()) {
        case $hash:
          if (scanner.peekChar(1) == $lbrace) {
            var (expression, span) = singleInterpolation();
            buffer.add(expression, span);
          } else {
            buffer.writeCharCode(scanner.readChar());
          }
          break;

        case $asterisk:
          buffer.writeCharCode(scanner.readChar());
          if (scanner.peekChar() != $slash) continue loop;

          buffer.writeCharCode(scanner.readChar());
          return LoudComment(buffer.interpolation(spanFrom(start)));

        case $cr:
          scanner.readChar();
          if (scanner.peekChar() != $lf) buffer.writeCharCode($lf);

        case $ff:
          scanner.readChar();
          buffer.writeCharCode($lf);

        default:
          buffer.writeCharCode(scanner.readChar());
      }
    }
  }

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

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