/**
 * 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 * as React from 'react';
import {useContext, useEffect, useRef} from 'react';
import {BridgeContext, StoreContext} from '../context';
import {TreeDispatcherContext} from '../Components/TreeContext';
import {useHighlightHostInstance, useScrollToHostInstance} from '../hooks';
import {
  SuspenseTreeDispatcherContext,
  SuspenseTreeStateContext,
} from './SuspenseTreeContext';
import styles from './SuspenseTimeline.css';
import SuspenseScrubber from './SuspenseScrubber';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';

function SuspenseTimelineInput() {
  const bridge = useContext(BridgeContext);
  const store = useContext(StoreContext);
  const treeDispatch = useContext(TreeDispatcherContext);
  const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
  const {highlightHostInstance, clearHighlightHostInstance} =
    useHighlightHostInstance();
  const scrollToHostInstance = useScrollToHostInstance();

  const {
    selectedRootID: rootID,
    timeline,
    timelineIndex,
    hoveredTimelineIndex,
    playing,
  } = useContext(SuspenseTreeStateContext);

  const min = 0;
  const max = timeline.length > 0 ? timeline.length - 1 : 0;

  function switchSuspenseNode(nextTimelineIndex: number) {
    const nextSelectedSuspenseID = timeline[nextTimelineIndex];
    highlightHostInstance(nextSelectedSuspenseID);
    treeDispatch({
      type: 'SELECT_ELEMENT_BY_ID',
      payload: nextSelectedSuspenseID,
    });
    suspenseTreeDispatch({
      type: 'SUSPENSE_SET_TIMELINE_INDEX',
      payload: nextTimelineIndex,
    });
  }

  function handleChange(pendingTimelineIndex: number) {
    switchSuspenseNode(pendingTimelineIndex);
  }

  function handleBlur() {
    clearHighlightHostInstance();
  }

  function handleFocus() {
    switchSuspenseNode(timelineIndex);
  }

  function handleHoverSegment(hoveredValue: number) {
    const suspenseID = timeline[hoveredValue];
    if (suspenseID === undefined) {
      throw new Error(
        `Suspense node not found for value ${hoveredValue} in timeline.`,
      );
    }
    highlightHostInstance(suspenseID);
  }

  function skipPrevious() {
    const nextSelectedSuspenseID = timeline[timelineIndex - 1];
    treeDispatch({
      type: 'SELECT_ELEMENT_BY_ID',
      payload: nextSelectedSuspenseID,
    });
    suspenseTreeDispatch({
      type: 'SUSPENSE_SKIP_TIMELINE_INDEX',
      payload: false,
    });
  }

  function skipForward() {
    const nextSelectedSuspenseID = timeline[timelineIndex + 1];
    treeDispatch({
      type: 'SELECT_ELEMENT_BY_ID',
      payload: nextSelectedSuspenseID,
    });
    suspenseTreeDispatch({
      type: 'SUSPENSE_SKIP_TIMELINE_INDEX',
      payload: true,
    });
  }

  function togglePlaying() {
    suspenseTreeDispatch({
      type: 'SUSPENSE_PLAY_PAUSE',
      payload: 'toggle',
    });
  }

  const isInitialMount = useRef(true);
  // TODO: useEffectEvent here once it's supported in all versions DevTools supports.
  // For now we just exclude it from deps since we don't lint those anyway.
  function changeTimelineIndex(newIndex: number) {
    // Synchronize timeline index with what is resuspended.
    if (rootID === null) {
      return;
    }
    const rendererID = store.getRendererIDForElement(rootID);
    if (rendererID === null) {
      console.error(
        `No renderer ID found for root element ${rootID} in suspense timeline.`,
      );
      return;
    }
    // We suspend everything after the current selection. The root isn't showing
    // anything suspended in the root. The step after that should have one less
    // thing suspended. I.e. the first suspense boundary should be unsuspended
    // when it's selected. This also lets you show everything in the last step.
    const suspendedSet = timeline.slice(timelineIndex + 1);
    bridge.send('overrideSuspenseMilestone', {
      rendererID,
      rootID,
      suspendedSet,
    });
    if (isInitialMount.current) {
      // Skip scrolling on initial mount. Only when we're changing the timeline.
      isInitialMount.current = false;
    } else {
      // When we're scrubbing through the timeline, scroll the current boundary
      // into view as it was just revealed. This is after we override the milestone
      // to reveal it.
      const selectedSuspenseID = timeline[timelineIndex];
      scrollToHostInstance(selectedSuspenseID);
    }
  }

  useEffect(() => {
    changeTimelineIndex(timelineIndex);
  }, [timelineIndex]);

  useEffect(() => {
    if (!playing) {
      return undefined;
    }
    // While playing, advance one step every second.
    const PLAY_SPEED_INTERVAL = 1000;
    const timer = setInterval(() => {
      suspenseTreeDispatch({
        type: 'SUSPENSE_PLAY_TICK',
      });
    }, PLAY_SPEED_INTERVAL);
    return () => {
      clearInterval(timer);
    };
  }, [playing]);

  if (rootID === null) {
    return (
      <div className={styles.SuspenseTimelineInput}>No root selected.</div>
    );
  }

  if (!store.supportsTogglingSuspense(rootID)) {
    return (
      <div className={styles.SuspenseTimelineInput}>
        Can't step through Suspense in production apps.
      </div>
    );
  }

  if (timeline.length === 0) {
    return (
      <div className={styles.SuspenseTimelineInput}>
        Root contains no Suspense nodes.
      </div>
    );
  }

  return (
    <>
      <Button
        disabled={timelineIndex === 0}
        title={'Previous'}
        onClick={skipPrevious}>
        <ButtonIcon type={'skip-previous'} />
      </Button>
      <Button
        disabled={max === 0 && !playing}
        title={playing ? 'Pause' : 'Play'}
        onClick={togglePlaying}>
        <ButtonIcon type={playing ? 'pause' : 'play'} />
      </Button>
      <Button
        disabled={timelineIndex === max}
        title={'Next'}
        onClick={skipForward}>
        <ButtonIcon type={'skip-next'} />
      </Button>
      <div
        className={styles.SuspenseTimelineInput}
        title={timelineIndex + '/' + max}>
        <SuspenseScrubber
          min={min}
          max={max}
          value={timelineIndex}
          highlight={hoveredTimelineIndex}
          onBlur={handleBlur}
          onChange={handleChange}
          onFocus={handleFocus}
          onHoverSegment={handleHoverSegment}
          onHoverLeave={clearHighlightHostInstance}
        />
      </div>
    </>
  );
}

export default function SuspenseTimeline(): React$Node {
  const {selectedRootID} = useContext(SuspenseTreeStateContext);
  return (
    <div className={styles.SuspenseTimelineContainer}>
      <SuspenseTimelineInput key={selectedRootID} />
    </div>
  );
}
