//===-- MPCUtils.h ----------------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#ifndef LLVM_LIBC_UTILS_MPCWRAPPER_MPCUTILS_H
#define LLVM_LIBC_UTILS_MPCWRAPPER_MPCUTILS_H

#include "hdr/stdint_proxy.h"
#include "src/__support/CPP/type_traits.h"
#include "src/__support/complex_type.h"
#include "src/__support/macros/config.h"
#include "src/__support/macros/properties/complex_types.h"
#include "src/__support/macros/properties/types.h"
#include "test/UnitTest/RoundingModeUtils.h"
#include "test/UnitTest/Test.h"

namespace LIBC_NAMESPACE_DECL {
namespace testing {
namespace mpc {

enum class Operation {
  // Operations which take a single complex floating point number as input
  // and produce a single floating point number as output which has the same
  // floating point type as the real/imaginary part of the input.
  BeginUnaryOperationsSingleOutputDifferentOutputType,
  Carg,
  Cabs,
  EndUnaryOperationsSingleOutputDifferentOutputType,

  // Operations which take a single complex floating point number as input
  // and produce a single complex floating point number of the same kind
  // as output.
  BeginUnaryOperationsSingleOutputSameOutputType,
  Cproj,
  Csqrt,
  Clog,
  Cexp,
  Csinh,
  Ccosh,
  Ctanh,
  Casinh,
  Cacosh,
  Catanh,
  Csin,
  Ccos,
  Ctan,
  Casin,
  Cacos,
  Catan,
  EndUnaryOperationsSingleOutputSameOutputType,

  // Operations which take two complex floating point numbers as input
  // and produce a single complex floating point number of the same kind
  // as output.
  BeginBinaryOperationsSingleOutput,
  Cpow,
  EndBinaryOperationsSingleOutput,
};

using LIBC_NAMESPACE::fputil::testing::RoundingMode;

template <typename T> struct BinaryInput {
  static_assert(LIBC_NAMESPACE::cpp::is_complex_v<T>,
                "Template parameter of BinaryInput must be a complex floating "
                "point type.");

  using Type = T;
  T x, y;
};

namespace internal {

template <typename InputType, typename OutputType>
bool compare_unary_operation_single_output_same_type(Operation op,
                                                     InputType input,
                                                     OutputType libc_output,
                                                     double ulp_tolerance,
                                                     RoundingMode rounding);

template <typename InputType, typename OutputType>
bool compare_unary_operation_single_output_different_type(
    Operation op, InputType input, OutputType libc_output, double ulp_tolerance,
    RoundingMode rounding);

template <typename InputType, typename OutputType>
bool compare_binary_operation_one_output(Operation op,
                                         const BinaryInput<InputType> &input,
                                         OutputType libc_output,
                                         double ulp_tolerance,
                                         RoundingMode rounding);

template <typename InputType, typename OutputType>
void explain_unary_operation_single_output_same_type_error(
    Operation op, InputType input, OutputType match_value, double ulp_tolerance,
    RoundingMode rounding);

template <typename InputType, typename OutputType>
void explain_unary_operation_single_output_different_type_error(
    Operation op, InputType input, OutputType match_value, double ulp_tolerance,
    RoundingMode rounding);

template <typename InputType, typename OutputType>
void explain_binary_operation_one_output_error(
    Operation op, const BinaryInput<InputType> &input, OutputType match_value,
    double ulp_tolerance, RoundingMode rounding);

template <Operation op, typename InputType, typename OutputType>
class MPCMatcher : public testing::Matcher<OutputType> {
private:
  InputType input;
  OutputType match_value;
  double ulp_tolerance;
  RoundingMode rounding;

public:
  MPCMatcher(InputType testInput, double ulp_tolerance, RoundingMode rounding)
      : input(testInput), ulp_tolerance(ulp_tolerance), rounding(rounding) {}

  bool match(OutputType libcResult) {
    match_value = libcResult;
    return match(input, match_value);
  }

  void explainError() override { // NOLINT
    explain_error(input, match_value);
  }

private:
  template <typename InType, typename OutType>
  bool match(InType in, OutType out) {
    if (cpp::is_same_v<InType, OutType>) {
      return compare_unary_operation_single_output_same_type(
          op, in, out, ulp_tolerance, rounding);
    } else {
      return compare_unary_operation_single_output_different_type(
          op, in, out, ulp_tolerance, rounding);
    }
  }

  template <typename T, typename U>
  bool match(const BinaryInput<T> &in, U out) {
    return compare_binary_operation_one_output(op, in, out, ulp_tolerance,
                                               rounding);
  }

  template <typename InType, typename OutType>
  void explain_error(InType in, OutType out) {
    if (cpp::is_same_v<InType, OutType>) {
      explain_unary_operation_single_output_same_type_error(
          op, in, out, ulp_tolerance, rounding);
    } else {
      explain_unary_operation_single_output_different_type_error(
          op, in, out, ulp_tolerance, rounding);
    }
  }

  template <typename T, typename U>
  void explain_error(const BinaryInput<T> &in, U out) {
    explain_binary_operation_one_output_error(op, in, out, ulp_tolerance,
                                              rounding);
  }
};

} // namespace internal

// Return true if the input and ouput types for the operation op are valid
// types.
template <Operation op, typename InputType, typename OutputType>
constexpr bool is_valid_operation() {
  return (Operation::BeginBinaryOperationsSingleOutput < op &&
          op < Operation::EndBinaryOperationsSingleOutput &&
          cpp::is_complex_type_same<InputType, OutputType>() &&
          cpp::is_complex_v<InputType>) ||
         (Operation::BeginUnaryOperationsSingleOutputSameOutputType < op &&
          op < Operation::EndUnaryOperationsSingleOutputSameOutputType &&
          cpp::is_complex_type_same<InputType, OutputType>() &&
          cpp::is_complex_v<InputType>) ||
         (Operation::BeginUnaryOperationsSingleOutputDifferentOutputType < op &&
          op < Operation::EndUnaryOperationsSingleOutputDifferentOutputType &&
          cpp::is_same_v<make_real_t<InputType>, OutputType> &&
          cpp::is_complex_v<InputType>);
}

template <Operation op, typename InputType, typename OutputType>
cpp::enable_if_t<is_valid_operation<op, InputType, OutputType>(),
                 internal::MPCMatcher<op, InputType, OutputType>>
get_mpc_matcher(InputType input, [[maybe_unused]] OutputType output,
                double ulp_tolerance, RoundingMode rounding) {
  return internal::MPCMatcher<op, InputType, OutputType>(input, ulp_tolerance,
                                                         rounding);
}

} // namespace mpc
} // namespace testing
} // namespace LIBC_NAMESPACE_DECL

#define EXPECT_MPC_MATCH_DEFAULT(op, input, match_value, ulp_tolerance)        \
  EXPECT_THAT(match_value,                                                     \
              LIBC_NAMESPACE::testing::mpc::get_mpc_matcher<op>(               \
                  input, match_value, ulp_tolerance,                           \
                  LIBC_NAMESPACE::fputil::testing::RoundingMode::Nearest))

#define EXPECT_MPC_MATCH_ROUNDING(op, input, match_value, ulp_tolerance,       \
                                  rounding)                                    \
  EXPECT_THAT(match_value, LIBC_NAMESPACE::testing::mpc::get_mpc_matcher<op>(  \
                               input, match_value, ulp_tolerance, rounding))

#define EXPECT_MPC_MATCH_ALL_ROUNDING_HELPER(op, input, match_value,           \
                                             ulp_tolerance, rounding)          \
  {                                                                            \
    MPCRND::ForceRoundingMode __r(rounding);                                   \
    if (__r.success) {                                                         \
      EXPECT_MPC_MATCH_ROUNDING(op, input, match_value, ulp_tolerance,         \
                                rounding);                                     \
    }                                                                          \
  }

#define EXPECT_MPC_MATCH_ALL_ROUNDING(op, input, match_value, ulp_tolerance)   \
  {                                                                            \
    namespace MPCRND = LIBC_NAMESPACE::fputil::testing;                        \
    for (int i = 0; i < 4; i++) {                                              \
      MPCRND::RoundingMode r_mode = static_cast<MPCRND::RoundingMode>(i);      \
      EXPECT_MPC_MATCH_ALL_ROUNDING_HELPER(op, input, match_value,             \
                                           ulp_tolerance, r_mode);             \
    }                                                                          \
  }

#define TEST_MPC_MATCH_ROUNDING(op, input, match_value, ulp_tolerance,         \
                                rounding)                                      \
  LIBC_NAMESPACE::testing::mpc::get_mpc_matcher<op>(input, match_value,        \
                                                    ulp_tolerance, rounding)   \
      .match(match_value)

#define ASSERT_MPC_MATCH_DEFAULT(op, input, match_value, ulp_tolerance)        \
  ASSERT_THAT(match_value,                                                     \
              LIBC_NAMESPACE::testing::mpc::get_mpc_matcher<op>(               \
                  input, match_value, ulp_tolerance,                           \
                  LIBC_NAMESPACE::fputil::testing::RoundingMode::Nearest))

#define ASSERT_MPC_MATCH_ROUNDING(op, input, match_value, ulp_tolerance,       \
                                  rounding)                                    \
  ASSERT_THAT(match_value, LIBC_NAMESPACE::testing::mpc::get_mpc_matcher<op>(  \
                               input, match_value, ulp_tolerance, rounding))

#define ASSERT_MPC_MATCH_ALL_ROUNDING_HELPER(op, input, match_value,           \
                                             ulp_tolerance, rounding)          \
  {                                                                            \
    MPCRND::ForceRoundingMode __r(rounding);                                   \
    if (__r.success) {                                                         \
      ASSERT_MPC_MATCH_ROUNDING(op, input, match_value, ulp_tolerance,         \
                                rounding);                                     \
    }                                                                          \
  }

#define ASSERT_MPC_MATCH_ALL_ROUNDING(op, input, match_value, ulp_tolerance)   \
  {                                                                            \
    namespace MPCRND = LIBC_NAMESPACE::fputil::testing;                        \
    for (int i = 0; i < 4; i++) {                                              \
      MPCRND::RoundingMode r_mode = static_cast<MPCRND::RoundingMode>(i);      \
      ASSERT_MPC_MATCH_ALL_ROUNDING_HELPER(op, input, match_value,             \
                                           ulp_tolerance, r_mode);             \
    }                                                                          \
  }

#endif // LLVM_LIBC_UTILS_MPCWRAPPER_MPCUTILS_H
