package ProgressMonitor::Stringify::Fields::Bar;

use warnings;
use strict;

use ProgressMonitor::Exceptions;
require ProgressMonitor::Stringify::Fields::AbstractDynamicField if 0;

# Attributes:
#	innerWidth
#		The computed width of the bar itself
#	idleTravellerIndex
#		The location to write the 'traveller' when total is unknown
#	idleSpinnerIndex
#		The next spinner in the sequence to use when resolution is too small
#	allEmpty
#		A precomputed bar that is empty
#	lastFiller
#		The last string to fill the bar (to detect resolution issues)
#
use classes
  extends  => 'ProgressMonitor::Stringify::Fields::AbstractDynamicField',
  new      => 'new',
  attrs_pr => ['innerWidth', 'idleTravellerIndex', 'idleSpinnerIndex', 'allEmpty', 'lastFiller', 'inited'],
  throws   => ['X::ProgressMonitor::InsufficientWidth'],
  ;

sub new
{
	my $class = shift;
	my $cfg   = shift;

	my $self = $class->SUPER::_new($cfg, $CLASS);

	$cfg = $self->_get_cfg;

	# compute the width, taking into account all the user choices
	#
	my $minWidth = $cfg->get_minWidth;
	my $width    =
	  length($cfg->get_leftWall) + $cfg->get_idleLeftSpace + length($cfg->get_idleTraveller) + $cfg->get_idleRightSpace +
	  length($cfg->get_rightWall);
	$width = $minWidth if $minWidth > $width;
	X::ProgressMonitor::InsufficientWidth->throw($width) if ($width > $cfg->get_maxWidth);

	$self->_set_width($width);

	# init the instance vars not affected by width
	#
	$self->{$ATTR_idleTravellerIndex} = 0;
	$self->{$ATTR_idleSpinnerIndex}   = 0;
	$self->{$ATTR_lastFiller}         = $cfg->get_fillCharacter;
	$self->{$ATTR_inited}             = 0;

	return $self;
}

sub widthChange
{
	my $self = shift;

	my $cfg = $self->_get_cfg;

	# recompute some vars
	#
	my $innerWidth = $self->get_width - length($cfg->get_leftWall) - length($cfg->get_rightWall);
	$self->{$ATTR_innerWidth} = $innerWidth;
	$self->{$ATTR_allEmpty}   = $cfg->get_emptyCharacter x $innerWidth;

	return;
}

sub render
{
	my $self       = shift;
	my $state      = shift;
	my $tick       = shift;
	my $totalTicks = shift;
	my $clean      = shift;

	my $cfg = $self->_get_cfg;

	my $iw  = $self->{$ATTR_innerWidth};
	my $bar = $self->{$ATTR_allEmpty};
	if (defined($totalTicks))
	{
		# the total is known, so compute how much filler we need to indicate the ratio
		#
		my $ratio = defined($totalTicks) && $totalTicks > 0 ? ($tick / $totalTicks) : 0;
		my $filler = $cfg->get_fillCharacter x ($ratio * $iw);
		substr($bar, 0, length($filler), $filler);

		# unless we're requested to be 'clean' and in case the filler is the
		# same as last time (and we're not full), twirl the spinner
		#
		if (!$clean && $ratio < 1 && $filler eq $self->{$ATTR_lastFiller})
		{
			my $lf  = length($filler);
			my $seq = $cfg->get_idleSpinnerSequence;
			substr($bar, ($lf == 0 ? 0 : $lf - 1), 1, $seq->[$self->{$ATTR_idleSpinnerIndex}++ % @$seq]);
		}
		$self->{$ATTR_lastFiller} = $filler;
	}
	else
	{
		if (!$self->{$ATTR_inited})
		{
			# first call, do nothing
			#
			$self->{$ATTR_inited} = 1;
		}
		else
		{
			# total is unknown (or we're still in prep mode)
			# run the traveller in round robin in the bar
			#
			my $begin = $self->{$ATTR_idleTravellerIndex}++ % $iw;
			my $it    = $cfg->get_idleTraveller;
			for (0 .. (length($it) - 1))
			{
				substr($bar, $begin, 1, substr($it, $_, 1));
				$begin = 0 if ++$begin >= $iw;
			}
		}
	}

	return $cfg->get_leftWall . $bar . $cfg->get_rightWall;
}

###

package ProgressMonitor::Stringify::Fields::BarConfiguration;

use strict;
use warnings;

use classes
  extends => 'ProgressMonitor::Stringify::Fields::AbstractDynamicFieldConfiguration',
  attrs   => [
			'emptyCharacter', 'fillCharacter', 'leftWall',       'rightWall',
			'idleTraveller',  'idleLeftSpace', 'idleRightSpace', 'idleSpinnerSequence'
		   ],
  ;

sub defaultAttributeValues
{
	my $self = shift;

	return {
			%{$self->SUPER::defaultAttributeValues()},
			emptyCharacter      => '.',
			fillCharacter       => '*',
			leftWall            => '[',
			rightWall           => ']',
			idleTraveller       => '==>',
			idleLeftSpace       => 1,
			idleRightSpace      => 1,
			idleSpinnerSequence => ['-', '\\', '|', '/'],
		   };
}

sub checkAttributeValues
{
	my $self = shift;

	$self->SUPER::checkAttributeValues;

	X::Usage->throw("length of leftWall can not be less than 0")   if length($self->get_leftWall) < 0;
	X::Usage->throw("length of rightWall can not be less than 0")  if length($self->get_rightWall) < 0;
	X::Usage->throw("length of emptyCharacter must have length 1") if length($self->get_emptyCharacter) != 1;
	X::Usage->throw("length of fillCharacter must have length 1")  if length($self->get_fillCharacter) != 1;
	X::Usage->throw("idleLeftSpace can not be less than 0")        if $self->get_idleLeftSpace < 0;
	X::Usage->throw("idleRightSpace can not be less than 0")       if $self->get_idleRightSpace < 0;
	my $seq = $self->get_idleSpinnerSequence;
	X::Usage->throw("idleSpinnerSequence must be an array") unless ref($seq) eq 'ARRAY';
	for (@$seq)
	{
		X::Usage->throw("all idleSpinnerSequence elements must have length of 1") if length($_) != 1;
	}

	return;
}

###########################

=head1 NAME

ProgressMonitor::Stringify::Field::Bar - a field implementation that renders progress
as a bar.

=head1 SYNOPSIS

  # call someTask and give it a monitor to call us back
  #
  my $bar = ProgressMonitor::Stringify::Fields::Bar->new;
  someTask(ProgressMonitor::Stringify::ToStream->new({fields => [ $bar ]});

=head1 DESCRIPTION

This is a dynamic field representing progress as a bar typically of this form:
"[###....]" etc. It will consume as much room as it can get unless limited by maxWidth.

It is very configurable in terms of what it prints. By default it will also do 
useful things to indicate 'idle' progress, i.e. either no ticks advanced, but still
tick is called, or just 'unknown' work.

Inherits from ProgressMonitor::Stringify::Fields::AbstractDynamicField.

=head1 METHODS

=over 2

=item new( $hashRef )

Configuration data:

=over 2

=item emptyCharacter (default => '.')

The character that should be used to indicate an empty location in the bar.

=item fillCharacter (default => '#')

The character that should be used to indicate a full location in the bar.

=item leftWall (default => '[')

The string that should be used to indicate the left wall of the bar. This can
be set to an empty string if you don't want a wall.

=item rightWall (default => ']')

The string that should be used to indicate the right wall of the bar. This can
be set to an empty string if you don't want a wall.

=item idleTraveller (default => '==>')

The string that should be used as a moving piece in order to indicate progress
for totals that are unknown.

=item idleLeftSpace (default => 1)

Amount of characters that should be allocated to the left of the idleTraveller.
This is necessary to insure that the idleTraveller has at least some room to travel
in.  

=item idleLeftRight (default => 1)

Amount of characters that should be allocated to the right of the idleTraveller.
This is necessary to insure that the idleTraveller has at least some room to travel
in.

=item idleSpinnerSequence (default => ['-', '\\', '|', '/'])

This should be a reference to a list of characters that should be used in sequence
for ticks that doesn't advance the bar, but we still want to show that something
is happening. If you do not wish this to happen at all, set to a single element list
with the fillCharacter.

=back

=back

=head1 AUTHOR

Kenneth Olwing, C<< <knth at cpan.org> >>

=head1 BUGS

I wouldn't be surprised! If you can come up with a minimal test that shows the
problem I might be able to take a look. Even better, send me a patch.

Please report any bugs or feature requests to
C<bug-progressmonitor at rt.cpan.org>, or through the web interface at
L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=ProgressMonitor>.
I will be notified, and then you'll automatically be notified of progress on
your bug as I make changes.

=head1 SUPPORT

You can find general documentation for this module with the perldoc command:

    perldoc ProgressMonitor

=head1 ACKNOWLEDGEMENTS

Thanks to my family. I'm deeply grateful for you!

=head1 COPYRIGHT & LICENSE

Copyright 2006,2007 Kenneth Olwing, all rights reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut

1;    # End of ProgressMonitor::Stringify::Fields::Bar
