/**
* @license Apache-2.0
*
* Copyright (c) 2018 The Stdlib Authors.
*
* Licensed 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.
*/

'use strict';

// MODULES //

var tape = require( 'tape' );
var PINF = require( '@stdlib/constants/float64/pinf' );
var NINF = require( '@stdlib/constants/float64/ninf' );
var PI = require( '@stdlib/constants/float64/pi' );
var EPS = require( '@stdlib/constants/float64/eps' );
var randu = require( '@stdlib/random/base/randu' );
var round = require( './../../../../base/special/round' );
var pow = require( './../../../../base/special/pow' );
var abs = require( './../../../../base/special/abs' );
var isnan = require( './../../../../base/assert/is-nan' );
var isNegativeZero = require( './../../../../base/assert/is-negative-zero' );
var isPositiveZero = require( './../../../../base/assert/is-positive-zero' );
var Float64Array = require( '@stdlib/array/float64' );
var cfloorn = require( './../lib' );


// TESTS //

tape( 'main export is a function', function test( t ) {
	t.ok( true, __filename );
	t.strictEqual( typeof cfloorn, 'function', 'main export is a function' );
	t.end();
});

tape( 'the function returns a `NaN` if provided a `NaN`', function test( t ) {
	var v;

	v = cfloorn( NaN, NaN, 0 );
	t.strictEqual( isnan( v[ 0 ] ), true, 'returns expected value' );
	t.strictEqual( isnan( v[ 1 ] ), true, 'returns expected value' );

	v = cfloorn( NaN, NaN, NaN );
	t.strictEqual( isnan( v[ 0 ] ), true, 'returns expected value' );
	t.strictEqual( isnan( v[ 1 ] ), true, 'returns expected value' );

	v = cfloorn( NaN, NaN, NaN );
	t.strictEqual( isnan( v[ 0 ] ), true, 'returns expected value' );
	t.strictEqual( isnan( v[ 1 ] ), true, 'returns expected value' );

	t.end();
});

tape( 'the function returns `NaNs` if provided `n = +-infinity`', function test( t ) {
	var v;

	v = cfloorn( 3.14, 3.14, PINF );
	t.strictEqual( isnan( v[ 0 ] ), true, 'returns expected value' );
	t.strictEqual( isnan( v[ 1 ] ), true, 'returns expected value' );

	v = cfloorn( 3.14, 3.14, NINF );
	t.strictEqual( isnan( v[ 0 ] ), true, 'returns expected value' );
	t.strictEqual( isnan( v[ 1 ] ), true, 'returns expected value' );

	t.end();
});

tape( 'the function returns `-0` if provided `-0`', function test( t ) {
	var v;

	v = cfloorn( -0.0, -0.0, 0 );
	t.strictEqual( isNegativeZero( v[ 0 ] ), true, 'returns expected value' );
	t.strictEqual( isNegativeZero( v[ 1 ] ), true, 'returns expected value' );

	v = cfloorn( -0.0, -0.0, -2 );
	t.strictEqual( isNegativeZero( v[ 0 ] ), true, 'returns expected value' );
	t.strictEqual( isNegativeZero( v[ 1 ] ), true, 'returns expected value' );

	v = cfloorn( -0.0, -0.0, 2 );
	t.strictEqual( isNegativeZero( v[ 0 ] ), true, 'returns expected value' );
	t.strictEqual( isNegativeZero( v[ 1 ] ), true, 'returns expected value' );

	t.end();
});

tape( 'the function returns `+0` if provided `+0`', function test( t ) {
	var v;

	v = cfloorn( +0.0, +0.0, 0 );
	t.strictEqual( isPositiveZero( v[ 0 ] ), true, 'returns expected value' );
	t.strictEqual( isPositiveZero( v[ 1 ] ), true, 'returns expected value' );

	v = cfloorn( +0.0, +0.0, -2 );
	t.strictEqual( isPositiveZero( v[ 0 ] ), true, 'returns expected value' );
	t.strictEqual( isPositiveZero( v[ 1 ] ), true, 'returns expected value' );

	v = cfloorn( +0.0, +0.0, 2 );
	t.strictEqual( isPositiveZero( v[ 0 ] ), true, 'returns expected value' );
	t.strictEqual( isPositiveZero( v[ 1 ] ), true, 'returns expected value' );

	t.end();
});

tape( 'the function returns `+infinity` if provided `+infinity`', function test( t ) {
	var v = cfloorn( PINF, PINF, 5 );
	t.strictEqual( v[ 0 ], PINF, 'returns expected value' );
	t.strictEqual( v[ 1 ], PINF, 'returns expected value' );
	t.end();
});

tape( 'the function returns `-infinity` if provided `-infinity`', function test( t ) {
	var v = cfloorn( NINF, NINF, -3 );
	t.strictEqual( v[ 0 ], NINF, 'returns expected value' );
	t.strictEqual( v[ 1 ], NINF, 'returns expected value' );
	t.end();
});

tape( 'the function supports rounding real and imaginary components to a desired number of decimals', function test( t ) {
	t.deepEqual( cfloorn( -PI, PI, -2 ), [ -3.15, 3.14 ], 'returns expected value' );
	t.deepEqual( cfloorn( PI, -PI, -2 ), [ 3.14, -3.15 ], 'returns expected value' );
	t.deepEqual( cfloorn( 9.99999, 9.9999, -2 ), [ 9.99, 9.99 ], 'returns expected value' );
	t.deepEqual( cfloorn( -9.99999, -9.99999, -2 ), [ -10.0, -10.0 ], 'returns expected value' );
	t.deepEqual( cfloorn( 0.0, 0.0, 2 ), [ 0.0, 0.0 ], 'returns expected value' );
	t.deepEqual( cfloorn( -12368.0, 12368.0, -3 ), [ -12368.0, 12368.0 ], 'returns expected value' );
	t.end();
});

tape( 'rounding components to a desired number of decimals can result in unexpected behavior', function test( t ) {
	var x = -0.2 - 0.1; // => -0.30000000000000004
	t.deepEqual( cfloorn( x, x, -16 ), [ -0.3000000000000001, -0.3000000000000001 ], 'returns -0.3000000000000001 and not -0.3' );
	t.end();
});

tape( 'the function supports rounding components to a desired number of digits', function test( t ) {
	var v;

	t.deepEqual( cfloorn( -PI, PI, 4 ), [ -10000.0, 0.0 ], 'returns expected value' );
	t.deepEqual( cfloorn( -12368.0, 12368.0, 3 ), [ -13000.0, 12000.0 ], 'returns expected value' );
	t.deepEqual( cfloorn( 12363.0, -12363.0, 1 ), [ 12360.0, -12370.0 ], 'returns expected value' );

	v = cfloorn( PI, -PI, 3 );
	t.strictEqual( isPositiveZero( v[ 0 ] ), true, 'returns expected value' );
	t.strictEqual( v[ 1 ], -1000.0, 'returns expected value' );

	t.end();
});

tape( 'the function returns the components unchanged if provided an `n` which is less than the minimum decimal exponential (-324)', function test( t ) {
	var exp;
	var n;
	var x;
	var v;
	var i;
	for ( i = 0; i < 100; i++ ) {
		exp = round( randu()*616.0 ) - 308;
		x = (1.0+randu()) * pow( 10.0, exp );
		n = -(round( randu()*1000.0 ) + 325);
		v = cfloorn( x, x, n );
		t.deepEqual( v, [ x, x ], 'returns components when provided re='+x+', im='+x+', n='+n+'.' );
	}
	t.end();
});

tape( 'if a component is too large a double to have decimals and `n < 0`, the component is returned unchanged', function test( t ) {
	var sign;
	var exp;
	var x;
	var n;
	var v;
	var i;
	for ( i = 0; i < 100; i++ ) {
		sign = ( randu()<0.5 ) ? -1.0 : 1.0;
		exp = 54 + round( randu()*254.0 );
		x = sign * (1.0+randu()) * pow( 10.0, exp );
		n = -( round( randu()*324.0) );
		v = cfloorn( x, x, n );
		t.deepEqual( v, [ x, x ], ' returns components when provided re='+x+', im='+x+', n='+n+'.' );
	}
	t.end();
});

tape( 'if `n > 308` and a component is less than zero, the function returns `-infinity`', function test( t ) {
	var exp;
	var x;
	var n;
	var v;
	var i;
	for ( i = 0; i < 100; i++ ) {
		exp = round( randu()*307.0 );
		x = -(1.0+randu()) * pow( 10.0, exp );
		n = round( randu()*100.0 ) + 309;
		v = cfloorn( x, x, n );
		t.deepEqual( v, [ NINF, NINF ], ' returns -infinity when provided re='+x+', im='+x+', n='+n+'.' );
	}
	t.end();
});

tape( 'if `n > 308` and a component is greater than or equal to zero, the function returns `+0` (sign preserving)', function test( t ) {
	var exp;
	var x;
	var n;
	var v;
	var i;
	for ( i = 0; i < 100; i++ ) {
		exp = round( randu()*307.0 );
		x = (1.0+randu()) * pow( 10.0, exp );
		n = round( randu()*100.0 ) + 309;
		v = cfloorn( x, x, n );
		t.strictEqual( isPositiveZero( v[ 0 ] ), true, ' returns +0 when provided re='+x+', n='+n+'.' );
		t.strictEqual( isPositiveZero( v[ 1 ] ), true, ' returns +0 when provided im='+x+', n='+n+'.' );
	}
	t.end();
});

tape( 'the function supports rounding very small numbers (including subnormals)', function test( t ) {
	var expected;
	var delta;
	var tol;
	var x;
	var n;
	var v;
	var i;

	x = 3.1468234343023397 * pow( 10.0, -308 );

	n = [];
	for ( i = -308; i > -325; i-- ) {
		n.push( i );
	}
	expected = [
		3e-308,
		3.1e-308,
		3.14e-308,
		3.146e-308,
		3.1468e-308,
		3.14682e-308,
		3.146823e-308,
		3.1468234e-308,
		3.14682343e-308,
		3.146823434e-308,
		3.1468234343e-308,
		3.14682343430e-308,
		3.146823434302e-308,
		3.1468234343023e-308,
		3.14682343430234e-308, // 3.1468234343023397e-308 * 1e308 = 3.14682343430234
		3.146823434302340e-308,  // 3.1468234343023397e-308 * 1e308 = 3.14682343430234
		3.1468234343023397e-308
	];

	for ( i = 0; i < n.length; i++ ) {
		v = cfloorn( x, x, n[i] );
		if ( v[0] === expected[i] ) {
			t.deepEqual( v, [ expected[i], expected[i] ], 'returns '+expected[i]+' when provided re='+x+', im='+x+', and n='+n[i]+'.' );
		} else {
			delta = abs( v[0] - expected[i] );
			tol = EPS * abs( expected[i] );
			t.strictEqual( delta <= tol, true, 're: '+x+'. n: '+n[i]+'. v: '+v[0]+'. expected: '+expected[i]+'. delta: '+delta+'. tol: '+tol );

			delta = abs( v[1] - expected[i] );
			tol = EPS * abs( expected[i] );
			t.strictEqual( delta <= tol, true, 'im: '+x+'. n: '+n[i]+'. v: '+v[1]+'. expected: '+expected[i]+'. delta: '+delta+'. tol: '+tol );
		}
	}
	t.end();
});

tape( 'if the function encounters overflow, the function returns the input component', function test( t ) {
	var x;
	var v;

	x = 3.1468234343023397;
	v = cfloorn( x, x, -314 );
	t.deepEqual( v, [ x, x ], 'returns expected value' );

	x = -3.1468234343023397;
	v = cfloorn( x, x, -314 );
	t.deepEqual( v, [ x, x ], 'returns expected value' );

	x = 9007199254740000;
	v = cfloorn( x, x, -300 );
	t.deepEqual( v, [ x, x ], 'returns expected value' );

	x = -9007199254740000;
	v = cfloorn( x, x, -300 );
	t.deepEqual( v, [ x, x ], 'returns expected value' );

	t.end();
});

tape( 'the function rounds real and imaginary components (output array)', function test( t ) {
	var expected;
	var actual;
	var out;

	out = new Array( 2 );
	actual = cfloorn( out, -4.2, 5.5, 0 );
	expected = [ -5.0, 5.0 ];

	t.deepEqual( actual, expected, 'returns expected value' );
	t.strictEqual( actual, out, 'returns output value' );

	t.end();
});

tape( 'the function rounds real and imaginary components (output typed array)', function test( t ) {
	var expected;
	var actual;
	var out;

	out = new Float64Array( 2 );
	actual = cfloorn( out, 9.99999, 0.1, 0 );
	expected = new Float64Array( [ 9.0, 0.0 ] );

	t.deepEqual( actual, expected, 'returns expected value' );
	t.strictEqual( actual, out, 'returns output value' );

	t.end();
});

tape( 'the function rounds real and imaginary components (output object)', function test( t ) {
	var expected;
	var actual;
	var out;

	out = {};
	actual = cfloorn( out, 0.0, 0.0, 0 );
	expected = {
		'0': 0.0,
		'1': 0.0
	};
	t.deepEqual( actual, expected, 'returns expected value' );
	t.strictEqual( actual, out, 'returns output value' );

	t.end();
});
