/**
 * This base class provides an lock interface to execute exclusive operations
 */
export class Lockable {
  private isLocked: boolean;

  private readyPromise: Promise<this>;

  constructor() {
    /**
     * Indicates if there is currently an onging exclusive operation
     * @type boolean
     * @private
     */
    this.isLocked = false;

    /**
     * A promise which represents the state of the least exclusive operation
     * @type Promise
     * @private
     */
    this.readyPromise = Promise.resolve(this);
  }

  /**
   * Indicates if there is currently no exclusive operation executed
   * <code>true</code> If no exclusive lock is hold
   */
  get isReady(): boolean {
    return !this.isLocked;
  }

  /**
   * Waits on the previously requested operation and calls the doneCallback if the operation is fulfilled
   * @param doneCallback The callback which will be invoked when the previously
   * operations on this object is completed.
   * @param failCallback When the lock can't be released caused by a none
   * recoverable error
   * @return A promise which completes successfully, when the previously requested
   * operation completes
   */
  ready(doneCallback?: (this: this) => any, failCallback?: (error: Error) => any): Promise<this> {
    return this.readyPromise.then(doneCallback, failCallback);
  }

  /**
   * Try to aquire an exclusive lock and executes the given callback.
   * @param callback The exclusive operation to execute
   * @param [critical=false] Indicates if the operation is critical. If the operation is critical and the
   * operation fails, then the lock will not be released
   * @return A promise
   * @throws If the lock can't be aquired
   * @protected
   */
  withLock<T>(callback: () => Promise<T>, critical = false): Promise<T> {
    if (this.isLocked) {
      throw new Error('Current operation has not been finished.');
    }

    try {
      this.isLocked = true;
      const result = callback().then((res) => {
        this.isLocked = false;
        return res;
      }, (e) => {
        if (!critical) {
          this.isLocked = false;
        }
        throw e;
      });

      this.readyPromise = result.then(() => this, (e) => {
        if (!critical) {
          return this;
        }
        throw e;
      });

      return result;
    } catch (e) {
      if (critical) {
        this.readyPromise = Promise.reject(e);
      } else {
        this.isLocked = false;
      }
      throw e;
    }
  }
}
