package Log::Deep::Read;

# Created on: 2008-11-11 19:37:26
# Create by:  ivan
# $Id$
# $Revision$, $HeadURL$, $Date$
# $Revision$, $Source$, $Date$

use strict;
use warnings;
use version;
use Carp;
use Data::Dump::Streamer;
use English qw/ -no_match_vars /;
use Readonly;
use Time::HiRes qw/sleep/;
use base qw/Exporter/;
use Log::Deep::File;
use Log::Deep::Line;

our $VERSION     = version->new('0.3.3');
our @EXPORT_OK   = qw//;
our %EXPORT_TAGS = ();

Readonly my @colours => qw/
	black
	red
	green
	yellow
	blue
	magenta
	cyan
	white
/;
Readonly my %excludes => map { $_ => 1 } qw/cyangreen greencyan bluemagenta magentablue cyanblue bluecyan greenblue bluegreen/;

sub new {
	my $caller = shift;
	my $class  = ref $caller ? ref $caller : $caller;
	my %param  = @_;
	my $self   = \%param;

	bless $self, $class;

	$self->{short_break}  ||= 2;
	$self->{short_lines}  ||= 2;
	$self->{long_break}   ||= 5;
	$self->{long_lines}   ||= 5;
	$self->{foreground}   ||= 0;
	$self->{background}   ||= 0;
	$self->{sessions_max} ||= 100;
	$self->{sleep_time}   ||= 0.5;

	$self->{dump} = Data::Dump::Streamer->new()->Indent(4);

	$self->{line} = {
		verbose => $self->{verbose},
		display => $self->{display},
		show    => $self->{show},
		dump    => $self->{dump},
	};

	delete $self->{show};
	delete $self->{display};

	return $self;
}

sub read_files {
	my ($self, @files) = @_;
	my $once = 1;
	my $read = 5;
	my %files;

	for my $file_glob (@files) {
		my (@files, $warn);
		{
			local $SIG{__WARN__} = sub { $warn = $_ };
			@files = glob $file_glob;
		}

		next if !@files || $warn;

		for my $file (sort @files) {
			$files{$file} ||= Log::Deep::File->new($file);
		}
	}
	die "No files to read!" if !keys %files;

	# record the current number of files watched
	$self->{file_count} = keys %files;

	# loop for ever if we are following the log file other wise we loop
	# only one time.
	while ( $self->{follow} || $once == 1 ) {
		# increment $once to keep track of the itteration number
		$once++;
		my $lines = 0;
		if ($read < 1) {
			$read = 1;
		}

		# itterate over each file found/specified
		FILE:
		for my $file (keys %files) {
			next FILE if !$file || !$files{$file};

			# process the file for any (new) log lines
			$lines += $self->read_file($files{$file});
			if ( !$files{$file}->{handle} ) {
				# delete the file if there was nothing to read
				delete $files{$file};
			}
		}

		# exit the loop if there was no data to be read
		last if !%files;

		# turn off tracking last lines/sessions
		$self->{number} = 0;
		$self->{'session-number'} = 0;

		# every 1,000 itterations check if there are any new files matching
		# any passed globs in, allows not having to re-run every time a new
		# log file is created.
		if ( $once % 1_000 || !%files ) {
			for my $file ( map { sort glob $_ } @files ) {
				# check that the file still exists
				next if !-e $file;

				# add the new file only if it doesn't already exist
				$files{$file} ||= { name => $file };
			}

			# record the current number of files watched
			$self->{file_count} = keys %files;
		}
		elsif ( $self->{follow} ) {
			$read += $lines ? 1 : -1;
			my $multiplier =
				  $lines ? 1
				: !$read ? 5
				:          2;
			# sleep every time we have cycled through all the files to
			# reduce CPU load.
			sleep $self->{sleep_time} * $multiplier;
		}

		# exit the loop if all log files have been deleted
		last if !%files;
	}

	return;
}

sub read_file {
	my ($self, $file) = @_;
	my @lines;
	my %sessions;
	my $line_count = 0;

	confess "read_file called with out a file object!" if !ref $file;

	# read the rest of the lines in the file
	LINE:
	while (my $line = $file->line) {

		chomp $line;
		next if !$line;
		$line_count++;

		# parse the line
		my $line = Log::Deep::Line->new( { %{ $self->{line} } }, $line, $file );

		# skip lines that don't have a session id
		next LINE if !$line->id;

		# set the colour for the line
		$line->colour( $self->session_colour($line->id) );

		# skip displaying the line if it should be filtered out
		next LINE if !$line->show();

		# get the display text for the line
		my $line_text = eval { $line->text() . join '', $line->data() };

		# check that there were no errors
		if ($EVAL_ERROR) {
			# warn the errors
			warn $EVAL_ERROR;

			# go on to the next line
			next LINE;
		}

		# check if we are displaying lines/sessions from the end of the file
		if ($self->{number}) {
			# add the line to end of the lines
			push @lines, $line_text;
			if (@lines > 10 * $self->{number}) {
				@lines = @lines[@lines - $self->{number} - 1 .. @lines - 1];
			}
		}
		elsif ( $self->{'session-number'} ) {
			# get the session id
			my $session = $line->id;

			# add the session to the list of session if we have not already come accross it
			push @lines, $session if !$sessions{$session};

			# add the line to the session's lines
			$sessions{$session} ||= '';
			$sessions{$session}  .= $line_text;
		}
		else {
			# show any file change info
			$self->changed_file($file);

			# print out the log line
			print $line_text;
		}
	}

	# check if we have any stored lines to print
	if (@lines) {
		# print any file change info
		$self->changed_file($file);

		# check which format we are using
		if ($self->{number}) {
			my $first_line = @lines - $self->{number} <= 0 ? 0 : @lines - $self->{number};
			print @lines[ $first_line .. (@lines - 1) ];
		}
		elsif ( $self->{'session-number'} ) {
			# work out what to do
			my $first_line = @lines - $self->{'session-number'} <= 0 ? 0 : @lines - $self->{'session-number'};
			for my $i ( $first_line .. (@lines - 1) ) {
				print $sessions{$lines[$i]};
			}
		}
	}

	$file->reset;

	return $file->{handle};
}

sub read {
	my ($self) = @_;
	my @lines;
	my %sessions;
	my $file = $self->{file};

	if (!ref $file) {
		$file = $self->{file} = Log::Deep::File->new($file);
	}

	my $line = $file->line;

	if ( !$line ) {
		$file->reset;
		return;
	}

	chomp $line;
	return $self->read() if !$line;

	# parse the line
	$line = Log::Deep::Line->new( { %{ $self->{line} } }, $line, $file );
	$line->colour( $self->session_colour($line->id) );

	# skip displaying the line if it should be filtered out
	return $self->read if !$line->show();

	return $line;
}

sub changed_file {
	my ( $self, $file ) = @_;

	# check if we have printed some lines from this file before
	if ( !$self->{last_print_file} || "$self->{last_print_file}" ne "$file" ) {
		if ( $self->{file_count} > 1 ) {
			# print out the change in file (same format as tail)
			print "\n==> $file <==\n";
		}

		# set this file as the last printed file
		$self->{last_print_file} = $file;
	}

	return;
}

sub session_colour {
	my ($self, $session_id) = @_;

	confess "No session id supplied!" if !$session_id;

	# return the cached session colour if we have one
	return $self->{sessions}{$session_id}{colour} if $self->{sessions}{$session_id};

	# set the next colour, cycle through backgrounds for each foreground
	if ( $self->{background} + 1 < @colours ) {
		$self->{background}++;
	}
	elsif ( $self->{foreground} + 1 < @colours ) {
		$self->{background} = 0;
		$self->{foreground}++;
	}
	else {
		$self->{background} = 0;
		$self->{foreground} = 0;
	}

	# check that the colour is not an excluded colour or that background and
	# foreground colours are not the same.
	if (
		$excludes{ $colours[$self->{foreground}] . $colours[$self->{background}] }
		|| $self->{foreground} == $self->{background}
	) {
		# we cannot use this colour so get the next colour in the sequence
		return $self->session_colour($session_id);
	}

	my $colour = "$colours[$self->{foreground}] on_$colours[$self->{background}]";

	# remove old sessions
	# TODO need to get this code working
	if ( 0 && keys %{ $self->{sessions} } > $self->{sessions_max} ) {
		# get max session with the current colour
		my $time = 0;
		for my $session ( keys %{ $self->{sessions} } ) {
			$time = $self->{session}{$session}{time} if $time < $self->{session}{$session}{time} && $self->{session}{$session}{colour} eq $colour;
		}

		# now remove sessions older than $time
		for my $session ( keys %{ $self->{sessions} } ) {
			delete $self->{session}{$session} if $self->{session}{$session}{time} <= $time;
		}
	}

	# cache the session info
	$self->{sessions}{$session_id}{time}   = time;
	$self->{sessions}{$session_id}{colour} = $colour;

	# return the colour
	return $colour;
}


1;

__END__

for file in files
	for line in file
		do stuff

'
for file in files
	while line = file->next
		do stuff

=head1 NAME

Log::Deep::Read - Read and prettily display log files generated by Log::Deep

=head1 VERSION

This documentation refers to Log::Deep::Read version 0.3.3.

=head1 SYNOPSIS

   use Log::Deep::Read;

   # Brief but working code example(s) here showing the most common usage(s)
   # This section will be as far as many users bother reading, so make it as
   # educational and exemplary as possible.

=head1 DESCRIPTION

Provides the functionality to read and analyse log files written by Log::Deep

=head1 SUBROUTINES/METHODS

=head3 C<new ( %args )>

Arg: C<mono> - bool - Display out put in mono ie don't use colour

Arg: C<follow> - bool - Follow the log files for any new additions

Arg: C<number> - int - The number of lines to display from the end of the log file

Arg: C<session-number> - int - The number of sessions to display from the end of the file

Arg: C<display> - hash ref - keys are the keys of the log's data to display
if a true value (or hide if false). The values can also be a comma separated
list (or an array reference) to turn on displaying of sub keys of the field
(requires the filed to be a hash)

Arg: C<filter> - hash ref - specifies the keys to filter (not yet implemented)

Arg: C<verbose> - bool - Turn on showing more verbose log messages.

Arg: C<short_break> - bool - Turn on showing a short break when some time has
passed between displaying log lines (when follow is true)

Arg: C<short_lines> - int - the number lines to print out when a short time
threshold has been exceeded.

Arg: C<long_break> - bool - Turn on showing a short break when a longer time has
passed between displaying log lines (when follow is true)

Arg: C<long_lines> - int - the number lines to print out when a longer time
threshold has been exceeded.

Arg: C<sessions_max> - int - The maximum number of sessions to keep before
starting to remove older sessions

Return: Log::Deep::Read - A new Log::Deep::Read object

Description: Sets up a Log::Deep::Read object to play with.

=head3 C<read_files ( @files )>

Param: C<@files> - List of strings - A list of files to be read

Description: Reads and parses all the log files specified

=head3 C<read_file ( $file, $fh )>

Param: C<$file> - string - The name of the file to read

Param: C<$fh> - File Handle - A (possibly) previously open file handle to
$file.

Return: File Handle - The opened file handle

Description: Reads through the lines of $file

=head3 C<changed_file ( $file )>

Param: C<$file> - hash ref - The file currently being examined

Description: Prints a message to the user that the current log file has
changed to a new file. The format is the same as for the tail command.

=head3 C<read ()>

Return: Log::Deep::Line - The next line read or undef if no more lines in file

Description: Just parses the next line in the log file (skips blank lines and
lines that are filtered out)

=head3 C<session_colour ( $session_id )>

Params: The session id that is to be coloured

Description: Colours session based on their ID's

=head1 DIAGNOSTICS

=head1 CONFIGURATION AND ENVIRONMENT

=head1 DEPENDENCIES

=head1 INCOMPATIBILITIES

=head1 BUGS AND LIMITATIONS

There are no known bugs in this module.

Please report problems to Ivan Wills (ivan.wills@gmail.com).

Patches are welcome.

=head1 AUTHOR

Ivan Wills - (ivan.wills@gmail.com)

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2009 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW 2077).
All rights reserved.

This module is free software; you can redistribute it and/or modify it under
the same terms as Perl itself. See L<perlartistic>.  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.

=cut