#include <cstddef>
#include <cstdint>
#include <fstream>
#include <sstream>
#include <string>

#ifdef _WIN32
#include <fcntl.h>
#else
#include <unistd.h>
#endif

#include "google/protobuf/descriptor.pb.h"
#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl/log/absl_log.h"
#include "absl/strings/escaping.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"

#if defined(_WIN32)
#include "google/protobuf/io/io_win32.h"

// DO NOT include <io.h>, instead create functions in io_win32.{h,cc} and
// import them like we do below.
using google::protobuf::io::win32::setmode;
#endif

ABSL_FLAG(std::string, encoding, "octal",
          "The encoding to use for the output.");
ABSL_FLAG(std::string, defaults_path, "defaults_path",
          "The path to the compile_edition_defaults file to embed.");
ABSL_FLAG(std::string, template_path, "template_path",
          "The template to use for generating the output file.");
ABSL_FLAG(std::string, output_path, "output_path",
          "The path to the the output file.");
ABSL_FLAG(
    std::string, placeholder, "placeholder",
    "The placeholder to replace with a serialized string in the template.");

int defaults_escape(const std::string& defaults_path,
                    const std::string& encoding, std::string& out_content) {
  std::ifstream defaults_file(defaults_path);
  if (!defaults_file.is_open()) {
    ABSL_LOG(ERROR) << "Could not open defaults file " << defaults_path;
    return 1;
  }

  google::protobuf::FeatureSetDefaults defaults;
  if (!defaults.ParseFromIstream(&defaults_file)) {
    ABSL_LOG(ERROR) << "Unable to parse edition defaults " << defaults_path;
    defaults_file.close();
    return 1;
  }

  defaults_file.close();

  std::string content = {};
  // TODO: Remove this suppression.
  (void)defaults.SerializeToString(&content);
  if (encoding == "base64") {
    content = absl::Base64Escape(content);
  } else if (encoding == "octal") {
    content = absl::CEscape(content);
  } else if (encoding == "decimal_array") {
    std::string encoded;
    bool first = true;
    for (uint8_t c : content) {
      if (first) {
        first = false;
        absl::StrAppend(&encoded, c);
      } else {
        absl::StrAppend(&encoded, ", ", c);
      }
    }
    content = encoded;
  } else if (encoding == "hex_array") {
    std::string encoded = {};
    size_t count = 0;
    for (uint8_t c : content) {
      absl::string_view prefix =
          (count % 12 != 0) ? ", 0x" : (count == 0 ? "  0x" : ",\n  0x");
      absl::StrAppend(&encoded, prefix, absl::Hex(c, absl::kZeroPad2));
      ++count;
    }
    content = encoded;
  } else {
    ABSL_LOG(FATAL) << "Unknown encoding: " << encoding;
    return 1;
  }

  out_content = content;
  return 0;
}

int read_to_string(const std::string& path, std::string& out_content) {
  std::ifstream input_file(path);
  if (!input_file.is_open()) {
    ABSL_LOG(ERROR) << "Could not open file " << path;
    return 1;
  }

  std::ostringstream buffer;
  buffer << input_file.rdbuf();
  out_content = buffer.str();
  input_file.close();

  return 0;
}

int replace_placeholder(std::string& out_content,
                        const std::string& placeholder,
                        const std::string& replacement) {
  size_t pos = 0;

  while ((pos = out_content.find(placeholder, pos)) != std::string::npos) {
    out_content.replace(pos, placeholder.length(), replacement);
    pos += replacement.length();
  }

  return 0;
}

int write(const std::string& path, const std::string& content) {
  std::ofstream output_file(path);
  if (!output_file.is_open()) {
    ABSL_LOG(ERROR) << "Could not write to file " << path;
    return 1;
  }

  output_file << content;
  output_file.close();

  return 0;
}

int main(int argc, char* argv[]) {
  absl::ParseCommandLine(argc, argv);
#ifdef _WIN32
  setmode(STDOUT_FILENO, _O_BINARY);
#endif
  std::string encoding = absl::GetFlag(FLAGS_encoding);
  std::string defaults_path = absl::GetFlag(FLAGS_defaults_path);
  std::string template_path = absl::GetFlag(FLAGS_template_path);
  std::string output_path = absl::GetFlag(FLAGS_output_path);
  std::string placeholder = absl::GetFlag(FLAGS_placeholder);

  std::string replacement = {};
  if (defaults_escape(defaults_path, encoding, replacement)) {
    return 1;
  }

  std::string content = {};
  if (read_to_string(template_path, content)) {
    return 1;
  }

  if (replace_placeholder(content, placeholder, replacement)) {
    return 1;
  }

  if (write(output_path, content)) {
    return 1;
  }

  return 0;
}
