'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _typeof2 = require('babel-runtime/helpers/typeof');

var _typeof3 = _interopRequireDefault(_typeof2);

var _stringify = require('babel-runtime/core-js/json/stringify');

var _stringify2 = _interopRequireDefault(_stringify);

var _defineProperty2 = require('babel-runtime/helpers/defineProperty');

var _defineProperty3 = _interopRequireDefault(_defineProperty2);

var _keys = require('babel-runtime/core-js/object/keys');

var _keys2 = _interopRequireDefault(_keys);

var _MAX_DISCRETE_VALUES, _isFilterable, _isMediaField, _isStringOrArray, _isNumberOrArray;

var _reselect = require('reselect');

var _urlRegex = require('url-regex');

var _urlRegex2 = _interopRequireDefault(_urlRegex);

var _url = require('url');

var _url2 = _interopRequireDefault(_url);

var _path = require('path');

var _path2 = _interopRequireDefault(_path);

var _constants = require('../constants');

var _filter_helpers = require('../util/filter_helpers');

var _flattened_features = require('./flattened_features');

var _flattened_features2 = _interopRequireDefault(_flattened_features);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

/**
 * Analyzes the fields of features in a featureCollection and guesses the
 *   field type and what type of filter to use: `discrete`, `number`,
 *   `date` (subtype of continuous), or `text` (and field that has more than
 *   `maxTextValues` discrete values). Number fields with <= `maxNumberCount`
 *   different values are considered discrete.
 * @param {object} featureCollection GeoJson FeatureCollection
 * @return {object} An object with a key for each unique field name in the
 *   FeatureCollection with properties `type` of filter to use, a count for
 *   each discrete option, or a min/max for continuous fields
 */
var getFieldAnalysis = (0, _reselect.createSelector)(_flattened_features2.default, function (state) {
  return state.fieldTypes;
}, function analyzeFields(features, fieldTypes) {
  var props;
  var feature;
  var keys;
  var i;
  var j;
  var value;
  var fieldname;
  var field;
  var geometryType;

  var analysis = {
    properties: {},
    $id: {},
    $type: {}

    // Iterate over every feature in the FeatureCollection
    // This is performance critical, so we use for loops instead of Array.reduce
  };for (i = 0; i < features.length; i++) {
    feature = features[i];
    props = feature.properties || {};
    keys = (0, _keys2.default)(props);
    for (j = 0; j < keys.length; j++) {
      value = props[keys[j]];
      fieldname = keys[j];
      field = analysis.properties[fieldname] = analysis.properties[fieldname] || { fieldname: fieldname };
      analyzeField(field, value, i);
    }
    analyzeField(analysis.$id, feature.id, i);
    geometryType = feature.geometry && feature.geometry.type;
    analyzeField(analysis.$type, geometryType, i);
  }

  for (fieldname in analysis.properties) {
    field = analysis.properties[fieldname];
    field.isUnique = isUnique(field, features.length);
    if (isUUIDField(field)) field.type = _constants.FIELD_TYPE_UUID;
    field.filterType = getFilterType(field);
    if (field.filterType === _constants.FILTER_TYPE_DISCRETE && field.count < features.length) {
      // Add count of undefined values
      field.values[_constants.UNDEFINED_KEY] = (field.values[_constants.UNDEFINED_KEY] || 0) + (features.length - field.count);
    }
    field.values = parseMapValues(field.values);
    field.type = fieldTypes[fieldname] || field.type;
  }

  analysis.$id.isUnique = isUnique(analysis.$id, features.length);
  analysis.$id.values = null;
  analysis.$type.filterType = getFilterType(analysis.$type);
  analysis.$type.values = parseMapValues(analysis.$type.values);

  return analysis;
});

exports.default = getFieldAnalysis;


var urlRegex = (0, _urlRegex2.default)({ exact: true });

// Max number of unique text values for a field to still be a filterable discrete field
var MAX_DISCRETE_VALUES = (_MAX_DISCRETE_VALUES = {}, (0, _defineProperty3.default)(_MAX_DISCRETE_VALUES, _constants.FIELD_TYPE_STRING, 100), (0, _defineProperty3.default)(_MAX_DISCRETE_VALUES, _constants.FIELD_TYPE_NUMBER, 5), _MAX_DISCRETE_VALUES);

var imageExts = ['jpg', 'tif', 'jpeg', 'png', 'tiff', 'webp'];
var videoExts = ['mov', 'mp4', 'avi', 'webm'];
var audioExts = ['3gpp', 'wav', 'wma', 'mp3', 'm4a', 'aiff', 'ogg'];
var mediaExts = imageExts.concat(videoExts, audioExts);

var types = {
  'string': _constants.FIELD_TYPE_STRING,
  'boolean': _constants.FIELD_TYPE_BOOLEAN,
  'number': _constants.FIELD_TYPE_NUMBER,
  'undefined': _constants.FIELD_TYPE_UNDEFINED
};

var isFilterable = (_isFilterable = {}, (0, _defineProperty3.default)(_isFilterable, _constants.FIELD_TYPE_DATE, true), (0, _defineProperty3.default)(_isFilterable, _constants.FIELD_TYPE_STRING, true), (0, _defineProperty3.default)(_isFilterable, _constants.FIELD_TYPE_NUMBER, true), (0, _defineProperty3.default)(_isFilterable, _constants.FIELD_TYPE_BOOLEAN, true), (0, _defineProperty3.default)(_isFilterable, _constants.FIELD_TYPE_ARRAY, true), (0, _defineProperty3.default)(_isFilterable, _constants.FIELD_TYPE_STRING_OR_ARRAY, true), (0, _defineProperty3.default)(_isFilterable, _constants.FIELD_TYPE_NUMBER_OR_ARRAY, true), _isFilterable);

var isMediaField = (_isMediaField = {}, (0, _defineProperty3.default)(_isMediaField, _constants.FIELD_TYPE_VIDEO, true), (0, _defineProperty3.default)(_isMediaField, _constants.FIELD_TYPE_IMAGE, true), (0, _defineProperty3.default)(_isMediaField, _constants.FIELD_TYPE_MEDIA, true), _isMediaField);

var isStringOrArray = (_isStringOrArray = {}, (0, _defineProperty3.default)(_isStringOrArray, _constants.FIELD_TYPE_STRING, true), (0, _defineProperty3.default)(_isStringOrArray, _constants.FIELD_TYPE_ARRAY, true), (0, _defineProperty3.default)(_isStringOrArray, _constants.FIELD_TYPE_STRING_OR_ARRAY, true), _isStringOrArray);

var isNumberOrArray = (_isNumberOrArray = {}, (0, _defineProperty3.default)(_isNumberOrArray, _constants.FIELD_TYPE_NUMBER, true), (0, _defineProperty3.default)(_isNumberOrArray, _constants.FIELD_TYPE_ARRAY, true), (0, _defineProperty3.default)(_isNumberOrArray, _constants.FIELD_TYPE_NUMBER_OR_ARRAY, true), _isNumberOrArray);

function analyzeField(field, value, i) {
  var type = getType(value);
  if (value) field.type = typeReduce(field.type, type);
  field.count = (field.count || 0) + 1;
  if (!isFilterable[field.type]) return;
  if (type === _constants.FIELD_TYPE_STRING) {
    field.wordStats = statReduce(field.wordStats, wc(value), i);
    field.lengthStats = statReduce(field.lengthStats, value.length, i);
  } else if (type === _constants.FIELD_TYPE_ARRAY) {
    field.maxArrayLength = Math.max(field.maxArrayLength || 1, value.length);
  } else if (type === _constants.FIELD_TYPE_NUMBER) {
    field.valueStats = statReduce(field.valueStats, value, i);
  } else if (type === _constants.FIELD_TYPE_DATE) {
    field.valueStats = statReduce(field.valueStats, (0, _filter_helpers.parseDate)(value), i);
  }
  field.values = valuesReduce(field.values, value);
}

function wc(s) {
  return s.split(' ').length;
}

/**
 * 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 Index of the current element being processed
 * @return {Object} New analysis including `x`
 */
function statReduce() {
  var p = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { mean: NaN, vari: NaN, min: +Infinity, max: -Infinity };
  var x = arguments[1];
  var i = arguments[2];

  p.mean = isNaN(p.mean) ? 0 : p.mean;
  var mean = p.mean + (x - p.mean) / (i + 1);
  x = x instanceof Date ? +x : x;
  return {
    mean: mean,
    min: x < p.min ? x : p.min,
    max: x > p.max ? x : p.max,
    vari: i < 1 ? 0 : (p.vari * i + (x - p.mean) * (x - mean)) / (i + 1)
  };
}

/**
 * Reducer that returns 'mixed' if values are not all the same,
 * or 'media' if field is a mixture of image and video files
 * @param {any} p Previous value
 * @param {any} v Current value
 * @return {any} `v` or `mixed`
 */
function typeReduce(p, v) {
  if (!p || v === p) return v;
  if (isMediaField[p] && isMediaField[v]) {
    return _constants.FIELD_TYPE_MEDIA;
  } else if (isMediaField[p] && (v === _constants.FIELD_TYPE_LINK || v === _constants.FIELD_TYPE_UNDEFINED || v === _constants.FIELD_TYPE_NULL || v === _constants.FIELD_TYPE_FILENAME)) {
    // If this contains media + links, assume the links are to the same type of media
    // media field might also have a string filename if image is lost
    return p;
  } else if (isMediaField[v] && (p === _constants.FIELD_TYPE_LINK || p === _constants.FIELD_TYPE_UNDEFINED || p === _constants.FIELD_TYPE_NULL || p === _constants.FIELD_TYPE_FILENAME)) {
    // If this contains media + links, assume the links are to the same type of media
    return v;
  } else if (p === _constants.FIELD_TYPE_LINK && (isMediaField[v] || p === _constants.FIELD_TYPE_UNDEFINED || p === _constants.FIELD_TYPE_NULL)) {
    return v;
  } else if (isStringOrArray[p] && isStringOrArray[v]) {
    return _constants.FIELD_TYPE_STRING_OR_ARRAY;
  } else if (isNumberOrArray[p] && isNumberOrArray[v]) {
    return _constants.FIELD_TYPE_NUMBER_OR_ARRAY;
  } else {
    return _constants.FIELD_TYPE_MIXED;
  }
}

/**
 * Reducer that returns the count of each value for a field
 * @param {Object} p Accumulator
 * @param {Any} v Any value
 * @return {Object} An object with a key for each value and the count of that value
 */
function valuesReduce() {
  var p = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  var v = arguments[1];

  v = Array.isArray(v) ? v : [v];
  v.forEach(function (w) {
    w = (0, _stringify2.default)(w);
    p[w] = typeof p[w] === 'undefined' ? 1 : p[w] + 1;
  });
  return p;
}

/**
 * Return the filter type for a field `f`.
 * @param {Object} f A field object with analysis props
 * @return {String} Filter type: date, range, discrete or text
 */
function getFilterType(f) {
  var keyCount = f.values && (0, _keys2.default)(f.values).length;
  if (!isFilterable[f.type]) return;
  switch (f.type) {
    case _constants.FIELD_TYPE_DATE:
      return _constants.FILTER_TYPE_DATE;
    case _constants.FIELD_TYPE_NUMBER:
      if (keyCount <= MAX_DISCRETE_VALUES[_constants.FIELD_TYPE_NUMBER]) {
        return _constants.FILTER_TYPE_DISCRETE;
      } else {
        return _constants.FILTER_TYPE_RANGE;
      }
    case _constants.FIELD_TYPE_BOOLEAN:
      return _constants.FILTER_TYPE_DISCRETE;
    case _constants.FIELD_TYPE_STRING:
      // Strings with lots of words we count as text fields, not discrete fields
      if (f.wordStats.mean > 3) {
        return _constants.FILTER_TYPE_TEXT;
      }
    // eslint-disable-next-line no-fallthrough
    case _constants.FIELD_TYPE_ARRAY:
    case _constants.FIELD_TYPE_STRING_OR_ARRAY:
    case _constants.FIELD_TYPE_NUMBER_OR_ARRAY:
      if (keyCount <= MAX_DISCRETE_VALUES[_constants.FIELD_TYPE_STRING]) {
        return _constants.FILTER_TYPE_DISCRETE;
      } else {
        return _constants.FILTER_TYPE_TEXT;
      }
  }
}

/**
 * Returns the type of a value, guessing types `date`, `link`, `image`, `video`
 * @param {any} v Value to be evaluated
 * @return {string} Field type
 */
function getType(v) {
  if (Array.isArray(v)) return _constants.FIELD_TYPE_ARRAY;
  if (v === null) return _constants.FIELD_TYPE_NULL;
  if (typeof v !== 'string') return types[typeof v === 'undefined' ? 'undefined' : (0, _typeof3.default)(v)];
  // isDate() is the most expensive test, so we do it as little as possible
  if ((0, _filter_helpers.isDate)(v)) return _constants.FIELD_TYPE_DATE;
  if (urlRegex.test(v)) {
    var pathname = _url2.default.parse(v).pathname;
    var _ext = _path2.default.extname(pathname).slice(1);
    if (imageExts.indexOf(_ext) > -1) return _constants.FIELD_TYPE_IMAGE;
    if (videoExts.indexOf(_ext) > -1) return _constants.FIELD_TYPE_VIDEO;
    if (audioExts.indexOf(_ext) > -1) return _constants.FIELD_TYPE_AUDIO;
    return _constants.FIELD_TYPE_LINK;
  }
  var ext = _path2.default.extname(v).slice(1);
  if (v.split('/').length === 1 && mediaExts.indexOf(ext) > -1) return _constants.FIELD_TYPE_FILENAME;
  return _constants.FIELD_TYPE_STRING;
}

/**
 * We initially process values as a map value -> count,
 * with the value encoded with JSON.stringify() so that
 * it is not coerced to a string when set as an object property.
 * This function parses those values back to their original
 * type and turns the values map into a sorted array
 */
function parseMapValues(values) {
  if (!values) return [];
  return (0, _keys2.default)(values).sort().map(function (v) {
    var parsed;
    if (v === _constants.UNDEFINED_KEY) {
      parsed = v;
    } else {
      try {
        parsed = JSON.parse(v);
      } catch (e) {
        console.error('problem parsing', v);
      }
    }
    return { value: parsed, count: values[v] };
  });
}

/**
 * Is a field unique?
 * @param {object} field        A field object with analysis props
 * @param {number} featureCount Total number of features
 * @return {Boolean}
 */
function isUnique(field, featureCount) {
  var valueCount = field.values && (0, _keys2.default)(field.values).length;
  return featureCount === valueCount;
}

/**
 * Guess if a field is a UUID: if it is unique, has a length greater than
 * 30, no variance in length, and is only one word.
 */
function isUUIDField(f) {
  if (!f.isUnique) return;
  if (f.type !== _constants.FIELD_TYPE_STRING) return;
  return f.lengthStats.mean > 30 && f.lengthStats.vari === 0 && f.wordStats.max === 1;
}
//# sourceMappingURL=field_analysis.js.map