#!/usr/pkg/bin/perl
#
# This file is courtesy of Jonathan I. Kamens <jik@cam.ov.com>
# Any errors are entirely my fault! - HTA
#

require 'ctime.pl';
# If you cannot find errno.ph, just uncomment the following line, if
# EEXIST is 17 in your /usr/include/sys/errno.h file.
eval 'sub EEXIST {17;}';

# require 'errno.ph';

$SIG{'HUP'} = 'signal';
$SIG{'INT'} = 'signal';
$SIG{'QUIT'} = 'signal';

if ($ENV{"TIMETRACKDIR"}) {
    $timetrackdir = $ENV{"TIMETRACKDIR"};
} elsif ($ENV{"TIMEXDIR"}) {
    $timetrackdir = $ENV{"TIMEXDIR"};
} else {
    $timetrackdir = "$ENV{\"HOME\"}/.timex";
}

$projects = "$timetrackdir/projectlist";
$lock_file = "$timetrackdir/LOCK";
$time_file = &time_file;

@select_list = ();

&lock;
    
&read_projects;
&read_days_times;

$current_project = $projects[0];
$added_seconds = 0;

&status;

while (<>) {
    chop;
    &update_times;
    if (/^s(tatus|)/i) { # status
	&status;
	next;
    }
    elsif (/^l(ist|)/i) { # list
	@select_list = ();
	&list(@projects);
	next;
    }
    elsif (/^n(umber|)/i || 
	   (/^\s*[0-9]+\s*$/ && ($_ = "number " . $_))) { # select by number
	split;
	if (@select_list == 0) {
	    @select_list = @projects;
	}
	if ((@_ != 2) || ($_[1] !~ /^[0-9]+$/)) {
	    warn "Usage: number n\n";
	    next;
	}
	if ($_[1] > @select_list) {
	    warn "Number $_[1] is out of range\n";
	    next;
	}
	&goto_project($select_list[$_[1] - 1]);
	next;
    }
    elsif (m,^/.*/[gio]*$,) { # select by regexp
	@narrow_found_list = ();
	@wide_found_list = ();
	if (@select_list > 0) {
	    eval "\@narrow_found_list = grep($_, \@select_list);";
	    if ($@) {
		warn "Error executing expression $_: $@\n";
		warn "Usage: /perl regular expression/[gio]\n";
		next;
	    }
	}
	eval "\@wide_found_list = grep($_, \@projects);";
	if ($@) {
	    warn "Error executing expression $_: $@\n";
	    warn "Usage: /perl regular expression/[gio]\n";
	    next;
	}
	if (@wide_found_list == 0) {
	    warn "No matches for $_\n";
	    next;
	}
	elsif ((@select_list > 0) && (@narrow_found_list == 0)) {
	    warn "No matches for $_ in narrowed list; returning to full list.\n";
	    @select_list = ();
	}
	if (@narrow_found_list == 1) {
	    &goto_project($current_project = $narrow_found_list[0]);
	    next;
	}
	elsif (@narrow_found_list > 1) {
	    @select_list = @narrow_found_list;
	}
	elsif (@wide_found_list == 1) {
	    &goto_project($current_project = $wide_found_list[0]);
	    next;
	}
	else {
	    @select_list = @wide_found_list;
	}
	print "Select from:\n";
	&list(@select_list);
	next;
    }
    elsif (/^\+/) { # add to the current (5 minutes or number specified)
	split;
	if (@_ == 1) {
	    if ($_[0] ne "+") {
		warn "Usage: + [ minutes ]\n";
		next;
	    }
	    $toadd = 5;
	}
	else {
	    if ($_[1] !~ /^[0-9]+$/) {
		warn "Usage: + [ minutes ]\n";
		next;
	    }
	    $toadd = $_[1];
	}
	$toadd *= 60;
	$projects{$current_project} += $toadd;
	$added_seconds += $toadd;
	&status;
    }
    elsif (/^\-/) { # subtract from the current (5 minutes or number specified)
	split;
	if (@_ == 1) {
	    if ($_[0] ne "-") {
		warn "Usage: - [ minutes ]\n";
		next;
	    }
	    $toadd = 5;
	}
	else {
	    if ($_[1] !~ /^[0-9]+$/) {
		warn "Usage: - [ minutes ]\n";
		next;
	    }
	    $toadd = $_[1];
	}
	$toadd *= 60;
	if ($projects{$current_project} < $toadd) {
	    $toadd = $projects{$current_project};
	}
	$projects{$current_project} -= $toadd;
	$added_seconds -= $toadd;
	&status;
	next;
    }
    elsif (/^=/) { # set the value
	split;
	if ((@_ != 2) || ($_[1] !~ /^(([0-9]+):|)([0-9]+)$/) || ($2 > 59)) {
	    warn "Usage: = hours:minutes\n";
	}
	$new_value = 60 * (60 * $2 + $3);
	$added_seconds += $new_value - $projects{$current_project};
	$projects{$current_project} = $new_value;
	&status;
	next;
    }
    elsif (/^a(dd|)/i) {
	if (! s/^a(dd|)[ \t]//) {
	    warn "Usage: add project-name\n";
	    next;
	}
	if ($projects{$_}) {
	    warn "Duplicate project, not adding.\n";
	}
	else {
	    push(@projects, $_);
	    $projects{$_} = 0;
	}
	&goto_project($_);
	next;
    }
    elsif (/^p(ause|)/i) {
	$paused++;
	&status;
	next;
    }
    elsif (/^c(ontinue|)/i) {
	$paused = undef;
	&status;
	next;
    }
    elsif (/^x ?(\S+)?/) {
	if ($1) {
	    $display = $1;
	    $display .= ":0" if $display !~ /:\d$/;
	}
	&save("Starting X timetrack");
	unlink($lock_file);
	if ($display) {
	    system("titrax -display $display");
	} else {
	    system("titrax");
	}
	&lock;
	&read_projects;
	&read_days_times;
	&status;
    } elsif (/^q(uit|)/i) {
	&save("Normal exit");
	unlink($lock_file);
	exit(0);
    }
    elsif (/^h(elp|)/i || /^\?/) {
	&usage_message;
	next;
    }
    elsif (/^\s*$/) {
	&status;
    } else {
	warn "Unrecognized command: $_\n";
	&usage_message;
	next;
    }
}

sub usage_message {
    print <<EOF;
Commands:
s[tatus]	Display total time and current project title and time.
l[ist]		List projects with numbers and times.
/expr/		Match perl regexp against projects.  If one matches,
		select it.  If more than one match, list them with
		times and numbers.
[n[umber]] #	Select project # (from last "list" or "/expr/" command).
		(Just a number can be specified.)
+ [ # ]		Add # minutes to current project, or 5 if # omitted.
- [ # ]		Subtract # minutes from current project, or 5 if #
		omitted.
= [#:]#		Set the current project to the specified number of
		[hours and] minutes.
a[dd] project	Add the specified project and select it.
p[ause]		Pause the clock.
c[ontinue]	Restart the clock.  Selecting a project (with
		"number", "/expr/" or "add") also restarts the clock.
x [host]	Start the X-based timetrack, and wait for it to complete
q[uit]		Quit.
h[elp] or ?	Print this message.
EOF
}

sub goto_project {
    $paused = undef;
    $current_project = $_[0];
    &status;
}


sub lock {
    open(LOCKFILE, ">$lock_file.$$") || die "$0: opening $lock_file.$$: $!\n";
    chop($hostname = `hostname`);
    print LOCKFILE $hostname, " ", $$, "\n" || 
	die "$0: writing to $lock_file.$$: $!\n";
    close(LOCKFILE) || die "$0: closing $lock_file.$$: $!\n";
    
    $ret = link("$lock_file.$$", $lock_file);
    unlink("$lock_file.$$");
    
    if (! $ret) {
	if ($! != &EEXIST) {
	    die "$0: error linking $lock_file.$$ to $lock_file: $!\n";
	}
	if (open(LOCKFILE, $lock_file)) {
	    $host = <LOCKFILE>;
	    close(LOCKFILE);
	    ($host =~ s/ (.*)\n//) && ($pid = $1);
	    $host = "<unknown>" if (! $host);
	    $pid = "<unknown>" if (! $pid);
	    die "TIMETRACK is already running on host $host as process $pid.
If you are sure this is not so, delete the file $lock_file.\n";
	}
    }
}


sub time_file {
    local($sec,$min,$hour,$mday,$mon,$year) = localtime(time);

    sprintf("%s/%d-%02d-%02d", $timetrackdir, $year + 1900, 
	    $mon + 1, $mday);
}

sub read_projects {
    open(PROJECTS, $projects) || die "$0: couldn't open $projects: $!\n";
    %projects = ();
    @projects = ();

    while (<PROJECTS>) {
	chop;
	push(@projects, $_);
	$projects{$_} = 0;
    }
    close(PROJECTS);
}
   
sub read_days_times {
    local($hours, $minutes, $project);

    open(DAYS_TIMES, $time_file) || return;
    &reset_times;
    while (<DAYS_TIMES>) {
	chop;
	next if (/^\#/);
	if (! m/^ *([0-9]+):([0-9]{2}) (.*)/) {
	    warn "$0: bad line in $days_times: $_\n";
	    next;
	}
	$hours = $1;
	$minutes = $2;
	$project = $3;
	if (! defined($projects{$project})) {
	    warn "$0: bogus project in $days_times: $project\n";
	    next;
	}
	$projects{$project} = 60 * (60 * $hours + $minutes);
	$total_time += $projects{$project};
    }
    close(DAYS_TIMES);
    $current_time = time;
}

sub update_times {
    local($new_time) = time;
    local($difference) = $new_time - $current_time;

    if (&time_file ne $time_file) {
	# XXX assumes that less than 24 hours has passed
	($sec,$min,$hour) = localtime($new_time);
	$difference -= 60 * (60 * $hour + $min) + $sec;
	if (! $paused) {
	    $projects{$current_project} += $difference;
	}
	&save("Flushed at end of day");
	&reset_times;
	$time_file = &time_file;
	$difference = 60 * (60 * $hour + $min) + $sec;
    }
    if (! $paused) {
	$projects{$current_project} += $difference;
	$total_time += $difference;
	&save("Periodic save");
    }
    $current_time = $new_time;
}

sub time_string {
    local($in, $always_print) = @_;
    local($hours, $minutes, $seconds);

    $seconds = $in % 60;
    $in -= $seconds;
    $in /= 60;
    $minutes = $in % 60;
    $in -= $minutes;
    $hours = $in / 60;

    if ($hours || $minutes || $always_print) {
	sprintf("%d:%02d", $hours, $minutes);
    }
    else {
	"";
    }
}

sub status {
    printf("%sTOTAL: %s%s%s CURRENT: %s %s\n",
	   $paused ? "PAUSED " : "",
	   &time_string($total_time, 1),
	   $added_seconds < 0 ? "" : "+", int($added_seconds / 60),
	   &time_string($projects{$current_project}, 1),
	   $current_project);
}

sub list {
    local(@projects) = @_;

    for (0..$#projects) {
	 print($_ + 1, "\t", &time_string($projects{$projects[$_]}, 0), 
	       "\t", $projects[$_], "\n");
     }
}

sub save {
    local($reason) = @_;

    if (! open(TIME_FILE, ">$time_file")) {
	warn "$0: opening $time_file for write: $!\n";
	return;
    }

    eval {
	print TIME_FILE "# TIMETRACK.PL log saved at ", &ctime(time) || die "$!\n";
	print TIME_FILE "# $reason\n" || die "$!\n";
	for (@projects) {
	    next if (! $projects{$_});
	    print(TIME_FILE " ", &time_string($projects{$_}, 1), " ",
		  $_, "\n") || die "$!\n";
	}
    };

    if ($@) {
	warn "$0: writing to $time_file: $@";
	close(TIME_FILE);
	return;
    }
    close(TIME_FILE) || warn "$0: closing $time_file: $!\n";
    return;
}

sub reset_times {
    for (keys %projects) {
	$projects{$_} = 0;
    }
    $total_time = 0;
}

sub signal {
    &update_times;
    &save("Signal exit");
    unlink($lock_file);
    exit(1);
}
