// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
 * @packageDocumentation
 * @module disposable
 */
import { ISignal, Signal } from '@lumino/signaling';

/**
 * An object which implements the disposable pattern.
 */
export interface IDisposable {
  /**
   * Test whether the object has been disposed.
   *
   * #### Notes
   * This property is always safe to access.
   */
  readonly isDisposed: boolean;

  /**
   * Dispose of the resources held by the object.
   *
   * #### Notes
   * If the object's `dispose` method is called more than once, all
   * calls made after the first will be a no-op.
   *
   * #### Undefined Behavior
   * It is undefined behavior to use any functionality of the object
   * after it has been disposed unless otherwise explicitly noted.
   */
  dispose(): void;
}

/**
 * A disposable object with an observable `disposed` signal.
 */
export interface IObservableDisposable extends IDisposable {
  /**
   * A signal emitted when the object is disposed.
   */
  readonly disposed: ISignal<this, void>;
}

/**
 * A disposable object which delegates to a callback function.
 */
export class DisposableDelegate implements IDisposable {
  /**
   * Construct a new disposable delegate.
   *
   * @param fn - The callback function to invoke on dispose.
   */
  constructor(fn: () => void) {
    this._fn = fn;
  }

  /**
   * Test whether the delegate has been disposed.
   */
  get isDisposed(): boolean {
    return !this._fn;
  }

  /**
   * Dispose of the delegate and invoke the callback function.
   */
  dispose(): void {
    if (!this._fn) {
      return;
    }
    let fn = this._fn;
    this._fn = null;
    fn();
  }

  private _fn: (() => void) | null;
}

/**
 * An observable disposable object which delegates to a callback function.
 */
export class ObservableDisposableDelegate
  extends DisposableDelegate
  implements IObservableDisposable
{
  /**
   * A signal emitted when the delegate is disposed.
   */
  get disposed(): ISignal<this, void> {
    return this._disposed;
  }

  /**
   * Dispose of the delegate and invoke the callback function.
   */
  dispose(): void {
    if (this.isDisposed) {
      return;
    }
    super.dispose();
    this._disposed.emit(undefined);
    Signal.clearData(this);
  }

  private _disposed = new Signal<this, void>(this);
}

/**
 * An object which manages a collection of disposable items.
 */
export class DisposableSet implements IDisposable {
  /**
   * Test whether the set has been disposed.
   */
  get isDisposed(): boolean {
    return this._isDisposed;
  }

  /**
   * Dispose of the set and the items it contains.
   *
   * #### Notes
   * Items are disposed in the order they are added to the set.
   */
  dispose(): void {
    if (this._isDisposed) {
      return;
    }
    this._isDisposed = true;
    this._items.forEach(item => {
      item.dispose();
    });
    this._items.clear();
  }

  /**
   * Test whether the set contains a specific item.
   *
   * @param item - The item of interest.
   *
   * @returns `true` if the set contains the item, `false` otherwise.
   */
  contains(item: IDisposable): boolean {
    return this._items.has(item);
  }

  /**
   * Add a disposable item to the set.
   *
   * @param item - The item to add to the set.
   *
   * #### Notes
   * If the item is already contained in the set, this is a no-op.
   */
  add(item: IDisposable): void {
    this._items.add(item);
  }

  /**
   * Remove a disposable item from the set.
   *
   * @param item - The item to remove from the set.
   *
   * #### Notes
   * If the item is not contained in the set, this is a no-op.
   */
  remove(item: IDisposable): void {
    this._items.delete(item);
  }

  /**
   * Remove all items from the set.
   */
  clear(): void {
    this._items.clear();
  }

  private _isDisposed = false;
  private _items = new Set<IDisposable>();
}

/**
 * The namespace for the `DisposableSet` class statics.
 */
export namespace DisposableSet {
  /**
   * Create a disposable set from an iterable of items.
   *
   * @param items - The iterable object of interest.
   *
   * @returns A new disposable initialized with the given items.
   */
  export function from(items: Iterable<IDisposable>): DisposableSet {
    let set = new DisposableSet();
    for (const item of items) {
      set.add(item);
    }
    return set;
  }
}

/**
 * An observable object which manages a collection of disposable items.
 */
export class ObservableDisposableSet
  extends DisposableSet
  implements IObservableDisposable
{
  /**
   * A signal emitted when the set is disposed.
   */
  get disposed(): ISignal<this, void> {
    return this._disposed;
  }

  /**
   * Dispose of the set and the items it contains.
   *
   * #### Notes
   * Items are disposed in the order they are added to the set.
   */
  dispose(): void {
    if (this.isDisposed) {
      return;
    }
    super.dispose();
    this._disposed.emit(undefined);
    Signal.clearData(this);
  }

  private _disposed = new Signal<this, void>(this);
}

/**
 * The namespace for the `ObservableDisposableSet` class statics.
 */
export namespace ObservableDisposableSet {
  /**
   * Create an observable disposable set from an iterable of items.
   *
   * @param items - The iterable object of interest.
   *
   * @returns A new disposable initialized with the given items.
   */
  export function from(items: Iterable<IDisposable>): ObservableDisposableSet {
    let set = new ObservableDisposableSet();
    for (const item of items) {
      set.add(item);
    }
    return set;
  }
}
