// Copyright 2023 The Crashpad Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <getopt.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#include <chrono>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <optional>
#include <random>
#include <ratio>
#include <string>

#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "client/annotation.h"
#include "client/length_delimited_ring_buffer.h"
#include "client/ring_buffer_annotation.h"
#include "tools/tool_support.h"
#include "util/stdlib/string_number_conversion.h"
#include "util/synchronization/scoped_spin_guard.h"
#include "util/thread/thread.h"

#if BUILDFLAG(IS_WIN)
#include <windows.h>
#else
#include <signal.h>
#endif  // BUILDFLAG(IS_WIN)

namespace crashpad {

namespace test {

namespace {

constexpr Annotation::Type kRingBufferLoadTestType =
    Annotation::UserDefinedType(0x0042);
std::atomic<bool> g_should_exit = false;

struct RingBufferAnnotationSnapshotParams final {
  enum class Mode {
    kUseScopedSpinGuard = 1,
    kDoNotUseSpinGuard = 2,
  };
  Mode mode = Mode::kUseScopedSpinGuard;
  using Duration = std::chrono::duration<uint64_t, std::nano>;
  Duration producer_thread_min_run_duration = std::chrono::milliseconds(1);
  Duration producer_thread_max_run_duration = std::chrono::milliseconds(10);
  Duration producer_thread_sleep_duration = std::chrono::nanoseconds(10);
  Duration consumer_thread_min_run_duration = std::chrono::milliseconds(5);
  Duration consumer_thread_max_run_duration = std::chrono::milliseconds(100);
  Duration quiesce_timeout = std::chrono::microseconds(500);
  uint64_t num_loops = std::numeric_limits<uint64_t>::max();
  std::optional<Duration> main_thread_run_duration = std::nullopt;
};

template <uint32_t RingBufferCapacity>
class RingBufferAnnotationSnapshot final {
  using RingBufferAnnotationType = RingBufferAnnotation<RingBufferCapacity>;

  struct State final {
    State()
        : ring_buffer_annotation(kRingBufferLoadTestType,
                                 "ring-buffer-load-test"),
          ring_buffer_ready(false),
          producer_thread_running(false),
          producer_thread_finished(false),
          consumer_thread_finished(false),
          should_exit(false) {}

    State(const State&) = delete;
    State& operator=(const State&) = delete;

    RingBufferAnnotationType ring_buffer_annotation;
    bool ring_buffer_ready;
    bool producer_thread_running;
    bool producer_thread_finished;
    bool consumer_thread_finished;
    bool should_exit;
  };

  class Thread final : public crashpad::Thread {
   public:
    Thread(std::function<void()> thread_main)
        : thread_main_(std::move(thread_main)) {}

   private:
    void ThreadMain() override { thread_main_(); }

    const std::function<void()> thread_main_;
  };

 public:
  RingBufferAnnotationSnapshot(const RingBufferAnnotationSnapshotParams& params)
      : params_(params),
        main_loop_thread_([this]() { MainLoopThreadMain(); }),
        producer_thread_([this]() { ProducerThreadMain(); }),
        consumer_thread_([this]() { ConsumerThreadMain(); }),
        mutex_(),
        state_changed_condition_(),
        state_() {}

  RingBufferAnnotationSnapshot(const RingBufferAnnotationSnapshot&) = delete;
  RingBufferAnnotationSnapshot& operator=(const RingBufferAnnotationSnapshot&) =
      delete;

  void Start() {
    main_loop_thread_.Start();
    producer_thread_.Start();
    consumer_thread_.Start();
  }

  void Stop() {
    consumer_thread_.Join();
    producer_thread_.Join();
    main_loop_thread_.Join();
  }

 private:
  void MainLoopThreadMain() {
    std::chrono::steady_clock::time_point main_thread_end_time;
    if (params_.main_thread_run_duration) {
      main_thread_end_time =
          std::chrono::steady_clock::now() + *params_.main_thread_run_duration;
    } else {
      main_thread_end_time = std::chrono::steady_clock::time_point::max();
    }
    for (uint64_t i = 0;
         i < params_.num_loops &&
         std::chrono::steady_clock::now() < main_thread_end_time;
         i++) {
      {
        std::unique_lock<std::mutex> start_lock(mutex_);
        state_.ring_buffer_annotation.ResetForTesting();
        state_.ring_buffer_ready = true;
        state_changed_condition_.notify_all();
      }

      {
        std::unique_lock<std::mutex> lock(mutex_);
        state_changed_condition_.wait(lock, [this] {
          return state_.producer_thread_finished &&
                 state_.consumer_thread_finished;
        });
        state_.ring_buffer_ready = false;
        if (g_should_exit) {
          printf("Exiting on Control-C.\n");
          break;
        }
        printf(".");
        fflush(stdout);
        state_changed_condition_.notify_all();
      }
    }
    state_.should_exit = true;
    state_changed_condition_.notify_all();
  }

  void ProducerThreadMain() {
    while (true) {
      {
        std::unique_lock<std::mutex> lock(mutex_);
        state_changed_condition_.wait(lock, [this] {
          return state_.should_exit || state_.ring_buffer_ready;
        });
        if (state_.should_exit) {
          return;
        }
        state_.producer_thread_running = true;
        state_.producer_thread_finished = false;
        state_changed_condition_.notify_all();
      }

      auto min_run_duration_micros =
          std::chrono::duration_cast<std::chrono::microseconds>(
              params_.producer_thread_min_run_duration);
      auto max_run_duration_micros =
          std::chrono::duration_cast<std::chrono::microseconds>(
              params_.producer_thread_max_run_duration);
      std::uniform_int_distribution<std::chrono::microseconds::rep>
          run_duration_distribution(min_run_duration_micros.count(),
                                    max_run_duration_micros.count());
      static thread_local std::mt19937 random_number_generator;
      auto run_duration = std::chrono::microseconds(
          run_duration_distribution(random_number_generator));
      auto end_time = std::chrono::steady_clock::now() + run_duration;
      uint64_t next_value = 0;
      while (std::chrono::steady_clock::now() < end_time) {
        if (!Produce(next_value++)) {
          // The consumer thread interrupted this.
          break;
        }
      }
      {
        std::unique_lock<std::mutex> lock(mutex_);
        state_changed_condition_.wait(
            lock, [this] { return state_.consumer_thread_finished; });
        state_.producer_thread_running = false;
        state_.producer_thread_finished = true;
        state_changed_condition_.notify_all();
      }
    }
  }

  bool Produce(uint64_t value) {
    std::string hex_value = base::StringPrintf("0x%08" PRIx64, value);
    if (!state_.ring_buffer_annotation.Push(
            hex_value.data(), static_cast<uint32_t>(hex_value.size()))) {
      fprintf(stderr,
              "Ignoring failed call to Push(0x%" PRIx64
              ") (ScopedSpinGuard was held by snapshot thread)\n",
              value);
      return false;
    }
    return true;
  }

  void ConsumerThreadMain() {
    while (true) {
      {
        std::unique_lock<std::mutex> lock(mutex_);
        state_changed_condition_.wait(lock, [this] {
          return state_.should_exit ||
                 (state_.ring_buffer_ready && state_.producer_thread_running);
        });
        if (state_.should_exit) {
          return;
        }
        state_.consumer_thread_finished = false;
        state_changed_condition_.notify_all();
      }
      auto min_run_duration_micros =
          std::chrono::duration_cast<std::chrono::microseconds>(
              params_.consumer_thread_min_run_duration);
      auto max_run_duration_micros =
          std::chrono::duration_cast<std::chrono::microseconds>(
              params_.consumer_thread_max_run_duration);
      std::uniform_int_distribution<std::chrono::microseconds::rep>
          run_duration_distribution(min_run_duration_micros.count(),
                                    max_run_duration_micros.count());
      static thread_local std::mt19937 random_number_generator;
      auto run_duration = std::chrono::microseconds(
          run_duration_distribution(random_number_generator));
      auto end_time = std::chrono::steady_clock::now() + run_duration;
      while (std::chrono::steady_clock::now() < end_time) {
        constexpr uint64_t kSleepTimeNs = 10000;  // 10 us
        SleepNanoseconds(kSleepTimeNs);
      }
      Snapshot();
      {
        std::unique_lock<std::mutex> lock(mutex_);
        state_.consumer_thread_finished = true;
        state_.ring_buffer_ready = false;
        state_changed_condition_.notify_all();
      }
    }
  }

  void Snapshot() {
    int64_t timeout_ns = static_cast<int64_t>(
        std::chrono::duration_cast<std::chrono::nanoseconds>(
            params_.quiesce_timeout)
            .count());
    uint8_t serialized_ring_buffer[sizeof(state_.ring_buffer_annotation)];
    Annotation::ValueSizeType ring_buffer_size;
    {
      std::optional<ScopedSpinGuard> scoped_spin_guard;
      if (params_.mode ==
          RingBufferAnnotationSnapshotParams::Mode::kUseScopedSpinGuard) {
        scoped_spin_guard =
            state_.ring_buffer_annotation.TryCreateScopedSpinGuard(timeout_ns);
      }
      if (params_.mode ==
              RingBufferAnnotationSnapshotParams::Mode::kUseScopedSpinGuard &&
          !scoped_spin_guard) {
        fprintf(stderr,
                "Could not quiesce writes within %" PRIi64 " ns\n",
                timeout_ns);
        abort();
      }
      ring_buffer_size = state_.ring_buffer_annotation.size();
      memcpy(&serialized_ring_buffer[0],
             state_.ring_buffer_annotation.value(),
             ring_buffer_size);
    }
    RingBufferData ring_buffer;
    if (!ring_buffer.DeserializeFromBuffer(serialized_ring_buffer,
                                           ring_buffer_size)) {
      fprintf(stderr, "Could not deserialize ring buffer\n");
      abort();
    }
    LengthDelimitedRingBufferReader ring_buffer_reader(ring_buffer);
    int value = std::numeric_limits<int>::max();
    std::vector<uint8_t> bytes;
    while (ring_buffer_reader.Pop(bytes)) {
      int next_value;
      base::StringPiece str(reinterpret_cast<const char*>(&bytes[0]),
                            bytes.size());
      if (!base::HexStringToInt(str, &next_value)) {
        fprintf(stderr,
                "Couldn't parse value: [%.*s]\n",
                base::checked_cast<int>(bytes.size()),
                bytes.data());
        abort();
      }
      if (value == std::numeric_limits<int>::max()) {
        // First value in buffer.
      } else if (value + 1 != next_value) {
        fprintf(stderr,
                "Expected value 0x%08x, got 0x%08x\n",
                value + 1,
                next_value);
        abort();
      }
      value = next_value;
      bytes.clear();
    }
  }

  const RingBufferAnnotationSnapshotParams params_;
  Thread main_loop_thread_;
  Thread producer_thread_;
  Thread consumer_thread_;
  std::mutex mutex_;

  // Fired whenever `state_` changes.
  std::condition_variable state_changed_condition_;

  // Protected by `mutex_`.
  State state_;
};

void Usage(const base::FilePath& me) {
  // clang-format off
  fprintf(stderr,
"Usage: %" PRFilePath " [OPTION]...\n"
"Runs a load test for concurrent I/O to RingBufferAnnotation.\n"
"\n"
"By default, enables the annotation spin guard and runs indefinitely\n"
"until interrupted (e.g., with Control-C or SIGINT).\n"
"\n"
"  -d,--disable-spin-guard  Disables the annotation spin guard\n"
"                           (the test is expected to crash in this case)\n"
"  -n,--num-loops=N         Runs the test for N iterations, not indefinitely\n"
"  -s,--duration-secs=SECS  Runs the test for SECS seconds, not indefinitely\n",
          me.value().c_str());
  // clang-format on
  ToolSupport::UsageTail(me);
}

int TestMain(int argc, char** argv) {
  const base::FilePath argv0(
      ToolSupport::CommandLineArgumentToFilePathStringType(argv[0]));
  const base::FilePath me(argv0.BaseName());

#if BUILDFLAG(IS_WIN)
  auto handler_routine = [](DWORD type) -> BOOL {
    if (type == CTRL_C_EVENT) {
      g_should_exit = true;
      return TRUE;
    }
    return FALSE;
  };
  if (!SetConsoleCtrlHandler(handler_routine, /*Add=*/TRUE)) {
    fprintf(stderr, "Couldn't set Control-C handler\n");
    return EXIT_FAILURE;
  }
#else
  signal(SIGINT, [](int signal) { g_should_exit = true; });
#endif  // BUILDFLAG(IS_WIN)
  RingBufferAnnotationSnapshotParams params;
  enum OptionFlags {
    // "Short" (single-character) options.
    kOptionDisableSpinGuard = 'd',
    kOptionNumLoops = 'n',
    kOptionDurationSecs = 's',

    // Standard options.
    kOptionHelp = -2,
    kOptionVersion = -3,
  };
  static constexpr option long_options[] = {
      {"disable-spin-guard", no_argument, nullptr, kOptionDisableSpinGuard},
      {"num-loops", required_argument, nullptr, kOptionNumLoops},
      {"duration-secs", required_argument, nullptr, kOptionDurationSecs},
      {"help", no_argument, nullptr, kOptionHelp},
      {"version", no_argument, nullptr, kOptionVersion},
      {nullptr, 0, nullptr, 0},
  };

  int opt;
  while ((opt = getopt_long(argc, argv, "dn:s:", long_options, nullptr)) !=
         -1) {
    switch (opt) {
      case kOptionDisableSpinGuard:
        printf("Disabling spin guard logic (this test will fail!)\n");
        params.mode =
            RingBufferAnnotationSnapshotParams::Mode::kDoNotUseSpinGuard;
        break;
      case kOptionNumLoops: {
        std::string num_loops(optarg);
        uint64_t num_loops_value;
        if (!StringToNumber(num_loops, &num_loops_value)) {
          ToolSupport::UsageHint(me, "--num-loops requires integer value");
          return EXIT_FAILURE;
        }
        params.num_loops = num_loops_value;
        break;
      }
      case kOptionDurationSecs: {
        std::string duration_secs(optarg);
        uint64_t duration_secs_value;
        if (!StringToNumber(duration_secs, &duration_secs_value)) {
          ToolSupport::UsageHint(me, "--duration-secs requires integer value");
          return EXIT_FAILURE;
        }
        params.main_thread_run_duration =
            std::chrono::seconds(duration_secs_value);
        break;
      }
      case kOptionHelp:
        Usage(me);
        return EXIT_SUCCESS;
      case kOptionVersion:
        ToolSupport::Version(me);
        return EXIT_SUCCESS;
      default:
        ToolSupport::UsageHint(me, nullptr);
        return EXIT_FAILURE;
    }
  }

  RingBufferAnnotationSnapshot<8192> test_producer_snapshot(params);
  printf("Starting test (Control-C to exit)...\n");
  test_producer_snapshot.Start();
  test_producer_snapshot.Stop();
  printf("Test finished.\n");
  return EXIT_SUCCESS;
}

}  // namespace
}  // namespace test
}  // namespace crashpad

#if BUILDFLAG(IS_POSIX)

int main(int argc, char** argv) {
  return crashpad::test::TestMain(argc, argv);
}

#elif BUILDFLAG(IS_WIN)

int wmain(int argc, wchar_t* argv[]) {
  return crashpad::ToolSupport::Wmain(argc, argv, crashpad::test::TestMain);
}

#endif
