content: // Copyright 2021 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 'dart:math' as math;
  
  import 'package:charcode/charcode.dart';
  import 'package:meta/meta.dart';
  import 'package:source_span/source_span.dart';
  
  import '../deprecation.dart';
  import '../evaluation_context.dart';
  import '../exception.dart';
  import '../callable.dart';
  import '../util/character.dart';
  import '../util/nullable.dart';
  import '../util/number.dart' as number_lib;
  import '../utils.dart';
  import '../value.dart';
  import '../visitor/interface/value.dart';
  import '../visitor/serialize.dart';
  
  /// A SassScript calculation.
  ///
  /// Although calculations can in principle have any name or any number of
  /// arguments, this class only exposes the specific calculations that are
  /// supported by the Sass spec. This ensures that all calculations that the user
  /// works with are always fully simplified.
  ///
  /// {@category Value}
  final class SassCalculation extends Value {
    /// The calculation's name, such as `"calc"`.
    final String name;
  
    /// The calculation's arguments.
    ///
    /// Each argument is either a [SassNumber], a [SassCalculation], an unquoted
    /// [SassString], or a [CalculationOperation].
    final List<Object> arguments;
  
    /// @nodoc
    @internal
    bool get isSpecialNumber => true;
  
    /// Creates a new calculation with the given [name] and [arguments]
    /// that will not be simplified.
    @internal
    static SassCalculation unsimplified(
      String name,
      Iterable<Object> arguments,
    ) =>
        SassCalculation._(name, List.unmodifiable(arguments));
  
    /// Creates a `calc()` calculation with the given [argument].
    ///
    /// The [argument] must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value calc(Object argument) => switch (_simplify(argument)) {
          SassNumber value => value,
          SassCalculation value => value,
          var simplified => SassCalculation._(
              "calc",
              List.unmodifiable([simplified]),
            ),
        };
  
    /// Creates a `min()` calculation with the given [arguments].
    ///
    /// Each argument must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation]. It must be passed at
    /// least one argument.
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value min(Iterable<Object> arguments) {
      var args = _simplifyArguments(arguments);
      if (args.isEmpty) {
        throw ArgumentError("min() must have at least one argument.");
      }
  
      SassNumber? minimum;
      for (var arg in args) {
        if (arg is! SassNumber ||
            (minimum != null && !minimum.isComparableTo(arg))) {
          minimum = null;
          break;
        } else if (minimum == null || minimum.greaterThan(arg).isTruthy) {
          minimum = arg;
        }
      }
      if (minimum != null) return minimum;
  
      _verifyCompatibleNumbers(args);
      return SassCalculation._("min", args);
    }
  
    /// Creates a `max()` calculation with the given [arguments].
    ///
    /// Each argument must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation]. It must be passed at
    /// least one argument.
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value max(Iterable<Object> arguments) {
      var args = _simplifyArguments(arguments);
      if (args.isEmpty) {
        throw ArgumentError("max() must have at least one argument.");
      }
  
      SassNumber? maximum;
      for (var arg in args) {
        if (arg is! SassNumber ||
            (maximum != null && !maximum.isComparableTo(arg))) {
          maximum = null;
          break;
        } else if (maximum == null || maximum.lessThan(arg).isTruthy) {
          maximum = arg;
        }
      }
      if (maximum != null) return maximum;
  
      _verifyCompatibleNumbers(args);
      return SassCalculation._("max", args);
    }
  
    /// Creates a `hypot()` calculation with the given [arguments].
    ///
    /// Each argument must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation]. It must be passed at
    /// least one argument.
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value hypot(Iterable<Object> arguments) {
      var args = _simplifyArguments(arguments);
      if (args.isEmpty) {
        throw ArgumentError("hypot() must have at least one argument.");
      }
      _verifyCompatibleNumbers(args);
  
      var subtotal = 0.0;
      var first = args.first;
      if (first is! SassNumber || first.hasUnit('%')) {
        return SassCalculation._("hypot", args);
      }
      for (var i = 0; i < args.length; i++) {
        var number = args.elementAt(i);
        if (number is! SassNumber || !number.hasCompatibleUnits(first)) {
          return SassCalculation._("hypot", args);
        }
        var value = number.convertValueToMatch(
          first,
          "numbers[${i + 1}]",
          "numbers[1]",
        );
        subtotal += value * value;
      }
      return SassNumber.withUnits(
        math.sqrt(subtotal),
        numeratorUnits: first.numeratorUnits,
        denominatorUnits: first.denominatorUnits,
      );
    }
  
    /// Creates a `sqrt()` calculation with the given [argument].
    ///
    /// The [argument] must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value sqrt(Object argument) =>
        _singleArgument("sqrt", argument, number_lib.sqrt, forbidUnits: true);
  
    /// Creates a `sin()` calculation with the given [argument].
    ///
    /// The [argument] must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value sin(Object argument) =>
        _singleArgument("sin", argument, number_lib.sin);
  
    /// Creates a `cos()` calculation with the given [argument].
    ///
    /// The [argument] must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value cos(Object argument) =>
        _singleArgument("cos", argument, number_lib.cos);
  
    /// Creates a `tan()` calculation with the given [argument].
    ///
    /// The [argument] must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value tan(Object argument) =>
        _singleArgument("tan", argument, number_lib.tan);
  
    /// Creates an `atan()` calculation with the given [argument].
    ///
    /// The [argument] must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value atan(Object argument) =>
        _singleArgument("atan", argument, number_lib.atan, forbidUnits: true);
  
    /// Creates an `asin()` calculation with the given [argument].
    ///
    /// The [argument] must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value asin(Object argument) =>
        _singleArgument("asin", argument, number_lib.asin, forbidUnits: true);
  
    /// Creates an `acos()` calculation with the given [argument].
    ///
    /// The [argument] must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value acos(Object argument) =>
        _singleArgument("acos", argument, number_lib.acos, forbidUnits: true);
  
    /// Creates an `abs()` calculation with the given [argument].
    ///
    /// The [argument] must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value abs(Object argument) {
      argument = _simplify(argument);
      if (argument is! SassNumber) return SassCalculation._("abs", [argument]);
      if (argument.hasUnit("%")) {
        warnForDeprecation(
          "Passing percentage units to the global abs() function is deprecated.\n"
          "In the future, this will emit a CSS abs() function to be resolved by the browser.\n"
          "To preserve current behavior: math.abs($argument)"
          "\n"
          "To emit a CSS abs() now: abs(#{$argument})\n"
          "More info: https://sass-lang.com/d/abs-percent",
          Deprecation.absPercent,
        );
      }
      return number_lib.abs(argument);
    }
  
    /// Creates an `exp()` calculation with the given [argument].
    ///
    /// The [argument] must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value exp(Object argument) {
      argument = _simplify(argument);
      if (argument is! SassNumber) {
        return SassCalculation._("exp", [argument]);
      }
      argument.assertNoUnits();
      return number_lib.pow(SassNumber(math.e), argument);
    }
  
    /// Creates a `sign()` calculation with the given [argument].
    ///
    /// The [argument] must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    static Value sign(Object argument) {
      argument = _simplify(argument);
      return switch (argument) {
        SassNumber(value: double(isNaN: true) || 0) => argument,
        SassNumber arg when !arg.hasUnit('%') => SassNumber(
            arg.value.sign,
          ).coerceToMatch(argument),
        _ => SassCalculation._("sign", [argument]),
      };
    }
  
    /// Creates a `clamp()` calculation with the given [min], [value], and [max].
    ///
    /// Each argument must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    ///
    /// This may be passed fewer than three arguments, but only if one of the
    /// arguments is an unquoted `var()` string.
    static Value clamp(Object min, [Object? value, Object? max]) {
      if (value == null && max != null) {
        throw ArgumentError("If value is null, max must also be null.");
      }
  
      min = _simplify(min);
      value = value.andThen(_simplify);
      max = max.andThen(_simplify);
  
      if (min is SassNumber &&
          value is SassNumber &&
          max is SassNumber &&
          min.hasCompatibleUnits(value) &&
          min.hasCompatibleUnits(max)) {
        if (value.lessThanOrEquals(min).isTruthy) return min;
        if (value.greaterThanOrEquals(max).isTruthy) return max;
        return value;
      }
  
      var args = List<Object>.unmodifiable([
        min,
        if (value != null) value,
        if (max != null) max,
      ]);
      _verifyCompatibleNumbers(args);
      _verifyLength(args, 3);
      return SassCalculation._("clamp", args);
    }
  
    /// Creates a `pow()` calculation with the given [base] and [exponent].
    ///
    /// Each argument must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    ///
    /// This may be passed fewer than two arguments, but only if one of the
    /// arguments is an unquoted `var()` string.
    static Value pow(Object base, Object? exponent) {
      var args = [base, if (exponent != null) exponent];
      _verifyLength(args, 2);
      base = _simplify(base);
      exponent = exponent.andThen(_simplify);
      if (base is! SassNumber || exponent is! SassNumber) {
        return SassCalculation._("pow", args);
      }
      base.assertNoUnits();
      exponent.assertNoUnits();
      return number_lib.pow(base, exponent);
    }
  
    /// Creates a `log()` calculation with the given [number] and [base].
    ///
    /// Each argument must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    ///
    /// If arguments contains exactly a single argument, the base is set to
    /// `math.e` by default.
    static Value log(Object number, Object? base) {
      number = _simplify(number);
      base = base.andThen(_simplify);
      var args = [number, if (base != null) base];
      if (number is! SassNumber || (base != null && base is! SassNumber)) {
        return SassCalculation._("log", args);
      }
      number.assertNoUnits();
      if (base is SassNumber) {
        base.assertNoUnits();
        return number_lib.log(number, base);
      }
      return number_lib.log(number, null);
    }
  
    /// Creates a `atan2()` calculation for [y] and [x].
    ///
    /// Each argument must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    ///
    /// This may be passed fewer than two arguments, but only if one of the
    /// arguments is an unquoted `var()` string.
    static Value atan2(Object y, Object? x) {
      y = _simplify(y);
      x = x.andThen(_simplify);
      var args = [y, if (x != null) x];
      _verifyLength(args, 2);
      _verifyCompatibleNumbers(args);
      if (y is! SassNumber ||
          x is! SassNumber ||
          y.hasUnit('%') ||
          x.hasUnit('%') ||
          !y.hasCompatibleUnits(x)) {
        return SassCalculation._("atan2", args);
      }
      return number_lib.atan2(y, x);
    }
  
    /// Creates a `rem()` calculation with the given [dividend] and [modulus].
    ///
    /// Each argument must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    ///
    /// This may be passed fewer than two arguments, but only if one of the
    /// arguments is an unquoted `var()` string.
    static Value rem(Object dividend, Object? modulus) {
      dividend = _simplify(dividend);
      modulus = modulus.andThen(_simplify);
      var args = [dividend, if (modulus != null) modulus];
      _verifyLength(args, 2);
      _verifyCompatibleNumbers(args);
      if (dividend is! SassNumber ||
          modulus is! SassNumber ||
          !dividend.hasCompatibleUnits(modulus)) {
        return SassCalculation._("rem", args);
      }
      var result = dividend.modulo(modulus);
      if (modulus.value.signIncludingZero != dividend.value.signIncludingZero) {
        if (modulus.value.isInfinite) return dividend;
        if (result.value == 0) {
          return result.unaryMinus();
        }
        return result.minus(modulus);
      }
      return result;
    }
  
    /// Creates a `mod()` calculation with the given [dividend] and [modulus].
    ///
    /// Each argument must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    ///
    /// This may be passed fewer than two arguments, but only if one of the
    /// arguments is an unquoted `var()` string.
    static Value mod(Object dividend, Object? modulus) {
      dividend = _simplify(dividend);
      modulus = modulus.andThen(_simplify);
      var args = [dividend, if (modulus != null) modulus];
      _verifyLength(args, 2);
      _verifyCompatibleNumbers(args);
      if (dividend is! SassNumber ||
          modulus is! SassNumber ||
          !dividend.hasCompatibleUnits(modulus)) {
        return SassCalculation._("mod", args);
      }
      return dividend.modulo(modulus);
    }
  
    /// Creates a `round()` calculation with the given [strategyOrNumber],
    /// [numberOrStep], and [step]. Strategy must be either nearest, up, down or
    /// to-zero.
    ///
    /// Number and step must be either a [SassNumber], a [SassCalculation], an
    /// unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation, so it may return a
    /// [SassNumber] rather than a [SassCalculation]. It throws an exception if it
    /// can determine that the calculation will definitely produce invalid CSS.
    ///
    /// This may be passed fewer than two arguments, but only if one of the
    /// arguments is an unquoted `var()` string.
    static Value round(
      Object strategyOrNumber, [
      Object? numberOrStep,
      Object? step,
    ]) =>
        roundInternal(
          strategyOrNumber,
          numberOrStep,
          step,
          span: null,
          inLegacySassFunction: null,
          warn: null,
        );
  
    /// Like [round], but with the internal-only [inLegacySassFunction] and
    /// [warn] parameters.
    ///
    /// If [inLegacySassFunction] isn't null, this allows unitless numbers to be
    /// added and subtracted with numbers with units, for backwards-compatibility
    /// with the old global `min()` and `max()` functions. This emits a
    /// deprecation warning using the string as the function's name.
    ///
    /// If [simplify] is `false`, no simplification will be done.
    ///
    /// The [warn] callback is used to surface deprecation warnings.
    ///
    /// @nodoc
    @internal
    static Value roundInternal(
      Object strategyOrNumber,
      Object? numberOrStep,
      Object? step, {
      required FileSpan? span,
      required String? inLegacySassFunction,
      required void Function(String message, [Deprecation? deprecation])? warn,
    }) {
      switch ((
        _simplify(strategyOrNumber),
        numberOrStep.andThen(_simplify),
        step.andThen(_simplify),
      )) {
        case (SassNumber(hasUnits: false) && var number, null, null):
          return SassNumber(number.value.round());
  
        case (SassNumber number, null, null) when inLegacySassFunction != null:
          warn!(
            "In future versions of Sass, round() will be interpreted as a CSS "
            "round() calculation. This requires an explicit modulus when "
            "rounding numbers with units. If you want to use the Sass "
            "function, call math.round() instead.\n"
            "\n"
            "See https://sass-lang.com/d/import",
            Deprecation.globalBuiltin,
          );
          return _matchUnits(number.value.round().toDouble(), number);
  
        case (SassNumber number, SassNumber step, null)
            when !number.hasCompatibleUnits(step):
          _verifyCompatibleNumbers([number, step]);
          return SassCalculation._("round", [number, step]);
  
        case (SassNumber number, SassNumber step, null):
          _verifyCompatibleNumbers([number, step]);
          return _roundWithStep('nearest', number, step);
  
        case (
              SassString(text: 'nearest' || 'up' || 'down' || 'to-zero') &&
                  var strategy,
              SassNumber number,
              SassNumber step,
            )
            when !number.hasCompatibleUnits(step):
          _verifyCompatibleNumbers([number, step]);
          return SassCalculation._("round", [strategy, number, step]);
  
        case (
            SassString(text: 'nearest' || 'up' || 'down' || 'to-zero') &&
                var strategy,
            SassNumber number,
            SassNumber step,
          ):
          _verifyCompatibleNumbers([number, step]);
          return _roundWithStep(strategy.text, number, step);
  
        case (
            SassString(text: 'nearest' || 'up' || 'down' || 'to-zero') &&
                var strategy,
            SassString rest,
            null,
          ):
          return SassCalculation._("round", [strategy, rest]);
  
        case (
            SassString(text: 'nearest' || 'up' || 'down' || 'to-zero'),
            _?,
            null,
          ):
          throw SassScriptException("If strategy is not null, step is required.");
  
        case (
            SassString(text: 'nearest' || 'up' || 'down' || 'to-zero'),
            null,
            null,
          ):
          throw SassScriptException(
            "Number to round and step arguments are required.",
          );
  
        case (var number, null, null):
          return SassCalculation._("round", [number]);
  
        case (var number, var step?, null):
          return SassCalculation._("round", [number, step]);
  
        case (
            (SassString(text: 'nearest' || 'up' || 'down' || 'to-zero') ||
                    SassString(isVar: true)) &&
                var strategy,
            var number?,
            var step?,
          ):
          return SassCalculation._("round", [strategy, number, step]);
  
        case (_, _?, _?):
          throw SassScriptException(
            "$strategyOrNumber must be either nearest, up, down or to-zero.",
          );
  
        case (_, null, _?):
        // TODO(pamelalozano): Get rid of this case once dart-lang/sdk#52908 is solved.
        // ignore: unreachable_switch_case
        case (_, _, _):
          throw SassScriptException("Invalid parameters.");
      }
    }
  
    /// Creates an `calc-size()` calculation with the given [basis] and [value].
    ///
    /// The [basis] and [value] must be either a [SassNumber], a
    /// [SassCalculation], an unquoted [SassString], or a [CalculationOperation].
    ///
    /// This automatically simplifies the calculation. It throws an exception if
    /// it can determine that the calculation will definitely produce invalid CSS.
    static SassCalculation calcSize(Object basis, Object? value) {
      var args = [basis, if (value != null) value];
      _verifyLength(args, 2);
      basis = _simplify(basis);
      value = value.andThen(_simplify);
      return SassCalculation._("calc-size", [basis, if (value != null) value]);
    }
  
    /// Creates and simplifies a [CalculationOperation] with the given [operator],
    /// [left], and [right].
    ///
    /// This automatically simplifies the operation, so it may return a
    /// [SassNumber] rather than a [CalculationOperation].
    ///
    /// Each of [left] and [right] must be either a [SassNumber], a
    /// [SassCalculation], an unquoted [SassString], or a [CalculationOperation].
    static Object operate(
      CalculationOperator operator,
      Object left,
      Object right,
    ) =>
        operateInternal(
          operator,
          left,
          right,
          inLegacySassFunction: null,
          simplify: true,
          warn: null,
        );
  
    /// Like [operate], but with the internal-only [inLegacySassFunction] and
    /// [warn] parameters.
    ///
    /// If [inLegacySassFunction] isn't null, this allows unitless numbers to be
    /// added and subtracted with numbers with units, for backwards-compatibility
    /// with the old global `min()` and `max()` functions. This emits a
    /// deprecation warning using the string as the function's name.
    ///
    /// If [simplify] is `false`, no simplification will be done.
    ///
    /// The [warn] callback is used to surface deprecation warnings.
    ///
    /// @nodoc
    @internal
    static Object operateInternal(
      CalculationOperator operator,
      Object left,
      Object right, {
      required String? inLegacySassFunction,
      required bool simplify,
      required void Function(String message, [Deprecation? deprecation])? warn,
    }) {
      if (!simplify) return CalculationOperation._(operator, left, right);
      left = _simplify(left);
      right = _simplify(right);
  
      if (operator case CalculationOperator.plus || CalculationOperator.minus) {
        if (left is SassNumber && right is SassNumber) {
          var compatible = left.hasCompatibleUnits(right);
          if (!compatible &&
              inLegacySassFunction != null &&
              left.isComparableTo(right)) {
            warn!(
              "In future versions of Sass, $inLegacySassFunction() will be "
              "interpreted as the CSS $inLegacySassFunction() calculation. "
              "This doesn't allow unitless numbers to be mixed with numbers "
              "with units. If you want to use the Sass function, call "
              "math.$inLegacySassFunction() instead.\n"
              "\n"
              "See https://sass-lang.com/d/import",
              Deprecation.globalBuiltin,
            );
            compatible = true;
          }
          if (compatible) {
            return operator == CalculationOperator.plus
                ? left.plus(right)
                : left.minus(right);
          }
        }
  
        _verifyCompatibleNumbers([left, right]);
  
        if (right is SassNumber && number_lib.fuzzyLessThan(right.value, 0)) {
          right = right.times(SassNumber(-1));
          operator = operator == CalculationOperator.plus
              ? CalculationOperator.minus
              : CalculationOperator.plus;
        }
  
        return CalculationOperation._(operator, left, right);
      } else if (left is SassNumber && right is SassNumber) {
        return operator == CalculationOperator.times
            ? left.times(right)
            : left.dividedBy(right);
      } else {
        return CalculationOperation._(operator, left, right);
      }
    }
  
    /// An internal constructor that doesn't perform any validation or
    /// simplification.
    SassCalculation._(this.name, this.arguments);
  
    // Returns [value] coerced to [number]'s units.
    static SassNumber _matchUnits(double value, SassNumber number) =>
        SassNumber.withUnits(
          value,
          numeratorUnits: number.numeratorUnits,
          denominatorUnits: number.denominatorUnits,
        );
  
    /// Returns a rounded [number] based on a selected rounding [strategy],
    /// to the nearest integer multiple of [step].
    static SassNumber _roundWithStep(
      String strategy,
      SassNumber number,
      SassNumber step,
    ) {
      if (!{'nearest', 'up', 'down', 'to-zero'}.contains(strategy)) {
        throw ArgumentError(
          "$strategy must be either nearest, up, down or to-zero.",
        );
      }
  
      if (number.value.isInfinite && step.value.isInfinite ||
          step.value == 0 ||
          number.value.isNaN ||
          step.value.isNaN) {
        return _matchUnits(double.nan, number);
      }
      if (number.value.isInfinite) return number;
  
      if (step.value.isInfinite) {
        return switch ((strategy, number.value)) {
          (_, 0) => number,
          ('nearest' || 'to-zero', > 0) => _matchUnits(0.0, number),
          ('nearest' || 'to-zero', _) => _matchUnits(-0.0, number),
          ('up', > 0) => _matchUnits(double.infinity, number),
          ('up', _) => _matchUnits(-0.0, number),
          ('down', < 0) => _matchUnits(-double.infinity, number),
          ('down', _) => _matchUnits(0, number),
          (_, _) => throw UnsupportedError("Invalid argument: $strategy."),
        };
      }
  
      var stepWithNumberUnit = step.convertValueToMatch(number);
      return switch (strategy) {
        'nearest' => _matchUnits(
            (number.value / stepWithNumberUnit).round() * stepWithNumberUnit,
            number,
          ),
        'up' => _matchUnits(
            (step.value < 0
                    ? (number.value / stepWithNumberUnit).floor()
                    : (number.value / stepWithNumberUnit).ceil()) *
                stepWithNumberUnit,
            number,
          ),
        'down' => _matchUnits(
            (step.value < 0
                    ? (number.value / stepWithNumberUnit).ceil()
                    : (number.value / stepWithNumberUnit).floor()) *
                stepWithNumberUnit,
            number,
          ),
        'to-zero' => number.value < 0
            ? _matchUnits(
                (number.value / stepWithNumberUnit).ceil() * stepWithNumberUnit,
                number,
              )
            : _matchUnits(
                (number.value / stepWithNumberUnit).floor() * stepWithNumberUnit,
                number,
              ),
        _ => _matchUnits(double.nan, number),
      };
    }
  
    /// Returns an unmodifiable list of [args], with each argument simplified.
    static List<Object> _simplifyArguments(Iterable<Object> args) =>
        List.unmodifiable(args.map(_simplify));
  
    /// Simplifies a calculation argument.
    static Object _simplify(Object arg) => switch (arg) {
          SassNumber() || CalculationOperation() => arg,
          CalculationInterpolation() =>
            SassString('(${arg.value})', quotes: false),
          SassString(hasQuotes: false) => arg,
          SassString() => throw SassScriptException(
              "Quoted string $arg can't be used in a calculation.",
            ),
          SassCalculation(
            name: 'calc',
            arguments: [SassString(hasQuotes: false, :var text)],
          )
              when _needsParentheses(text) =>
            SassString('($text)', quotes: false),
          SassCalculation(name: 'calc', arguments: [var value]) => value,
          SassCalculation() => arg,
          Value() => throw SassScriptException(
              "Value $arg can't be used in a calculation."),
          _ => throw ArgumentError("Unexpected calculation argument $arg."),
        };
  
    /// Returns whether [text] needs parentheses if it's the contents of a
    /// `calc()` being embedded in another calculation.
    static bool _needsParentheses(String text) {
      var first = text.codeUnitAt(0);
      if (_charNeedsParentheses(first)) return true;
      var couldBeVar = text.length >= 4 && characterEqualsIgnoreCase(first, $v);
  
      if (text.length < 2) return false;
      var second = text.codeUnitAt(1);
      if (_charNeedsParentheses(second)) return true;
      couldBeVar = couldBeVar && characterEqualsIgnoreCase(second, $a);
  
      if (text.length < 3) return false;
      var third = text.codeUnitAt(2);
      if (_charNeedsParentheses(third)) return true;
      couldBeVar = couldBeVar && characterEqualsIgnoreCase(third, $r);
  
      if (text.length < 4) return false;
      var fourth = text.codeUnitAt(3);
      if (couldBeVar && fourth == $lparen) return true;
      if (_charNeedsParentheses(fourth)) return true;
  
      for (var i = 4; i < text.length; i++) {
        if (_charNeedsParentheses(text.codeUnitAt(i))) return true;
      }
      return false;
    }
  
    /// Returns whether [character] intrinsically needs parentheses if it appears
    /// in the unquoted string argument of a `calc()` being embedded in another
    /// calculation.
    static bool _charNeedsParentheses(int character) =>
        character.isWhitespace || character == $slash || character == $asterisk;
  
    /// Verifies that all the numbers in [args] aren't known to be incompatible
    /// with one another, and that they don't have units that are too complex for
    /// calculations.
    static void _verifyCompatibleNumbers(List<Object> args) {
      // Note: this logic is largely duplicated in
      // _EvaluateVisitor._verifyCompatibleNumbers and most changes here should
      // also be reflected there.
      for (var arg in args) {
        if (arg case SassNumber(hasComplexUnits: true)) {
          throw SassScriptException(
            "Number $arg isn't compatible with CSS calculations.",
          );
        }
      }
  
      for (var i = 0; i < args.length - 1; i++) {
        var number1 = args[i];
        if (number1 is! SassNumber) continue;
  
        for (var j = i + 1; j < args.length; j++) {
          var number2 = args[j];
          if (number2 is! SassNumber) continue;
          if (number1.hasPossiblyCompatibleUnits(number2)) continue;
          throw SassScriptException("$number1 and $number2 are incompatible.");
        }
      }
    }
  
    /// Throws a [SassScriptException] if [args] isn't [expectedLength] *and*
    /// doesn't contain a [SassString].
    static void _verifyLength(List<Object> args, int expectedLength) {
      if (args.length == expectedLength) return;
      if (args.any((arg) => arg is SassString)) {
        return;
      }
      throw SassScriptException(
        "$expectedLength arguments required, but only ${args.length} "
        "${pluralize('was', args.length, plural: 'were')} passed.",
      );
    }
  
    /// Returns a [Callable] named [name] that calls a single argument
    /// math function.
    ///
    /// If [forbidUnits] is `true` it will throw an error if [argument] has units.
    static Value _singleArgument(
      String name,
      Object argument,
      SassNumber mathFunc(SassNumber value), {
      bool forbidUnits = false,
    }) {
      argument = _simplify(argument);
      if (argument is! SassNumber) {
        return SassCalculation._(name, [argument]);
      }
      if (forbidUnits) argument.assertNoUnits();
      return mathFunc(argument);
    }
  
    /// @nodoc
    @internal
    T accept<T>(ValueVisitor<T> visitor) => visitor.visitCalculation(this);
  
    SassCalculation assertCalculation([String? name]) => this;
  
    /// @nodoc
    @internal
    Value plus(Value other) {
      if (other is SassString) return super.plus(other);
      throw SassScriptException('Undefined operation "$this + $other".');
    }
  
    /// @nodoc
    @internal
    Value minus(Value other) =>
        throw SassScriptException('Undefined operation "$this - $other".');
  
    /// @nodoc
    @internal
    Value unaryPlus() =>
        throw SassScriptException('Undefined operation "+$this".');
  
    /// @nodoc
    @internal
    Value unaryMinus() =>
        throw SassScriptException('Undefined operation "-$this".');
  
    bool operator ==(Object other) =>
        other is SassCalculation &&
        name == other.name &&
        listEquals(arguments, other.arguments);
  
    int get hashCode => name.hashCode ^ listHash(arguments);
  }
  
  /// A binary operation that can appear in a [SassCalculation].
  ///
  /// {@category Value}
  final class CalculationOperation {
    /// We use a getters to allow overriding the logic in the JS API
    /// implementation.
  
    /// The operator.
    CalculationOperator get operator => _operator;
    final CalculationOperator _operator;
  
    /// The left-hand operand.
    ///
    /// This is either a [SassNumber], a [SassCalculation], an unquoted
    /// [SassString], or a [CalculationOperation].
    Object get left => _left;
    final Object _left;
  
    /// The right-hand operand.
    ///
    /// This is either a [SassNumber], a [SassCalculation], an unquoted
    /// [SassString], or a [CalculationOperation].
    Object get right => _right;
    final Object _right;
  
    CalculationOperation._(this._operator, this._left, this._right);
  
    bool operator ==(Object other) =>
        other is CalculationOperation &&
        operator == other.operator &&
        left == other.left &&
        right == other.right;
  
    int get hashCode => operator.hashCode ^ left.hashCode ^ right.hashCode;
  
    String toString() {
      var parenthesized = serializeValue(
        SassCalculation._("", [this]),
        inspect: true,
      );
      return parenthesized.substring(1, parenthesized.length - 1);
    }
  }
  
  /// An enumeration of possible operators for [CalculationOperation].
  ///
  /// {@category Value}
  enum CalculationOperator {
    /// The addition operator.
    plus('plus', '+', 1),
  
    /// The subtraction operator.
    minus('minus', '-', 1),
  
    /// The multiplication operator.
    times('times', '*', 2),
  
    /// The division operator.
    dividedBy('divided by', '/', 2);
  
    /// The English name of `this`.
    final String name;
  
    /// The CSS syntax for `this`.
    final String operator;
  
    /// The precedence of `this`.
    ///
    /// An operator with higher precedence binds tighter.
    ///
    /// @nodoc
    @internal
    final int precedence;
  
    const CalculationOperator(this.name, this.operator, this.precedence);
  
    String toString() => name;
  }
  
  /// A deprecated representation of a string injected into a [SassCalculation]
  /// using interpolation.
  ///
  /// This only exists for backwards-compatibility with an older version of Dart
  /// Sass. It's now equivalent to creating a `SassString` whose value is wrapped
  /// in parentheses.
  ///
  /// {@category Value}
  @Deprecated("Use SassString instead.")
  @sealed
  class CalculationInterpolation {
    /// We use a getters to allow overriding the logic in the JS API
    /// implementation.
  
    String get value => _value;
    final String _value;
  
    CalculationInterpolation(this._value);
  
    bool operator ==(Object other) =>
        other is CalculationInterpolation && value == other.value;
  
    int get hashCode => value.hashCode;
  
    String toString() => value;
  }
  