#include "google/protobuf/symbol_checker.h"

#include <string>
#include <vector>

#include "absl/strings/str_cat.h"
#include "google/protobuf/descriptor.h"
#include "google/protobuf/descriptor.pb.h"
#include "google/protobuf/descriptor_visitor.h"

// Must be included last.
#include "google/protobuf/port_def.inc"

namespace google {
namespace protobuf {

std::string SymbolCheckerError::message() const {
  switch (type_) {
    case NESTED_MESSAGE_STRICT_VIOLATION:
      return absl::StrCat(
          "\"", symbol_name_,
          "\" is a nested message and cannot be `export` with STRICT "
          "default_symbol_visibility. It must be moved to top-level, "
          "ideally "
          "in its own file in order to be `export`.");
    case NESTED_ENUM_STRICT_VIOLATION:
      return absl::StrCat(
          "\"", symbol_name_,
          "\" is a nested enum and cannot be marked `export` with "
          "STRICT "
          "default_symbol_visibility. It must be moved to "
          "top-level, ideally "
          "in its own file in order to be `export`. For C++ "
          "namespacing of enums in a messages use: `local "
          "message <OuterNamespace> { export enum ",
          symbol_name_, " {...} reserved 1 to max; }`");
    default:
      return "";
  }
}

namespace {

// Populate VisibilityCheckerState for messages.
void VisitType(const Descriptor& message, const DescriptorProto& proto,
               internal::SymbolCheckerState* state) {
  if (message.containing_type() != nullptr) {
    state->nested_messages.push_back(
        internal::MessageDescriptorAndProto{&message, &proto});
  }
}

void VisitType(const EnumDescriptor& enm, const EnumDescriptorProto& proto,
               internal::SymbolCheckerState* state) {
  if (enm.containing_type() != nullptr) {
    if (SymbolChecker::IsNamespacedEnum(enm)) {
      state->namespaced_enums.push_back(
          internal::EnumDescriptorAndProto{&enm, &proto});
    } else {
      state->nested_enums.push_back(
          internal::EnumDescriptorAndProto{&enm, &proto});
    }
  }
}
void VisitType(const FileDescriptor& file, const FileDescriptorProto& proto,
               internal::SymbolCheckerState* state) {}

void VisitType(const FieldDescriptor& field, const FieldDescriptorProto& proto,
               internal::SymbolCheckerState* state) {}

void VisitType(const EnumValueDescriptor& value,
               const EnumValueDescriptorProto& proto,
               internal::SymbolCheckerState* state) {}

void VisitType(const OneofDescriptor& one, const OneofDescriptorProto& proto,
               internal::SymbolCheckerState* state) {}

void VisitType(const Descriptor::ExtensionRange&,
               const DescriptorProto::ExtensionRange& proto,
               internal::SymbolCheckerState* state) {}

void VisitType(const MethodDescriptor& method,
               const MethodDescriptorProto& proto,
               internal::SymbolCheckerState* state) {}

void VisitType(const ServiceDescriptor& service,
               const ServiceDescriptorProto& proto,
               internal::SymbolCheckerState* state) {}

}  // namespace

SymbolChecker::SymbolChecker(const FileDescriptor* file,
                             const FileDescriptorProto& proto)
    : initialized_(false), descriptor_(file), proto_(proto) {}

bool SymbolChecker::IsEnumNamespaceMessage(const Descriptor& container) {
  const FeatureSet::VisibilityFeature::DefaultSymbolVisibility
      default_visibility =
          container.file()->features().default_symbol_visibility();

  // Only allowed for top-level messages
  if (container.containing_type() != nullptr) {
    return false;
  }

  bool default_to_local =
      default_visibility == FeatureSet::VisibilityFeature::STRICT ||
      default_visibility == FeatureSet::VisibilityFeature::LOCAL_ALL;

  bool is_container_local =
      container.visibility_keyword() == SymbolVisibility::VISIBILITY_LOCAL ||
      (container.visibility_keyword() == SymbolVisibility::VISIBILITY_UNSET &&
       default_to_local);

  // the containing message must either be marked local, or unset with file
  // default making it local
  if (!is_container_local) {
    return false;
  }

  // We also require a single reserved range of 1 to max.
  if (container.reserved_range_count() != 1) {
    return false;
  }

  // Always not-null due to reserved_range_count check above.
  const Descriptor::ReservedRange* range = container.reserved_range(0);

  // The reserved range must be 1 to max.  Max is exclusive so results in
  // kMaxNumber + 1
  return range->start == 1 && range->end == (FieldDescriptor::kMaxNumber + 1);
}

bool SymbolChecker::IsNamespacedEnum(const EnumDescriptor& enm) {
  // Only allowed for top-level messages
  if (enm.containing_type() == nullptr) {
    return false;
  }

  const FeatureSet::VisibilityFeature::DefaultSymbolVisibility
      default_visibility = enm.file()->features().default_symbol_visibility();

  bool default_to_local =
      default_visibility == FeatureSet::VisibilityFeature::STRICT ||
      default_visibility == FeatureSet::VisibilityFeature::LOCAL_ALL;

  // This check only cares if the enum is explicitly marked export, the
  // containing message can be local or unset with the file default making it
  // local.
  if (!default_to_local ||
      enm.visibility_keyword() != SymbolVisibility::VISIBILITY_EXPORT) {
    return false;
  }

  return IsEnumNamespaceMessage(*enm.containing_type());
}

void SymbolChecker::Initialize() {
  if (initialized_) {
    return;
  }

  internal::VisitDescriptors(*descriptor_, proto_,
                             [&](const auto& descriptor, const auto& proto) {
                               VisitType(descriptor, proto, &state_);
                             });

  initialized_ = true;
}

// Enforce File-wide visibility and future co-location rules.
std::vector<SymbolCheckerError> SymbolChecker::CheckSymbolVisibilityRules() {
  std::vector<SymbolCheckerError> errors;

  Initialize();

  // Check DefaultSymbolVisibility first.
  //
  // For Edition 2024:
  // If DefaultSymbolVisibility is STRICT enforce it with caveats for:

  // Build our state object so we can apply rules based on type.

  // In edition 2024 we only enforce STRICT visibility rules. There are possibly
  // more rules to come in future editions, but for now just apply the rule for
  // enforcing nested symbol local visibility. There is a single caveat for,
  // allowing nested enums to have visibility set only when
  //
  // local msg { export enum {} reserved 1 to max; }
  for (auto& nested : state_.nested_messages) {
    if (nested.descriptor->visibility_keyword() ==
            SymbolVisibility::VISIBILITY_EXPORT &&
        nested.descriptor->features().default_symbol_visibility() ==
            FeatureSet::VisibilityFeature::STRICT) {
      errors.push_back(SymbolCheckerError(nested.descriptor->full_name(),
                                          *nested.proto,
                                          NESTED_MESSAGE_STRICT_VIOLATION));
    }
  }

  for (auto& nested : state_.nested_enums) {
    if (nested.descriptor->visibility_keyword() ==
            SymbolVisibility::VISIBILITY_EXPORT &&
        nested.descriptor->features().default_symbol_visibility() ==
            FeatureSet::VisibilityFeature::STRICT) {
      // This list contains only enums not considered 'namespaced' by
      // IsEnumNamespaceMessage
      errors.push_back(SymbolCheckerError(nested.descriptor->full_name(),
                                          *nested.proto,
                                          NESTED_ENUM_STRICT_VIOLATION));
    }
  }

  // Enforce Future rules here:
  return errors;
}

}  // namespace protobuf
}  // namespace google

#include "google/protobuf/port_undef.inc"
