/*****************************************************************************
 * Open MCT, Copyright (c) 2014-2024, United States Government
 * as represented by the Administrator of the National Aeronautics and Space
 * Administration. All rights reserved.
 *
 * Open MCT 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 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.
 *****************************************************************************/

function copyRelatedMetadata(metadata) {
  let compare = metadata.comparisonFunction;
  let copiedMetadata = JSON.parse(JSON.stringify(metadata));
  copiedMetadata.comparisonFunction = compare;

  return copiedMetadata;
}

import IndependentTimeContext from '@/api/time/IndependentTimeContext';
export default class RelatedTelemetry {
  constructor(openmct, domainObject, telemetryKeys, timeContext) {
    this._openmct = openmct;
    this._domainObject = domainObject;
    this.timeContext = timeContext;

    let metadata = this._openmct.telemetry.getMetadata(this._domainObject);
    let imageHints = metadata.valuesForHints(['image'])[0];

    this.hasRelatedTelemetry = imageHints.relatedTelemetry !== undefined;

    if (this.hasRelatedTelemetry) {
      this.keys = telemetryKeys;

      this._timeFormatter = undefined;
      this._timeSystemChange(this.timeContext.getTimeSystem());

      // grab related telemetry metadata
      for (let key of this.keys) {
        if (imageHints.relatedTelemetry[key]) {
          this[key] = copyRelatedMetadata(imageHints.relatedTelemetry[key]);
        }
      }

      this.load = this.load.bind(this);
      this._parseTime = this._parseTime.bind(this);
      this._timeSystemChange = this._timeSystemChange.bind(this);
      this.destroy = this.destroy.bind(this);

      this.timeContext.on('timeSystem', this._timeSystemChange);
    }
  }

  async load() {
    if (!this.hasRelatedTelemetry) {
      throw new Error(
        'This domain object does not have related telemetry, use "hasRelatedTelemetry" to check before loading.'
      );
    }

    await Promise.all(
      this.keys.map(async (key) => {
        if (this[key]) {
          if (this[key].historical) {
            await this._initializeHistorical(key);
          }

          if (
            this[key].realtime &&
            this[key].realtime.telemetryObjectId &&
            this[key].realtime.telemetryObjectId !== ''
          ) {
            await this._initializeRealtime(key);
          }
        }
      })
    );
  }

  async _initializeHistorical(key) {
    if (!this[key].historical.telemetryObjectId) {
      this[key].historical.hasTelemetryOnDatum = true;
    } else if (this[key].historical.telemetryObjectId !== '') {
      this[key].historicalDomainObject = await this._openmct.objects.get(
        this[key].historical.telemetryObjectId
      );

      this[key].requestLatestFor = async (datum) => {
        // We need to create a throwaway time context and pass it along
        // as a request option. We do this to "trick" the Time API
        // into thinking we are in fixed time mode in order to bypass this logic:
        // https://github.com/akhenry/openmct-yamcs/blob/1060d42ebe43bf346dac0f6a8068cb288ade4ba4/src/providers/historical-telemetry-provider.js#L59
        // Context: https://github.com/akhenry/openmct-yamcs/pull/217
        const ephemeralContext = new IndependentTimeContext(this._openmct, this._openmct.time, [
          this[key].historicalDomainObject
        ]);

        // Stop following the global context, stop the clock,
        // and set bounds.
        ephemeralContext.resetContext();
        const newBounds = {
          start: this.timeContext.getBounds().start,
          end: this._parseTime(datum)
        };
        ephemeralContext.setBounds(newBounds);

        const options = {
          start: newBounds.start,
          end: newBounds.end,
          timeContext: ephemeralContext,
          strategy: 'latest'
        };
        let results = await this._openmct.telemetry.request(
          this[key].historicalDomainObject,
          options
        );

        return results[results.length - 1];
      };
    }
  }

  async _initializeRealtime(key) {
    this[key].realtimeDomainObject = await this._openmct.objects.get(
      this[key].realtime.telemetryObjectId
    );
    this[key].listeners = [];
    this[key].subscribe = (callback) => {
      if (!this[key].isSubscribed) {
        this._subscribeToDataForKey(key);
      }

      if (!this[key].listeners.includes(callback)) {
        this[key].listeners.push(callback);

        return () => {
          this[key].listeners.remove(callback);
        };
      } else {
        return () => {};
      }
    };
  }

  _subscribeToDataForKey(key) {
    if (this[key].isSubscribed) {
      return;
    }

    if (this[key].realtimeDomainObject) {
      this[key].unsubscribe = this._openmct.telemetry.subscribe(
        this[key].realtimeDomainObject,
        (datum) => {
          this[key].listeners.forEach((callback) => {
            callback(datum);
          });
        }
      );

      this[key].isSubscribed = true;
    }
  }

  _parseTime(datum) {
    return this._timeFormatter.parse(datum);
  }

  _timeSystemChange(system) {
    let key = system.key;
    let metadata = this._openmct.telemetry.getMetadata(this._domainObject);
    let metadataValue = metadata.value(key) || { format: key };
    this._timeFormatter = this._openmct.telemetry.getValueFormatter(metadataValue);
  }

  destroy() {
    this.timeContext.off('timeSystem', this._timeSystemChange);
    for (let key of this.keys) {
      if (this[key] && this[key].unsubscribe) {
        this[key].unsubscribe();
      }
    }
  }
}
