#
#	$Id: Calculator.pm,v 1.40 2004/12/30 02:48:21 kevin Exp $
#
#	Author: Kevin Walsh <kevin@cursor.biz>
#
#	Copyright (c) 2003-2004 Cursor Software Limited.
#	All rights reserved.
#
#	----------------------------------------------------------------------
#
#	Calculator plug-in for the SlimServer.
#
#	Formulas are executed in left-right order, instead of enforcing
#	operator precedence rules, therefore the module will:
#
#	    * Calculate "2+3*4" as 20 (not 14).
#
#	    * Calculate "2+(3*4)" as 14.
#
#	    * Calculate "((9/3)+(2*3))*(1+1*4)" as 72 (not 45).
#
#	    * Calculate "+14+-4" as 10.
#
#	    * Calculate "(50*4)+25%" as 250.
#
#	The above results are what most people would expect when
#	using a pocket calculator, and this module is not intended
#	to be any more sophisticated than that.
#
#	Various other operators are available, such as "square" and
#	"square root".  Use the up/down keys to scroll through the list.
#	The list also includes a "pi" value, and the "N" value, which
#	will be used as the result of your previous calculation (if any).
#
#	Your calculation result and current formula will be saved for
#	one hour (configurable "forget_time" minutes) after its last
#	use.  If you return to the player after that time, the details
#	will be lost.
#	
#	Controls:
#
#	* Use the remote control to enter numbers, as you'd expect.
#	* The up/down buttons scroll through the available character set,
#	  which includes various operators and parentheses.
#	* The left/right buttons allow you to navigate back/forward
#	  through the formula to edit.  If you press left when you are
#	  already on the leftmost character, you will return to the
#	  plug-ins menu.
#	* The play key initiates the calculation of your formula.
#	* The stop key clears the formula you have entered.
#	* The forward key deletes the character at the cursor's position.
#	* The rewind key deletes the character to the left of the cursor.
#
#	If you are in Europe, or another country that uses "," as a
#	decimal point then remember to set the PLUGIN_CALCULATOR_DECIMAL_POINT
#	value at the end of this file.  In that case, you may also need to
#	set the SLIMP3 server's THOUSANDS_SEP key in your strings.txt file.
#
#	This plug-in module requires the Math::BigFloat Perl module to be
#	installed.  The Math::BigFloat module should come with Perl.  A couple
#	of hacks had to be coded to allow Perl versions prior to 5.8.0 to work.
#	This plug-in has been tested with Perl 5.8.0 and 5.6.1.  If you have
#	an ancient Perl version then don't moan about it to me - upgrade.
#	This plug-in module will display an error if the prerequisite
#	Math::BigFloat module is not installed.
#
#	This is my first attempt at writing a calculator for any system,
#	and in any language, so try to not laugh too loud when you look at
#	the source. :-)
#
#	----------------------------------------------------------------------
#
#	This program 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; if not, write to the Free Software
#	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
#	02111-1307 USA
#
package Plugins::Calculator;
use Slim::Utils::Strings qw(string);
use strict;

use vars qw($VERSION);
$VERSION = substr(q$Revision: 1.40 $,10);

my @CHARSET = (
    ' ',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    'N', 'P', '^', 'R', 'S', ')', '(', '%',
    '/', '*', '-', '+', '.',
);
my %CHARMAP = (
    '*' => 'x',			# lval times rval (3*3 = 9)
    '^' => chr(0x9E),		# raise lval to the power of rval (3^3 = 27)
);
my %CUSTOM = (
    '/' => 'divide',		# lval divided by rval (9/3 = 2)
    'P' => 'pi',		# pi (see the $PI variable, below)
    'R' => 'square-root',	# square root of rval (R9 = 3)
    'S' => 'squared',		# lval squared (3S = 9)
);

my %TOKEN = (
    numeric	=> qr/[+-]?(?:\d+\.?\d*|\d*\.?\d+)/,
    op		=> qr/[*\/^SR+-]/,
    token_split	=> qr/[*\/()^SR+-]/,
    parentheses	=> qr/^[()]$/,
    plus_minus	=> qr/^[+-]$/,
    percent	=> qr/%$/,
);

my $PI = 3.141592654;

my %prefs = (
    'forget_time' => {
	order => 0,
	default => 60,
	widget => {
	    validate => \&Slim::Web::Setup::validateInt,
	    validateArgs => [1,1440,1,1440],
	},
    },
    'clear_after' => {
	order => 1,
	default => 1,
	translate => 1,
	widget => {
	    options => {
		'1' => 'YES',
		'0' => 'NO',
	    },
	},
    },
);

my $found_module;	# true if the Math::BigFloat module is present
my %context;		# current context information for each client

my %functions = (
    'left' => sub {
	#
	#	move the cursor one character to the left
	#
	my $client = shift;
	my $ref = $context{$client};

	Slim::Utils::Timers::killTimers($client,\&auto_next);
	delete $ref->{time};

	if (move_cursor($client,$ref,-1)) {
	    #
	    #	exit the module and return to the SLIMP3 menu system
	    #
	    Slim::Buttons::Common::popModeRight($client);
	}
	else {
	    $ref->{lines} = undef;
	    $client->update();
	}
    },
    'right' => sub {
	return undef unless $found_module;

	#
	#	move the cursor one character to the right
	#
	my $client = shift;
	my $ref = $context{$client};

	Slim::Utils::Timers::killTimers($client,\&auto_next);
	delete $ref->{time};

	move_cursor($client,$ref,1);
	$ref->{lines} = undef;
	$client->update();
    },
    'up' => sub {
	return undef unless $found_module;

	#
	#	rotate the character at the corrent position from
	#	the choices in the @CHARSET array.  Rotation is
	#	performed in forward order
	#
	my $client = shift;

	rotate_char($client,1);
	$context{$client}->{lines} = undef;
    },
    'down' => sub {
	return undef unless $found_module;

	#
	#	rotate the character at the corrent position from
	#	the choices in the @CHARSET array.  Rotation is
	#	performed in reverse order
	#
	my $client = shift;

	rotate_char($client,-1);
	$context{$client}->{lines} = undef;
    },
    'jump_rew' => sub {
	return undef unless $found_module;

	#
	#	delete the character to the left of the cursor,
	#	by moving the cursor to the left and shifting all
	#	right-most characters to the left to overwrite the
	#	deleted character
	#
	my $client = shift;
	my $ref = $context{$client};

	Slim::Utils::Timers::killTimers($client,\&auto_next);
	delete $ref->{time};

	if ($ref->{cursor}) {
	    substr($ref->{formula},$ref->{cursor} - 1) = substr($ref->{formula},$ref->{cursor});
	    move_cursor($client,$ref,-1);
	    $ref->{lines} = undef;
	    $client->update();
	}
    },
    'scan_rew' => sub {
	undef;
    },
    'jump_fwd' => sub {
	return undef unless $found_module;

	#
	#	delete the character under the cursor, shifting
	#	all remaining characters to the left
	#
	my $client = shift;
	my $ref = $context{$client};

	Slim::Utils::Timers::killTimers($client,\&auto_next);
	delete $ref->{time};

	my $len = length($ref->{formula});
	if ($len && $ref->{cursor} < $len) {
	    substr($ref->{formula},$ref->{cursor}) = substr($ref->{formula},$ref->{cursor} + 1);
	    $ref->{lines} = undef;
	    $client->update();
	}
    },
    'scan_fwd' => sub {
	undef;
    },
    'stop' => sub {
	return undef unless $found_module;

	#
	#	clear the formula
	#
	my $client = shift;
	my $ref = $context{$client};

	Slim::Utils::Timers::killTimers($client,\&auto_next);
	delete $ref->{time};

	$ref->{formula} = '';
	$ref->{cursor} = 0;
	$ref->{window} = 0;
	$ref->{lines} = undef;
	$client->update();
    },
    'pause' => sub {
	undef;
    },
    'play' => sub {
	return undef unless $found_module;

	#
	#	perform the calculation
	#
	my $client = shift;
	my $ref = $context{$client};

	Slim::Utils::Timers::killTimers($client,\&auto_next);
	delete $ref->{time};

	my @oldlines = Slim::Display::Display::curLines($client);

	#
	#	perform the calculation
	#
	my $result = eval { calculator($ref->{formula},$ref->{result}) };

	if ($@) {
	    my $str = $@;
	    chomp $str;
	    $str =~ s/ at \S+ line .+$//;
	    Slim::Display::Animation::showBriefly($client,$str);
	}
	else {
	    $ref->{result} = $result;
	    $ref->{display_result} = commify($result);

	    load_preferences();

	    if ($prefs{clear_after}->{current}) {
		$ref->{formula} = '';
		$ref->{cursor} = 0;
		$ref->{window} = 0;
	    }

	    #
	    #	push the old lines off the left side of the display
	    #
	    Slim::Display::Animation::pushLeft(
		$client,
		@oldlines,
		Slim::Display::Display::curLines($client),
	    );

	    #
	    #	Fake the last remote control keypress time to make the
	    #	"screen saver" wait a little longer than usual before
	    #	taking over
	    #
	    Slim::Hardware::IR::setLastIRTime(
		$client,
		Time::HiRes::time() + Slim::Utils::Prefs::get("screensavertimeout"),
	    );
	    $ref->{lines} = undef;
	    $client->update();
	}
    },
    'numberScroll' => sub {
	return undef unless $found_module;

	#
	#	handle numeric buttons
	#
	my ($client,$button,$digit) = @_;
	my $ref = $context{$client};

	Slim::Utils::Timers::killTimers($client,\&auto_next);

	if (length($ref->{formula}) == $ref->{cursor} + 1 &&
	    $ref->{time} && $ref->{time} + Slim::Utils::Prefs::get('displaytexttimeout') > Time::HiRes::time()
	) {
	    delete $ref->{time};
	    move_cursor($client,$ref,1);
	}

	if (length($ref->{formula})) {
	    if ($ref->{cursor} > length($ref->{formula})) {
		#
		#	append the number at the end of the formula
		#	because that's where the cursor is
		#
		$ref->{formula} .= $digit;
	    }
	    else {
		#
		#	insert the number into the formula at the
		#	current cursor position
		#
		my $slice = substr($ref->{formula},$ref->{cursor});
		substr($ref->{formula},$ref->{cursor}) = $digit . $slice;
	    }
	    move_cursor($client,$ref,1);
	}
	else {
	    #
	    #	this is the first character to be added to the formula
	    #
	    $ref->{formula} = $digit;
	    $ref->{cursor} = 1;
	}

	$ref->{lines} = undef;
	$client->update();
    },
);

BEGIN {
    eval {
	require Math::BigFloat;
	import Math::BigFloat;
    };
    $found_module = 1 unless $@; 
};

#
#	move_cursor()
#	-------------
#	Move the cursor in the direction indicated.  Move the window
#	as well, if that's what it takes to keep the cursor in view.
#
sub move_cursor
{
    my ($client,$ref,$direction) = @_;
    my $len = length($ref->{formula});
    my $over;

    $ref->{cursor} += $direction;

    if ($direction > 0) {
	#
	#	forward
	#
	my $off = 2;
	if ($ref->{cursor} >= $len - 1) {
	    $off = 1;
	}
	if ($ref->{cursor} > $len) {
	    $ref->{cursor} = $len;
	    $off = 0;
	}
	if (($ref->{cursor} - $ref->{window}) >= ($client->displayWidth() - $off)) {
	    $ref->{window} = $ref->{cursor} - ($client->displayWidth() - $off);
	}
    }
    else {
	#
	#	backward
	#
	if ($ref->{cursor} < 0) {
	    $over = 0 - $ref->{cursor};
	    $ref->{cursor} = 0;
	    $ref->{window} = 0;
	}
	elsif ($ref->{window} && $ref->{window} == $ref->{cursor}) {
	    $ref->{window}--;
	}
    }
    $over;
}

#
#	auto_next()
#	-----------
#	Called by a timer to automatically advance the cursor
#	to the next character if no keys have been pressed after
#	a non-numeric character was entered.
#
sub auto_next
{
    my $client = shift;
    my $ref = $context{$client};

    delete $ref->{time};
    move_cursor($client,$ref,1);
    $client->update();
}

#
#	rotate_char()
#	-------------
#	Select the next character in the @CHARSET array,
#	scrolling in the direction specified.
#
sub rotate_char
{
    my ($client,$direction) = @_;
    my $ref = $context{$client};
    my $index = 0;
    my $slice;
    my $char;

    Slim::Utils::Timers::killTimers($client,\&auto_next);
    delete $ref->{time};

    if ($ref->{cursor} > length($ref->{formula})) {
	#
	#	we will be appending to the end of the formula
	#	because that's where the cursor is
	#
	$slice = $char = ' ';
    }
    else {
	#
	#	we will be changing the value of the existing character
	#	at the current cursor position
	#
	$slice = substr($ref->{formula},$ref->{cursor});
	$char = substr($slice,0,1);
    }

    #
    #	find the selected character in the @CHARSET array so that we
    #	have a starting point to work from
    #
    if (defined($char)) {
	foreach (@CHARSET) {
	    last if $_ eq $char;
	    $index++;
	}
    }

    #
    #	call the SLIMP3 library functon to perform the character scroll
    #
    $index = Slim::Buttons::Common::scroll(
	$client,
	$direction,
	$#CHARSET + 1,
	$index
    );

    #
    #	if we fell off either end of the array then wrap ourselves
    #	back round again
    #
    if ($index) {
	$index = 1 if $index > $#CHARSET;
	$index = $#CHARSET if $index < 1;
    }
    else {
        $index = $direction == 1 ? 1 : $#CHARSET;
    }

    if ($char ne $CHARSET[$index]) {
	if ($ref->{cursor} >= length($ref->{formula})) {
	    #
	    #	append the new character to the end of the array
	    #
	    $ref->{formula} .= $CHARSET[$index];
	}
	else {
	    #
	    #	replace the character at the current cursor position
	    #	with the rotated character
	    #
	    $slice =~ s/^.//;
	    substr($ref->{formula},$ref->{cursor}) = $CHARSET[$index] . $slice;
	}
    }

    #
    #	if we are at the end of the formula then set a timer
    #	to automatically advance the cursor position after a set
    #	time (user preference).  You will have to use this plug-in
    #	to see what I mean
    #
    if (length($ref->{formula}) == $ref->{cursor} + 1) {
	Slim::Utils::Timers::setTimer(
	    $client,
	    Time::HiRes::time() + Slim::Utils::Prefs::get('displaytexttimeout'),
	    \&auto_next,
	);
	$ref->{time} = Time::HiRes::time();
    }

    #
    #	update the display and our work is done
    #
    $client->update();
    undef;
}

#
#	lines()
#	-------
#	Create and return the two-line display.  Also fake the IR time so
#	that the "screen saver" doesn't take over.
#
sub lines
{
    my $client = shift;

    unless ($found_module) {
	return(
	    string('PLUGIN_CALCULATOR_NO_MODULES'),
	    string('PLUGIN_CALCULATOR_CANNOT_CONTINUE'),
	);
    }

    my $ref = $context{$client};
    $ref->{last_use} = Time::HiRes::time();

    #
    #	we "cache" the display until a key is pressed and only
    #	re-write the display when there is something new
    #
    unless (ref($ref->{lines}) eq 'ARRAY') {
	my @lines;

	if (length($ref->{display_result})) {
	    $lines[0] = string('PLUGIN_CALCULATOR_RESULT') . ": $ref->{display_result}";
	}
	else {
	    $lines[0] = string('PLUGIN_CALCULATOR_ENTER_FORMULA');
	}
	
	my $cursor_shown;
	my $long_line;
	my $first_char_len;
	my $charpos = 0;

	#
	#	mark the cursor position in the formula
	#
	if (defined($ref->{formula})) {
	    my $line = $ref->{formula};
	    if ($ref->{window}) {
		$line = substr($ref->{formula},$ref->{window},$client->displayWidth() + 1);
	    }
	    if (length($line) > $client->displayWidth()) {
		$line = substr($line,0,$client->displayWidth());
		$long_line = 1;
	    }
	    foreach (split('',$line)) {
		if (($ref->{cursor} - $ref->{window}) == $charpos++) {
		    $lines[1] .= Slim::Hardware::VFD::symbol('cursorpos');
		    $cursor_shown = 1;
		}
		my $char = special_char($client,$_);
		$lines[1] .= $char;
		$first_char_len ||= length($char);
	    }
	}
	unless ($cursor_shown) {
	    $lines[1] .= Slim::Hardware::VFD::symbol('cursorpos');
	    $long_line = 0;
	}

	#
	#	use the internationalised decimal point
	#
	my $dp = string('PLUGIN_CALCULATOR_DECIMAL_POINT');
	$lines[1] =~ s/\./$dp/g if $dp ne '.';

	#
	#	display the "more left" and/or "more right" markers, as appropriate
	#
	substr($lines[1],0,$first_char_len,Slim::Hardware::VFD::symbol('moreleft')) if $ref->{window};
	push(@lines,undef,Slim::Hardware::VFD::symbol('moreright')) if $long_line;
	$ref->{lines} = [ @lines ];
    }
    @{$ref->{lines}};
}

#
#	special_char()
#	--------------
#	Replace the given character with its %CHARMAP equivilent
#	(if any).  Don't use custom characters in double-size mode.
#
sub special_char
{
    my ($client,$char) = @_;

    if ($CHARMAP{$char}) {
	$char = $CHARMAP{$char};
    }
    elsif ($CUSTOM{$char} && !Slim::Utils::Prefs::clientGet($client,'doublesize')) {
	$char = Slim::Hardware::VFD::symbol($CUSTOM{$char});
    }
    $char;
}

#
#	calculator()
#	------------
#	Take a simple or complex formula and return a result.
#
sub calculator
{
    my ($formula,$result) = @_;
    $result ||= 0;

    #
    #	we allow constructs such as 2P (2 * pi), so 10/2P is parsed
    #	as 10/(2*P) = 1.59154943071114, and NOT as (10/2)*P which would be
    #	15.70796327.  2N works in the same way.  NP, PN, NN and PP are
    #	not allowed at this time - use constructs such as N*P instead.
    #
    #	As another example, 2+2PS is parsed as 2+((2*P)S) = 41.478417614667.
    #	(Remember, "S" means "squared".)
    #
    #	2+2PS is NOT parsed as 2+(2*(PS)) = 21.739208807332.
    #	2+2PS is NOT parsed as ((2+2)*P)S = 157.913670458668.
    #	2+2PS is NOT parsed using any of the other methods you can think of.
    #
    #	If you think the above is incorrect then please email the author.
    #
    $formula =~ s/($TOKEN{op}?)($TOKEN{numeric})([NP])/$1($2*$3)/g;

    #
    #	replace the P symbol, in the provided formula, with the "pi" value
    #	(see the $PI global variable) and replace the N symbol with the
    #	result of the previous calculation (or zero).
    #
    $formula =~ s/\s+//g;
    $formula =~ s/P/$PI/g;
    $formula =~ s/N/$result/g;

    unless (length($formula)) {
	die string('PLUGIN_CALCULATOR_NO_FORMULA');
    }

    #
    #	split the input formula into tokens and verify that the
    #	parentheses (if any) are correctly matched
    #
    my @tokens = calc_subformulas($formula,1);

    #
    #	once the parentheses (if any) have been parsed we will be left
    #	with a simple formula to calculate
    #
    run_calc(@tokens);
}

#
#	calc_subformulas()
#	------------------
#	Transforms a formula into tokens and calculates any subformulas
#	(formulas in parentheses).  Returns the remaining simple formula
#	in tokenised format.
#
sub calc_subformulas
{
    my ($formula,$verify) = @_;

    #
    #	split the input formula into tokens
    #
    my @tokens = grep(!/^$/,split(/($TOKEN{token_split})/,$formula));

    #
    #	verify that the parentheses (if any) are correctly matched
    #
    if ($verify) {
	my $count = 0;
	foreach (@tokens) {
	    if ($_ eq '(') {
		$count++;
	    }
	    elsif ($_ eq ')' && --$count < 0) {
		die string('PLUGIN_CALCULATOR_PARENTHESES_MISMATCH');
	    }
	}
	if ($count) {
	    die string('PLUGIN_CALCULATOR_PARENTHESES_MISMATCH');
	}
    }

    #
    #	loop until there are no more parentheses
    #
    while (1) {
	my $start;
	my $curr = 0;

	#
	#	look for ending braces while marking the position of
	#	any starting braces.  the last seen starting brace will
	#	be the match for our ending brace
	#
	foreach (@tokens) {
	    if ($_ eq '(') {
		$start = $curr;
	    }
	    elsif ($_ eq ')') {
		my $length = $curr - $start;

		if ($length == 1) {
		    #
		    #	handle empty braces () gracefully, rather than
		    #	raising an error - just throw them away
		    #
		    splice(@tokens,0,2);
		}
		else {
		    #
		    #	splice out the tokens from the starting brace
		    #	to the ending brace and replace those tokens
		    #	with the calculation result
		    #
		    splice(@tokens,$start,1,run_calc(splice(@tokens,$start,$length)));
		}

		#
		#	we have changed the array so restart the loop from
		#	the begining
		#
		last;
	    }
	    $curr++;
	}
	#
	#	break out of the "forever" loop if no starting brace
	#	was found in the scan loop
	#
	last unless defined $start;
    }
    @tokens;
}

#
#	run_calc()
#	----------
#	Take a simple (no parentheses) (sub)formula and return a result.
#	This algorithm handes operators on a "left to right" basis,
#	rather than using any "operator precedence rules.  The exception
#	to this is the "squared" operator, so "1+-3S" calculates as 10
#	and not 4.  I.e. "1+(-3S)" instead of "(1+-3)S".
#
#	The following operators are recognised:
#
#	oper	Name		example
#	-------	---------------	----------------
#	+	Plus		3 + 3 = 6
#	-	Minus		3 - 3 = 0
#	*	Times		3 * 3 = 9
#	/	Divide		9 / 3 = 2
#	^	Power		3 ^ 3 = 27
#	S	Square		3 S   = 9
#	R	Square Root	  R 9 = 3
#
sub run_calc
{
    my @tokens = @_;
    my $result = 0;

    return $result unless scalar(@tokens);

    my $formula = join('',@tokens);
    $formula =~ s/^\(//;
    $formula =~ s/\)$//;
    0 while $formula =~ s/--|\+\+//g;

    #
    #	the following is required to turn "1+-3S" into "1+(-3S)" so that
    #	the "-3S" can be calculated, simpifing the formula to "1+9", which
    #	will be calculated into 10 later in this subroutine.
    #
    unless ($formula =~ m/^$TOKEN{numeric}S$/) {
	if ($formula =~ s/($TOKEN{op}?)($TOKEN{numeric}S)/$1($2)/g) {
	    @tokens = calc_subformulas($formula,0);
	}
	else {
	    @tokens = grep(!/^$/,split(/($TOKEN{token_split})/,$formula));
	}
    }

    #
    #	start the loop by reading a lval
    #
    while (1) {
	my $lval = shift(@tokens);
	last unless defined($lval);
	next if $lval =~ $TOKEN{parentheses};
	my $op;

	if ($lval eq 'R') {
	    #
	    #	"square root" operator - deal with this after we
	    #	have read the rval
	    #
	    $op = $lval;
	}
	else {
	    #
	    #	if we read a lval modifier then read and append the
	    #	actual lval 
	    #
	    if ($lval =~ $TOKEN{plus_minus}) {
		$lval .= shift(@tokens);
	    }
	    unless ($lval =~ m/^$TOKEN{numeric}$/) {
		die string('PLUGIN_CALCULATOR_INPUT_ERROR_LVAL');
	    }
	    $lval = new Math::BigFloat($lval);

	    #
	    #	the next token should be an operator
	    #
	    $op = shift(@tokens);

	    #
	    #	if no operator was read then the lval must be the final result
	    #
	    unless ($op) {
		$result = $lval;
		last;
	    }

	    #
	    #	verify that the operator is valid
	    #
	    unless ($op =~ $TOKEN{op}) {
		die string('PLUGIN_CALCULATOR_INPUT_ERROR_OP');
	    }
	}

	if ($op eq 'S') {
	    #
	    #	"squared" operator - no need for a rval
	    #
	    $result = $lval * $lval;
	}
	else {
	    #
	    #	the next token should be the rval
	    #
	    my $rval = shift(@tokens);

	    #
	    #	if we read a rval modifier then read and append the
	    #	actual rval 
	    #
	    if ($rval && $rval =~ $TOKEN{plus_minus}) {
		$rval .= shift(@tokens);
	    }
	    unless ($rval =~ m/^$TOKEN{numeric}%?$/) {
		die string('PLUGIN_CALCULATOR_INPUT_ERROR_RVAL');
	    }

	    #
	    #	do the actual calculation based upon the operator and
	    #	whether the rval is a numeric value or a percentage
	    #
	    if ($op eq '+') {
		if ($rval =~ s/$TOKEN{percent}//) {
		    $rval = ($lval / 100) * (new Math::BigFloat($rval));
		}
		$result = $lval + $rval;
	    }
	    elsif ($op eq '-') {
		if ($rval =~ s/$TOKEN{percent}//) {
		    $rval = ($lval / 100) * (new Math::BigFloat($rval));
		}
		$result = $lval - $rval;
	    }
	    elsif ($op eq '*') {
		$lval /= 100 if $rval =~ s/$TOKEN{percent}//;
		$result = $lval * $rval;
	    }
	    elsif ($op eq '/') {
		if ($rval =~ s/$TOKEN{percent}//) {
		    $rval = new Math::BigFloat($rval);
		    $rval /= 100
		}
		if ($rval == 0) {
		    die string('PLUGIN_CALCULATOR_ZERO_DIVIDE');
		}
		$result = $lval / $rval;
	    }
	    elsif ($op eq '^') {
		if ($] >= 5.8) {
		    $result = $lval->bpow($rval);
		}
		else {
		    #
		    #	try to handle old Perl versions
		    #
		    $result = new Math::BigFloat("$lval" ** $rval);
		}
	    }
	    elsif ($op eq 'R') {
		if ($rval < 0) {
		    die string('PLUGIN_CALCULATOR_IMAGINARY');
		}
		if ($] >= 5.8) {
		    $rval = new Math::BigFloat($rval);
		    $result = $rval->bsqrt();
		}
		else {
		    #
		    #	try to handle old Perl versions
		    #
		    $result = sqrt($rval);
		}
	    }
	}

	#
	#	put the result at the top of our tokens stack so
	#	that it will be read as a lval on the next iteration
	#
	unshift(@tokens,"$result");
    }
    $result;
}

#
#	commify()
#	---------
#	UK/US convert 1000000.0001 into 1,000,000.0001
#	EURO  convert 1000000.0001 into 1.000.000,0001
#
#	Uses the SLIMP3 server's THOUSANDS_SEP i18n key and this module's
#	PLUGIN_CALCULATOR_DECIMAL_POINT i18n key.  The SLIMP3 server doesn't
#	have a PLUGIN_CALCULATOR_DECIMAL_POINT for us to use.
#
sub commify
{
    my $num = shift;

    $num =~ s/\./_/;
    my $sep = string('THOUSANDS_SEP');
    0 while $num =~ s/^(-?\d+)(\d{3})(\.?)/$1$sep$2$3/;
    $num =~ s/_/string('PLUGIN_CALCULATOR_DECIMAL_POINT')/e;
    $num;
}

#
#	load_custom_chars()
#	-------------------
#	Creates all of the custom characters we use in this plug-in
#
sub load_custom_chars
{
    Slim::Hardware::VFD::setCustomChar('moreleft',(
	0b00001,
	0b00011,
	0b00111,
	0b01111,
	0b00111,
	0b00011,
	0b00001,
	0,
    ));
    Slim::Hardware::VFD::setCustomChar('moreright',(
	0b10000,
	0b11000,
	0b11100,
	0b11110,
	0b11100,
	0b11000,
	0b10000,
	0,
    ));
    Slim::Hardware::VFD::setCustomChar('squared',(
	0b01100,
	0b10010,
	0b00100,
	0b01000,
	0b11110,
	0b00000,
	0b00000,
	0,
    ));
    Slim::Hardware::VFD::setCustomChar('square-root',(
	0b00111,
	0b00100,
	0b00100,
	0b00100,
	0b10100,
	0b01100,
	0b00100,
	0,
    ));
    Slim::Hardware::VFD::setCustomChar('divide',(
	0b00000,
	0b00100,
	0b00000,
	0b11111,
	0b00000,
	0b00100,
	0b00000,
	0,
    ));
    Slim::Hardware::VFD::setCustomChar('pi',(
	0b00000,
	0b00000,
	0b00001,
	0b01110,
	0b11010,
	0b01010,
	0b01010,
	0,
    ));
}

#
#	load_preferences()
#	------------------
#	Load the current preferences or set defaults
#
sub load_preferences
{
    while (my ($key,$val) = each %prefs) {
	my $name = __PACKAGE__;
	$name =~ s/^.*:://;
	$key = 'plugin_' . lc($name) . "_$key";

	if (Slim::Utils::Prefs::isDefined($key)) {
	    $val->{current} = Slim::Utils::Prefs::get($key);
	}
	else {
	    $val->{current} = $val->{default};
	    Slim::Utils::Prefs::set($key,$val->{default});
	}
    }
}

sub setupGroup
{
    my $name = __PACKAGE__;
    $name =~ s/^.*:://;
    $name = 'plugin_' . lc($name);

    load_preferences();

    my @order;
    push(@order,"${name}_$_") for (sort {$prefs{$a}->{order} <=> $prefs{$b}->{order}} keys %prefs);

    my $version = ' (' . string("${name}_VERSION") . ')';
    $version =~ s/%s/$VERSION/;
    $version =~ s/\s+\)/)/;

    my %group = (
	PrefOrder => \@order,
	GroupHead => string("${name}_MODULE_NAME") . $version,
	GroupDesc => string("${name}_MODULE_DESCRIPTION"),
	GroupLine => 1,
	GroupSub => 1,
	Suppress_PrefSub => 1,
	Suppress_PrefLine => 1,
    );

    my %widgets;
    while (my ($key,$val) = each %prefs) {
	$widgets{"${name}_$key"} = $val->{widget};

	if ($val->{translate} && exists($val->{widget}->{options})) {
	    $_ = string("${name}_$_") for (values %{$val->{widget}->{options}});
	    delete $val->{translate};
	}
    }

    return (\%group,\%widgets);
}

sub setMode
{
    my $client = shift;
    my $time = Time::HiRes::time();

    load_preferences();

    #
    #	reset the calculator memory if this module hasn't been
    #	used in the last "forget_time" minutes
    #
    if (!$context{$client} || ($context{$client}->{last_use} + ($prefs{forget_time}->{current} * 60)) < $time) {
	$context{$client} = {
	    formula => '',
	    display_result => '',
	    result => 0,
	    cursor => 0,
	    window => 0,
	    time => $time,
	    last_use => $time,
	};
    }
    if (!defined(Slim::Buttons::Common::param($client,'noScroll'))) {
	Slim::Buttons::Common::param($client,'noScroll',1)
    }

    load_custom_chars();

    $client->lines(\&lines);
    $client->update();
}

sub getFunctions
{
    \%functions;
}

sub getDisplayName
{
    my $name = 'PLUGIN_CALCULATOR_MODULE_NAME';

    $::VERSION =~ /^(\d+)/;
    return ($1 >= 6) ? $name : string($name);
}

sub strings
{
    local $/ = undef;
    <DATA>;
}

1;

__DATA__

PLUGIN_CALCULATOR_MODULE_NAME
	EN	Calculator

PLUGIN_CALCULATOR_MODULE_DESCRIPTION
	EN	Calculator

PLUGIN_CALCULATOR_VERSION
	EN	Version %s

PLUGIN_CALCULATOR_ENTER_FORMULA
	EN	Enter a formula and press PLAY

PLUGIN_CALCULATOR_RESULT
	EN	Result

PLUGIN_CALCULATOR_ERROR
	EN	error

PLUGIN_CALCULATOR_NO_MODULES
	EN	Math::BigFloat module not found

PLUGIN_CALCULATOR_CANNOT_CONTINUE
	EN	Cannot continue

PLUGIN_CALCULATOR_NO_FORMULA
	EN	No calculation specified

PLUGIN_CALCULATOR_PARENTHESES_MISMATCH
	EN	Parentheses Mismatch

PLUGIN_CALCULATOR_INPUT_ERROR_LVAL
	EN	Input error (lval)

PLUGIN_CALCULATOR_INPUT_ERROR_OP
	EN	Input error (operator)

PLUGIN_CALCULATOR_INPUT_ERROR_RVAL
	EN	Input error (rval)

PLUGIN_CALCULATOR_ZERO_DIVIDE
	EN	Cannot divide by zero

PLUGIN_CALCULATOR_IMAGINARY
	EN	Imaginary numbers are not supported

PLUGIN_CALCULATOR_DECIMAL_POINT
	DE	,
	EN	.
	ES	,
	FR	,
	NL	,

PLUGIN_CALCULATOR_YES
	EN	Yes

PLUGIN_CALCULATOR_NO
	EN	No

SETUP_PLUGIN_CALCULATOR_FORGET_TIME
	EN	Forget the previous formula/result after this many minutes

SETUP_PLUGIN_CALCULATOR_CLEAR_AFTER
	EN	Clear the current formula after a calculation has been performed


