/*
 * top_three.c a slightly modified wmtop.c -- copied from the WindowMaker 
 * process view dock app which is:
 *
 * Modified by Adi Zaimi
 *
 * Derived by Dan Piponi dan@tanelorn.demon.co.uk
 * http://www.tanelorn.demon.co.uk
 * http://wmtop.sourceforge.net 
 * from code originally contained in wmsysmon by Dave Clark (clarkd@skynet.ca)
 * This software is licensed through the GNU General Public License.
 */

/*
 * Ensure there's an operating system defined. There is *no* default
 * because every OS has it's own way of revealing CPU/memory usage.
 * compile with gcc -DOS ...
 */
#if defined(FREEBSD)
#define OS_DEFINED
#endif        /* defined(FREEBSD) */

#if defined(LINUX)
#define OS_DEFINED
#endif        /* defined(LINUX) */

#if !defined(OS_DEFINED)
#error No operating system selected
#endif        /* !defined(OS_DEFINED) */

/******************************************/
/* Includes                               */
/******************************************/

#include "gkrelltop.h"

/******************************************/
/* Defines                                */
/******************************************/


/*
 * XXX: I shouldn't really use this BUFFER_LEN variable but scanf is so
 * lame and it'll take me a while to write a replacement.
 */
#define BUFFER_LEN 1024

#if defined(LINUX)
#define PROCFS_TEMPLATE "/proc/%d/stat"
#define PROCFS_CMDLINE_TEMPLATE "/proc/%d/cmdline"
#define PROCFS_IO_TEMPLATE "/proc/%d/io"
#endif        /* defined(LINUX) */

#if defined(FREEBSD)
#define PROCFS_TEMPLATE "/proc/%d/status"
#endif        /* defined(FREEBSD) */

/******************************************/
/* Globals                                */
/******************************************/

regex_t *exclusion_expression = 0;
int show_nice_processes   = 0;
int exclusionchanged      = 0;
enum modes pluginMode     = cpu;

static int previous_total = 0;
static int memory_total   = 0;



/******************************************/
/* Debug                                  */
/******************************************/

#if defined(DEBUG)
/*
 * Memory handler
 */
static int g_malloced = 0;

static void *gktop_malloc(int n)
{
    int *p = (int *) malloc(sizeof(int) + n);
    p[0] = n;
    g_malloced += n;
    return (void *) (p + 1);
}

static void gktop_free(void *n)
{
    int *p = (int *) n;
    g_malloced -= p[-1];
    free(p - 1);
}

static void show_memory()
{
    fprintf(stderr, "%d bytes allocated\n", g_malloced);
}
#else        /* defined(DEBUG) */
#define gktop_malloc malloc
#define gktop_free free
#endif        /* defined(DEBUG) */

static char *gktop_strdup(const char *s)
{
    return strcpy((char *) gktop_malloc(strlen(s) + 1), s);
}

/******************************************/
/* Process class                          */
/******************************************/

/*
 * Pointer to head of process list
 */
static struct process *first_process = 0;


/* find the process with the given pid by performing
 * a sequential search in the list. 
 * It could be slow if there are many processes in the list; 
 * may be better to use a hash table or a map from glib instead of a list
 */
static inline struct process *find_process(struct process *p, pid_t pid)
{
    while (p && p->pid != pid) 
        p = p->next;
    return p;
}

void print_process(struct process *p)
{
    printf("pid = %d; name = %s \n",p->pid, p->name);

}

static inline void delete_list(struct process *p)
{
    while (p) 
    {
        struct process * curr = p;

        if (p->name)
            gktop_free(p->name);
        p = p->next;

        gktop_free(curr);
    }
}

/* Remove process p from list. list is passed as &list to save loc */
static void remove_from_list(struct process **list, struct process *p)
{
    if (p->next) 
        p->next->previous = p->previous; //null at worse
    
    if (p->previous) 
        p->previous->next = p->next;     //null at worse
    else if(p == *list)
        *list = p->next; //*list will at the end point to null
    else
        printf("p->next is null and it should not be "
                "-- not part of the list? %d \n " , p->pid);
    p->next = 0;
    p->previous = 0;
}


/* insert process p at the end (tail) of the list */
static inline void insert_in_list(struct process **lhead, 
                struct process **ltail, struct process *p)
{
    if(*ltail) {
        (*ltail)->next = p;
        p->previous = *ltail;
    }
    else 
        *lhead = p;

    /* insert at the end */
    *ltail = p;
}



/*
 * Create a new process object and return it
 */
static struct process *new_process(int pid)
{
    struct process *process;
    process = gktop_malloc(sizeof(struct process));

    /*
     * Do stitching necessary for doubly linked list
     */
    process->name = 0;
    process->previous = 0;
    process->next = 0;

    process->pid = pid;
    process->previous_user_time = INT_MAX;
    process->previous_kernel_time = INT_MAX;
    process->include = 1;

    return process;
}


/******************************************/
/* Functions                              */
/******************************************/

static int process_parse_procfs(struct process *);
static int update_process_table(void);
static int calculate_cpu(struct process *);
static int calc_cpu_total(void);

#if defined(LINUX)
static int calc_mem_total(void);
static void calc_mem_each(void);
static void calc_io_each(void);
#endif

/******************************************/
/* Extract information from /proc         */
/******************************************/

/*
 * These are the guts that extract information out of /proc.
 * Anyone hoping to port wmtop should look here first.
 */
static int process_parse_procfs(struct process *process)
{
    char line[BUFFER_LEN] = {0};
    char filename[BUFFER_LEN] = {0};
    char procname[BUFFER_LEN] = {0};
    int  user_time = 0;
    int  kernel_time = 0;
#if defined(LINUX)
    int  nice_val = 0;
#endif        /* defined(LINUX) */
#if defined(FREEBSD)
    int  us = 0; 
    int  um = 0; 
    int  ks = 0;
    int  km = 0;
#endif        /* defined(FREEBSD) */

    snprintf(filename, sizeof(filename), PROCFS_TEMPLATE, process->pid);

    int ps = open(filename, O_RDONLY);
    if (ps < 0)
        /* The process must have finished in the last few jiffies!  */
        return 1;

    int rc = read(ps, line, sizeof(line));
    close(ps);
    if (rc < 0)
        return 1;

#if defined(LINUX)
    /* Extract cpu times from data in /proc filesystem */
    rc = sscanf(line,
            "%*s %s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %d %d %*s %*s %*s %d %*s %*s %*s %d %d",
            procname, &user_time, &kernel_time,&nice_val,
            &process->vsize, &process->rss);
    if (rc < 5)
        return 1;

    /* don't read the process name if we have done once already */
    if (!process->name) 
    {
        char *r, *q;
        char deparenthesised_name[BUFFER_LEN] = {0};
        int  endl = 0;

        /* Remove parentheses from the process name 
         * stored in /proc/ under Linux...  */
        r = procname + 1;
        /* remove any "kdeinit: " */
        if (r == strstr(r, "kdeinit")) {
            snprintf(filename, sizeof(filename), 
                    PROCFS_CMDLINE_TEMPLATE, process->pid);

            ps = open(filename, O_RDONLY);
            if (ps < 0)
                /*
                 * The process must have finished in the last few jiffies!
                 */
                return 1;

            endl = read(ps, line, sizeof(line));
            close(ps);

            /* null terminate the input */
            line[endl] = 0;
            /* account for "kdeinit: " */
            if ((char *) line == strstr(line, "kdeinit: "))
                r = ((char *) line) + 9;
            else
                r = (char *) line;

            q = deparenthesised_name;
            /* stop at space */
            while (*r && *r != ' ')
                *q++ = *r++;
            *q = 0;
        } else {
            q = deparenthesised_name;
            while (*r && *r != ')')
                *q++ = *r++;
            *q = 0;
        }
        process->name = gktop_strdup(deparenthesised_name);
    }

    process->rss *= getpagesize();

#endif        /* defined(LINUX) */

#if defined(FREEBSD)
    /*
     * Extract cpu times from data in /proc/<pid>/stat
     * XXX: Process name extractor for FreeBSD is untested right now.
     */
    rc = sscanf(line, "%s %*s %*s %*s %*s %*s %*s %*s %d,%d %d,%d",
        procname, &us, &um, &ks, &km);
    if (rc < 5)
        return 1;

    if (!process->name)
        process->name = gktop_strdup(procname);

    user_time = us * 1000 + um / 1000;
    kernel_time = ks * 1000 + km / 1000;
#endif        /* defined(FREEBSD) */


    /* store only the difference of the user_time */
    process->user_time = (process->previous_user_time == INT_MAX) ? 0 :
                user_time - process->previous_user_time;

    process->kernel_time = (process->previous_kernel_time == INT_MAX) ? 0 :
                kernel_time - process->previous_kernel_time;

    process->amount = process->user_time + process->kernel_time;

    /* backup the full values of these two for next cycle */
    process->previous_user_time   = user_time;
    process->previous_kernel_time = kernel_time;

#if defined(LINUX)
    // if i knew how to get the nice value 
    // from freebsd for a process...i hope sb. tell me
    
    /* set it to zero for niced processes */
    if(show_nice_processes == 0 && nice_val > 1) {
        process->user_time   = 0;
        process->kernel_time = 0;
    }
#endif

    return 0;
}


/******************************************/
/* Update process table                   */
/******************************************/
static int update_process_table()
{
    DIR *dir;
    struct dirent *entry;

    if (!(dir = opendir("/proc")))
        return 1;

    struct process * visited_list_head = 0;
    struct process * visited_list_tail = 0;

    /* Get list of processes from /proc directory */
    while ((entry = readdir(dir))) 
    {
        if (!entry) {           /* Problem reading list of processes */
            closedir(dir);
            return 1;
        }

        pid_t pid;
        if (sscanf(entry->d_name, "%d", &pid) > 0) {
            struct process *p;
            p = find_process(first_process, pid);

            if(p) {
                remove_from_list(&first_process, p);
            }
            else if (!p) {
                p = new_process(pid);
            }

            /* compute each process cpu usage */
            calculate_cpu(p);
            insert_in_list(&visited_list_head, &visited_list_tail, p);
        }
    }

    closedir(dir);

    //delete the items remaining in first_list--exited processes
    delete_list(first_process);
    first_process = visited_list_head;

    return 0;
}


/******************************************/
/* Get process structure for process pid  */
/******************************************/
static int calculate_cpu(struct process *process)
{
    int rc;

    /* compute each process cpu usage by reading /proc/<proc#>/stat */
    rc = process_parse_procfs(process);
    if (rc)
        return 1;

    /* Check name against the exclusion list */
    if (process->include && exclusion_expression
        && !regexec(exclusion_expression, process->name, 0, 0, 0)) {
        process->include = 0; /* if match, don't include it */
    }
    else if(exclusionchanged) {
        /* this ensures that the process will reapear after you remove
         * the exclusion expression... */
        process->include = 1;
        exclusionchanged=0;
    }
    if (!exclusion_expression) {
        process->include = 1;
    }

    return 0;
}


/******************************************/
/* Calculate cpu total                    */
/******************************************/
static int calc_cpu_total()
{
    int total;
#if defined(LINUX)
    char line[BUFFER_LEN] = { '\0' };
    int cpu = 0;
    int nice = 0;
    int system = 0;
    int idle = 0;

    int ps = open("/proc/stat", O_RDONLY);
    int rc = read(ps, line, sizeof(line));
    close(ps);
    if (rc < 0)
        return 0;
    sscanf(line, "%*s %d %d %d %d", &cpu, &nice, &system, &idle);
    total = cpu + nice + system + idle;

#endif        /* defined(LINUX) */

#if defined(FREEBSD)
    struct timeval tv;

    gettimeofday(&tv, 0);
    total = tv.tv_sec * 1000 + tv.tv_usec / 1000;
#endif        /* defined(FREEBSD) */

    int dt = total - previous_total;
    previous_total = total;

    if (dt < 0)
        dt = 0;

    return dt;
}


#if defined(LINUX)

/******************************************/
/* Calculate total memory                 */
/* This should be done only once because  */
/* memory does not change all of a sudden */
/******************************************/
static int calc_mem_total()
{
    int ps;
    char line[512];
    char *total;
    char *free;
    int rc;

    ps = open("/proc/meminfo", O_RDONLY);
    rc = read(ps, line, sizeof(line));
    close(ps);

    if (rc < 0 || (total = strstr(line, "MemTotal:")) == NULL || 
                    (free = strstr(line, "MemFree:")) == NULL) {
        return 0;
    } 
    else {
        return atoi(total+9)-atoi(free+9);
    }

}


/******************************************/
/* Calculate each processes memory        */
/******************************************/
static void calc_mem_each()
{
    struct process *p = first_process;
    while (p) {
        p->amount = (float)p->rss; /* total is in kb */
        p = p->next;
    }
}

/******************************************
 * Calculate each processes io stats      
 * io stats look as follows:
 * rchar: 44445340
 * wchar: 3348787
 * syscr: 28815
 * syscw: 9780
 * read_bytes: 27828224
 * write_bytes: 0
 *
 * the first two deal with bytes r/w from the vfs
 * the nexst two deal with read() and write() syscalls
 * and the last two deal with actual bytes r/w to disk
 *
 * The last two are what we need i think.
 *
 ******************************************/
static void calc_io_each()
{
    char line[BUFFER_LEN], filename[BUFFER_LEN];
    int ps;
    int rc;
    struct process *p = first_process;
    while (p) {    
        snprintf(filename,sizeof(filename), PROCFS_IO_TEMPLATE, p->pid);

        ps = open(filename, O_RDONLY);
        if (ps < 0) /*The process must have finished in the last few jiffies! */
        {
            p->amount = 0;
            continue;
        }
        p->previous_io_read = p->io_read;
        p->previous_io_write = p->io_write;

        /* the file gets read in one line */
        rc = read(ps, line, sizeof(line)); 

        /* scanf into variables only the last two r/w entries */
        sscanf(line,"%*s %*d %*s %*d %*s %*d %*s %*d %*s %d %*s %d ", 
                &p->io_read, &p->io_write) ;
        close(ps);

        p->amount = (p->io_read - p->previous_io_read ) + 
                    (p->io_write - p->previous_io_write); 
        p = p->next;
    }
}


/******************************************/
/* Calculate total io                     */
/* will get info by summing all the       */
/* other disk activity for all processes  */
/* this will miss activity from processes */
/* that dissapeared within the interval   */
/* but it is only the relative scaling    */
/* that really matters not the accurate % */
/******************************************/
static int calc_io_total()
{    
    int total = 1;
    struct process *p = first_process;
    while (p) {    
        total += p->amount;
        p = p->next;
    }
    return total;
}


#endif        /* defined(LINUX) */

/******************************************/
/* Find the top three processes           */
/******************************************/

/*
 * Result is stored in decreasing order in best[0-2].
 */
int gkrelltop_process_find_top_three(struct process **best)
{
    int n = 0;
    float coeficient = 0;

    update_process_table();    /* update the table with process list */

    if(pluginMode == cpu) {
        int total = calc_cpu_total(); /* calculate the total of the processor */
        if(total == 0)  
            return 0;
        coeficient = 100.0 / (float) total;
    }
#if defined(LINUX)
    else if(pluginMode == mem) { 
        if(memory_total == 0)
            memory_total = calc_mem_total();
        coeficient = 100.0 / ((float) memory_total * 1000);
        calc_mem_each();
    }
    else if(pluginMode == io) { 
        calc_io_each();
        int total = calc_io_total();
        if(total <= 1)  
            return 0;
        coeficient = 100.0 / (float) total; 
    }
#endif /* defined(LINUX) */


    /*
     * Insertion sort approach to skim top 3
     * can this be made more efficient?
     */
    struct process *p = first_process;
    while (p) {
        if (p->include && p->amount > 0) {
            if (!best[0] || p->amount > best[0]->amount) {
                best[2] = best[1];
                best[1] = best[0];
                best[0] = p;
                ++n;
            } else if (!best[1] || p->amount > best[1]->amount) {
                best[2] = best[1];
                best[1] = p;
                ++n;
            } else if (!best[2] || p->amount > best[2]->amount) {
                ++n;
                best[2] = p;
            }
        }
        p = p->next;
    }
    /* n can be greater than 3 so need to constrain value before returning */
    n =  n > 3 ? 3 : n;

    int i = 0;
    for(i = 0; i < n; i++)
        best[i]->amount *= coeficient; //to turn it to percentage

    return n;
}

void onexit_cleanup(void)
{
    delete_list(first_process);
}
