// Copyright 2019 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:collection';
import 'dart:math' as math;

import 'package:collection/collection.dart';

import '../callable.dart';
import '../deprecation.dart';
import '../evaluation_context.dart';
import '../exception.dart';
import '../module/built_in.dart';
import '../util/number.dart';
import '../value.dart';

/// The global definitions of Sass math functions.
final global = UnmodifiableListView([
  _function("abs", r"$number", (arguments) {
    var number = arguments[0].assertNumber("number");
    if (number.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($number)"
        "\n"
        "To emit a CSS abs() now: abs(#{$number})\n"
        "More info: https://sass-lang.com/d/abs-percent",
        Deprecation.absPercent,
      );
    } else {
      warnForGlobalBuiltIn('math', 'abs');
    }
    return SassNumber.withUnits(
      number.value.abs(),
      numeratorUnits: number.numeratorUnits,
      denominatorUnits: number.denominatorUnits,
    );
  }),
  _ceil.withDeprecationWarning('math'),
  _floor.withDeprecationWarning('math'),
  _max.withDeprecationWarning('math'),
  _min.withDeprecationWarning('math'),
  _percentage.withDeprecationWarning('math'),
  _randomFunction.withDeprecationWarning('math'),
  _round.withDeprecationWarning('math'),
  _unit.withDeprecationWarning('math'),
  _compatible.withDeprecationWarning('math').withName("comparable"),
  _isUnitless.withDeprecationWarning('math').withName("unitless"),
]);

/// The Sass math module.
final module = BuiltInModule(
  "math",
  functions: <Callable>[
    _numberFunction("abs", (value) => value.abs()),
    _acos, _asin, _atan, _atan2, _ceil, _clamp, _cos, _compatible, _floor, //
    _hypot, _isUnitless, _log, _max, _min, _percentage, _pow, _randomFunction,
    _round, _sin, _sqrt, _tan, _unit, _div,
  ],
  variables: {
    "e": SassNumber(math.e),
    "pi": SassNumber(math.pi),
    "epsilon": SassNumber(2.220446049250313e-16),
    "max-safe-integer": SassNumber(9007199254740991),
    "min-safe-integer": SassNumber(-9007199254740991),
    "max-number": SassNumber(double.maxFinite),
    "min-number": SassNumber(double.minPositive),
  },
);

///
/// Bounding functions
///

final _ceil = _numberFunction("ceil", (value) => value.ceil().toDouble());

final _clamp = _function("clamp", r"$min, $number, $max", (arguments) {
  var min = arguments[0].assertNumber("min");
  var number = arguments[1].assertNumber("number");
  var max = arguments[2].assertNumber("max");

  // Even though we don't use the resulting values, `convertValueToMatch`
  // generates more user-friendly exceptions than [greaterThanOrEquals] since it
  // has more context about parameter names.
  number.convertValueToMatch(min, "number", "min");
  max.convertValueToMatch(min, "max", "min");

  if (min.greaterThanOrEquals(max).isTruthy) return min;
  if (min.greaterThanOrEquals(number).isTruthy) return min;
  if (number.greaterThanOrEquals(max).isTruthy) return max;
  return number;
});

final _floor = _numberFunction("floor", (value) => value.floor().toDouble());

final _max = _function("max", r"$numbers...", (arguments) {
  SassNumber? max;
  for (var value in arguments[0].asList) {
    var number = value.assertNumber();
    if (max == null || max.lessThan(number).isTruthy) max = number;
  }
  if (max != null) return max;
  throw SassScriptException("At least one argument must be passed.");
});

final _min = _function("min", r"$numbers...", (arguments) {
  SassNumber? min;
  for (var value in arguments[0].asList) {
    var number = value.assertNumber();
    if (min == null || min.greaterThan(number).isTruthy) min = number;
  }
  if (min != null) return min;
  throw SassScriptException("At least one argument must be passed.");
});

final _round = _numberFunction("round", (number) => number.round().toDouble());

///
/// Distance functions
///

final _hypot = _function("hypot", r"$numbers...", (arguments) {
  var numbers =
      arguments[0].asList.map((argument) => argument.assertNumber()).toList();
  if (numbers.isEmpty) {
    throw SassScriptException("At least one argument must be passed.");
  }

  var subtotal = 0.0;
  for (var i = 0; i < numbers.length; i++) {
    var number = numbers[i];
    var value = number.convertValueToMatch(
      numbers[0],
      "numbers[${i + 1}]",
      "numbers[1]",
    );
    subtotal += math.pow(value, 2);
  }
  return SassNumber.withUnits(
    math.sqrt(subtotal),
    numeratorUnits: numbers[0].numeratorUnits,
    denominatorUnits: numbers[0].denominatorUnits,
  );
});

///
/// Exponential functions
///

final _log = _function("log", r"$number, $base: null", (arguments) {
  var number = arguments[0].assertNumber("number");
  if (number.hasUnits) {
    throw SassScriptException("\$number: Expected $number to have no units.");
  } else if (arguments[1] == sassNull) {
    return SassNumber(math.log(number.value));
  }

  var base = arguments[1].assertNumber("base");
  if (base.hasUnits) {
    throw SassScriptException("\$base: Expected $base to have no units.");
  } else {
    return SassNumber(math.log(number.value) / math.log(base.value));
  }
});

final _pow = _function("pow", r"$base, $exponent", (arguments) {
  var base = arguments[0].assertNumber("base");
  var exponent = arguments[1].assertNumber("exponent");
  return pow(base, exponent);
});

final _sqrt = _singleArgumentMathFunc("sqrt", sqrt);

///
/// Trigonometric functions
///

final _acos = _singleArgumentMathFunc("acos", acos);

final _asin = _singleArgumentMathFunc("asin", asin);

final _atan = _singleArgumentMathFunc("atan", atan);

final _atan2 = _function("atan2", r"$y, $x", (arguments) {
  var y = arguments[0].assertNumber("y");
  var x = arguments[1].assertNumber("x");
  return atan2(y, x);
});

final _cos = _singleArgumentMathFunc("cos", cos);

final _sin = _singleArgumentMathFunc("sin", sin);

final _tan = _singleArgumentMathFunc("tan", tan);

///
/// Unit functions
///

final _compatible = _function("compatible", r"$number1, $number2", (arguments) {
  var number1 = arguments[0].assertNumber("number1");
  var number2 = arguments[1].assertNumber("number2");
  return SassBoolean(number1.isComparableTo(number2));
});

final _isUnitless = _function("is-unitless", r"$number", (arguments) {
  var number = arguments[0].assertNumber("number");
  return SassBoolean(!number.hasUnits);
});

final _unit = _function("unit", r"$number", (arguments) {
  var number = arguments[0].assertNumber("number");
  return SassString(number.unitString, quotes: true);
});

///
/// Other functions
///

final _percentage = _function("percentage", r"$number", (arguments) {
  var number = arguments[0].assertNumber("number");
  number.assertNoUnits("number");
  return SassNumber(number.value * 100, '%');
});

final _random = math.Random();

final _randomFunction = _function("random", r"$limit: null", (arguments) {
  if (arguments[0] == sassNull) return SassNumber(_random.nextDouble());
  var limit = arguments[0].assertNumber("limit");

  if (limit.hasUnits) {
    warnForDeprecation(
      "math.random() will no longer ignore \$limit units ($limit) in a "
      "future release.\n"
      "\n"
      "Recommendation: "
      "math.random(math.div(\$limit, 1${limit.unitString})) * 1${limit.unitString}\n"
      "\n"
      "To preserve current behavior: "
      "math.random(math.div(\$limit, 1${limit.unitString}))\n"
      "\n"
      "More info: https://sass-lang.com/d/function-units",
      Deprecation.functionUnits,
    );
  }

  var limitScalar = limit.assertInt("limit");
  if (limitScalar < 1) {
    throw SassScriptException("\$limit: Must be greater than 0, was $limit.");
  }
  return SassNumber(_random.nextInt(limitScalar) + 1);
});

final _div = _function("div", r"$number1, $number2", (arguments) {
  var number1 = arguments[0];
  var number2 = arguments[1];

  if (number1 is! SassNumber || number2 is! SassNumber) {
    warn(
      "math.div() will only support number arguments in a future release.\n"
      "Use list.slash() instead for a slash separator.",
    );
  }

  return number1.dividedBy(number2);
});

///
/// Helpers
///

/// Returns a [Callable] named [name] that calls a single argument
/// math function.
BuiltInCallable _singleArgumentMathFunc(
  String name,
  SassNumber mathFunc(SassNumber value),
) {
  return _function(name, r"$number", (arguments) {
    var number = arguments[0].assertNumber("number");
    return mathFunc(number);
  });
}

/// Returns a [Callable] named [name] that transforms a number's value
/// using [transform] and preserves its units.
BuiltInCallable _numberFunction(String name, double transform(double value)) {
  return _function(name, r"$number", (arguments) {
    var number = arguments[0].assertNumber("number");
    return SassNumber.withUnits(
      transform(number.value),
      numeratorUnits: number.numeratorUnits,
      denominatorUnits: number.denominatorUnits,
    );
  });
}

/// Like [_function.function], but always sets the URL to `sass:math`.
BuiltInCallable _function(
  String name,
  String arguments,
  Value callback(List<Value> arguments),
) =>
    BuiltInCallable.function(name, arguments, callback, url: "sass:math");
