#include "common.hpp"
#include "genbase.hpp"
#include "genns.hpp"
#include "repository.hpp"

#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;

#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>

#include <boost/program_options.hpp>
namespace po = boost::program_options;

#include <map>
#include <set>
#include <vector>

// thanks go to glib
#define GI_STRINGIFY(macro_or_string) GI_STRINGIFY_ARG(macro_or_string)
#define GI_STRINGIFY_ARG(contents) #contents

Log _loglevel = Log::WARNING;

class Generator
{
  GeneratorContext &ctx_;
  std::vector<std::string> girdirs_;
  // processes ns (ns: ns header)
  std::map<std::string, std::string> processed_;

public:
  Generator(GeneratorContext &_ctx, const std::vector<std::string> &girdirs)
      : ctx_(_ctx), girdirs_(girdirs)
  {}

  std::string find_in_dir(const fs::path p, const std::string &ns) const
  {
    std::string result;
    // check if in this directory
    if (!fs::is_directory(p))
      return "";

    auto f = p / (ns + GIR_SUFFIX);
    std::vector<fs::directory_entry> dirs;
    for (auto &&entry : fs::directory_iterator(p)) {
      if (fs::is_directory(entry)) {
        dirs.emplace_back(entry);
      } else if (f == entry) {
        // exact match
        result = f.native();
      } else {
        // non-version match
        auto ename = entry.path().filename().native();
        auto s = ns.size();
        if (ename.substr(0, s) == ns && ename.size() > s && ename[s] == '-')
          result = entry.path().native();
      }
    }
    // check dirs
    while (!dirs.empty() && result.empty()) {
      result = find_in_dir(dirs.back(), ns);
      dirs.pop_back();
    }
    return result;
  }

  std::string find(const std::string &ns) const
  {
    for (auto &&d : girdirs_) {
      auto res = find_in_dir(fs::path(d), ns);
      if (!res.empty())
        return res;
    }
    return "";
  }

  // gir might be:
  // + a GIR filename
  // + or a GIR namespace (ns-version)
  // + or a GIR namespace (no version appended)
  void generate(const std::string &gir, bool recurse)
  {
    fs::path f(gir);
    auto path = fs::exists(f) ? gir : find(gir);
    if (path.empty())
      throw std::runtime_error("could not find GIR for " + gir);
    // avoid reading if possible
    if (processed_.count(gir))
      return;
    auto genp = NamespaceGenerator::new_(ctx_, path);
    auto &gen = *genp;
    // normalize namespace
    auto &&ns = gen.get_ns();
    // prevent duplicate processing
    if (processed_.count(ns))
      return;
    // generate deps
    auto &&deps = gen.get_dependencies();
    std::vector<std::string> headers;
    if (recurse) {
      for (auto &&d : deps) {
        generate(d, recurse);
        // should be available now
        headers.emplace_back(processed_[d]);
      }
    }
    // now generate this one
    // also mark processed and retain ns header file
    processed_[ns] = gen.process_tree(headers);
  }
};

static std::string
wrap(const char *s)
{
  std::string res;
  if (s)
    res = s;
  return res;
}

static int
getnumber(const char *s)
{
  auto v = getenv(s);
  if (v)
    return atoi(v);
  return 0;
}

static int
die(const po::options_description &desc, const std::string &msg = "")
{
  std::cout << msg << std::endl << std::endl;
  ;
  std::cout << desc << std::endl;
  return 1;
}

static void
addsplit(std::vector<std::string> &target, const std::string &src,
    const std::string &seps = ":")
{
  std::vector<std::string> tmp;
  boost::split(tmp, src, boost::is_any_of(seps));
  for (auto &&d : tmp) {
    if (d.size())
      target.emplace_back(d);
  }
}

int
main(int argc, char *argv[])
{
  (void)argc;
  (void)argv;

  // first collect settings from environment
  int debug_level = getnumber("GI_DEBUG");
  auto fpath_ignore = wrap(getenv("GI_IGNORE"));
  auto fpath_suppress = wrap(getenv("GI_SUPPRESSION"));
  auto fpath_gen_suppress = wrap(getenv("GI_GEN_SUPPRESSION"));
  auto output_dir = wrap(getenv("GI_OUTPUT"));
  int doclass = getnumber("GI_CLASS");
  int use_expected = getnumber("GI_EXPECTED");

  auto gir_path = wrap(getenv("GI_GIR_PATH"));
  auto gir_top = wrap(getenv("GI_GIR"));

  // then consider options
  const char *const OP_HELP = "help";
  const char *const OP_CLASS = "class";
  const char *const OP_EXPECTED = "expected";
  po::options_description desc("Supported options");
  std::vector<std::string> girs, ignore_files, suppress_files;
  std::string gir_path_op;
  // clang-format off
  desc.add_options()(OP_HELP, "produce help message")
    ("debug", po::value<int>(&debug_level), "debug level")
    ("gir", po::value<std::vector<std::string>>(&girs), "GIR to process")
    ("ignore", po::value<std::string>(&fpath_ignore),
        "colon separated ignore files")
    ("suppression", po::value<std::string>(&fpath_suppress),
        "colon separated suppression files")
    ("gen-suppression", po::value<std::string>(&fpath_gen_suppress),
        "generate suppression file")
    ("output", po::value<std::string>(&output_dir), "output directory")
    ("gir-path", po::value<std::string>(&gir_path_op),
        "colon separated GIR search path")
    (OP_CLASS, "generate class implementation")
    (OP_EXPECTED, "use expected<> return rather than exception");
  // clang-format on

  po::positional_options_description p;
  p.add("gir", -1);

  // collect ignore files
  addsplit(ignore_files, fpath_ignore);
  fpath_ignore.clear();
  // suppress files
  addsplit(suppress_files, fpath_suppress);
  fpath_suppress.clear();

  po::variables_map vm;
  try {
    po::store(
        po::command_line_parser(argc, argv).options(desc).positional(p).run(),
        vm);
    po::notify(vm);
  } catch (const po::error &) {
    return die(desc);
  }

  if (vm.count(OP_HELP))
    return die(desc);

  // level
  if (debug_level > 0)
    _loglevel = (Log)debug_level;

  doclass |= vm.count(OP_CLASS);
  use_expected |= vm.count(OP_EXPECTED);

  std::vector<std::string> girdirs;
  for (auto &&v : {gir_path, gir_path_op})
    addsplit(girdirs, v);

  // system default
  {
    // gobject-introspection uses XDG_DATA_DIRS
    auto xdg_data_dirs = wrap(getenv("XDG_DATA_DIRS"));
    std::vector<std::string> default_gir_dirs;
    addsplit(default_gir_dirs, xdg_data_dirs);
#ifdef DEFAULT_GIRPATH
    // optional fallback
    if (default_gir_dirs.empty())
      addsplit(default_gir_dirs, GI_STRINGIFY(DEFAULT_GIRPATH));
#endif
    for (auto &d : default_gir_dirs) {
      const char PATH_SEP = '/';
      if (d.back() != PATH_SEP)
        d += PATH_SEP;
      d += "gir-1.0";
      girdirs.push_back(std::move(d));
    }
  }

  for (auto &&d : girdirs)
    logger(Log::DEBUG, "extending GIR path " + d);

  // collect some more files
  addsplit(ignore_files, fpath_ignore);
  addsplit(suppress_files, fpath_suppress);

  // system default
#ifdef DEFAULT_IGNORE_FILE
  addsplit(ignore_files, GI_STRINGIFY(DEFAULT_IGNORE_FILE));
#endif

  // collect girs to process
  addsplit(girs, gir_top);

  // sanity check
  if (output_dir.empty())
    return die(desc, "missing output directory");
  if (girs.empty())
    return die(desc, "nothing to process");

  // check for now
  if (girdirs.empty())
    return die(desc, "empty search path");

  // at least the standard ignore file is required
  // or things will go wrong
  int cnt = 0;
  for (auto &f : ignore_files)
    cnt += fs::exists(f);

  if (cnt == 0)
    return die(desc, "required default ignore file location not specified");

  auto match_ignore = Matcher(ignore_files);
  auto match_suppress = Matcher(suppress_files);

  // now let's start
  GeneratorOptions options;
  options.rootdir = output_dir;
  options.classimpl = doclass;
  options.expected = use_expected;

  logger(Log::INFO, "generating to directory {}", options.rootdir);

  auto repo = Repository::new_();
  std::set<std::string> suppressions;
  GeneratorContext ctx{
      options, *repo, match_ignore, match_suppress, suppressions};
  Generator gen(ctx, girdirs);
  try {
    for (auto &&g : girs)
      gen.generate(g, true);

    // write suppression
    if (fpath_gen_suppress.size()) {
      std::vector<std::string> sup(suppressions.begin(), suppressions.end());
      sort(sup.begin(), sup.end());
      logger(Log::INFO, "writing {} suppressions to {}", sup.size(),
          fpath_gen_suppress);

      std::ofstream fsup(fpath_gen_suppress);
      for (auto &&v : sup)
        fsup << v << std::endl;
    }
  } catch (std::runtime_error &ex) {
    logger(Log::ERROR, ex.what());
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}
