package Lire::ReportParser::ExcelWriter;

use strict;

use vars qw/ $VERSION @ISA $DATE_SYSTEM @name_cols_width/;

use Lire::ReportParser::DocBookFormatter;

use Lire::Program qw/ :msg /;

use Spreadsheet::WriteExcel;
#use Time::Timezone;
use POSIX qw/ceil/;

use constant MAX_EXCEL_STRING	=> 255;
use constant MAX_SHEET_STRING	=> 31;
use constant DEFAULT_ROW_HEIGHT => 12.75;

# Those values are in Excel Points
#use constant CHAR_WIDTH		=> 5.3; 
#use constant DEFAULT_CELL_WIDTH => 48;
#use constant MAX_CELL_WIDTH	=> 4 * 48;
# Spreadsheet::WriteExcel seems to work with characters
use constant CHAR_WIDTH	=> 1;
use constant DEFAULT_CELL_WIDTH => 9;
use constant MAX_CELL_WIDTH => 36;

BEGIN {
    ($VERSION)	= '$Revision: 1.4 $' =~ m!Revision: ([.\d]+)!;
    @ISA = qw/ Lire::ReportParser::DocBookFormatter/;

    # Use 1900 date system on all platforms other than Apple Mac (for which
    # use 1904 date system).
    $DATE_SYSTEM = ($^O eq 'MacOS') ? 1 : 0;
}

#
# This was inspired by the comments and programs datecalc1.pl 
# included in Spreadsheet::WriteExcel and is
# Copyright (c) 2000, Andrew Benham.
# This program is free software. It may be used, redistributed and/or
# modified under the same terms as Perl itself.
#
# It was modified to use Time::Timezone which is included in Lire.
#
sub epoch2excel_date {
    my ($time) = @_;

    # Divide timestamp by number of seconds in a day.
    # This gives a date serial with '0' on 1 Jan 1970.
    my $serial = $time / 86400;

    # Adjust the date serial by the offset appropriate to the
    # currently selected system (1900/1904).
    if ($DATE_SYSTEM == 0) {	# use 1900 system
	$serial += 25569;
    } else {			# use 1904 system
	$serial += 24107;
    }

    # Actually, this gives the wrong date. Maybe this is specific to
    # gnumeric and OpenOffice on UNIX.
    # The correct date appears if we don't correct the value for time zone.
    #
    # Fix for timezone
    # my $offset = tz_local_offset( $time );
    # $serial += $offset / 8640;

    # Perpetuate the error that 1900 was a leap year by decrementing
    # the serial if we're using the 1900 system and the date is prior to
    # 1 Mar 1900. This has the effect of making serial value '60'
    # 29 Feb 1900.

    # This fix only has any effect if UNIX/Perl time on the platform
    # can represent 1900. Many can't.

    unless ($DATE_SYSTEM) {
	$serial-- if ($serial < 61);	# '61' is 1 Mar 1900
    }

    return $serial;
}

sub report_start {
    my $self = shift;
    $self->SUPER::report_start( @_ );

    $self->{report_date} = undef;
    $self->{report_timespan} = undef;
    $self->{workbook} = new Spreadsheet::WriteExcel( "-" );

    # Create some format that we are going to use
    $self->{title_fmt} = 
      $self->{workbook}->addformat(
				   size => 18,
				   bold	=> 1,
				  );
    $self->{date_fmt} = 
      $self->{workbook}->addformat(
				   num_format => "yyyy/mm/dd",
				   align      => "left",
				   valign     => "top",
				  );
    $self->{timestamp_fmt} = 
      $self->{workbook}->addformat(
				   num_format => "yyyy/mm/dd hh:mm:ss",
				   align      => "left",
				   valign     => "top",
				  );
    $self->{datetime_fmt} = 
      $self->{workbook}->addformat(
				   num_format => "yyyy/mm/dd hh:mm",
				   align      => "left",
				   valign     => "top",
				  );
    $self->{time_fmt} = 
      $self->{workbook}->addformat(
				   num_format => "hh:mm",
				   align      => "left",
				   valign     => "top",
				  );
    $self->{name_fmt} =
      $self->{workbook}->addformat(
				   num_format => 0x31,  # Builtin: @
				   align      => "justify",
				   valign     => "top",
				  );
    $self->{label_fmt} =
      $self->{workbook}->addformat(
				   num_format => 0x31,  # Builtin: @
				   align      => "left",
				   valign     => "left",
				   bold	      => 1,
				   size	      => 12,
				  );

    $self->{description_fmt} =
      $self->{workbook}->addformat(
				   num_format => 0x31,  # Builtin: @
				   align      => "left",
				   valign     => "left",
				  );

    $self->{value_fmt} = 
      $self->{workbook}->addformat( 
				   num_format => 0x00,   # Builtin: General
				   align      => "right",
				   valign     => "top",
				  );
}

sub report_end {
    my $self = shift;
    $self->SUPER::report_end( @_ );

    # Activate the first sheet
    my $first = ($self->{workbook}->sheets)[0];
    if ( $first ) {
	$first->set_first_sheet();
	$first->activate();
    }
    $self->{workbook}->close;
}

sub section_end {
    my ( $self ) = shift;

    $self->{curr_section}->write( $self->{curr_row}++, 0,
				  "No subreports were generated for this section.",
				  $self->{label_fmt},
				)
      if ( ! $self->current_section_subreport_count);

    $self->SUPER::section_end( @_ );
}

sub handle_title {
    my ( $self, $title ) = @_;

    # Trim the title
    $title =~ s/^\s*//;
    $title =~ s/\s*$//;
    if ($self->in_element( "lire:section" ) ) {
	# Section's title
	my $title = substr( $title, 0, MAX_SHEET_STRING );

	my $first_sheet = ! $self->{workbook}->sheets;

	# Create a new worksheet
	$self->{curr_section} = $self->{workbook}->addworksheet( $title );
	$self->{curr_row} = 0;
	$self->{curr_cols_width} = [];

	if ( $first_sheet ) {
	    # Put "header" information on first sheet
	    my @labels = ( "Report generated", "Log file started",
			   "Log file ended" );
	    my @dates = ( $self->{report_date}, @{$self->{report_timespan}} );
	    for (my $i=0; $i<@labels; $i++) {
		my $date = epoch2excel_date( $dates[$i] );
		$self->{curr_section}->write( $i, 0, $labels[$i],
					      $self->{label_fmt},
					    );
		$self->{curr_section}->write( $i, 1, $date,
					      $self->{timestamp_fmt},
					    );
		$self->{curr_section}->set_row( $i, 14 );
	    }

	    $self->{curr_row} = @labels + 1;

	    my $width = length("yyyy/mm/dd hh:mm:ss") * CHAR_WIDTH;
	    $self->{curr_cols_width}[1] = $width;
	    $self->{curr_section}->set_column( 1, 1, $width );
	}
    } else {
	# Subreport's title
	my $title = substr( $title, 0, MAX_EXCEL_STRING );

	$self->{curr_section}->set_row( $self->{curr_row}, 22 );
	$self->{curr_section}->write( $self->{curr_row}++, 0,
				      $title,
				      $self->{title_fmt},
				    );
    }
}

sub handle_description {
    my ( $self, $desc ) = @_;

    # Put each line in a separate cell
    foreach my $line ( split /\n/, $desc ) {
	if ( defined $line ) {
	    # Trim spaces
	    $line =~ s/^\s+//;
	    $line =~ s/\s+$//;
	    $self->{curr_section}->write( $self->{curr_row}, 0,
					  $line,
					  $self->{description_fmt},
					);
	}

	$self->{curr_row}++;
    }

    # Skip one row after the description
    $self->{curr_row}++;
}

sub handle_date {
    my ( $self, $date, $time ) = @_;

    $self->{report_date} = $time
      unless defined $self->{report_date};
}

sub handle_timespan {
    my ( $self, $timespan, $start, $end ) = @_;

    $self->{report_timespan} = [$start, $end]
      unless defined $self->{report_timespan};
}

sub subreport_end {
    my ( $self ) = shift;

    $self->SUPER::subreport_end( @_ );

    # Leave one extra empty row
    $self->{curr_row} += 2;
}

sub table_start {
    my ( $self ) = shift;

    $self->SUPER::table_start( @_ );

    # We may need to move the first entry values since we'll only
    # be sure of the column in which the values starts once the
    # first entry is completely process
    $self->{first_entry} = 1;
    $self->{first_entry_values} = [];
    $self->{value_col} = 0;
}

sub table_end {
    my ( $self ) = shift;

    # Skip one row
    $self->{curr_row}++;

    $self->{curr_section}->write( $self->{curr_row}++, 0, 
				  "No content in report.",
				  $self->{label_fmt},
				)
      if ! $self->current_table_entry_count;

    $self->SUPER::table_end( @_ );
}

sub entry_start {
    my ( $self ) = shift;

    $self->SUPER::entry_start( @_ );

    return unless $self->show_current_entry;

    $self->{curr_col} = $self->current_group_level;
    $self->{curr_row}++;
    $self->{curr_row_height} = DEFAULT_ROW_HEIGHT;
}

sub entry_end {
    my ( $self ) = shift;

    $self->SUPER::entry_end( @_ );
    return unless $self->{first_entry};

    return unless $self->current_group_level == 0;

    # We have completed the first entry
    # Rewrite all the values that were writen before the value column
    foreach my $tvalue ( @{$self->{first_entry_values}} ) {
	my ( $row, $col, $value ) = @$tvalue;
	if ( $col < $self->{value_col} ) {
	    $self->{curr_section}->write( $row, $col, " ",
					  $self->{value_fmt} );
	    $self->{curr_section}->write( $row, $self->{value_col}, 
					  $value, $self->{value_fmt} );
	}
    }
    
    $self->{first_entry} = 0;
}

sub handle_name {
    my ( $self, $name ) = @_;

    my $s = substr $name->{content}, 0, MAX_EXCEL_STRING;
    my $fmt = $self->{name_fmt};

    if ( $name->{value} =~ /^\d+$/ &&
	 $name->{content} !~ /^(\[|Week)/ && #! Skip Week and rangegroup
	 defined $name->{range} &&
	 $name->{range} =~ /^\d+$/
       ) 
    {
	$s = epoch2excel_date( $name->{value} );
	if ( $name->{range} < 86400 ) {
	    $fmt = $self->{datetime_fmt};
	} else {
	    $fmt = $self->{date_fmt};
	}
    }

    my $col = $self->{curr_col};

    # Fix the column width
    my $len	    = length $s;
    my $curr_width  = $self->{curr_cols_width}[$col] || DEFAULT_CELL_WIDTH;
    my $width	    = $len * CHAR_WIDTH > MAX_CELL_WIDTH ? MAX_CELL_WIDTH : 
      $len * CHAR_WIDTH;
    if ( $width > $curr_width ) {
	$self->{curr_cols_width}[$col] = $width;
	$self->{curr_section}->set_column( $col, $col, $width );
    }

    # Enlarge the row's height i
    if ( $len * CHAR_WIDTH > MAX_CELL_WIDTH ) {
	my $row = ceil( $len * CHAR_WIDTH / MAX_CELL_WIDTH );
	my $height = $row * DEFAULT_ROW_HEIGHT;

	if ( $height > $self->{curr_row_height} ) {
	    $self->{curr_section}->set_row( $self->{curr_row}, $height, );
	    $self->{curr_row_height} = $height;
	}
    }

    # Write it
    $self->{curr_section}->write( $self->{curr_row}, $col, $s, $fmt );
    $self->{curr_col}++;
}

sub handle_value {
    my ( $self, $value ) = @_;

    if ( $self->{first_entry} ) {
	push @{$self->{first_entry_values}}, [ $self->{curr_row}, 
					       $self->{curr_col},
					       $value->{value},
					     ];
	$self->{value_col} = $self->{curr_col} 
	  if $self->{curr_col} > $self->{value_col};
    } elsif ( $self->{curr_col} < $self->{value_col}) {
	# Jump straight to value column
	$self->{curr_col} = $self->{value_col};
    }

    $self->{curr_section}->write( $self->{curr_row}, 
				  $self->{curr_col}++,
				  $value->{value},
				  $self->{value_fmt},
				);
}

# keep perl happy
1;

__END__

=pod

=head1 NAME

Lire::ReportParser::ExcelWriter - Transform Lire XML Reports into a Excel95(tm) spreadsheet.

=head1 SYNOPSIS

    use Lire::ReportParser::ExceltWriter;

    my $parser = new Lire::ReportParser::ExcelWriter();

    eval { $parser->parsefile( "report.xml" ) };
    print "Parse failed: $@" if $@;

=head1 DESCRIPTION

This is a Lire::ReportParser processor which will transform the Lire
XML report into a Excel95(tm) spreadsheet which will be output on
STDOUT.

Its constructor doesn't take any parameters.

=head1 SEE ALSO

Lire::ReportParser(3pm) Spreadsheet::WriteExcel(3pm)

=head1 VERSION

$Id: ExcelWriter.pm,v 1.4 2002/08/17 01:56:12 flacoste Exp $

=head1 COPYRIGHT

Copyright (C) 2001-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
