Загрузка данных


import { BehaviorSubject, Observable } from 'rxjs';

import {
  ChartSeriesType,
  Intervals,
  SymbolInfo,
  TimeFormat,
  Timeframes,
} from '@src/types';
import { DateFormat } from '@src/utils';

export type UndoKey = keyof UndoConfig;

export interface UndoRedoState {
  canUndo: boolean;
  canRedo: boolean;
}

interface UndoConfig {
  timeframe: (value: Timeframes) => void;
  seriesSelected: (value: ChartSeriesType) => void;
  symbolInfo: (value: SymbolInfo) => void;
  timeFormat: (value: TimeFormat) => void;
  dateFormat: (value: DateFormat) => void;
  interval: (value: Intervals | null) => void;
}

interface HistoryItem {
  type: 'item';
  key: UndoKey;
  previousValue: unknown;
  nextValue: unknown;
}

interface HistoryCommand {
  type: 'command';
  undo: () => void;
  redo: () => void;
}

interface HistoryGroup {
  type: 'group';
  entries: HistoryEntry[];
}

type HistoryEntry =
  | HistoryItem
  | HistoryCommand
  | HistoryGroup;

interface PushCommandParams {
  undo: () => void;
  redo: () => void;
}

export class UndoRedo {
  private undoStack: HistoryEntry[] = [];
  private redoStack: HistoryEntry[] = [];
  private groupStack: HistoryEntry[][] = [];

  private state$ = new BehaviorSubject<UndoRedoState>({
    canUndo: false,
    canRedo: false,
  });

  constructor(private readonly config: UndoConfig) {}

  public push(
    key: UndoKey,
    previousValue: unknown,
    nextValue: unknown,
  ): void {
    if (Object.is(previousValue, nextValue)) {
      return;
    }

    this.pushEntry({
      type: 'item',
      key,
      previousValue,
      nextValue,
    });
  }

  public pushCommand({
    undo,
    redo,
  }: PushCommandParams): void {
    this.pushEntry({
      type: 'command',
      undo,
      redo,
    });
  }

  public group<T>(callback: () => T): T {
    this.groupStack.push([]);

    try {
      return callback();
    } finally {
      this.completeGroup();
    }
  }

  public undo = (): void => {
    const entry = this.undoStack.pop();

    if (!entry) {
      return;
    }

    this.applyEntry(entry, 'undo');
    this.redoStack.push(entry);
    this.updateState();
  };

  public redo = (): void => {
    const entry = this.redoStack.pop();

    if (!entry) {
      return;
    }

    this.applyEntry(entry, 'redo');
    this.undoStack.push(entry);
    this.updateState();
  };

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

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

  public getState(): Observable<UndoRedoState> {
    return this.state$.asObservable();
  }

  public clear(): void {
    this.undoStack = [];
    this.redoStack = [];
    this.groupStack = [];

    this.updateState();
  }

  private completeGroup(): void {
    const entries = this.groupStack.pop();

    if (!entries?.length) {
      return;
    }

    this.pushEntry({
      type: 'group',
      entries,
    });
  }

  private pushEntry(entry: HistoryEntry): void {
    if (this.redoStack.length > 0) {
      this.redoStack = [];
    }

    const currentGroup =
      this.groupStack[this.groupStack.length - 1];

    if (currentGroup) {
      currentGroup.push(entry);

      return;
    }

    this.undoStack.push(entry);
    this.updateState();
  }

  private applyEntry(
    entry: HistoryEntry,
    direction: 'undo' | 'redo',
  ): void {
    if (entry.type === 'group') {
      this.applyGroup(entry, direction);

      return;
    }

    if (entry.type === 'command') {
      entry[direction]();

      return;
    }

    const apply = this.config[entry.key] as (
      value: unknown,
    ) => void;

    apply(
      direction === 'undo'
        ? entry.previousValue
        : entry.nextValue,
    );
  }

  private applyGroup(
    group: HistoryGroup,
    direction: 'undo' | 'redo',
  ): void {
    if (direction === 'undo') {
      for (
        let index = group.entries.length - 1;
        index >= 0;
        index -= 1
      ) {
        this.applyEntry(
          group.entries[index],
          direction,
        );
      }

      return;
    }

    group.entries.forEach((entry) => {
      this.applyEntry(entry, direction);
    });
  }

  private updateState(): void {
    const nextState: UndoRedoState = {
      canUndo: this.canUndo(),
      canRedo: this.canRedo(),
    };

    const currentState = this.state$.value;

    if (
      currentState.canUndo === nextState.canUndo &&
      currentState.canRedo === nextState.canRedo
    ) {
      return;
    }

    this.state$.next(nextState);
  }
}