// 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 'dart:math' as math;

import '../value.dart';

/// The power of ten to which to round Sass numbers to determine if they're
/// [fuzzy equal] to one another
///
/// [fuzzy-equal]: https://github.com/sass/sass/blob/main/spec/types/number.md#fuzzy-equality
///
/// This is also the minimum distance such that `a - b > _epsilon` implies that
/// `a` isn't [fuzzy-equal] to `b`. Note that the inverse implication is not
/// necessarily true! For example, if `a = 5.1e-11` and `b = 4.4e-11`, then
/// `a - b < 1e-11` but `a` fuzzy-equals 5e-11 and b fuzzy-equals 4e-11.
final _epsilon = math.pow(10, -SassNumber.precision - 1);

/// `1 / _epsilon`, cached since [math.pow] may not be computed at compile-time
/// and thus this probably won't be constant-folded.
final _inverseEpsilon = math.pow(10, SassNumber.precision + 1);

/// Returns whether [number1] and [number2] are equal up to the 11th decimal
/// digit.
bool fuzzyEquals(num number1, num number2) {
  if (number1 == number2) return true;
  return (number1 - number2).abs() <= _epsilon &&
      (number1 * _inverseEpsilon).round() ==
          (number2 * _inverseEpsilon).round();
}

/// Like [fuzzyEquals], but allows null values for [number1] and [number2].
///
/// null values are only equal to one another.
bool fuzzyEqualsNullable(num? number1, num? number2) {
  if (number1 == number2) return true;
  if (number1 == null || number2 == null) return false;
  return (number1 - number2).abs() <= _epsilon &&
      (number1 * _inverseEpsilon).round() ==
          (number2 * _inverseEpsilon).round();
}

/// Returns a hash code for [number] that matches [fuzzyEquals].
int fuzzyHashCode(double number) {
  if (!number.isFinite) return number.hashCode;
  return (number * _inverseEpsilon).round().hashCode;
}

/// Returns whether [number1] is less than [number2], and not [fuzzyEquals].
bool fuzzyLessThan(num number1, num number2) =>
    number1 < number2 && !fuzzyEquals(number1, number2);

/// Returns whether [number1] is less than [number2], or [fuzzyEquals].
bool fuzzyLessThanOrEquals(num number1, num number2) =>
    number1 < number2 || fuzzyEquals(number1, number2);

/// Returns whether [number1] is greater than [number2], and not [fuzzyEquals].
bool fuzzyGreaterThan(num number1, num number2) =>
    number1 > number2 && !fuzzyEquals(number1, number2);

/// Returns whether [number1] is greater than [number2], or [fuzzyEquals].
bool fuzzyGreaterThanOrEquals(num number1, num number2) =>
    number1 > number2 || fuzzyEquals(number1, number2);

/// Returns whether [number] is [fuzzyEquals] to an integer.
bool fuzzyIsInt(double number) {
  if (number.isInfinite || number.isNaN) return false;
  return fuzzyEquals(number, number.round());
}

/// If [number] is an integer according to [fuzzyIsInt], returns it as an
/// [int].
///
/// Otherwise, returns `null`.
int? fuzzyAsInt(double number) {
  if (number.isInfinite || number.isNaN) return null;
  var rounded = number.round();
  return fuzzyEquals(number, rounded) ? rounded : null;
}

/// Rounds [number] to the nearest integer.
///
/// This rounds up numbers that are [fuzzyEquals] to `X.5`.
int fuzzyRound(num number) {
  // If the number is within epsilon of X.5, round up (or down for negative
  // numbers).
  if (number > 0) {
    return fuzzyLessThan(number % 1, 0.5) ? number.floor() : number.ceil();
  } else {
    return fuzzyLessThanOrEquals(number % 1, 0.5)
        ? number.floor()
        : number.ceil();
  }
}

/// Returns whether [number] is within [min] and [max] inclusive, using fuzzy
/// equality.
bool fuzzyInRange(double number, num min, num max) =>
    fuzzyGreaterThanOrEquals(number, min) && fuzzyLessThanOrEquals(number, max);

/// Returns [number] if it's within [min] and [max], or `null` if it's not.
///
/// If [number] is [fuzzyEquals] to [min] or [max], it's clamped to the
/// appropriate value.
double? fuzzyCheckRange(double number, num min, num max) {
  if (fuzzyEquals(number, min)) return min.toDouble();
  if (fuzzyEquals(number, max)) return max.toDouble();
  if (number > min && number < max) return number;
  return null;
}

/// Throws a [RangeError] if [number] isn't within [min] and [max].
///
/// If [number] is [fuzzyEquals] to [min] or [max], it's clamped to the
/// appropriate value. [name] is used in error reporting.
double fuzzyAssertRange(double number, int min, int max, [String? name]) {
  var result = fuzzyCheckRange(number, min, max);
  if (result != null) return result;
  throw RangeError.range(
    number,
    min,
    max,
    name,
    "must be between $min and $max",
  );
}

/// Return [num1] modulo [num2], using Sass's [floored division] modulo
/// semantics, which it inherited from Ruby and which differ from Dart's.
///
/// [floored division]: https://en.wikipedia.org/wiki/Modulo_operation#Variants_of_the_definition
double moduloLikeSass(double num1, double num2) {
  if (num1.isInfinite) return double.nan;
  if (num2.isInfinite) {
    return num1.signIncludingZero == num2.sign ? num1 : double.nan;
  }

  if (num2 > 0) return num1 % num2;
  if (num2 == 0) return double.nan;

  // Dart has different mod-negative semantics than Ruby, and thus than
  // Sass.
  var result = num1 % num2;
  return result == 0 ? 0 : result + num2;
}

//// Returns [num] clamped between [lowerBound] and [upperBound], with `NaN`
//// preferring the lower bound (unlike Dart for which it prefers the upper
//// bound).
double clampLikeCss(double number, double lowerBound, double upperBound) =>
    number.isNaN ? lowerBound : number.clamp(lowerBound, upperBound);

/// Returns the square root of [number].
SassNumber sqrt(SassNumber number) {
  number.assertNoUnits("number");
  return SassNumber(math.sqrt(number.value));
}

/// Returns the sine of [number].
SassNumber sin(SassNumber number) =>
    SassNumber(math.sin(number.coerceValueToUnit("rad", "number")));

/// Returns the cosine of [number].
SassNumber cos(SassNumber number) =>
    SassNumber(math.cos(number.coerceValueToUnit("rad", "number")));

/// Returns the tangent of [number].
SassNumber tan(SassNumber number) =>
    SassNumber(math.tan(number.coerceValueToUnit("rad", "number")));

/// Returns the arctangent of [number].
SassNumber atan(SassNumber number) {
  number.assertNoUnits("number");
  return _radiansToDegrees(math.atan(number.value));
}

/// Returns the arcsine of [number].
SassNumber asin(SassNumber number) {
  number.assertNoUnits("number");
  return _radiansToDegrees(math.asin(number.value));
}

/// Returns the arccosine of [number]
SassNumber acos(SassNumber number) {
  number.assertNoUnits("number");
  return _radiansToDegrees(math.acos(number.value));
}

/// Returns the absolute value of [number].
SassNumber abs(SassNumber number) =>
    SassNumber(number.value.abs()).coerceToMatch(number);

/// Returns the logarithm of [number] with respect to [base].
SassNumber log(SassNumber number, SassNumber? base) {
  if (base != null) {
    return SassNumber(math.log(number.value) / math.log(base.value));
  }
  return SassNumber(math.log(number.value));
}

/// Returns the value of [base] raised to the power of [exponent].
SassNumber pow(SassNumber base, SassNumber exponent) {
  base.assertNoUnits("base");
  exponent.assertNoUnits("exponent");
  return SassNumber(math.pow(base.value, exponent.value));
}

/// Returns the arctangent for [y] and [x].
SassNumber atan2(SassNumber y, SassNumber x) =>
    _radiansToDegrees(math.atan2(y.value, x.convertValueToMatch(y, 'x', 'y')));

/// Returns [radians] as a [SassNumber] with unit `deg`.
SassNumber _radiansToDegrees(double radians) =>
    SassNumber.withUnits(radians * (180 / math.pi), numeratorUnits: ['deg']);

/// Extension methods to get the sign of the double's numerical value,
/// including positive and negative zero.
extension DoubleWithSignedZero on double {
  double get signIncludingZero {
    if (identical(this, -0.0)) return -1.0;
    if (this == 0) return 1.0;
    return sign;
  }
}
