#!/usr/bin/perl
#
# This tests the new PDL constructor with a string argument.
# There are three goals from the new functionality: (1) to allow
# MATLAB to use familiar syntax to create arrays, (2) to allow
# cut-n-paste of PDL print output as input for scripts and programs,
# and (3) to allow easy ways to construct nan and inf values in piddles.
#

use Test::More tests => 113;
use strict;
use warnings;
use Config;

#############################
# Loading and ISA tests - 2 #
#############################

BEGIN {
   # if we've got this far in the tests then
   # we can probably assume PDL::LiteF works!
   #
   use_ok( "PDL::LiteF" );
}

isa_ok( pdl("[1,2]"), "PDL", qq{pdl("[1,2]") returns a piddle} );

###################
# Basic Tests - 8 #
###################

ok( all(pdl([1,2])==pdl("[1,2]")), qq{pdl(ARRAY REF) equals pdl("ARRAY REF")});

my $compare = pdl([
	[1, 0, 8],
	[6, 3, 5],
	[3, 0, 5],
	[2, 4, 2]
]);

my $test_string = <<EOPDL;
   [
     [1, 0, 8],
     [6, 3, 5],
     [3, 0, 5],
     [2, 4, 2],
   ]
EOPDL

my $t1 = pdl $test_string;
ok(all(approx($t1, $compare)), "properly interprets good PDL input string");

# See what happens when we remove the end commas
$test_string =~ s/\],/]/g;

my $t2 = pdl $test_string;
ok(all(approx($t2, $compare)), "properly interprets good PDL input string sans ending commas");

my $t3 = pdl '[1, 0, 8; 6, 3, 5; 3, 0, 5; 2, 4, 2]';
ok(all(approx($t3, $compare)), "properly handles semicolons");

my $t4 = pdl "$compare";
ok(all(approx($t4, $compare)), "properly interprets good PDL output string");

my $expected = pdl(1.2e3);
my $got = pdl q[1.2e3];
is($got, $expected, "Correctly interprets [1.2e3]");

$expected = pdl(1.2e3, 4, 5.6e-7);
$got = pdl q[1.2e3 4 5.6e-7];
ok(all($got == $expected), "Correclty interprets [1.2e3 4 5.6e-7]");

$expected = pdl(1.2e3, 4, 5.e-7);
$got = pdl q[1.2e3 4 5.e-7];
ok(all($got == $expected), "Correclty interprets [1.2e3 4 5.e-7]");


###########################
# Signs and operators - 6 #
###########################

# This functionality does not with the parsed (as opposed to eval'd) method
# for building the pdl-from-string. I'm commenting out the tests that will
# fail.

# Now some more interesting tests
my $t5 = pdl "[1 -4]";
$compare = pdl [1, -4];
ok(all(approx($t5, $compare)), "properly identifies negative numbers with white-space separation");

my $t6 = pdl "[1 - 4]";
$compare = pdl [1,-4];
ok(all(approx($t6, $compare)), "properly affixes negation operator to right operand");

ok(all(approx(pdl("[1 - .4]"), pdl([1,-0.4]))), "properly handles decimals");

my $t8 = pdl <<EOPDL;
[
	[1,2,3; 4,-5,6]
	[7 +8, 8 + 9; 10, - .11, 12e3]
]
EOPDL

$compare = pdl([[[1,2,3], [4,-5,6]],[[7,8,8,9],[10,-.11,12e3]]]);
ok(all(approx($t8, $compare)), "properly handles all sorts of stuff!");

$compare = pdl [1,2,-5];
my $t9 = pdl '[1  + 2 - 5]';
ok(all(approx($t9, $compare)), "Another operator check for pdl_from_string");

$compare = pdl [1, 2, -5];
my $t10 = pdl '[1  +2 -5]';
ok(all(approx($t10, $compare)), "Yet another operator check for pdl_from_string");

#######################################
# Semicolons as column seperators - 2 #
#######################################

$compare = pdl [[1], [2], [3]];
my $t11 = pdl '[1;2;3]';
ok(all(approx($t11, $compare)), "column check");

$compare = pdl([[1,2,3],[4,5,6]]);
my $t12 = pdl q[1 2 3; 4 5 6];
ok(all(approx($t12, $compare)), "implicit bracketing check");

##################################
# Implicit bracketing checks - 9 #
##################################

$compare = pdl([1,2,3,4]);
my $t13 = pdl q[1 2 3 4];
my $t14 = pdl q[1,2,3,4];
my $t15 = pdl '[1 2 3 4]';
my $t16 = pdl '[1,2,3,4]';

ok(all(approx($t13, $compare)), "Double-check implicit bracketing - no brackets");
ok(all(approx($t14, $compare)), "Double-check implicit bracketing - no brackets and commas");
ok(all(approx($t15, $compare)), "Double-check implicit bracketing - brackets");
ok(all(approx($t16, $compare)), "Double-check implicit bracketing - brackets and commas");

# check dimensions of tests
ok($t13->ndims == 1, "Implicit bracketing gets proper number of dimensions - no brackets, no commas");
ok($t14->ndims == 1, "Implicit bracketing gets proper number of dimensions - no brackets, commas");
ok($t15->ndims == 1, "Implicit bracketing gets proper number of dimensions - brackets, no commas");
ok($t16->ndims == 1, "Implicit bracketing gets proper number of dimensions - brackets and commas");

$expected = pdl [];
$got = pdl q[];
ok(all($got == $expected), 'Blank strings are interpreted as empty arrays');
# This generates an annoying warning, and the piddle should be Empty anyway
#$expected = pdl [];
$got = pdl q[[]];
ok(all($got == $expected), 'Empty bracket is correctly interpreted');

#############################
# Bad, inf, nan checks - 15 #
#############################

my $bad_values = pdl q[nan inf -inf bad];

# Bad value testing depends on whether nan and inf are represented as bad
# values
require PDL::Config;

TODO: {
	# conditional TODO
	local $TODO = 'ActivePerl and/or perls built using MS compilers might fail this test'
		if($ActivePerl::VERSION || $Config{cc} eq 'cl');

	if ($PDL::Config{BADVAL_USENAN}) {
		ok($bad_values->isbad->at(0), 'sets nan to bad')
			or diag("Zeroeth bad value should be bad but it describes itself as "
		. $bad_values->at(0));
	}
	else {
		SKIP: {
			skip "broken for PDL_Index", 1;
			ok($bad_values->at(0) != $bad_values->at(0), 'properly handles nan')
				or diag("Zeroeth bad value should be nan but it describes itself as "
				. $bad_values->at(0));
		}
	}
}

# inf test: inf == inf but inf * 0 != 0
ok((	$PDL::Config{BADVAL_USENAN} and $bad_values->isbad->at(1)
		or  $bad_values->at(1) == $bad_values->at(1)
		and $bad_values->at(1) * 0.0 != 0.0), 'properly handles inf')
	or diag("First bad value should be inf but it describes itself as " . $bad_values->at(1));
# inf test: -inf == -1 * inf
ok((	$PDL::Config{BADVAL_USENAN} and $bad_values->isbad->at(2)
		or  $bad_values->at(2) == $bad_values->at(2)
		and $bad_values->at(2) * 0.0 != 0.0), 'properly handles -inf')
	or diag("Second bad value should be -inf but it describes itself as " . $bad_values->at(2));
SKIP: {
	skip "because BADVAL_USENAN makes -inf and inf both bad, "
		. "so checking signs is silly", 1 if $PDL::Config{BADVAL_USENAN};
	ok($bad_values->at(2) == -$bad_values->at(1), "negative inf is numerically equal to -inf");
}
# bad test
SKIP: {
skip "Bad values disabled", 1 if !$PDL::Config{WITH_BADVAL};
ok($bad_values->isbad->at(3), 'properly handles bad values')
	or diag("Third bad value should be BAD but it describes itself as " . $bad_values->slice(3));
}

my $infty = pdl 'inf';
my $min_inf = pdl '-inf';
my $nan = pdl 'nan';

my $nan2 = $^O =~ /MSWin32/i && !$ActivePerl::VERSION && $Config{cc} ne 'cl' ? pdl (-((-1) ** 0.5))
                             : pdl '-nan';

my $bad = pdl 'bad';

TODO: {
	# conditional TODO
	local $TODO = 'ActivePerl and/or perls built using MS compilers might fail this test'
		if($ActivePerl::VERSION || $Config{cc} eq 'cl');

	ok((	$PDL::Config{BADVAL_USENAN} and $infty->isbad
		or $infty == $infty and $infty * 0.0 != 0.0), "pdl 'inf' works by itself")
		or diag("pdl 'inf' gave me $infty");
	ok((	$PDL::Config{BADVAL_USENAN} and $min_inf->isbad
		or $min_inf == $min_inf and $min_inf * 0.0 != 0.0), "pdl '-inf' works by itself")
		or diag("pdl '-inf' gave me $min_inf");
}

SKIP: {
	skip "because BADVAL_USENAN makes -inf and inf both bad, "
		. "so checking signs is silly", 1 if $PDL::Config{BADVAL_USENAN};
	ok($min_inf == -$infty, "pdl '-inf' == -pdl 'inf'");
}


TODO: {
	local $TODO = 'Sign of Nan depends on platform, still some loose ends';

	# conditional TODO for a different reason
	local $TODO = 'Cygwin perl and/or ActivePerl might fail these tests'
		if($ActivePerl::VERSION || $^O =~ /cygwin/i);

	ok((   $PDL::Config{BADVAL_USENAN} and $nan->isbad
	       or $nan != $nan), "pdl 'nan' works by itself")
	       or diag("pdl 'nan' gave me $nan");
	ok((   $PDL::Config{BADVAL_USENAN} and $nan2->isbad
	       or $nan2 != $nan2), "pdl '-nan' works by itself")
	       or diag("pdl '-nan' gave me $nan2");

	# On MS Windows, nan is -1.#IND and -nan is 1.#QNAN. IOW, nan has
	# a leading minus sign, and -nan is not signed.
	if($^O =~ /MSWin32/i) {
		ok((   $PDL::Config{BADVAL_USENAN} and $nan->isbad
		       or $nan =~ /-/), "pdl 'nan' has a negative sign (MS Windows only)")
		       or diag("pdl 'nan' gave me $nan");
		ok((   $PDL::Config{BADVAL_USENAN} and $nan2->isbad
		       or $nan2 !~ /-/), "pdl '-nan' doesn't have a negative sign (MS Windows only)")
		       or diag("pdl -'nan' gave me $nan2");
	}
	else {
		ok((   $PDL::Config{BADVAL_USENAN} and $nan->isbad
		       or $nan !~ /-/), "pdl 'nan' has a positive sign")
		       or diag("pdl 'nan' gave me $nan");
		ok((   $PDL::Config{BADVAL_USENAN} and $nan2->isbad
		       or $nan2 =~ /-/), "pdl '-nan' has a negative sign")
		       or diag("pdl '-nan' gave me $nan2");
	}
}

SKIP: {
skip "Bad values disabled", 1 if !$PDL::Config{WITH_BADVAL};
ok($bad->isbad, "pdl 'bad' works by itself")
	or diag("pdl 'bad' gave me $bad");
}

# Checks for windows strings:
$infty = pdl q[1.#INF];
$nan = pdl q[-1.#IND];

TODO: {
	# conditional TODO
	local $TODO = 'ActivePerl and/or perls built using MS compilers might fail this test'
		if($ActivePerl::VERSION || $Config{cc} eq 'cl');

	ok((	$PDL::Config{BADVAL_USENAN} and $infty->isbad
		or $infty == $infty and $infty * 0 != 0), "pdl '1.#INF' works")
		or diag("pdl '1.#INF' gave me $infty");

	ok(($PDL::Config{BADVAL_USENAN} and $nan->isbad
		or $nan != $nan), "pdl '-1.#IND' works")
		or diag("pdl '-1.#IND' gave me $nan");
}

########################
# Pi and e checks - 10 #
########################

$expected = pdl(1)->exp;
# using approx() here since PDL only has support for double data
# so there will be differences in the least significant places for
# perls compiled with uselongdouble
#
$got = pdl q[e];
ok(approx($got, $expected, 1e-15), 'q[e] returns exp(1)')
	or diag("Got $got");
# using approx() here since PDL only has support for double data
# so there will be differences in the least significant places for
# perls compiled with uselongdouble
#
$got = pdl q[E];
ok(approx($got, $expected, 1e-15), 'q[E] returns exp(1)')
	or diag("Got $got");
$expected = pdl(1, exp(1));
$got = pdl q[1 e];
ok(all($got == $expected), 'q[1 e] returns [1 exp(1)]')
	or diag("Got $got");
$got = pdl q[1 E];
ok(all($got == $expected), 'q[1 E] returns [1 exp(1)]')
	or diag("Got $got");
$expected = pdl(exp(1), 1);
$got = pdl q[e 1];
ok(all($got == $expected), 'q[e 1] returns [exp(1) 1]')
	or diag("Got $got");
$got = pdl q[E 1];
ok(all($got == $expected), 'q[E 1] returns [exp(1) 1]')
	or diag("Got $got");
$expected = pdl(1, exp(1), 2);
$got = pdl q[1 e 2];
ok(all($got == $expected), 'q[1 e 2] returns [1 exp(1) 2]')
	or diag("Got $got");
$got = pdl q[1 E 2];
ok(all($got == $expected), 'q[1 E 2] returns [1 exp(1) 2]')
	or diag("Got $got");

# Already checked all the permutations of e, so just make sure that it
# properly substitutes pi
$expected = pdl(1, 4 * atan2(1,1));
$got = pdl q[1 pi];
ok(all($got == $expected), 'q[1 pi] returns [1 4*atan2(1,1)]')
	or diag("Got $got");
$got = pdl q[1 PI];
ok(all($got == $expected), 'q[1 PI] returns [1 4*atan2(1,1)]')
	or diag("Got $got");

########################
# Security checks - 10 #
########################

# Check croaking on arbitrary bare-words:
eval {pdl q[1 foobar 2]};
isnt($@, '', 'croaks on arbitrary string input');
eval {pdl q[$a $b $c]};
isnt($@, '', 'croaks with non-interpolated strings');

# Install a function that knows if it's been executed.
{
	no warnings 'redefine';
	my $e_was_run = 0;
	sub PDL::Core::e { $e_was_run++ }

	my $to_check = q[1 e 2];
	sub PDL::Core::e { $e_was_run++ }
	eval {pdl $to_check};
	is($e_was_run, 0, "Does not execute local function e in [$to_check]");
	$e_was_run = 0;

	$to_check = q[1 +e 2];
	sub PDL::Core::e { $e_was_run++ }
	eval {pdl $to_check};
	is($e_was_run, 0, "Does not execute local function e in [$to_check]");
	$e_was_run = 0;

	$to_check = q[1 e+ 2];
	sub PDL::Core::e { $e_was_run++ }
	eval {pdl $to_check};
	is($e_was_run, 0, "Does not execute local function e in [$to_check]");
	$e_was_run = 0;

	$to_check = q[1e 2];
	sub PDL::Core::e { $e_was_run++ }
	eval {pdl $to_check};
	is($e_was_run, 0, "Does not execute local function e in [$to_check]");
	$e_was_run = 0;

	$to_check = q[1e+ 2];
	sub PDL::Core::e { $e_was_run++ }
	eval {pdl $to_check};
	is($e_was_run, 0, "Does not execute local function e in [$to_check]");
	$e_was_run = 0;

	$to_check = q[1+e 2];
	sub PDL::Core::e { $e_was_run++ }
	eval {pdl $to_check};
	is($e_was_run, 0, "Does not execute local function e in [$to_check]");
	$e_was_run = 0;

	$to_check = q[1+e+ 2];
	sub PDL::Core::e { $e_was_run++ }
	eval {pdl $to_check};
	is($e_was_run, 0, "Does not execute local function e in [$to_check]");
	$e_was_run = 0;

	$to_check = q[1 e123 2];
	sub PDL::Core::e123 { $e_was_run++ }
	eval {pdl $to_check};
	is($e_was_run, 0, "Does not execute local function e123 in [$to_check]");
	$e_was_run = 0;

}

###############################
# Useful croaking output - 36 #
###############################

eval{ pdl q[1 l 3] };
isnt($@, '', 'Croaks when invalid character is specified');
like($@, qr/found disallowed character\(s\) 'l'/, 'Gives meaningful explanation of problem');
eval{ pdl q[1 po 3] };
isnt($@, '', 'Croaks when invalid characters are specified');
like($@, qr/found disallowed character\(s\) 'po'/, 'Gives meaningful explanation of problem');

# checks for croaking behavior for consecutive signs like +-2:
eval{ pdl q[1 +-2 3] };
isnt($@, '', 'Croaks when it finds consecutive signs');
like($@, qr/found a \w+ sign/, 'Gives meaningful explanation of problem');
eval{ pdl q[1 -+2 3] };
isnt($@, '', 'Croaks when it finds consecutive signs');
like($@, qr/found a \w+ sign/, 'Gives meaningful explanation of problem');

# 'larger word' croak checks (36)
foreach my $special (qw(bad inf pi)) {
	foreach my $append (qw(2 e l)) {
		eval "pdl q[1 $special$append 2]";
		isnt($@, '', "Croaks when it finds $special$append");
		like($@, qr/larger word/, 'Gives meaningful explanation of problem');
		eval "pdl q[1 $append$special 2]";
		isnt($@, '', "Croaks when it finds $append$special");
		like($@, qr/larger word/, 'Gives meaningful explanation of problem');
	}
}

# e croaks (6)
my $special = 'e';
foreach my $append (qw(2 e l)) {
		eval "pdl q[1 $special$append 2]";
		isnt($@, '', "Croaks when it finds $special$append");
		eval "pdl q[1 $append$special 2]";
		isnt($@, '', "Croaks when it finds $append$special");
}

# Basic 2D array
# pdl> p $x = pdl q[ [ 1, 2, 3 ], [ 4, 5, 6 ] ];
# pdl> p $x = pdl q[ 1 2 3 ; 4 5 6 ]
# pdl> p $x = pdl '[ [ 1, 2, 3 ], [ 4, 5, 6 ] ]';
#
# [
#  [1 2 3]
#  [4 5 6]
# ]

# Basic 1D array
# pdl> p $y = pdl [ 1, 2, 3, 4, 5, 6 ]
# pdl> p $y = pdl q[ 1 2 3 4 5 6 ]
# pdl> p $y = pdl q[1,2,3,4,5,6]
# [1 2 3 4 5 6]

# 1D array with signs
# pdl> p $c = pdl [ 7, -2, +5 ]
# pdl> p $c = pdl q[ 7 -2 +5 ]
# pdl> p $c = pdl q[ 7, -2, +5 ]
# [7 -2 5]

# 1D array with mixed ops and signs
# pdl> p $d = pdl [ 7 - 2, +5 ]
# pdl> p $d = pdl q[ 7 - 2 +5 ]
# [5 5]

# ...another
# pdl> p $d = pdl [ 7, -2 + 5 ]
# pdl> p $d = pdl q[ 7 -2 + 5 ]
# [7 3]

# 1D array with ops, not signs
# pdl> p $d = pdl [ 7 - 2 + 5 ]
# pdl> p $d = pdl q[ 7 - 2 + 5 ]
# 10

# A [2,3,4] shape piddle
# pdl> p $d = pdl [ [ [0, 1], [4, 0], [0, 3] ],
#                   [ [2, 0], [4, 0], [4, 1] ],
#                   [ [0, 1], [3, 2], [1, 4] ],
#                   [ [1, 2], [2, 2], [2, 1] ] ];
#
# [
#  [
#   [0 1]
#   [4 0]
#   [0 3]
#  ]
#  [
#   [2 0]
#   [4 0]
#   [4 1]
#  ]
#  [
#   [0 1]
#   [3 2]
#   [1 4]
#  ]
#  [
#   [1 2]
#   [2 2]
#   [2 1]
#  ]
# ]
#
# ...the same, just different formatting...
#
# [
#  [ [0 1] [4 0] [0 3] ]
#  [ [2 0] [4 0] [4 1] ]
#  [ [0 1] [3 2] [1 4] ]
#  [ [1 2] [2 2] [2 1] ]
# ]

# A 3x3 2D array
# pdl> p pdl [ [1, 2, 3], [2, 1, 0], [2, 2, 1] ];
# pdl> p $e = pdl q[ [ 1 2 3 ] ; [ 2 1 0 ] ; [ 2 2 1 ] ];
# pdl> p pdl q[  1 2 3 ; 2 1 0 ; 2 2 1 ]  # this should be the same
#
# [
#  [1 2 3]
#  [2 1 0]
#  [2 2 1]
# ]
