#!/usr/local/bin/perl -w

# $Header: /home/jost/cvs/hacks/KPAstacks,v 1.16 2009/12/25 09:17:31 jost Exp $

# Checks Image-Stacks in KPA index.xml-File
# see help() for details

use strict;

use English qw( -no_match_vars);
use IO::File;
use Getopt::Long;

use constant INFILE => "/images/fotos/index.xml"; # Default Input File

my($optHelp,
   $optShowStacks,
   $optShowCorrect,
   $optEmacs,
   $inFile,
   $stackCount,
   %stacks,
   %fileNames, # fileNames{filename} = lineno, for all images (in stack or outside)
   %noStack,   # noStack{filename} = lineno, if filename with label is in no stack
   $lastLabel, # Label of last Image without stackid
   $inStack);  # number of current stack (undef, wenn outside of stack)

# $stacks{stackid}{'correct'}: true: Stack is correct
#                 {'before'}: Label of image before last entry without stackid before $inStack
#                 {'after'}:  Label of image after last entry with stackid $inStack
#                 {'stack'}: Array with entries:
#                 {'stack'}[$stackOrder]{'label'} = Image Label
#                 {'stack'}[$stackOrder]{'fileName'} = Image File name (with path)
#                 {'stack'}[$stackOrder]{'lineno'} = Linenumber in input file
#                 {'stack'}[$stackOrder]{'splitted'} = true, if stack is splitted before $stackOrder
# if defined:     {'stack'}[$stackOrder]{'twice'} = Linenumber (of second def. stackOrder/stackId)


$lastLabel = undef;
$inStack = undef;

sub help()
  {
    print STDERR <<EOM;
Reads KPhotoalbum index.xml file; checks image stacks and complains about problems.

Usage:
KPAstacks [-cehs] Input-File

Reads the Input-File. If none is given a (probably (for anyone but "me") useless) default is tried.

-h, -help:    Help
-c, -correct: List correct stacks
-e, -emacs:   Create listing for loading into emacs compilation-minor-mode
-s, -stacks:  Show used stack numbers

Checks done:
- StackId and StackOrder are only allowed together
- Each combination of StackId/StackOrder is allowed only once
- The StackOrder are numbered consecutive, starting with 1
  (This also means, that no images outside any stack or with another StackId
   are allowed in between)
- Each image (as identified by 'fileName' is only allowed once; either within a stack or outside

EOM
    exit(1);
  }


sub errMsg($$)
  {
    my($lineno, $msg) = @_;
    # Prints error message $msg
    # If $optEmacs is defined, prints $lineno and $inFile in format suitable for emacs compilation-minor-mode
    # $lineno is used there if it isn't undef

    if (defined($optEmacs))
      {
        print "$inFile:";
        if (defined($lineno))
          {
            print "$lineno:";
          }
        else
          {
            print ":";
          }
      }
    print $msg;
  }


sub parse()
 {
   my($inFh);
   $inFh = new IO::File $inFile;
   die "Couldn't open file \"" . $inFile . "\"\n($ERRNO)\nExit" unless(defined($inFh));

   my($line,
      $lineNo,
      $stackOrder,
      $splitted,
      $label,
      $fileName,
      $stackId);

   $stackCount = 0;
   print STDERR "Found Stacks: ";
   while ($line = $inFh -> getline())
     {
       if ($line =~ /^ \s* <image \s+ .* label="(.*?)"/x)
         {
           # Image
           $label = $1;

           if ($line =~ /^ \s* <image \s+ .* file="(.*?)"/x)
             {
               $fileName = $1;
               if (defined($fileNames{$fileName}))
                 {
                   errMsg($inFh -> input_line_number(),
                          "\nError: \"$fileName\" in line ".  $fileNames{$fileName} .
                          " and " . $inFh -> input_line_number() . "\n");
                 }
               else
                 {
                   $fileNames{$fileName} = $inFh -> input_line_number();
                 }
             }
           else
             {
               $fileName = undef; # to provoce error message in case of this problem ("use of undef var....")
             }

           if ($line =~ /^ \s* <image \s+ .* stackOrder="(\d+)" /x)
             {
               # Stack
               $stackOrder = $1;
               if ($line =~ /^ \s* <image \s+ .* stackId="(\d+)" /x)
                 {
                   $stackId = $1;
                   if (!defined($stacks{$stackId}))
                     {
                       # New Stack
                       $stackCount++;
                       if ($stackCount % 10 == 0)
                         {
                           print STDERR "$stackCount ";
                         }
                     }

                   $splitted = 0;
                   if (!defined($inStack) || ($inStack != $stackId))
                     {
                       # Entry in (this) stack $stackId
                       $inStack = $stackId;
                       $splitted = defined($stacks{$stackId}); # Entry in Stack, but stackId already seen
                       $stacks{$stackId}{'before'} = $lastLabel;
                     }

                   if (defined($stacks{$stackId}{'stack'}[$stackOrder]))
                     {
                       # Found StackId/StackOrder twice
                       $stacks{$stackId}{'stack'}[$stackOrder]{'twice'} = $stacks{$stackId}{'stack'}[$stackOrder]{'lineno'};
                       $stacks{$stackId}{'incorrect'} = 1;
                     }

                   $stacks{$stackId}{'stack'}[$stackOrder]{'splitted'} = $splitted;
                   $stacks{$stackId}{'stack'}[$stackOrder]{'label'} = $label;
                   $stacks{$stackId}{'stack'}[$stackOrder]{'fileName'} = $fileName;
                   $stacks{$stackId}{'stack'}[$stackOrder]{'lineno'} = $inFh -> input_line_number();
                 }
               else
                 {
                   errMsg($inFh -> input_line_number(),
                          "\nError: \"stackOrder\" without \"stackId\"; line " . $inFh -> input_line_number() . "\n");
                 }
             }
           else
             {
               # No StackOrder => Image not in a stack
               if ($line =~ /^ \s* <image \s+ .* stackId="(\d+)" /x)
                 {
                   errMsg($inFh -> input_line_number(),
                          "\nError: \"stackId\" without \"stackOrder\"; line " . $inFh -> input_line_number() . "\n");
                 }

               if ($line =~ /^ \s* <image \s+ .* label="(.*?)"/x)
                 {
                   $lastLabel = $1;
                   if (defined($inStack))
                     {
                       $stacks{$stackId}{'after'} = $lastLabel;
                     }
                   $inStack = undef;
                 }

               if ($line =~ /^ \s* <image \s+ .* file="(.*?)"/x)
                 {
                   $noStack{$1} = $inFh -> input_line_number();
                 }
             }
         }
     }

   if (!$inFh -> close())
     {
       die "Couldn't close file \"" . $inFile . "\".\n($ERRNO)\nExit";
     }

   print STDERR " $stackCount\n";
 }


sub analyse()
  {
    my($stackId,
       $stackOrder,
       $ok,        # true: so far ok
       $msg,
       $lastLine,        # Line number of last stackOrder seen so far in stack
       $lastStackOrder); # stackOrder corresponding to lastLine
    foreach $stackId (sort {$a <=> $b} keys(%stacks))
      {
        $ok = 1;
        $lastLine = undef;
        $lastStackOrder = undef;
        for ($stackOrder = 1; $stackOrder < $#{$stacks{$stackId}{'stack'}}; $stackOrder++)
          {
            if (defined($stacks{$stackId}{'stack'}[$stackOrder]))
              {
                if ($stacks{$stackId}{'stack'}[$stackOrder]{'splitted'})
                  {
                    errMsg( $stacks{$stackId}{'stack'}[$stackOrder]{'lineno'},
                            "Error: stackId=\"$stackId\": splitted before stackOrder=\"$stackOrder\", line " . $stacks{$stackId}{'stack'}[$stackOrder]{'lineno'} . " !\n");
                  }

                if (defined($stacks{$stackId}{'stack'}[$stackOrder]{'twice'}))
                  {
                    errMsg($stacks{$stackId}{'stack'}[$stackOrder]{'lineno'},
                           "Error: stackId=\"$stackId\", stackOrder=\"$stackOrder\" defined twice in line " . $stacks{$stackId}{'stack'}[$stackOrder]{'lineno'} . " and " .  $stacks{$stackId}{'stack'}[$stackOrder]{'twice'} . " !\n");
                  }

                if (defined($lastLine) && ($stacks{$stackId}{'stack'}[$stackOrder]{'lineno'} < $lastLine))
                  {
                    errMsg($stacks{$stackId}{'stack'}[$stackOrder]{'lineno'},
                           "Error: stackId=\"$stackId\": stackOrder=\"$stackOrder\" before stackOrder=\"$lastStackOrder\", line " .
                           $stacks{$stackId}{'stack'}[$stackOrder]{'lineno'} . "\n");
                    $stacks{$stackId}{'incorrect'} = 1;
                  }
                $lastLine = $stacks{$stackId}{'stack'}[$stackOrder]{'lineno'};
                $lastStackOrder = $stackOrder;
                if (defined($noStack{$stacks{$stackId}{'stack'}[$stackOrder]{'fileName'}}))
                  {
                    errMsg($stacks{$stackId}{'stack'}[$stackOrder]{'lineno'},
                           "Error: stackId=\"$stackId\": Image \"" . $stacks{$stackId}{'stack'}[$stackOrder]{'fileName'} .
                           "\" in Stack, line " . $stacks{$stackId}{'stack'}[$stackOrder]{'lineno'} .
                           " and outside Stack, line " . $noStack{$stacks{$stackId}{'stack'}[$stackOrder]{'fileName'}} .
                           "\n");
                    $stacks{$stackId}{'incorrect'} = 1;
                  }
              }
            else
              {
                # !defined($stacks{$stackId}{'stack'}[$stackOrder]) => stackId missing
                $ok = 0;
                $stacks{$stackId}{'incorrect'} = 1;
                $msg = "Error: stackId=\"$stackId\": stackOrder=\"$stackOrder\" missing";
                if (defined($lastLine))
                  {
                    $msg .= ", line $lastLine";
                  }
                $msg .= "\n";
                errMsg($lastLine, $msg);
              }
          }

        if ($ok && defined($optShowCorrect))
          {
            print "Stack stackId=\"$stackId\": OK\n";
          }
      }

    my($wrongStackCount);
    $stackCount = 0;
    $wrongStackCount = 0;
    foreach $stackId (keys(%stacks))
      {
        $stackCount++;
        if (defined($stacks{$stackId}{'incorrect'}) && ($stacks{$stackId}{'incorrect'}))
          {
            $wrongStackCount++;
          }
      }

    if (defined($optShowStacks))
      {
        # show used stack numbers
        my($lastId);
        $lastId = undef;
        print "\n Used StackIDs:\n";
        foreach $stackId (sort {$a <=> $b} keys(%stacks))
          {
            if (defined($lastId) && ($stackId != $lastId +1))
              {
                print "\n";
              }
            print "$stackId\n";
            $lastId = $stackId;
          }
      }

    $msg = sprintf("\n$wrongStackCount stacks out of $stackCount (%.2f %%) incorrect !\n", $wrongStackCount / $stackCount * 100);
    print $msg;
    print STDERR $msg;


    if (defined($optEmacs))
      {
        # emacs postfix for minor mode
        print "\l\n";
        print "Local variables:\n";
        print "mode: compilation-minor\n";
        print "End:\n";
        print ";\n";
      }

  }

################################################# main #######################################
  if (!(GetOptions("h|help" => \$optHelp,
                   "e|emacs" => \$optEmacs,
                   "c|correct" => \$optShowCorrect,
                   "s|stacks" => \$optShowStacks)))
  {
    print STDERR "Help with '-h' !\n";
    exit(1);
  }

  if (defined($optHelp))
  {
    help();
    exit(0);
  }

  if ($#ARGV < 0)
    {
      $inFile = INFILE;
    }
  elsif ($#ARGV == 0)
    {
      $inFile = $ARGV[0];
    }
  else
    {
      print STDERR "Too many arguments !\n";
      help();
      exit(1);
    }


parse();
analyse();
