import type { GestureRef, GestureType } from './gesture';
import { BaseGesture, Gesture } from './gesture';

function extendRelation(
  currentRelation: GestureRef[] | undefined,
  extendWith: GestureType[]
) {
  if (currentRelation === undefined) {
    return [...extendWith];
  } else {
    return [...currentRelation, ...extendWith];
  }
}

/**
 * @deprecated `ComposedGesture` is deprecated and will be removed in the future. Please use `useCompetingGestures` instead.
 */
export class ComposedGesture extends Gesture {
  protected gestures: Gesture[] = [];
  protected simultaneousGestures: GestureType[] = [];
  protected requireGesturesToFail: GestureType[] = [];

  constructor(...gestures: Gesture[]) {
    super();
    this.gestures = gestures;
  }

  protected prepareSingleGesture(
    gesture: Gesture,
    simultaneousGestures: GestureType[],
    requireGesturesToFail: GestureType[]
  ) {
    if (gesture instanceof BaseGesture) {
      // Capture the relations defined directly on the gesture before composition
      // extends them, then always rebuild from that snapshot. Otherwise, when the
      // gesture is stable (e.g. wrapped in `useMemo`) but the composition is
      // recreated on every render, the relations would keep accumulating
      // references to gestures from previous renders, leaking memory (see #3763).
      // We keep the original references (instead of collapsing them to handler
      // tags) so relations can still be re-resolved after a remount, such as a
      // `react-freeze` unfreeze (see #4238).
      gesture.relationsSnapshot ??= {
        simultaneousWith: gesture.config.simultaneousWith,
        requireToFail: gesture.config.requireToFail,
      };

      const newConfig = { ...gesture.config };

      // No need to extend `blocksHandlers` here, because it's not changed in composition.
      // The same effect is achieved by reversing the order of 2 gestures in `Exclusive`
      newConfig.simultaneousWith = extendRelation(
        gesture.relationsSnapshot.simultaneousWith,
        simultaneousGestures
      );
      newConfig.requireToFail = extendRelation(
        gesture.relationsSnapshot.requireToFail,
        requireGesturesToFail
      );

      gesture.config = newConfig;
    } else if (gesture instanceof ComposedGesture) {
      gesture.simultaneousGestures = simultaneousGestures;
      gesture.requireGesturesToFail = requireGesturesToFail;
      gesture.prepare();
    }
  }

  prepare() {
    for (const gesture of this.gestures) {
      this.prepareSingleGesture(
        gesture,
        this.simultaneousGestures,
        this.requireGesturesToFail
      );
    }
  }

  initialize() {
    for (const gesture of this.gestures) {
      gesture.initialize();
    }
  }

  toGestureArray(): GestureType[] {
    return this.gestures.flatMap((gesture) => gesture.toGestureArray());
  }
}

/**
 * @deprecated `SimultaneousGesture` is deprecated and will be removed in the future. Please use `useSimultaneousGestures` instead.
 */
export class SimultaneousGesture extends ComposedGesture {
  override prepare() {
    // This piece of magic works something like this:
    // for every gesture in the array
    const simultaneousArrays = this.gestures.map((gesture) =>
      // we take the array it's in
      this.gestures
        // and make a copy without it
        .filter((x) => x !== gesture)
        // then we flatmap the result to get list of raw (not composed) gestures
        // this way we don't make the gestures simultaneous with themselves, which is
        // important when the gesture is `ExclusiveGesture` - we don't want to make
        // exclusive gestures simultaneous
        .flatMap((x) => x.toGestureArray())
    );

    for (let i = 0; i < this.gestures.length; i++) {
      this.prepareSingleGesture(
        this.gestures[i],
        simultaneousArrays[i],
        this.requireGesturesToFail
      );
    }
  }
}

/**
 * @deprecated `ExclusiveGesture` is deprecated and will be removed in the future. Please use `useExclusiveGestures` instead.
 */
export class ExclusiveGesture extends ComposedGesture {
  override prepare() {
    // Transforms the array of gestures into array of grouped raw (not composed) gestures
    // i.e. [gesture1, gesture2, ComposedGesture(gesture3, gesture4)] -> [[gesture1], [gesture2], [gesture3, gesture4]]
    const gestureArrays = this.gestures.map((gesture) =>
      gesture.toGestureArray()
    );

    let requireToFail: GestureType[] = [];

    for (let i = 0; i < this.gestures.length; i++) {
      this.prepareSingleGesture(
        this.gestures[i],
        this.simultaneousGestures,
        this.requireGesturesToFail.concat(requireToFail)
      );

      // Every group gets to wait for all groups before it
      requireToFail = requireToFail.concat(gestureArrays[i]);
    }
  }
}

/**
 * @deprecated `ComposedGestureType` is deprecated and will be removed in the future. Please use `ComposedGesture` instead.
 */
export type ComposedGestureType = InstanceType<typeof ComposedGesture>;
/**
 * @deprecated `RaceGestureType` is deprecated and will be removed in the future. Please use `ComposedGesture` instead.
 */
export type RaceGestureType = ComposedGestureType;
/**
 * @deprecated `SimultaneousGestureType` is deprecated and will be removed in the future. Please use `ComposedGesture` instead.
 */
export type SimultaneousGestureType = InstanceType<typeof SimultaneousGesture>;
/**
 * @deprecated `ExclusiveGestureType` is deprecated and will be removed in the future. Please use `ComposedGesture` instead.
 */
export type ExclusiveGestureType = InstanceType<typeof ExclusiveGesture>;
