// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc.  All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd

#include "google/protobuf/compiler/objectivec/enum.h"

#include <cstddef>
#include <limits>
#include <string>

#include "absl/container/flat_hash_set.h"
#include "absl/strings/escaping.h"
#include "absl/strings/str_cat.h"
#include "google/protobuf/compiler/objectivec/helpers.h"
#include "google/protobuf/compiler/objectivec/names.h"
#include "google/protobuf/compiler/objectivec/options.h"
#include "google/protobuf/compiler/objectivec/tf_decode_data.h"
#include "google/protobuf/descriptor.h"
#include "google/protobuf/io/printer.h"

namespace google {
namespace protobuf {
namespace compiler {
namespace objectivec {
namespace {
std::string SafelyPrintIntToCode(int v) {
  if (v == std::numeric_limits<int>::min()) {
    // Some compilers try to parse -2147483648 as two tokens and then get spicy
    // about the fact that +2147483648 cannot be represented as an int.
    return absl::StrCat(v + 1, " - 1");
  } else {
    return absl::StrCat(v);
  }
}
}  // namespace

EnumGenerator::EnumGenerator(const EnumDescriptor* descriptor,
                             const GenerationOptions& generation_options)
    : descriptor_(descriptor),
      generation_options_(generation_options),
      name_(EnumName(descriptor_)) {
  // Track the names for the enum values, and if an alias overlaps a base
  // value, skip making a name for it. Likewise if two alias overlap, the
  // first one wins.
  // The one gap in this logic is if two base values overlap, but for that
  // to happen you have to have "Foo" and "FOO" or "FOO_BAR" and "FooBar",
  // and if an enum has that, it is already going to be confusing and a
  // compile error is just fine.
  // The values are still tracked to support the reflection apis and
  // TextFormat handing since they are different there.
  absl::flat_hash_set<std::string> value_names;

  for (int i = 0; i < descriptor_->value_count(); i++) {
    const EnumValueDescriptor* value = descriptor_->value(i);
    const EnumValueDescriptor* canonical_value =
        descriptor_->FindValueByNumber(value->number());

    if (value == canonical_value) {
      base_values_.push_back(value);
      value_names.insert(EnumValueName(value));
    } else {
      if (!value_names.insert(EnumValueName(value)).second) {
        alias_values_to_skip_.insert(value);
      }
    }
    all_values_.push_back(value);
  }
}

void EnumGenerator::GenerateHeader(io::Printer* printer) const {
  // Swift 5 included SE0192 "Handling Future Enum Cases"
  //   https://github.com/apple/swift-evolution/blob/master/proposals/0192-non-exhaustive-enums.md
  // Since a .proto file can get new values added to an enum at any time, they
  // are effectively "non-frozen". Even with an EnumType::Open there is support
  // for the unknown value, an edit to the file can always add a new value
  // moving something from unknown to known. Since Swift is ABI stable, it also
  // means a binary could contain Swift compiled against one version of the
  // .pbobjc.h file, but finally linked against an enum with more cases. So the
  // Swift code will always have to treat ObjC Proto Enums as "non-frozen". The
  // default behavior in SE0192 is for all objc enums to be "non-frozen" unless
  // marked as otherwise, so this means this generation doesn't have to bother
  // with the `enum_extensibility` clang attribute, as the default will be what
  // is needed.

  printer->Emit(
      {
          {"enum_name", name_},
          {"enum_comments",
           [&] {
             EmitCommentsString(printer, generation_options_, descriptor_);
           }},
          {"enum_deprecated_attribute",
           GetOptionalDeprecatedAttribute(descriptor_, descriptor_->file())},
          {"maybe_unknown_value",
           [&] {
             if (descriptor_->is_closed()) return;

             // Include the unknown value.
             printer->Emit(R"objc(
               /**
                * Value used if any message's field encounters a value that is not defined
                * by this enum. The message will also have C functions to get/set the rawValue
                * of the field.
                **/
               $enum_name$_GPBUnrecognizedEnumeratorValue = kGPBUnrecognizedEnumeratorValue,
             )objc");
           }},
          {"enum_values",
           [&] {
             CommentStringFlags comment_flags = kCommentStringFlags_None;
             for (const auto* v : all_values_) {
               if (alias_values_to_skip_.contains(v)) continue;
               printer->Emit(
                   {
                       {"name", EnumValueName(v)},
                       {"comments",
                        [&] {
                          EmitCommentsString(printer, generation_options_, v,
                                             comment_flags);
                        }},
                       {"deprecated_attribute",
                        GetOptionalDeprecatedAttribute(v)},
                       {"value", SafelyPrintIntToCode(v->number())},

                   },
                   R"objc(
                     $comments$
                     $name$$ deprecated_attribute$ = $value$,
                   )objc");
               comment_flags = kCommentStringFlags_AddLeadingNewline;
             }
           }},
      },
      R"objc(
        #pragma mark - Enum $enum_name$

        $enum_comments$
        typedef$ enum_deprecated_attribute$ GPB_ENUM($enum_name$) {
          $maybe_unknown_value$
          $enum_values$
        };

        GPBEnumDescriptor *$enum_name$_EnumDescriptor(void);

        /**
         * Checks to see if the given value is defined by the enum or was not known at
         * the time this source was generated.
         **/
        BOOL $enum_name$_IsValidValue(int32_t value);
      )objc");
  printer->Emit("\n");
}

void EnumGenerator::GenerateSource(io::Printer* printer) const {
  // Note: For the TextFormat decode info, we can't use the enum value as
  // the key because protocol buffer enums have 'allow_alias', which lets
  // a value be used more than once. Instead, the index into the list of
  // enum value descriptions is used. Note: start with -1 so the first one
  // will be zero.
  TextFormatDecodeData text_format_decode_data;
  int enum_value_description_key = -1;
  std::string text_blob;

  for (const auto* v : all_values_) {
    ++enum_value_description_key;
    std::string short_name(EnumValueShortName(v));
    text_blob += short_name + '\0';
    if (UnCamelCaseEnumShortName(short_name) != v->name()) {
      text_format_decode_data.AddString(enum_value_description_key, short_name,
                                        v->name());
    }
  }

  printer->Emit(
      {{"name", name_},
       {"values_name_blob",
        [&] {
          static const int kBytesPerLine = 40;  // allow for escaping
          for (size_t i = 0; i < text_blob.size(); i += kBytesPerLine) {
            printer->Emit({{"data", EscapeTrigraphs(absl::CEscape(
                                        text_blob.substr(i, kBytesPerLine)))},
                           {"ending_semi",
                            (i + kBytesPerLine) < text_blob.size() ? "" : ";"}},
                          R"objc(
                            "$data$"$ending_semi$
                          )objc");
          }
        }},
       {"values",
        [&] {
          for (const auto* v : all_values_) {
            printer->Emit({{"value_name", EnumValueName(v)}},
                          R"objc(
                            $value_name$,
                          )objc");
          }
        }},
       {"maybe_extra_text_format_decl",
        [&] {
          if (text_format_decode_data.num_entries()) {
            printer->Emit({{"extraTextFormatInfo",
                            absl::CEscape(text_format_decode_data.Data())}},
                          R"objc(
                            static const char *extraTextFormatInfo = "$extraTextFormatInfo$";
                          )objc");
          }
        }},
       {"maybe_extraTextFormatInfo",
        // Could not find a better way to get this extra line inserted and
        // correctly formatted.
        (text_format_decode_data.num_entries() == 0
             ? ""
             : "\n                              "
               "extraTextFormatInfo:extraTextFormatInfo")},
       {"enum_flags", descriptor_->is_closed()
                          ? "GPBEnumDescriptorInitializationFlag_IsClosed"
                          : "GPBEnumDescriptorInitializationFlag_None"},
       {"enum_cases",
        [&] {
          for (const auto* v : base_values_) {
            printer->Emit({{"case_name", EnumValueName(v)}},
                          R"objc(
                            case $case_name$:
                          )objc");
          }
        }}},
      R"objc(
        #pragma mark - Enum $name$

        GPBEnumDescriptor *$name$_EnumDescriptor(void) {
          static _Atomic(GPBEnumDescriptor*) descriptor = nil;
          if (!descriptor) {
            GPB_DEBUG_CHECK_RUNTIME_VERSIONS();
            static const char *valueNames =
                $values_name_blob$
            static const int32_t values[] = {
                $values$
            };
            $maybe_extra_text_format_decl$
            GPBEnumDescriptor *worker =
                [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol($name$)
                                               valueNames:valueNames
                                                   values:values
                                                    count:(uint32_t)(sizeof(values) / sizeof(int32_t))
                                             enumVerifier:$name$_IsValidValue
                                                    flags:$enum_flags$$maybe_extraTextFormatInfo$];
            GPBEnumDescriptor *expected = nil;
            if (!atomic_compare_exchange_strong(&descriptor, &expected, worker)) {
              [worker release];
            }
          }
          return descriptor;
        }

        BOOL $name$_IsValidValue(int32_t value__) {
          switch (value__) {
            $enum_cases$
              return YES;
            default:
              return NO;
          }
        }
      )objc");
  printer->Emit("\n");
}

}  // namespace objectivec
}  // namespace compiler
}  // namespace protobuf
}  // namespace google
