/*
 * Copyright (C) 2019 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 <jsonpb/verify.h>

#include <iostream>
#include <memory>
#include <sstream>
#include <string>

#include <android-base/strings.h>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/descriptor.pb.h>
#include <google/protobuf/message.h>
#include <google/protobuf/reflection.h>
#include <json/reader.h>
#include <json/writer.h>
#include <jsonpb/jsonpb.h>

namespace android {
namespace jsonpb {

using google::protobuf::FieldDescriptor;
using google::protobuf::FieldDescriptorProto;
using google::protobuf::Message;

// Return json_name of the field. If it is not set, return the name of the
// field.
const std::string& GetJsonName(const FieldDescriptor& field_descriptor) {
  // The current version of libprotobuf does not define
  // FieldDescriptor::has_json_name() yet. Use a workaround.
  // TODO: use field_descriptor.has_json_name() when libprotobuf version is
  // bumped.
  FieldDescriptorProto proto;
  field_descriptor.CopyTo(&proto);
  return proto.has_json_name() ? field_descriptor.json_name()
                               : field_descriptor.name();
}

bool AllFieldsAreKnown(const Message& message, const Json::Value& json,
                       std::vector<std::string>* path,
                       std::stringstream* error) {
  if (!json.isObject()) {
    *error << base::Join(*path, ".") << ": Not a JSON object\n";
    return false;
  }
  auto&& descriptor = message.GetDescriptor();

  auto json_members = json.getMemberNames();
  std::set<std::string> json_keys{json_members.begin(), json_members.end()};

  std::set<std::string> known_keys;
  for (int i = 0; i < descriptor->field_count(); ++i) {
    known_keys.insert(GetJsonName(*descriptor->field(i)));
  }

  std::set<std::string> unknown_keys;
  std::set_difference(json_keys.begin(), json_keys.end(), known_keys.begin(),
                      known_keys.end(),
                      std::inserter(unknown_keys, unknown_keys.begin()));

  if (!unknown_keys.empty()) {
    *error << base::Join(*path, ".") << ": contains unknown keys: ["
           << base::Join(unknown_keys, ", ")
           << "]. Keys must be a known field name of "
           << descriptor->full_name() << "(or its json_name option if set): ["
           << base::Join(known_keys, ", ") << "]\n";
    return false;
  }

  bool success = true;

  // Check message fields.
  auto&& reflection = message.GetReflection();
  std::vector<const FieldDescriptor*> set_field_descriptors;
  reflection->ListFields(message, &set_field_descriptors);
  for (auto&& field_descriptor : set_field_descriptors) {
    if (field_descriptor->cpp_type() !=
        FieldDescriptor::CppType::CPPTYPE_MESSAGE) {
      continue;
    }
    if (field_descriptor->is_map()) {
      continue;
    }

    const std::string& json_name = GetJsonName(*field_descriptor);
    const Json::Value& json_value = json[json_name];

    if (field_descriptor->is_repeated()) {
      auto&& fields =
          reflection->GetRepeatedFieldRef<Message>(message, field_descriptor);

      if (json_value.type() != Json::ValueType::arrayValue) {
        *error << base::Join(*path, ".")
               << ": not a JSON list. This should not happen.\n";
        success = false;
        continue;
      }

      if (json_value.size() != static_cast<size_t>(fields.size())) {
        *error << base::Join(*path, ".") << ": JSON list has size "
               << json_value.size() << " but message has size " << fields.size()
               << ". This should not happen.\n";
        success = false;
        continue;
      }

      std::unique_ptr<Message> scratch_space(fields.NewMessage());
      for (int i = 0; i < fields.size(); ++i) {
        path->push_back(json_name + "[" + std::to_string(i) + "]");
        auto res = AllFieldsAreKnown(fields.Get(i, scratch_space.get()),
                                     json_value[i], path, error);
        path->pop_back();
        if (!res) {
          success = false;
        }
      }
    } else {
      auto&& field = reflection->GetMessage(message, field_descriptor);
      path->push_back(json_name);
      auto res = AllFieldsAreKnown(field, json_value, path, error);
      path->pop_back();
      if (!res) {
        success = false;
      }
    }
  }
  return success;
}

bool AllFieldsAreKnown(const google::protobuf::Message& message,
                       const std::string& json, std::string* error) {
  Json::Reader reader;
  Json::Value value;
  if (!reader.parse(json, value)) {
    *error = reader.getFormattedErrorMessages();
    return false;
  }

  std::stringstream errorss;
  std::vector<std::string> json_tree_path{"<root>"};
  if (!AllFieldsAreKnown(message, value, &json_tree_path, &errorss)) {
    *error = errorss.str();
    return false;
  }
  return true;
}

bool EqReformattedJson(const std::string& json,
                       google::protobuf::Message* scratch_space,
                       std::string* error) {
  Json::Reader reader;
  Json::Value old_json;
  if (!reader.parse(json, old_json)) {
    *error = reader.getFormattedErrorMessages();
    return false;
  }

  auto new_json_string = internal::FormatJson(json, scratch_space);
  if (!new_json_string.ok()) {
    *error = new_json_string.error();
    return false;
  }
  Json::Value new_json;
  if (!reader.parse(*new_json_string, new_json)) {
    *error = reader.getFormattedErrorMessages();
    return false;
  }

  if (old_json != new_json) {
    std::stringstream ss;
    ss << "Formatted JSON tree does not match source. Possible reasons "
          "include: \n"
          "- JSON Integers (without quotes) are matched against 64-bit "
          "integers in Prototype\n"
          "  (Reformatted integers will now have quotes.) Quote these integers "
          "in source\n"
          "  JSON or use 32-bit integers instead.\n"
          "- Enum values are stored as integers in source JSON file. Use enum "
          "value name \n"
          "  string instead, or change schema field to string / integers.\n"
          "- JSON keys are re-formatted to be lowerCamelCase. To fix, define "
          "json_name "
          "option\n"
          "  for appropriate fields.\n"
          "\n"
          "Reformatted JSON is printed below.\n"
       << Json::StyledWriter().write(new_json);
    *error = ss.str();
    return false;
  }
  return true;
}

namespace internal {
ErrorOr<std::string> FormatJson(const std::string& json,
                                google::protobuf::Message* scratch_space) {
  auto res = internal::JsonStringToMessage(json, scratch_space);
  if (!res.ok()) {
    return MakeError<std::string>(res.error());
  }
  return MessageToJsonString(*scratch_space);
}
}  // namespace internal

}  // namespace jsonpb
}  // namespace android
