#**************************************************************
#  
#  Licensed to the Apache Software Foundation (ASF) under one
#  or more contributor license agreements.  See the NOTICE file
#  distributed with this work for additional information
#  regarding copyright ownership.  The ASF licenses this file
#  to you under the Apache License, Version 2.0 (the
#  "License"); you may not use this file except in compliance
#  with the License.  You may obtain a copy of the License at
#  
#    http://www.apache.org/licenses/LICENSE-2.0
#  
#  Unless required by applicable law or agreed to in writing,
#  software distributed under the License is distributed on an
#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#  KIND, either express or implied.  See the License for the
#  specific language governing permissions and limitations
#  under the License.
#  
#**************************************************************



package installer::logger;

use installer::files;
use installer::globals;
use Time::HiRes qw(gettimeofday tv_interval);
use English;
use IO::Handle;
use strict;

my $StartTime = undef;

sub PrintStackTrace ();
sub Die ($);

=head1 NAME

    installer::logger

    Logging for the installer modules.

=cut

=head1 DESCRIPTION

    This module is in a transition state from a set of loosly connected functions to a single class.

    There are three globaly available logger objects:

=over

=item $Lang

    is language specific and writes messages to a log file.

=cut

=item $Glob

    is independent of the current language.  Its messages are prepended to each $Lang logger.

=cut

=item $Info

    is for output to the console.

=cut

=back

=cut
    

our $Global = installer::logger->new("glob",
    'is_save_lines' => 1,
    'is_print_to_console' => 0,
    'is_show_relative_time' => 1);
our $Lang = installer::logger->new("lang",
    'is_print_to_console' => 0,
    'is_show_relative_time' => 1,
    'is_show_log_id' => 1
    );
our $Info = installer::logger->new("info",
    'is_show_relative_time' => 0,
    'is_show_process_id' => 0,
    'is_show_log_id' => 0
    );



=head2 SetupSimpleLogging ($filename)

    Setup logging so that $Global, $Lang and $Info all print to the console.
    If $filename is given then logging also goes to that file.
    
=cut
sub SetupSimpleLogging (;$)
{
    my ($log_filename) = @_;

    $Info = installer::logger->new("info",
        'is_print_to_console' => 1,
        'is_show_relative_time' => 1,
        );
    $Global = installer::logger->new("glob",
        'is_print_to_console' => 0,
        'is_show_relative_time' => 1,
        'forward' => [$Info]
        );
    $Lang = installer::logger->new("lang",
        'is_print_to_console' => 0,
        'is_show_relative_time' => 1,
        'forward' => [$Info]
        );
    if (defined $log_filename)
    {
        $Info->set_filename($log_filename);
    }
    $Info->{'is_print_to_console'} = 1;
    $installer::globals::quiet = 0;
    starttime();
}




=head2 new($class, $id, @arguments)

    Create a new instance of the logger class.
    @arguments lets you override default values.

=cut

sub new ($$@)
{
    my ($class, $id, @arguments) = @_;
    
    my $self = {
        'id' => $id,
        'filename' => "",
        # When set then lines are printed to this file.
        'file' => undef,
        # When true then lines are printed to the console.
        'is_print_to_console' => 1,
        'is_save_lines' => 0,
        # A container of printed lines.  Lines are added only when 'is_save_lines' is true.
        'lines' => [],
        # Another logger to which all prints are forwarded.
        'forward' => [],
        # A filter function that for example can recoginze build errors.
        'filter' => undef,
        # Show relative time
        'is_show_relative_time' => 0,
        # Show log id (mostly for debugging the logger)
        'is_show_log_id' => 0,
        # Show the process id, useful on the console when doing a multiprocessor build.
        'is_show_process_id' => 0,
        # Current indentation
        'indentation' => "",
    };
    while (scalar @arguments >= 2)
    {
        my $key = shift @arguments;
        my $value = shift @arguments;
        $self->{$key} = $value;
    }
    
    bless($self, $class);

    return $self;
}



=head2 printf($self, $message, @arguments)

    Identical in syntax and semantics to the usual perl (s)printf.

=cut
sub printf ($$@)
{
    my ($self, $format, @arguments) = @_;

    if ($format =~ /\%\{/)
    {
        printf(">%s<\n", $format);
        PrintStackTrace();
    }
    my $message = sprintf($format, @arguments);
    $self->print($message, 0);
}




=head2 print ($self, $message, [optional] $force)

    Print the given message.
    If the optional $force parameter is given and it evaluates to true then the message
    is printed even when the golbal $installer::globals::quiet is true.

=cut
sub print ($$;$)
{
    my ($self, $message, $force) = @_;

    Die "newline at start of line" if ($message =~ /^\n.+/);

    $force = 0 unless defined $force;

    my $relative_time = tv_interval($StartTime, [gettimeofday()]);
    foreach my $target ($self, @{$self->{'forward'}})
    {
        $target->process_line(
            $relative_time,
            $self->{'id'},
            $PID,
            $message,
            $force);
    }
}




=head2 process_line ($self, $relative_time, $log_id, $pid, $message, $force)

    Internal function that decides whether to
    a) write to a log file
    b) print to the console
    c) store in an array for later use
    the preformatted message.

=cut
sub process_line ($$$$$$)
{
    my ($self, $relative_time, $log_id, $pid, $message, $force) = @_;

    # Apply the line filter.
    if (defined $self->{'filter'})
    {
        $message = &{$self->{'filter'}}($relative_time, $log_id, $pid, $message);
    }

    # Format the line.
    my $line = "";
    if ($self->{'is_show_relative_time'})
    {
        $line .= sprintf("%12.6f : ", $relative_time);
    }
    if ($self->{'is_show_log_id'})
    {
        $line .= $log_id . " : ";
    }
    if ($self->{'is_show_process_id'})
    {
        $line .= $pid . " : ";
    }
    $line .= $self->{'indentation'};
    $line .= $message;

    # Print the line to a file or to the console or store it for later use.
    my $fid = $self->{'file'};
    if (defined $fid)
    {
        print $fid ($line);
    }
    if (($force || ! $installer::globals::quiet)
        && $self->{'is_print_to_console'})
    {
        print($line);
    }
    if ($self->{'is_save_lines'})
    {
        push @{$self->{'lines'}}, [$relative_time, $log_id, $pid, $message, $force];
    }
}




=head2 set_filename (Self, $filename)

    When the name of a writable file is given then all future messages will go to that file.
    Output to the console is turned off.
    This method is typically used to tie the language dependent $Lang logger to different log files.

=cut
sub set_filename ($$)
{
    my ($self, $filename) = @_;

    $filename = "" unless defined $filename;
    if ($self->{'filename'} ne $filename)
    {
        if (defined $self->{'file'})
        {
            $self->{'is_print_to_console'} = 1;
            close $self->{'file'};
            $self->{'file'} = undef;
        }

        $self->{'filename'} = $filename;
            
        if ($filename ne "")
        {
            open $self->{'file'}, ">", $self->{'filename'}
            || Die "can not open log file ".$self->{'filename'}." for writing";
            $self->{'is_print_to_console'} = 0;

            # Make all writes synchronous so that we don't loose any messages on an
            # 'abrupt' end.
            my $handle = select $self->{'file'};
            $| = 1;
            select $handle;
        }
    }
}




=head2 set_filter ($self, $filter)
    
    Sets $filter (a function reference) as line filter.  It is applied to each line.
    The filter can extract information from the given message and modify it before it is printed.

=cut
sub set_filter ($$)
{
    my ($self, $filter) = @_;
    $self->{'filter'} = $filter;
}




=head2 add_timestamp ($self, $message)

    Print the given message together with the current (absolute) time.

=cut
sub add_timestamp ($$)
{
	my ($self, $message) = @_;
	
	my $timestring = get_time_string();
    $self->printf("%s\t%s", $message, $timestring);
}



=head2 copy_lines_from ($self, $other)

    Copy saved lines from another logger object.

=cut
sub copy_lines_from ($$)
{
    my ($self, $other) = @_;

    my $is_print_to_console = $self->{'is_print_to_console'};
    my $is_save_lines = $self->{'is_save_lines'};
    my $fid = $self->{'file'};

    foreach my $line (@{$other->{'lines'}})
    {
        $self->process_line(@$line);
    }
}




=head2 set_forward ($self, $other)

    Set a forwarding target.  All future messages are forwarded (copied) to $other.
    A typical use is to tie $Info to $Lang so that all messages sent to $Info are
    printed to the console AND written to the log file.

=cut
sub set_forward ($$)
{
    my ($self, $other) = @_;

    # At the moment at most one forward target is allowed.
    if (defined $other)
    {
        $self->{'forward'} = [$other];
    }
    else
    {
        $self->{'forward'} = [];
    }
}




sub increase_indentation ($)
{
    my ($self) = @_;
    $self->{'indentation'} .= "    ";
}




sub decrease_indentation ($)
{
    my ($self) = @_;
    $self->{'indentation'} = substr($self->{'indentation'}, 4);
}




####################################################
# Including header files into the logfile
####################################################

sub include_header_into_logfile
{
	my ($message) = @_;

	$Lang->print("\n");
	$Lang->print(get_time_string());
	$Lang->print("######################################################\n");
	$Lang->print($message."\n");
	$Lang->print("######################################################\n");
}

####################################################
# Including header files into the logfile
####################################################

sub include_header_into_globallogfile
{
	my ($message) = @_;
	
	$Global->print("\n");
	$Global->print(get_time_string());
	$Global->print("######################################################\n");
	$Global->print($message."\n");
	$Global->print("######################################################\n");
}

####################################################
# Write timestamp into log file
####################################################

sub include_timestamp_into_logfile
{
    Die "deprected";
	my ($message) = @_;
	
	my $infoline;
	my $timestring = get_time_string();
    $Lang->printf("%s\t%s", $message, $timestring);
}

####################################################
# Writing all variables content into the log file
####################################################

sub log_hashref
{
	my ($hashref) = @_;

    $Global->print("\n");
    $Global->print("Logging variable settings:\n");

	my $itemkey;
		
	foreach $itemkey ( keys %{$hashref} )
	{
		my $line = "";
		my $itemvalue = "";
		if ( $hashref->{$itemkey} ) { $itemvalue = $hashref->{$itemkey}; }
        $Global->printf("%s=%s\n", $itemkey, $itemvalue);
	}

    $Global->print("\n");
}

#########################################################
# Including global logging info into global log array
#########################################################

sub globallog
{
	my ($message) = @_;

	my $infoline;

    $Global->print("\n");
    $Global->print(get_time_string());
    $Global->print("################################################################\n");
    $Global->print($message."\n");
    $Global->print("################################################################\n");
}

###############################################################
# For each product (new language) a new log file is created.
# Therefore the global logging has to be saved in this file.
###############################################################

sub copy_globalinfo_into_logfile
{
	for ( my $i = 0; $i <= $#installer::globals::globallogfileinfo; $i++ )
	{
		push(@installer::globals::logfileinfo, $installer::globals::globallogfileinfo[$i]);
	}
}

###############################################################
# For each product (new language) a new log file is created.
# Therefore the global logging has to be saved in this file.
###############################################################

sub debuginfo
{
	my  ( $message ) = @_;
	
	$message = $message . "\n";
	push(@installer::globals::functioncalls, $message);
}

###############################################################
# Saving the debug information.
###############################################################

sub savedebug
{
	my ( $outputdir ) = @_;
	
	installer::files::save_file($outputdir . $installer::globals::debugfilename, \@installer::globals::functioncalls);	
	print_message( "... writing debug file " . $outputdir . $installer::globals::debugfilename . "\n" );
}

###############################################################
# Starting the time
###############################################################

sub starttime
{	
	$installer::globals::starttime = time();
	$StartTime = [gettimeofday()];

    my $localtime = localtime();
}

###############################################################
# Convert time string
###############################################################

sub convert_timestring
{
	my ($secondstring) = @_;

	my $timestring = "";
	
	if ( $secondstring < 60 )	 # less than a minute
	{
		if ( $secondstring < 10 ) { $secondstring = "0" . $secondstring; }
		$timestring = "00\:$secondstring min\.";	
	}
	elsif ( $secondstring < 3600 )
	{
		my $minutes = $secondstring / 60;
		my $seconds = $secondstring % 60;
		if ( $minutes =~ /(\d*)\.\d*/ ) { $minutes = $1; }
		if ( $minutes < 10 ) { $minutes = "0" . $minutes; }
		if ( $seconds < 10 ) { $seconds = "0" . $seconds; }
		$timestring = "$minutes\:$seconds min\.";
	}
	else	# more than one hour
	{
		my $hours = $secondstring / 3600;
		my $secondstring = $secondstring % 3600;
		my $minutes = $secondstring / 60;
		my $seconds = $secondstring % 60;
		if ( $hours =~ /(\d*)\.\d*/ ) { $hours = $1; }
		if ( $minutes =~ /(\d*)\.\d*/ ) { $minutes = $1; }
		if ( $hours < 10 ) { $hours = "0" . $hours; }
		if ( $minutes < 10 ) { $minutes = "0" . $minutes; }
		if ( $seconds < 10 ) { $seconds = "0" . $seconds; }
		$timestring = "$hours\:$minutes\:$seconds hours";		
	}
	
	return $timestring;
}

###############################################################
# Returning time string for logging
###############################################################

sub get_time_string
{
	my $currenttime = time();
	$currenttime = $currenttime - $installer::globals::starttime;
	$currenttime = convert_timestring($currenttime);
	$currenttime = localtime() . " \(" . $currenttime . "\)\n";
	return $currenttime;
}

###############################################################
# Returning the age of a file (in seconds)
###############################################################

sub get_file_age
{
	my ( $filename ) = @_;
	
	my $filetime = (stat($filename))[9];
	my $timediff = time() - $filetime;
	return $timediff;
}

###############################################################
# Stopping the time
###############################################################

sub stoptime
{
    my $localtime = localtime();
    $Info->printf("stopping log at %s\n", $localtime);
}

###############################################################
# Set date string, format: yymmdd
###############################################################

sub set_installation_date
{
	my $datestring = "";

	my @timearray = localtime(time);

	my $day = $timearray[3];
	my $month = $timearray[4] + 1;
	my $year = $timearray[5] - 100;
	
	if ( $year < 10 ) { $year = "0" . $year; } 
	if ( $month < 10 ) { $month = "0" . $month; } 
	if ( $day < 10 ) { $day = "0" . $day; } 

	$datestring = $year . $month . $day;

	return $datestring;
}

###############################################################
# Console output: messages
###############################################################

sub print_message
{
    Die "print_message is deprecated";

    my $message = shift;
    chomp $message;
    my $force = shift || 0;
    print "$message\n" if ( $force || ! $installer::globals::quiet );
    return;
}

sub print_message_without_newline
{
    my $message = shift;
    chomp $message;
    print "$message" if ( ! $installer::globals::quiet );
    return;
}

###############################################################
# Console output: warnings
###############################################################

sub print_warning
{
    my $message = shift;
    chomp $message;
    print STDERR "WARNING: $message";
    return;
}

###############################################################
# Console output: errors
###############################################################

sub print_error
{
    my $message = shift;
    chomp $message;

    PrintError($message);

    print STDERR "\n";
    print STDERR "**************************************************\n";
    print STDERR "ERROR: $message";
    print STDERR "\n";
    print STDERR "**************************************************\n";
    return;
}




sub PrintError ($@)
{
    my ($format, @arguments) = @_;

    $Info->printf("Error: ".$format, @arguments);
}




=head2 PrintStackTrace()
    This is for debugging the print and printf methods of the logger class and their use.
    Therefore we use the Perl print/printf directly and not the logger methods to avoid loops in case of errors.
=cut
sub PrintStackTrace ()
{
    print "Stack Trace:\n";
    my $i = 1;
    while ((my @call_details = (caller($i++))))
    {
        printf("%s:%s in function %s\n", $call_details[1], $call_details[2], $call_details[3]);
    }
}


sub Die ($)
{
    my ($message) = @_;
    PrintStackTrace();
    die $message;
}



1;
