// Copyright 2023 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:meta/meta.dart';

import '../ast/sass.dart';
import '../exception.dart';
import '../util/map.dart';
import 'interface/if_condition_expression.dart';
import 'interface/expression.dart';

/// A visitor that recursively traverses each expression in a SassScript AST and
/// replaces its contents with the values returned by nested recursion.
///
/// In addition to the methods from [ExpressionVisitor], this has more general
/// protected methods that can be overridden to add behavior for a wide variety
/// of AST nodes:
///
/// * [visitArgumentList]
/// * [visitSupportsCondition]
/// * [visitInterpolation]
///
/// {@category Visitor}
mixin ReplaceExpressionVisitor
    implements
        ExpressionVisitor<Expression>,
        IfConditionExpressionVisitor<IfConditionExpression> {
  Expression visitBinaryOperationExpression(BinaryOperationExpression node) =>
      BinaryOperationExpression(
        node.operator,
        node.left.accept(this),
        node.right.accept(this),
      );

  Expression visitBooleanExpression(BooleanExpression node) => node;

  Expression visitColorExpression(ColorExpression node) => node;

  Expression visitFunctionExpression(FunctionExpression node) =>
      FunctionExpression(
        node.originalName,
        visitArgumentList(node.arguments),
        node.span,
        namespace: node.namespace,
      );

  Expression visitInterpolatedFunctionExpression(
    InterpolatedFunctionExpression node,
  ) =>
      InterpolatedFunctionExpression(
        visitInterpolation(node.name),
        visitArgumentList(node.arguments),
        node.span,
      );

  Expression visitLegacyIfExpression(LegacyIfExpression node) =>
      LegacyIfExpression(visitArgumentList(node.arguments), node.span);

  Expression visitListExpression(ListExpression node) => ListExpression(
        node.contents.map((item) => item.accept(this)),
        node.separator,
        node.span,
        brackets: node.hasBrackets,
      );

  Expression visitMapExpression(MapExpression node) => MapExpression([
        for (var (key, value) in node.pairs)
          (key.accept(this), value.accept(this)),
      ], node.span);

  Expression visitNullExpression(NullExpression node) => node;

  Expression visitNumberExpression(NumberExpression node) => node;

  Expression visitParenthesizedExpression(ParenthesizedExpression node) =>
      ParenthesizedExpression(node.expression.accept(this), node.span);

  Expression visitSelectorExpression(SelectorExpression node) => node;

  Expression visitStringExpression(StringExpression node) =>
      StringExpression(visitInterpolation(node.text), quotes: node.hasQuotes);

  Expression visitSupportsExpression(SupportsExpression node) =>
      SupportsExpression(visitSupportsCondition(node.condition));

  Expression visitUnaryOperationExpression(UnaryOperationExpression node) =>
      UnaryOperationExpression(
        node.operator,
        node.operand.accept(this),
        node.span,
      );

  Expression visitValueExpression(ValueExpression node) => node;

  Expression visitVariableExpression(VariableExpression node) => node;

  // `if()` condition expressions

  IfConditionExpression visitIfConditionParenthesized(
          IfConditionParenthesized node) =>
      IfConditionParenthesized(node.expression.accept(this), node.span);

  IfConditionExpression visitIfConditionNegation(IfConditionNegation node) =>
      IfConditionNegation(node.expression.accept(this), node.span);

  IfConditionExpression visitIfConditionOperation(IfConditionOperation node) =>
      IfConditionOperation(
        node.expressions.map((expression) => expression.accept(this)),
        node.op,
      );

  IfConditionExpression visitIfConditionFunction(IfConditionFunction node) =>
      IfConditionFunction(
        visitInterpolation(node.name),
        visitInterpolation(node.arguments),
        node.span,
      );

  IfConditionExpression visitIfConditionSass(IfConditionSass node) =>
      IfConditionSass(node.expression.accept(this), node.span);

  IfConditionExpression visitIfConditionRaw(IfConditionRaw node) =>
      IfConditionRaw(visitInterpolation(node.text));

  /// Replaces each expression in an [invocation].
  ///
  /// The default implementation of the visit methods calls this to replace any
  /// argument invocation in an expression.
  @protected
  ArgumentList visitArgumentList(ArgumentList invocation) => ArgumentList(
        invocation.positional.map((expression) => expression.accept(this)),
        {
          for (var (name, value) in invocation.named.pairs)
            name: value.accept(this),
        },
        invocation.namedSpans,
        invocation.span,
        rest: invocation.rest?.accept(this),
        keywordRest: invocation.keywordRest?.accept(this),
      );

  /// Replaces each expression in [condition].
  ///
  /// The default implementation of the visit methods call this to visit any
  /// [SupportsCondition] they encounter.
  @protected
  SupportsCondition visitSupportsCondition(SupportsCondition condition) {
    if (condition is SupportsOperation) {
      return SupportsOperation(
        visitSupportsCondition(condition.left),
        visitSupportsCondition(condition.right),
        condition.operator,
        condition.span,
      );
    } else if (condition is SupportsNegation) {
      return SupportsNegation(
        visitSupportsCondition(condition.condition),
        condition.span,
      );
    } else if (condition is SupportsInterpolation) {
      return SupportsInterpolation(
        condition.expression.accept(this),
        condition.span,
      );
    } else if (condition is SupportsDeclaration) {
      return SupportsDeclaration(
        condition.name.accept(this),
        condition.value.accept(this),
        condition.span,
      );
    } else {
      throw SassException(
        "BUG: Unknown SupportsCondition $condition.",
        condition.span,
      );
    }
  }

  /// Replaces each expression in an [interpolation].
  ///
  /// The default implementation of the visit methods call this to visit any
  /// interpolation in an expression.
  @protected
  Interpolation visitInterpolation(Interpolation interpolation) =>
      Interpolation(
        interpolation.contents.map(
          (node) => node is Expression ? node.accept(this) : node,
        ),
        interpolation.spans,
        interpolation.span,
      );
}
