/// OS process functions
///
/// psutil is a good reference for cross-platform syscall voodoo:
/// https://github.com/giampaolo/psutil/tree/master/psutil/arch

// IWYU pragma: no_include <sys/param.h>

#include <assert.h>
#include <signal.h>
#include <stdbool.h>
#include <stddef.h>
#include <uv.h>

#ifdef MSWIN
# include <tlhelp32.h>
#endif

#if defined(__FreeBSD__)
# include <string.h>
# include <sys/types.h>
# include <sys/user.h>
#endif

#if defined(__NetBSD__) || defined(__OpenBSD__)
# include <sys/param.h>
#endif

#if defined(__APPLE__) || defined(BSD)
# include <sys/sysctl.h>

# include "nvim/macros_defs.h"
#endif

#if defined(__linux__)
# include <stdio.h>
#endif

#include "nvim/log.h"
#include "nvim/memory.h"
#include "nvim/os/proc.h"

#ifdef MSWIN
# include "nvim/api/private/helpers.h"
#endif

#include "os/proc.c.generated.h"

#ifdef MSWIN
static bool os_proc_tree_kill_rec(void *proc, int sig)
{
  if (proc == NULL) {
    return false;
  }
  PROCESSENTRY32 pe;
  DWORD pid = GetProcessId(proc);

  if (pid != 0) {
    HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (h != INVALID_HANDLE_VALUE) {
      pe.dwSize = sizeof(PROCESSENTRY32);
      if (!Process32First(h, &pe)) {
        goto theend;
      }
      do {
        if (pe.th32ParentProcessID == pid) {
          HANDLE ph = OpenProcess(PROCESS_ALL_ACCESS, false, pe.th32ProcessID);
          if (ph != NULL) {
            os_proc_tree_kill_rec(ph, sig);
            CloseHandle(ph);
          }
        }
      } while (Process32Next(h, &pe));
      CloseHandle(h);
    }
  }

theend:
  return (bool)TerminateProcess(proc, (unsigned)sig);
}
/// Kills process `pid` and its descendants recursively.
bool os_proc_tree_kill(int pid, int sig)
{
  assert(sig >= 0);
  assert(sig == SIGTERM || sig == SIGKILL);
  if (pid > 0) {
    ILOG("terminating process tree: %d", pid);
    HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, false, (DWORD)pid);
    return os_proc_tree_kill_rec(h, sig);
  } else {
    ELOG("invalid pid: %d", pid);
  }
  return false;
}
#else
/// Kills process group where `pid` is the process group leader.
bool os_proc_tree_kill(int pid, int sig)
{
  assert(sig == SIGTERM || sig == SIGKILL);
  if (pid == 0) {
    // Never kill self (pid=0).
    return false;
  }
  ILOG("sending %s to PID %d", sig == SIGTERM ? "SIGTERM" : "SIGKILL", -pid);
  return uv_kill(-pid, sig) == 0;
}
#endif

/// Gets the process ids of the immediate children of process `ppid`.
///
/// @param ppid Process to inspect.
/// @param[out,allocated] proc_list Child process ids.
/// @param[out] proc_count Number of child processes.
/// @return 0 on success, 1 if process not found, 2 on other error.
int os_proc_children(int ppid, int **proc_list, size_t *proc_count)
  FUNC_ATTR_NONNULL_ALL
{
  if (ppid < 0) {
    return 2;
  }

  int *temp = NULL;
  *proc_list = NULL;
  *proc_count = 0;

#ifdef MSWIN
  PROCESSENTRY32 pe;

  // Snapshot of all processes.
  HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  if (h == INVALID_HANDLE_VALUE) {
    return 2;
  }

  pe.dwSize = sizeof(PROCESSENTRY32);
  // Get root process.
  if (!Process32First(h, &pe)) {
    CloseHandle(h);
    return 2;
  }
  // Collect processes whose parent matches `ppid`.
  do {
    if (pe.th32ParentProcessID == (DWORD)ppid) {
      temp = xrealloc(temp, (*proc_count + 1) * sizeof(*temp));
      temp[*proc_count] = (int)pe.th32ProcessID;
      (*proc_count)++;
    }
  } while (Process32Next(h, &pe));
  CloseHandle(h);

#elif defined(__APPLE__) || defined(BSD)
# if defined(__APPLE__)
#  define KP_PID(o) o.kp_proc.p_pid
#  define KP_PPID(o) o.kp_eproc.e_ppid
# elif defined(__FreeBSD__)
#  define KP_PID(o) o.ki_pid
#  define KP_PPID(o) o.ki_ppid
# else
#  define KP_PID(o) o.p_pid
#  define KP_PPID(o) o.p_ppid
# endif
# ifdef __NetBSD__
  static int name[] = {
    CTL_KERN, KERN_PROC2, KERN_PROC_ALL, 0, (int)(sizeof(struct kinfo_proc2)), 0
  };
# else
  static int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 };
# endif

  // Get total process count.
  size_t len = 0;
  int rv = sysctl(name, ARRAY_SIZE(name) - 1, NULL, &len, NULL, 0);
  if (rv) {
    return 2;
  }

  // Get ALL processes.
# ifdef __NetBSD__
  struct kinfo_proc2 *p_list = xmalloc(len);
# else
  struct kinfo_proc *p_list = xmalloc(len);
# endif
  rv = sysctl(name, ARRAY_SIZE(name) - 1, p_list, &len, NULL, 0);
  if (rv) {
    xfree(p_list);
    return 2;
  }

  // Collect processes whose parent matches `ppid`.
  bool exists = false;
  size_t p_count = len / sizeof(*p_list);
  for (size_t i = 0; i < p_count; i++) {
    exists = exists || KP_PID(p_list[i]) == ppid;
    if (KP_PPID(p_list[i]) == ppid) {
      temp = xrealloc(temp, (*proc_count + 1) * sizeof(*temp));
      temp[*proc_count] = KP_PID(p_list[i]);
      (*proc_count)++;
    }
  }
  xfree(p_list);
  if (!exists) {
    return 1;  // Process not found.
  }

#elif defined(__linux__)
  char proc_p[256] = { 0 };
  // Collect processes whose parent matches `ppid`.
  // Rationale: children are defined in thread with same ID of process.
  snprintf(proc_p, sizeof(proc_p), "/proc/%d/task/%d/children", ppid, ppid);
  FILE *fp = fopen(proc_p, "r");
  if (fp == NULL) {
    return 2;  // Process not found, or /proc/…/children not supported.
  }
  int match_pid;
  while (fscanf(fp, "%d", &match_pid) > 0) {
    temp = xrealloc(temp, (*proc_count + 1) * sizeof(*temp));
    temp[*proc_count] = match_pid;
    (*proc_count)++;
  }
  fclose(fp);
#endif

  *proc_list = temp;
  return 0;
}

#ifdef MSWIN
/// Gets various properties of the process identified by `pid`.
///
/// @param pid Process to inspect.
/// @return Map of process properties, empty on error.
Dict os_proc_info(int pid, Arena *arena)
{
  Dict pinfo = ARRAY_DICT_INIT;
  PROCESSENTRY32 pe;

  // Snapshot of all processes.  This is used instead of:
  //    OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, …)
  // to avoid ERROR_PARTIAL_COPY.  https://stackoverflow.com/a/29942376
  HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  if (h == INVALID_HANDLE_VALUE) {
    return pinfo;  // Return empty.
  }

  pe.dwSize = sizeof(PROCESSENTRY32);
  // Get root process.
  if (!Process32First(h, &pe)) {
    CloseHandle(h);
    return pinfo;  // Return empty.
  }
  // Find the process.
  do {
    if (pe.th32ProcessID == (DWORD)pid) {
      break;
    }
  } while (Process32Next(h, &pe));
  CloseHandle(h);

  if (pe.th32ProcessID == (DWORD)pid) {
    pinfo = arena_dict(arena, 3);
    PUT_C(pinfo, "pid", INTEGER_OBJ(pid));
    PUT_C(pinfo, "ppid", INTEGER_OBJ((int)pe.th32ParentProcessID));
    PUT_C(pinfo, "name", CSTR_TO_ARENA_OBJ(arena, pe.szExeFile));
  }

  return pinfo;
}
#endif

/// Return true if process `pid` is running.
bool os_proc_running(int pid)
{
  int err = uv_kill(pid, 0);
  // If there is no error the process must be running.
  if (err == 0) {
    return true;
  }
  // If the error is ESRCH then the process is not running.
  if (err == UV_ESRCH) {
    return false;
  }
  // If the process is running and owned by another user we get EPERM.  With
  // other errors the process might be running, assuming it is then.
  return true;
}
