"use strict";

var _interopRequireWildcard = require("@babel/runtime-corejs3/helpers/interopRequireWildcard");

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _Object$defineProperty = require("@babel/runtime-corejs3/core-js-stable/object/define-property");

require("core-js/modules/es.number.constructor");

require("core-js/modules/es.regexp.exec");

require("core-js/modules/es.string.split");

_Object$defineProperty(exports, "__esModule", {
  value: true
});

exports.default = createMemoizedStats;
exports.diffArrays = diffArrays;
exports.statReduce = statReduce;

var _concat = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/concat"));

var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/map"));

var _indexOf = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/index-of"));

var _filter = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/filter"));

var _values = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/values"));

var _stringify = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/json/stringify"));

var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/slicedToArray"));

var _forEach = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/for-each"));

var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/defineProperty"));

var _map2 = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/map"));

var _cloneDeep = _interopRequireDefault(require("clone-deep"));

var _flat_object_entries = require("../../utils/flat_object_entries");

var _value_types = require("./value_types");

var valueTypes = _interopRequireWildcard(require("../../constants/value_types"));

var _helpers = require("../../utils/helpers");

// @flow
function defaultStats()
/*: FieldStatistic*/
{
  var _ref;

  return _ref = {}, (0, _defineProperty2.default)(_ref, valueTypes.STRING, {
    count: 0,
    values: new _map2.default()
  }), (0, _defineProperty2.default)(_ref, valueTypes.NUMBER, {
    count: 0,
    values: new _map2.default()
  }), (0, _defineProperty2.default)(_ref, valueTypes.DATE, {
    count: 0,
    values: new _map2.default()
  }), (0, _defineProperty2.default)(_ref, valueTypes.DATETIME, {
    count: 0,
    values: new _map2.default()
  }), (0, _defineProperty2.default)(_ref, valueTypes.BOOLEAN, {
    count: 0,
    values: new _map2.default([[true, 0], [false, 0]])
  }), (0, _defineProperty2.default)(_ref, valueTypes.URL, 0), (0, _defineProperty2.default)(_ref, valueTypes.IMAGE_URL, 0), (0, _defineProperty2.default)(_ref, valueTypes.AUDIO_URL, 0), (0, _defineProperty2.default)(_ref, valueTypes.VIDEO_URL, 0), (0, _defineProperty2.default)(_ref, valueTypes.NULL, 0), (0, _defineProperty2.default)(_ref, valueTypes.UNDEFINED, 0), (0, _defineProperty2.default)(_ref, valueTypes.LOCATION, 0), _ref;
}

function createMemoizedStats() {
  var stats
  /*: Statistics*/
  = {};
  var dataMemo = [];
  return function getStats(data
  /*: Array<JSONObject>*/
  )
  /*: Statistics*/
  {
    if (data === dataMemo) return stats;

    var _diffArrays = diffArrays(dataMemo, data),
        added = _diffArrays.added,
        removed = _diffArrays.removed;

    if (!added.length && !removed.length) return (0, _cloneDeep.default)(stats);

    if (removed.length) {
      // Anything removed, calculate all the stats
      // TODO: more efficient stats on removal
      stats = {};
      (0, _forEach.default)(data).call(data, function (item) {
        return addItemStats(item, stats);
      });
    } else {
      // Only added items -> only need to process new items
      (0, _forEach.default)(added).call(added, function (item) {
        return addItemStats(item, stats);
      });
    }

    dataMemo = data;
    return (0, _cloneDeep.default)(stats);
  };
}

function addItemStats() {
  var _context;

  var item
  /*: JSONObject*/
  = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  var stats
  /*: Statistics*/
  = arguments.length > 1 ? arguments[1] : undefined;
  (0, _forEach.default)(_context = (0, _flat_object_entries.flatObjectEntries)(item)).call(_context, function (_ref2) {
    var _ref3 = (0, _slicedToArray2.default)(_ref2, 2),
        path = _ref3[0],
        value = _ref3[1];

    var key = (0, _stringify.default)(path);
    if (!stats[key]) stats[key] = defaultStats();
    addFieldStats(value, stats[key]);
  });
}

function addFieldStats(value
/*: any*/
, fieldStats
/*: FieldStatistic*/
) {
  var type = (0, _value_types.guessValueType)(value);

  if (typeof fieldStats[type] === 'number') {
    fieldStats[type] += 1;
    return;
  }

  switch (type) {
    case valueTypes.ARRAY:
      var arrayStats = fieldStats[valueTypes.ARRAY];

      if (arrayStats === undefined) {
        arrayStats = fieldStats[valueTypes.ARRAY] = {
          count: 0,
          lengthMin: +Infinity,
          lengthMax: -Infinity,
          valueStats: defaultStats()
        };
      }

      addArrayStats(value, arrayStats);
      return;

    case valueTypes.STRING:
      addStringStats(value, fieldStats[valueTypes.STRING]);
      return;

    case valueTypes.NUMBER:
      addNumberStats(value, fieldStats[valueTypes.NUMBER]);
      return;

    case valueTypes.DATE:
      addDateStats(value, fieldStats[valueTypes.DATE]);
      return;

    case valueTypes.DATETIME:
      addDateTimeStats(value, fieldStats[valueTypes.DATETIME]);
      return;

    case valueTypes.BOOLEAN:
      fieldStats[valueTypes.BOOLEAN].count += 1;
      (0, _values.default)(fieldStats[valueTypes.BOOLEAN]).set(value, (0, _values.default)(fieldStats[valueTypes.BOOLEAN]).get(value) + 1);
      return;

    default:
      // $FlowFixMe - flow doesn't realise that type is not values above
      fieldStats[type].count += 1;
  }
}

function addArrayStats(value
/*: []*/
, stats
/*: $NonMaybeType<$ElementType<FieldStatistic, 'array'>>*/
) {
  if (value.length > stats.lengthMax) stats.lengthMax = value.length;
  if (value.length < stats.lengthMin) stats.lengthMin = value.length;
  stats.count += 1;
  (0, _forEach.default)(value).call(value, function (item) {
    addFieldStats(item, stats.valueStats);
  });
}

function addNumberStats(value
/*: number*/
, stats
/*: NumberStatistic*/
) {
  stats.count += 1;

  var _statReduce = statReduce(stats, value, stats.count - 1),
      min = _statReduce.min,
      max = _statReduce.max,
      variance = _statReduce.variance,
      mean = _statReduce.mean;

  stats.min = min;
  stats.max = max;
  stats.variance = variance;
  stats.mean = mean;
  if ((0, _values.default)(stats).has(value)) (0, _values.default)(stats).set(value, (0, _values.default)(stats).get(value) + 1);else (0, _values.default)(stats).set(value, 1);
}

function addDateTimeStats(value
/*: string*/
, stats
/*: DateStatistic*/
) {
  var dateAsNumber = +Date.parse(value);
  stats.count += 1;

  var _statReduce2 = statReduce({
    mean: stats.mean !== undefined ? +Date.parse(stats.mean) : undefined
  }, dateAsNumber, stats.count - 1),
      mean = _statReduce2.mean;

  stats.min = stats.min === undefined ? value : value < stats.min ? value : stats.min;
  stats.max = stats.max === undefined ? value : value > stats.max ? value : stats.max;
  stats.mean = mean !== undefined ? new Date(mean).toISOString() : undefined;
  if ((0, _values.default)(stats).has(value)) (0, _values.default)(stats).set(value, (0, _values.default)(stats).get(value) + 1);else (0, _values.default)(stats).set(value, 1);
}
/** This requires slightly special treatment because date does not include time */


function addDateStats(value
/*: string*/
, stats
/*: DateStatistic*/
) {
  var dateAsNumber = dateToNumber(value);
  stats.count += 1;

  var _statReduce3 = statReduce({
    mean: stats.mean !== undefined ? dateToNumber(stats.mean) : undefined
  }, dateAsNumber, stats.count - 1),
      mean = _statReduce3.mean;

  stats.min = stats.min === undefined ? value : value < stats.min ? value : stats.min;
  stats.max = stats.max === undefined ? value : value > stats.max ? value : stats.max;
  stats.mean = mean !== undefined ? numberToDate(mean) : undefined;
  if ((0, _values.default)(stats).has(value)) (0, _values.default)(stats).set(value, (0, _values.default)(stats).get(value) + 1);else (0, _values.default)(stats).set(value, 1);
}

function addStringStats(value
/*: string*/
, stats
/*: StringStatistic*/
) {
  stats.count += 1;
  var lengthStats = statReduce({
    min: stats.lengthMin,
    max: stats.lengthMax,
    variance: stats.lengthVariance,
    mean: stats.lengthMean
  }, value.length, stats.count - 1);
  var wordStats = statReduce({
    min: stats.wordsMin,
    max: stats.wordsMax,
    variance: stats.wordsVariance,
    mean: stats.wordsMean
  }, value.split(' ').length, stats.count - 1);
  stats.lengthMin = lengthStats.min;
  stats.lengthMax = lengthStats.max;
  stats.lengthVariance = lengthStats.variance;
  stats.lengthMean = lengthStats.mean;
  stats.wordsMin = wordStats.min;
  stats.wordsMax = wordStats.max;
  stats.wordsVariance = wordStats.variance;
  stats.wordsMean = wordStats.mean;
  if ((0, _values.default)(stats).has(value)) (0, _values.default)(stats).set(value, (0, _values.default)(stats).get(value) + 1);else (0, _values.default)(stats).set(value, 1);
}
/**
 * Compare two arrays of objects and return items in the second but not in the
 * first (added) and items in the first but not in the second (removed).
 * Compares using strict equality.
 */


function diffArrays(oldArray
/*: Array<Object>*/
, newArray
/*: Array<Object>*/
)
/*: { added: Array<Object>, removed: Array<Object> }*/
{
  var added = (0, _filter.default)(newArray).call(newArray, function (v) {
    return (0, _indexOf.default)(oldArray).call(oldArray, v) === -1;
  });
  var removed = (0, _filter.default)(oldArray).call(oldArray, function (v) {
    return (0, _indexOf.default)(newArray).call(newArray, v) === -1;
  });
  return {
    added: added,
    removed: removed
  };
}
/*:: type MathStat = {
  mean?: number,
  variance?: number,
  min?: number,
  max?: number
}*/


/**
 * Reducer that computes running mean, variance, min and max
 * Adapted from http://www.johndcook.com/blog/standard_deviation/
 * @param {Object} p The previous value for the analysis
 * @param {Number} x New value to be included in analysis
 * @param {Number} i zero-based index of the current element being processed
 * @return {Object} New analysis including `x`
 */
function statReduce(_ref4, x
/*: number*/
, i
/*: number*/
)
/*: $ObjMap<MathStat, <V>(V) => $NonMaybeType<V>>*/
{
  var _ref4$mean = _ref4.mean,
      mean = _ref4$mean === void 0 ? NaN : _ref4$mean,
      _ref4$variance = _ref4.variance,
      variance = _ref4$variance === void 0 ? NaN : _ref4$variance,
      _ref4$min = _ref4.min,
      min = _ref4$min === void 0 ? +Infinity : _ref4$min,
      _ref4$max = _ref4.max,
      max = _ref4$max === void 0 ? -Infinity : _ref4$max;
  mean = isNaN(mean) ? 0 : mean;
  x = x instanceof Date ? +x : x;
  var newMean = mean + (x - mean) / (i + 1);
  return {
    mean: newMean,
    min: x < min ? x : min,
    max: x > max ? x : max,
    variance: i < 1 ? 0 : (variance * i + (x - mean) * (x - newMean)) / (i + 1)
  };
}
/** Convert date in the format YYYY-MM-DD to a number */


function dateToNumber(value
/*: string*/
)
/*: number*/
{
  var _context2;

  var _value$split$map = (0, _map.default)(_context2 = value.split('-')).call(_context2, Number),
      _value$split$map2 = (0, _slicedToArray2.default)(_value$split$map, 3),
      year = _value$split$map2[0],
      month = _value$split$map2[1],
      day = _value$split$map2[2]; // Add 12 hours -> middle of day


  return new Date(year, month - 1, day).getTime() + 12 * 60 * 60 * 1000;
}

function numberToDate(value
/*: number*/
)
/*: string*/
{
  var _context3, _context4;

  var date = new Date(value);
  var YYYY = date.getFullYear();
  var MM = (0, _helpers.leftPad)(date.getMonth() + 1 + '', 2, '0');
  var DD = (0, _helpers.leftPad)(date.getDate() + '', 2, '0');
  return (0, _concat.default)(_context3 = (0, _concat.default)(_context4 = "".concat(YYYY, "-")).call(_context4, MM, "-")).call(_context3, DD);
}
//# sourceMappingURL=statistics.js.map