// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { DocumentRegistry } from '@jupyterlab/docregistry';
import { IDisposable } from '@lumino/disposable';
import { Signal } from '@lumino/signaling';

/**
 * A class that manages the auto saving of a document.
 *
 * #### Notes
 * Implements https://github.com/ipython/ipython/wiki/IPEP-15:-Autosaving-the-IPython-Notebook.
 */
export class SaveHandler implements IDisposable {
  /**
   * Construct a new save handler.
   */
  constructor(options: SaveHandler.IOptions) {
    this._context = options.context;
    this._isConnectedCallback = options.isConnectedCallback || (() => true);
    const interval = options.saveInterval || 120;
    this._minInterval = interval * 1000;
    this._interval = this._minInterval;
    // Restart the timer when the contents model is updated.
    this._context.fileChanged.connect(this._setTimer, this);
    this._context.disposed.connect(this.dispose, this);
  }

  /**
   * The save interval used by the timer (in seconds).
   */
  get saveInterval(): number {
    return this._interval / 1000;
  }
  set saveInterval(value: number) {
    this._minInterval = this._interval = value * 1000;
    if (this._isActive) {
      this._setTimer();
    }
  }

  /**
   * Get whether the handler is active.
   */
  get isActive(): boolean {
    return this._isActive;
  }

  /**
   * Get whether the save handler is disposed.
   */
  get isDisposed(): boolean {
    return this._isDisposed;
  }

  /**
   * Dispose of the resources used by the save handler.
   */
  dispose(): void {
    if (this.isDisposed) {
      return;
    }
    this._isDisposed = true;
    clearTimeout(this._autosaveTimer);
    Signal.clearData(this);
  }

  /**
   * Start the autosaver.
   */
  start(): void {
    this._isActive = true;
    this._setTimer();
  }

  /**
   * Stop the autosaver.
   */
  stop(): void {
    this._isActive = false;
    clearTimeout(this._autosaveTimer);
  }

  /**
   * Set the timer.
   */
  private _setTimer(): void {
    clearTimeout(this._autosaveTimer);
    if (!this._isActive) {
      return;
    }
    this._autosaveTimer = window.setTimeout(() => {
      if (this._isConnectedCallback()) {
        this._save();
      } else {
        this._setTimer();
      }
    }, this._interval);
  }

  /**
   * Handle an autosave timeout.
   */
  private _save(): void {
    const context = this._context;

    // Trigger the next update.
    this._setTimer();

    if (!context) {
      return;
    }

    // Bail if the model is not dirty or the file is not non-savable, or the dialog
    // is already showing.
    if (!(context.canSave ?? true) || !context.model.dirty || this._inDialog) {
      return;
    }

    const start = new Date().getTime();
    context
      .save()
      .then(() => {
        if (this.isDisposed) {
          return;
        }
        const duration = new Date().getTime() - start;
        // New save interval: higher of 10x save duration or min interval.
        this._interval = Math.max(
          this._multiplier * duration,
          this._minInterval
        );
        // Restart the update to pick up the new interval.
        this._setTimer();
      })
      .catch(err => {
        // If the user canceled the save, do nothing.
        const { name } = err;
        if (name === 'ModalCancelError' || name === 'ModalDuplicateError') {
          return;
        }
        // Otherwise, log the error.
        console.error('Error in Auto-Save', err.message);
      });
  }

  private _autosaveTimer = -1;
  private _minInterval = -1;
  private _interval = -1;
  private _context: DocumentRegistry.Context;
  private _isConnectedCallback: () => boolean;
  private _isActive = false;
  private _inDialog = false;
  private _isDisposed = false;
  private _multiplier = 10;
}

/**
 * A namespace for `SaveHandler` statics.
 */
export namespace SaveHandler {
  /**
   * The options used to create a save handler.
   */
  export interface IOptions {
    /**
     * The context associated with the file.
     */
    context: DocumentRegistry.Context;

    /**
     * Autosaving should be paused while this callback function returns `false`.
     * By default, it always returns `true`.
     */
    isConnectedCallback?: () => boolean;

    /**
     * The minimum save interval in seconds (default is two minutes).
     */
    saveInterval?: number;
  }
}
