// © 2020 Erik Rigtorp <erik@rigtorp.se>
// SPDX-License-Identifier: CC0-1.0

// Install build dependencies:
// $ dnf install llvm-devel clang-devel

// Build:
// $ g++ -std=c++17 -Wall genostream.cpp -o genostream -lclang-cpp -lLLVM

// Example usage:
// $ genostream -p build src/foo.cpp
// `-p build` should be a directory with `compile_commands.json` for
// `src/foo.cpp`

// If the tool fails to find `stddef.h` or similar headers move the binary to
// the same directory as clang or specify the clang resource dir:
// `--extra-arg="-resource-dir /usr/lib64/clang/10.0.1/"`. See
// <https://clang.llvm.org/docs/LibTooling.html#builtin-includes>.

#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Frontend/ASTUnit.h"
#include "clang/Frontend/FrontendActions.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/Support/CommandLine.h"

using namespace llvm;
using namespace clang;
using namespace clang::ast_matchers;
using namespace clang::tooling;

auto EnumMatcher = enumDecl(isExpansionInMainFile()).bind("enum");

auto RecordMatcher =
    recordDecl(isExpansionInMainFile(), unless(isImplicit())).bind("record");

class Printer : public MatchFinder::MatchCallback {
public:
  virtual void run(const MatchFinder::MatchResult &Result) {
    if (const auto *Enum = Result.Nodes.getNodeAs<EnumDecl>("enum")) {
      // Enum->dump();
      outs() << "std::ostream & operator<<(std::ostream &os, "
             << Enum->getName() << " " << Enum->getName() << ") {\n"
             << "  switch (" << Enum->getName() << ") {\n";
      for (const EnumConstantDecl *EnumConstant : Enum->enumerators()) {
        outs() << "  case " << EnumConstant->getQualifiedNameAsString()
               << ": os << \"" << EnumConstant->getName() << "\";\n";
      }
      outs() << "  }\n  return os;\n}\n";
    }
    if (const auto *Record = Result.Nodes.getNodeAs<RecordDecl>("record")) {
      // Record->dump();
      outs() << "std::ostream & operator<<(std::ostream &os, const "
             << Record->getName() << " &v) {\n"
             << "  os << \"" << Record->getName() << "(\";\n";
      for (const FieldDecl *Field : Record->fields()) {
        bool IsFirst = Field == *Record->field_begin();
        outs() << "  os << \"" << (IsFirst ? "" : ", ") << Field->getName()
               << "=\" << v." << Field->getName() << ";\n";
      }
      outs() << "  os << \")\"\n  return os;\n}\n";
    }
  }
};

static cl::OptionCategory GenOstreamCategory("genostream options");
static cl::extrahelp CommonHelp(CommonOptionsParser::HelpMessage);

int main(int argc, const char **argv) {
  CommonOptionsParser OptionsParser(argc, argv, GenOstreamCategory);
  ClangTool Tool(OptionsParser.getCompilations(),
                 OptionsParser.getSourcePathList());

  Printer Printer;
  MatchFinder Finder;
  Finder.addMatcher(EnumMatcher, &Printer);
  Finder.addMatcher(RecordMatcher, &Printer);

  return Tool.run(newFrontendActionFactory(&Finder).get());
}
