#include "pstream.h"
#include <cstdlib>
#include <fstream>
#include <functional>
#include <hash_map>
#include <ios>
#include <iostream>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <thread>
#include <unistd.h>
#include <vector>
#define POPT_IMPL 1
#include "popt.h"
#undef POPT_IMPL
#include "json.hpp"
#include <libgen.h>
struct catchall_definition : public popt::definition {
public:
  catchall_definition() {}
  virtual bool execute(popt::processor &, char) { return true; }
  virtual bool execute(popt::processor &, char, const char *) { return true; }
  virtual bool execute(popt::processor &, const char *) { return true; }
  virtual ~catchall_definition() {}
};

struct special_string_definition : public popt::string_definition {
public:
  special_string_definition(char s, const char *l, const char *a, const char *d,
                            const char *&v)
      : string_definition(s, l, a, d, v) {}
  virtual bool execute(popt::processor &, char) { return false; }
  virtual bool execute(popt::processor &, char, const char *) { return false; }
  virtual bool execute(popt::processor &, const char *s) {
    if (const char *long_name = query_long_name()) {
      const std::size_t len(std::strlen(long_name));
      if (0 == std::memcmp(long_name, s, len) && '=' == s[len]) {
        value = s + len + 1;
        return true;
      }
    }
    return false;
  }
};
bool has_cache(std::string x);
static inline std::list<std::string> p_split(const char *s, bool prefixdash) {
  std::list<std::string> l;
  if (prefixdash && *s && std::isspace(*s))
    prefixdash = false;
  for (;;) {
    while (!prefixdash && *s && std::isspace(*s))
      ++s;
    if (!*s)
      break;
    std::string r(prefixdash ? "-" : "");
    prefixdash = false;
    bool quoted(false);
    while (char c = *s) {
      if ('\"' == c)
        quoted = !quoted;
      else if ('\\' == c) {
        c = *++s;
        if (c)
          r += c;
        else {
          r += '\\';
          break;
        }
      } else if (!quoted && std::isspace(c))
        break;
      else
        r += c;
      ++s;
    }
    l.push_back(r);
  }
  return l;
}

static std::vector<const char *> p_convert(const std::list<std::string> &s) {
  std::vector<const char *> v;
  for (std::list<std::string>::const_iterator i(s.begin()); s.end() != i; ++i)
    v.push_back(i->c_str());
  return v;
}
struct _JT {
  std::thread x;
  operator std::thread &() { return x; };
  _JT(std::thread y) : x(std::move(y)){};
  ~_JT() {
    if (x.joinable())
      x.join();
  }
};
struct _Thread {
  std::shared_ptr<_JT> target;
  operator _JT &() { return *target; };
  _Thread(std::thread y) : target(std::make_shared<_JT>(std::move(y))){};
  _Thread() : _Thread(std::thread([]() {})){};
};
template <typename... Args> std::vector<std::string> cmd(Args... a) {
  return {a...};
}
template <typename... Args> std::vector<std::string> deps(Args... a) {
  return {a...};
}
struct makeflags {};
static inline bool procure_job_slot(const char *prog, int *jobserver_fds) {
  if (-1 != jobserver_fds[0]) {
    char c;
    const int r(read(jobserver_fds[0], &c, sizeof c));
    return 0 < r;
  }
  return false;
}

static inline void vacate_job_slot(const char *prog, int *jobserver_fds) {
  if (-1 != jobserver_fds[1]) {
    const char c('\0');
    write(jobserver_fds[1], &c, sizeof c);
  }
}
struct pv;
struct rules;
std::shared_ptr<pv> procure(rules &);
struct ruleData {
  std::string realPath;
};
using rule = std::function<std::string(rules &, ruleData)>;
struct rules : __gnu_cxx::hash_map<std::string, rule> {
  int fds[2];
  nlohmann::json cfg;
  std::string bdir;
  rules(int f[2], std::string r, std::string b)
      : bdir(b), fds{f[0], f[1]}, cfg(r) {}
  bool procure() { return procure_job_slot("", fds); }
  void vacate() { vacate_job_slot("", fds); }
  __gnu_cxx::hash_map<std::string, bool> _nocache;
  bool nocache = false;
  bool cl = false;
  long t = 0, tt = 0;
  std::string context;
  std::string toBuildPath(std::string x) {
    if (x.rfind("//", 0) == 0) {
      return bdir + x.substr(1);
    };
    x = context + x;
    if (x.rfind("//", 0) == 0) {
      return bdir + x.substr(1);
    };
    return x;
  };
  std::string operator()(std::string x) {
    auto p = toBuildPath(x);
    auto rp = p;
  start:
    if (tc[p].joinable())
      tc[p].join();
    if (!_nocache[x]) {
      if (has_cache(x))
        return x;
      t++;
    };
    try {
      auto c = context;
      auto d = c + x;
      context = dirname((char *)d.c_str());
      auto r = (*this)[p](*this, ruleData{rp});
      context = c;
      return r;
    } catch (std::bad_function_call _) {
      p = dirname((char *)p.c_str());
      goto start;
    }
  }
  __gnu_cxx::hash_map<std::string, _Thread> tc;
  std::vector<std::function<void(rules &, std::string)>> trackers;
  std::map<std::string, std::function<rule(std::string, rules &)>> extra;
  template <typename... Args>
  void operator()(std::string x, std::vector<std::string> rr,
                  std::function<rule(std::string, rules &, Args...)> y,
                  Args... z) {
    auto o = tc[x];
    std::vector<_Thread> r;
    for (auto rr2 : rr)
      r.push_back(tc[rr2]);
    auto p = toBuildPath(x);
    tc[p] = std::thread([&]() {
      if (o.joinable())
        o.join();
      for (auto l : r)
        if (l.joinable())
          l.join();
      while (!cl)
        ;
      cl = true;
      nocache = false;
      extra = {};
      while (!procure_job_slot("[dissolve]", fds))
        ;
      auto p = toBuildPath(x);
      auto c = context;
      auto d = c + x;
      context = dirname((char *)d.c_str());
      (*this)[p] = y(x, *this, z...);
      for (auto t : trackers) {
        t(*this, x);
      };
      for (auto e : extra) {
        (*this)[toBuildPath(e.first)] = e.second(x, *this);
        for (auto t : trackers) {
          t(*this, e.first);
        };
      };
      context = c;
      vacate_job_slot("[dissolve]", fds);
      _nocache[x] = nocache;
      if (!_nocache[x])
        tt++;
      cl = false;

      std::cout << "Configured " << x << std::endl;
    });
  }
  template <typename... Args>
  void operator()(std::string x,
                  std::function<rule(std::string, rules &, Args...)> y,
                  Args... z) {
    (*this)(x, {}, y, z...);
  }
  template <typename... Args>
  void operator()(std::string x, std::function<rule(std::string, Args...)> y,
                  Args... z) {
    (*this)(
        x,
        [&](std::string a, rules &b, Args... c) -> rule { return y(a, c...); },
        z...);
  }
};
template <typename... Args>
rule defer(std::string y, std::function<rule(std::string, rules &, Args...)> fn,
           Args... args) {
  return [&](rules &x, ruleData _) -> std::string {
    x(y, fn, args...);
    return x(_.realPath);
  };
}
template <typename... Args>
rule defer(std::string y, std::function<rule(std::string, Args...)> fn,
           Args... args) {
  return [&](rules &x, ruleData _) -> std::string {
    x(y, fn, args...);
    return x(_.realPath);
  };
}
template <typename... Args>
std::function<rule(std::string, rules &)>
bind(std::function<rule(std::string, rules &, Args...)> x, Args... y) {
  return std::bind(x, std::placeholders::_1, std::placeholders::_2, y...);
}
template <typename... Args>
std::function<rule(std::string)>
bind(std::function<rule(std::string, Args...)> x, Args... y) {
  return std::bind(x, std::placeholders::_1, y...);
}
std::function<rule(std::string, rules &)>
both(std::function<rule(std::string, rules &)> a,
     std::function<rule(std::string, rules &)> b) {
  return [&](std::string a2, rules &b2) -> rule {
    auto c = a(a2, b2);
    auto d = b(a2, b2);
    return [&](rules &e, ruleData f) -> std::string {
      c(e, f);
      d(e, f);
      return a2;
    };
    ;
  };
}
std::function<rule(std::string)> both(std::function<rule(std::string)> a,
                                      std::function<rule(std::string)> b) {
  return [&](std::string a2) -> rule {
    auto c = a(a2);
    auto d = b(a2);
    return [&](rules &e, ruleData f) -> std::string {
      c(e, f);
      d(e, f);
      return a2;
    };
    ;
  };
}
struct pv {
  rules &r;
  pv(rules &r) : r(r) {
    while (!r.procure())
      ;
  }
  pv(pv &x) = delete;
  ~pv() { r.vacate(); }
};
std::shared_ptr<pv> procure(rules &x) { return std::make_shared<pv>(x); }
static bool parse_fds(const char *prog, const char *str, int fds[],
                      size_t count) {
  char *s(const_cast<char *>(str));
  for (size_t j(0); j < count; ++j) {
    if (j > 0) {
      if (!*s) {
        fds[j] = fds[j - 1];
        continue;
      } else if (',' != *s) {
        std::cerr << str << ": " << s << ": Missing comma.\n";
        return false;
      } else
        ++s;
    }
    const char *old(s);
    long i(std::strtol(old, &s, 0));
    if (old == s) {
      std::cerr << str << ": " << s
                << ": File descriptor number not supplied.\n";
      return false;
    }
    fds[j] = i;
  }
  if (*s) {
    std::cerr << str << ": " << s
              << ": Trailing junk after file descriptor(s).\n";
    return false;
  }
  return true;
}
int _makeflags(int &a, int &b, int argc, char **argv) {
  const char *prog(basename(argv[0]));

  std::vector<const char *> filev;
  int jobserver_fds[2];
  try {
    std::string jobserver_fds_string;
    std::string redoparent_fd_string;
    const char *jobserver_fds_c_str = 0;
    const char *redoparent_fd_c_str = 0;
    const char *directory = 0;
    unsigned long max_jobs = 0;
    popt::bool_definition silent_option('s', "silent", "Operate quietly.",
                                        silent);
    popt::bool_definition quiet_option('\0', "quiet", "alias for --silent",
                                       silent);
    popt::bool_definition keep_going_option(
        'k', "keep-going",
        "Continue with the next target if a .do script fails.", keep_going);
    popt::bool_definition debug_option('d', "debug",
                                       "Output debugging information.", debug);
    popt::bool_definition verbose_option(
        '\0', "verbose", "Display information about the database.", verbose);
    popt::bool_definition print_option('p', "print", "alias for --verbose",
                                       verbose);
    popt::unsigned_number_definition jobs_option(
        'j', "jobs", "number", "Allow multiple jobs to run in parallel.",
        max_jobs, 0);
    popt::string_definition directory_option(
        'C', "directory", "directory",
        "Change to directory before doing anything.", directory);
    popt::definition *top_table[] = {
        &silent_option,  &quiet_option, &debug_option, &keep_going_option,
        &verbose_option, &print_option, &jobs_option,  &directory_option};
    popt::top_table_definition main_option(sizeof top_table / sizeof *top_table,
                                           top_table, "Main options",
                                           "filename(s)");
    catchall_definition ignore;
    special_string_definition jobserver_option(
        '\0', "jobserver-fds", "fd-list",
        "Provide the file descriptor numbers of the jobserver pipe.",
        jobserver_fds_c_str);
    special_string_definition redoparent_option(
        '\0', "redoparent-fd", "fd",
        "Provide the file descriptor number of the redo database current "
        "parent file.",
        redoparent_fd_c_str);
    popt::definition *make_env_top_table[] = {
        &silent_option,     &quiet_option,     &debug_option,
        &keep_going_option, &verbose_option,   &print_option,
        &jobs_option,       &jobserver_option, &ignore};
    popt::table_definition make_env_main_option(
        sizeof make_env_top_table / sizeof *make_env_top_table,
        make_env_top_table, "Main options (environment variable arguments)");
    popt::definition *redo_env_top_table[] = {
        &silent_option,     &quiet_option,     &debug_option,
        &keep_going_option, &verbose_option,   &print_option,
        &jobs_option,       &jobserver_option, &redoparent_option,
    };
    popt::table_definition redo_env_main_option(
        sizeof redo_env_top_table / sizeof *redo_env_top_table,
        redo_env_top_table, "Main options (environment variable arguments)");

    static const char *vars[] = {"REDOFLAGS", "MAKEFLAGS", "MFLAGS"};
    for (unsigned i = 0; i < sizeof vars / sizeof *vars; ++i) {
      const char *var(vars[i]);
      if (const char *s = std::getenv(var)) {
        std::list<std::string> args(
            p_split(s, 1 == i)); // Special bodge for MAKEFLAGS, which omits the
                                 // "-" from the first option.
        std::vector<const char *> argv(p_convert(args));
        std::vector<const char *> filev;
        popt::arg_processor<std::vector<const char *>::const_iterator> p(
            argv.begin(), argv.end(), prog,
            i ? make_env_main_option : redo_env_main_option, filev);
        p.process(false /* allow intermingling of options and arguments */);
        if (p.stopped())
          std::cerr << var << ": Ignoring halt of option processing.\n";
        if (1 == i) { // Special bodge for MAKEFLAGS, which BSD make puts macro
                      // definitions into.
          for (std::vector<const char *>::iterator j = filev.begin();
               j != filev.end();) {
            if (std::strchr(*j, '='))
              j = filev.erase(j);
            else
              ++j;
          }
        }
        if (!filev.empty())
          std::cerr << var << ": Ignoring filenames.\n";
        if (jobserver_fds_c_str) {
          jobserver_fds_string = jobserver_fds_c_str;
          jobserver_fds_c_str = 0;
        }
        if (redoparent_fd_c_str) {
          redoparent_fd_string = redoparent_fd_c_str;
          redoparent_fd_c_str = 0;
        }
        break;
      }
    }
    popt::arg_processor<const char **> p(argv + 1, argv + argc, prog,
                                         main_option, filev);
    p.process(false /* allow intermingling of options and arguments */);
    if (p.stopped())
      return EXIT_SUCCESS;
    if (directory) {
      const int r(chdir(directory));
      if (r < 0) {
        const int error(errno);
        std::cerr << directory << ": " << std::strerror(error) << "\n";
        return EXIT_FAILURE;
      }
    }
    if (jobserver_fds_c_str) {
      jobserver_fds_string = jobserver_fds_c_str;
      jobserver_fds_c_str = 0;
    }
    if (redoparent_fd_c_str) {
      redoparent_fd_string = redoparent_fd_c_str;
      redoparent_fd_c_str = 0;
    }

    if (!jobserver_fds_string.empty()) {
      if (!parse_fds(prog, jobserver_fds_string.c_str(), jobserver_fds,
                     sizeof jobserver_fds / sizeof *jobserver_fds))
        return EXIT_FAILURE;
      if (jobs_option.is_set())
        std::cerr
            << "Ignoring jobs option when there's already a job server.\n";
    } else {
      if (jobs_option.is_set()) {
        if (max_jobs < 1U) {
          std::cerr << "Invalid job limit.\n";
          return EXIT_FAILURE;
        }
        if (0 > pipe(jobserver_fds)) {
          const int error(errno);
          std::cerr << "pipe: " << std::strerror(error) << "\n";
          return EXIT_FAILURE;
        }
        for (unsigned m(max_jobs); m > 1U; --m)
          vacate_job_slot(prog, jobserver_fds);
      }
    }
    if (!redoparent_fd_string.empty()) {
      // if (!parse_fds(prog, redoparent_fd_string.c_str(), &redoparent_fd, 1U))
      //	return EXIT_FAILURE;
    }
  } catch (const popt::error &e) {
    std::cerr << e.arg << ": " << e.msg << "\n";
    return EXIT_FAILURE;
  }
  a = jobserver_fds[0];
  b = jobserver_fds[1];
  std::ostringstream s;
  s << "--jobserver-fds=" << a << "," << b;
  if (!getenv("MAKEFLAGS"))
    setenv("MAKEFLAGS", s.str().c_str(), 0);
}
template <typename... Args> std::vector<rule> dyndeps(Args... a) {
  return {a...};
}
template <typename T> void append(std::vector<T> &b, std::vector<T> a) {
  b.insert(b.end(), a.begin(), a.end());
}
bool has_cache(std::string file) {
  std::ifstream i("cache.txt");
  redi::pstream p({"sha256sum", file});
  std::string h;
  std::getline(p, h);
  h = h + " | " + file;
  std::string t;
  while (std::getline(i, t))
    if (t == h)
      return true;
  return false;
}
rule run(std::string makes, std::vector<std::string> x,
         std::vector<std::string> deps, std::vector<rule> dyndeps = deps()) {
  return [&](rules &x, ruleData p) -> std::string {
    if (has_cache(makes))
      return makes;
    auto fd = procure(x);
    std::vector<_Thread> d2;
    for (auto d : deps)
      d2.push_back(_Thread(std::thread([&]() { x(d); })));
    for (auto dd : dyndeps) {
      std::ifstream d(dd(x));
      std::string l;
      while (std::getline(d, l))
        d2.push_back(_Thread(std::thread([&]() { x[l](x); })));
    };
    for (auto e : d2)
      e.join();
    auto t =
        cmd("sh", "-c", "target=\"$1\";shift 1;. \"$target.env\";exec \"$@\"",
            makes, "env", std::string("REALTARGET=") + p.realPath,
            std::string("SUBTARGET=") + p.realPath.substr(makes.length()),
            std::string("SELFEXE=/proc/") + std::to_string(getpid()) + "/exe");
    for (auto c : x.cfg[makes]) {
      t.push_back(std::string(c[0]) + "=" + std::string(c[1]));
    };
    append(t, x);
    redi::pstream p(t);
    std::string l;
    while (std::getline(p, l))
      std::cout << makes << " | " << l;
    std::ofstream o("cache.txt", std::ios_base::app);
    redi::pstream p({"sha256sum", makes});
    std::string h;
    std::getline(p, h);
    o << h << " | " << makes << std::endl;

    return makes;
  };
};
rule make(std::string makes, std::vector<std::string> scriptTargets) {
  return run(makes, cmd("sh", "-c", "make -C \"$1\" $SUBTARGET", "--", makes),
             scriptTargets);
}
rule ninja(std::string makes, std::vector<std::string> scriptTargets) {
  return run(makes, cmd("sh", "-c", "ninja -C \"$1\" $SUBTARGET", "--", makes),
             scriptTargets);
}
rule configure_make(std::string makes, std::vector<std::string> scriptTargets) {
  return run(makes,
             cmd("sh", "-c",
                 "(cd \"$1\";mkdir build;cd build;../configure);make -C "
                 "\"$1\"/build $SUBTARGET",
                 "--", makes),
             scriptTargets);
}
rule redo(std::string makes, std::vector<std::string> scriptTargets) {
  return run(makes,
             cmd("sh", "-c", "redo-ifchange -C \"$1\" $SUBTARGET", "--", makes),
             scriptTargets);
}
rule pijul_clone(std::string makes, std::string patchRemote) {
  return run(makes, cmd("pijul", "clone", patchRemote, makes));
}
rule subrepo_internal(std::string makes) {
  return run(makes, cmd("sh", "-c",
                        "cd \"$1\";cc -I$CXXBUILDPATH BUILD.cpp -o "
                        "BUILD;BDIR=\"$(pwd)\" ./BUILD //$SUBTARGET",
                        "--", makes));
}
rule pijul_repo(std::string makes, std::string patchRemote) {
  return both(bind(pijul_clone, patchRemote), subrepo_internal)(makes);
}
rule pijul_external_repo(
    std::string makes, std::string patchRemote,
    std::function<rule(std::string, std::vector<std::string>)> r,
    std::vector<std::string> targets) {
  return both(bind(pijul_clone, patchRemote), bind(r, targets))(makes);
}
rule git_clone(std::string makes, std::string remote) {
  return run(makes, cmd("git", "clone", remote, makes));
}
rule git_repo(std::string makes, std::string remote) {
  return both(bind(git_clone, remote), subrepo_internal)(makes);
}
rule git_external_repo(
    std::string makes, std::string remote,
    std::function<rule(std::string, std::vector<std::string>)> r,
    std::vector<std::string> targets) {
  return both(bind(git_clone, remote), bind(r, targets))(makes);
}
nlohmann::json readManifest(rules &r, std::string target) {
  r(target);
  redi::pstream p({"ar", "p", target, "manifest.json"});
  std::string x;
  char y;
  while (p >> y)
    x += y;
  return nlohmann::json(x);
}
std::map<std::string, std::shared_ptr<std::ifstream>>
getFilesOf(rules &r, std::string target, std::string type) {
  auto m = readManifest(r, target);
  std::map<std::string, std::shared_ptr<std::ifstream>> n;
  for (auto x : m[type]) {
    r(x["target"]);
    n[x["result"]] = std::make_shared<std::ifstream>(x["target"]);
  };
  return n;
}
rule file(std::string makes, std::shared_ptr<std::istream> t) {
  return [&](rules &x) -> std::string {
    if (has_cache(makes))
      return makes;
    auto fd = procure(x);
    std::ofstream u(makes);
    char v;
    while (*t >> v)
      u << v;
    return makes;
  };
}
rule files(std::string prefix, std::string target, std::string type) {
  return [&](rules &r) -> std::string {
    for (auto f : getFilesOf(r, target, type)) {
      r(prefix + f.first, file, f.second);
    }
    return prefix;
  };
}
std::vector<std::string> listdir(std::string x) {
  redi::pstream p({"ls", x});
  std::vector<std::string> a;
  std::string b;
  while (std::getline(p, b))
    a.push_back(b);
  return a;
}
rule concat(std::string target, std::vector<std::string> srcs) {
  auto v = cmd("sh", "-c", "t=\"$1\";shift 1;cat \"$@\" > \"$t\"", target);
  for (auto s : srcs)
    v.push_back(s);
  return run(target, v, srcs);
}
rule cc_compile(std::string target, std::vector<std::string> hdrs,
                std::string src) {
  auto v = cmd("cxx", src, "-c", "-o", target);
  auto vv = hdrs;
  vv.push_back(src);
  return run(target, v, vv);
}
rule cc_library(std::string name, std::vector<std::string> hdrs,
                std::vector<std::string> srcs, std::vector<std::string> deps) {
  return [&](rules &r) -> std::string {
    for (auto s : srcs) {
      r(s + ".o", cc_compile, hdrs, s);
    };
    std::vector<std::string> a{"ar", "crus"}, d;
    for (auto s : srcs) {
      a.push_back(s + ".o");
      d.push_back(s + ".o");
    }
    auto c = a;
    for (auto hd : hdrs) {
      a.push_back(hd);
      d.push_back(hd);
    }
    a.push_back(name + ".d/manifest.json");
    d.push_back(name + ".d/manifest.json");
    for (auto dep : deps) {
      d.push_back(name + ".d/_" + dep);
      r(name + ".d/_", run, {"ar", "x", dep})
    };
    r(name, run, a, d);
    r(name + ".d/manifest.json", file,
      std::make_shared<std::istringstream>(
          nlohmann::json::object({{"hdrs", hdrs}, {"objs", c}}).to_ubjson()));
    r(name);
    return name;
  };
}
void build_rules(rules &);
int main(int argc, char **argv) {
  int a, b;
  _makeflags(a, b, argc, argv);
  int f[2]{a, b};
  rules r(f, getenv("CONFIG"), getenv("BDIR"));
  build_rules(r);
  r("default.do", file,
    std::make_shared<std::istringstream>(std::string("exec '") + argv[0] +
                                         "' \"$1\""));
  std::string o(argv[1]);
  if (!has_cache(argv[0]) && r.find(argv[0]) != r.end()) {
    r[argv[0]](r);
    std::vector<char *> a(argv, argv + argc);
    a.push_back(nullptr);
    execv(argv[0], &a[0]);
  };
  if (o == "--list") {
    for (auto e : r)
      std::cout << e.first << std::endl;
  } else if (o == "--all" || o == "-A") {
    for (auto e : r)
      e.second(r);
  } else {
    r[argv[1]](r);
  }
};