import _extends from "@babel/runtime/helpers/extends";
import { isInteger } from './number.js';
import { isNumber, isBigNumber, isArray, isString } from './is.js';
import { format } from './string.js';
import { DimensionError } from '../error/DimensionError.js';
import { IndexError } from '../error/IndexError.js';
import { deepStrictEqual } from './object.js';

/**
 * Calculate the size of a multi dimensional array.
 * This function checks the size of the first entry, it does not validate
 * whether all dimensions match. (use function `validate` for that)
 * @param {Array} x
 * @Return {Number[]} size
 */
export function arraySize(x) {
  var s = [];
  while (Array.isArray(x)) {
    s.push(x.length);
    x = x[0];
  }
  return s;
}

/**
 * Recursively validate whether each element in a multi dimensional array
 * has a size corresponding to the provided size array.
 * @param {Array} array    Array to be validated
 * @param {number[]} size  Array with the size of each dimension
 * @param {number} dim   Current dimension
 * @throws DimensionError
 * @private
 */
function _validate(array, size, dim) {
  var i;
  var len = array.length;
  if (len !== size[dim]) {
    throw new DimensionError(len, size[dim]);
  }
  if (dim < size.length - 1) {
    // recursively validate each child array
    var dimNext = dim + 1;
    for (i = 0; i < len; i++) {
      var child = array[i];
      if (!Array.isArray(child)) {
        throw new DimensionError(size.length - 1, size.length, '<');
      }
      _validate(array[i], size, dimNext);
    }
  } else {
    // last dimension. none of the childs may be an array
    for (i = 0; i < len; i++) {
      if (Array.isArray(array[i])) {
        throw new DimensionError(size.length + 1, size.length, '>');
      }
    }
  }
}

/**
 * Validate whether each element in a multi dimensional array has
 * a size corresponding to the provided size array.
 * @param {Array} array    Array to be validated
 * @param {number[]} size  Array with the size of each dimension
 * @throws DimensionError
 */
export function validate(array, size) {
  var isScalar = size.length === 0;
  if (isScalar) {
    // scalar
    if (Array.isArray(array)) {
      throw new DimensionError(array.length, 0);
    }
  } else {
    // array
    _validate(array, size, 0);
  }
}

/**
 * Validate whether the source of the index matches the size of the Array
 * @param {Array | Matrix} array    Array to be validated
 * @param {Index} index  Index with the source information to validate
 * @throws DimensionError
 */
export function validateIndexSourceSize(value, index) {
  var valueSize = value.isMatrix ? value._size : arraySize(value);
  var sourceSize = index._sourceSize;
  // checks if the source size is not null and matches the valueSize
  sourceSize.forEach((sourceDim, i) => {
    if (sourceDim !== null && sourceDim !== valueSize[i]) {
      throw new DimensionError(sourceDim, valueSize[i]);
    }
  });
}

/**
 * Test whether index is an integer number with index >= 0 and index < length
 * when length is provided
 * @param {number} index    Zero-based index
 * @param {number} [length] Length of the array
 */
export function validateIndex(index, length) {
  if (index !== undefined) {
    if (!isNumber(index) || !isInteger(index)) {
      throw new TypeError('Index must be an integer (value: ' + index + ')');
    }
    if (index < 0 || typeof length === 'number' && index >= length) {
      throw new IndexError(index, length);
    }
  }
}

/**
 * Test if and index has empty values
 * @param {number} index    Zero-based index
 */
export function isEmptyIndex(index) {
  for (var i = 0; i < index._dimensions.length; ++i) {
    var dimension = index._dimensions[i];
    if (dimension._data && isArray(dimension._data)) {
      if (dimension._size[0] === 0) {
        return true;
      }
    } else if (dimension.isRange) {
      if (dimension.start === dimension.end) {
        return true;
      }
    } else if (isString(dimension)) {
      if (dimension.length === 0) {
        return true;
      }
    }
  }
  return false;
}

/**
 * Resize a multi dimensional array. The resized array is returned.
 * @param {Array | number} array         Array to be resized
 * @param {number[]} size Array with the size of each dimension
 * @param {*} [defaultValue=0]  Value to be filled in in new entries,
 *                              zero by default. Specify for example `null`,
 *                              to clearly see entries that are not explicitly
 *                              set.
 * @return {Array} array         The resized array
 */
export function resize(array, size, defaultValue) {
  // check the type of the arguments
  if (!Array.isArray(size)) {
    throw new TypeError('Array expected');
  }
  if (size.length === 0) {
    throw new Error('Resizing to scalar is not supported');
  }

  // check whether size contains positive integers
  size.forEach(function (value) {
    if (!isNumber(value) || !isInteger(value) || value < 0) {
      throw new TypeError('Invalid size, must contain positive integers ' + '(size: ' + format(size) + ')');
    }
  });

  // convert number to an array
  if (isNumber(array) || isBigNumber(array)) {
    array = [array];
  }

  // recursively resize the array
  var _defaultValue = defaultValue !== undefined ? defaultValue : 0;
  _resize(array, size, 0, _defaultValue);
  return array;
}

/**
 * Recursively resize a multi dimensional array
 * @param {Array} array         Array to be resized
 * @param {number[]} size       Array with the size of each dimension
 * @param {number} dim          Current dimension
 * @param {*} [defaultValue]    Value to be filled in in new entries,
 *                              undefined by default.
 * @private
 */
function _resize(array, size, dim, defaultValue) {
  var i;
  var elem;
  var oldLen = array.length;
  var newLen = size[dim];
  var minLen = Math.min(oldLen, newLen);

  // apply new length
  array.length = newLen;
  if (dim < size.length - 1) {
    // non-last dimension
    var dimNext = dim + 1;

    // resize existing child arrays
    for (i = 0; i < minLen; i++) {
      // resize child array
      elem = array[i];
      if (!Array.isArray(elem)) {
        elem = [elem]; // add a dimension
        array[i] = elem;
      }
      _resize(elem, size, dimNext, defaultValue);
    }

    // create new child arrays
    for (i = minLen; i < newLen; i++) {
      // get child array
      elem = [];
      array[i] = elem;

      // resize new child array
      _resize(elem, size, dimNext, defaultValue);
    }
  } else {
    // last dimension

    // remove dimensions of existing values
    for (i = 0; i < minLen; i++) {
      while (Array.isArray(array[i])) {
        array[i] = array[i][0];
      }
    }

    // fill new elements with the default value
    for (i = minLen; i < newLen; i++) {
      array[i] = defaultValue;
    }
  }
}

/**
 * Re-shape a multi dimensional array to fit the specified dimensions
 * @param {Array} array           Array to be reshaped
 * @param {number[]} sizes        List of sizes for each dimension
 * @returns {Array}               Array whose data has been formatted to fit the
 *                                specified dimensions
 *
 * @throws {DimensionError}       If the product of the new dimension sizes does
 *                                not equal that of the old ones
 */
export function reshape(array, sizes) {
  var flatArray = flatten(array);
  var currentLength = flatArray.length;
  if (!Array.isArray(array) || !Array.isArray(sizes)) {
    throw new TypeError('Array expected');
  }
  if (sizes.length === 0) {
    throw new DimensionError(0, currentLength, '!=');
  }
  sizes = processSizesWildcard(sizes, currentLength);
  var newLength = product(sizes);
  if (currentLength !== newLength) {
    throw new DimensionError(newLength, currentLength, '!=');
  }
  try {
    return _reshape(flatArray, sizes);
  } catch (e) {
    if (e instanceof DimensionError) {
      throw new DimensionError(newLength, currentLength, '!=');
    }
    throw e;
  }
}

/**
 * Replaces the wildcard -1 in the sizes array.
 * @param {number[]} sizes  List of sizes for each dimension. At most on wildcard.
 * @param {number} currentLength  Number of elements in the array.
 * @throws {Error}                If more than one wildcard or unable to replace it.
 * @returns {number[]}      The sizes array with wildcard replaced.
 */
export function processSizesWildcard(sizes, currentLength) {
  var newLength = product(sizes);
  var processedSizes = sizes.slice();
  var WILDCARD = -1;
  var wildCardIndex = sizes.indexOf(WILDCARD);
  var isMoreThanOneWildcard = sizes.indexOf(WILDCARD, wildCardIndex + 1) >= 0;
  if (isMoreThanOneWildcard) {
    throw new Error('More than one wildcard in sizes');
  }
  var hasWildcard = wildCardIndex >= 0;
  var canReplaceWildcard = currentLength % newLength === 0;
  if (hasWildcard) {
    if (canReplaceWildcard) {
      processedSizes[wildCardIndex] = -currentLength / newLength;
    } else {
      throw new Error('Could not replace wildcard, since ' + currentLength + ' is no multiple of ' + -newLength);
    }
  }
  return processedSizes;
}

/**
 * Computes the product of all array elements.
 * @param {number[]} array Array of factors
 * @returns {number}            Product of all elements
 */
function product(array) {
  return array.reduce((prev, curr) => prev * curr, 1);
}

/**
 * Iteratively re-shape a multi dimensional array to fit the specified dimensions
 * @param {Array} array           Array to be reshaped
 * @param {number[]} sizes  List of sizes for each dimension
 * @returns {Array}               Array whose data has been formatted to fit the
 *                                specified dimensions
 */

function _reshape(array, sizes) {
  // testing if there are enough elements for the requested shape
  var tmpArray = array;
  var tmpArray2;
  // for each dimensions starting by the last one and ignoring the first one
  for (var sizeIndex = sizes.length - 1; sizeIndex > 0; sizeIndex--) {
    var size = sizes[sizeIndex];
    tmpArray2 = [];

    // aggregate the elements of the current tmpArray in elements of the requested size
    var length = tmpArray.length / size;
    for (var i = 0; i < length; i++) {
      tmpArray2.push(tmpArray.slice(i * size, (i + 1) * size));
    }
    // set it as the new tmpArray for the next loop turn or for return
    tmpArray = tmpArray2;
  }
  return tmpArray;
}

/**
 * Squeeze a multi dimensional array
 * @param {Array} array
 * @param {Array} [size]
 * @returns {Array} returns the array itself
 */
export function squeeze(array, size) {
  var s = size || arraySize(array);

  // squeeze outer dimensions
  while (Array.isArray(array) && array.length === 1) {
    array = array[0];
    s.shift();
  }

  // find the first dimension to be squeezed
  var dims = s.length;
  while (s[dims - 1] === 1) {
    dims--;
  }

  // squeeze inner dimensions
  if (dims < s.length) {
    array = _squeeze(array, dims, 0);
    s.length = dims;
  }
  return array;
}

/**
 * Recursively squeeze a multi dimensional array
 * @param {Array} array
 * @param {number} dims Required number of dimensions
 * @param {number} dim  Current dimension
 * @returns {Array | *} Returns the squeezed array
 * @private
 */
function _squeeze(array, dims, dim) {
  var i, ii;
  if (dim < dims) {
    var next = dim + 1;
    for (i = 0, ii = array.length; i < ii; i++) {
      array[i] = _squeeze(array[i], dims, next);
    }
  } else {
    while (Array.isArray(array)) {
      array = array[0];
    }
  }
  return array;
}

/**
 * Unsqueeze a multi dimensional array: add dimensions when missing
 *
 * Paramter `size` will be mutated to match the new, unqueezed matrix size.
 *
 * @param {Array} array
 * @param {number} dims       Desired number of dimensions of the array
 * @param {number} [outer]    Number of outer dimensions to be added
 * @param {Array} [size] Current size of array.
 * @returns {Array} returns the array itself
 * @private
 */
export function unsqueeze(array, dims, outer, size) {
  var s = size || arraySize(array);

  // unsqueeze outer dimensions
  if (outer) {
    for (var i = 0; i < outer; i++) {
      array = [array];
      s.unshift(1);
    }
  }

  // unsqueeze inner dimensions
  array = _unsqueeze(array, dims, 0);
  while (s.length < dims) {
    s.push(1);
  }
  return array;
}

/**
 * Recursively unsqueeze a multi dimensional array
 * @param {Array} array
 * @param {number} dims Required number of dimensions
 * @param {number} dim  Current dimension
 * @returns {Array | *} Returns the squeezed array
 * @private
 */
function _unsqueeze(array, dims, dim) {
  var i, ii;
  if (Array.isArray(array)) {
    var next = dim + 1;
    for (i = 0, ii = array.length; i < ii; i++) {
      array[i] = _unsqueeze(array[i], dims, next);
    }
  } else {
    for (var d = dim; d < dims; d++) {
      array = [array];
    }
  }
  return array;
}
/**
 * Flatten a multi dimensional array, put all elements in a one dimensional
 * array
 * @param {Array} array   A multi dimensional array
 * @return {Array}        The flattened array (1 dimensional)
 */
export function flatten(array) {
  if (!Array.isArray(array)) {
    // if not an array, return as is
    return array;
  }
  var flat = [];
  array.forEach(function callback(value) {
    if (Array.isArray(value)) {
      value.forEach(callback); // traverse through sub-arrays recursively
    } else {
      flat.push(value);
    }
  });
  return flat;
}

/**
 * A safe map
 * @param {Array} array
 * @param {function} callback
 */
export function map(array, callback) {
  return Array.prototype.map.call(array, callback);
}

/**
 * A safe forEach
 * @param {Array} array
 * @param {function} callback
 */
export function forEach(array, callback) {
  Array.prototype.forEach.call(array, callback);
}

/**
 * A safe filter
 * @param {Array} array
 * @param {function} callback
 */
export function filter(array, callback) {
  if (arraySize(array).length !== 1) {
    throw new Error('Only one dimensional matrices supported');
  }
  return Array.prototype.filter.call(array, callback);
}

/**
 * Filter values in a callback given a regular expression
 * @param {Array} array
 * @param {RegExp} regexp
 * @return {Array} Returns the filtered array
 * @private
 */
export function filterRegExp(array, regexp) {
  if (arraySize(array).length !== 1) {
    throw new Error('Only one dimensional matrices supported');
  }
  return Array.prototype.filter.call(array, entry => regexp.test(entry));
}

/**
 * A safe join
 * @param {Array} array
 * @param {string} separator
 */
export function join(array, separator) {
  return Array.prototype.join.call(array, separator);
}

/**
 * Assign a numeric identifier to every element of a sorted array
 * @param {Array} a  An array
 * @return {Array} An array of objects containing the original value and its identifier
 */
export function identify(a) {
  if (!Array.isArray(a)) {
    throw new TypeError('Array input expected');
  }
  if (a.length === 0) {
    return a;
  }
  var b = [];
  var count = 0;
  b[0] = {
    value: a[0],
    identifier: 0
  };
  for (var i = 1; i < a.length; i++) {
    if (a[i] === a[i - 1]) {
      count++;
    } else {
      count = 0;
    }
    b.push({
      value: a[i],
      identifier: count
    });
  }
  return b;
}

/**
 * Remove the numeric identifier from the elements
 * @param {array} a  An array
 * @return {array} An array of values without identifiers
 */
export function generalize(a) {
  if (!Array.isArray(a)) {
    throw new TypeError('Array input expected');
  }
  if (a.length === 0) {
    return a;
  }
  var b = [];
  for (var i = 0; i < a.length; i++) {
    b.push(a[i].value);
  }
  return b;
}

/**
 * Check the datatype of a given object
 * This is a low level implementation that should only be used by
 * parent Matrix classes such as SparseMatrix or DenseMatrix
 * This method does not validate Array Matrix shape
 * @param {Array} array
 * @param {function} typeOf   Callback function to use to determine the type of a value
 * @return {string}
 */
export function getArrayDataType(array, typeOf) {
  var type; // to hold type info
  var length = 0; // to hold length value to ensure it has consistent sizes

  for (var i = 0; i < array.length; i++) {
    var item = array[i];
    var _isArray = Array.isArray(item);

    // Saving the target matrix row size
    if (i === 0 && _isArray) {
      length = item.length;
    }

    // If the current item is an array but the length does not equal the targetVectorSize
    if (_isArray && item.length !== length) {
      return undefined;
    }
    var itemType = _isArray ? getArrayDataType(item, typeOf) // recurse into a nested array
    : typeOf(item);
    if (type === undefined) {
      type = itemType; // first item
    } else if (type !== itemType) {
      return 'mixed';
    } else {
      // we're good, everything has the same type so far
    }
  }
  return type;
}

/**
 * Return the last item from an array
 * @param {array}
 * @returns {*}
 */
export function last(array) {
  return array[array.length - 1];
}

/**
 * Get all but the last element of array.
 * @param {array}
 * @returns {*}
 */
export function initial(array) {
  return array.slice(0, array.length - 1);
}

/**
 * Recursively concatenate two matrices.
 * The contents of the matrices is not cloned.
 * @param {Array} a             Multi dimensional array
 * @param {Array} b             Multi dimensional array
 * @param {number} concatDim    The dimension on which to concatenate (zero-based)
 * @param {number} dim          The current dim (zero-based)
 * @return {Array} c            The concatenated matrix
 * @private
 */
function concatRecursive(a, b, concatDim, dim) {
  if (dim < concatDim) {
    // recurse into next dimension
    if (a.length !== b.length) {
      throw new DimensionError(a.length, b.length);
    }
    var c = [];
    for (var i = 0; i < a.length; i++) {
      c[i] = concatRecursive(a[i], b[i], concatDim, dim + 1);
    }
    return c;
  } else {
    // concatenate this dimension
    return a.concat(b);
  }
}

/**
 * Concatenates many arrays in the specified direction
 * @param {...Array} arrays All the arrays to concatenate
 * @param {number} concatDim The dimension on which to concatenate (zero-based)
 * @returns
*/
export function concat() {
  var arrays = Array.prototype.slice.call(arguments, 0, -1);
  var concatDim = Array.prototype.slice.call(arguments, -1);
  if (arrays.length === 1) {
    return arrays[0];
  }
  if (arrays.length > 1) {
    return arrays.slice(1).reduce(function (A, B) {
      return concatRecursive(A, B, concatDim, 0);
    }, arrays[0]);
  } else {
    throw new Error('Wrong number of arguments in function concat');
  }
}

/**
 * Receives two or more sizes and get's the broadcasted size for both.
 * @param  {...number[]} sizes Sizes to broadcast together
 * @returns
 */
export function broadcastSizes() {
  for (var _len = arguments.length, sizes = new Array(_len), _key = 0; _key < _len; _key++) {
    sizes[_key] = arguments[_key];
  }
  var dimensions = sizes.map(s => s.length);
  var N = Math.max(...dimensions);
  var sizeMax = new Array(N).fill(null);
  // check for every size
  for (var i = 0; i < sizes.length; i++) {
    var size = sizes[i];
    var dim = dimensions[i];
    for (var j = 0; j < dim; j++) {
      var n = N - dim + j;
      if (size[j] > sizeMax[n]) {
        sizeMax[n] = size[j];
      }
    }
  }
  for (var _i = 0; _i < sizes.length; _i++) {
    checkBroadcastingRules(sizes[_i], sizeMax);
  }
  return sizeMax;
}

/**
 * Checks if it's possible to broadcast a size to another size
 * @param {number[]} size The size of the array to check
 * @param {number[]} toSize The size of the array to validate if it can be broadcasted to
 */
export function checkBroadcastingRules(size, toSize) {
  var N = toSize.length;
  var dim = size.length;
  for (var j = 0; j < dim; j++) {
    var n = N - dim + j;
    if (size[j] < toSize[n] && size[j] > 1 || size[j] > toSize[n]) {
      throw new Error("shape missmatch: missmatch is found in arg with shape (".concat(size, ") not possible to broadcast dimension ").concat(dim, " with size ").concat(size[j], " to size ").concat(toSize[n]));
    }
  }
}

/**
 * Broadcasts a single array to a certain size
 * @param {array} array Array to be broadcasted
 * @param {number[]} toSize Size to broadcast the array
 * @returns The broadcasted array
 */
export function broadcastTo(array, toSize) {
  var Asize = arraySize(array);
  if (deepStrictEqual(Asize, toSize)) {
    return array;
  }
  checkBroadcastingRules(Asize, toSize);
  var broadcastedSize = broadcastSizes(Asize, toSize);
  var N = broadcastedSize.length;
  var paddedSize = [...Array(N - Asize.length).fill(1), ...Asize];
  var A = clone(array);
  // reshape A if needed to make it ready for concat
  if (Asize.length < N) {
    A = reshape(A, paddedSize);
    Asize = arraySize(A);
  }

  // stretches the array on each dimension to make it the same size as index
  for (var dim = 0; dim < N; dim++) {
    if (Asize[dim] < broadcastedSize[dim]) {
      A = stretch(A, broadcastedSize[dim], dim);
      Asize = arraySize(A);
    }
  }
  return A;
}

/**
 * Broadcasts arrays and returns the broadcasted arrays in an array
 * @param  {...Array | any} arrays
 * @returns
 */
export function broadcastArrays() {
  for (var _len2 = arguments.length, arrays = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
    arrays[_key2] = arguments[_key2];
  }
  if (arrays.length === 0) {
    throw new Error('Insuficient number of argumnets in function broadcastArrays');
  }
  if (arrays.length === 1) {
    return arrays[0];
  }
  var sizes = arrays.map(function (array) {
    return arraySize(array);
  });
  var broadcastedSize = broadcastSizes(...sizes);
  var broadcastedArrays = [];
  arrays.forEach(function (array) {
    broadcastedArrays.push(broadcastTo(array, broadcastedSize));
  });
  return broadcastedArrays;
}

/**
 * stretches a matrix up to a certain size in a certain dimension
 * @param {Array} arrayToStretch
 * @param {number[]} sizeToStretch
 * @param {number} dimToStretch
 * @returns
 */
export function stretch(arrayToStretch, sizeToStretch, dimToStretch) {
  return concat(...Array(sizeToStretch).fill(arrayToStretch), dimToStretch);
}

/**
* Retrieves a single element from an array given an index.
*
* @param {Array} array - The array from which to retrieve the value.
* @param {Array<number>} idx - An array of indices specifying the position of the desired element in each dimension.
* @returns {*} - The value at the specified position in the array.
*
* @example
* const arr = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]];
* const index = [1, 0, 1];
* console.log(getValue(arr, index)); // 6
*/
export function get(array, index) {
  if (!Array.isArray(array)) {
    throw new Error('Array expected');
  }
  var size = arraySize(array);
  if (index.length !== size.length) {
    throw new DimensionError(index.length, size.length);
  }
  for (var x = 0; x < index.length; x++) {
    validateIndex(index[x], size[x]);
  }
  return index.reduce((acc, curr) => acc[curr], array);
}

/**
 * Recursive function to map a multi-dimensional array.
 *
 * @param {*} value - The current value being processed in the array.
 * @param {Array} index - The index of the current value being processed in the array.
 * @param {Array} array - The array being processed.
 * @param {Function} callback - Function that produces the element of the new Array, taking three arguments: the value of the element, the index of the element, and the Array being processed.
 * @returns {*} The new array with each element being the result of the callback function.
 */
export function recurse(value, index, array, callback) {
  if (Array.isArray(value)) {
    return value.map(function (child, i) {
      // we create a copy of the index array and append the new index value
      return recurse(child, index.concat(i), array, callback);
    });
  } else {
    // invoke the callback function with the right number of arguments
    return callback(value, index, array);
  }
}

/**
 * Deep clones a multidimensional array
 * @param {Array} array
 * @returns cloned array
 */
export function clone(array) {
  return _extends([], array);
}