/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {FiberRoot} from './ReactInternalTypes';
import type {GestureOptions} from 'shared/ReactTypes';
import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig';
import type {TransitionTypes} from 'react/src/ReactTransitionType';
import type {Lane} from './ReactFiberLane';

import {
  GestureLane,
  markRootEntangled,
  markRootFinished,
  NoLane,
  NoLanes,
} from './ReactFiberLane';
import {
  ensureRootIsScheduled,
  requestTransitionLane,
} from './ReactFiberRootScheduler';
import {getCurrentGestureOffset, stopViewTransition} from './ReactFiberConfig';
import {pingGestureRoot, restartGestureRoot} from './ReactFiberWorkLoop';

// This type keeps track of any scheduled or active gestures.
export type ScheduledGesture = {
  provider: GestureTimeline,
  count: number, // The number of times this same provider has been started.
  rangeStart: number, // The percentage along the timeline where the "current" state starts.
  rangeEnd: number, // The percentage along the timeline where the "destination" state is reached.
  types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition.
  running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
  commit: null | (() => void), // Callback to run to commit if there's a pending commit.
  committing: boolean, // If the gesture was released in a committed state and should actually commit.
  revertLane: Lane, // The Lane that we'll use to schedule the revert.
  prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
  next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
};

export function scheduleGesture(
  root: FiberRoot,
  provider: GestureTimeline,
): ScheduledGesture {
  let prev = root.pendingGestures;
  while (prev !== null) {
    if (prev.provider === provider) {
      // Existing instance found.
      return prev;
    }
    const next = prev.next;
    if (next === null) {
      break;
    }
    prev = next;
  }
  const gesture: ScheduledGesture = {
    provider: provider,
    count: 0,
    rangeStart: 0, // Uninitialized
    rangeEnd: 100, // Uninitialized
    types: null,
    running: null,
    commit: null,
    committing: false,
    revertLane: NoLane, // Starts uninitialized.
    prev: prev,
    next: null,
  };
  if (prev === null) {
    root.pendingGestures = gesture;
  } else {
    prev.next = gesture;
  }
  ensureRootIsScheduled(root);
  return gesture;
}

export function startScheduledGesture(
  root: FiberRoot,
  gestureTimeline: GestureTimeline,
  gestureOptions: ?GestureOptions,
  transitionTypes: null | TransitionTypes,
): null | ScheduledGesture {
  const rangeStart =
    gestureOptions && gestureOptions.rangeStart != null
      ? gestureOptions.rangeStart
      : getCurrentGestureOffset(gestureTimeline);
  const rangeEnd =
    gestureOptions && gestureOptions.rangeEnd != null
      ? gestureOptions.rangeEnd
      : rangeStart < 50
        ? 100
        : 0;
  let prev = root.pendingGestures;
  while (prev !== null) {
    if (prev.provider === gestureTimeline) {
      // Existing instance found.
      prev.count++;
      // Update the options.
      prev.rangeStart = rangeStart;
      prev.rangeEnd = rangeEnd;
      if (transitionTypes !== null) {
        let scheduledTypes = prev.types;
        if (scheduledTypes === null) {
          scheduledTypes = prev.types = [];
        }
        for (let i = 0; i < transitionTypes.length; i++) {
          const transitionType = transitionTypes[i];
          if (scheduledTypes.indexOf(transitionType) === -1) {
            scheduledTypes.push(transitionType);
          }
        }
      }
      return prev;
    }
    const next = prev.next;
    if (next === null) {
      break;
    }
    prev = next;
  }
  // No scheduled gestures. It must mean nothing for this renderer updated but
  // some other renderer might have updated.
  return null;
}

export function cancelScheduledGesture(
  root: FiberRoot,
  gesture: ScheduledGesture,
): void {
  // Entangle any Transitions started in this event with the revertLane of the gesture
  // so that we commit them all together.
  if (gesture.revertLane !== NoLane) {
    const entangledLanes = gesture.revertLane | requestTransitionLane(null);
    markRootEntangled(root, entangledLanes);
  }

  gesture.count--;
  if (gesture.count === 0) {
    // If the end state is closer to the end than the beginning then we commit into the
    // end state before reverting back (or applying a new Transition).
    // Otherwise we just revert back and don't commit.
    let shouldCommit: boolean;
    const finalOffset = getCurrentGestureOffset(gesture.provider);
    const rangeStart = gesture.rangeStart;
    const rangeEnd = gesture.rangeEnd;
    if (rangeStart < rangeEnd) {
      shouldCommit = finalOffset > rangeStart + (rangeEnd - rangeStart) / 2;
    } else {
      shouldCommit = finalOffset < rangeEnd + (rangeStart - rangeEnd) / 2;
    }
    // TODO: If we're currently rendering this gesture, we need to restart the render
    // on a different gesture or cancel the render..
    // TODO: We might want to pause the View Transition at this point since you should
    // no longer be able to update the position of anything but it might be better to
    // just commit the gesture state.
    const runningTransition = gesture.running;
    if (runningTransition !== null && shouldCommit) {
      // If we are going to commit this gesture in its to state, we need to wait to
      // stop it until it commits. We should now schedule a render at the gesture
      // lane to actually commit it.
      gesture.committing = true;
      if (root.pendingGestures === gesture) {
        const commitCallback = gesture.commit;
        if (commitCallback !== null) {
          gesture.commit = null;
          // If we already have a commit prepared we can immediately commit the tree
          // without rerendering.
          // TODO: Consider scheduling this in a task instead of synchronously inside the last cancellation.s
          commitCallback();
        } else {
          // Ping the root given the new state. This is similar to pingSuspendedRoot.
          pingGestureRoot(root);
        }
      }
    } else {
      // If we're not going to commit this gesture we can stop the View Transition
      // right away and delete the scheduled gesture from the pending queue.
      if (gesture.prev === null) {
        if (root.pendingGestures === gesture) {
          // This was the currently rendering gesture.
          root.pendingGestures = gesture.next;
          let remainingLanes = root.pendingLanes;
          if (root.pendingGestures === null) {
            // Gestures don't clear their lanes while the gesture is still active but it
            // might not be scheduled to do any more renders and so we shouldn't schedule
            // any more gesture lane work until a new gesture is scheduled.
            remainingLanes &= ~GestureLane;
          }
          markRootFinished(
            root,
            GestureLane,
            remainingLanes,
            NoLane,
            NoLane,
            NoLanes,
          );
          // If we had a currently rendering gesture we need to now reset the gesture lane to
          // now render the next gesture or cancel if there's no more gestures in the queue.
          restartGestureRoot(root);
        }
        gesture.running = null;
        if (runningTransition !== null) {
          stopViewTransition(runningTransition);
        }
      } else {
        // This was not the current gesture so it doesn't affect the current render.
        gesture.prev.next = gesture.next;
        if (gesture.next !== null) {
          gesture.next.prev = gesture.prev;
        }
        gesture.prev = null;
        gesture.next = null;
      }
    }
  }
}

export function stopCommittedGesture(root: FiberRoot) {
  // The top was just committed. We can delete it from the queue
  // and stop its View Transition now.
  const committedGesture = root.pendingGestures;
  if (committedGesture !== null) {
    // Mark it as no longer committing and should no longer be included in rerenders.
    committedGesture.committing = false;
    const nextGesture = committedGesture.next;
    if (nextGesture === null) {
      // Gestures don't clear their lanes while the gesture is still active but it
      // might not be scheduled to do any more renders and so we shouldn't schedule
      // any more gesture lane work until a new gesture is scheduled.
      root.pendingLanes &= ~GestureLane;
    } else {
      nextGesture.prev = null;
    }
    root.pendingGestures = nextGesture;
    const runningTransition = committedGesture.running;
    if (runningTransition !== null) {
      committedGesture.running = null;
      stopViewTransition(runningTransition);
    }
  }
}

export function scheduleGestureCommit(
  gesture: ScheduledGesture,
  callback: () => void,
): () => void {
  gesture.commit = callback;
  return function () {
    gesture.commit = null;
  };
}
