import { Metadata } from './Metadata';
import { InstanceMetadata } from './InstanceMetadata';

export class SeriesMetadata extends Metadata {
  constructor(data, uid) {
    super(data, uid);
    // Initialize Private Properties
    Object.defineProperties(this, {
      _seriesInstanceUID: {
        configurable: true, // configurable so that it can be redefined in sub-classes...
        enumerable: false,
        writable: true,
        value: null,
      },
      _instances: {
        configurable: false,
        enumerable: false,
        writable: false,
        value: [],
      },
      _firstInstance: {
        configurable: false,
        enumerable: false,
        writable: true,
        value: null,
      },
    });
    // Initialize Public Properties
    this._definePublicProperties();
  }

  /**
   * Private Methods
   */

  /**
   * Define Public Properties
   * This method should only be called during initialization (inside the class constructor)
   */
  _definePublicProperties() {
    /**
     * Property: this.seriesInstanceUID
     * Same as this.getSeriesInstanceUID()
     * It's specially useful in contexts where a method call is not suitable like in search criteria. For example:
     * seriesCollection.findBy({
     *   seriesInstanceUID: '1.2.3.4.5.6.77777.8888888.99999999999.0'
     * });
     */
    Object.defineProperty(this, 'seriesInstanceUID', {
      configurable: false,
      enumerable: false,
      get: function() {
        return this.getSeriesInstanceUID();
      },
    });
  }

  /**
   * Public Methods
   */

  /**
   * Returns the SeriesInstanceUID of the current series.
   */
  getSeriesInstanceUID() {
    return this._seriesInstanceUID;
  }

  /**
   * Append an instance to the current series.
   * @param {InstanceMetadata} instance The instance to be added to the current series.
   * @returns {boolean} Returns true on success, false otherwise.
   */
  addInstance(instance) {
    let result = false;
    if (
      instance instanceof InstanceMetadata &&
      this.getInstanceByUID(instance.getSOPInstanceUID()) === void 0
    ) {
      this._instances.push(instance);
      result = true;
    }
    return result;
  }

  /**
   * Get the first instance of the current series retaining a consistent result across multiple calls.
   * @return {InstanceMetadata} An instance of the InstanceMetadata class or null if it does not exist.
   */
  getFirstInstance() {
    let instance = this._firstInstance;
    if (!(instance instanceof InstanceMetadata)) {
      instance = null;
      const found = this.getInstanceByIndex(0);
      if (found instanceof InstanceMetadata) {
        this._firstInstance = found;
        instance = found;
      }
    }
    return instance;
  }

  /**
   * Find an instance by index.
   * @param {number} index An integer representing a list index.
   * @returns {InstanceMetadata} Returns a InstanceMetadata instance when found or undefined otherwise.
   */
  getInstanceByIndex(index) {
    let found; // undefined by default...
    if (Metadata.isValidIndex(index)) {
      found = this._instances[index];
    }
    return found;
  }

  /**
   * Find an instance by SOPInstanceUID.
   * @param {string} uid An UID string.
   * @returns {InstanceMetadata} Returns a InstanceMetadata instance when found or undefined otherwise.
   */
  getInstanceByUID(uid) {
    let found; // undefined by default...
    if (Metadata.isValidUID(uid)) {
      found = this._instances.find(instance => {
        return instance.getSOPInstanceUID() === uid;
      });
    }
    return found;
  }

  /**
   * Retrieve the number of instances within the current series.
   * @returns {number} The number of instances in the current series.
   */
  getInstanceCount() {
    return this._instances.length;
  }

  /**
   * Invokes the supplied callback for each instance in the current series passing
   * two arguments: instance (an InstanceMetadata instance) and index (the integer
   * index of the instance within the current series)
   * @param {function} callback The callback function which will be invoked for each instance in the series.
   * @returns {undefined} Nothing is returned.
   */
  forEachInstance(callback) {
    if (Metadata.isValidCallback(callback)) {
      this._instances.forEach((instance, index) => {
        callback.call(null, instance, index);
      });
    }
  }

  /**
   * Find the index of an instance inside the series.
   * @param {InstanceMetadata} instance An instance of the SeriesMetadata class.
   * @returns {number} The index of the instance inside the series or -1 if not found.
   */
  indexOfInstance(instance) {
    return this._instances.indexOf(instance);
  }

  /**
   * Search the associated instances using the supplied callback as criteria. The callback is passed
   * two arguments: instance (a InstanceMetadata instance) and index (the integer
   * index of the instance within its series)
   * @param {function} callback The callback function which will be invoked for each instance.
   * @returns {InstanceMetadata|undefined} If an instance is found based on callback criteria it
   *                                     returns a InstanceMetadata. "undefined" is returned otherwise
   */
  findInstance(callback) {
    if (Metadata.isValidCallback(callback)) {
      return this._instances.find((instance, index) => {
        return callback.call(null, instance, index);
      });
    }
  }

  /**
   * Compares the current series with another one.
   * @param {SeriesMetadata} series An instance of the SeriesMetadata class.
   * @returns {boolean} Returns true if both instances refer to the same series.
   */
  equals(series) {
    const self = this;
    return (
      series === self ||
      (series instanceof SeriesMetadata &&
        series.getSeriesInstanceUID() === self.getSeriesInstanceUID())
    );
  }
}
