// Protocol Buffers - Google's data interchange format
// Copyright 2023 Google LLC.  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

#ifndef PROTOBUF_COMPILER_HBP_OUTPUT_H_
#define PROTOBUF_COMPILER_HBP_OUTPUT_H_

#include <vector>

#include "absl/log/absl_log.h"
#include "absl/strings/str_replace.h"
#include "absl/strings/substitute.h"
#include "google/protobuf/descriptor.h"
#include "google/protobuf/io/zero_copy_stream.h"

namespace google::protobuf::hpb_generator {

class Output {
 public:
  Output(google::protobuf::io::ZeroCopyOutputStream* stream) : stream_(stream) {}
  ~Output() { stream_->BackUp((int)buffer_size_); }

  template <class... Arg>
  void operator()(absl::string_view format, const Arg&... arg) {
    Write(absl::Substitute(format, arg...));
  }

  // Indentation size in characters.
  static constexpr size_t kIndentationSize = 2;

  void Indent() { Indent(kIndentationSize); }
  void Indent(size_t size) { indent_ += size; }

  void Outdent() { Outdent(kIndentationSize); }
  void Outdent(size_t size) {
    if (indent_ < size) {
      ABSL_LOG(FATAL) << "mismatched Output indent/unindent calls";
    }
    indent_ -= size;
  }

 private:
  void Write(absl::string_view data) {
    std::string stripped;
    if (absl::StartsWith(data, "\n ")) {
      size_t indent = data.substr(1).find_first_not_of(' ');
      if (indent > indent_) {
        indent -= indent_;
      }
      if (indent != absl::string_view::npos) {
        // Remove indentation from all lines.
        auto line_prefix = data.substr(0, indent + 1);
        // The final line has an extra newline and is indented two less, eg.
        //    R"cc(
        //      UPB_INLINE $0 $1_$2(const $1 *msg) {
        //        return $1_has_$2(msg) ? *UPB_PTR_AT(msg, $3, $0) : $4;
        //      }
        //    )cc",
        std::string last_line_prefix = std::string(line_prefix);
        last_line_prefix.resize(last_line_prefix.size() - 2);
        data.remove_prefix(line_prefix.size());
        stripped = absl::StrReplaceAll(
            data, {{line_prefix, "\n"}, {last_line_prefix, "\n"}});
        data = stripped;
      }
    } else {
      WriteIndent();
    }
    WriteRaw(data);
  }

  void WriteRaw(absl::string_view data) {
    while (!data.empty()) {
      RefreshOutput();
      size_t to_write = std::min(data.size(), buffer_size_);
      memcpy(output_buffer_, data.data(), to_write);
      data.remove_prefix(to_write);
      output_buffer_ += to_write;
      buffer_size_ -= to_write;
    }
  }

  void WriteIndent() {
    if (indent_ == 0) {
      return;
    }
    size_t size = indent_;
    while (size > buffer_size_) {
      if (buffer_size_ > 0) {
        memset(output_buffer_, ' ', buffer_size_);
      }
      size -= buffer_size_;
      buffer_size_ = 0;
      RefreshOutput();
    }
    memset(output_buffer_, ' ', size);
    output_buffer_ += size;
    buffer_size_ -= size;
  }

  void RefreshOutput() {
    while (buffer_size_ == 0) {
      void* void_buffer;
      int size;
      if (!stream_->Next(&void_buffer, &size)) {
        fprintf(stderr, "upb_generator: Failed to write to to output\n");
        abort();
      }
      output_buffer_ = static_cast<char*>(void_buffer);
      buffer_size_ = size;
    }
  }

  google::protobuf::io::ZeroCopyOutputStream* stream_;
  char* output_buffer_ = nullptr;
  size_t buffer_size_ = 0;
  // Current indentation size in characters.
  size_t indent_ = 0;
  friend class OutputIndenter;
};

class OutputIndenter {
 public:
  OutputIndenter(Output& output)
      : OutputIndenter(output, Output::kIndentationSize) {}
  OutputIndenter(Output& output, size_t indent_size)
      : indent_size_(indent_size), output_(output) {
    output.Indent(indent_size);
  }
  ~OutputIndenter() { output_.Outdent(indent_size_); }

 private:
  size_t indent_size_;
  Output& output_;
};

std::string ToCIdent(absl::string_view str);
std::string ToPreproc(absl::string_view str);
void EmitFileWarning(const google::protobuf::FileDescriptor* file, Output& output);
std::string MessageName(const google::protobuf::Descriptor* descriptor);
std::string FileLayoutName(const google::protobuf::FileDescriptor* file);
std::string CHeaderFilename(const google::protobuf::FileDescriptor* file);

}  // namespace protobuf
}  // namespace google::hpb_generator

#endif  // PROTOBUF_COMPILER_HBP_OUTPUT_H_
