/*
 * Copyright (c) 2023, Alliance for Open Media. All rights reserved
 *
 * This source code is subject to the terms of the BSD 3-Clause Clear License
 * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause Clear
 * License was not distributed with this source code in the LICENSE file, you
 * can obtain it at www.aomedia.org/license/software-license/bsd-3-c-c. If the
 * Alliance for Open Media Patent License 1.0 was not distributed with this
 * source code in the PATENTS file, you can obtain it at
 * www.aomedia.org/license/patent.
 */
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <ios>
#include <sstream>
#include <string>

#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl/flags/usage.h"
#include "absl/log/log.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "iamf/cli/adm_to_user_metadata/app/adm_to_user_metadata_main_lib.h"
#include "iamf/cli/encoder_main_lib.h"
#include "iamf/cli/proto/test_vector_metadata.pb.h"
#include "iamf/cli/proto/user_metadata.pb.h"
#include "iamf/obu/ia_sequence_header.h"
#include "src/google/protobuf/text_format.h"

// Flags to parse input user metadata.
ABSL_FLAG(
    std::string, user_metadata_filename, "",
    "Filename of the proto containing user metadata. It will be read as "
    "a textproto if the file extension is `.txtpb or `.textproto`. It will "
    "be read as a binary proto if the file extension is `.binpb`.  Exactly one "
    "of --adm_filename or --user_metadata_filename must be provided.");
ABSL_FLAG(std::string, input_wav_directory, "",
          "Directory containing the input wav files. Used only if "
          "--user_metadata_filename is provided.");

// Flags to parse input ADM file.
ABSL_FLAG(std::string, adm_filename, "",
          "Filename of the ADM BW64 file to use. Exactly one of --adm_filename "
          "or --user_metadata_filename must be provided.");
ABSL_FLAG(std::string, adm_profile_version, "base",
          "IAMF version to be used: (base/enhanced). Used only if "
          "--adm_filename is provided.");
ABSL_FLAG(int32_t, adm_importance_threshold, 0,
          "Importance value used to skip an audioObject. Clamped to [0, 10]. "
          "Used only if --adm_filename is provided.");
ABSL_FLAG(int32_t, adm_frame_duration_ms, 10,
          "Target frame duration in milliseconds. The actual frame duration "
          "may vary slightly. Used only if --adm_filename is provided.");

// Flags to control output directory for either type of input.
ABSL_FLAG(std::string, output_iamf_directory, "",
          "Output directory for iamf files");
// TODO(b/349504599): Add support to write output WAV files.

namespace {

// Reads in a user metadata proto from a binary or textproto file.
absl::StatusOr<iamf_tools_cli_proto::UserMetadata> ReadUserMetadataFromFile(
    const std::filesystem::path& user_metadata_filename) {
  std::ifstream user_metadata_file(user_metadata_filename.string(),
                                   std::ios::binary | std::ios::in);
  if (!user_metadata_file) {
    return absl::FailedPreconditionError(
        absl::StrCat("Error loading user_metadata_filename= ",
                     user_metadata_filename.string()));
  }

  std::ostringstream user_metadata_stream;
  user_metadata_stream << user_metadata_file.rdbuf();

  bool is_parse_successful = false;
  iamf_tools_cli_proto::UserMetadata user_metadata;
  if (user_metadata_filename.extension() == ".binpb") {
    is_parse_successful =
        user_metadata.ParseFromString(user_metadata_stream.str());
  } else if (user_metadata_filename.extension() == ".textproto" ||
             user_metadata_filename.extension() == ".txtpb") {
    is_parse_successful = google::protobuf::TextFormat::ParseFromString(
        user_metadata_stream.str(), &user_metadata);
  }

  if (!is_parse_successful) {
    return absl::InvalidArgumentError(
        absl::StrCat("Error parsing proto with user_metadata_filename= ",
                     user_metadata_filename.string()));
  }

  return user_metadata;
}

// Gets a user metadata proto and directory which the encoder will read wav
// files from. The proto may be read directly from a file or be generated based
// on an input ADM file.
absl::StatusOr<iamf_tools_cli_proto::UserMetadata>
GetUserMetadataAndInputWavDirectory(
    const std::filesystem::path& input_user_metadata_filename,
    const std::filesystem::path& adm_filename,
    std::filesystem::path& encoder_input_wav_directory,
    const iamf_tools::ProfileVersion profile_version) {
  if (input_user_metadata_filename.empty() == adm_filename.empty()) {
    return absl::InvalidArgumentError(
        "Please provide exactly one of --user_metadata_filename or "
        "--adm_filename.");
  } else if (!input_user_metadata_filename.empty()) {
    // The user directly provided a proto. Load it from the input file.
    encoder_input_wav_directory =
        absl::GetFlag(FLAGS_input_wav_directory).empty()
            ? std::filesystem::path("iamf/cli/testdata/")
            : std::filesystem::path(absl::GetFlag(FLAGS_input_wav_directory));
    return ReadUserMetadataFromFile(input_user_metadata_filename);
  } else {
    // Generate user metadata and wav files based on the input ADM file.
    std::ifstream adm_file(adm_filename, std::ios::binary | std::ios::in);

    // Wav files associated with each audio object will be written to a
    // temporary directory. The encoder will read back in the wav files from
    // this temporary directory.
    const auto temp_wav_file_directory = std::filesystem::temp_directory_path();
    encoder_input_wav_directory = temp_wav_file_directory;
    return iamf_tools::adm_to_user_metadata::
        GenerateUserMetadataAndSpliceWavFiles(
            std::filesystem::path(adm_filename).stem().string(),
            absl::GetFlag(FLAGS_adm_frame_duration_ms),
            absl::GetFlag(FLAGS_adm_importance_threshold),
            temp_wav_file_directory, adm_file, profile_version);
  }
}

}  // namespace

int main(int argc, char** argv) {
  absl::SetProgramUsageMessage(argv[0]);
  absl::ParseCommandLine(argc, argv);

  // Log the profile version flag.
  using enum iamf_tools::ProfileVersion;
  std::string iamf_profile = absl::GetFlag(FLAGS_adm_profile_version);
  iamf_tools::ProfileVersion profile_version;
  if (iamf_profile == "base") {
    profile_version = kIamfBaseProfile;
  } else if (iamf_profile == "enhanced") {
    profile_version = kIamfBaseEnhancedProfile;
  } else {
    LOG(ERROR) << "Invalid profile version: " << iamf_profile;
    return static_cast<int>(absl::StatusCode::kInvalidArgument);
  }
  LOG(INFO) << "Using IAMF" << iamf_profile << "profile version.";

  // Prepare `user_metadata` and `input_wav_directory` depending on the
  // input source.
  std::filesystem::path input_wav_directory;
  const auto& user_metadata = GetUserMetadataAndInputWavDirectory(
      absl::GetFlag(FLAGS_user_metadata_filename),
      absl::GetFlag(FLAGS_adm_filename), input_wav_directory, profile_version);
  if (!user_metadata.ok()) {
    LOG(ERROR) << user_metadata.status();
    return static_cast<int>(user_metadata.status().code());
  }

  LOG(INFO) << user_metadata;

  // Get the directory for the output .iamf files.
  const auto& output_iamf_directory =
      absl::GetFlag(FLAGS_output_iamf_directory).empty()
          ? std::filesystem::temp_directory_path()
          : std::filesystem::path(absl::GetFlag(FLAGS_output_iamf_directory));

  absl::Status status =
      iamf_tools::TestMain(*user_metadata, input_wav_directory.string(),
                           output_iamf_directory.string());

  // Log success or failure. Success is defined as a valid test vector returning
  // `absl::OkStatus()` or an invalid test vector returning a different status.
  const bool test_vector_is_valid =
      user_metadata->test_vector_metadata().is_valid();
  std::stringstream ss;
  ss << "Test case expected to " << (test_vector_is_valid ? "pass" : "fail")
     << ".\nstatus= " << status;
  if (test_vector_is_valid == status.ok()) {
    LOG(INFO) << "Success. " << ss.str();
  } else {
    LOG(ERROR) << "Failure. " << ss.str();
  }

  return static_cast<int>(status.code());
}
