# vim: ts=8 sw=8 tw=0 ai nu noet
#
# PDF::ReportWriter::Report
#
# (C) Daniel Kasak: dan@entropy.homelinux.org ...
#  ... with contributions from Bill Hess and Cosimo Streppone
#      ( see the changelog for details )
#
# See COPYRIGHT file for full license
#
# See 'perldoc PDF::ReportWriter::Report',
#     'perldoc PDF::ReportWriter' for full documentation
#

package PDF::ReportWriter::Report;
use strict;
use XML::Simple;
use PDF::ReportWriter::Datasource;

BEGIN {
	$PDF::ReportWriter::Report::VERSION = '1.0';
}

sub new {

	my ( $class, $opt ) = @_;
	$class = ref($class) || $class;

	# Make $opt a hashref if it isn't already
	# Passing a string with report filename should work.
	if( ! ref $opt ) {
		if( ( defined $opt ) && ( $opt ne '' ) ) {
			$opt = { report => $opt };
		} else {
			$opt = {};
		}
	}

	my $self = { %$opt };
	my $file;

	if( exists $self->{report} ) {
		$file = $self->{report};
	}

	if( defined $file && ! -e $file )
	{
		#croak qq(Can't find $file xml report file);
		return(undef);
	}

	bless $self, $class;
}

#
# Returns current filename of xml report
#
sub file {
	my $self = $_[0];
	return $self->{report};
}

#
# Loads xml report definition
#
sub load {
	my $self = shift;
	my $file = shift || $self->file();
	my $cfg  = $self->config();

	# Return already loaded configuration instead of
	# reloading from scratch
	return $cfg if $cfg;

	my $xml = XML::Simple->new(
		
		# Don't create group name keys
		#
		# FIXME Is there a way to avoid `name' hash keys creation,
		#       without issuing warnings about nonexistent `_' key?

		KeyAttr => {
			group => '_',
			field => '_',
		},
		
		GroupTags => {
			header => 'cell',
			footer => 'cell',
			groups => 'group',
			fields => 'field',
		},

		Variables => $self->get_macros(),

	);

	$cfg = $xml->XMLin( $file );

	if( ! ref( $cfg ) )
	{
		warn qq(Can't read from xml file $file);
		return ( undef );
	}

	$self->_adjust_struct( $cfg );

	# Store configuration inside object
	$self->config( $cfg );

	return ( $cfg );

}

#
# Returns all available data sources found in xml file
# There should be one `detail' data source at least
#
sub data_sources
{
	my $self = $_[0];
	my $ds   = $self->config->{data}->{datasource};
	my %ds;

	if ( ref $ds eq 'HASH' ) {
		$ds = [ $ds ];
	}

	for( @$ds )
	{
		$ds{$_->{name}} = $_;
	}

	return(\%ds);
}

#
# Report's get_data() passes the work to PDF::ReportWriter::Datasource objects
#
{
	# Main cache for datasources actual data, to avoid
	# useless repeated calls to get_data()
	my %ds_cache;

sub get_data
{
	my $self   = $_[0];
	my $dsname = $_[1] || 'detail';

	# Check if cached data already exists
	# Here we assume that while report is generating, data does not change
	if( exists $ds_cache{$dsname} )
	{
		return $ds_cache{$dsname};
	}

	# Get all available d.s.
	my $ds = $self->data_sources();

	# Datasource does not exists, return blank data
	if( ! exists $ds->{$dsname} )
	{
		warn 'Data source '.$dsname.' does not exist';
		return ();
	}

	# Try to create a P::R::Datasource object
	my $ds_obj = PDF::ReportWriter::Datasource->new($ds->{$dsname});

	if( ! defined $ds_obj )
	{
		warn 'Data source '.$dsname.' not available!';
		return ();
	}

	# Ok, datasource object loaded, call get_data() on it
	return ( $ds_cache{$dsname} = $ds_obj->get_data($self) );
}

}

#
# Default implementation for get_macros() returns nothing.
# Get variables should be used for search&replace in the XML file.
# before loading.
#
sub get_macros {
	return undef;
}

#
# Returns (or modifies) current report configuration (profile)
#
sub config {
	my $self = shift;

	# If passed a parameter, change config member to that
	if( @_ ) {
		$self->{_config} = $_[0];
	}

	return ( $self->{_config} );
}

#
# Sanity checks and adjustments on data structures
#
# TODO 1: here XML::Simple does magic but not enough to simply
# pass the data structure plainly as read.
#
# TODO 2: Should we avoid at all this thing?
#         Probably modifying PDF::ReportWriter to handle
#         all the different cases (HASH, ARRAY, ...)
#
sub _adjust_struct {

	my ( $self, $config ) = @_;
	my $data = $config->{data};
	local $_;

	# Force `fields' to be an array even with 1 element
	if( ref ( $data->{fields} ) eq 'HASH' ) {
		$data->{fields} = [ $data->{fields} ];
	}

	if( ref ( $data->{groups} ) eq 'HASH' ) {
		$data->{groups} = [ $data->{groups} ];
	}

	# Now for `groups' section
	for(@{ $data->{groups} }) {

		# Remove empty header/footer sections
		# Force header/footer to array even if they have 1 element
		if( ref ( $_->{header} ) eq 'HASH' ) {
			if( keys %{$_->{header}} ) {
				$_->{header} = [ $_->{header} ];
			} else {
				delete $_->{header};
			}
		}

		if( ref ( $_->{footer} ) eq 'HASH' ) {
			if( keys %{$_->{footer}} ) {
				$_->{footer} = [ $_->{footer} ];
			} else {
				delete $_->{footer};
			}
		}
	}

	# Same as above for `page' structure
	my $page = $data->{page};
	if( ref ( $page->{header} ) eq 'HASH' ) {
		$page->{header} = [ $page->{header} ];
	}
	if( ref ( $page->{footer} ) eq 'HASH' ) {
		$page->{footer} = [ $page->{footer} ];
	}

	# Same as above for font structure
	if( ! ref $config->{definition}->{font} )
	{
		$config->{definition}->{font} = [ $config->{definition}->{font} ];
	}

	return ( $config );
}

#
# ->save( \%config [, filename] )
#
# Saves the report back to XML format.
# This is mainly an experiment. Don't know if it works correctly,
# even if the test suite seems to say so.
#
# %config = (
#
#    definition => {
#        ...,
#        info => { ... },
#        ...,
#    },
#
#    page   => {
#        header => [ ],
#        footer => [ ],
#    },
#
#    data   => {
#        fields => ...,
#        groups => ...,
#        ...
#    },
# )
#
sub save
{
	my($self, $cfg, $file) = @_;

	$file ||= $self->file();

	my $xml = XML::Simple->new(
		AttrIndent => 1,
		ForceArray => 1,
		KeepRoot   => 1,
		NoAttr     => 1,
		NoSort     => 1,
		RootName   => 'report',
		XMLDecl    => 1,
	);

	my $xml_stream = $xml->XMLout($cfg);
	my $ok = 0;

	#warn 'opening file '.$file;

	if( open(XML_REPORT, '>' . $file) )
	{
		#warn 'opened file';
		$ok = print XML_REPORT $xml_stream;
		#warn 'printed '.$ok.' on it';
		$ok &&= close(XML_REPORT);
		#warn 'closed '.$ok;
	}

	# Report saved?
	return($ok);
}


1;

=head1 NAME

PDF::ReportWriter::Report

=head1 DESCRIPTION

PDF::ReportWriter::Report is a PDF::ReportWriter class that represents a single report.
It handles the conversions from/to XML to PDF::ReportWriter correct data structures,
and can provide the data to be used in the report.
XML::Simple module is used for data structures serialization to XML and restore.

This class is designed in a way that should be simple to be overloaded,
and thus provide alternative classes that load reports in a totally different way,
or supply data connecting automatically to a DBI DSN, or who knows...

=head1 USAGE

The most useful usage for this class is through the C<PDF::ReportWriter::render_report()>
call. If you really want an example of usage of standalone Report object, here it is:

	# Create a blank report object
	my $report = PDF::ReportWriter::Report->new();
	my $config;

	# Load XML report definition
	eval { $config = $report->load('/home/cosimo/myreport.xml') };
	if( $@ ) {
	    # Incorrect xml file!
	    print 'Error in XML report:', $@, "\n";
	}

	# Now save the report object to xml file
	my $ok = $report->save($config);
	my $ok = $report->save($config, 'Copy of report.xml');

=head1 METHODS

=head2 new( options )

Creates a new C<PDF::ReportWriter::Report> object. C<options> is a hash reference.
Its only required key is C<report>, which is the xml filename of the report definition.
It is stored inside the object, allowing to later B<save> your report in that filename.

=head2 get_data( ds_name )

The only parameter required is the datasource name C<ds_name>.
If no data is supplied to the C<PDF::ReportWriter::render_report()> call,
this method checks for all available data sources defined in your xml
report. They must be included in the C<data> section.
Check out the examples.

The main data source that provides the data for report main table must
be called C<detail>, or you get an empty report.
Additional data sources can be defined, as in the following (fake) example:

    <report>
        ...
        <data>
	    ...
	    <datasource name="ldapdirectory">
	    	<hostname>192.168.0.1</hostname>
		<port>389</port>
		<rootdn>o=Users,dc=domain,dc=com</rootdn>
		<binddn>cn=DirectoryManager,dc=domain,dc=com</binddn>
		<password>secret</password>
	    </datasource>
	    ...
	</data>
	...
    </report>

=head2 get_macros()

Should be used to return all text macros that must be searched and replaced inside
the XML content before converting it into the C<PDF::ReportWriter> profile.
Example:

    ...
    <!-- Xml report -->
    <report>
        <definition>
	    <name>My Report</name>
	    <info>
	        <Author>${AUTHOR}</Author>
		...

A corresponding C<get_macros> method should return:

    sub get_macros {
        return { 'AUTHOR' => 'Isaac Asimov' };
    }

The default implementation returns no macro.

=head2 load( [xml_file] )

Loads the report definition from C<xml_file>. No, don't be afraid! This is a friendly
and nice xml file, not those ugly monsters that populate JavaLand. :-)
Return value is an hashref with complete report profile.

	my $report  = PDF::ReportWriter::Report->new();
	my $profile = $report->load('myreport.xml');
	if( ! $profile ) {
		print "Something wrong in the XML?";
	}

=head2 save( config [, xml_file ] )

Saves the report profile passed in C<config> parameter (as a hashref)
to the file defined in C<new()> or to C<xml_file> if supplied.

The result won't be exactly equal to the source xml file, but should
be equivalent when loading the data structures to build your final report.

=head1 CUSTOM REPORT CLASSES

The design of C<PDF::ReportWriter::Report> allows one to build a custom class
that provides alternative behavior for C<load()> and C<get_data()> methods.

C<load()> method can do anything, but it must return a complete report
data structure to be fed into C<PDF::ReportWriter> object. That consists
into several hashrefs:

=over *

=item definition

All high-level report properties, such as C<paper>,
C<destination>, ...

=item page

Page header and footer list of cells. See xml report samples in
the C<examples> folder.

=item data

The main section which defines C<fields> and C<groups>.
Check out the examples and use Data::Dumper on results
of C<load()> method. Sorry. :-)

=back

=head1 TODO

=over *

=item Complete documentation?

=back

=head1 AUTHORS

=over 4

=item Dan <dan@entropy.homelinux.org>

=item Cosimo Streppone <cosimo@cpan.org>

=back

=head1 Other cool things you should know about:

=over 4

This module is part of an umbrella project, 'Axis Not Evil', which aims to make
Rapid Application Development of database apps using open-source tools a reality.
The project includes:

Gtk2::Ex::DBI                 - forms

Gtk2::Ex::Datasheet::DBI      - datasheets

PDF::ReportWriter             - reports

All the above modules are available via cpan, or for more information, screenshots, etc, see:
http://entropy.homelinux.org/axis_not_evil

=back

=cut
