#include <ctype.h>
#include <dirent.h>
#include <fcntl.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

struct thread_info {
    int pid;
    int tid;
    char name[64];
    unsigned long long exec_time;
    unsigned long long delay_time;
    unsigned long long run_count;
};

struct thread_table {
    size_t allocated;
    size_t active;
    struct thread_info *data;
};

enum {
    FLAG_BATCH = 1U << 0,
    FLAG_HIDE_IDLE = 1U << 1,
    FLAG_SHOW_THREADS = 1U << 2,
    FLAG_USE_ALTERNATE_SCREEN = 1U << 3,
};

static int time_dp = 9;
static int time_div = 1;
#define NS_TO_S_D(ns) \
    (uint32_t)((ns) / 1000000000), time_dp, ((uint32_t)((ns) % 1000000000) / time_div)

struct thread_table processes;
struct thread_table last_processes;
struct thread_table threads;
struct thread_table last_threads;

static void grow_table(struct thread_table *table)
{
    size_t size = table->allocated;
    struct thread_info *new_table;
    if (size < 128)
        size = 128;
    else
        size *= 2;
    
    new_table = realloc(table->data, size * sizeof(*table->data));
    if (new_table == NULL) {
        fprintf(stderr, "out of memory\n");
        exit(1);
    }
    table->data = new_table;
    table->allocated = size;
}

static struct thread_info *get_item(struct thread_table *table)
{
    if (table->active >= table->allocated)
        grow_table(table);
    return table->data + table->active;
}

static void commit_item(struct thread_table *table)
{
    table->active++;
}

static int read_line(char *line, size_t line_size)
{
    int fd;
    int len;
    fd = open(line, O_RDONLY);
    if(fd == 0)
        return -1;
    len = read(fd, line, line_size - 1);
    close(fd);
    if (len <= 0)
        return -1;
    line[len] = '\0';
    return 0;
}

static void add_thread(int pid, int tid, struct thread_info *proc_info)
{
    char line[1024];
    char *name, *name_end;
    size_t name_len;
    struct thread_info *info;
    if(tid == 0)
        info = get_item(&processes);
    else
        info = get_item(&threads);
    info->pid = pid;
    info->tid = tid;

    if(tid)
        sprintf(line, "/proc/%d/task/%d/schedstat", pid, tid);
    else
        sprintf(line, "/proc/%d/schedstat", pid);
    if (read_line(line, sizeof(line)))
        return;
    if(sscanf(line, "%llu %llu %llu",
              &info->exec_time, &info->delay_time, &info->run_count) != 3)
        return;
    if (proc_info) {
        proc_info->exec_time += info->exec_time;
        proc_info->delay_time += info->delay_time;
        proc_info->run_count += info->run_count;
    }

    name = NULL;
    if (!tid) {
        sprintf(line, "/proc/%d/cmdline", pid);
        if (read_line(line, sizeof(line)) == 0 && line[0]) {
            name = line;
            name_len = strlen(name);
        }
    }
    if (!name) {
        if (tid)
            sprintf(line, "/proc/%d/task/%d/stat", pid, tid);
        else
            sprintf(line, "/proc/%d/stat", pid);
        if (read_line(line, sizeof(line)))
            return;
        name = strchr(line, '(');
        if (name == NULL)
            return;
        name_end = strchr(name, ')');
        if (name_end == NULL)
            return;
        name++;
        name_len = name_end - name;
    }
    if (name_len >= sizeof(info->name))
        name_len = sizeof(info->name) - 1;
    memcpy(info->name, name, name_len);
    info->name[name_len] = '\0';
    if(tid == 0)
        commit_item(&processes);
    else
        commit_item(&threads);
}

static void add_threads(int pid, struct thread_info *proc_info)
{
    char path[1024];
    DIR *d;
    struct dirent *de;
    sprintf(path, "/proc/%d/task", pid);
    d = opendir(path);
    if(d == 0) return;
    while((de = readdir(d)) != 0){
        if(isdigit(de->d_name[0])){
            int tid = atoi(de->d_name);
            add_thread(pid, tid, proc_info);
        }
    }
    closedir(d);
}

static void print_threads(int pid, uint32_t flags)
{
    size_t i, j;
    for (i = 0; i < last_threads.active; i++) {
        int epid = last_threads.data[i].pid;
        int tid = last_threads.data[i].tid;
        if (epid != pid)
            continue;
        for (j = 0; j < threads.active; j++)
            if (tid == threads.data[j].tid)
                break;
        if (j == threads.active)
            printf(" %5u died\n", tid);
        else if (!(flags & FLAG_HIDE_IDLE) || threads.data[j].run_count - last_threads.data[i].run_count)
            printf(" %5u %2u.%0*u %2u.%0*u %5llu %5u.%0*u %5u.%0*u %7llu  %s\n", tid,
                NS_TO_S_D(threads.data[j].exec_time - last_threads.data[i].exec_time),
                NS_TO_S_D(threads.data[j].delay_time - last_threads.data[i].delay_time),
                threads.data[j].run_count - last_threads.data[i].run_count,
                NS_TO_S_D(threads.data[j].exec_time), NS_TO_S_D(threads.data[j].delay_time),
                threads.data[j].run_count, threads.data[j].name);
    }
}

static void update_table(DIR *d, uint32_t flags)
{
    size_t i, j;
    struct dirent *de;
    
    rewinddir(d);
    while((de = readdir(d)) != 0){
        if(isdigit(de->d_name[0])){
            int pid = atoi(de->d_name);
            struct thread_info *proc_info;
            add_thread(pid, 0, NULL);
            proc_info = &processes.data[processes.active - 1];
            proc_info->exec_time = 0;
            proc_info->delay_time = 0;
            proc_info->run_count = 0;
            add_threads(pid, proc_info);
        }
    }
    if (!(flags & FLAG_BATCH))
        printf("\e[H\e[0J");
    printf("Processes: %zu, Threads %zu\n", processes.active, threads.active);
    switch (time_dp) {
    case 3:
        printf("   TID --- SINCE LAST ---- ---------- TOTAL ----------\n");
        printf("  PID  EXEC_T  DELAY SCHED EXEC_TIME DELAY_TIM   SCHED NAME\n");
        break;
    case 6:
        printf("   TID ------ SINCE LAST -------    ------------ TOTAL -----------\n");
        printf("  PID  EXEC_TIME DELAY_TIM SCHED    EXEC_TIME   DELAY_TIME   SCHED NAME\n");
        break;
    default:
        printf("   TID    -------- SINCE LAST --------       ------------- TOTAL -------------\n");
        printf("  PID     EXEC_TIME   DELAY_TIME SCHED       EXEC_TIME      DELAY_TIME   SCHED NAME\n");
        break;
    }
    for (i = 0; i < last_processes.active; i++) {
        int pid = last_processes.data[i].pid;
        for (j = 0; j < processes.active; j++)
            if (pid == processes.data[j].pid)
                break;
        if (j == processes.active)
            printf("%5u died\n", pid);
        else if (!(flags & FLAG_HIDE_IDLE) || processes.data[j].run_count - last_processes.data[i].run_count) {
            printf("%5u  %2u.%0*u %2u.%0*u %5llu %5u.%0*u %5u.%0*u %7llu %s\n", pid,
                NS_TO_S_D(processes.data[j].exec_time - last_processes.data[i].exec_time),
                NS_TO_S_D(processes.data[j].delay_time - last_processes.data[i].delay_time),
                processes.data[j].run_count - last_processes.data[i].run_count,
                NS_TO_S_D(processes.data[j].exec_time), NS_TO_S_D(processes.data[j].delay_time),
                processes.data[j].run_count, processes.data[j].name);
            if (flags & FLAG_SHOW_THREADS)
                print_threads(pid, flags);
        }
    }

    {
        struct thread_table tmp;
        tmp = last_processes;
        last_processes = processes;
        processes = tmp;
        processes.active = 0;
        tmp = last_threads;
        last_threads = threads;
        threads = tmp;
        threads.active = 0;
    }
}

void
sig_abort(int signum)
{
    printf("\e[?47l");
    exit(0);
}


int schedtop_main(int argc, char **argv)
{
    int c;
    DIR *d;
    uint32_t flags = 0;    
    int delay = 3000000;
    float delay_f;

    while(1) {
        c = getopt(argc, argv, "d:ibtamun");
        if (c == EOF)
            break;
        switch (c) {
        case 'd':
            delay_f = atof(optarg);
            delay = delay_f * 1000000;
            break;
        case 'b':
            flags |= FLAG_BATCH;
            break;
        case 'i':
            flags |= FLAG_HIDE_IDLE;
            break;
        case 't':
            flags |= FLAG_SHOW_THREADS;
            break;
        case 'a':
            flags |= FLAG_USE_ALTERNATE_SCREEN;
            break;
        case 'm':
            time_dp = 3;
            time_div = 1000000;
            break;
        case 'u':
            time_dp = 6;
            time_div = 1000;
            break;
        case 'n':
            time_dp = 9;
            time_div = 1;
            break;
        }
    }

    d = opendir("/proc");
    if(d == 0) return -1;

    if (!(flags & FLAG_BATCH)) {
        if(flags & FLAG_USE_ALTERNATE_SCREEN) {
            signal(SIGINT, sig_abort);
            signal(SIGPIPE, sig_abort);
            signal(SIGTERM, sig_abort);
            printf("\e7\e[?47h");
        }
        printf("\e[2J");
    }
    while (1) {
        update_table(d, flags);
        usleep(delay);
    }
    closedir(d);
    return 0;
}