﻿#!/usr/bin/perl

use strict;
#use warnings;
use Proc::ProcessTable;
use X11::IdleTime;
use GD::Simple;
#use DateTime;
use POSIX qw(ceil);
use DB_File;


my $filename = 'actogram_disk.png';
my $webdir = '/home/superkuh/www/actogram/';
my $xsize = '8640'; # (8640 samples/day)
my $ysize = '80'; # in actogram() it's	$ysize = 80 + (60 * ceil((keys %active)/8640) - 1));
my $epochstodisplay = 604800; # 1 week in seconds
my $fontsize = 8; #px

my $idlethreshold = 60; #if no keyboard or mouse events register in X11 longer x seconds...
my $interval    = 10; # sleep interval of 10 seconds (8640 samples/day). If you change this NOTHING will work.
my $partofminute  =   0; # keep track of where in hour we are

my %ptable = {};
my %collection = {};
my %colormap = {};
my %active = {};
my %log = {};
my ($ACTIVEHANDLE, $LOGHANDLE);
#if (-e 'activelog') {
	$ACTIVEHANDLE = tie %active, "DB_File", "activelog_disk", O_RDWR|O_CREAT, 0666, $DB_HASH 
		or die "Cannot open file 'activelog_disk': $!\n";

	$LOGHANDLE = tie %log, "DB_File", "log_disk", O_RDWR|O_CREAT, 0666, $DB_HASH 
		or die "Cannot open file 'log_disk': $!\n";
#}
# %active = {}; #( time0 => fname, time1 => fname, ...); with time gaps
# %log = {};
	#%log = ( time0 => {
	#					PID0 => [fname, active_bool],
	#					PID1 => [fname, active_bool],
	#					PID2 => [fname, active_bool],
	#					},
	#		  time1 => {
	#					PID0 => [fname, active_bool],
	#					PID1 => [fname, active_bool],
	#					PID2 => [fname, active_bool],
	#					},
	#		...
	#);

if ((-e 'colormap2') && (-s 'colormap2' > 0)) {
	loadcolors(); # not using storable so it is human editable
} else {
	print "default colors\n";
	%colormap = (
	'idle' => 'white',
	'opera' => 'red',
	'xchat' => 'green',
	'evince' => 'blue',
	'geany' => 'yellow',
	'gnome-terminal' => 'purple',
	'nautilus' => 'gray',
	'gedit' => 'brown',
	'eog' => 'plum',
	'synergys' => 'black',
	'Tribes.exe' => 'orange'			
);
}

my $tobj = new Proc::ProcessTable; # create new process object

# forever loop, collecting stats every $intervar secs 
# and dumping them once an hour
while(1){
    &collectstats;
    &imagedumpandreset if ($partofminute >= 3600); #hour = 3600
    # &imagedumpandreset if ($partofminute >= 60); #hour = 3600
    sleep($interval);
}

sub collectstats {
	my $activePID;
 		   
    foreach my $process (@{$tobj->table}){ 
        # we should ignore ourselves
        next if ($process->pid(  ) == $$);       
        # save this process info pid => [fname, active_bool] for our next run
        $ptable{$process->pid()} = [ $process->fname(  ), "0"];
        # ignore this process if we saw it last iteration
        # next if ($last{$process->pid()} eq $process->fname(  ));
        $collection{$process->fname(  )}++;
    }

	#get the active xwindow pid and name
	my $crap = `xprop -root | grep "_NET_ACTIVE_WINDOW(WINDOW)"`;
	$crap =~ /(0x[0-9A-F]{1,7})$/i;
	my $hexwinid = $1;
	if ($hexwinid eq '0x0') {
		$hexwinid = "mouse on other computer";
		print "xwinID: $hexwinid\n";
		my $tpid = `pidof synergys`;
		chomp $tpid;
		$activePID = $tpid;
	} elsif ($hexwinid eq "0x300001a") {
		print "xwinID: $hexwinid\n";
		my $tpid = `pidof xmms`;
		chomp $tpid;
		$activePID = $tpid;
	} else {
		print "xwinID: $hexwinid\n";
		$activePID = `xprop -id $hexwinid _NET_WM_PID`;
		$activePID =~ /(\d{1,7})$/i;
		$activePID = $1;
	}

	my $idletime = GetIdleTime();
	#mark the pid of the active xwindow as being active (1)
	if ($idlethreshold < $idletime) {
    	$ptable{$activePID}[1] = 0;
    	print "Idle for $idletime! pid: $activePID: $ptable{$activePID}[1], name: $ptable{$activePID}[0]\n\n";
    } else {
    	$ptable{$activePID}[1] = 1;
    	print "pid: $activePID: $ptable{$activePID}[1], name: $ptable{$activePID}[0]\n\n";	
	}
    
    # save procceses list for this perl epoch time.
    my $timerightnow = time(); # moved to top of function
    $log{$timerightnow} = \%ptable;
	#$LOGHANDLE-> uhhh, how do I do this with DB_File?
    if ($idlethreshold < $idletime) {
		$ACTIVEHANDLE->put($timerightnow, 'idle');
    	# $active{$timerightnow} = 'idle';
	} else {
		$ACTIVEHANDLE->put($timerightnow, $ptable{$activePID}[0]);
		# $active{$timerightnow} = $ptable{$activePID}[0];
	}
     
	$ACTIVEHANDLE->sync();
	$LOGHANDLE->sync();
    savecolors(); # human editable
  
    $partofminute += $interval; 
}

# dump out the results and reset our counters
sub imagedumpandreset{
    print scalar localtime(time).("-"x50)."\n";
    actogram(); #generate the image 
    for (sort reverse_value_sort keys %collection){
        #write;
    }

    undef %collection;
    $partofminute = 0;
}

# (reverse) sort by values in %collection and by key name
sub reverse_value_sort{
    return $collection{$b} <=> $collection{$a} || $a cmp $b;
}

sub actogram {
	$ysize = 80 + (60 * ceil((keys %active)/8640) - 1);
	my $img = GD::Simple->new($xsize,$ysize);
	drawtimeline(0,33,$img);
	
	my @sorted = keys %active;
	@sorted = sort @sorted;

	my $x_pos = 0; 
	my $y_pos = 60;
	foreach my $index (@sorted) {
		if ($x_pos > 8640) {
			$x_pos = 0;
			$y_pos += 60;	
		}

		# $dt = DateTime->from_epoch( epoch => $index );
		# print "time: $index, name: $alltimesandactives{$index}\n"; 
		$img->moveTo($x_pos,$y_pos);
		$img->bgcolor(getcolor($active{$index}));
		$img->fgcolor(getcolor($active{$index}));
		$img->ellipse(2,40);
		$x_pos += 1;	
	}
	#add graph name to color key
	my @sorted2 = reverse sort (keys %colormap);
	my $x_pos2 = 2; 
	foreach my $app (@sorted2) {
		next if ($app eq 'idle');
		$img->moveTo($x_pos2,25);
		$img->fgcolor($colormap{$app});
		$img->fontsize($fontsize);
		# $img->string("$app:$colormap{$app}");
		$img->string("$app");
		my $wordwidth = (($fontsize - 1) * (length $app)) + 2;
		#$x_pos2 += 80;
		$x_pos2 += $wordwidth;
	}
	
	$img->moveTo(2, 10);
	$img->fgcolor('black');
	$img->string('Actogram: 24 hour graph of the active xwindow process name versus time. Each tick represents 1 minute.');
	# convert into png data
	open (ACTOGRAM,">","$filename");
	print ACTOGRAM $img->png;
	close ACTOGRAM;
	`cp $filename $webdir`;
}

sub drawtimeline { 
	# takes current x,y position and GD object, $img
	my ($x,$y,$img) = @_;
	
	# moves just below, draws horizontal line.
	$img->bgcolor('black');
    $img->fgcolor('black');
	$img->moveTo(0, $y);
   	$img->lineTo(8640, $y);
   	
   	# puts vertical dashes in every 6 pixels (60 seconds).
   	$img->moveTo(0, $y);
   	my $i = 0;
  	while ($i < 8640) {
  		$img->bgcolor('black');
		unless ($i % 360) { # hour ticks are red and large
			$img->bgcolor('red');
			$img->ellipse(2,15);
			$i += 6;
			next;	
		}
   		$img->moveTo($i, $y);
		$img->ellipse(2,10);
		$i += 6;
	}  	
}

sub getcolor {
	my $name = shift;
	
	if ($colormap{$name}) {
		#print "found color: $colormap{$name}\n";
		return $colormap{$name};
	} else {
		#get a new 'random' color.
		my @colors = GD::Simple->color_names;
		my $color = $colors[int(rand(scalar @colors))];

		#check to see if it's unique in a lame way
		for (values %colormap)  { #this will only catch SOME color collisions.
			if (/^$color$/i) {
				#print "1. this color, $color, is not unique.\n";
				$color = $colors[int(rand(scalar @colors))];
				#repeat. this is so lame.
				for (values %colormap)  {  # should catch most.
					if (/^$color$/i) {
						#print "2. this color, $color, is not unique.\n";
						$color = $colors[int(rand(scalar @colors))];			
					}	
				} 
			}		
		}
		
		print "new color for $name: $color\n";
		$colormap{$name} = $color;
		return $color;
	}
}

sub savecolors {
	open (COLORS, ">colormap2") or die "Can't save colormap.\n$!";
	while ( my ($name, $color) = each(%colormap) ) {
		print COLORS "$name:$color\n" if $name;
    }
    close COLORS;
}

sub loadcolors {
	print "loading colors from map\n";
	open (COLORS, "<colormap2") or die "Can't load colormap.\n$!";
	while (<COLORS>) {
		print;
		my ($name, $color) = split (/:/);
		chomp $color;
		chomp $name;
		if ($name && $color) {
			$colormap{$name} = $color;
		}
	}
    close COLORS;
    print "\n";
}

sub roundup
{
   my $number = shift;
   my $round = shift;

   if ($number % $round) {
      return (1 + int($number/$round)) * $round;
   }
   else {
      return $number;
   }
}

format STDOUT =
@<<<<<<<<<<<<<  @>>>>
$_,             $collection{$_}
.

format STDOUT_TOP =
Name            Count
--------------  -----
.

format footer =

.