#define _GNU_SOURCE

#include "prometheus_process_collector.h"

#ifdef __linux__
static long pagesize(void)
{
  return sysconf(_SC_CLK_TCK);
}
#endif

#ifdef __FreeBSD__
static long pagesize(void)
{
  int pageSize;
  size_t len = sizeof(pageSize);
  if (sysctlbyname("vm.stats.vm.v_page_size", &pageSize, &len, NULL, 0) == -1) {
    pageSize = sysconf(_SC_PAGESIZE);
  }
  return pageSize;
}
#endif

#ifdef __FreeBSD__

/*
  from https://github.com/freebsd/freebsd/blob/9e0a154b0fd5fa9010238ac9497ec59f84167c92/lib/libutil/kinfo_getfile.c#L22-L51
  I don't need unpacked structs here, just count. Hope it won't break someday.
*/
static int get_process_open_fd_totals(pid_t pid, int* count)
{
  int mib[4];
  int error;
  int cnt;
  size_t len;
  char *buf, *eb;
  struct kinfo_file *kf;

  // get size of all pids
  len = 0;
  mib[0] = CTL_KERN;
  mib[1] = KERN_PROC;
  mib[2] = KERN_PROC_FILEDESC;
  mib[3] = pid;

  error = sysctl(mib, nitems(mib), NULL, &len, NULL, 0);
  if (error)
    return 1;

  // allocate buf for pids
  len = len * 4 / 3;
  buf = malloc(len);
  if (buf == NULL)
    return 1;

  // fill buf with kinfo_files
  error = sysctl(mib, nitems(mib), buf, &len, NULL, 0);
  if (error) {
    free(buf);
    return 1;
  }

  // count structs in the buf
  cnt = 0;
  eb = buf + len;
  while (buf < eb) {
    kf = (struct kinfo_file *)(uintptr_t)buf;
    if (kf->kf_structsize == 0)
      break;
    buf += kf->kf_structsize;
    cnt++;
  }

  free(buf-len);
  *count = cnt;

  return 0;
}

#endif

#ifdef __linux__
static int get_process_open_fd_totals(pid_t pid, int* count)
{
  static char fd_path[32];

  sprintf(fd_path, "/proc/%d/fd", pid);

  int file_total = 0;
  DIR* dirp;
  struct dirent* entry;
  dirp = opendir(fd_path);
  if (dirp == NULL) {
    return 1;
  }
  while ((entry = readdir(dirp)) != NULL) {
    if (entry->d_type == DT_LNK) {
      file_total++;
    }
  }
  closedir(dirp);
  *count = file_total;
  return 0;
}

#endif

#ifdef __APPLE__
static int get_process_open_fd_totals(pid_t pid, int* count)
{
  int pidinfo_result = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, 0, 0);
  if(pidinfo_result < 0) {
    // printf("Can't get open fds");
    return 1;
  }
  struct proc_fdinfo *temp_buffer;
  temp_buffer = malloc(pidinfo_result);

  pidinfo_result = proc_pidinfo(pid, PROC_PIDLISTFDS, 0,
                                temp_buffer, pidinfo_result);

  free(temp_buffer);

  if(pidinfo_result < 0) {
    // printf("Can't get open fds");
    return 1;
  }
  *count = pidinfo_result / PROC_PIDLISTFD_SIZE;
  return 0;
}
#endif

#ifdef __FreeBSD__
static int get_process_limit(pid_t pid, int resource, struct rlimit* rlp)
{
  int name[5];
  size_t len;

  name[0] = CTL_KERN;
  name[1] = KERN_PROC;
  name[2] = KERN_PROC_RLIMIT;
  name[3] = pid;
  name[4] = resource;
  len = sizeof(*rlp);
  return sysctl(name, 5, rlp, &len, NULL, 0);
}
#endif

#if defined(__linux__) || defined(__APPLE__)
static int get_process_limit(pid_t pid, int resource, struct rlimit* rlp)
{
  UNUSED(pid);
  return getrlimit(resource, rlp);
}
#endif

#ifdef __linux__

static int clk_tck(long *clk_tck)
{
  long result = sysconf(_SC_CLK_TCK);

  if(result == -1 || result == 0){
    // printf("failed on clk_tck\r\n");
    return 1;
  }

  *clk_tck = result;
  return 0;
}

static int system_boot_time(long *boot_time)
{
  FILE *fd;
  char *stat_line = NULL;
  size_t len = 0;
  ssize_t read;

  fd = fopen("/proc/stat", "r");
  if (fd == NULL) {
    // printf("failed on /proc/stat\r\n");
    return 1;
  }

  while ((read = getline(&stat_line, &len, fd)) != -1 &&
         !sscanf(stat_line, "btime %ld", boot_time)) {
  }

  fclose(fd);
  if (stat_line)
    free(stat_line);


  return 0;
}

#define MAX_STAT 24

static unsigned long get_process_stat(int index, char *stat[]) {
  char *dummy;
  return strtoul(stat[index], &dummy, 10);
}

static struct kinfo_proc* kinfo_getproc(pid_t pid)
{
  long ticks;
  if(clk_tck(&ticks)) {
    // printf("failed on ticks");
    return NULL;
  }

  long boot_time;
  if(system_boot_time(&boot_time)) {
    // printf("failed on boottime\r\n");
    return NULL;
  }

  static char stat_path[32];
  sprintf(stat_path, "/proc/%d/stat", pid);

  char *stat_line = NULL;
  size_t len = 0;
  FILE *fd = fopen(stat_path, "r");
  if(!fd) {
    // printf("failed on stat\r\n");
    return NULL;
  }

  if(getline(&stat_line, &len, fd) == -1) {
    if (stat_line) {
      free(stat_line);
    }
    // printf("failed on getline\r\n");
    return NULL;
  }
  fclose(fd);

  char *old_stat_line = stat_line;
  int index = 0;
  char *stat[MAX_STAT + 1];

  while (index < MAX_STAT) {
    stat[index] = strsep(&stat_line, " ");
    index++;
  }

  struct kinfo_proc *proc = malloc(sizeof(struct kinfo_proc));

  struct timeval ki_start;
  ki_start.tv_sec = get_process_stat(21, stat)/ticks + boot_time;;
  proc->ki_start = ki_start;

  proc->ki_numthreads = get_process_stat(19, stat);

  proc->ki_size = get_process_stat(22, stat);

  proc->ki_rssize = get_process_stat(23, stat);

  struct rusage ki_rusage;
  getrusage(RUSAGE_SELF, &ki_rusage);
  /* ki_rusage.ru_utime.tv_sec = get_process_stat(13, stat)/ticks; */
  /* ki_rusage.ru_stime.tv_sec = get_process_stat(14, stat)/ticks; */
  proc->ki_rusage = ki_rusage;

  free(old_stat_line);
  return proc;
}
#endif

#if defined(__FreeBSD__) || defined(__APPLE__)
static struct kinfo_proc* kinfo_getproc(pid_t pid) {
  struct kinfo_proc *kipp;
  int mib[4];
  size_t len;

  len = 0;
  mib[0] = CTL_KERN;
  mib[1] = KERN_PROC;
  mib[2] = KERN_PROC_PID;
  mib[3] = pid;
  if (sysctl(mib, 4, NULL, &len, NULL, 0) < 0)
    return (NULL);

  kipp = malloc(len);
  if (kipp == NULL)
    return (NULL);

  if (sysctl(mib, 4, kipp, &len, NULL, 0) < 0)
    goto bad;
  if (len != sizeof(*kipp))
    goto bad;
#if defined(__FreeBSD__)
  if (kipp->ki_structsize != sizeof(*kipp))
    goto bad;
  if (kipp->ki_pid != pid)
    goto bad;
#endif
  return (kipp);
 bad:
  free(kipp);
  return (NULL);
}
#endif

int fill_prometheus_process_info(pid_t pid, struct prometheus_process_info* prometheus_process_info)
{
  struct kinfo_proc *proc = kinfo_getproc(pid);
  // printf("proc alloced at %p\r\n", proc);

  if(!proc) {
    // printf("failed on kinfo_getproc\r\n");
    return 1;
  }

  int fds_total;
  if(get_process_open_fd_totals(pid, &fds_total)) {
    // printf("failed on get open fds\r\n");
    return 1;
  }
  prometheus_process_info->fds_total = fds_total;

  struct rlimit rlimit;
  if(get_process_limit(pid, RLIMIT_NOFILE, &rlimit)) {
    // printf("failed on get process limits\r\n");
    return 1;
  } else {
    prometheus_process_info->fds_limit = rlimit.rlim_cur;
  }

  time_t now;
  time(&now);

#ifdef __APPLE__

  prometheus_process_info->start_time_seconds = proc->kp_proc.p_starttime.tv_sec;

  struct proc_taskinfo pti;

  if(sizeof(pti) == proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti))) {
    prometheus_process_info->uptime_seconds = now - prometheus_process_info->start_time_seconds;
    prometheus_process_info->threads_total = pti.pti_threadnum;
    prometheus_process_info->vm_bytes = pti.pti_virtual_size;
    prometheus_process_info->rm_bytes = pti.pti_resident_size;
  } else {
    // printf("failed on proc_pidinfo")
    return 1;
  }
#else
  prometheus_process_info->start_time_seconds = proc->ki_start.tv_sec;

  prometheus_process_info->uptime_seconds = now - proc->ki_start.tv_sec;

  prometheus_process_info->threads_total = proc->ki_numthreads;

  prometheus_process_info->vm_bytes = proc->ki_size;

  prometheus_process_info->rm_bytes = proc->ki_rssize * pagesize();
#endif

#ifdef __APPLE__
  struct rusage rusage;
  getrusage(RUSAGE_SELF, &rusage);
#else
  struct rusage rusage = proc->ki_rusage;
#endif
  // printf("MAXRSS: %ld\r\n", rusage.ru_inblock),
  prometheus_process_info->utime_seconds = rusage.ru_utime.tv_sec + rusage.ru_utime.tv_usec/1000000.00;
  prometheus_process_info->stime_seconds = rusage.ru_stime.tv_sec + rusage.ru_stime.tv_usec/1000000.00;
  prometheus_process_info->max_rm_bytes = rusage.ru_maxrss * 1024;
  prometheus_process_info->noio_pagefaults_total = rusage.ru_minflt;
  prometheus_process_info->io_pagefaults_total = rusage.ru_majflt;
  prometheus_process_info->swaps_total = rusage.ru_nswap;
  prometheus_process_info->disk_reads_total = rusage.ru_inblock;
  prometheus_process_info->disk_writes_total = rusage.ru_oublock;
  prometheus_process_info->signals_delivered_total = rusage.ru_nsignals;
  prometheus_process_info->voluntary_context_switches_total = rusage.ru_nvcsw;
  prometheus_process_info->involuntary_context_switches_total = rusage.ru_nivcsw;

  free(proc);

  return 0;
}

#ifdef __STANDALONE_TEST__
int main(int argc, char** argv) {

  if(argc != 2) {
    // printf("Usage: %s <iterations>\n", argv[0]);
    return 1;
  }


  char *dummy;
  unsigned long iterations = strtoul(argv[1], &dummy, 10);

  if(*dummy != '\0') {
    // printf("Iterations must be an integer\n");
    return 1;
  }

  pid_t pid = getpid();

  while(iterations--) {
    struct prometheus_process_info* prometheus_process_info = malloc(sizeof(struct prometheus_process_info));
    // printf("prometheus_process_info alloced at %p\r\n", prometheus_process_info);
    fill_prometheus_process_info(pid, prometheus_process_info);
    free(prometheus_process_info);
  }

  return 0;
}
#endif
