/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * 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 <stdio.h>
#include <string>

#include <google/protobuf/compiler/importer.h>
#include <google/protobuf/io/zero_copy_stream_impl.h>

#include "perfetto/base/logging.h"
#include "perfetto/ext/base/file_utils.h"
#include "perfetto/ext/base/getopt.h"
#include "perfetto/ext/base/scoped_file.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/version.h"
#include "src/tools/proto_merger/allowlist.h"
#include "src/tools/proto_merger/proto_file.h"
#include "src/tools/proto_merger/proto_file_serializer.h"
#include "src/tools/proto_merger/proto_merger.h"

namespace perfetto {
namespace proto_merger {
namespace {

class MultiFileErrorCollectorImpl
    : public google::protobuf::compiler::MultiFileErrorCollector {
 public:
  ~MultiFileErrorCollectorImpl() override;
  void AddError(const std::string&, int, int, const std::string&) override;
  void AddWarning(const std::string&, int, int, const std::string&) override;
};

MultiFileErrorCollectorImpl::~MultiFileErrorCollectorImpl() = default;

void MultiFileErrorCollectorImpl::AddError(const std::string& filename,
                                           int line,
                                           int column,
                                           const std::string& message) {
  PERFETTO_ELOG("Error %s %d:%d: %s", filename.c_str(), line, column,
                message.c_str());
}

void MultiFileErrorCollectorImpl::AddWarning(const std::string& filename,
                                             int line,
                                             int column,
                                             const std::string& message) {
  PERFETTO_ELOG("Warning %s %d:%d: %s", filename.c_str(), line, column,
                message.c_str());
}

struct ImportResult {
  std::unique_ptr<google::protobuf::compiler::Importer> importer;
  const google::protobuf::FileDescriptor* file_descriptor;
};

ImportResult ImportProto(const std::string& proto_file,
                         const std::string& proto_dir_path) {
  MultiFileErrorCollectorImpl mfe;

  google::protobuf::compiler::DiskSourceTree dst;
  dst.MapPath("", proto_dir_path);

  ImportResult result;
  result.importer.reset(new google::protobuf::compiler::Importer(&dst, &mfe));
  result.file_descriptor = result.importer->Import(proto_file);
  return result;
}

const char kUsage[] =
    R"(Usage: proto_merger [-i input proto] [-I import dir]

-i, --input:                 Path to the input .proto file (relative to
                              --input-include directory). The contents of this
                              file will be updated using the upstream proto.
-I, --input-include:         Root directory from which includes for --input
                              proto should be searched.
-u, --upstream:              Path to the upstream .proto file; the contents of
                              this file will be used to update
                              the input proto.
-U, --upstream-include:      Root directory from which includes for --upstream
                              proto should be searched.
-a, --allowlist:             Allowlist file which is used to add new fields in
                              the upstream proto to the input proto.
-r, --upstream-root-message: Root message in the upstream proto for which new
                              fields from the allowlist will be allowed.
-o, --output:                Output path for writing the merged proto file.

Example usage:

# Updating logs proto from Perfetto repo (must be run in G3):
  proto_merger \
    -u third_party/perfetto/protos/perfetto/trace/perfetto_trace.proto \
    -U . \
    -i <path to logs proto>/perfetto_log.proto \
    -I . \
    --allowlist /tmp/allowlist.txt \
    -r perfetto.protos.Trace \
    --output /tmp/output.proto
)";

int Main(int argc, char** argv) {
  static const option long_options[] = {
      {"help", no_argument, nullptr, 'h'},
      {"version", no_argument, nullptr, 'v'},
      {"input", required_argument, nullptr, 'i'},
      {"input-include", required_argument, nullptr, 'I'},
      {"upstream", required_argument, nullptr, 'u'},
      {"upstream-include", required_argument, nullptr, 'U'},
      {"allowlist", required_argument, nullptr, 'a'},
      {"upstream-root-message", required_argument, nullptr, 'r'},
      {"output", required_argument, nullptr, 'o'},
      {nullptr, 0, nullptr, 0}};

  std::string input;
  std::string input_include;
  std::string upstream;
  std::string upstream_include;
  std::string allowlist;
  std::string upstream_root_message;
  std::string output;

  for (;;) {
    int option =
        getopt_long(argc, argv, "hvi:I:u:U:a:r:o:", long_options, nullptr);

    if (option == -1)
      break;  // EOF.

    if (option == 'v') {
      printf("%s\n", base::GetVersionString());
      return 0;
    }

    if (option == 'i') {
      input = optarg;
      continue;
    }

    if (option == 'I') {
      input_include = optarg;
      continue;
    }

    if (option == 'u') {
      upstream = optarg;
      continue;
    }

    if (option == 'U') {
      upstream_include = optarg;
      continue;
    }

    if (option == 'a') {
      allowlist = optarg;
      continue;
    }

    if (option == 'r') {
      upstream_root_message = optarg;
      continue;
    }

    if (option == 'o') {
      output = optarg;
      continue;
    }

    if (option == 'h') {
      fprintf(stdout, kUsage);
      return 0;
    }

    fprintf(stderr, kUsage);
    return 1;
  }

  if (input.empty()) {
    PERFETTO_ELOG("Input proto (--input) should be specified");
    return 1;
  }

  if (input_include.empty()) {
    PERFETTO_ELOG(
        "Input include directory (--input-include) should be specified");
    return 1;
  }

  if (upstream.empty()) {
    PERFETTO_ELOG("Upstream proto (--upstream) should be specified");
    return 1;
  }

  if (upstream_include.empty()) {
    PERFETTO_ELOG(
        "Upstream include directory (--upstream-include) should be specified");
    return 1;
  }

  if (output.empty()) {
    PERFETTO_ELOG("Output file (--output) should be specified");
    return 1;
  }

  if (!allowlist.empty() && upstream_root_message.empty()) {
    PERFETTO_ELOG(
        "Need to specifiy upstream root message (--upstream-root-message) when "
        "specifying allowlist");
    return 1;
  }

  ImportResult input_proto = ImportProto(input, input_include);
  ProtoFile input_file = ProtoFileFromDescriptor(*input_proto.file_descriptor);

  ImportResult upstream_proto = ImportProto(upstream, upstream_include);
  ProtoFile upstream_file =
      ProtoFileFromDescriptor(*upstream_proto.file_descriptor);

  Allowlist allowed;
  if (!allowlist.empty()) {
    std::string allowlist_contents;
    if (!base::ReadFile(allowlist, &allowlist_contents)) {
      PERFETTO_ELOG("Failed to read allowlist");
      return 1;
    }

    auto* desc = upstream_proto.importer->pool()->FindMessageTypeByName(
        upstream_root_message);
    if (!desc) {
      PERFETTO_ELOG(
          "Failed to find root message descriptor in upstream proto file");
      return 1;
    }

    auto field_list = base::SplitString(allowlist_contents, "\n");
    base::Status status = AllowlistFromFieldList(*desc, field_list, allowed);
    if (!status.ok()) {
      PERFETTO_ELOG("Failed creating allowlist: %s", status.c_message());
      return 1;
    }
  }

  ProtoFile merged;
  base::Status status =
      MergeProtoFiles(input_file, upstream_file, allowed, merged);
  if (!status.ok()) {
    PERFETTO_ELOG("Failed merging protos: %s", status.c_message());
    return 1;
  }

  base::ScopedFile output_file(
      base::OpenFile(output, O_CREAT | O_WRONLY | O_TRUNC, 0664));
  if (!output_file) {
    PERFETTO_ELOG("Failed opening output file: %s", output.c_str());
    return 1;
  }
  std::string out = ProtoFileToDotProto(merged);
  base::WriteAll(*output_file, out.c_str(), out.size());

  return 0;
}

}  // namespace
}  // namespace proto_merger
}  // namespace perfetto

int main(int argc, char** argv) {
  return perfetto::proto_merger::Main(argc, argv);
}
