#!/usr/bin/perl -w
# cleanscore - Remove expired entrys from slrn's Scorefile.

# Copyright (c) 1999 - 2002 Felix Schueller <fschueller@netcologne.de>
# 
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either Version 2 of the License, or (at your option)
# any later Version.
#     
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.   

use warnings;
use strict;

use Fcntl qw(:DEFAULT :flock);
use Getopt::Std;

# Functions

sub help();
sub reset_vars(%);
sub insert_comment(%%);
sub end_of_score(%$);
sub clean_file($);

# globals variables.

our $Version="0.9.8.1";
our $Debug = 0;
our $Remove_Comments = 0;
our $Max_Empty_Lines = -1;
our $Verbose = 0;
our $Backup_Extension = ".bak";
our $Ignore_File_Pattern = "";
our $Test_Mode = 0;
our $Keep_Days = 0;
our $Save_File = "";

# # #  Phrase commandline-options.

my $target;
my %option;

getopts('b:de:f:hi:k:hrs:tVv', \%option);
if (defined($option{b})) {$Backup_Extension = $option{b};}
if (defined($option{d})) {$Debug = $option{d};}
if (defined($option{e})) {$Max_Empty_Lines = $option{e};}
if (defined($option{h})) {help(); exit(0);}
if (defined($option{i})) {$Ignore_File_Pattern = $option{i};}
if (defined($option{r})) {$Remove_Comments = $option{r};}
if (defined($option{s})) {$Save_File = $option{s};}
if (defined($option{t})) {$Test_Mode = $option{t};}
if (defined($option{k})) {$Keep_Days = $option{k};}
if (defined($option{v})) {$Verbose = $option{v};}
if (defined($option{V})) {print ("cleanscore - Version: $Version (bugreports to: fschueller\@netcologne.de)\n"); exit(0);}

if (defined($option{f}))
  {
    ($target = $option{f}) =~ s#/$##g;
  }
else
  {
    print("You must specify a scorefile with the '-f scorefile' option.\n");
    print("Try 'cleanscore -h' for a more detailed help\n");
    exit 1;
  }

# # # # # Start the show  

if ($Debug)
  {
    print ("Version: $Version\n");
    if ($Keep_Days) {print ("Keep: $Keep_Days\n");}
    print ("\n");
  }

if (-f $target) # $target is a file -> clean it.
  {
    clean_file($target);
  }
elsif (-d $target) # $target is a directory -> clean some files in it.
  {
    my $backup_pattern=$Backup_Extension;
    
    opendir(SCOREDIR, $target) || die ("Can't open $target: $!");
    # escape characters with special meaning.
    $backup_pattern=~ s/\./\\./g;
    foreach (readdir(SCOREDIR))
      {
        if (/^\.\.?$/) {next;} # skip '.' and '..' 
	if (/$backup_pattern$/o) {next;} # skip $Backup_Extension
	if ($Ignore_File_Pattern) { if (/$Ignore_File_Pattern$/o) {next;} };
	unless ( -f "$target/$_") {next;} # skip everything that is not a normal file.
	clean_file("$target/$_");
      }
  }

############################ END OF MAIN ###############################

sub clean_file($)
{
  my $score_file = shift;
  my $prev_empty_lines=0;
  my $group_line= -1;
  my $file_is_changed=0;
  my %today;
  my %this_entry;
  my %comment;
  
  if ($Debug)
    {
       print ("\nScorefile: $score_file\n");
       print ("Dates: Entry / System");
       if ($Keep_Days) {print (" - $Keep_Days Days");}
       print ("\n\n");
    }
  
  if ($Test_Mode)
    {
       print ("\nFollowing lines would removed from Scorefile \"$score_file\":\n\n");
    }
    
  @{$today{raw}} = localtime (time - ($Keep_Days * 86400));
  $today{year} = ($today{raw}[5] + 1900);
  $today{month} = ($today{raw}[4] + 1);
  $today{day} = $today{raw}[3];

  unless ($Test_Mode)
    {  
      sysopen (SCORE, "$score_file", O_RDWR) || die ("Can't open $score_file: $!");
      flock (SCORE, LOCK_EX | LOCK_NB) || die ("Can't lock $score_file: $!");
      sysopen (OUT, "$score_file.cs", O_RDWR | O_CREAT) || die ("Can't open $score_file.cs: $!");
      $file_is_changed=0;
    }
  else
    {
      open (SCORE, "$score_file") || die ("Can't read $score_file: $!");
    }

  reset_vars(\%this_entry);
  $comment{lines} = 0;
  @{$comment{data}} = "";
    
  #Magic starts here
  while (<SCORE>)
    {
      # Removing empty lines is a problem, we don't know to whitch entrie they belong.
      # So we provide the an option to cut multiple empty lines down to $Max_Empty_Lines 
      if ($Max_Empty_Lines >= 0) 
        {
          if (/^\s*$/)
            {
	      if ($prev_empty_lines==$Max_Empty_Lines)
	        {
	          $file_is_changed=1;
		  next;
	        }
	      else
	        { $prev_empty_lines++; }
	    }
          else
            {
	      if ($prev_empty_lines)
	        { $prev_empty_lines=0; }
	    }
        }
	
      if ($Remove_Comments) # Remove '%' comments
        {
          if (/^\s*\%/)
            {
              if ($Verbose || $Debug || $Test_Mode) {print ($_);}
              $file_is_changed=1;
	      next;
            }
        }
  
      if (/\%EOS/ || /#EOS/)
        {
          $comment{data}[$comment{lines}] = $_;
          $comment{lines}++;
          insert_comment(\%comment, \%this_entry);	    
          end_of_score(\%this_entry, \$file_is_changed);
          next;
        }
  
      if (/\%BOS/ || /#BOS/)
        {
          insert_comment(\%comment, \%this_entry);
          end_of_score(\%this_entry, \$file_is_changed);
          $this_entry{seen_bos} = 1;
        }
  
      if (/^\s*\%/ || /^\s*#/ || /^\s*$/)  # put comments in an extra array
        {
          $comment{data}[$comment{lines}] = $_;
          $comment{lines}++;
          next;
        }

      if (/^\S*\[.*\]\S*$/)  # Found a new groupexpression - entry ends here
        {
          unless ($this_entry{seen_bos})
            {
	      end_of_score(\%this_entry, \$file_is_changed);
              insert_comment(\%comment, \%this_entry);
	    }
          $group_line=$this_entry{line};
        }
  
      if (/Score:/i)
        {
          if ($this_entry{seen_score}) #there was a 'Score:' before entry ends here
            {
              if ($this_entry{is_expired} && $group_line >= 0) # Save Groupexp if necessary
                {
	          unless ($Test_Mode) {print (OUT $this_entry{data}[$group_line]);}
                }
              end_of_score(\%this_entry, \$file_is_changed);
              insert_comment(\%comment, \%this_entry);
              $group_line = -1;
            }
          $this_entry{seen_score} = 1;
        }

      if (/Expires:/i)
        {
          if (/\d{1,2}-\d{1,2}-\d{4}/)
            {
              ($this_entry{day}, $this_entry{month}, $this_entry{year}) = /(\d{1,2})-(\d{1,2})-(\d{4})/;
            }
          else
            {
              ($this_entry{month}, $this_entry{day}, $this_entry{year}) = m#(\d{1,2})/(\d{1,2})/(\d{4})#;
            }
          if ($Debug)
            {
              print ("Year: $this_entry{year} / $today{year}\n");
              print ("Month: $this_entry{month} / $today{month}\n");
              print ("Day: $this_entry{day} / $today{day}\n");
            }
          if ($this_entry{year} < $today{year})
            {
              $this_entry{is_expired} = 1;
            }
          elsif ($this_entry{year} == $today{year})
            {
              if ($this_entry{month} < $today{month})
	        {
                  $this_entry{is_expired} = 1;
                }
  	      elsif ($this_entry{month} == $today{month})
  	        {
                  if ($this_entry{day} <= $today{day}) {$this_entry{is_expired} = 1;}
                }
            }
          if ($Debug && $this_entry{is_expired}) {print ("Entry is expired\n");}
        }

      insert_comment(\%comment, \%this_entry);
      $this_entry{data}[$this_entry{line}] = $_;
      $this_entry{line}++;
    } #while (<SCORE>)

  end_of_score(\%this_entry, \$file_is_changed);
  insert_comment(\%comment, \%this_entry);
  end_of_score(\%this_entry, \$file_is_changed);

  unless ($Test_Mode)
    {
      if ($file_is_changed)
        {
          # $score_file.cs contains the new scorefile $score_file the old.
          
	  # copy $score_file to $score_file$Backup_Extension
          seek (SCORE, 0, 0) || die ("Can't rewind $score_file: $!");
          open (DEST, ">$score_file$Backup_Extension") || die ("Can't write $score_file$Backup_Extension: $!");
          while (<SCORE>) {print (DEST $_);}
          close (DEST);
          
	  # copy $score_file.cs to $score_file
          seek (SCORE, 0, 0) || die ("Can't rewind $score_file: $!");
          truncate (SCORE, 0);
          seek (OUT, 0, 0) || die ("Can't rewind $score_file.cs: $!");
          while (<OUT>)
	    {
	       # Removing empty lines is a problem, we don't know to whitch entrie they belong.
               # So we provide the an option to cut multiple empty lines down to $Max_Empty_Lines 
               if ($Max_Empty_Lines >= 0) 
                 {
		    if (/^\s*$/)
		      {
			 if ($prev_empty_lines==$Max_Empty_Lines)
			   { next; }
			 else
			   { $prev_empty_lines++; }
	    
		       }
          
		     else
		       {
			 if ($prev_empty_lines)
			   { $prev_empty_lines=0; }
		       }
        
		  }
	      print (SCORE $_);
	    }
	}
      close (OUT);
      if (-e "$score_file.cs") { unlink("$score_file.cs")};
    }
  close (SCORE);
} #sub clean_file($)

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

sub end_of_score(%$)
{
  my $entry = shift;
  my $file_is_changed = shift;
  
  unless ($$entry{is_expired})
    { # entry is not expired
       unless ($Test_Mode)
         {
	   print (OUT @{$$entry{data}});
         }
    }
  else
    { # entry is expired
      $$file_is_changed=1;
      if ($Save_File && $$entry{is_expired})
        {
	  open (SAVE, ">>$Save_File") || die ("Can't append to $Save_File: $!");
	  print (SAVE @{$$entry{data}});
	  close (SAVE);
	}
      if ($Verbose || $Debug || $Test_Mode)
        {
          print (@{$$entry{data}});
          print ("\nNext Entry:\n\n");
        }
    }
  reset_vars($entry);
} #sub end_of_score()

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

sub insert_comment(%%)
{
  my $comment=shift;
  my $entry=shift;
  my $i; 

  if ($$comment{lines})
    {
      for ($i=0; $i < $$comment{lines}; $i++)
        {
          $$entry{data}[$$entry{line}] = $$comment{data}[$i];
          $$entry{line}++;
        }
    }
  $$comment{lines} = 0;
  @{$$comment{data}} = "";
}

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

sub reset_vars(%)
{
  my $entry=shift;
  
  @{$$entry{data}} ="";
  $$entry{is_expired} = 0;
  $$entry{seen_bos} = 0;
  $$entry{seen_score} = 0;
  $$entry{line} = 0;
}

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

sub help()
{
  print <<EOF;
cleanscore - Remove expired entries from slrn's Scorefile.

Required Options:

-f <filename>   "File".   Chose "filename" for cleaning.
                          If "filename" is a directory, clean
                          all files in it.


Standard Options for "help" and "version":

-V              "Version".  Print Version and exit.
-h              "Help".     Prints a help message.


Other Options:

-b <extension>  "Backup extension".   Overwrites the default backup-
                extension ('.bak').

-d              "Debug".    Prints dates and status for each entry.

-e N            "Empty lines". Cut multiple empty lines down to N.

-i <pattern>    "Ignore pattern".   When scanning through a directory,
                ignore files with names matching "pattern".
                The "backup extension" is matched automaticly.

-k N            "Keep for N days".
                Do not remove expired entries immediately; instead, hold them
                for N more days.  This allows to keep expired entries so you
                can still edit them, eg. change the expire date.

-r              "Remove".  Removes comment lines, i.e. lines beginning
                with '%'. (i.e. remove slrn generated comments
                if you use '#' for your own comments)

-s <filename>   "Save to". Save removed entries to "filename".

-t              "Test".  Just check for expired entries,
                but do not change the scorefile.
                Prints "removed" entries to stdout.

-v              "Verbose".  Prints all expired entries to stdout.
EOF
}
