text: /*****************************************************************************
   * Open MCT, Copyright (c) 2014-2022, United States Government
   * as represented by the Administrator of the National Aeronautics and Space
   * Administration. All rights reserved.
   *
   * Open openmct 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 openmct 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.
   *****************************************************************************/
  
  const { TelemetryCollection } = require("./TelemetryCollection");
  
  define([
      '../../plugins/displayLayout/CustomStringFormatter',
      './TelemetryMetadataManager',
      './TelemetryValueFormatter',
      './DefaultMetadataProvider',
      'objectUtils',
      'lodash'
  ], function (
      CustomStringFormatter,
      TelemetryMetadataManager,
      TelemetryValueFormatter,
      DefaultMetadataProvider,
      objectUtils,
      _
  ) {
      /**
       * A LimitEvaluator may be used to detect when telemetry values
       * have exceeded nominal conditions.
       *
       * @interface LimitEvaluator
       * @memberof module:openmct.TelemetryAPI~
       */
  
      /**
       * Check for any limit violations associated with a telemetry datum.
       * @method evaluate
       * @param {*} datum the telemetry datum to evaluate
       * @param {TelemetryProperty} the property to check for limit violations
       * @memberof module:openmct.TelemetryAPI~LimitEvaluator
       * @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about
       *          the limit violation, or undefined if a value is within limits
       */
  
      /**
       * A violation of limits defined for a telemetry property.
       * @typedef LimitViolation
       * @memberof {module:openmct.TelemetryAPI~}
       * @property {string} cssClass the class (or space-separated classes) to
       *           apply to display elements for values which violate this limit
       * @property {string} name the human-readable name for the limit violation
       */
  
      /**
       * A TelemetryFormatter converts telemetry values for purposes of
       * display as text.
       *
       * @interface TelemetryFormatter
       * @memberof module:openmct.TelemetryAPI~
       */
  
      /**
       * Retrieve the 'key' from the datum and format it accordingly to
       * telemetry metadata in domain object.
       *
       * @method format
       * @memberof module:openmct.TelemetryAPI~TelemetryFormatter#
       */
  
      /**
       * Describes a property which would be found in a datum of telemetry
       * associated with a particular domain object.
       *
       * @typedef TelemetryProperty
       * @memberof module:openmct.TelemetryAPI~
       * @property {string} key the name of the property in the datum which
       *           contains this telemetry value
       * @property {string} name the human-readable name for this property
       * @property {string} [units] the units associated with this property
       * @property {boolean} [temporal] true if this property is a timestamp, or
       *           may be otherwise used to order telemetry in a time-like
       *           fashion; default is false
       * @property {boolean} [numeric] true if the values for this property
       *           can be interpreted plainly as numbers; default is true
       * @property {boolean} [enumerated] true if this property may have only
       *           certain specific values; default is false
       * @property {string} [values] for enumerated states, an ordered list
       *           of possible values
       */
  
      /**
       * Describes and bounds requests for telemetry data.
       *
       * @typedef TelemetryRequest
       * @memberof module:openmct.TelemetryAPI~
       * @property {string} sort the key of the property to sort by. This may
       *           be prefixed with a "+" or a "-" sign to sort in ascending
       *           or descending order respectively. If no prefix is present,
       *           ascending order will be used.
       * @property {*} start the lower bound for values of the sorting property
       * @property {*} end the upper bound for values of the sorting property
       * @property {string[]} strategies symbolic identifiers for strategies
       *           (such as `minmax`) which may be recognized by providers;
       *           these will be tried in order until an appropriate provider
       *           is found
       */
  
      /**
       * Provides telemetry data. To connect to new data sources, new
       * TelemetryProvider implementations should be
       * [registered]{@link module:openmct.TelemetryAPI#addProvider}.
       *
       * @interface TelemetryProvider
       * @memberof module:openmct.TelemetryAPI~
       */
  
      /**
       * An interface for retrieving telemetry data associated with a domain
       * object.
       *
       * @interface TelemetryAPI
       * @augments module:openmct.TelemetryAPI~TelemetryProvider
       * @memberof module:openmct
       */
      function TelemetryAPI(openmct) {
          this.openmct = openmct;
  
          this.formatMapCache = new WeakMap();
          this.formatters = new Map();
          this.limitProviders = [];
          this.metadataCache = new WeakMap();
          this.metadataProviders = [new DefaultMetadataProvider(this.openmct)];
          this.noRequestProviderForAllObjects = false;
          this.requestAbortControllers = new Set();
          this.requestProviders = [];
          this.subscriptionProviders = [];
          this.valueFormatterCache = new WeakMap();
      }
  
      TelemetryAPI.prototype.abortAllRequests = function () {
          this.requestAbortControllers.forEach((controller) => controller.abort());
          this.requestAbortControllers.clear();
      };
  
      /**
       * Return Custom String Formatter
       *
       * @param {Object} valueMetadata valueMetadata for given telemetry object
       * @param {string} format custom formatter string (eg: %.4f, &lts etc.)
       * @returns {CustomStringFormatter}
       */
      TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) {
          return new CustomStringFormatter.default(this.openmct, valueMetadata, format);
      };
  
      /**
       * Return true if the given domainObject is a telemetry object.  A telemetry
       * object is any object which has telemetry metadata-- regardless of whether
       * the telemetry object has an available telemetry provider.
       *
       * @param {module:openmct.DomainObject} domainObject
       * @returns {boolean} true if the object is a telemetry object.
       */
      TelemetryAPI.prototype.isTelemetryObject = function (domainObject) {
          return Boolean(this.findMetadataProvider(domainObject));
      };
  
      /**
       * Check if this provider can supply telemetry data associated with
       * this domain object.
       *
       * @method canProvideTelemetry
       * @param {module:openmct.DomainObject} domainObject the object for
       *        which telemetry would be provided
       * @returns {boolean} true if telemetry can be provided
       * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
       */
      TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) {
          return Boolean(this.findSubscriptionProvider(domainObject))
                 || Boolean(this.findRequestProvider(domainObject));
      };
  
      /**
       * Register a telemetry provider with the telemetry service. This
       * allows you to connect alternative telemetry sources.
       * @method addProvider
       * @memberof module:openmct.TelemetryAPI#
       * @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new
       *        telemetry provider
       */
      TelemetryAPI.prototype.addProvider = function (provider) {
          if (provider.supportsRequest) {
              this.requestProviders.unshift(provider);
          }
  
          if (provider.supportsSubscribe) {
              this.subscriptionProviders.unshift(provider);
          }
  
          if (provider.supportsMetadata) {
              this.metadataProviders.unshift(provider);
          }
  
          if (provider.supportsLimits) {
              this.limitProviders.unshift(provider);
          }
      };
  
      /**
       * @private
       */
      TelemetryAPI.prototype.findSubscriptionProvider = function () {
          const args = Array.prototype.slice.apply(arguments);
          function supportsDomainObject(provider) {
              return provider.supportsSubscribe.apply(provider, args);
          }
  
          return this.subscriptionProviders.filter(supportsDomainObject)[0];
      };
  
      /**
       * @private
       */
      TelemetryAPI.prototype.findRequestProvider = function (domainObject) {
          const args = Array.prototype.slice.apply(arguments);
          function supportsDomainObject(provider) {
              return provider.supportsRequest.apply(provider, args);
          }
  
          return this.requestProviders.filter(supportsDomainObject)[0];
      };
  
      /**
       * @private
       */
      TelemetryAPI.prototype.findMetadataProvider = function (domainObject) {
          return this.metadataProviders.filter(function (p) {
              return p.supportsMetadata(domainObject);
          })[0];
      };
  
      /**
       * @private
       */
      TelemetryAPI.prototype.findLimitEvaluator = function (domainObject) {
          return this.limitProviders.filter(function (p) {
              return p.supportsLimits(domainObject);
          })[0];
      };
  
      /**
       * @private
       */
      TelemetryAPI.prototype.standardizeRequestOptions = function (options) {
          if (!Object.prototype.hasOwnProperty.call(options, 'start')) {
              options.start = this.openmct.time.bounds().start;
          }
  
          if (!Object.prototype.hasOwnProperty.call(options, 'end')) {
              options.end = this.openmct.time.bounds().end;
          }
  
          if (!Object.prototype.hasOwnProperty.call(options, 'domain')) {
              options.domain = this.openmct.time.timeSystem().key;
          }
      };
  
      /**
       * Request telemetry collection for a domain object.
       * The `options` argument allows you to specify filters
       * (start, end, etc.), sort order, and strategies for retrieving
       * telemetry (aggregation, latest available, etc.).
       *
       * @method requestCollection
       * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
       * @param {module:openmct.DomainObject} domainObject the object
       *        which has associated telemetry
       * @param {module:openmct.TelemetryAPI~TelemetryRequest} options
       *        options for this telemetry collection request
       * @returns {TelemetryCollection} a TelemetryCollection instance
       */
      TelemetryAPI.prototype.requestCollection = function (domainObject, options = {}) {
          return new TelemetryCollection(
              this.openmct,
              domainObject,
              options
          );
      };
  
      /**
       * Request historical telemetry for a domain object.
       * The `options` argument allows you to specify filters
       * (start, end, etc.), sort order, and strategies for retrieving
       * telemetry (aggregation, latest available, etc.).
       *
       * @method request
       * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
       * @param {module:openmct.DomainObject} domainObject the object
       *        which has associated telemetry
       * @param {module:openmct.TelemetryAPI~TelemetryRequest} options
       *        options for this historical request
       * @returns {Promise.<object[]>} a promise for an array of
       *          telemetry data
       */
      TelemetryAPI.prototype.request = function (domainObject) {
          if (this.noRequestProviderForAllObjects) {
              return Promise.resolve([]);
          }
  
          if (arguments.length === 1) {
              arguments.length = 2;
              arguments[1] = {};
          }
  
          const abortController = new AbortController();
          arguments[1].signal = abortController.signal;
          this.requestAbortControllers.add(abortController);
  
          this.standardizeRequestOptions(arguments[1]);
          const provider = this.findRequestProvider.apply(this, arguments);
          if (!provider) {
              this.requestAbortControllers.delete(abortController);
  
              return this.handleMissingRequestProvider(domainObject);
          }
  
          return provider.request.apply(provider, arguments)
              .catch((rejected) => {
                  if (rejected.name !== 'AbortError') {
                      this.openmct.notifications.error('Error requesting telemetry data, see console for details');
                      console.error(rejected);
                  }
  
                  return Promise.reject(rejected);
              }).finally(() => {
                  this.requestAbortControllers.delete(abortController);
              });
      };
  
      /**
       * Subscribe to realtime telemetry for a specific domain object.
       * The callback will be called whenever data is received from a
       * realtime provider.
       *
       * @method subscribe
       * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
       * @param {module:openmct.DomainObject} domainObject the object
       *        which has associated telemetry
       * @param {Function} callback the callback to invoke with new data, as
       *        it becomes available
       * @returns {Function} a function which may be called to terminate
       *          the subscription
       */
      TelemetryAPI.prototype.subscribe = function (domainObject, callback, options) {
          const provider = this.findSubscriptionProvider(domainObject);
  
          if (!this.subscribeCache) {
              this.subscribeCache = {};
          }
  
          const keyString = objectUtils.makeKeyString(domainObject.identifier);
          let subscriber = this.subscribeCache[keyString];
  
          if (!subscriber) {
              subscriber = this.subscribeCache[keyString] = {
                  callbacks: [callback]
              };
              if (provider) {
                  subscriber.unsubscribe = provider
                      .subscribe(domainObject, function (value) {
                          subscriber.callbacks.forEach(function (cb) {
                              cb(value);
                          });
                      }, options);
              } else {
                  subscriber.unsubscribe = function () {};
              }
          } else {
              subscriber.callbacks.push(callback);
          }
  
          return function unsubscribe() {
              subscriber.callbacks = subscriber.callbacks.filter(function (cb) {
                  return cb !== callback;
              });
              if (subscriber.callbacks.length === 0) {
                  subscriber.unsubscribe();
                  delete this.subscribeCache[keyString];
              }
          }.bind(this);
      };
  
      /**
       * Get telemetry metadata for a given domain object.  Returns a telemetry
       * metadata manager which provides methods for interrogating telemetry
       * metadata.
       *
       * @returns {TelemetryMetadataManager}
       */
      TelemetryAPI.prototype.getMetadata = function (domainObject) {
          if (!this.metadataCache.has(domainObject)) {
              const metadataProvider = this.findMetadataProvider(domainObject);
              if (!metadataProvider) {
                  return;
              }
  
              const metadata = metadataProvider.getMetadata(domainObject);
  
              this.metadataCache.set(
                  domainObject,
                  new TelemetryMetadataManager(metadata)
              );
          }
  
          return this.metadataCache.get(domainObject);
      };
  
      /**
       * Return an array of valueMetadatas that are common to all supplied
       * telemetry objects and match the requested hints.
       *
       */
      TelemetryAPI.prototype.commonValuesForHints = function (metadatas, hints) {
          const options = metadatas.map(function (metadata) {
              const values = metadata.valuesForHints(hints);
  
              return _.keyBy(values, 'key');
          }).reduce(function (a, b) {
              const results = {};
              Object.keys(a).forEach(function (key) {
                  if (Object.prototype.hasOwnProperty.call(b, key)) {
                      results[key] = a[key];
                  }
              });
  
              return results;
          });
          const sortKeys = hints.map(function (h) {
              return 'hints.' + h;
          });
  
          return _.sortBy(options, sortKeys);
      };
  
      /**
       * Get a value formatter for a given valueMetadata.
       *
       * @returns {TelemetryValueFormatter}
       */
      TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) {
          if (!this.valueFormatterCache.has(valueMetadata)) {
              this.valueFormatterCache.set(
                  valueMetadata,
                  new TelemetryValueFormatter(valueMetadata, this.formatters)
              );
          }
  
          return this.valueFormatterCache.get(valueMetadata);
      };
  
      /**
       * Get a value formatter for a given key.
       * @param {string} key
       *
       * @returns {Format}
       */
      TelemetryAPI.prototype.getFormatter = function (key) {
          return this.formatters.get(key);
      };
  
      /**
       * Get a format map of all value formatters for a given piece of telemetry
       * metadata.
       *
       * @returns {Object<String, {TelemetryValueFormatter}>}
       */
      TelemetryAPI.prototype.getFormatMap = function (metadata) {
          if (!metadata) {
              return {};
          }
  
          if (!this.formatMapCache.has(metadata)) {
              const formatMap = metadata.values().reduce(function (map, valueMetadata) {
                  map[valueMetadata.key] = this.getValueFormatter(valueMetadata);
  
                  return map;
              }.bind(this), {});
              this.formatMapCache.set(metadata, formatMap);
          }
  
          return this.formatMapCache.get(metadata);
      };
  
      /**
       * Error Handling: Missing Request provider
       *
       * @returns Promise
       */
      TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) {
          this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
              const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
              const hasRequestProvider = Object.hasOwn(requestProvider, 'request');
  
              return supportsRequest && hasRequestProvider;
          });
  
          let message = '';
          let detailMessage = '';
          if (this.noRequestProviderForAllObjects) {
              message = 'Missing request providers, see console for details';
              detailMessage = 'Missing request provider for all request providers';
          } else {
              message = 'Missing request provider, see console for details';
              const { name, identifier } = domainObject;
              detailMessage = `Missing request provider for domainObject, name: ${name}, identifier: ${JSON.stringify(identifier)}`;
          }
  
          this.openmct.notifications.error(message);
          console.error(detailMessage);
  
          return Promise.resolve([]);
      };
  
      /**
       * Register a new telemetry data formatter.
       * @param {Format} format the
       */
      TelemetryAPI.prototype.addFormat = function (format) {
          this.formatters.set(format.key, format);
      };
  
      /**
       * Get a limit evaluator for this domain object.
       * Limit Evaluators help you evaluate limit and alarm status of individual
       * telemetry datums for display purposes without having to interact directly
       * with the Limit API.
       *
       * This method is optional.
       * If a provider does not implement this method, it is presumed
       * that no limits are defined for this domain object's telemetry.
       *
       * @param {module:openmct.DomainObject} domainObject the domain
       *        object for which to evaluate limits
       * @returns {module:openmct.TelemetryAPI~LimitEvaluator}
       * @method limitEvaluator
       * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
       */
      TelemetryAPI.prototype.limitEvaluator = function (domainObject) {
          return this.getLimitEvaluator(domainObject);
      };
  
      /**
       * Get a limits for this domain object.
       * Limits help you display limits and alarms of
       * telemetry for display purposes without having to interact directly
       * with the Limit API.
       *
       * This method is optional.
       * If a provider does not implement this method, it is presumed
       * that no limits are defined for this domain object's telemetry.
       *
       * @param {module:openmct.DomainObject} domainObject the domain
       *        object for which to get limits
       * @returns {module:openmct.TelemetryAPI~LimitEvaluator}
       * @method limits
       * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
       */
      TelemetryAPI.prototype.limitDefinition = function (domainObject) {
          return this.getLimits(domainObject);
      };
  
      /**
       * Get a limit evaluator for this domain object.
       * Limit Evaluators help you evaluate limit and alarm status of individual
       * telemetry datums for display purposes without having to interact directly
       * with the Limit API.
       *
       * This method is optional.
       * If a provider does not implement this method, it is presumed
       * that no limits are defined for this domain object's telemetry.
       *
       * @param {module:openmct.DomainObject} domainObject the domain
       *        object for which to evaluate limits
       * @returns {module:openmct.TelemetryAPI~LimitEvaluator}
       * @method limitEvaluator
       * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
       */
      TelemetryAPI.prototype.getLimitEvaluator = function (domainObject) {
          const provider = this.findLimitEvaluator(domainObject);
          if (!provider) {
              return {
                  evaluate: function () {}
              };
          }
  
          return provider.getLimitEvaluator(domainObject);
      };
  
      /**
       * Get a limit definitions for this domain object.
       * Limit Definitions help you indicate limits and alarms of
       * telemetry for display purposes without having to interact directly
       * with the Limit API.
       *
       * This method is optional.
       * If a provider does not implement this method, it is presumed
       * that no limits are defined for this domain object's telemetry.
       *
       * @param {module:openmct.DomainObject} domainObject the domain
       *        object for which to display limits
       * @returns {module:openmct.TelemetryAPI~LimitEvaluator}
       * @method limits returns a limits object of
       * type {
       *          level1: {
       *              low: { key1: value1, key2: value2, color: <supportedColor> },
       *              high: { key1: value1, key2: value2, color: <supportedColor> }
       *          },
       *          level2: {
       *              low: { key1: value1, key2: value2 },
       *              high: { key1: value1, key2: value2 }
       *          }
       *       }
       *  supported colors are purple, red, orange, yellow and cyan
       * @memberof module:openmct.TelemetryAPI~TelemetryProvider#
       */
      TelemetryAPI.prototype.getLimits = function (domainObject) {
          const provider = this.findLimitEvaluator(domainObject);
          if (!provider || !provider.getLimits) {
              return {
                  limits: function () {
                      return Promise.resolve(undefined);
                  }
              };
          }
  
          return provider.getLimits(domainObject);
      };
  
      return TelemetryAPI;
  });
  