// Copyright 2022 The Centipede 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
//
//      https://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 "./centipede/minimize_crash.h"

#include <algorithm>
#include <cstddef>
#include <cstdlib>
#include <filesystem>  // NOLINT
#include <string>
#include <string_view>
#include <vector>

#include "absl/base/thread_annotations.h"
#include "absl/synchronization/mutex.h"
#include "./centipede/centipede_callbacks.h"
#include "./centipede/environment.h"
#include "./centipede/mutation_input.h"
#include "./centipede/runner_result.h"
#include "./centipede/stop.h"
#include "./centipede/thread_pool.h"
#include "./centipede/util.h"
#include "./centipede/workdir.h"
#include "./common/defs.h"
#include "./common/hash.h"
#include "./common/logging.h"  // IWYU pragma: keep

namespace fuzztest::internal {

// Work queue for the minimizer.
// Thread-safe.
struct MinimizerWorkQueue {
 public:
  // Creates the queue.
  // `crash_dir_path` is the directory path where new crashers are written.
  // `crasher` is the initial crashy input.
  MinimizerWorkQueue(const std::string_view crash_dir_path,
                     const ByteArray crasher)
      : crash_dir_path_(crash_dir_path), crashers_{ByteArray(crasher)} {
    std::filesystem::create_directory(crash_dir_path_);
  }

  // Returns up to `max_num_crashers` most recently added crashers.
  std::vector<ByteArray> GetRecentCrashers(size_t max_num_crashers) {
    absl::MutexLock lock(&mutex_);
    size_t num_crashers_to_return =
        std::min(crashers_.size(), max_num_crashers);
    return {crashers_.end() - num_crashers_to_return, crashers_.end()};
  }

  // Adds `crasher` to the queue, writes it to `crash_dir_path_/Hash(crasher)`.
  // The crasher must be smaller than the original one.
  void AddCrasher(ByteArray crasher) {
    absl::MutexLock lock(&mutex_);
    FUZZTEST_CHECK_LT(crasher.size(), crashers_.front().size());
    crashers_.emplace_back(crasher);
    // Write the crasher to disk.
    auto hash = Hash(crasher);
    auto dir = crash_dir_path_;
    std::string file_path = dir.append(hash);
    WriteToLocalFile(file_path, crasher);
  }

  // Returns true if new smaller crashes were found.
  bool SmallerCrashesFound() const {
    absl::MutexLock lock(&mutex_);
    return crashers_.size() > 1;
  }

 private:
  mutable absl::Mutex mutex_;
  const std::filesystem::path crash_dir_path_;
  std::vector<ByteArray> crashers_ ABSL_GUARDED_BY(mutex_);
};

// Performs a minimization loop in one thread.
static void MinimizeCrash(const Environment &env,
                          CentipedeCallbacksFactory &callbacks_factory,
                          MinimizerWorkQueue &queue) {
  ScopedCentipedeCallbacks scoped_callback(callbacks_factory, env);
  auto callbacks = scoped_callback.callbacks();
  BatchResult batch_result;

  size_t num_batches = env.num_runs / env.batch_size;
  for (size_t i = 0; i < num_batches; ++i) {
    FUZZTEST_LOG_EVERY_POW_2(INFO)
        << "[" << i << "] Minimizing... Interrupt to stop";
    if (ShouldStop()) break;
    // Get up to kMaxNumCrashersToGet most recent crashers. We don't want just
    // the most recent crasher to avoid being stuck in local minimum.
    constexpr size_t kMaxNumCrashersToGet = 20;
    const auto recent_crashers = queue.GetRecentCrashers(kMaxNumCrashersToGet);
    FUZZTEST_CHECK(!recent_crashers.empty());
    // Compute the minimal known crasher size.
    size_t min_known_size = recent_crashers.front().size();
    for (const auto &crasher : recent_crashers) {
      min_known_size = std::min(min_known_size, crasher.size());
    }

    // Create several mutants that are smaller than the current smallest one.
    //
    // Currently, we do this by calling the vanilla mutator and
    // discarding all inputs that are too large.
    // TODO(kcc): modify the Mutate() interface such that max_len can be passed.
    //
    const std::vector<ByteArray> mutants = callbacks->Mutate(
        GetMutationInputRefsFromDataInputs(recent_crashers), env.batch_size);
    std::vector<ByteArray> smaller_mutants;
    for (const auto &m : mutants) {
      if (m.size() < min_known_size) smaller_mutants.push_back(m);
    }

    // Execute all mutants. If a new crasher is found, add it to `queue`.
    if (!callbacks->Execute(env.binary, smaller_mutants, batch_result)) {
      size_t crash_inputs_idx = batch_result.num_outputs_read();
      FUZZTEST_CHECK_LT(crash_inputs_idx, smaller_mutants.size());
      const auto &new_crasher = smaller_mutants[crash_inputs_idx];
      FUZZTEST_LOG(INFO) << "Crasher: size: " << new_crasher.size() << ": "
                         << AsPrintableString(new_crasher, /*max_len=*/40);
      queue.AddCrasher(new_crasher);
    }
  }
}

int MinimizeCrash(ByteSpan crashy_input, const Environment &env,
                  CentipedeCallbacksFactory &callbacks_factory) {
  ScopedCentipedeCallbacks scoped_callback(callbacks_factory, env);
  auto callbacks = scoped_callback.callbacks();

  FUZZTEST_LOG(INFO) << "MinimizeCrash: trying the original crashy input";

  BatchResult batch_result;
  ByteArray original_crashy_input(crashy_input.begin(), crashy_input.end());
  if (callbacks->Execute(env.binary, {original_crashy_input}, batch_result)) {
    FUZZTEST_LOG(INFO) << "The original crashy input did not crash; exiting";
    return EXIT_FAILURE;
  }

  FUZZTEST_LOG(INFO) << "Starting the crash minimization loop in "
                     << env.num_threads << "threads";

  MinimizerWorkQueue queue(WorkDir{env}.CrashReproducerDirPaths().MyShard(),
                           original_crashy_input);

  {
    ThreadPool threads{static_cast<int>(env.num_threads)};
    for (size_t i = 0; i < env.num_threads; ++i) {
      threads.Schedule([&env, &callbacks_factory, &queue]() {
        MinimizeCrash(env, callbacks_factory, queue);
      });
    }
  }  // The threads join here.

  return queue.SmallerCrashesFound() ? EXIT_SUCCESS : EXIT_FAILURE;
}

}  // namespace fuzztest::internal
