/**
* @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 hasOwnProp = require( '@stdlib/assert/has-own-property' );
var isObject = require( '@stdlib/assert/is-plain-object' );
var isBoolean = require( '@stdlib/assert/is-boolean' ).isPrimitive;
var isArray = require( '@stdlib/assert/is-array' );
var isNonNegativeInteger = require( '@stdlib/assert/is-nonnegative-integer' ).isPrimitive;
var isndarrayLike = require( '@stdlib/assert/is-ndarray-like' );
var shape2strides = require( './../../base/shape2strides' );
var strides2offset = require( './../../base/strides2offset' );
var strides2order = require( './../../base/strides2order' );
var numel = require( './../../base/numel' );
var ndarray = require( './../../ctor' );
var isDataType = require( './../../base/assert/is-data-type' );
var isOrder = require( './../../base/assert/is-order' );
var isCastingMode = require( './../../base/assert/is-casting-mode' );
var isAllowedCast = require( './../../base/assert/is-allowed-data-type-cast' );
var createBuffer = require( './../../base/buffer' );
var getType = require( './../../base/buffer-dtype' );
var arrayShape = require( '@stdlib/array/shape' );
var flattenArray = require( '@stdlib/utils/flatten-array' );
var isArrayLikeObject = require( './is_array_like_object.js' );
var defaults = require( './defaults.json' );
var castBuffer = require( './cast_buffer.js' );
var copyView = require( './copy_view.js' );
var expandShape = require( './expand_shape.js' );
var expandStrides = require( './expand_strides.js' );


// MAIN //

/**
* Returns a multidimensional array.
*
* @param {(ArrayLikeObject|TypedArrayLike|Buffer|ndarrayLike)} [buffer] - data source
* @param {Options} [options] - function options
* @param {(ArrayLikeObject|TypedArrayLike|Buffer|ndarrayLike)} [options.buffer] - data source
* @param {string} [options.dtype="float64"] - underlying storage data type (if the input data is not of the same type, this option specifies the data type to which to cast the input data)
* @param {string} [options.order="row-major"] - specifies the memory layout of the array as either row-major (C-style) or column-major (Fortran-style)
* @param {NonNegativeIntegerArray} [options.shape] - array shape
* @param {string} [options.mode="throw"] - specifies how to handle indices which exceed array dimensions
* @param {StringArray} [options.submode=["throw"]] - specifies how to handle subscripts which exceed array dimensions on a per dimension basis
* @param {boolean} [options.copy=false] - boolean indicating whether to copy source data to a new data buffer
* @param {boolean} [options.flatten=true] - boolean indicating whether to automatically flatten generic array data sources
* @param {NonNegativeInteger} [options.ndmin=0] - minimum number of dimensions
* @param {string} [options.casting="safe"] - casting rule used to determine what constitutes an acceptable cast
* @throws {TypeError} options argument must be an object
* @throws {TypeError} must provide valid options
* @throws {Error} must provide either an array shape, data source, or both
* @throws {Error} invalid cast
* @throws {RangeError} data source must be compatible with specified meta data
* @returns {ndarray} ndarray instance
*
* @example
* var arr = array( [ [ 1, 2 ], [ 3, 4 ] ] );
* // returns <ndarray>
*
* var v = arr.get( 0, 0 );
* // returns 1
*
* @example
* var opts = {
*     'dtype': 'generic',
*     'flatten': false
* };
*
* var arr = array( [ [ 1, 2 ], [ 3, 4 ] ], opts );
* // returns <ndarray>
*
* var v = arr.get( 0 );
* // returns [ 1, 2 ]
*
* @example
* var Float64Array = require( '@stdlib/array/float64' );
*
* var opts = {
*     'shape': [ 2, 2 ]
* };
*
* var arr = array( new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] ), opts );
* // returns <ndarray>
*
* var v = arr.get( 0, 0 );
* // returns 1.0
*/
function array() {
	var options;
	var strides;
	var buffer;
	var offset;
	var order;
	var dtype;
	var btype;
	var shape;
	var ndims;
	var nopts;
	var opts;
	var len;
	var ord;
	var FLG;

	if ( arguments.length === 1 ) {
		if ( isArrayLikeObject( arguments[ 0 ] ) ) {
			buffer = arguments[ 0 ];
			options = {};
		} else {
			options = arguments[ 0 ];
			if ( !isObject( options ) ) {
				throw new TypeError( 'invalid argument. Must provide either a valid data source, options argument, or both. Value: `' + options + '`.' );
			}
			if ( hasOwnProp( options, 'buffer' ) ) {
				buffer = options.buffer;
				if ( !isArrayLikeObject( buffer ) ) { // weak test
					throw new TypeError( 'invalid option. `buffer` option must be an array-like object, typed-array-like, a Buffer, or an ndarray. Option: `' + buffer + '`.' );
				}
			}
		}
	} else {
		buffer = arguments[ 0 ];
		if ( !isArrayLikeObject( buffer ) ) { // weak test
			throw new TypeError( 'invalid option. Data source must be an array-like object, typed-array-like, a Buffer, or an ndarray. Value: `' + buffer + '`.' );
		}
		options = arguments[ 1 ];
		if ( !isObject( options ) ) {
			throw new TypeError( 'invalid argument. Options argument must be an object. Value: `' + options + '`.' );
		}
		// Note: we ignore whether `options` has a `buffer` property
	}
	if ( buffer ) {
		if ( isndarrayLike( buffer ) ) {
			btype = buffer.dtype;
			FLG = true;
		} else {
			btype = getType( buffer );
			FLG = false;
		}
	}
	nopts = {};
	opts = {};

	// Validate some options before others...
	if ( hasOwnProp( options, 'casting' ) ) {
		opts.casting = options.casting;
		if ( !isCastingMode( opts.casting ) ) {
			throw new TypeError( 'invalid option. `casting` option must be a recognized casting mode. Option: `' + opts.casting + '`.' );
		}
	} else {
		opts.casting = defaults.casting;
	}
	if ( hasOwnProp( options, 'flatten' ) ) {
		opts.flatten = options.flatten;
		if ( !isBoolean( opts.flatten ) ) {
			throw new TypeError( 'invalid option. `flatten` option must be a boolean. Option: `' + opts.flatten + '`.' );
		}
	} else {
		opts.flatten = defaults.flatten;
	}
	if ( hasOwnProp( options, 'ndmin' ) ) {
		opts.ndmin = options.ndmin;
		if ( !isNonNegativeInteger( opts.ndmin ) ) {
			throw new TypeError( 'invalid option. `ndmin` option must be a nonnegative integer. Option: `' + opts.ndmin + '`.' );
		}
		// TODO: validate that minimum number of dimensions does not exceed the maximum number of possible dimensions (in theory, infinite; in practice, determined by max array length; see https://github.com/stdlib-js/stdlib/blob/ac350059877c036640775d6b30d0e98e840d07cf/lib/node_modules/%40stdlib/ndarray/ctor/lib/main.js#L57)
	} else {
		opts.ndmin = defaults.ndmin;
	}

	// Validate the remaining options...
	if ( hasOwnProp( options, 'dtype' ) ) {
		dtype = options.dtype;
		if ( !isDataType( dtype ) ) {
			throw new TypeError( 'invalid option. `dtype` option must be a recognized data type. Option: `' + dtype + '`.' );
		}
		if ( btype && !isAllowedCast( btype, dtype, opts.casting ) ) {
			throw new Error( 'invalid option. Data type cast is not allowed. Casting mode: `' + opts.casting + '`. From: `' + btype + '`. To: `' + dtype + '`.' );
		}
	} else if ( btype ) {
		// TODO: reconcile difference in behavior when provided a generic array and no `dtype` option. Currently, we cast here, but do not allow casting a generic array (by default) when explicitly providing a `dtype` option.

		// Only cast generic array data sources when not provided an ndarray...
		if ( !FLG && btype === 'generic' ) {
			dtype = defaults.dtype;
		} else {
			dtype = btype;
		}
	} else {
		dtype = defaults.dtype;
	}
	if ( hasOwnProp( options, 'order' ) ) {
		order = options.order;
		if ( order === 'any' || order === 'same' ) {
			if ( FLG ) {
				// If the user indicated that "any" order suffices (meaning the user does not care about ndarray order), then we use the default order, unless the input ndarray is either unequivocally "row-major" or "column-major" or configured as such....
				if ( order === 'any' ) {
					// Compute the layout order in order to ascertain whether an ndarray can be considered both "row-major" and "column-major":
					ord = strides2order( buffer.strides );

					// If the ndarray can be considered both "row-major" and "column-major", then use the default order; otherwise, use the ndarray's stated layout order...
					if ( ord === 3 ) {
						order = defaults.order;
					} else {
						order = buffer.order;
					}
				}
				// Otherwise, use the same order as the provided ndarray...
				else if ( order === 'same' ) {
					order = buffer.order;
				}
			} else {
				order = defaults.order;
			}
		} else if ( !isOrder( order ) ) {
			throw new TypeError( 'invalid option. `order` option must be a recognized order. Option: `' + order + '`.' );
		}
	} else {
		order = defaults.order;
	}
	if ( hasOwnProp( options, 'mode' ) ) {
		nopts.mode = options.mode;
	} else {
		nopts.mode = defaults.mode;
	}
	if ( hasOwnProp( options, 'submode' ) ) {
		nopts.submode = options.submode;
	} else {
		nopts.submode = [ nopts.mode ];
	}
	if ( hasOwnProp( options, 'copy' ) ) {
		opts.copy = options.copy;
		if ( !isBoolean( opts.copy ) ) {
			throw new TypeError( 'invalid option. `copy` option must be a boolean. Option: `' + opts.copy + '`.' );
		}
	} else {
		opts.copy = defaults.copy;
	}
	// If not provided a shape, infer from a provided data source...
	if ( hasOwnProp( options, 'shape' ) ) {
		shape = options.shape;
		if ( !isArrayLikeObject( shape ) ) { // weak test
			throw new TypeError( 'invalid option. `shape` option must be an array-like object containing nonnegative integers. Option: `' + shape + '`.' );
		}
		ndims = shape.length;
		len = numel( shape );
	} else if ( buffer ) {
		if ( FLG ) {
			shape = buffer.shape;
			ndims = buffer.ndims;
			len = buffer.length;
		} else if ( opts.flatten && isArray( buffer ) ) {
			shape = arrayShape( buffer );
			ndims = shape.length;
			len = numel( shape );
		} else {
			ndims = 1;
			len = buffer.length;
			shape = [ len ]; // assume a 1-dimensional array (vector)
		}
	} else {
		throw new Error( 'invalid arguments. Must provide either a data source, array shape, or both.' );
	}
	// Adjust the array shape to satisfy the minimum number of dimensions...
	if ( ndims < opts.ndmin ) {
		shape = expandShape( ndims, shape, opts.ndmin );
		ndims = opts.ndmin;
	}
	// If not provided a data buffer, create it; otherwise, see if we need to cast a provided data buffer to another data type or perform a copy...
	if ( FLG ) {
		if ( buffer.length !== len ) {
			throw new RangeError( 'invalid arguments. Array shape is incompatible with provided data source. Number of data source elements does not match array shape.' );
		}
		if ( btype !== dtype || opts.copy ) {
			buffer = copyView( buffer, dtype );
		} else {
			strides = buffer.strides;
			offset = buffer.offset;
			buffer = buffer.data;
			if ( strides.length < ndims ) {
				// Account for augmented dimensions (note: expanding the strides array to account for prepended singleton dimensions does **not** affect the index offset):
				strides = expandStrides( ndims, shape, strides, order );
			}
		}
	} else if ( buffer ) {
		if ( btype === 'generic' && opts.flatten ) {
			buffer = flattenArray( buffer );
		}
		if ( buffer.length !== len ) {
			throw new RangeError( 'invalid arguments. Array shape is incompatible with provided data source. Number of data source elements does not match array shape.' );
		}
		if ( btype !== dtype || opts.copy ) {
			buffer = castBuffer( buffer, len, dtype );
		}
	} else {
		buffer = createBuffer( dtype, len );
	}
	// If we have yet to determine array strides, we assume that we can compute the strides, along with the index offset, for a **contiguous** data source based solely on the array shape and specified memory layout order...
	if ( strides === void 0 ) {
		strides = shape2strides( shape, order );
		offset = strides2offset( shape, strides );
	}
	return new ndarray( dtype, buffer, shape, strides, offset, order, nopts );
}


// EXPORTS //

module.exports = array;
