// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_GEOMETRY_MATH_FUNCTIONS_H_
#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_GEOMETRY_MATH_FUNCTIONS_H_

#include <array>
#include <cfloat>
#include <cmath>
#include <optional>
#include <type_traits>
#include <utility>

#include "base/notreached.h"
#include "third_party/blink/renderer/platform/platform_export.h"
#include "third_party/blink/renderer/platform/wtf/math_extras.h"
#include "ui/gfx/geometry/sin_cos_degrees.h"

namespace blink {

namespace {

template <class ValueType>
std::pair<ValueType, ValueType> GetNearestMultiples(ValueType a, ValueType b) {
  using std::swap;
  bool is_negative = a < 0.0;
  a = std::abs(a);
  b = std::abs(b);
  // To get rid of rounding and range problems we use std::fmod
  // to get the lower to A multiple of B.
  ValueType c = -std::fmod(a, b);
  ValueType lower = a + c;
  // Sort a, b, c by increasing magnitude so that a + b + c later can
  // avoid rounding problems (e.g., if one number is very small compared to
  // other).
  if (std::abs(a) > std::abs(c)) {
    swap(a, c);
  }
  if (std::abs(a) > std::abs(b)) {
    swap(a, b);
  }
  if (std::abs(b) > std::abs(c)) {
    swap(b, c);
  }
  ValueType upper = a + b + c;
  if (is_negative) {
    swap(lower, upper);
    lower = -lower;
    upper = -upper;
  }
  return {lower, upper};
}

template <class OperatorType, typename ValueType>
std::optional<ValueType> PreCheckSteppedValueFunctionArguments(OperatorType op,
                                                               ValueType a,
                                                               ValueType b) {
  // In round(A, B), if B is 0, the result is NaN.
  // In mod(A, B) or rem(A, B), if B is 0, the result is NaN.
  // If A and B are both infinite, the result is NaN.
  if (b == 0.0 || (std::isinf(a) && std::isinf(b))) {
    return std::numeric_limits<ValueType>::quiet_NaN();
  }
  // If A is exactly equal to an integer multiple of B,
  // round() resolves to A exactly.
  // If A is infinite but B is finite, the result is the same infinity.
  if (OperatorType::kRoundNearest <= op && op <= OperatorType::kRoundToZero &&
      (std::fmod(a, b) == 0.0 || (std::isinf(a) && !std::isinf(b)))) {
    return a;
  }
  // In mod(A, B) or rem(A, B), if A is infinite, the result is NaN.
  if (OperatorType::kMod <= op && op <= OperatorType::kRem && std::isinf(a)) {
    return std::numeric_limits<ValueType>::quiet_NaN();
  }
  return {};
}

template <typename T>
  requires std::floating_point<T>
T TanDegrees(T degrees) {
  // Use table values for tan() if possible.
  // We pick a pretty arbitrary limit that should be safe.
  if (degrees > -90000000.0 && degrees < 90000000.0) {
    // Make sure 0, 45, 90, 135, 180, 225 and 270 degrees get exact results.
    T n45degrees = degrees / 45.0;
    int octant = static_cast<int>(n45degrees);
    if (octant == n45degrees) {
      constexpr std::array<T, 8> kTanN45 = {
          /* 0deg */ 0.0,
          /* 45deg */ 1.0,
          /* 90deg */ std::numeric_limits<T>::infinity(),
          /* 135deg */ -1.0,
          /* 180deg */ 0.0,
          /* 225deg */ 1.0,
          /* 270deg */ -std::numeric_limits<T>::infinity(),
          /* 315deg */ -1.0,
      };
      return kTanN45[octant & 7];
    }
  }
  // Slow path for non-table cases.
  T x = Deg2rad(degrees);
  return std::tan(x);
}

}  // namespace

template <class OperatorType, typename ValueType>
  requires std::is_enum_v<OperatorType> && std::floating_point<ValueType>
ValueType EvaluateTrigonometricFunction(
    OperatorType op,
    ValueType a,
    std::optional<ValueType>(b) = std::nullopt) {
  switch (op) {
    case OperatorType::kSin: {
      return gfx::SinCosDegrees(a).sin;
    }
    case OperatorType::kCos: {
      return gfx::SinCosDegrees(a).cos;
    }
    case OperatorType::kTan: {
      return TanDegrees(a);
    }
    case OperatorType::kAsin: {
      ValueType value = Rad2deg(std::asin(a));
      DCHECK(value >= -90 && value <= 90 || std::isnan(value));
      return value;
    }
    case OperatorType::kAcos: {
      ValueType value = Rad2deg(std::acos(a));
      DCHECK(value >= 0 && value <= 180 || std::isnan(value));
      return value;
    }
    case OperatorType::kAtan: {
      ValueType value = Rad2deg(std::atan(a));
      DCHECK(value >= -90 && value <= 90 || std::isnan(value));
      return value;
    }
    case OperatorType::kAtan2: {
      DCHECK(b.has_value());
      ValueType value = Rad2deg(std::atan2(a, b.value()));
      DCHECK(value >= -180 && value <= 180 || std::isnan(value));
      return value;
    }
    default:
      NOTREACHED();
  }
}

template <typename ValueType>
  requires std::floating_point<ValueType>
ValueType EvaluateRoundDownFunction(ValueType a, ValueType b) {
  auto [lower, _] = GetNearestMultiples(a, b);
  if (!std::isinf(a) && std::isinf(b)) {
    if (!a) {
      return a;
    } else {
      return std::signbit(a) ? -std::numeric_limits<ValueType>::infinity()
                             : +0.0;
    }
  } else {
    return lower;
  }
}

template <class OperatorType, typename ValueType>
  requires std::is_enum_v<OperatorType> && std::floating_point<ValueType>
ValueType EvaluateSteppedValueFunction(OperatorType op,
                                       ValueType a,
                                       ValueType b) {
  // https://drafts.csswg.org/css-values/#round-infinities
  std::optional<ValueType> pre_check =
      PreCheckSteppedValueFunctionArguments(op, a, b);
  if (pre_check.has_value()) {
    return pre_check.value();
  }
  // If A is finite but B is infinite, the result depends
  // on the <rounding-strategy> and the sign of A:
  // -- nearest, to-zero
  // If A is positive or +0.0, return +0.0. Otherwise, return -0.0.
  // -- up
  // If A is positive (not zero), return +∞. If A is +0.0, return +0.0.
  // Otherwise, return -0.0. down If A is negative (not zero), return −∞. If A
  // is -0.0, return -0.0. Otherwise, return +0.0. If A is infinite but B is
  // finite, the result is the same infinity.
  auto [lower, upper] = GetNearestMultiples(a, b);
  switch (op) {
    case OperatorType::kRoundNearest: {
      if (!std::isinf(a) && std::isinf(b)) {
        return std::signbit(a) ? -0.0 : +0.0;
      } else {
        // In the negative case we need to swap lower and upper for the nearest
        // rounding. This also means tie-breaking should pick the lower rather
        // than upper,
        const bool a_is_negative = a < 0.0;
        if (a_is_negative) {
          using std::swap;
          swap(lower, upper);
        }
        const ValueType distance = std::abs(std::fmod(a, b));
        const ValueType half_b = std::abs(b) / 2;
        if (distance < half_b || (a_is_negative && distance == half_b)) {
          return lower;
        } else {
          return upper;
        }
      }
    }
    case OperatorType::kRoundUp: {
      if (!std::isinf(a) && std::isinf(b)) {
        if (!a) {
          return a;
        } else {
          return std::signbit(a) ? -0.0
                                 : std::numeric_limits<ValueType>::infinity();
        }
      } else {
        return upper;
      }
    }
    case OperatorType::kRoundDown: {
      return EvaluateRoundDownFunction(a, b);
    }
    case OperatorType::kRoundToZero: {
      if (!std::isinf(a) && std::isinf(b)) {
        return std::signbit(a) ? -0.0 : +0.0;
      } else {
        return std::abs(upper) < std::abs(lower) ? upper : lower;
      }
    }
    case OperatorType::kMod: {
      // In mod(A, B) only, if B is infinite and A has opposite sign to B
      // (including an oppositely-signed zero), the result is NaN.
      if (std::isinf(b) && std::signbit(a) != std::signbit(b)) {
        return std::numeric_limits<ValueType>::quiet_NaN();
      }
      // If both arguments are positive or both are negative:
      // the value of the function is equal to the value of A
      // shifted by the integer multiple of B that brings
      // the value between zero and B.
      // If the A value and the B step are on opposite sides of zero:
      // mod() (short for “modulus”) continues to choose the integer
      // multiple of B that puts the value between zero and B.
      // std::fmod - the returned value has the same sign as A
      // and is less than B in magnitude.
      ValueType result = std::fmod(a, b);
      if (result == 0.0) {
        // If A is an exact multiple of B, the result should be zero with the
        // sign of B.
        return std::copysign(0.0, b);
      }
      if (std::signbit(result) != std::signbit(b)) {
        // If the result is on opposite side of zero from B,
        // put it between 0 and B. As the result of std::fmod is less
        // than B in magnitude, adding B would perform a correct shift.
        return result + b;
      }
      return result;
    }
    case OperatorType::kRem: {
      // If both arguments are positive or both are negative:
      // the value of the function is equal to the value of A
      // shifted by the integer multiple of B that brings
      // the value between zero and B.
      // If the A value and the B step are on opposite sides of zero:
      // rem() (short for "remainder") chooses the integer multiple of B
      // that puts the value between zero and -B,
      // avoiding changing the sign of the value.
      // std::fmod - the returned value has the same sign as A
      // and is less than B in magnitude.
      return std::fmod(a, b);
    }
    default:
      NOTREACHED();
  }
}

template <typename ValueType>
  requires std::floating_point<ValueType>
ValueType EvaluateSignFunction(ValueType v) {
  return (v == 0 || std::isnan(v)) ? v : ((v > 0) ? 1 : -1);
}

// https://drafts.csswg.org/css-values-5/#random-evaluation
PLATFORM_EXPORT double ComputeCSSRandomValue(double random_base_value,
                                             double min,
                                             double max,
                                             std::optional<double> step);

}  // namespace blink

#endif  // THIRD_PARTY_BLINK_RENDERER_PLATFORM_GEOMETRY_MATH_FUNCTIONS_H_
