// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import type {Edge, Node} from "@xyflow/react";
import type {StateCreator, StoreApi } from 'zustand/vanilla';
import type {
  SeverityIndex,
  WarningRegistry
} from "./components/EditorWarnings.tsx";
import type {FlowState} from "./VisProgTypes.tsx";

export type FlowSnapshot = {
  nodes: Node[];
  edges: Edge[];
  warnings: {
    warningRegistry: WarningRegistry;
    severityIndex: SeverityIndex;
  }
}

/**
 * A reduced version of the flowState type,
 * This removes the functions that are provided by UndoRedo from the expected input type
 */
type BaseFlowState = Omit<FlowState, 'undo' | 'redo' | 'pushSnapshot' | 'beginBatchAction' | 'endBatchAction'>;


/**
 * UndoRedo is implemented as a middleware for the FlowState store,
 * this allows us to keep the undo redo logic separate from the flowState,
 * and thus from the internal editor logic
 *
 * Allows users to undo and redo actions in the visual programming editor
 *
 * @param {(set: StoreApi<FlowState>["setState"], get: () => FlowState, api: StoreApi<FlowState>) => BaseFlowState} config
 * @returns {StateCreator<FlowState>}
 * @constructor
 */
export const UndoRedo = (
  config: (
    set: StoreApi<FlowState>['setState'],
    get: () => FlowState,
    api: StoreApi<FlowState>
  ) => BaseFlowState ) : StateCreator<FlowState> => (set, get, api) => {
  let batchTimeout: number | null = null;

  /**
   * Captures the current state for
   *
   * @param {BaseFlowState} state - the current state of the editor
   * @returns {FlowSnapshot} - returns a snapshot of the current editor state
   */
  const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({
    nodes: state.nodes,
    edges: state.edges,
    warnings: {
      warningRegistry: state.editorWarningRegistry,
      severityIndex: state.severityIndex,
    }
  }));

  const initialState = config(set, get, api);

  return {
    ...initialState,

    /**
     *  Adds a snapshot of the current state to the undo history
     */
    pushSnapshot: () => {
      const state = get();
      // we don't add new snapshots during an ongoing batch action
      if (!state.isBatchAction) {
        set({
          past: [...state.past, getSnapshot(state)],
          future: []
        });
      }

    },

    /**
     * Undoes the last action from the editor,
     * The state before undoing is added to the future for potential redoing
     */
    undo: () => {
      const state = get();
      if (!state.past.length) return;

      const snapshot = state.past.pop()!; // pop last snapshot
      const currentSnapshot: FlowSnapshot = getSnapshot(state);

      set({
        nodes: snapshot.nodes,
        edges: snapshot.edges,
        editorWarningRegistry: snapshot.warnings.warningRegistry,
        severityIndex: snapshot.warnings.severityIndex,
      });

      state.future.push(currentSnapshot); // push current to redo
    },

    /**
     * redoes the last undone action,
     * The state before redoing is added to the past for potential undoing
     */
    redo: () => {
      const state = get();
      if (!state.future.length) return;

      const snapshot = state.future.pop()!; // pop last redo
      const currentSnapshot: FlowSnapshot = getSnapshot(state);

      set({
        nodes: snapshot.nodes,
        edges: snapshot.edges,
        editorWarningRegistry: snapshot.warnings.warningRegistry,
        severityIndex: snapshot.warnings.severityIndex,
      });

      state.past.push(currentSnapshot); // push current to undo
    },

    /**
     * Begins a batched action
     *
     * An example of a batched action is dragging a node in the editor,
     * where we want the entire action of moving a node to a different position
     * to be covered by one undoable snapshot
     */
    beginBatchAction: () => {
      get().pushSnapshot();
      set({ isBatchAction: true });
      if (batchTimeout) clearTimeout(batchTimeout);
    },

    /**
     * Ends a batched action,
     * a very short timeout is used to prevent new snapshots from being added
     * until we are certain that the batch event is finished
     */
    endBatchAction: () => {
      batchTimeout = window.setTimeout(() => {
        set({ isBatchAction: false });
      }, 10);
    }
  }
}