//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

#include <cstdint>
#include <functional>
#include <memory>
#include <string>

#include "CartesianBenchmarks.hpp"
#include "benchmark/benchmark.h"
#include "test_macros.h"

namespace {

enum class FunctionType {
  Null,
  FunctionPointer,
  MemberFunctionPointer,
  MemberPointer,
  SmallTrivialFunctor,
  SmallNonTrivialFunctor,
  LargeTrivialFunctor,
  LargeNonTrivialFunctor
};

struct AllFunctionTypes : EnumValuesAsTuple<AllFunctionTypes, FunctionType, 8> {
  static constexpr const char* Names[] = {"Null",
                                          "FuncPtr",
                                          "MemFuncPtr",
                                          "MemPtr",
                                          "SmallTrivialFunctor",
                                          "SmallNonTrivialFunctor",
                                          "LargeTrivialFunctor",
                                          "LargeNonTrivialFunctor"};
};

enum class Opacity { kOpaque, kTransparent };

struct AllOpacity : EnumValuesAsTuple<AllOpacity, Opacity, 2> {
  static constexpr const char* Names[] = {"Opaque", "Transparent"};
};

struct S {
  int function() const { return 0; }
  int field = 0;
};

int FunctionWithS(const S*) { return 0; }

struct SmallTrivialFunctor {
  int operator()(const S*) const { return 0; }
};
struct SmallNonTrivialFunctor {
  SmallNonTrivialFunctor() {}
  SmallNonTrivialFunctor(const SmallNonTrivialFunctor&) {}
  ~SmallNonTrivialFunctor() {}
  int operator()(const S*) const { return 0; }
};
struct LargeTrivialFunctor {
  LargeTrivialFunctor() {
      // Do not spend time initializing the padding.
  }
  int padding[16];
  int operator()(const S*) const { return 0; }
};
struct LargeNonTrivialFunctor {
  int padding[16];
  LargeNonTrivialFunctor() {
      // Do not spend time initializing the padding.
  }
  LargeNonTrivialFunctor(const LargeNonTrivialFunctor&) {}
  ~LargeNonTrivialFunctor() {}
  int operator()(const S*) const { return 0; }
};

using Function = std::function<int(const S*)>;

TEST_ALWAYS_INLINE
inline Function MakeFunction(FunctionType type, bool opaque = false) {
  switch (type) {
    case FunctionType::Null:
      return nullptr;
    case FunctionType::FunctionPointer:
      return maybeOpaque(FunctionWithS, opaque);
    case FunctionType::MemberFunctionPointer:
      return maybeOpaque(&S::function, opaque);
    case FunctionType::MemberPointer:
      return maybeOpaque(&S::field, opaque);
    case FunctionType::SmallTrivialFunctor:
      return maybeOpaque(SmallTrivialFunctor{}, opaque);
    case FunctionType::SmallNonTrivialFunctor:
      return maybeOpaque(SmallNonTrivialFunctor{}, opaque);
    case FunctionType::LargeTrivialFunctor:
      return maybeOpaque(LargeTrivialFunctor{}, opaque);
    case FunctionType::LargeNonTrivialFunctor:
      return maybeOpaque(LargeNonTrivialFunctor{}, opaque);
  }
}

template <class Opacity, class FunctionType>
struct ConstructAndDestroy {
  static void run(benchmark::State& state) {
    for (auto _ : state) {
      if (Opacity() == ::Opacity::kOpaque) {
        benchmark::DoNotOptimize(MakeFunction(FunctionType(), true));
      } else {
        MakeFunction(FunctionType());
      }
    }
  }

  static std::string name() {
    return "BM_ConstructAndDestroy" + FunctionType::name() + Opacity::name();
  }
};

template <class FunctionType>
struct Copy {
  static void run(benchmark::State& state) {
    auto value = MakeFunction(FunctionType());
    for (auto _ : state) {
      benchmark::DoNotOptimize(value);
      auto copy = value;  // NOLINT
      benchmark::DoNotOptimize(copy);
    }
  }

  static std::string name() { return "BM_Copy" + FunctionType::name(); }
};

template <class FunctionType>
struct Move {
  static void run(benchmark::State& state) {
    Function values[2] = {MakeFunction(FunctionType())};
    int i = 0;
    for (auto _ : state) {
      benchmark::DoNotOptimize(values);
      benchmark::DoNotOptimize(values[i ^ 1] = std::move(values[i]));
      i ^= 1;
    }
  }

  static std::string name() {
    return "BM_Move" + FunctionType::name();
  }
};

template <class Function1, class Function2>
struct Swap {
  static void run(benchmark::State& state) {
    Function values[2] = {MakeFunction(Function1()), MakeFunction(Function2())};
    for (auto _ : state) {
      benchmark::DoNotOptimize(values);
      values[0].swap(values[1]);
    }
  }

  static bool skip() { return Function1() > Function2(); }

  static std::string name() {
    return "BM_Swap" + Function1::name() + Function2::name();
  }
};

template <class FunctionType>
struct OperatorBool {
  static void run(benchmark::State& state) {
    auto f = MakeFunction(FunctionType());
    for (auto _ : state) {
      benchmark::DoNotOptimize(f);
      benchmark::DoNotOptimize(static_cast<bool>(f));
    }
  }

  static std::string name() { return "BM_OperatorBool" + FunctionType::name(); }
};

template <class FunctionType>
struct Invoke {
  static void run(benchmark::State& state) {
    S s;
    const auto value = MakeFunction(FunctionType());
    for (auto _ : state) {
      benchmark::DoNotOptimize(value);
      benchmark::DoNotOptimize(value(&s));
    }
  }

  static bool skip() { return FunctionType() == ::FunctionType::Null; }

  static std::string name() { return "BM_Invoke" + FunctionType::name(); }
};

template <class FunctionType>
struct InvokeInlined {
  static void run(benchmark::State& state) {
    S s;
    for (auto _ : state) {
      MakeFunction(FunctionType())(&s);
    }
  }

  static bool skip() { return FunctionType() == ::FunctionType::Null; }

  static std::string name() {
    return "BM_InvokeInlined" + FunctionType::name();
  }
};

}  // namespace

int main(int argc, char** argv) {
  benchmark::Initialize(&argc, argv);
  if (benchmark::ReportUnrecognizedArguments(argc, argv))
    return 1;

  makeCartesianProductBenchmark<ConstructAndDestroy, AllOpacity,
                                AllFunctionTypes>();
  makeCartesianProductBenchmark<Copy, AllFunctionTypes>();
  makeCartesianProductBenchmark<Move, AllFunctionTypes>();
  makeCartesianProductBenchmark<Swap, AllFunctionTypes, AllFunctionTypes>();
  makeCartesianProductBenchmark<OperatorBool, AllFunctionTypes>();
  makeCartesianProductBenchmark<Invoke, AllFunctionTypes>();
  makeCartesianProductBenchmark<InvokeInlined, AllFunctionTypes>();
  benchmark::RunSpecifiedBenchmarks();
}
