import { Model } from "../../model";
import { AnyOperation } from "../../persist";
import { Selection } from "../../editor";

export interface Step {
  change: AnyOperation[];
  inverse: AnyOperation[];
  selection: {
    prev?: Selection | null;
    next?: Selection | null;
  };
}

interface UndoRedoStackerHooks {
  restoreSelection: (selection: Selection) => void;
}

export class UndoRedoStacker {
  private waiting: Step | undefined = undefined;

  private undoStack: Step[] = [];

  private redoStack: Step[] = [];

  private readonly model: Model;

  private readonly hooks: UndoRedoStackerHooks;

  private readonly maxStackerLength: number;

  constructor(model: Model, hooks: UndoRedoStackerHooks) {
    this.model = model;
    this.hooks = hooks;
    this.maxStackerLength = 100;
  }

  stash(
    change: AnyOperation[],
    inverse: AnyOperation[],
    prevSelection?: Selection,
    nextSelection?: Selection
  ) {
    const { waiting } = this;
    if (waiting) {
      waiting.change = waiting.change.concat(change);
      waiting.inverse = inverse.concat(waiting.inverse);
      if (!waiting.selection.prev) {
        waiting.selection.prev = prevSelection;
      }
      waiting.selection.next = nextSelection;
    } else {
      this.waiting = {
        change,
        inverse,
        selection: {
          prev: prevSelection,
          next: nextSelection,
        },
      };
    }
    this.submit();
  }

  preteat(change: AnyOperation[], inverse: AnyOperation[], prevSelection?: Selection) {
    const { waiting } = this;
    if (!waiting) {
      this.waiting = {
        change,
        inverse,
        selection: {
          prev: prevSelection,
        },
      };
    } else {
      waiting.change = waiting.change.concat(change);
      waiting.inverse = inverse.concat(waiting.inverse);
      if (!waiting.selection.prev) {
        waiting.selection.prev = prevSelection;
      }
    }
  }

  commit(change: AnyOperation[], inverse: AnyOperation[]) {
    this.stash(change, inverse);
  }

  hasStash() {
    return this.waiting !== undefined;
  }

  getPopStash() {
    const { waiting } = this;
    this.waiting = undefined;
    return waiting;
  }

  submit() {
    const popStash = this.getPopStash();
    if (!popStash) {
      return;
    }
    this.pushUndoStack(popStash);
    this.redoStack = [];
  }

  pushUndoStack(stash: Step) {
    if (this.undoStack.length >= this.maxStackerLength) {
      this.undoStack.shift();
    }

    this.undoStack.push(stash);
  }

  restoreSelection(selection: Selection) {
    this.hooks.restoreSelection(selection);
  }

  undo() {
    const { undoStack, model, redoStack } = this;
    const last = undoStack.pop();
    if (!last) {
      return;
    }
    const {
      inverse,
      selection: { prev },
    } = last;

    model.book.emit("zyEditor", { type: "undo", data: inverse });

    model.applyOps(inverse);
    redoStack.push(last);
    if (prev) {
      this.restoreSelection(prev);
    }
  }

  redo() {
    const { undoStack, model, redoStack } = this;
    const last = redoStack.pop();
    if (!last) {
      return;
    }

    const {
      change,
      selection: { next },
    } = last;

    model.book.emit("zyEditor", { type: "redo", data: change });

    model.applyOps(change);
    undoStack.push(last);
    if (next) {
      this.restoreSelection(next);
    }
  }

  canRedo() {
    return this.redoStack.length > 0;
  }

  canUndo() {
    return this.undoStack.length > 0;
  }

  reset() {
    this.undoStack = [];
    this.redoStack = [];
  }
}
