/*
 * Copyright (c) 2014-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#pragma once

#include <functional>
#include <memory>
#include <stdlib.h>
#include <string>
#include <unordered_map>
#include <vector>
#include <utility>

#include <clang/AST/ASTConsumer.h>
#include <clang/Frontend/CompilerInstance.h>
#include <clang/Frontend/FrontendAction.h>
#include <clang/Frontend/FrontendActions.h>
#include <clang/Lex/Preprocessor.h>
#include <clang/Tooling/Tooling.h>

#include "FileUtils.h"

namespace ASTPluginLib {

struct PluginASTOptionsBase {
  
    // source file being parsed
  clang::FrontendInputFile inputFile;
  // output file for the plugin
  std::string outputFile;
  // object file produced by the usual frontend (possibly empty)
  std::string objectFile;

  /* Will contain the current directory if PREPEND_CURRENT_DIR was specified.
   * The intention is to make file paths in the AST absolute if needed.
   */
  std::string basePath;

  /* Configure a second pass on file paths to make them relative to the repo
   * root. */
  std::string repoRoot;
  /* Configure a third pass on (absolute) file paths to blank the system root:
   *    /path/to/sysroot/usr/lib/foo.h --> /usr/lib/foo.h
   */
  std::string iSysRoot;
  /* Configure a fourth pass on (absolute) file paths to detect siblings to
   * the repo root. If the repo root is /some/path, /some/other_path will be
   * rewritten ../other_path
   */
  bool allowSiblingsToRepoRoot = false;
  /* Whether file paths that could not be normalized by any of the rules above
   * should be kept or blanked.
   */
  bool keepExternalPaths = false;
  /* Resolve symlinks to their real path. */
  bool resolveSymlinks = false;

  /* Recursion level determining how decls referenced by types
   * are parsed
   */
  long recursionLevel = 0;

  /* do not emit string literals larger than this size */
  unsigned long maxStringSize = 65535;

  typedef std::unordered_map<std::string, std::string> argmap_t;

  static argmap_t makeMap(const std::vector<std::string> &args);

 private:
  /* cache for normalizeSourcePath */
  std::unique_ptr<std::unordered_map<const char *, std::string>>
      normalizationCache;

 protected:
  static const std::string envPrefix;

  static bool loadString(const argmap_t &map,
                         const char *key,
                         std::string &val);

  static bool loadBool(const argmap_t &map, const char *key, bool &val);

  static bool loadInt(const argmap_t &map, const char *key, long &val);

  static bool loadUnsignedInt(const argmap_t &map,
                              const char *key,
                              unsigned long &val);

 public:
  PluginASTOptionsBase() {
    normalizationCache.reset(
        new std::unordered_map<const char *, std::string>());
  };

  void loadValuesFromEnvAndMap(const argmap_t map);

  // This should be called after outputFile has been set, so as to finalize
  // the output file in case a pattern "%.bla" was given.
  void setObjectFile(const std::string &path);

  const std::string &normalizeSourcePath(const char *path) const;
  const std::string &normalizeSourcePath(llvm::StringRef path) const;
};

struct EmptyPreprocessorHandlerData {};

struct EmptyPreprocessorHandler : public clang::PPCallbacks {
  EmptyPreprocessorHandler(
      clang::SourceManager &SM,
      std::shared_ptr<PluginASTOptionsBase> options,
      std::shared_ptr<EmptyPreprocessorHandlerData> sharedData) {}
};

struct IncludesPreprocessorHandlerData {
    std::vector<std::pair<std::string, std::string>> includes;
};

struct IncludesPreprocessorHandler : public clang::PPCallbacks {
    
    IncludesPreprocessorHandler(
      clang::SourceManager &SM,
      std::shared_ptr<PluginASTOptionsBase> options,
      std::shared_ptr<IncludesPreprocessorHandlerData> sharedData
    ) :
        source_manager(SM),
        opts(*options),
        shared(*sharedData)
    {
        return;
    }

    virtual void InclusionDirective(
      clang::SourceLocation hash_loc,
      clang::Token const &include_token,
      clang::StringRef file_name,
      bool is_angled,
      clang::CharSourceRange filename_range,
      clang::FileEntry const *file,
      clang::StringRef search_path,
      clang::StringRef relative_path,
      clang::Module const *imported,
      clang::SrcMgr::CharacteristicKind file_type
      ) override
    {

        if (source_manager.isInMainFile(hash_loc)) {
            shared.includes.push_back(
                    std::make_pair(
                        search_path.str(),
                        file->getName().str()
                        )
                    );
        }

        return;
    }

    private:
    clang::SourceManager            &source_manager;
    PluginASTOptionsBase            &opts;
    IncludesPreprocessorHandlerData &shared;

};

template <class PluginASTOptions = PluginASTOptionsBase,
          class PreprocessorHandler = EmptyPreprocessorHandler,
          class PreprocessorHandlerData = EmptyPreprocessorHandlerData>
class SimplePluginASTActionBase : public clang::PluginASTAction {
 protected:
  std::shared_ptr<PluginASTOptions> options;
  std::shared_ptr<PreprocessorHandlerData> sharedData;

  void ExecuteAction() override {
    auto &preprocessor = getCompilerInstance().getPreprocessor();
    preprocessor.addPPCallbacks(std::make_unique<PreprocessorHandler>(
        preprocessor.getSourceManager(), options, sharedData));
    clang::PluginASTAction::ExecuteAction();
    return;
  }

  // Called when FrontendPluginRegistry is used.
  bool ParseArgs(const clang::CompilerInstance &CI,
                 const std::vector<std::string> &args_) override {
    std::vector<std::string> args = args_;
    if (args.size() > 0) {
      options->outputFile = *(args.end()-1);
    }
    options->loadValuesFromEnvAndMap(PluginASTOptions::makeMap(args));
    return true;
  }

  SimplePluginASTActionBase() {
    // These data structures will be shared between PreprocessorHandler
    // and ASTConsumer (the relative lifetimes of which are unknown).
    // During the AST traversal, it is expected that `options` is only read
    // and `sharedData` is only written.
    options = std::make_shared<PluginASTOptions>();
    sharedData = std::make_shared<PreprocessorHandlerData>();
  }

  // Alternate constructor to pass an optional sequence "KEY=VALUE,.."
  // expected to be use with SimpleFrontendActionFactory below.
  explicit SimplePluginASTActionBase(const std::vector<std::string> &args)
      : SimplePluginASTActionBase() {
    options->loadValuesFromEnvAndMap(PluginASTOptions::makeMap(args));
  }

  bool SetFileOptions(clang::CompilerInstance &CI,
                      llvm::StringRef inputFilename) {
    // When running clang tool on more than one source file, CreateASTConsumer
    // will be ran for each of them separately. Hence, Inputs.size() = 1.
    clang::FrontendInputFile inputFile = CI.getFrontendOpts().Inputs[0];

    switch (inputFile.getKind().getLanguage()) {
    case clang::Language::Unknown:
    case clang::Language::Asm:
    case clang::Language::LLVM_IR:
      // We can't do anything with these - they may trigger errors when running
      // clang frontend
      return false;
    default:
      // run the consumer for IK_AST and all others
      break;
    }
    options->inputFile = inputFile;
    options->setObjectFile(CI.getFrontendOpts().OutputFile);
    // success
    return true;
  }
};

template <class SimpleASTAction>
class SimpleFrontendActionFactory
    : public clang::tooling::FrontendActionFactory {
  std::vector<std::string> args_;

 public:
  explicit SimpleFrontendActionFactory(std::vector<std::string> args)
      : args_(args) {}

  std::unique_ptr<clang::FrontendAction> create() override {
    return std::unique_ptr<SimpleASTAction>(new SimpleASTAction(args_));
  }
};

template <class ASTConsumer,
          bool Binary = 0,
          bool RemoveFileOnSignal = 1,
          bool UseTemporary = 1,
          bool CreateMissingDirectories = 0>
class SimplePluginASTAction
    : public SimplePluginASTActionBase<
          typename ASTConsumer::ASTConsumerOptions,
          typename ASTConsumer::PreprocessorHandler,
          typename ASTConsumer::PreprocessorHandlerData> {
  using Parent =
      SimplePluginASTActionBase<typename ASTConsumer::ASTConsumerOptions,
                                typename ASTConsumer::PreprocessorHandler,
                                typename ASTConsumer::PreprocessorHandlerData>;

 public:
  SimplePluginASTAction() {}

  explicit SimplePluginASTAction(const std::vector<std::string> &args)
      : Parent(args) {}

 protected:
  std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
      clang::CompilerInstance &CI, llvm::StringRef inputFilename) {
    if (!Parent::SetFileOptions(CI, inputFilename)) {
      return nullptr;
    }
    std::unique_ptr<llvm::raw_ostream> OS =
        CI.createOutputFile(Parent::options->outputFile,
                            Binary,
                            RemoveFileOnSignal,
                            "",
                            "",
                            UseTemporary,
                            CreateMissingDirectories);
    if (!OS) {
      return nullptr;
    }

    return std::unique_ptr<clang::ASTConsumer>(new ASTConsumer(
        CI, Parent::options, Parent::sharedData, std::move(OS)));
  }
};

template <class ASTConsumer>
class NoOpenSimplePluginASTAction
    : public SimplePluginASTActionBase<
          typename ASTConsumer::ASTConsumerOptions,
          typename ASTConsumer::PreprocessorHandler,
          typename ASTConsumer::PreprocessorHandlerData> {
  using Parent =
      SimplePluginASTActionBase<typename ASTConsumer::ASTConsumerOptions,
                                typename ASTConsumer::PreprocessorHandler,
                                typename ASTConsumer::PreprocessorHandlerData>;

 public:
  NoOpenSimplePluginASTAction() {}

  explicit NoOpenSimplePluginASTAction(const std::vector<std::string> &args)
      : Parent(args) {}

 protected:
  std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(
      clang::CompilerInstance &CI, llvm::StringRef inputFilename) {
    if (!Parent::SetFileOptions(CI, inputFilename)) {
      return nullptr;
    }
    std::unique_ptr<std::string> outputFile = std::unique_ptr<std::string>(
        new std::string(Parent::options->outputFile));
    return std::unique_ptr<clang::ASTConsumer>(new ASTConsumer(
        CI, Parent::options, Parent::sharedData, std::move(outputFile)));
  }
};
} // namespace ASTPluginLib
