# Copyright (c) 2010-2012 Zmanda, Inc.  All Rights Reserved.
#
# 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
#
# Contact information: Zmanda Inc., 465 S. Mathilda Ave., Suite 300
# Sunnyvale, CA 94085, USA, or: http://www.zmanda.com


=head1 NAME

Amanda::Curinfo::Info - Perl extension for representing dump
information

=head1 SYNOPSIS

   use Amanda::Curinfo::Info;

   my $info = Amanda::Curinfo::Info->new($infofile);

=head1 DESCRIPTION

C<Amanda::Curinfo::Info> is the format representation for the curinfo
database.  It handles the reading and writing of the individual
entries, while the entry management is left to C<Amanda::Curinfo>.
Further parsing is also dispatched to C<Amanda::Curinfo::History>,
C<Amanda::Curinfo::Stats>, and C<Amanda::Curinfo::Perf>.

=head1 INTERFACE

The constructor for a new info object is very simple.

   my $info = Amanda::Curinfo::Info->new();

Will return an empty info object with the necessary fields all blank.

Given an existing C<$info> object, for example, as provided by
C<Amanda::Curinfo::get_info>, there are other functions present in this
library, but they are helper functions to the previously described
methods, and not to be used directly.

It should also be noted that the reading and writing methods of
C<Amanda::Curinfo::Info> are not meant to be used directly, and should be
left to L<Amanda::Curinfo>.

Reading a previously stored info object is handled with the same
subroutine.

   my $info = Amanda::Curinfo::Info->new($infofile);

Here, C<$info> will contain all the information that was stored in
C<$infofile>.

To write the file to a new location, use the following command:

   $info->write_to_file($infofile);

There are also three corresponding container classes that hold data
and perform parsing functions.  They should only be used when actually
writing info file data.

   my $history =
     Amanda::Curinfo::History->new( $level, $size, $csize, $date, $secs );
   my $stats =
     Amanda::Curinfo::Stats->new( $level, $size, $csize, $secs, $date, $filenum,
       $label );

   my $perf = Amanda::Curinfo::Perf->new();
   $perf->set_rate( $pct1, $pct2, $pct3 );
   $perf->set_comp( $dbl1, $dbl2, $dbl3 );

Note that C<Amanda::Curinfo::Perf> is different.  This is because its
structure is broken up into two lines in the infofile format, and the
length of the C<rate> and C<comp> arrays maybe subject to change in
the future.

You can also instantiate these objects directly from a
properly-formatted line in an infofile:

   my $history = Amanda::Curinfo::History->from_line($hist_line);
   my $stats   = Amanda::Curinfo::Stats->from_line($stat_line);

   my $perf = Amanda::Curinfo::Perf->new();
   $perf->set_rate_from_line($rate_line);
   $perf->set_comp_from_line($comp_line);

Again, creating C<Amanda::Curinfo::Perf> is broken into two calls
because its object appears on two lines.

Writing these objects back to the info file, however, are all identical:

   print $infofh $history->to_line();
   print $infofh $stats->to_line();
   print $infofh $perf_full->to_line("full");
   print $infofh $perf_incr->to_line("incr");

Additionally, the C<$perf> object accepts a prefix to the line.

=head1 SEE ALSO

This package is meant to replace the file reading and writing portions
of server-src/infofile.h.  If you notice any bugs or compatibility
issues, please report them.

=head1 AUTHOR

Paul C. Mantz E<lt>pcmantz@zmanda.comE<gt>

=cut

my $numdot = qr{[.\d]};

package Amanda::Curinfo::Info;

use strict;
use warnings;
use Carp;

use Amanda::Config;

sub new
{
    my ($class, $infofile) = @_;

    my $self = {
        command => undef,
        full    => Amanda::Curinfo::Perf->new(),
        incr    => Amanda::Curinfo::Perf->new(),
        inf              => [],      # contains Amanda::Curinfo::Stats
        history          => [],      # contains Amanda::Curinfo::History
        last_level       => undef,
        consecutive_runs => undef,
    };

    bless $self, $class;
    $self->read_infofile($infofile) if -e $infofile;

    return $self;
}

sub get_dumpdate
{
    my ( $self, $level ) = @_;
    my $inf  = $self->{inf};
    my $date = 0;            # Ideally should be set to the epoch, but 0 is fine

    for ( my $l = 0 ; $l < $level ; $l++ ) {

        my $this_date = $inf->[$l]->{date};
        $date = $this_date if ( $this_date > $date );
    }

    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
      gmtime $date;

    my $dumpdate = sprintf(
        '%d:%d:%d:%d:%d:%d',
        $year + 1900,
        $mon + 1, $mday, $hour, $min, $sec
    );

    return $dumpdate;
}

sub read_infofile
{
    my ( $self, $infofile ) = @_;

    open my $fh, "<", $infofile or croak "couldn't open $infofile: $!";

    ## read in the fixed-length data
    $self->read_infofile_perfs($fh);

    ## read in the stats data
    $self->read_infofile_stats($fh);

    ## read in the history data
    $self->read_infofile_history($fh);

    close $fh;

    return 1;
}

sub read_infofile_perfs
{
    my ($self, $fh) = @_;

    my $fail = sub {
        my ($line) = @_;
        croak "error: malformed infofile header in $self->infofile:$line\n";
    };

    my $skip_blanks = sub {
        my $line = "";
        while ($line eq "") {
            croak "error: infofile ended prematurely" if eof($fh);
            $line = <$fh>;
        }
        return $line;
    };

    # version not paid attention to right now
    my $line = $skip_blanks->();
    ($line =~ /^version: ($numdot+)/) ? 1 : $fail->($line);

    $line = $skip_blanks->();
    ($line =~ /^command: ($numdot+)/) ? $self->{command} = $1 : $fail->($line);

    $line = $skip_blanks->();
    ($line =~ /^full-rate: ($numdot+) ($numdot+) ($numdot+)/)
      ? $self->{full}->set_rate($1, $2, $3)
      : $fail->($line);

    $line = $skip_blanks->();
    ($line =~ /^full-comp: ($numdot+) ($numdot+) ($numdot+)/)
      ? $self->{full}->set_comp($1, $2, $3)
      : $fail->($line);

    $line = $skip_blanks->();
    ($line =~ /^incr-rate: ($numdot+) ($numdot+) ($numdot+)/)
      ? $self->{incr}->set_rate($1, $2, $3)
      : $fail->($line);

    $line = $skip_blanks->();
    ($line =~ /^incr-comp: ($numdot+) ($numdot+) ($numdot+)/)
      ? $self->{incr}->set_comp($1, $2, $3)
      : $fail->($line);

    return 1;
}

sub read_infofile_stats
{
    my ( $self, $fh ) = @_;

    my $inf = $self->{inf};

    while ( my $line = <$fh> ) {

        ## try next line if blank
        if ( $line eq "" ) {
            next;

        } elsif ( $line =~ m{^//} ) {
            croak "unexpected end of data in stats section (received //)\n";

        } elsif ( $line =~ m{^history:} ) {
            croak "history line before end of stats section\n";

        } elsif ( $line =~ m{^stats:} ) {

            ## make a new Stats object and push it on to the queue
            my $stats = Amanda::Curinfo::Stats->from_line($line);
            push @$inf, $stats;

        } elsif ( $line =~ m{^last_level: (\d+) (\d+)$} ) {

            $self->{last_level}       = $1;
            $self->{consecutive_runs} = $2;
            last;

        } else {
            croak "bad line in read_infofile_stats: $line";
        }
    }

    return 1;
}

sub read_infofile_history
{
    my ( $self, $fh ) = @_;

    my $history = $self->{history};

    while ( my $line = <$fh> ) {

        if ( $line =~ m{^//} ) {
            return;

        } elsif ( $line =~ m{^history:} ) {
            my $hist = Amanda::Curinfo::History->from_line($line);
            push @$history, $hist;

        } else {
            croak "bad line found in history section:$line\n";
        }
    }

    #
    # TODO: make sure there were the right number of history lines
    #

    return 1;
}

sub write_to_file
{
    my ( $self, $infofile ) = @_;

    unlink $infofile if -f $infofile;

    open my $fh, ">", $infofile or die "error: couldn't open $infofile: $!";

    ## print basics

    print $fh "version: 0\n";    # 0 for now, may change in future
    print $fh "command: $self->{command}\n";
    print $fh $self->{full}->to_line("full");
    print $fh $self->{incr}->to_line("incr");

    ## print stats

    foreach my $stat ( @{ $self->{inf} } ) {
        print $fh $stat->to_line();
    }
    print $fh "last_level: $self->{last_level} $self->{consecutive_runs}\n";

    foreach my $hist ( @{ $self->{history} } ) {
        print $fh $hist->to_line();
    }
    print $fh "//\n";

    return 1;
}

1;

#
#
#

package Amanda::Curinfo::History;

use strict;
use warnings;
use Carp;

sub new
{
    my $class = shift;
    my ( $level, $size, $csize, $date, $secs ) = @_;

    my $self = {
        level => $level,
        size  => $size,
        csize => $csize,
        date  => $date,
        secs  => $secs,
    };

    return bless $self, $class;
}

sub from_line
{
    my ( $class, $line ) = @_;

    my $self = undef;

    if (
        $line =~ m{^history:    \s+
                     (\d+)      \s+  # level
                     ($numdot+) \s+  # size
                     ($numdot+) \s+  # csize
                     ($numdot+) \s+  # date
                     ($numdot+) $    # secs
                  }x
      ) {
        $self = {
            level => $1,
            size  => $2,
            csize => $3,
            date  => $4,
            secs  => $5,
        };
    } else {
        croak "bad history line: $line";
    }

    return bless $self, $class;
}

sub to_line
{
    my ($self) = @_;
    return
"history: $self->{level} $self->{size} $self->{csize} $self->{date} $self->{secs}\n";
}

1;

#
#
#

package Amanda::Curinfo::Perf;

use strict;
use warnings;
use Carp;

use Amanda::Config;

sub new
{
    my ($class) = @_;

    my $self = {
        rate => undef,
        comp => undef,
    };

    return bless $self, $class;
}

sub set_rate
{
    my ( $self, @rate ) = @_;
    $self->{rate} = \@rate;
}

sub set_comp
{
    my ( $self, @comp ) = @_;
    $self->{comp} = \@comp;
}

sub set_rate_from_line
{
    my ( $self, $line ) = @_;
    return $self->set_field_from_line( $self, $line, "rate" );

}

sub set_comp_from_line
{
    my ( $self, $line ) = @_;
    return $self->set_field_from_line( $self, $line, "comp" );

}

sub set_field_from_line
{
    my ( $self, $line, $field ) = @_;

    if (
        $line =~ m{\w+-$field\: \s+
                      ($numdot) \s+
                      ($numdot) \s+
                      ($numdot) $
                   }x
      ) {
        $self->{$field} = [ $1, $2, $3 ];

    } else {
        croak "bad perf $field line: $line";
    }

    return;
}

sub to_line
{
    my ( $self, $lvl ) = @_;
    return
        "$lvl-rate: "
      . join( " ", @{ $self->{rate} } ) . "\n"
      . "$lvl-comp: "
      . join( " ", @{ $self->{comp} } ) . "\n";
}

1;

#
#
#

package Amanda::Curinfo::Stats;

use strict;
use warnings;
use Carp;

sub new
{
    my $class = shift;
    my ( $level, $size, $csize, $secs, $date, $filenum, $label ) = @_;

    my $self = {
        level   => $level,
        size    => $size,
        csize   => $csize,
        secs    => $secs,
        date    => $date,
        filenum => $filenum,
        label   => $label,
    };

    bless $self, $class;
    return $self;
}

sub from_line
{
    my ( $class, $line ) = @_;
    my $self = undef;

    $line =~ m{^stats:      \s+
                     (\d+)      \s+   # level
                     ($numdot+) \s+   # size
                     ($numdot+) \s+   # csize
                     ($numdot+) \s+   # sec
                     ($numdot+) \s+   # date
                     ($numdot+) \s+   # filenum
                     (.*) $           # label
              }x
      or croak "bad stats line: $line";

    $self = {
        level   => $1,
        size    => $2,
        csize   => $3,
        secs    => $4,
        date    => $5,
        filenum => $6,
        label   => $7,
    };
    return bless $self, $class;
}

sub to_line
{
    my ($self) = @_;
    return join( " ",
        "stats:",      $self->{level}, $self->{size},    $self->{csize},
        $self->{secs}, $self->{date},  $self->{filenum}, $self->{label} )
      . "\n";
}

1;
