import {CursorPos} from 'app/client/components/Cursor';
import {GristDoc} from 'app/client/components/GristDoc';
import {getStorage} from 'app/client/lib/localStorageObs';
import {IDocPage} from 'app/common/gristUrls';
import {Disposable} from 'grainjs';

/**
 * Enriched cursor position with a view id
 */
export type ViewCursorPos = CursorPos & { viewId: number }

/**
 * Component for GristDoc that allows it to keep track of the latest cursor position.
 * In case, when a document is reloaded abnormally, the latest cursor
 * position should be restored from a local storage.
 */
export class CursorMonitor extends Disposable {

  // abstraction to work with local storage
  private _store: StorageWrapper;
  // document id that this monitor is attached
  private _docId: string;
  // flag that tells if the position was already restored
  // we track document's view change event, so we only want
  // to react to that event once
  private _restored = false;

  constructor(
    doc: GristDoc,
    store?: Storage) {
    super();

    this._store = new StorageWrapper(store);
    this._docId = doc.docId();

    /**
     * When document loads last cursor position should be restored from local storage.
     */
    this._whenDocumentLoadsRestorePosition(doc);

    /**
     * When a cursor position changes, its value is stored in a local storage.
     */
    this._whenCursorHasChangedStoreInMemory(doc);
  }

  private _whenCursorHasChangedStoreInMemory(doc: GristDoc) {
    // whenever current position changes, store it in the memory
    this.autoDispose(doc.cursorPosition.addListener(pos => {
      // if current position is not restored yet, don't change it
      if (!this._restored) { return; }
      if (pos) { this._storePosition(pos); }
    }));
  }

  private _whenDocumentLoadsRestorePosition(doc: GristDoc) {
    // if doc was opened with a hash link, don't restore last position
    if (doc.hasCustomNav.get()) {
      this._restored = true;
      return;
    }

    // on view shown
    this.autoDispose(doc.currentView.addListener(async view => {
      // if the position was restored for this document do nothing
      if (this._restored) { return; }
      // set that we already restored the position, as some view is shown to the user
      this._restored = true;
      // if view wasn't rendered (page is displaying history or code view) do nothing
      if (!view) { return; }
      const viewId = doc.activeViewId.get();
      const position = this._restoreLastPosition(viewId);
      if (position) {
        await doc.recursiveMoveToCursorPos(position, true);
      }
    }));
  }

  private _storePosition(pos: ViewCursorPos) {
    this._store.update(this._docId, pos);
  }

  private _restoreLastPosition(view: IDocPage) {
    const lastPosition = this._store.read(this._docId);
    this._store.clear(this._docId);
    if (lastPosition && lastPosition.position.viewId == view) {
      return lastPosition.position;
    }
    return null;
  }
}

// Internal implementations for working with local storage
class StorageWrapper {

  constructor(private _storage = getStorage()) {

  }

  public update(docId: string, position: ViewCursorPos): void {
    try {
      const storage = this._storage;
      const data = { docId, position, timestamp: Date.now() };
      storage.setItem(this._key(docId), JSON.stringify(data));
    } catch (e) {
      console.error("Can't store latest position in storage. Detail error " + e.message);
    }
  }

  public clear(docId: string,): void {
    const storage = this._storage;
    storage.removeItem(this._key(docId));
  }

  public read(docId: string): { docId: string; position: ViewCursorPos; } | undefined {
    const storage = this._storage;
    const result = storage.getItem(this._key(docId));
    if (!result) { return undefined; }
    return JSON.parse(result);
  }

  protected _key(docId: string) {
    return `grist-last-position-${docId}`;
  }
}
