// Copyright 2023 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/environment_flags.h"

#include <cstdlib>
#include <filesystem>  // NOLINT
#include <string>
#include <vector>

#include "absl/flags/flag.h"
#include "absl/strings/match.h"
#include "absl/strings/str_split.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "./centipede/environment.h"
#include "./common/logging.h"

using ::fuzztest::internal::Environment;

#define CENTIPEDE_FLAG(TYPE, NAME, DEFAULT, DESC) \
  ABSL_FLAG(TYPE, NAME, DEFAULT, DESC);
#include "./centipede/centipede_flags.inc"
#undef CENTIPEDE_FLAG

#define CENTIPEDE_FLAG_ALIAS(ALIAS_NAME, ORIGINAL_NAME)                 \
  ABSL_FLAG(decltype(Environment::Default().ORIGINAL_NAME), ALIAS_NAME, \
            Environment::Default().ORIGINAL_NAME,                       \
            "Alias of --" #ORIGINAL_NAME)                               \
      .OnUpdate([]() {                                                  \
        absl::SetFlag(&FLAGS_##ORIGINAL_NAME,                           \
                      absl::GetFlag(FLAGS_##ALIAS_NAME));               \
      });
CENTIPEDE_FLAG_ALIAS(first_shard_index, my_shard_index)
CENTIPEDE_FLAG_ALIAS(timeout, timeout_per_input)
CENTIPEDE_FLAG_ALIAS(num_crash_reports, max_num_crash_reports)
CENTIPEDE_FLAG_ALIAS(minimize_crash, minimize_crash_file_path)
#undef CENTIPEDE_FLAG_ALIAS

ABSL_FLAG(absl::Duration, stop_after, absl::InfiniteDuration(),
          "Equivalent to setting --stop_at to the current date/time + this "
          "duration. These two flags are mutually exclusive.");
ABSL_RETIRED_FLAG(size_t, distill_shards, 0,
                  "No longer supported: use --distill instead.");

namespace fuzztest::internal {

namespace {

// Computes the final stop-at time based on the possibly user-provided inputs.
absl::Time GetStopAtTime(absl::Time stop_at, absl::Duration stop_after) {
  const bool stop_at_is_non_default = stop_at != absl::InfiniteFuture();
  const bool stop_after_is_non_default = stop_after != absl::InfiniteDuration();
  FUZZTEST_CHECK_LE(stop_at_is_non_default + stop_after_is_non_default, 1)
      << "At most one of --stop_at and --stop_after should be specified, "
         "including via --config file: "
      << VV(stop_at) << VV(stop_after);
  if (stop_at_is_non_default) {
    return stop_at;
  } else if (stop_after_is_non_default) {
    return absl::Now() + stop_after;
  } else {
    return absl::InfiniteFuture();
  }
}

}  // namespace

Environment CreateEnvironmentFromFlags(const std::vector<std::string> &argv) {
  Environment env_from_flags = {
#define CENTIPEDE_FLAG(_TYPE, NAME, _DEFAULT, _DESC) \
  absl::GetFlag(FLAGS_##NAME),
#include "./centipede/centipede_flags.inc"
#undef CENTIPEDE_FLAG
  };

  env_from_flags.stop_at =
      GetStopAtTime(env_from_flags.stop_at, absl::GetFlag(FLAGS_stop_after));

  if (env_from_flags.coverage_binary.empty()) {
    env_from_flags.coverage_binary =
        *absl::StrSplit(env_from_flags.binary, ' ').begin();
  }
  env_from_flags.binary_name =
      std::filesystem::path(env_from_flags.coverage_binary).filename().string();
  env_from_flags.UpdateBinaryHashIfEmpty();

  env_from_flags.UpdateTimeoutPerBatchIfEqualTo(
      Environment::Default().timeout_per_batch);

  if (size_t j = absl::GetFlag(FLAGS_j)) {
    env_from_flags.total_shards = j;
    env_from_flags.num_threads = j;
    env_from_flags.my_shard_index = 0;
  }
  FUZZTEST_CHECK_GE(env_from_flags.total_shards, 1);
  FUZZTEST_CHECK_GE(env_from_flags.batch_size, 1);
  FUZZTEST_CHECK_GE(env_from_flags.num_threads, 1);
  FUZZTEST_CHECK_LE(env_from_flags.num_threads, env_from_flags.total_shards);
  FUZZTEST_CHECK_LE(env_from_flags.my_shard_index + env_from_flags.num_threads,
                    env_from_flags.total_shards)
      << VV(env_from_flags.my_shard_index) << VV(env_from_flags.num_threads);

  if (!argv.empty()) {
    env_from_flags.exec_name = argv[0];
    for (size_t i = 1; i < argv.size(); ++i) {
      env_from_flags.args.emplace_back(argv[i]);
    }
  }

  if (!env_from_flags.clang_coverage_binary.empty())
    env_from_flags.extra_binaries.push_back(
        env_from_flags.clang_coverage_binary);

  if (absl::StrContains(env_from_flags.binary, "@@")) {
    FUZZTEST_LOG(INFO)
        << "@@ detected; running in standalone mode with batch_size=1";
    env_from_flags.has_input_wildcards = true;
    env_from_flags.batch_size = 1;
    // TODO(kcc): do we need to check if extra_binaries have @@?
  }

  env_from_flags.ReadKnobsFileIfSpecified();
  return env_from_flags;
}

}  // namespace fuzztest::internal
