#include <cstdio>
#include <cstdlib>
#include <climits>
#include <cassert>

#include <unistd.h>
#include <dirent.h>

#include <vector>
#include <string>
#include <memory>
#include <utility>
#include <sstream>
#include <fstream>
#include <algorithm>
#include <functional>
#include <unordered_map>

#include <zmq.h>
#include <capnp/serialize.h>
#include "cereal/gen/cpp/log.capnp.h"

#include "common/timing.h"
#include "common/utilpp.h"

namespace {

struct ProcCache {
  std::string name;
  std::vector<std::string> cmdline;
  std::string exe;
};

}

int main() {
  int err;

  void *context = zmq_ctx_new();
  void *publisher = zmq_socket(context, ZMQ_PUB);
  err = zmq_bind(publisher, "tcp://*:8031");
  assert(err == 0);

  double jiffy = sysconf(_SC_CLK_TCK);
  size_t page_size = sysconf(_SC_PAGE_SIZE);

  std::unordered_map<pid_t, ProcCache> proc_cache;

  while (1) {

    capnp::MallocMessageBuilder msg;
    cereal::Event::Builder event = msg.initRoot<cereal::Event>();
    event.setLogMonoTime(nanos_since_boot());
    auto procLog = event.initProcLog();

    auto orphanage = msg.getOrphanage();

    // stat
    {
      std::vector<capnp::Orphan<cereal::ProcLog::CPUTimes>> otimes;

      std::ifstream sstat("/proc/stat");
      std::string stat_line;
      while (std::getline(sstat, stat_line)) {
        if (util::starts_with(stat_line, "cpu ")) {
          // cpu total
        } else if (util::starts_with(stat_line, "cpu")) {
          // specific cpu
          int id;
          unsigned long utime, ntime, stime, itime;
          unsigned long iowtime, irqtime, sirqtime;

          int count = sscanf(stat_line.data(), "cpu%d %lu %lu %lu %lu %lu %lu %lu",
            &id, &utime, &ntime, &stime, &itime, &iowtime, &irqtime, &sirqtime);

          auto ltimeo = orphanage.newOrphan<cereal::ProcLog::CPUTimes>();
          auto ltime = ltimeo.get();
          ltime.setCpuNum(id);
          ltime.setUser(utime / jiffy);
          ltime.setNice(ntime / jiffy);
          ltime.setSystem(stime / jiffy);
          ltime.setIdle(itime / jiffy);
          ltime.setIowait(iowtime / jiffy);
          ltime.setIrq(irqtime / jiffy);
          ltime.setSoftirq(irqtime / jiffy);

          otimes.push_back(std::move(ltimeo));

        } else {
          break;
        }
      }

      auto ltimes = procLog.initCpuTimes(otimes.size());
      for (size_t i = 0; i < otimes.size(); i++) {
        ltimes.adoptWithCaveats(i, std::move(otimes[i]));
      }
    }

    // meminfo
    {
      auto mem = procLog.initMem();

      std::ifstream smem("/proc/meminfo");
      std::string mem_line;
      
      uint64_t mem_total = 0, mem_free = 0, mem_available = 0, mem_buffers = 0;
      uint64_t mem_cached = 0, mem_active = 0, mem_inactive = 0, mem_shared = 0;

      while (std::getline(smem, mem_line)) {
        if (util::starts_with(mem_line, "MemTotal:")) sscanf(mem_line.data(), "MemTotal: %lu kB", &mem_total);
        else if (util::starts_with(mem_line, "MemFree:")) sscanf(mem_line.data(), "MemFree: %lu kB", &mem_free);
        else if (util::starts_with(mem_line, "MemAvailable:")) sscanf(mem_line.data(), "MemAvailable: %lu kB", &mem_available);
        else if (util::starts_with(mem_line, "Buffers:")) sscanf(mem_line.data(), "Buffers: %lu kB", &mem_buffers);
        else if (util::starts_with(mem_line, "Cached:")) sscanf(mem_line.data(), "Cached: %lu kB", &mem_cached);
        else if (util::starts_with(mem_line, "Active:")) sscanf(mem_line.data(), "Active: %lu kB", &mem_active);
        else if (util::starts_with(mem_line, "Inactive:")) sscanf(mem_line.data(), "Inactive: %lu kB", &mem_inactive);
        else if (util::starts_with(mem_line, "Shmem:")) sscanf(mem_line.data(), "Shmem: %lu kB", &mem_shared);
      }

      mem.setTotal(mem_total * 1024);
      mem.setFree(mem_free * 1024);
      mem.setAvailable(mem_available * 1024);
      mem.setBuffers(mem_buffers * 1024);
      mem.setCached(mem_cached * 1024);
      mem.setActive(mem_active * 1024);
      mem.setInactive(mem_inactive * 1024);
      mem.setShared(mem_shared * 1024);
    }

    // processes
    {
      std::vector<capnp::Orphan<cereal::ProcLog::Process>> oprocs;
      struct dirent *de = NULL;
      DIR *d = opendir("/proc");
      assert(d);
      while ((de = readdir(d))) {
        if (!isdigit(de->d_name[0])) continue;
        pid_t pid = atoi(de->d_name);


        auto lproco = orphanage.newOrphan<cereal::ProcLog::Process>();
        auto lproc = lproco.get();

        lproc.setPid(pid);

        char tcomm[PATH_MAX] = {0};

        {
          std::string stat = util::read_file(util::string_format("/proc/%d/stat", pid));

          char state;

          int ppid;
          unsigned long utime, stime;
          long cutime, cstime, priority, nice, num_threads;
          unsigned long long starttime;
          unsigned long vms, rss;
          int processor;

          int count = sscanf(stat.data(),
            "%*d (%1024[^)]) %c %d %*d %*d %*d %*d %*d %*d %*d %*d %*d "
             "%lu %lu %ld %ld %ld %ld %ld %*d %lld "
             "%lu %lu %*d %*d %*d %*d %*d %*d %*d "
             "%*d %*d %*d %*d %*d %*d %*d %d",
            tcomm, &state, &ppid,
            &utime, &stime, &cutime, &cstime, &priority, &nice, &num_threads, &starttime,
            &vms, &rss, &processor);

          if (count != 14) continue;

          lproc.setState(state);
          lproc.setPpid(ppid);
          lproc.setCpuUser(utime / jiffy);
          lproc.setCpuSystem(stime / jiffy);
          lproc.setCpuChildrenUser(cutime / jiffy);
          lproc.setCpuChildrenSystem(cstime / jiffy);
          lproc.setPriority(priority);
          lproc.setNice(nice);
          lproc.setNumThreads(num_threads);
          lproc.setStartTime(starttime / jiffy);
          lproc.setMemVms(vms);
          lproc.setMemRss((uint64_t)rss * page_size);
          lproc.setProcessor(processor);
        }

        std::string name(tcomm);
        lproc.setName(name);

        // populate other things from cache
        auto cache_it = proc_cache.find(pid);
        ProcCache cache;
        if (cache_it != proc_cache.end()) {
          cache = cache_it->second;
        }
        if (cache_it == proc_cache.end() || cache.name != name) {
          cache = (ProcCache){
            .name = name,
            .exe = util::readlink(util::string_format("/proc/%d/exe", pid)),
          };

          // null-delimited cmdline arguments to vector
          std::string cmdline_s = util::read_file(util::string_format("/proc/%d/cmdline", pid));
          const char* cmdline_p = cmdline_s.c_str();
          const char* cmdline_ep = cmdline_p + cmdline_s.size();
          
          // strip trailing null bytes
          while ((cmdline_ep-1) > cmdline_p && *(cmdline_ep-1) == 0) {
            cmdline_ep--;
          }

          while (cmdline_p < cmdline_ep) {
            std::string arg(cmdline_p);
            cache.cmdline.push_back(arg);
            cmdline_p += arg.size() + 1;
          }

          proc_cache[pid] = cache;
        }

        auto lcmdline = lproc.initCmdline(cache.cmdline.size());
        for (size_t i = 0; i < lcmdline.size(); i++) {
          lcmdline.set(i, cache.cmdline[i]);
        }
        lproc.setExe(cache.exe);

        oprocs.push_back(std::move(lproco));
      }
      closedir(d);

      auto lprocs = procLog.initProcs(oprocs.size());
      for (size_t i = 0; i < oprocs.size(); i++) {
        lprocs.adoptWithCaveats(i, std::move(oprocs[i]));
      }
    }

    auto words = capnp::messageToFlatArray(msg);
    auto bytes = words.asBytes();
    zmq_send(publisher, bytes.begin(), bytes.size(), 0);

    usleep(2000000); // 2 secs
  }

  return 0;
}
