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

use strict;
my $VERSION = "1.9.2";
my $dumped = 0;

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

my $arg = shift;
if ($arg eq "-v" || $arg eq "--version") {
  print "$0 version $VERSION\n";
  $dumped = 1;
  exit;
}
elsif ($arg eq "-h" || $arg eq "--help") {
  print qq(
 zeff - Zentrale Zeit Erfassung 
        Reads /tools/etc/zeff/config, 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. Enter "stop" or CTRL-C if you are finished with your workday.

        You can use short versions of the cost center names if they are
        unique. I.e. use "p" for "pause" if there is no other cost center
        starting with "p".

        zeff 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.

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


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

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


my $cur_day = &getday;



$current = "default";
($current_time, $current_start_time) = &getdate;

local $| = 1;

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

while (1) {
  #
  # endless loop
  #
  %config = &readconf($config_file);
  $start_time = $current_time;
  my $new = &menu;
  next if($current eq $new);
  ($current_time, $current_start_time) = &getdate;
  # save the diff
  &add_time($current, $current_time - $start_time);
  $start_time = $current_time;
  my $day = &getday;
  if ($new eq "stop") {
    # finished for today
    &dump;
    $dumped = 1;
    print "Press ENTER to restart or CTRL-C to quit>";
    my $in = <STDIN>; # wait
    $day = $cur_day = &getday;
    ($current_time, $current_start_time) = &getdate;
    $dumped = 0;
    $new = "default";
  }
  if ($day ne $cur_day) {
    # the date changed, use another dump file
    &dump;
    $cur_day = $day;
  }
  $current = $new;
}


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





sub sigend {
  #
  # received SIGINT or SIGTERM, saving
  # time data before exit
  #
  print "\nfinishing.\n";
  ($current_time, $current_start_time) = &getdate;
  &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";
  print "\nSaving Time data to $db.\n\n";
  &check_existing_entries($db);
  open DB, ">>$db" or die "Could not open database $db: $!\n";
  foreach my $cost (sort keys %zeff) {
    next if(!$cost);
    print DB $cur_day . ";" . $cost . ";" . &sec2sec($zeff{$cost}) . "\n";
  }
  close DB;

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

  # reset data
  %zeff = ();
}


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 die "$db~ exists, but is not readable: $!\n";
    open DBO , ">$db"  or die "$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;
  }
  print "\nAdding time " . &sec2sec($time) . " to \"$current\"\n";
  &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 die "Could not create log directory $base/log: $!\n";
  }
  my $logfile = "$base/log/$month.log";
  open LOG, ">>$logfile" or die "Could not open log file $logfile: $!\n";
  print LOG scalar localtime(time) . " $msg\n";
  close LOG;
}



sub menu {
  #
  # print a menu from which the user can
  # choose a cost center and return the
  # choosed one, or "default" if nothing
  # appropriate has been choosed.
  #
  my $bold_c    = "\033[1;30m"; # BOLD
  my $default_c = "\033[0m";    # last current color
  print "\nAvailable cost centers:\n";
  foreach my $short (sort keys %config) {
    printf "  %10s => %s (%s)\n", $short, $config{$short}->{description}, $config{$short}->{number};
  }
  printf "  %10s => %s\n", "pause", "Pause machen!";
  printf "  %10s => %s\n", "stop", "Feierabend :-)";
  print "\nYou are currently doing " . $bold_c . $current . $default_c . " since $current_start_time.\n"
       ."Enter next: ";
  my $input = <STDIN>;
  chomp $input;
  my $expanded = &expand_shortcut($input);
  if ($expanded) {
    print "\nUsing expanded cost center \"$expanded\".\n";
    return $expanded;
  }
  elsif (grep {$input eq $_} keys %config) {
    print "\nChanging to \"$input\".\n";
    return $input;
  }
  elsif ($input eq "stop") {
    print "\n\n\nWorkday finished.\n";
    return "stop";
  }
  elsif ($input eq "pause") {
    print "\nChanging to \"PAUSE\".\n";
  }
  else {
    print "\nUnknown cost center. Changing to the \"default\" cost center.\n";
    return "default";
  }
}


sub expand_shortcut {
  #
  # expand shortcut for short cost center name
  # and return true if the shortcut expands only
  # to exactly one cost center
  #
  my $cut = shift;
  my @shorts = keys %config;
  push @shorts, ("stop", "pause");
  my @number = grep { /^\Q$cut\E/ } @shorts;
  if ( (scalar @number) == 1) {
    # exactly one match
    return $number[0];
  }
  elsif ( (scalar @number) > 1 ) {
    return 0;
  }
  else {
    return 0;
  }
}


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 die "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}) {
    die "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 getdate {
  #
  # return the complete date (HH:MM:SS DD.MM.YYYY)
  #
  my $time = time;
  my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time);
  $year += 1900;
  $mon += 1;
  my $datum = sprintf "%02d:%02d:%02d %02d.%02d.%04d", $hour, $min, $sec, $mday, $mon, $year;
  return ($time, $datum);
}


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 find_conf {
  #
  # look for the global config file
  #
  my $unix = "/etc/zeff.config";
  my $me   = "$base/config";
  return $unix if -e $unix;
  return $me;
}


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