Source: Filter.js

var _ = require('underscore'),
    SignalEmitter = require('./utility/SignalEmitter.js');

/**
 * TODO Filter is badly missing a correct documentation
 * @constructor
 */
var Filter = module.exports = function Filter(settings) {
  SignalEmitter.extend(this, Messages, 'Filter');

  this._settings = _.extend({
    //TODO: set default values
    streams: null, //ids
    tags: null,
    fromTime: null,  // serverTime
    toTime: null,  // serverTime
    limit: null,
    skip: null,
    types: null,
    modifiedSince: null,
    state: null
  }, settings);
};

var Messages = Filter.Messages = {
  /**
   * generic change event called on any change
   * content: {filter, signal, content}
   **/
  ON_CHANGE : 'changed',
  /**
   * called on streams changes
   * content: streams
   */
  STREAMS_CHANGE : 'streamsChanged',

  /**
   * called on streams structure changes
   * content: changes
   */
  STRUCTURE_CHANGE : 'structureChange',

  /*
   * called on date changes
   * content: streams
   */
  DATE_CHANGE : 'timeFrameChanged',

  /*
   * called on state changes
   * content: {state: value}
   */
  STATE_CHANGE : 'stateChanged'
};

// TODO
// redundant with get
function _normalizeTimeFrameST(filterData) {
  var result = [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY];
  if (filterData.fromTime || filterData.fromTime === 0) {
    result[0] = filterData.fromTime;
  }
  if (filterData.toTime || filterData.toTime === 0) {
    result[1] = filterData.toTime;
  }
  return result;
}



/**
 * TODO write doc
 * TODO complete with tags and state and modified and..
 * check if this event is in this filter
 */
Filter.prototype.matchEvent = function (event) {
  if (event.time > this.toTimeSTNormalized) { return false; }
  if (event.time < this.fromTimeSTNormalized) { return false; }


  if (this._settings.state !== 'all') {
    if (event.trashed) { return false; }
  }

  if (this._settings.streams) {

    if (this._settings.streams.length === 0) { return false; }

    if (this._settings.streams.indexOf(event.streamId) < 0) {
      var found = false;
      if (!event.stream) {
        return false;
      }
      event.stream.ancestors.forEach(function (ancestor) {
        if (this._settings.streams.indexOf(ancestor.id) >= 0) {
          if (this._settings.state !== 'all') {
            if (ancestor.trashed) { return false; }
          }
          found = true;
        }
      }.bind(this));
      if (!found) {
        return false;
      }
    }
  }



  // TODO complete test
  return true;
};

/**
 * Compare this filter with data form another filter
 * @param {Object} filterDataTest data got with filter.getData
 * @returns keymap \{ timeFrame : -1, 0 , 1 \}
 * (1 = more than test, -1 = less data than test, 0 == no changes)
 */
Filter.prototype.compareToFilterData = function (filterDataTest) {
  var result = { timeFrame : 0, streams : 0 };


  // timeFrame
  var myTimeFrameST = [this.fromTimeSTNormalized, this.toTimeSTNormalized];
  var testTimeFrameST = _normalizeTimeFrameST(filterDataTest);
  console.log(myTimeFrameST);
  console.log(testTimeFrameST);

  if (myTimeFrameST[0] < testTimeFrameST[0]) {
    result.timeFrame = 1;
  } else if (myTimeFrameST[0] > testTimeFrameST[0]) {
    result.timeFrame = -1;
  }
  if (result.timeFrame <= 0) {
    if (myTimeFrameST[1] > testTimeFrameST[1]) {
      result.timeFrame = 1;
    } else  if (myTimeFrameST[1] < testTimeFrameST[1]) {
      result.timeFrame = -1;
    }
  }

  // streams
  //TODO look if this processing can be optimized

  var nullStream = 0;
  if (! this._settings.streams) {
    if (filterDataTest.streams) {
      result.streams = 1;
    }
    nullStream = 1;
  }
  if (! filterDataTest.streams) {
    if (this._settings.streams) {
      result.streams = -1;
    }
    nullStream = 1;
  }

  if (! nullStream) {
    var notinTest = _.difference(this._settings.streams, filterDataTest.streams);
    if (notinTest.length > 0) {
      result.streams = 1;
    } else {
      var notinLocal = _.difference(filterDataTest.streams, this._settings.streams);
      if (notinLocal.length > 0) {
        result.streams = -1;
      }
    }
  }

  return result;
};

/**
 * Create a clone of this filter and changes some properties
 * @param properties
 * @returns pryv.Filter
 */
Filter.prototype.cloneWithDelta = function (properties) {
  var newProps = _.clone(this._settings);
  _.extend(newProps, properties);
  return new Filter(newProps);
};

/**
 * returns a dictionary containing all the settings of this filter.
 * @param ignoreNulls (optional) boolean
 * @param withDelta (optional) adds this differences on the data
 * @returns {*}
 */
Filter.prototype.getData = function (ignoreNulls, withDelta) {
  ignoreNulls = ignoreNulls || false;
  var result = _.clone(this._settings);
  if (withDelta)  {
    _.extend(result, withDelta);
  }
  if (ignoreNulls) {
    _.each(_.keys(result), function (key) {
      if ((result[key] === null)) {
        delete result[key];
      }
    });
  }
  return result;
};

/**
 * @private
 */
Filter.prototype._fireFilterChange = function (signal, content, batch) {
  // generic
  this._fireEvent(Messages.ON_CHANGE, {filter: this, signal: signal, content: content}, batch);
  // specific
  this._fireEvent(signal, content, batch);
};

/**
 * TODO review documentation and add example
 * Change several values of the filter in batch.. this wil group all events behind a batch id
 * @param keyValueMap {Object}
 * @param batch {SignalEmitter~Batch}
 */
Filter.prototype.set = function (keyValueMap, batch) {
  batch = this.startBatch('set', batch);

  _.each(keyValueMap, function (value, key) {
    this._setValue(key, value, batch);
  }.bind(this));

  batch.done('set');
};

/**
 * Internal that take in charge of changing values
 * @param keyValueMap
 * @param batch
 * @private
 */
Filter.prototype._setValue = function (key, newValue, batch) {
  batch = this.startBatch('setValue:' + key, batch);

  if (key === 'limit') {
    this._settings.limit = newValue;

    // TODO handle changes
    return;
  }


  if (key === 'state') {
    if (this._settings.state !== newValue) {
      this._settings.state = newValue;
      this._fireFilterChange(Messages.STATE_CHANGE, {state: newValue}, batch);
    }
    batch.done('setValue:' + key);
    return;
  }

  if (key === 'timeFrameST') {
    if (! _.isArray(newValue) || newValue.length !== 2) {
      throw new Error('Filter.timeFrameST is an Array of two timestamps [fromTime, toTime]');
    }
    if (this._settings.fromTime !== newValue[0] || this._settings.toTime !== newValue[1]) {
      this._settings.fromTime = newValue[0];
      this._settings.toTime = newValue[1];
      this._fireFilterChange(Messages.DATE_CHANGE, this.timeFrameST, batch);
    }
    batch.done('setValue:' + key);
    return;
  }

  if (key === 'streamsIds') {

    if (newValue === null || typeof newValue === 'undefined') {
      if (this._settings.streams === null) {

        return;
      }
      newValue = null;
    } else if (! _.isArray(newValue)) {
      newValue = [newValue];
    }

    // TODO check that this stream is valid
    this._settings.streams = newValue;
    this._fireFilterChange(Messages.STREAMS_CHANGE, this.streams, batch);
    batch.done('setValue:' + key);
    return;
  }

  batch.done('setValue:' + key);
  throw new Error('Filter has no property : ' + key);
};

/**
 * get toTime, return Number.POSITIVE_INFINITY if null
 */
Object.defineProperty(Filter.prototype, 'toTimeSTNormalized', {
  get: function () {
    if (this._settings.toTime || this._settings.toTime === 0) {
      return this._settings.toTime;
    }
    return Number.POSITIVE_INFINITY;
  }
});

/**
 * get fromTime, return Number.POSITIVE_INFINITY if null
 */
Object.defineProperty(Filter.prototype, 'fromTimeSTNormalized', {
  get: function () {
    if (this._settings.fromTime || this._settings.fromTime === 0) {
      return this._settings.fromTime;
    }
    return Number.NEGATIVE_INFINITY;
  }
});



/**
 * timeFrameChange ..  [fromTime, toTime]
 * setting them to "null" => ALL
 */
Object.defineProperty(Filter.prototype, 'timeFrameST', {
  get: function () {
    return [this._settings.toTime, this._settings.fromTime];
  },
  set: function (newValue) {
    this._setValue('timeFrameST', newValue);
    return this.timeFrameST;
  }
});


/**
 * StreamIds ..
 * setting them to "null" => ALL and to "[]" => NONE
 */
Object.defineProperty(Filter.prototype, 'streamsIds', {
  get: function () {
    return this._settings.streams;
  },
  set: function (newValue) {
    this._setValue('streamsIds', newValue);
    return this._settings.streams;
  }
});


/**
 * return true if context (stream is on a single stream)
 * This is usefull to check when creating and event in a context.
 * This way, no need to ask the user for a stream specification.
 * TODO determine if this should stay in the lib.. or handle by apps
 */
Filter.prototype.focusedOnSingleStream = function () {
  if (_.isArray(this._settings.streams) && this._settings.streams.length === 1) {
    return this._settings.streams[0];
  }
  return null;
};

/**
 * An pryv Filter or an object corresponding at what we can get with Filter.getData().
 * @typedef {(Filter|Object)} FilterLike
 * @property {String[]} [streams]
 * @property {String[]} [tags]
 * @property {number} [fromTime] -- serverTime
 * @property {number} [toTime] -- serverTime
 * @property {number} [modifiedSince] -- serverTime
 * @property {number} [limit] -- response to 'n' events
 * @property {number} [skip] -- skip the first 'n' events of he response
 */