// Copyright 2021 The Tint 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 "fuzzers/tint_spirv_tools_fuzzer/spirv_fuzz_mutator.h"

#include <fstream>
#include <utility>

#include "fuzzers/tint_spirv_tools_fuzzer/util.h"
#include "source/opt/build_module.h"

namespace tint {
namespace fuzzers {
namespace spvtools_fuzzer {

SpirvFuzzMutator::SpirvFuzzMutator(
    spv_target_env target_env,
    std::vector<uint32_t> binary,
    unsigned seed,
    const std::vector<spvtools::fuzz::fuzzerutil::ModuleSupplier>& donors,
    bool enable_all_passes,
    spvtools::fuzz::RepeatedPassStrategy repeated_pass_strategy,
    bool validate_after_each_pass,
    uint32_t transformation_batch_size)
    : transformation_batch_size_(transformation_batch_size),
      errors_(std::make_unique<std::stringstream>()),
      fuzzer_(nullptr),
      validator_options_(),
      original_binary_(std::move(binary)),
      seed_(seed) {
  auto ir_context = spvtools::BuildModule(
      target_env, spvtools::fuzz::fuzzerutil::kSilentMessageConsumer,
      original_binary_.data(), original_binary_.size());
  assert(ir_context && "|binary| is invalid");

  auto transformation_context =
      std::make_unique<spvtools::fuzz::TransformationContext>(
          std::make_unique<spvtools::fuzz::FactManager>(ir_context.get()),
          validator_options_);

  auto fuzzer_context = std::make_unique<spvtools::fuzz::FuzzerContext>(
      std::make_unique<spvtools::fuzz::PseudoRandomGenerator>(seed),
      spvtools::fuzz::FuzzerContext::GetMinFreshId(ir_context.get()), false);
  fuzzer_ = std::make_unique<spvtools::fuzz::Fuzzer>(
      std::move(ir_context), std::move(transformation_context),
      std::move(fuzzer_context), util::GetBufferMessageConsumer(errors_.get()),
      donors, enable_all_passes, repeated_pass_strategy,
      validate_after_each_pass, validator_options_);
}

Mutator::Result SpirvFuzzMutator::Mutate() {
  // The assertion will fail in |fuzzer_->Run| if the previous fuzzing led to
  // invalid module.
  auto result = fuzzer_->Run(transformation_batch_size_);
  switch (result.status) {
    case spvtools::fuzz::Fuzzer::Status::kComplete:
      return {Mutator::Status::kComplete, result.is_changed};
    case spvtools::fuzz::Fuzzer::Status::kModuleTooBig:
    case spvtools::fuzz::Fuzzer::Status::kTransformationLimitReached:
      return {Mutator::Status::kLimitReached, result.is_changed};
    case spvtools::fuzz::Fuzzer::Status::kFuzzerStuck:
      return {Mutator::Status::kStuck, result.is_changed};
    case spvtools::fuzz::Fuzzer::Status::kFuzzerPassLedToInvalidModule:
      return {Mutator::Status::kInvalid, result.is_changed};
  }
}

std::vector<uint32_t> SpirvFuzzMutator::GetBinary() const {
  std::vector<uint32_t> result;
  fuzzer_->GetIRContext()->module()->ToBinary(&result, true);
  return result;
}

std::string SpirvFuzzMutator::GetErrors() const {
  return errors_->str();
}

void SpirvFuzzMutator::LogErrors(const std::string* path,
                                 uint32_t count) const {
  auto message = GetErrors();
  std::cout << count << " | SpirvFuzzMutator (seed: " << seed_ << ")"
            << std::endl;
  std::cout << message << std::endl;

  if (path) {
    auto prefix = *path + std::to_string(count);

    // Write errors to file.
    std::ofstream(prefix + ".fuzzer.log") << "seed: " << seed_ << std::endl
                                          << message << std::endl;

    // Write the invalid SPIR-V binary.
    util::WriteBinary(prefix + ".fuzzer.invalid.spv", GetBinary());

    // Write the original SPIR-V binary.
    util::WriteBinary(prefix + ".fuzzer.original.spv", original_binary_);

    // Write transformations.
    google::protobuf::util::JsonOptions options;
    options.add_whitespace = true;
    std::string json;
    google::protobuf::util::MessageToJsonString(
        fuzzer_->GetTransformationSequence(), &json, options);
    std::ofstream(prefix + ".fuzzer.transformations.json") << json << std::endl;

    std::ofstream binary_transformations(
        prefix + ".fuzzer.transformations.binary",
        std::ios::binary | std::ios::out);
    fuzzer_->GetTransformationSequence().SerializeToOstream(
        &binary_transformations);
  }
}

}  // namespace spvtools_fuzzer
}  // namespace fuzzers
}  // namespace tint