#include <dirent.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "process.h"
#include "lxcstat.h"
#include "parsing.h"

#ifndef PF_KTHREAD
#define PF_KTHREAD 0x00200000
#endif

#define skipRange(inclusive, exclusive)                            \
  for (int i##inclusive = 0; i##inclusive < exclusive - inclusive; \
       i##inclusive++)                                             \
    location = strchr(location, ' ') + 1;

int parseStatFile(Process *proc, char *filedata) {
  char *location = filedata;
  // (1) pid - %d
  proc->pid = fast_str2ull(&location);
  location += 2;

  // (2) comm - %s
  char *end = strrchr(location, ')');
  size_t length = end - location;
  memcpy(proc->label, location, length);
  proc->label[length] = 0;
  location = end + 2;

  // (3) State - %c
  location += 2;

  // (4) ppid - %d
  proc->ppid = fast_str2ull(&location);
  location += 1;

  // skip [5 - 9)
  skipRange(5, 9);

  // (9) flags - %u
  proc->flags = fast_str2ull(&location);
  location += 1;

  proc->iskernel = (proc->flags & PF_KTHREAD) ? true : false;

  // skip [10 - 14)
  skipRange(10, 14);

  // (14) utime - %lu
  proc->cpuTime = fast_str2ull(&location);
  location += 1;

  // (15) stime - %lu
  proc->cpuTime += fast_str2ull(&location);
  location += 1;

  // (16) cutime - %lu
  proc->cpuTime += fast_str2ull(&location);
  location += 1;

  // (17) cstime - %lu
  proc->cpuTime += fast_str2ull(&location);
  proc->cCpuTime = proc->cpuTime;
  location += 1;

  // skip 18-19
  skipRange(18, 20);

  // (20) num_threads - %ld
  proc->nThreads = fast_str2ull(&location);
  location += 1;

  return 0;
}

int parseStatusFile(Process *proc, char *filedata) {
  proc->tgid = findAndParseField(&filedata, "Tgid");
  proc->pid = findAndParseField(&filedata, "Pid");

  if (proc->pid != proc->tgid)
    return -1;  // ignore child threads, as their stats are the same as the
                // parent

  proc->resident = 1024 * findAndParseField(&filedata, "VmRSS");
  proc->swap = 1024 * findAndParseField(&filedata, "VmSwap");
  proc->cMemory = proc->resident;
  proc->cSwap = proc->swap;
  return 0;
}

int parseIOFile(Process *proc, char *filedata) {
  proc->read = findAndParseField(&filedata, "read_bytes");
  proc->written = findAndParseField(&filedata, "write_bytes");
  proc->cRead = proc->read;
  proc->cWrite = proc->written;
  return 0;
}

int resetVisits(Process *head) {
  Process *current = head;
  int processCount = 0;

  while (current != NULL) {
    current->visited = 0;
    current = current->next;
    processCount++;
  }

  return processCount;
}


void linkFamily(Process *head) {
  int orphans = 1;

  while (orphans) {
    orphans = 0;
    Process *current = head;
    // while on an actual process node, and it has a parent link or doesn't need a parent link (kernel)
    while (current != NULL && (current->parent != NULL || current->ppid == current->pid)) current = current->next;
    if (current == NULL) break;
    orphans = 1;
    
    Process *parent = head;
    while (parent != NULL && parent->pid != current->ppid) parent = parent->next;
    if (parent == NULL) {
      printf("Found orphan process: %d\n", current->pid);
      current->ppid = -1;
      continue;
    }

    // We have a parent and a child ready to be united
    current->parent = parent;
    Process **placement = &(parent->child);
    while ((*placement) != NULL) placement = &((*placement)->sibling);
    *placement = current;
  }
}

void printFamilyTree(Process *head) {
  int processCount = resetVisits(head);
  int visited = 0;
  Process *current = head;
  int depth = 0;
  
  while (visited < processCount) {
    if (current == NULL) {
      printf("Bad visit count: %d visited of %d processes\n", visited, processCount);
      break;
    }

    current->visited = 1;
    visited++;

    printf("%10d  %10d  %1d  ", current->pid, current->ppid, current->iskernel);
    // `ps f` style tree
    for (int i=1; i<depth; i++) printf("    ");
    if (depth > 0) printf(" \\_ ");
    printf("%s\n", current->label);

nextProcess:
    if (current->child != NULL && !current->child->visited) {
      // process children first
      depth++;
      current = current->child; 
    } else if (current->sibling != NULL) {
      // process siblings next
      current = current->sibling;
    } else if (current->parent != NULL) {
      // return to parent when tree is exhausted
      depth--;
      current = current->parent;
      goto nextProcess;      // parent was already visited, so find next process from parent
    } else {
      // no parent - scan for unvisited process
      while (current != NULL && current->visited) current = current->next;
    }
  }
}

void aggregateStats(Process *head) {
  int processCount = resetVisits(head);
  Process *current = head;
  int depth = 0;
  int maxdepth = 0;
  int visited = 0;

  // assign depth (aka level) of each process
  while (visited < processCount) {
    if (current == NULL) {
      printf("Bad visit count: %d visited of %d processes\n", visited, processCount);
      break;
    }

    visited++;
    current->visited = visited; // store to tag the order the process is visited
    current->level = depth;

    if (depth > maxdepth) maxdepth = depth;

nextProcess:
    if (current->child != NULL && !current->child->visited) {
      // process children first
      depth++;
      current = current->child; 
    } else if (current->sibling != NULL) {
      // process siblings next
      current = current->sibling;
    } else if (current->parent != NULL) {
      // return to parent when tree is exhausted
      depth--;
      current = current->parent;
      goto nextProcess;      // parent was already visited, so find next process from parent
    } else {
      // no parent - scan for unvisited process
      while (current != NULL && current->visited) current = current->next;
    }
  }

  // scan for each depth level, tally stats upwards
  for (depth=maxdepth; depth>0; depth--) {
    current = head;
    while (current != NULL) {
      while (current != NULL && current->level != depth) current = current->next;
      if (current == NULL) continue;

      Process *parent = current->parent;
      parent->cCpuTime += current->cCpuTime;
      parent->cMemory += current->cMemory;
      parent->cSwap += current->cSwap;
      parent->cRead += current->cRead;
      parent->cWrite += current->cWrite;
      current = current->next;
    }
  }
}

char *readProcesses(char *procdir) {
  int len;
  struct dirent *pDirent;
  DIR *pDir;

  // Ensure we can open directory.

  pDir = opendir(procdir);
  if (pDir == NULL) {
    printf("Cannot open directory '%s'\n", procdir);
    return NULL;
  }

  // Process each entry.

  char *buffer = malloc(4096);
  char *fname = malloc(1024);
  char first;
  FILE *file;

  Process *head = calloc(1, sizeof(Process));
  Process *cur = head;
  Process *prev = NULL;
  lxcinfo *lxcs = NULL;

  // First process node is the kernel
  strcpy(cur->label, "kernel");
  cur->lxc = -1;
  cur->visited = 1;

  while ((pDirent = readdir(pDir)) != NULL) {
    first = pDirent->d_name[0];
    if (first < '0' || first > '9') continue;

    if (cur->visited) {
      // get new process if necessary
      prev = cur;
      cur->next = calloc(1, sizeof(Process));
      cur = cur->next;
    }

    sprintf(fname, "/proc/%s/status", pDirent->d_name);
    file = fopen(fname, "rb");
    if (file == NULL) continue;
    fread(buffer, 1, 4096, file);
    fclose(file);

    if (parseStatusFile(cur, buffer)) continue;

    // truncate 'status' to 'stat'
    len = strlen(fname);
    fname[len - 2] = 0;
    file = fopen(fname, "rb");
    if (file == NULL) continue;
    fread(buffer, 1, 4096, file);
    fclose(file);

    if (parseStatFile(cur, buffer)) continue;

    sprintf(fname, "/proc/%s/io", pDirent->d_name);
    file = fopen(fname, "rb");
    if (file == NULL) continue;
    fread(buffer, 1, 4096, file);
    fclose(file);

    if (parseIOFile(cur, buffer)) continue;

    sprintf(fname, "/proc/%s/cpuset", pDirent->d_name);
    file = fopen(fname, "rb");
    if (file == NULL) goto nextProcess;
    fread(cur->cpuset, 1, sizeof(cur->cpuset), file);
    fclose(file);

    strtok(cur->cpuset, "\n\t ");
    char *lxcTag = "/lxc/";
    if (memcmp(cur->cpuset, lxcTag, strlen(lxcTag)) == 0) {
      // Resides in LXC -- read file and tag
      sscanf(cur->cpuset, "/lxc/%d/ns", &cur->lxc);
      lxcinfo *lxc = getLXCInfo(&lxcs, cur->lxc);
      if (lxc == NULL) {
        printf("Failed to read LXC config <%d>\n", cur->lxc);
      } else {
        lxc->running = 1;
      }
    } else {
      cur->lxc = -1;
    }

nextProcess:
    cur->visited = 1;
  }

  if (!cur->visited) {
    // if there's an unused node, remove it
    prev->next = NULL;
    free(cur);
  }
  
  linkFamily(head);
  aggregateStats(head);
  getInactiveLXCs(&lxcs);

  int clocks = sysconf(_SC_CLK_TCK);
  char *output = malloc(8 * 1024 * 1024);
  char *ptr = output;

  cur = head;
  while (cur != NULL) {
    // create process descriptor
    sprintf(buffer,
            "pid=\"%d\",ppid=\"%d\",label=\"%s\",lxc=\"%d\"",
            cur->pid, cur->ppid, cur->label, cur->lxc);
    // process specific stats
    ptr += sprintf(ptr, "process_cpu_time_seconds{%s} %f\n", buffer, (double) cur->cpuTime / clocks);
    ptr += sprintf(ptr, "process_num_threads{%s} %lu\n", buffer, cur->nThreads);
    ptr += sprintf(ptr, "process_resident_bytes{%s} %lu\n", buffer, cur->resident);
    ptr += sprintf(ptr, "process_swap_bytes{%s} %lu\n", buffer, cur->swap);
    ptr += sprintf(ptr, "process_fileio_bytes_written{%s} %lu\n", buffer, cur->written);
    ptr += sprintf(ptr, "process_fileio_bytes_read{%s} %lu\n", buffer, cur->read);
    ptr += sprintf(ptr, "process_is_kernel_process{%s} %d\n", buffer, cur->iskernel);
    // cumulative fields
    ptr += sprintf(ptr, "process_cumulative_cpu_time_seconds{%s} %lu\n", buffer, cur->cCpuTime);
    ptr += sprintf(ptr, "process_cumulative_resident_bytes{%s} %lu\n", buffer, cur->cMemory);
    ptr += sprintf(ptr, "process_cumulative_swap_bytes{%s} %lu\n", buffer, cur->cSwap);
    ptr += sprintf(ptr, "process_cumulative_bytes_read{%s} %lu\n", buffer, cur->cRead);
    ptr += sprintf(ptr, "process_cumulative_bytes_written{%s} %lu\n", buffer, cur->cWrite);

    // stats used to make flame chart
    ptr += sprintf(ptr, "process_tree_depth{%s} %d\n", buffer, cur->level);
    ptr += sprintf(ptr, "process_visit_index{%s} %d\n", buffer, cur->visited);
    // free and proceed
    Process *prev = cur;
    cur = cur->next;
    free(prev);
  }

  lxcinfo *lxc = lxcs;
  while (lxc != NULL) {
    sprintf(buffer, "lxc=\"%d\",lxcname=\"%s\"", lxc->lxcid, lxc->hostname);
    ptr += sprintf(ptr, "lxc_cpu_core_count{%s} %u\n", buffer, lxc->cpucount);
    ptr += sprintf(ptr, "lxc_memory_limit_bytes{%s} %llu\n", buffer, lxc->memlimit);
    ptr += sprintf(ptr, "lxc_swap_limit_bytes{%s} %llu\n", buffer, lxc->swaplimit);
    ptr += sprintf(ptr, "lxc_is_running{%s} %d\n", buffer, lxc->running);
    // free and proceed
    lxcinfo *prev = lxc;
    lxc = lxc->next;
    free(prev);
  }

  closedir(pDir);
  free(buffer);
  free(fname);
  return output;
}
