text: /*****************************************************************************
   * Open MCT Web, Copyright (c) 2014-2015, United States Government
   * as represented by the Administrator of the National Aeronautics and Space
   * Administration. All rights reserved.
   *
   * Open MCT Web is licensed under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   * http://www.apache.org/licenses/LICENSE-2.0.
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   * License for the specific language governing permissions and limitations
   * under the License.
   *
   * Open MCT Web includes source code licensed under additional open source
   * licenses. See the Open Source Licenses file (LICENSES.md) included with
   * this source code distribution or the Licensing information page available
   * at runtime from the About dialog for additional information.
   *****************************************************************************/
  
  define(
      ['./TelemetryQueue', './TelemetryTable', './TelemetryDelegator'],
      function (TelemetryQueue, TelemetryTable, TelemetryDelegator) {
  
          /**
           * A pool of telemetry values.
           * @interface platform/telemetry.TelemetryPool
           * @private
           */
          /**
           * Check if any value groups remain in this pool.
           * @return {boolean} true if value groups remain
           * @method platform/telemetry.TelemetryPool#isEmpty
           */
          /**
           * Retrieve the next value group from this pool.
           * This gives an object containing key-value pairs,
           * where keys and values correspond to the arguments
           * given to previous put functions.
           * @return {object} key-value pairs
           * @method platform/telemetry.TelemetryPool#poll
           */
          /**
           * Put a key-value pair into the pool.
           * @param {string} key the key to store the value under
           * @param {*} value the value to store
           * @method platform/telemetry.TelemetryPool#put
           */
  
  
          /**
           * A TelemetrySubscription tracks latest values for streaming
           * telemetry data and handles notifying interested observers.
           * It implements the interesting behavior behind the
           * `telemetrySubscriber` service.
           *
           * Subscriptions may also be made directly using the
           * `telemetry` capability of a domain objcet; the subscriber
           * uses this as well, but additionally handles delegation
           * (e.g. for telemetry panels) as well as latest-value
           * extraction.
           *
           * @memberof platform/telemetry
           * @constructor
           * @param $q Angular's $q
           * @param $timeout Angular's $timeout
           * @param {DomainObject} domainObject the object whose
           *        associated telemetry data is of interest
           * @param {Function} callback a function to invoke
           *        when new data has become available.
           * @param {boolean} lossless true if callback should be invoked
           *        once with every data point available; otherwise, multiple
           *        data events in a short period of time will only invoke
           *        the callback once, with access to the latest data
           */
          function TelemetrySubscription($q, $timeout, domainObject, callback, lossless) {
              var self = this,
                  delegator = new TelemetryDelegator($q),
                  pool = lossless ? new TelemetryQueue() : new TelemetryTable(),
                  updatePending;
  
              // Look up domain objects which have telemetry capabilities.
              // This will either be the object in view, or object that
              // this object delegates its telemetry capability to.
              function promiseRelevantObjects(domainObject) {
                  return delegator.promiseTelemetryObjects(domainObject);
              }
  
              function updateValuesFromPool() {
                  var values = pool.poll();
                  Object.keys(values).forEach(function (k) {
                      self.latestValues[k] = values[k];
                  });
              }
  
              // Invoke the observer callback to notify that new streaming
              // data has become available.
              function fireCallback() {
                  // Play back from queue if we are lossless
                  while (!pool.isEmpty()) {
                      updateValuesFromPool();
                      // Fire callback, if one was provided
                      if (callback) {
                          callback();
                      }
                  }
  
                  // Clear the pending flag so that future updates will
                  // schedule this callback.
                  updatePending = false;
              }
  
  
              // Look up metadata associated with an object's telemetry
              function lookupMetadata(domainObject) {
                  var telemetryCapability =
                      domainObject.getCapability("telemetry");
                  return telemetryCapability &&
                          telemetryCapability.getMetadata();
              }
  
              // Update the latest telemetry data for a specific
              // domain object. This will notify listeners.
              function update(domainObject, series) {
                  var count = series && series.getPointCount();
  
                  // Only schedule notification if there isn't already
                  // a notification pending (and if we actually have
                  // data)
                  if (!updatePending && count) {
                      updatePending = true;
                      $timeout(fireCallback, 0);
                  }
  
                  // Update the latest-value table
                  if (count > 0) {
                      pool.put(domainObject.getId(), {
                          domain: series.getDomainValue(count - 1),
                          range: series.getRangeValue(count - 1),
                          datum: self.makeDatum(domainObject, series, count - 1)
                      });
                  }
              }
  
              // Prepare a subscription to a specific telemetry-providing
              // domain object.
              function subscribe(domainObject) {
                  var telemetryCapability =
                      domainObject.getCapability("telemetry");
                  return telemetryCapability.subscribe(function (telemetry) {
                      update(domainObject, telemetry);
                  });
              }
  
              // Prepare subscriptions to all relevant telemetry-providing
              // domain objects.
              function subscribeAll(domainObjects) {
                  return domainObjects.map(subscribe);
              }
  
              // Cache a reference to all relevant telemetry-providing
              // domain objects. This will be called during the
              // initial subscription chain; this allows `getTelemetryObjects()`
              // to return a non-Promise to simplify usage elsewhere.
              function cacheObjectReferences(objects) {
                  self.telemetryObjects = objects;
                  self.metadatas = objects.map(lookupMetadata);
  
                  self.metadataById = {};
                  objects.forEach(function (obj, i) {
                      self.metadataById[obj.getId()] = self.metadatas[i];
                  });
                  // Fire callback, as this will be the first time that
                  // telemetry objects are available, or these objects
                  // will have changed.
                  if (callback) {
                      callback();
                  }
                  return objects;
              }
  
              function initialize() {
                  // Get a reference to relevant objects (those with telemetry
                  // capabilities) and subscribe to their telemetry updates.
                  // Keep a reference to their promised return values, as these
                  // will be unsubscribe functions. (This must be a promise
                  // because delegation is supported, and retrieving delegate
                  // telemetry-capable objects may be an asynchronous operation.)
                  self.telemetryObjectPromise = promiseRelevantObjects(domainObject);
                  self.unsubscribePromise = self.telemetryObjectPromise
                          .then(cacheObjectReferences)
                          .then(subscribeAll);
              }
  
              function idsMatch(ids) {
                  return ids.length === self.telemetryObjects.length &&
                      ids.every(function (id, index) {
                          return self.telemetryObjects[index].getId() === id;
                      });
              }
  
              function modelChange(model) {
                  if (!idsMatch((model || {}).composition || [])) {
                      // Reinitialize if composition has changed
                      self.unsubscribeAll().then(initialize);
                  }
              }
  
              function addMutationListener() {
                  var mutation = domainObject &&
                      domainObject.getCapability('mutation');
                  if (mutation) {
                      return mutation.listen(modelChange);
                  }
              }
  
              this.$q = $q;
              this.latestValues = {};
              this.telemetryObjects = [];
              this.metadatas = [];
  
              initialize();
              this.unlistenToMutation = addMutationListener();
          }
  
  
          /**
           * From a telemetry series, retrieve a single data point
           * containing all fields for domains/ranges
           * @private
           */
          TelemetrySubscription.prototype.makeDatum = function (domainObject, series, index) {
              var id = domainObject && domainObject.getId(),
                  metadata = (id && this.metadataById[id]) || {},
                  result = {};
  
              (metadata.domains || []).forEach(function (domain) {
                  result[domain.key] =
                      series.getDomainValue(index, domain.key);
              });
  
              (metadata.ranges || []).forEach(function (range) {
                  result[range.key] =
                      series.getRangeValue(index, range.key);
              });
  
              return result;
          };
  
          /**
           * Terminate all underlying subscriptions.
           * @private
           */
          TelemetrySubscription.prototype.unsubscribeAll = function () {
              var $q = this.$q;
              return this.unsubscribePromise.then(function (unsubscribes) {
                  return $q.all(unsubscribes.map(function (unsubscribe) {
                      return unsubscribe();
                  }));
              });
          };
  
          /**
           * Terminate all underlying subscriptions associated
           * with this object.
           */
          TelemetrySubscription.prototype.unsubscribe = function () {
              if (this.unlistenToMutation) {
                  this.unlistenToMutation();
              }
              return this.unsubscribeAll();
          };
  
          /**
           * Get the most recent domain value that has been observed
           * for the specified domain object. This will typically be
           * a timestamp.
           *
           * The domain object passed here should be one that is
           * subscribed-to here; that is, it should be one of the
           * domain objects returned by `getTelemetryObjects()`.
           *
           * @param {DomainObject} domainObject the object of interest
           * @param {string} [key] the symbolic identifier of the domain
           *        to look up; if omitted, the value for this object's
           *        default domain will be used
           * @returns the most recent domain value observed
           */
          TelemetrySubscription.prototype.getDomainValue = function (domainObject, key) {
              var id = domainObject.getId(),
                  latestValue = this.latestValues[id];
              return latestValue && (key ?
                                     latestValue.datum[key] :
                                     latestValue.domain);
          };
  
          /**
           * Get the most recent range value that has been observed
           * for the specified domain object. This will typically
           * be a numeric measurement.
           *
           * The domain object passed here should be one that is
           * subscribed-to here; that is, it should be one of the
           * domain objects returned by `getTelemetryObjects()`.
           *
           * @param {DomainObject} domainObject the object of interest
           * @param {string} [key] the symbolic identifier of the range
           *        to look up; if omitted, the value for this object's
           *        default range will be used
           * @returns the most recent range value observed
           */
          TelemetrySubscription.prototype.getRangeValue = function (domainObject, key) {
              var id = domainObject.getId(),
                  latestValue = this.latestValues[id];
              return latestValue && (key ?
                                     latestValue.datum[key] :
                                     latestValue.range);
          };
  
          /**
           * Get the latest telemetry datum for this domain object.
           *
           * @param {DomainObject} domainObject the object of interest
           * @returns {TelemetryDatum} the most recent datum
           */
          TelemetrySubscription.prototype.getDatum = function (domainObject) {
              var id = domainObject.getId();
              return (this.latestValues[id] || {}).datum;
          };
  
          /**
           * Get all telemetry-providing domain objects which are
           * being observed as part of this subscription.
           *
           * Capability delegation will be taken into account (so, if
           * a Telemetry Panel was passed in the constructor, this will
           * return its contents.) Capability delegation is resolved
           * asynchronously so the return value here may change over
           * time; while this resolution is pending, this method will
           * return an empty array.
           *
           * @returns {DomainObject[]} all subscribed-to domain objects
           */
          TelemetrySubscription.prototype.getTelemetryObjects = function () {
              return this.telemetryObjects;
          };
  
          /**
           * Get all telemetry metadata associated with
           * telemetry-providing domain objects managed by
           * this controller.
           *
           * This will ordered in the
           * same manner as `getTelemetryObjects()` or
           * `getResponse()`; that is, the metadata at a
           * given index will correspond to the telemetry-providing
           * domain object at the same index.
           * @returns {TelemetryMetadata[]} an array of metadata objects
           */
          TelemetrySubscription.prototype.getMetadata = function () {
              return this.metadatas;
          };
  
          /**
           * Get a promise for all telemetry-providing objects
           * associated with this subscription.
           * @returns {Promise.<DomainObject[]>} a promise for
           *          telemetry-providing objects
           * @memberof platform/telemetry.TelemetrySubscription#
           */
          TelemetrySubscription.prototype.promiseTelemetryObjects = function () {
              // Unsubscribe promise is available after objects
              // are loaded.
              return this.telemetryObjectPromise;
          };
  
          return TelemetrySubscription;
  
      }
  );
  
  