#!/usr/bin/perl
#
# zeffix - Zentrale Zeit Erfassung (X version)
#
# Thomas Linden <tom@daemon.de>.
#
# Copyright (c) 2002 Thomas Linden.

use strict;
use Tk;
use Tk::Dialog;
use Data::Dumper;

sub mprint;
sub xdie;
sub popup;

my $VERSION = "1.9.9";
my $dumped = 0;

my $base = $ENV{HOME} . "/.zeff";

my $arg = shift;
if ($arg) {
  if ($arg eq "-v" || $arg eq "--version") {
    print "$0 version $VERSION\n";
    $dumped = 1;
    exit;
  }
  elsif ($arg eq "-h" || $arg eq "--help") {
    print qq(
 zeffix - Zentrale Zeit Erfassung (X version)
        Reads the config file, which contains all valid cost centers.
        Just execute it and choose the current cost center you are working
        for. Choose another cost center if you are finished with the last
        one. Choose "stop" or close the window if you are finished with your workday.

        zeffix writes the time data in a comma separated file in your home directory:

           ~/.zeff/zeff.MMYYYY.csv.

        If it finds existing entries of the current day in the data file it
        will accumulate them with the data collected so far.

 zeffix version $VERSION. COPYRIGHT (c) 2002 Thomas Linden.
    );
    $dumped = 1;
    exit;
  }
  elsif ($arg eq "--debug") {
    $base = ".zeff";
  }
}


my (%config, %zeff, $current, $current_time, $start_time);

if (!-d $base) {
  system("mkdir", "-p", "$base") and xdie "Could not create directory $base!\n";
}


my $cur_day = &getday;



$current = "default";
$current_time = time;
$start_time = $current_time;

local $| = 1;

$SIG{INT}  = \&sigend;
$SIG{TERM} = \&sigend;



my $ms = 480; # milliseconds to sleep between timer updates



##############################################
#           Main X widgets drawing           #
##############################################

my $X = new MainWindow;

my $config_file = &find_conf;

%config = &readconf($config_file);

my $selected = " default - " . $config{default}->{description};

my %FrameConf = (
		 -side   => 'top',
		 -expand => 'yes',
		 -fill   => 'x',
		 -padx   => 2,
		 -pady   => 2
		);

my $top  = $X->Frame->pack(%FrameConf);
my $down = $X->Frame->pack(%FrameConf);

$top->Label(-text => 'Task:')->pack(-side => 'left');


my $list = $top->Optionmenu(
			    -options   => [],
			    -relief    => 'raised',
			    -width     => 30,
			    -variable  => \$selected,
			    -command   => sub { &update_list; &entry_changed },
			   );

$list->pack(-expand => 1, -fill => 'both', -side => 'right');

&update_list;

my $status_bar = $down->Label(-relief => 'sunken');
$status_bar->pack(-side => 'left', -fill => 'both', -expand => 1);

my $time_bar = $down->Label(-relief => 'sunken', -width => 8);
$time_bar->pack(-side => 'right' );

$list->focus;
$X->focusNext;
$X->title("zeffix - $VERSION");


my $popup = $X->Menu(
		     -menuitems =>
		     [
		      [ Button => 'Edit Timer', -command =>
			sub { &ask4newtime(); }
		      ],
		      [ Button => 'Save Time Data', -command =>
			sub {
			  &add_time($current, $current_time - $start_time);
			  &dump;
			  $current_time = $start_time = time;
			}
		      ],
		      [ Button => 'Reset Timer', -command =>
			sub {
			  $current_time = $start_time = time;
			}
		      ],
		      undef,
		      [ Button => 'Create hh:mm Excel File', -command =>
			sub {
			  &create_excel("");
			}
		      ],
		      [ Button => 'Create decimal Excel File', -command =>
			sub {
			  &create_excel("-d");
			}
		      ],
		      undef,
		      [ Button => 'About zeffix...', -command =>
			sub {
			  popup "zeffix version $VERSION.\nby Thomas Linden.";
			}
		      ],
		      undef,
		      [ Button => 'Quit', -command => \&sigend ],
		     ]
		     );

$X->bind('<Button-3>' => sub {
	                     $popup->Popup(
					   -popanchor  => 'c',
					   -overanchor => 'c',
					   -popover    => 'cursor',
					  );
			     }
       );

$X->bind("<FocusOut>" => \&refresh_status);
$X->protocol('WM_DELETE_WINDOW' => \&sigend);

my $counter = $X->repeat($ms, \&repaint_time);

MainLoop;

exit;



#
# END of script ###############################
#


sub create_excel {
  my $mode = shift;
  my $month = &getmonth;
  my $db    = $base . "/zeff." . $month . ".csv";
  if (-e $db) {
    my $s;
    open NULL, "</dev/null";
    system ('zeffex', $mode, $db) and $s = 1;
    if ($s) {
      popup "Could not create Excel file:\n$! !";
      return;
    }
  }
  else {
    popup "Could not create Excel file. No time data available so far!";
    return;
  }
  my $excel = $db;
  $excel =~ s/\.csv$/\.xls/;
  popup "$excel created.";
}


sub repaint_time {
  my $msg;
  if ($current eq "stop" ) {
    return;
  }
  else {
    $msg = &sec2sec(time - $start_time);
  }
  $time_bar->configure(-text => $msg);
}



sub refresh_status {
  #
  # add the current cost to the status bar
  #
  my $msg = $current;
  if (exists $config{$current}) {
    $msg .= " (" . $config{$current}->{number} . ")";
  }
  mprint $msg;
}


sub mprint {
  #
  # print something to the status bar
  #
  my $msg = shift;
  $status_bar->configure(-text => $msg);
}


sub entry_changed {
  #
  # cost center has changed. save the collected time
  # if any
  #
  my $new = $selected;
  $new =~ s/\s\-\s.*$//;
  $new =~ s/\s*//;
  mprint $new;
  return if ($new eq $current);
  &repaint_time;
  $current_time = time;

  # last task was stop
  if ($current eq "stop") {
    $current = $new;
    $current_time = time;
    $start_time = $current_time;
    $dumped = 0;
    $counter = $X->repeat($ms, \&repaint_time);
    return;
  }
  # save the diff
  &add_time($current, $current_time - $start_time);
  mprint "Adding time " . &sec2hours($current_time - $start_time) . " to \"$current\"";
  $start_time = $current_time;
  #mprint $new;
  my $day = &getday;
  if ($new eq "stop") {
    # finished for today
    $day = $cur_day = &getday;
    &dump;
    $dumped = 1;
    $time_bar->configure(-text => "00:00:00");
    $X->afterCancel($counter);
    $new = "stop";
  }
  if ($day ne $cur_day) {
    # the date changed, use another dump file
    $cur_day = $day;
    &dump;
  }
  $current = $new;
}


sub update_list {
  #
  # callback sub, updates the selection
  # list with the current content of
  # the global config
  #
  my $maxlen = 0;
  %config = &readconf($config_file);
  my (@options, $default);
  foreach my $short (sort keys %config) {
    my $out = " " . $short . " - " . $config{$short}->{description};
    if ($short eq "default") {
      $default = $out;
    }
    else {
      push @options, $out;
      if (length($out) > $maxlen) {
	$maxlen = length($out);
      }
    }
  }
  @options = ($default, @options);
  push @options, " pause - Pause machen!";
  push @options, " stop - Feierabend!  :-)";

  # this causes this error (and only once!):
  # Can't set -options to `ARRAY(0x831f03c)' for Tk::Optionmenu=HASH(0x83727e0)
  # I can't figure it out, therefore I ignore it.
  eval { $list->configure(-options =>  \@options ); };
}






sub sigend {
  #
  # received SIGINT or SIGTERM, saving
  # time data before exit
  #
  $X->bind("<FocusOut>" => sub {});
  mprint "finishing.";
  $current_time = time;
  &add_time($current, $current_time - $start_time);
  &dump;
  $dumped = 1;
  exit;
}


sub dump {
  #
  # save time data to csv file
  #
  if ($dumped) {
    # dump already done, do nothing
    return;
  }
  my $month = &getmonth;
  my $db    = $base . "/zeff." . $month . ".csv";
  my $list = sprintf "\n\n%-20s - %s\n", "Cost Center", "Time";
  $list .= "-----------------------\n";

  &check_existing_entries($db);
  open DB, ">>$db" or xdie "Could not open database $db: $!\n";

  foreach my $cost (sort keys %zeff) {
    next if(!$cost);
    print DB $cur_day . ";" . $cost . ";" . &sec2sec($zeff{$cost}) . "\n";
    $list .= sprintf "%-20s - %s\n", $cost, &sec2sec($zeff{$cost});
  }
  close DB;

  # reset data
  %zeff = ();

  &log_time("STOP issued. Dumped time date to $db");

  popup "Saved time data to $db.\n$list";
}


sub check_existing_entries {
  #
  # check if entries for today already exist
  # if yes, then accumulate them with the current
  # entries and write it back to the datafile (without
  # the accumulated entries, they got saved in &dump()
  #
  my $db = shift;
  my %ze;
  if (-e $db) {
    system "cp", "$db", "$db~"; # backup just in case
    open DB , "<$db~" or xdie "$db~ exists, but is not readable: $!\n";
    open DBO , ">$db"  or xdie "$db exists, but is not writable: $!\n";

    print DBO qq(#
# Time data file for zeff(ix)
# Do not edit manually.
#
# Format: DD.MM.YYYY;Cost Center;hh:mm:ss
#
# Cost center file used: $config_file
# Last edited by $0 $VERSION
#
);

    while (<DB>) {
      next if /^\s*#/; # ignore comments
      if (/^\Q$cur_day\E/) {
	# entry from today
	my($date,$cost,$hours) = split /;/;
	chomp $hours;
	my $time = &human2sec($hours);
	if (exists $zeff{$cost}) {
	  # add time
	  $zeff{$cost} += $time;
	}
	else {
	  $zeff{$cost} = $time;
	}
      }
      else {
	# preserve old entries
	print DBO;
      }
    }
    close DB;
    close DBO;
  }
}


sub human2sec {
  #
  # convert hh:mm:ss => %010d seconds since 1.1.1970
  #
  my $human = shift;
  my($hh, $mm, $ss) = split /:/, $human;
  return sprintf "%010d", ($hh * 3600) + ($mm * 60) + $ss;
}


sub add_time {
  #
  # add the collected seconds to the global
  # %zeff hash which contains the time data
  #
  my($current, $time) = @_;
  my $cost = $config{$current}->{number};
  if (exists $zeff{$cost}) {
    # add time
    $zeff{$cost} += $time;
  }
  else {
    $zeff{$cost} = $time;
  }
  &log_time("$current($cost) " . sec2sec($time) . " " . sprintf("%010d", $time));
}


sub log_time {
  #
  # log what has happend last
  #
  my $msg = shift;
  my $month = &getmonth;
  if (! -d "$base/log") {
    system ("mkdir", "$base/log") and xdie "Could not create log directory $base/log: $!\n";
  }
  my $logfile = "$base/log/$month.log";
  open LOG, ">>$logfile" or xdie "Could not open log file $logfile: $!\n";
  print LOG scalar localtime(time) . " $msg\n";
  close LOG;
}


sub readconf {
  #
  # read the global config file which contains
  # the cost centers into the global %config hash
  #
  my $file = shift;
  my %config;
  open CONF, "<$file" or xdie "Could not open config $file: $!\n";
  while (<CONF>) {
    chomp;
    next if /^\s*#/; # ignore comments
    next if /^\s*$/; # ignore empty lines
    s/#.*$//;        # remove comment from end of line, if any
    my($number, $name, $description) = split /:/;
    $config{$name} = {
		      name        => $name,
		      number      => $number,
		      description => $description,
		     };
  }
  close CONF;

  if (!exists $config{default}) {
    xdie "Your config file $file\n"
        ."does not contain a \"default\" entry.";
  }

  %config;
}



sub getmonth {
  #
  # return the current month+year (MMYYYY)
  #
  my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  $year += 1900;
  $mon += 1;
  return sprintf "%02d%04d", $mon, $year;
}



sub getday {
  #
  # return the current day (DD.MM.YYYY)
  #
  my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  $year += 1900;
  $mon += 1;
  return sprintf "%02d.%02d.%04d", $mday, $mon, $year;
}



sub sec2hours {
  #
  # convert given seconds into
  # human readable hour value (YY:MM)
  # seconds > 30 will be rounded up to 1 minute
  #
  my $seconds = shift;

  my $hours = $seconds / 3600;
  my($hour, $min) = split /\./, $hours;

  $min =~ s/^(\d\d)\d*$/$1/;

  $min = int($min * 0.6);
  return sprintf "%02d:%02d", $hour, $min;
}

sub sec2sec {
  #
  # convert given seconds into
  # human readable hour value (YY:MM:SS)
  #
  my $seconds = shift;

  my $hours = int($seconds / 3600);

  my $min   = int(($seconds - ($hours * 3600)) / 60);

  my $secs  = $seconds - ($hours * 3600) - ($min * 60);

  return sprintf "%02d:%02d:%02d", $hours, $min, $secs;
}

sub xdie {
  #
  # die with a popup window
  #
  my $msg = shift;
  my $diag = $X->Dialog(
			-title => "zeffix Error",
			-text => $msg,
			-default_button => 'OK',
			-buttons => ['OK'],
		       );
  $X->bell;
  $diag->Show;
  exit -1;
}

sub popup {
  #
  # just a normal popup window
  #
  my $msg = shift;
  my $diag = $X->Dialog(
			-title          => "zeffix message",
			-text           => $msg,
			-default_button => 'OK',
			-buttons        => ['OK'],
			);
  $diag->Show;
}


sub ask4newtime {
  my $old = &sec2sec(time - $start_time);
  my $new = $old;

  $X->afterCancel($counter);

  my $Xn = new MainWindow;

  my $cmd = sub {
                  if ($new ne $old) {
		    if ($new =~ /^(\d+?):(\d\d):(\d\d)$/) {
		      my $duration = ($1 * 3600) + ($2 * 60) + $3;
		      $start_time  = time - $duration;
		    }
		  }
		  $counter = $X->repeat($ms, \&repaint_time);
		  $Xn->destroy;
		};

  my $frame = $Xn->Frame()->pack(
				-side   => 'top',
				-expand => 'yes',
				-fill   => 'x',
				-padx   => 2,
				-pady   => 2
			       );
  $frame->Label(-text => "Edit Timer (hh:mm:ss) ")->pack(-side => 'left');

  my $box = $frame->Entry(
			  -width => 8,
			  -textvariable => \$new,
			  -justify => 'right'
			 )->pack(-side => 'left', -expand => 'yes', -fill => 'x');

  my $button = $frame->Button(-text => 'OK', -command => $cmd)->pack(-side => 'right');

  $box->bind('<Return>' => $cmd);

  $Xn->protocol('WM_DELETE_WINDOW' => sub { $counter = $X->repeat($ms, \&repaint_time); $Xn->destroy;});

  $X->waitVariable(\$Xn);
  $X->waitWindow;
}


sub find_conf {
  #
  # look for the global config file
  #
  my $unix = "/etc/zeff.config";
  my $me   = "$base/config";
  return $unix if -e $unix;
  return $me   if -e $me;
}



END {
  #
  # this get's executed in case of abnormal exit
  #
  eval {
    $current_time = time;
    &add_time($current, $current_time - $start_time);
    &dump;
  };
}


1;



