package Lire::ReportConfig;

use strict;

use vars qw/ $VERSION @ISA /;

use Lire::DataTypes qw/:special/;
use Lire::ReportSpec;
use Lire::FilterSpec;
use Lire::Report;
use Lire::ReportSection;
use Lire::ReportSpecFactory;

use Text::ParseWords qw/shellwords/;

use Carp;

BEGIN {
    ($VERSION)	= '$Revision: 1.4 $' =~ m!Revision: ([.\d]+)!;
}

my $debug = 0;
sub debug {
    $debug and warn $_[0];
}

=pod

=head1 NAME

Lire::ReportConfig - API to report configuration file

=head1 SYNOPSIS

    use Lire::ReportConfig;

    my $cfg = eval { new_from_file Lire::ReportConfig };
    $cfg->print();

=head1 DESCRIPTION

This class parses and writes report configuration file. All methods
will die on error.

=head1 CONFIGURATION FILE SYNTAX

Report configuration files are made of a series of section. Sections
are introduced by the C<=section> marker:

    =section Section's Title

Each section is made up of a series of filter specification that
should be shared by all the report specifications that follows.

Filter specification are introduced by the C<|> character followed by
the filter specification's id. The rest of the line is interpreted as
parameter assigment. Here is an example:

    |select-url url=http://www.logreport.org/

This will makes all report specifications of this section used the
C<select-url> filter specfication with the C<url> parameter set to the
C<http://www.logreport.org> value.

Report specifications lines starts with the specification's id and the
rest of the line is interpreted as parameters' assigment like in the
filter specification's case. Here is an example:

    top-clients clients_to_show=10

=head1 CONSTRUCTORS

=head2 new($superservice, [$factory] )

This creates a new Lire::ReportConfig object for the superservice
$superservice. The $factory parameter should be an instance of a
Lire::ReportSpecFactory which would be created to instantiate the
filter and report specification objects related to this configuration
file. If this parameter is omitted, a new object of the class
Lire::ReportSpecFactory will be created.

The created report configuration object created doesn't contain any
section, report or filter specifications.

=cut

sub new {
    my $proto = shift;
    my $class = ref $proto || $proto;

    my ( $super, $factory ) = @_;

    croak "invalid superservice: $super"
      unless check_superservice( $super );

    $factory ||= new Lire::ReportSpecFactory;
      croak "factory parameter isn't a Lire::ReportSpecFactory: $factory"
	unless UNIVERSAL::isa( $factory, "Lire::ReportSpecFactory" );
    
    bless { superservice => $super,
	    factory	 => $factory,
	    sections	 => [],
	  }, $class;
}

=pod

=head2 new_from_file( $superservice, $report_cfg, [$factory] )

This will create a new report configuration object for the
$superservice superservice based on the report configuration file
$report_cfg. The $factory parameter gives the Lire::ReportSpecFactory
object which should be used to create the report and filter
specifications contained in this report configuration. If omitted, a
default one will be used.

=cut

sub new_from_file {
    my $proto	= shift;
    my $class	= ref $proto || $proto;
    my ($superservice, $report_cfg, $factory) =	@_;

    my $self = $class->new( $superservice, $factory );

    debug( "new_from_file: calling load_from_file" );
    $self->load_from_file( $report_cfg );

    $self;
}

=pod

=head1 OBJECT METHODS

=head2 superservice()

Returns the report configuration's superservice.

=cut

sub superservice {
    my ( $self ) = @_;

    $self->{superservice};
}

=pod

=head2 factory()

Returns the factory object used by this report configuration.

=cut

sub factory {
    $_[0]{factory}
}

sub load_from_file {
    my ( $self, $report_cfg, $factory ) = @_;

    # Reset the sections
    $self->{sections} = [];

    # Format of the configuration file is
    # ( (=section <title>)?
    #   (|filter_id <param>)*
    #   (report_id  <param>))+
    #  )+

    my $state = "section"; # Can also be filter or report
    my $curr_section  = undef;

    # Load the report configuration file
    open CFG_FILE, $report_cfg
      or die "can't open report configuration file $report_cfg: $!\n";
    debug( "load_from_file: reading report configuration file " .
        "'$report_cfg'\n" );

    my $line;
  CFG_LINE:
    while ( defined( $line = <CFG_FILE> ) ) {
	next if $line =~ /^\s*#/; # Skip comments
	next if $line =~ /^\s*$/; # Skip blank lines

	chomp $line;

      debug( "load_from_file: processing line '$line'\n" );

      STATE:
	while ( $_ = $state ) {
	    /^section$/ && do {
		if ( $line =~ /^=section (.*)$/ ) {
		    $curr_section = new Lire::ReportSection( $self->superservice, $1 );
		    $self->add_section( $curr_section );
		    $state = "filter";
		    next CFG_LINE;
		} else {
		    # Create default section
		    $curr_section = 
		      new Lire::ReportSection( $self->superservice );
		    $self->add_section( $curr_section );
		    $state = "filter";
		    next STATE;
		}
	    };
	    /^filter$/ && do {
		if ( $line =~ /^\|(.*)/ ) {
		    my ( $id, $params ) = parse_param_line( $1 );

		    eval {
			my $spec = Lire::FilterSpec->load( $self->superservice,
							   $id, 
							   $self->{factory} );

			while ( my ($name, $value) = each %$params ) {
			    $spec->param( $name )->value( $value );
			}
			$curr_section->add_filter( $spec );
		    };
		    if ( $@ ) {
			warn( "error at line $.: $@\n" );
			warn( "Omitting filter $id defined at line $.\n" );
		    }
		    next CFG_LINE;
		} elsif ( $line =~ /^=section/ ) {
		    warn "unexpected =section at line $.. Ignoring.\n";
		    next CFG_LINE;
		} else {
		    # Report
		    $state = "report";
		    next STATE;
		}
	    };
	    /^report$/ && do {
		if ( $line =~ /^=section/ ) {
		    # Some reports were defined
		    $state = "section";
		    next STATE;
		} elsif ( $line =~ /^\|/ ) {
		    # Filter -> create a new section
		    $curr_section = 
		      new Lire::ReportSection( $self->superservice );
		    $self->add_section( $curr_section );
		    $state = "filter";
		    next STATE;
		} else {
		    my ( $id, $params ) = parse_param_line( $line );

		    eval {
			my $report_spec =
			  Lire::ReportSpec->load( $self->superservice, $id,
						  $self->{factory} );

			while ( my ($name, $value) = each %$params ) {
			    $report_spec->param( $name )->value( $value );
			}
			$curr_section->add_report( $report_spec );
		    };
		    if ( $@ ) {
			warn( "error at line $.: $@\n" );
			warn( "Omitting report $id defined at line $.\n" );
		    }
		    next CFG_LINE;
		}
	    };
	    die "unknown state: $state";
	}
    }
    close CFG_FILE;
}

sub parse_param_line {
    my ( $id, @p ) = shellwords( $_[0] );
    my %params = ();
    foreach my $param_str ( @p ) {
	my ( $param, $value ) = $param_str =~ /^([-.\w]+)=(.*)$/;
	unless ( defined $param ) {
	    warn( "error parsing parameter $param_str at line $.. Ignoring parameter.\n" );
	    next;
	}

	$value = "" unless defined $value;
	$params{$param} = $value;
    }

    return ( $id, \%params );
}

sub create_param_line {
    my ( $id, $spec ) = @_;

    my @line = ( $id );
    foreach my $name ( $spec->param_names ) {
	my $value = $spec->param( $name )->value;
	$value =~ s/\\/\\\\/g;	# Escape backslashes
	$value =~ s/"/\\"/g;	# and double quotes
	
	push @line, $name . '="' . $value . '"';
    }
    return join( " ", @line );
}

=pod

=head2 sections()

Return's this report configuration's sections as an array of
Lire::Section objects

=cut

sub sections {
    my ( $self ) = @_;

    return @{$self->{sections}};
}

=pod

=head2 add_section( $section )

Adds a section to this report configuration. The $section parameter
should be a Lire::ReportSection object.

=cut

sub add_section {
    my ( $self, $section ) = @_;

    croak ( "section should be of type Lire::ReportSection (not $section)" )
      unless UNIVERSAL::isa( $section, "Lire::ReportSection" );

    croak( "section's superservice ", $section->superservice, 
	   " is different than this report configuration's one:",
	   $self->superservice )
      if $self->superservice ne $section->superservice;

    push @{$self->{sections}}, $section; 
}

=pod 

=head2 merge_filters()

Calling this method will make sure that all report specifications
takes into account their section's filter specification. 

This method will modify all report specifications. After this their
object representation won't be identical to the one in the XML report
specification.

=cut

sub merge_filters {
    my ( $self ) = @_;

    my $factory = $self->factory;
    foreach my $section ( $self->sections ) {
	my @filters = map { $_->filter_spec } $section->filters;
	my @reports = $section->reports;

	foreach my $r ( @reports ) {
	    if ( @filters ) {
		my $expr;
		if ( $r->filter_spec || @filters > 1) {
		    $expr = $factory->create_and_expr( container => $r );
		    if ( $r->filter_spec ) {
			$expr->expr( [ @filters, $r->filter_spec ] );
		    } else {
			$expr->expr( [@filters] );
		    }
		} else {
		    $expr = $filters[0];
		}
		$r->filter_spec( $expr );
	    }
	}
    }
}

=pod

=head2 print( [$fh] )

Prints the report configuration on the $fh filehandle. If the $fh
parameter is omitted, the report configuration will be printed on
STDOUT.

=cut

sub print {
    my ( $self, $fh ) = @_;
    
    $fh ||= \*STDOUT;

    foreach my $section ( $self->sections ) {
	print $fh "=section ", $section->title, "\n";

	foreach my $filter ( $section->filters ) {
	    print $fh create_param_line( "|" . $filter->id, $filter ), "\n";
	}

	foreach my $report ( $section->reports ) {
	    print $fh create_param_line( $report->id, $report ), "\n";
	}

	# Empty line
	print $fh "\n"
    }
}

=pod

=head2 create_report( $timespan_start, $timespan_end )

Returns a Lire::Report object based on this report configuration file.
The $timespan_start and $timespan_end attribute will be used to

=cut

sub create_report {
    my ( $self, $timespan_start, $timespan_end) = @_;

    my $report = new Lire::Report( $self->superservice, $timespan_start, 
				   $timespan_end,
				 );

    foreach my $s ( $self->sections ) {
	$s->create_report_section( $report );
    }

    return $report;
}

# keep perl happy
1;

__END__

=pod

=head1 SEE ALSO

Lire::ReportSection(3pm) Lire::ReportSpec(3pm) Lire::FilterSpec(3pm)
Lire::Report(3pm) Lire::ReportSpecFactory(3pm)

=head1 VERSION

$Id: ReportConfig.pm,v 1.4 2002/06/21 19:49:36 flacoste Exp $

=head1 COPYRIGHT

Copyright (C) 2002 Stichting LogReport Foundation LogReport@LogReport.org

This file is part of Lire.

Lire 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 (see COPYING); if not, check with
http://www.gnu.org/copyleft/gpl.html or write to the Free Software 
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.

=head1 AUTHOR

Francis J. Lacoste <flacoste@logreport.org>

=cut
