export type Undoable<L, T> = {
  loc: L;
  value_initial: T;
  value_updated: T;
};

export class UndoStack<T extends Undoable<unknown, unknown>> {
  /**
   * All undo- and redo-able diffs, in order.
   */
  private _diffs: T[][] = [];

  /**
   * Index of last delta currently represented in application state. Decrements
   * when undoing; increments when pushing or redoing. -1 when stack is empty.
   */
  private _cursor: number = -1;

  private readonly _apply_diff: (diff: T[]) => unknown;

  /**
   * NOTE: `apply_diff()` is permitted to mutate the array it receives as a
   * parameter, but otherwise `UndoStack` expects that the deltas it receives
   * and provides will not be tampered with. Mutating arrays after providing
   * them to `.push()` or mutating the individual array items supplied to
   * `apply_diff()`, for example, is considered undefined behavior.
   */
  constructor(
    { apply_diff }: { apply_diff(deltas: T[]): unknown },
  ) {
    // Always shallow copy array so that callback may perform some mutations.
    this._apply_diff = (diff: T[]) => apply_diff([...diff]);
  }

  /**
   * Pushes a batch of deltas to the end of the undo stack, and clears the redo
   * stack. `apply_diff()` will be immediately called with a copy of `deltas`.
   */
  push(diff: T[]): undefined {
    // Clear redo stack. `Array.splice()` does nothing if start index > array
    // length, and `this._cursor` is always >= -1, so no conditional needed.
    this._diffs.splice(this._cursor + 1);

    this._diffs.push(diff);
    this._cursor += 1;

    // Call `_apply_diff()` after shifting cursor, in case it recursively
    // mutates this UndoStack.
    this._apply_diff(diff);
  }

  undo(): undefined {
    if (this._cursor > -1) {
      this._cursor -= 1;

      // Call `_apply_diff()` after shifting cursor, in case it recursively
      // mutates this UndoStack.
      this._apply_diff(invert_diff(this._diffs[this._cursor + 1]));
    }
  }

  redo(): undefined {
    if (this._diffs.length > this._cursor + 1) {
      this._cursor += 1;

      // Call `_apply_diff()` after shifting cursor, in case it recursively
      // mutates this UndoStack.
      this._apply_diff(this._diffs[this._cursor]);
    }
  }
}

/**
 * Returns a copy of the parameter with initial and updated values swapped.
 */
export function invert_diff<T extends Undoable<unknown, unknown>>(
  diff: T[],
): T[] {
  return diff.map((undoable) => ({
    loc: undoable.loc,
    value_initial: undoable.value_updated,
    value_updated: undoable.value_initial,
  } as T));
}
