import { BaseSubscribable } from "../../subscribable/subscribable";

type Transform<TState, TResult> = {
  execute: () => Promise<TResult>;

  /** transform the state after the promise resolves */
  then?: (state: TState, result: TResult) => TState;

  /** transform the state during resolution and afterwards */
  optimistic?: (state: TState) => TState;

  /** transform the state only while loading */
  loading?: (state: TState, task: Promise<TResult>) => TState;
};

type PendingTransform<TState, TResult> = Transform<TState, TResult> & {
  task: Promise<TResult>;
};

const pipeTransforms = <TState, TExtra>(
  initialState: TState,
  extraParam: TExtra,
  transforms: (((state: TState, extra: TExtra) => TState) | undefined)[],
): TState => {
  return transforms.reduce((state, transform) => {
    return transform?.(state, extraParam) ?? state;
  }, initialState);
};

export class OptimisticState<TState> extends BaseSubscribable {
  private readonly _pendingTransforms: Array<PendingTransform<TState, any>> =
    [];

  /**
   * `optimistic` callbacks from transforms that have already resolved.
   * Re-applied after every `then` callback so that a wholesale state
   * replacement (e.g. list()) cannot erase earlier completed effects
   * (e.g. delete). Cleared when no pending transforms remain.
   *
   * Correctness requirement: `optimistic` callbacks must be idempotent.
   */
  private readonly _completedOptimistics: Array<(state: TState) => TState> = [];

  private _baseValue: TState;
  private _cachedValue: TState;

  public constructor(initialState: TState) {
    super();
    this._baseValue = initialState;
    this._cachedValue = initialState;
  }

  private _updateState(): void {
    this._cachedValue = this._pendingTransforms.reduce((state, transform) => {
      return pipeTransforms(state, transform.task, [
        transform.loading,
        transform.optimistic,
      ]);
    }, this._baseValue);

    this._notifySubscribers();
  }

  public get baseValue(): TState {
    return this._baseValue;
  }

  public get value(): TState {
    return this._cachedValue;
  }

  public update(state: TState): void {
    this._baseValue = state;
    this._updateState();
  }

  public async optimisticUpdate<TResult>(
    transform: Transform<TState, TResult>,
  ): Promise<TResult> {
    const task = transform.execute();
    const pendingTransform = { ...transform, task };
    try {
      this._pendingTransforms.push(pendingTransform);
      this._updateState();

      const result = await task;
      this._baseValue = pipeTransforms(this._baseValue, result, [
        transform.optimistic,
        transform.then,
      ]);

      // Re-apply previously completed optimistic callbacks so that a
      // then() that does wholesale replacement cannot erase their effects.
      for (const fn of this._completedOptimistics) {
        this._baseValue = fn(this._baseValue);
      }

      if (transform.optimistic) {
        this._completedOptimistics.push(transform.optimistic);
      }

      return result;
    } finally {
      const index = this._pendingTransforms.indexOf(pendingTransform);
      if (index > -1) {
        this._pendingTransforms.splice(index, 1);
      }

      if (this._pendingTransforms.length === 0) {
        this._completedOptimistics.length = 0;
      }

      this._updateState();
    }
  }
}
