import getIsSortable from './getIsSortable';
import getTypedValue from './getTypedValue';

/**
 * Utility function to get the relative sort comparison of two values, useful
 * for use inside a comparator function passed to `Array.prototype.sort()`. It
 * is a safe(r) and easier way to handle sorting at a low level in React. The
 * `getSortValue` function takes four positional arguments specifying what's
 * being compared and how, plus an object with additional configuration. It
 * returns a nonzero number that can be interpreted as follows:
 *
 * - if the return value is greater than 0, it means that `a` is greater than
 *   `b`; and
 * - if the return value is less than 0, it means that `a` is less than `b`.
 *
 * In a standard comparator function, a return value of 0 would indicate that
 * the two items are of equal value, but this utility enforces an explicit
 * sorting with no equal values, so if two values are found to be equal and no
 * fallback function is provided (see the options below), an error will be
 * thrown.
 *
 * **Note:** Remember that `Array.prototype.sort()` mutates the array that you
 * call it on. In general you should try to avoid mutating arrays that are
 * passed into a function as arguments or into a component as props. To avoid
 * mutating an array you can first make a copy of it by doing something like
 *
 * ```js
 * import { getSortValue } from '@politico/interactive-style';
 *
 * // unsortedArray provided as argument or prop
 * const sortedArray = [...unsortedArray].sort((a, b) => getSortValue(a, b));
 * ```
 *
 * @param {any} a The first value to compare
 * @param {any} b The second value to compare
 * @param {'asc'|'desc'} [direction=asc] The direction of the sort
 * @param {'string'|'boolean'|'date'|'number'|'lookup'|'custom'} [type=number]
 *   The type of the values being compared
 * @param {Object} [options]
 *   Additional configuration:
 *   - `fallback`: If `a` and `b` are found to be of equal value, this function
 *     will be run as a fallback. Usually another `getSortValue` is supplied
 *     here and these can be chained. Note that if two values are found to be
 *     of equal value and no `fallback` function was provided, an error will be
 *     thrown.
 *   - `nullFirst`: A boolean indicating if a falsy value (except 0s) should
 *     appear before a truthy value (`true` – the default) or after it
 *     (`false`).
 *   - `lookup`: If type is "lookup", this should be an object in which the
 *     values of `a` and `b` are keys and the values are a numerical value of
 *     those keys in this relative scale.
 *   - `getValue`: If type is "custom", this should be a function which is
 *     passed the value of `a` or `b`, and returns a numerical value
 *
 * @returns number
 */
const getSortValue = function getSortValue(a, b, direction = 'asc', type = 'number', options = {}) {
  const fallback = options.fallback || function fallbackSort() {
    throw new Error('Fallback called in sorting function, ' + 'but no fallback function provided.');
  };
  const aVal = getTypedValue(a, type, options);
  const bVal = getTypedValue(b, type, options);
  const isSortable = getIsSortable(aVal, bVal, options);
  if (typeof isSortable === 'number') {
    return isSortable;
  }
  if (isSortable === false) {
    return fallback(a, b);
  }
  if (type === 'string') {
    return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
  }
  return direction === 'asc' ? aVal - bVal : bVal - aVal;
};
export default getSortValue;