// Generated by CoffeeScript 1.10.0
(function() {
  var OLAPCube, Time, Timeline, TransitionsCalculator, ref, utils;

  OLAPCube = require('./OLAPCube').OLAPCube;

  ref = require('tztime'), utils = ref.utils, Time = ref.Time, Timeline = ref.Timeline;

  TransitionsCalculator = (function() {

    /*
    @class TransitionsCalculator
    
    Used to accumlate counts and sums about transitions.
    
    Let's say that you want to create a throughput or velocity chart where each column on the chart represents the
    number of work items that cross over from one state into another state in a given month/week/quarter/etc. You would
    send a transitions to a temporal data store like Rally's Lookback API specifying both the current values and the
    previous values. For instance, the work items crossing from "In Progress" to "Completed" could be found
    with this query clause `"_PreviousValues.ScheduleState": {"$lte": "In Progress"}, "ScheduleState": {"$gt": "In Progress"}`
    
        {TransitionsCalculator, Time} = require('../')
    
        snapshots = [
          { id: 1, from: '2011-01-03T00:00:00.000Z', PlanEstimate: 10 },
          { id: 1, from: '2011-01-05T00:00:00.000Z', PlanEstimate: 10 },
          { id: 2, from: '2011-01-04T00:00:00.000Z', PlanEstimate: 20 },
          { id: 3, from: '2011-01-10T00:00:00.000Z', PlanEstimate: 30 },
          { id: 4, from: '2011-01-11T00:00:00.000Z', PlanEstimate: 40 },
          { id: 5, from: '2011-01-17T00:00:00.000Z', PlanEstimate: 50 },
          { id: 6, from: '2011-02-07T00:00:00.000Z', PlanEstimate: 60 },
          { id: 7, from: '2011-02-08T00:00:00.000Z', PlanEstimate: 70 },
        ]
    
    But that's not the entire story. What if something crosses over into "Completed" and beyond but crosses back. It could
    do this several times and get counted multiple times. That would be bad. The way we deal with this is to also
    look for the list of snapshots that pass backwards across the boundary and subract thier impact on the final calculations.
    
    One can think of alternative aproaches for avoiding this double counting. You could, for instance, only count the last
    transition for each unique work item. The problem with this approach is that the backward moving transition might
    occur in a different time period from the forward moving one. A later snapshot could invalidate an earlier calculation
    which is bad for incremental calculation and caching. To complicate matters, the field values being summed by the
    calculator might have changed between subsequent forward/backward transitions. The chosen algorithm is the only way I know to
    preserve the idempotency and cachable incremental calculation properties.
    
        snapshotsToSubtract = [
          { id: 1, from: '2011-01-04T00:00:00.000Z', PlanEstimate: 10 },
          { id: 7, from: '2011-02-09T00:00:00.000Z', PlanEstimate: 70 },
        ]
    
    The calculator will keep track of the count of items automatically (think throughput), but if you want to sum up a
    particular field (think velocity), you can specify that with the 'fieldsToSum' config property.
    
        fieldsToSum = ['PlanEstimate']
    
    Now let's build our config object.
    
        config =
          asOf: '2011-02-10'  # Leave this off if you want it to continuously update to today
          granularity: Time.MONTH
          tz: 'America/Chicago'
          validFromField: 'from'
          validToField: 'to'
          uniqueIDField: 'id'
          fieldsToSum: fieldsToSum
          asterixToDateTimePeriod: true  # Set to false or leave off if you are going to reformat the timePeriod
    
    In most cases, you'll want to leave off the `asOf` configuration property so the data can be continuously updated
    with new snapshots as they come in. We include it in this example so the output stays stable. If we hadn't, then
    the rows would continue to grow to encompass today.
    
        startOn = '2011-01-02T00:00:00.000Z'
        endBefore = '2011-02-27T00:00:00.000Z'
    
        calculator = new TransitionsCalculator(config)
        calculator.addSnapshots(snapshots, startOn, endBefore, snapshotsToSubtract)
    
        console.log(calculator.getResults())
         * [ { timePeriod: '2011-01', count: 5, PlanEstimate: 150 },
         *   { timePeriod: '2011-02*', count: 1, PlanEstimate: 60 } ]
    
    The asterix on the last row in the results is to indicate that it is a to-date value. As more snapshots come in, this
    last row will change. The caching and incremental calcuation capability of this Calculator are designed to take
    this into account.
    
    Now, let's use the same data but aggregate in granularity of weeks.
    
        config.granularity = Time.WEEK
        calculator = new TransitionsCalculator(config)
        calculator.addSnapshots(snapshots, startOn, endBefore, snapshotsToSubtract)
    
        console.log(calculator.getResults())
         * [ { timePeriod: '2010W52', count: 1, PlanEstimate: 10 },
         *   { timePeriod: '2011W01', count: 2, PlanEstimate: 50 },
         *   { timePeriod: '2011W02', count: 2, PlanEstimate: 90 },
         *   { timePeriod: '2011W03', count: 0, PlanEstimate: 0 },
         *   { timePeriod: '2011W04', count: 0, PlanEstimate: 0 },
         *   { timePeriod: '2011W05', count: 1, PlanEstimate: 60 },
         *   { timePeriod: '2011W06*', count: 0, PlanEstimate: 0 } ]
    
    Remember, you can easily convert weeks to other granularities for display.
    
        weekStartingLabel = 'week starting ' + new Time('2010W52').inGranularity(Time.DAY).toString()
        console.log(weekStartingLabel)
         * week starting 2010-12-27
    
    If you want to display spinners while the chart is rendering, you can read this calculator's upToDateISOString property and
    compare it directly to the getResults() row's timePeriod property using code like this. Yes, this works eventhough
    upToDateISOString is an ISOString.
    
        row = {timePeriod: '2011W07'}
        if calculator.upToDateISOString < row.timePeriod
          console.log("#{row.timePeriod} not yet calculated.")
         * 2011W07 not yet calculated.
     */
    function TransitionsCalculator(config) {

      /*
      @constructor
      @param {Object} config
      @cfg {String} tz The timezone for analysis in the form like `America/New_York`
      @cfg {String} [validFromField = "_ValidFrom"]
      @cfg {String} [validToField = "_ValidTo"]
      @cfg {String} [uniqueIDField = "_EntityID"] Not used right now but when drill-down is added it will be
      @cfg {String} granularity 'month', 'week', 'quarter', etc. Use Time.MONTH, Time.WEEK, etc.
      @cfg {String[]} [fieldsToSum=[]] It will track the count automatically but it can keep a running sum of other fields also
      @cfg {Boolean} [asterixToDateTimePeriod=false] If set to true, then the still-in-progress last time period will be asterixed
       */
      var cubeConfig, dimensions, f, i, len, metrics, ref1, ref2;
      this.config = utils.clone(config);
      if (this.config.validFromField == null) {
        this.config.validFromField = "_ValidFrom";
      }
      if (this.config.validToField == null) {
        this.config.validToField = "_ValidTo";
      }
      if (this.config.uniqueIDField == null) {
        this.config.uniqueIDField = "_EntityID";
      }
      if (this.config.fieldsToSum == null) {
        this.config.fieldsToSum = [];
      }
      if (this.config.asterixToDateTimePeriod == null) {
        this.config.asterixToDateTimePeriod = false;
      }
      utils.assert(this.config.tz != null, "Must provide a timezone to this calculator.");
      utils.assert(this.config.granularity != null, "Must provide a granularity to this calculator.");
      if ((ref1 = this.config.granularity) === Time.HOUR || ref1 === Time.MINUTE || ref1 === Time.SECOND || ref1 === Time.MILLISECOND) {
        throw new Error("Transitions calculator is not designed to work on granularities finer than 'day'");
      }
      dimensions = [
        {
          field: 'timePeriod'
        }
      ];
      metrics = [
        {
          field: 'count',
          f: 'sum'
        }
      ];
      ref2 = this.config.fieldsToSum;
      for (i = 0, len = ref2.length; i < len; i++) {
        f = ref2[i];
        metrics.push({
          field: f,
          f: 'sum'
        });
      }
      cubeConfig = {
        dimensions: dimensions,
        metrics: metrics
      };
      this.cube = new OLAPCube(cubeConfig);
      this.upToDateISOString = null;
      this.lowestTimePeriod = null;
      if (this.config.asOf != null) {
        this.maxTimeString = new Time(this.config.asOf, Time.MILLISECOND).getISOStringInTZ(this.config.tz);
      } else {
        this.maxTimeString = Time.getISOStringFromJSDate();
      }
      this.virgin = true;
    }

    TransitionsCalculator.prototype.addSnapshots = function(snapshots, startOn, endBefore, snapshotsToSubtract) {
      var filteredSnapshots, filteredSnapshotsToSubstract, startOnString;
      if (snapshotsToSubtract == null) {
        snapshotsToSubtract = [];
      }

      /*
      @method addSnapshots
        Allows you to incrementally add snapshots to this calculator.
      @chainable
      @param {Object[]} snapshots An array of temporal data model snapshots.
      @param {String} startOn A ISOString (e.g. '2012-01-01T12:34:56.789Z') indicating the time start of the period of
        interest. On the second through nth call, this should equal the previous endBefore.
      @param {String} endBefore A ISOString (e.g. '2012-01-01T12:34:56.789Z') indicating the moment just past the time
        period of interest.
      @return {TransitionsCalculator}
       */
      if (this.upToDateISOString != null) {
        utils.assert(this.upToDateISOString === startOn, "startOn (" + startOn + ") parameter should equal endBefore of previous call (" + this.upToDateISOString + ") to addSnapshots.");
      }
      this.upToDateISOString = endBefore;
      startOnString = new Time(startOn, this.config.granularity, this.config.tz).toString();
      if (this.lowestTimePeriod != null) {
        if (startOnString < this.lowestTimePeriod) {
          this.lowestTimePeriod = startOnString;
        }
      } else {
        this.lowestTimePeriod = startOnString;
      }
      filteredSnapshots = this._filterSnapshots(snapshots);
      this.cube.addFacts(filteredSnapshots);
      filteredSnapshotsToSubstract = this._filterSnapshots(snapshotsToSubtract, -1);
      this.cube.addFacts(filteredSnapshotsToSubstract);
      this.virgin = false;
      return this;
    };

    TransitionsCalculator.prototype._filterSnapshots = function(snapshots, sign) {
      var f, filteredSnapshots, fs, i, j, len, len1, ref1, s;
      if (sign == null) {
        sign = 1;
      }
      filteredSnapshots = [];
      for (i = 0, len = snapshots.length; i < len; i++) {
        s = snapshots[i];
        if (s[this.config.validFromField] <= this.maxTimeString) {
          if (s.count != null) {
            throw new Error('Snapshots passed into a TransitionsCalculator cannot have a `count` field.');
          }
          if (s.timePeriod != null) {
            throw new Error('Snapshots passed into a TransitionsCalculator cannot have a `timePeriod` field.');
          }
          fs = utils.clone(s);
          fs.count = sign * 1;
          fs.timePeriod = new Time(s[this.config.validFromField], this.config.granularity, this.config.tz).toString();
          ref1 = this.config.fieldsToSum;
          for (j = 0, len1 = ref1.length; j < len1; j++) {
            f = ref1[j];
            fs[f] = sign * s[f];
          }
          filteredSnapshots.push(fs);
        }
      }
      return filteredSnapshots;
    };

    TransitionsCalculator.prototype.getResults = function() {

      /*
      @method getResults
        Returns the current state of the calculator
      @return {Object[]} Returns an Array of Maps like `{timePeriod: '2012-12', count: 10, otherField: 34}`
       */
      var cell, config, f, filter, i, j, k, len, len1, len2, out, outRow, ref1, ref2, t, timeLine, timePeriods, tp;
      if (this.virgin) {
        return [];
      }
      out = [];
      this.highestTimePeriod = new Time(this.maxTimeString, this.config.granularity, this.config.tz).toString();
      config = {
        startOn: this.lowestTimePeriod,
        endBefore: this.highestTimePeriod,
        granularity: this.config.granularity
      };
      timeLine = new Timeline(config);
      timePeriods = (function() {
        var i, len, ref1, results;
        ref1 = timeLine.getAllRaw();
        results = [];
        for (i = 0, len = ref1.length; i < len; i++) {
          t = ref1[i];
          results.push(t.toString());
        }
        return results;
      })();
      timePeriods.push(this.highestTimePeriod);
      for (i = 0, len = timePeriods.length; i < len; i++) {
        tp = timePeriods[i];
        filter = {};
        filter['timePeriod'] = tp;
        cell = this.cube.getCell(filter);
        outRow = {};
        outRow.timePeriod = tp;
        if (cell != null) {
          outRow.count = cell.count_sum;
          ref1 = this.config.fieldsToSum;
          for (j = 0, len1 = ref1.length; j < len1; j++) {
            f = ref1[j];
            outRow[f] = cell[f + '_sum'];
          }
        } else {
          outRow.count = 0;
          ref2 = this.config.fieldsToSum;
          for (k = 0, len2 = ref2.length; k < len2; k++) {
            f = ref2[k];
            outRow[f] = 0;
          }
        }
        out.push(outRow);
      }
      if (this.config.asterixToDateTimePeriod) {
        out[out.length - 1].timePeriod += '*';
      }
      return out;
    };

    TransitionsCalculator.prototype.getStateForSaving = function(meta) {

      /*
      @method getStateForSaving
        Enables saving the state of this calculator. See TimeInStateCalculator documentation for a detailed example.
      @param {Object} [meta] An optional parameter that will be added to the serialized output and added to the meta field
        within the deserialized calculator.
      @return {Object} Returns an Ojbect representing the state of the calculator. This Object is suitable for saving to
        to an object store. Use the static method `newFromSavedState()` with this Object as the parameter to reconstitute
        the calculator.
       */
      var out;
      out = {
        config: this.config,
        cubeSavedState: this.cube.getStateForSaving(),
        upToDateISOString: this.upToDateISOString,
        maxTimeString: this.maxTimeString,
        lowestTimePeriod: this.lowestTimePeriod,
        virgin: this.virgin
      };
      if (meta != null) {
        out.meta = meta;
      }
      return out;
    };

    TransitionsCalculator.newFromSavedState = function(p) {

      /*
      @method newFromSavedState
        Deserializes a previously saved calculator and returns a new calculator. See TimeInStateCalculator for a detailed example.
      @static
      @param {String/Object} p A String or Object from a previously saved state
      @return {TransitionsCalculator}
       */
      var calculator;
      if (utils.type(p) === 'string') {
        p = JSON.parse(p);
      }
      calculator = new TransitionsCalculator(p.config);
      calculator.cube = OLAPCube.newFromSavedState(p.cubeSavedState);
      calculator.upToDateISOString = p.upToDateISOString;
      calculator.maxTimeString = p.maxTimeString;
      calculator.lowestTimePeriod = p.lowestTimePeriod;
      calculator.virgin = p.virgin;
      if (p.meta != null) {
        calculator.meta = p.meta;
      }
      return calculator;
    };

    return TransitionsCalculator;

  })();

  exports.TransitionsCalculator = TransitionsCalculator;

}).call(this);
