#!/usr/bin/perl -w

######################################################################
###
###   IMPORTANT!
###   Please read the warranty and legal notice 
###   at the end of this file!
###
######################################################################

require 5.000;
use lib '/usr/local/bin',"$ENV{HOME}/bin",'/usr/sl';
use slutil;
use English;
use Time::ParseDate 100.01_03_01;

######################################################################
### Voreinstellungen
######################################################################

$version = '2.2.1';
$appname = 'Remind';

$debug = $FALSE;
if ($ARGV[0] eq '-d') { $debug=$TRUE; shift; };

$past = 0;
if ($ARGV[0] eq '-past') { shift; $past = shift; };

#              0  1  2  3  4  5  6 (So liefert das localtime!)
@Wochentag = (So,Mo,Di,Mi,Do,Fr,Sa);

%TagZuParseDateTag = (
		Mo => Mon,
		Di => Tue,
		Tu => Tue,
		Mi => Wed,
		We => Wed,
		Do => Thu,
		Th => Thu,
		Fr => Fri,
		Sa => Sat,
		So => Sun,
		Su => Sun
	       );

($tag,$mon,$jahr,$wochentag) = (localtime(time))[3..6];
$mon++;
$jahr += 1900;
# So viele Tage sind seit dem 1.1.1970 vergangen
$Heute = Tage($tag,$mon,$jahr); # Oder: parsedate('today midnight')

$Heute = $Heute - $past;

######################################################################
### Hauptprogramm
######################################################################

printumlaute Kopf();

# Richtige Parameter?
if (($#ARGV<0) || (! -e $ARGV[0]))
{
  &Hilfe;
  exit;
}

$remindfile = shift;

print "Heute ist der: ", $Wochentag[$wochentag], " $tag.$mon.$jahr\n";
print "Aktuelles Referenz-Datum wurde um $past Tage zurückgesetzt.\n" if $past;
print "\n";

open (REM, "$remindfile") || die "Kann File nicht lesen!\n";
@remdat = <REM>;
close REM;
$i = 0;
while ($i <= $#remdat)
{
  $_ = $remdat[$i];

  ###############
  # Parser ANFANG
  ###############

  # Kommentare überspringen
  if (/^[;\#]/ )
  {
    $i++;
    next;
  }

  # [dd.dd.dddd] : genaues Datum mit Jahr
  if ( /\[(\d{1,2})\.(\d{1,2})\.(\d{4})\]/ )
  {
    ($re_tag,$re_mon,$re_jahr,$re_anz,$re_wday) = ($1,$2,$3,undef,undef);
  }
  # [dd.dd.]     : genaues Datum
  elsif ( /\[(\d{1,2})\.(\d{1,2})\.\]/ )
  {
    ($re_tag,$re_mon,$re_jahr,$re_anz,$re_wday) = ($1,$2,undef,undef,undef);
  }
  # [dd.]        : Jeder Tag dd im Monat (z.B.: jeder 1.)
  elsif ( /\[(\d{1,2})\.\]/ )
  {
    ($re_tag,$re_mon,$re_jahr,$re_anz,$re_wday) = ($1,undef,undef,undef,undef);
  }
  # [WDAY.]    : Der Wochentag WDAY
  # [dd.WDAY.] : Der dd-te Wochentag im Monat (Zweiter Freitag i.Mon.)
  elsif ( /\[(\d{1,2})*\.*(Mo|Di|Tu|Mi|We|Do|Th|Fr|Sa|So|Su)[^\.]*\.*\]/ )
  {
    ($re_tag,$re_mon,$re_jahr,$re_anz,$re_wday) = (undef,undef,undef,$1,$2);
  }
  else
  {
    # Alles falsche außer Leerzeilen ausgeben
    warn "*** Zeile ",$i+1," wird ignoriert: $_" if ( ! /^\s*$/ );
    $i++;
    next;
  }

  #############
  # Parser ENDE
  #############

  # Termine von vergangenen Jahren ignorieren
  if ((defined $re_jahr) && ($re_jahr < $jahr))
  {
    print "Abgelaufener fixer Termin: $re_tag.$re_mon.$re_jahr:\n";
    $i++;
    $i++;
    while ( ! ( ($_ = $remdat[$i]) =~ /\[[Ee]nd\]/ ) )
    {
      printumlaute Parse($_, $jahr);
      $i++;
    }
    print "---\n";
    $i++;
    next;
  }

  # Wochentag-Modus:
  if ( ! defined $re_tag)
  {
    if (defined $re_anz) { TagGueltig($re_anz,$i+1) }
    $Termin = WochentagTerminberechnung($re_anz,$re_wday);
  }

  # Datum-Modus
  else
  {
    # [dd.] : Jeder Tag dd im Monat (z.B.: jeder 1.)
    if ( !defined $2 )
    {
      TagGueltig($re_tag,$i+1);
      $tag   = $re_tag;
      $monat = $mon; # Dieser Monat.
      # So viele Tage sind zwischen dem Termin un dem Bezugsjahr
      $Termin = Tage($tag,$monat,$jahr);
      # Wenn die Tage-Anzahl in $Termin kleiner als die Tage bis heute
      # sind, dann ist der Tag schon vorbei.
      # Dann testen wir, ob es im naechsten Monat ist:
      if ($Termin < $Heute)
      {
	$nextmon  = $monat+1;
	$NJahr = $jahr;
	# Falls Wechsel von Dezember zu Januar:
	if ($nextmon==13)
	{
	  $nextmon=1;
	  $NJahr++;
	}
	$Termin = Tage($tag,$nextmon,$NJahr);
      }
    }
    # [dd.dd.]     : genaues Datum oder
    # [dd.dd.dddd] : genaues Datum mit Jahr
    else
    {
      TagGueltig($re_tag,$i+1);
      MonatGueltig($re_mon,$i+1);
      $tag   = $re_tag;
      $monat = $re_mon;
      $jahrangabe = (defined $re_jahr) ? $re_jahr : $jahr;
      # So viele Tage sind zwischen dem Termin un dem Bezugsjahr
      $Termin = Tage($tag,$monat,$jahrangabe);
      # Wenn die Tage-Anzahl in $Termin kleiner als die Tage bis heute
      # sind, dann ist der Tag schon vorbei.
      # Dann testen wir, ob es im naechsten Jahr ist:
      $TerminLiegtImNaechstenJahr = $FALSE;
      if ($Termin < $Heute)
      {
	$Termin = Tage($tag,$monat,$jahrangabe+1);
	$TerminLiegtImNaechstenJahr = $TRUE;
      }
    }
  }

  # Wieviele Tage im voraus soll erinnert werden?
  $i++;
  $_ = $remdat[$i];
  unless ( /\[[Nn]ag=(\d+)\]/ )
  {
    warn "*** Warnung: Kein Nag=.. in Zeile ",$i+1,
    " Verwende Default Nag=10.\n";
    $nag = 10;
  }
  else   { $nag = $1 }

  # Wenn Termin minus Vorwarnzeit kleiner Heute, dann ausgeben
  if ( $Termin-$nag <= $Heute )
  {
    $differenz = $Termin-$Heute;

    if ($differenz > 1) { $Tagtext="Tagen" } else { $Tagtext="Tag" }
    $differenz = $differenz - $past;
    if ($differenz == 0)
    { print "Heute: $remdat[$i-1]" }
    elsif ($differenz > 0)
    { print "In $differenz $Tagtext: $remdat[$i-1]" }
    else
    {
      $differenz = - $differenz;
      print "Vor $differenz $Tagtext: $remdat[$i-1]"
    }

    $i++;
    while ( ! ( ($_ = $remdat[$i]) =~ /\[[Ee]nd\]/ ) )
    {
      if ($TerminLiegtImNaechstenJahr)
      {
	printumlaute Parse($_, $jahr+1);
      }
      else
      {
	printumlaute Parse($_, $jahr);
      }
      $i++;
    }
    print "---\n";
  }
  # Nur überspringen
  else
  {
    while ( ! ( $remdat[$i] =~ /\[[Ee]nd\]/ ) )
    {
      $i++;
    }
  }

  $i++;
}


######################################################################
### Unterprogramme
######################################################################

sub WochentagTerminberechnung
{
  # Parameter: (dd,WDAY)
  # Z.B.:      (undef,4) oder (3,5)
  # Return:    Termin (in Tagen seit 1.1.1970)

  # [WDAY.]    : Der Wochentag WDAY
  # [dd.WDAY.] : Der dd-te Wochentag im Monat (Zweiter Freitag i.Mon.)
  # Der reguläre Ausdruck im Hauptprogramm findet dann zwei Felder:
  # dd.WDAY

  # [WDAY]       : Der Wochentag WDAY
  if ( !defined $_[0] )
  {
    my $wday = $_[1];
    $wday = $TagZuParseDateTag{$wday};
    my $x = parsedate("next $wday", NOW=>scalar parsedate('today midnight'));
    $x = s2d($x);
    # Oder ist es heute?
    if ( ($x-$Heute) == 7 )
    {
      return $Heute;
    }
    return $x;
  }

  # [dd.WDAY.] : Der dd-te Wochentag im Monat (z.B. zweiter Freitag im Monat)
  else
  {
    my $wievielte = $_[0];
    my $wday      = $_[1];

    $wday = $TagZuParseDateTag{$wday};
    my $x;
    my $count = 1;
    # Der erste Tag in diesem Monat
    my $last = parsedate("1.$mon.$jahr",UK=>$TRUE);
    while ($count <= $wievielte)
    {
      ($x,$err) = parsedate("next $wday", NOW => $last);
      $last = $x;
      $count++;
    }
    # Ist dieser Tag schon vorbei?
    if (s2d($x) < $Heute)
    {
      # Der erste Tag im nächsten Monat
      $count = 1;
      $last = parsedate('+ 1 month',
			NOW => scalar parsedate("1.$mon.$jahr",UK=>$TRUE));
      while ($count <= $wievielte)
      {
	($x,$err) = parsedate("next $wday", NOW => $last);
	$last = $x;
	$count++;
      }
    }
    return s2d($x);
  }
}


sub Parse
{
  # Parameter: (Text,akt_jahr)
  # - Text:     Dieser Text wird geparst und ausgegeben
  # - akt_jahr: aktuelles Jahr, z.B. 1997
  #
  # Es wird "[zahl]" nach "relativeZahl" umgewandelt und zurückgegeben
  # Also: [1971] wird zu 26.
  #
  my $input     = shift;
  my $JahrJetzt = shift;
  my $diff;

  while ($input =~ /\[(\d{4})\]/) # Solange es noch nicht ersetzte Stellen gibt
  {
    $diff = $JahrJetzt-$1;
    $input =~ s/(.*?)\[$1\](.*?)/$1$diff$2/;
  }
  return $input;
}


sub Tage
{
  # Es werden die Tage seit dem 1.1.1970 berechnet
  # Parameter: (Tag,Monat,Jahr) mit Tag=1..31, Monat=1..12, Jahr=1996...
  #
  my ($tag,$mon,$jahr) = @_;
  return s2d(parsedate("$tag.$mon.$jahr",UK=>$TRUE));
}


sub s2d
{
  # Seconds2Day()
  # Umrechnung von Sekunden auf (gerundete) Tage.
  # Parameter: Sekunden
  # Return:    Tage
  return int(shift()/60/60/24);
}


sub schaltjahr
{
  my $jahr = shift;
  my $schalt = (($jahr % 4) == 0);
  if ((($jahr % 100)==0) && (($jahr % 400)!=0)) { $schalt = $FALSE }
  return $schalt;
}


sub TagGueltig
{
  die "Falscher Tag '$_[0]' in Zeile $_[1]!\n" if ( ($_[0]<1) || ($_[0]>31) );
}


sub MonatGueltig
{
  die "Falscher Monat '$_[0]' in Zeile $_[1]!\n" if ( ($_[0]<1)||($_[0]>12) );
}


sub Kopf
{
  my $head = "$appname $version   -   von Stephan Löscher";
  return "\n$head\n" . '~' x length($head) . "\n";
}


sub Hilfe
{
  printumlautepaged
  Kopf().
"Syntax: remind [-d] [-past TAGE] File

Remind erinnert an bestimmte Termine.
Die Option '-d' schaltet den Debug-Modus ein.

Die Option -past TAGE schaltet den Remind um TAGE Tage zurück.
Z.B.: remind -past 10

Es wird ein Eingabe-File mit folgenden Block-Strukturen eingelesen:
   [Zeiteintrag]
   [Nag=xx]
   text
   ...
   [End]

Dabei ist:

- Zeiteintrag:
  tt.mm.jjjj : z.B. [11.3.1998], es wird an den 11.3.1998 erinnert (auch wenn
               das Datum schon vorbei ist, also z.B: 1999, um nicht zu
               vergessen es zu löschen.)
  tt.mm.     : z.B. [11.3.], es wird an den 11.3. erinnert
  tt.        : es wird an den Tag tt im Monat erinnert, z.B. jeder Erste [1.]
  WDAY       : erinnert immer an diesen Wochentag, z.B.: [Sat]
  y.WDAY     : erinnert an jeden y-ten Wochentag, z.B.: [3.Fr] erinnert an
               jeden dritten Freitag im Monat
- xx      die Angabe, wieviele Tage im Voraus erinnert werden soll
- text    ein beliebiger Text (auch mehrzeilig), der durch [End] abgeschlossen
          wird. Im Text wird [yyyy] als Jahr interpretiert und durch die
          Differenz zum aktuellen Jahr ersetzt, z.B.:
          \"Freds [1950]. Geburtstag.\"   wird zu:   \"".
          Parse("Freds [1950]. Geburtstag.",$jahr)."\"

Zeilen, die mit ';' oder '#' beginnen sind Kommentare und werden ignoriert,
wenn sie außerhalb der Textblöcke auftreten

Typischer Aufruf in der ~./xinitrc ist:
(sleep 10;xterm -geometry 80x24-0-0 -e bash -c \"remind /t/remind.dat;read\")&\n";
}


######################################################################
#
# Warranty and legal notice
# ~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Copyright (c) 1997 by Stephan Löscher  -  all rights reserved
# My Address: Stephan Löscher, Dr.Troll-str. 3, 82194 Gröbenzell, Germany
# Email: loescher@gmx.de
# WWW: http://www.loescher-online.de/
#
# This program is freeware.
# It is NOT Public-Domain-Software!
# The author (Stephan Löscher) does NOT give up his copyright, but he 
# reserves his copyright. Usage and copying is free of charge for private
# use, but NOT for commercial use!
#
# You may and should copy this program free of charge, use it,
# give it to your friends, upload it to a BBS or something similar, under
# the following conditions:
# * Don't charge any money for it. If you upload it to a BBS, make sure that
#    it can be downloaded free (without paying for downloading it, except
#    for usage fees that have to be paid anyway). Small copying fees (up to
#    5 DM or 3 $US) may be charged.
#  * Only distribute the whole original package, with all the files included.
#  * This program may not be part of any commercial product or service without
#    the written permission by the author.
#  * If you want to include this program on a CD-ROM and/or book, please send
#    me a free copy of the CD/book (this is not a must, but I would appreciate
#    it very much).
#
# Distribution of the program is explicitly desired, provided that the above
# conditions are accepted.
#
# YOU ARE USING THIS PROGRAM AT YOUR OWN RISK! THE AUTHOR (STEPHAN LÖSCHER)
# IS NOT LIABLE FOR ANY DAMAGE OR DATA-LOSS CAUSED BY THE USE OF THIS PROGRAM
# OR BY THE INABILITY TO USE THIS PROGRAM. IF YOU ARE NOT SURE ABOUT THIS, OR
# IF YOU DON'T ACCEPT THIS, THEN DO NOT USE THIS PROGRAM!
# BECAUSE OF THE VARIOUS HARDWARE AND SOFTWARE ENVIRONMENTS INTO WHICH THIS
# PROGRAM MAY BE PUT, NO WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE IS
# OFFERED.
# GOOD DATA PROCESSING PROCEDURE DICTATES THAT ANY PROGRAM BE THOROUGHLY
# TESTED WITH NON-CRITICAL DATA BEFORE RELYING ON IT.
#
# No part of the documentation may be reproduced, transmitted, transcribed,
# stored in any retrieval system, or translated into any other language in
# whole or in part, in any form or by any means, whether it be electronic,
# mechanical, magnetic, optical, manual or otherwise, without prior written
# consent of the author, Stephan Löscher.
#
# You may not make any changes or modifications to this software or this
# manual. You may not decompile, disassemble, or otherwise reverse-engineer
# the software in any way.
# If you got the source, then you are permitted to modify it if you
# contact me and tell me your enhancements.
# You also may include the source as a whole or parts of it into other
# programs, as long as you don't make profit directly out of selling
# the result. If you re-use code of this program then do not remove my name!
# If you include this source-code in your projects, mark it clearly as such
# "... derived from code XXX by Stephan Löscher".
# But don't distribute modified code!
#
# If you believe your copy of this software has been tampered or altered in
# anyway, shape or form, please contact me immediately! Do not hesitate a
# moment to inform me. Remember, this software should be available to all, in
# the original form, so please do not accept modified or damaged versions of
# my software.
#
# The author reserves his right for taking legal steps if the copyright or the
# license agreement is violated.
#
# All product names mentioned in this software are trademarks or registered
# trademarks of their respective owners.
#
# If you have any questions, ideas, suggestions for improvements or if you find
# bugs (I don't hope so.) then feel free to contact me. (Email is appreciated.)
#
# I'm not a native english speaker. If you are one and discover some strange
# sounding parts in this documentation or in the program, please, feel free
# to point it out to me and give me suggestions for alteration!
#
# If the program works for you, and you want to honour my efforts, you are
# invited to donate as much as you want... :)
#
# In any case, if you don't like the restrictions in this license, contact
# me, and we can work something out.
#
######################################################################
