/*****************************************************************************
 * 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.
 *****************************************************************************/
import _ from 'lodash';

import { makeKeyString, parseKeyString } from '../objects/object-utils.js';

/**
 * @typedef {import('openmct').DomainObject} DomainObject
 */

/**
 * @typedef {import('openmct').Identifier} Identifier
 */

/**
 * @typedef {import('./CompositionAPI').default} CompositionAPI
 */

/**
 * @typedef {import('openmct').OpenMCT} OpenMCT
 */

/**
 * A CompositionProvider provides the underlying implementation of
 * composition-related behavior for certain types of domain object.
 *
 * By default, a composition provider will not support composition
 * modification.  You can add support for mutation of composition by
 * defining `add` and/or `remove` methods.
 *
 * If the composition of an object can change over time-- perhaps via
 * server updates or mutation via the add/remove methods, then one must
 * trigger events as necessary.
 *
 */
export default class CompositionProvider {
  #publicAPI;
  #listeningTo;

  /**
   * @param {OpenMCT} publicAPI
   * @param {CompositionAPI} compositionAPI
   */
  constructor(publicAPI, compositionAPI) {
    this.#publicAPI = publicAPI;
    this.#listeningTo = {};

    compositionAPI.addPolicy(this.#cannotContainItself.bind(this));
    compositionAPI.addPolicy(this.#supportsComposition.bind(this));
  }

  get listeningTo() {
    return this.#listeningTo;
  }

  get establishTopicListener() {
    return this.#establishTopicListener.bind(this);
  }

  get publicAPI() {
    return this.#publicAPI;
  }

  /**
   * Check if this provider should be used to load composition for a
   * particular domain object.
   * @method appliesTo
   * @param {DomainObject} domainObject the domain object
   *        to check
   * @returns {boolean} true if this provider can provide composition for a given domain object
   */
  appliesTo(domainObject) {
    throw new Error('This method must be implemented by a subclass.');
  }
  /**
   * Load any domain objects contained in the composition of this domain
   * object.
   * @param {DomainObject} domainObject the domain object
   *        for which to load composition
   * @returns {Promise<Identifier[]>} a promise for
   *          the Identifiers in this composition
   */
  load(domainObject) {
    throw new Error('This method must be implemented by a subclass.');
  }
  /**
   * Attach listeners for changes to the composition of a given domain object.
   * Supports `add` and `remove` events.
   *
   * @param {DomainObject} domainObject to listen to
   * @param {string} event the event to bind to, either `add` or `remove`.
   * @param {Function} callback callback to invoke when event is triggered.
   * @param {any} [context] to use when invoking callback.
   */
  on(domainObject, event, callback, context) {
    throw new Error('This method must be implemented by a subclass.');
  }
  /**
   * Remove a listener that was previously added for a given domain object.
   * event name, callback, and context must be the same as when the listener
   * was originally attached.
   *
   * @param {DomainObject} domainObject to remove listener for
   * @param {string} event event to stop listening to: `add` or `remove`.
   * @param {Function} callback callback to remove.
   * @param {any} context of callback to remove.
   */
  off(domainObject, event, callback, context) {
    throw new Error('This method must be implemented by a subclass.');
  }
  /**
   * Remove a domain object from another domain object's composition.
   *
   * This method is optional; if not present, adding to a domain object's
   * composition using this provider will be disallowed.
   *
   * @param {DomainObject} domainObject the domain object
   *        which should have its composition modified
   * @param {Identifier} childId the domain object to remove
   */
  remove(domainObject, childId) {
    throw new Error('This method must be implemented by a subclass.');
  }
  /**
   * Add a domain object to another domain object's composition.
   *
   * This method is optional; if not present, adding to a domain object's
   * composition using this provider will be disallowed.
   *
   * @param {DomainObject} parent the domain object
   *        which should have its composition modified
   * @param {Identifier} childId the domain object to add
   */
  add(parent, childId) {
    throw new Error('This method must be implemented by a subclass.');
  }

  /**
   * @param {DomainObject} parent
   * @param {Identifier} childId
   * @returns {boolean}
   */
  includes(parent, childId) {
    throw new Error('This method must be implemented by a subclass.');
  }

  /**
   * @param {DomainObject} domainObject
   * @param {number} oldIndex
   * @param {number} newIndex
   * @returns
   */
  reorder(domainObject, oldIndex, newIndex) {
    throw new Error('This method must be implemented by a subclass.');
  }

  /**
   * Listens on general mutation topic, using injector to fetch to avoid
   * circular dependencies.
   */
  #establishTopicListener() {
    if (this.topicListener) {
      return;
    }

    const onMutation = this.#onMutation.bind(this);
    this.#publicAPI.objects.eventEmitter.on('mutation', onMutation);
    this.topicListener = () => {
      this.#publicAPI.objects.eventEmitter.off('mutation', onMutation);
    };
  }

  /**
   * @param {DomainObject} parent
   * @param {DomainObject} child
   * @returns {boolean}
   */
  #cannotContainItself(parent, child) {
    return !(
      parent.identifier.namespace === child.identifier.namespace &&
      parent.identifier.key === child.identifier.key
    );
  }

  /**
   * @param {DomainObject} parent
   * @returns {boolean}
   */
  #supportsComposition(parent, _child) {
    return this.#publicAPI.composition.supportsComposition(parent);
  }

  /**
   * Handles mutation events.  If there are active listeners for the mutated
   * object, detects changes to composition and triggers necessary events.
   *
   * @param {DomainObject} oldDomainObject
   */
  #onMutation(newDomainObject, oldDomainObject) {
    const id = makeKeyString(oldDomainObject.identifier);
    const listeners = this.#listeningTo[id];

    if (!listeners) {
      return;
    }

    if (oldDomainObject.composition === undefined || newDomainObject.composition === undefined) {
      return;
    }

    const oldComposition = oldDomainObject.composition.map(makeKeyString);
    const newComposition = newDomainObject.composition.map(makeKeyString);

    const added = _.difference(newComposition, oldComposition).map(parseKeyString);
    const removed = _.difference(oldComposition, newComposition).map(parseKeyString);

    function notify(value) {
      return function (listener) {
        if (listener.context) {
          listener.callback.call(listener.context, value);
        } else {
          listener.callback(value);
        }
      };
    }

    added.forEach(function (addedChild) {
      listeners.add.forEach(notify(addedChild));
    });

    removed.forEach(function (removedChild) {
      listeners.remove.forEach(notify(removedChild));
    });
  }
}
