// [!!!] IMPORTANT: do not import React in this file
// since it will be executed before the react devtools hook is created

import type * as React from "react";

import type {
  ContextDependency,
  Fiber,
  FiberRoot,
  MemoizedState,
  ReactDevToolsGlobalHook,
  ReactRenderer,
} from "./types.js";

import {
  BIPPY_INSTRUMENTATION_STRING,
  getRDTHook,
  hasRDTHook,
  isReactRefresh,
  isRealReactDevtools,
} from "./rdt-hook.js";

// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactWorkTags.js
export const FunctionComponentTag = 0;
export const ClassComponentTag = 1;
export const HostRootTag = 3;
export const HostPortalTag = 4;
export const HostComponentTag = 5;
export const HostTextTag = 6;
export const FragmentTag = 7;
export const ContextConsumerTag = 9;
export const ForwardRefTag = 11;
export const SuspenseComponentTag = 13;
export const MemoComponentTag = 14;
export const SimpleMemoComponentTag = 15;
export const LazyComponentTag = 16;
export const DehydratedSuspenseComponentTag = 18;
export const SuspenseListComponentTag = 19;
export const OffscreenComponentTag = 22;
export const LegacyHiddenComponentTag = 23;
export const HostHoistableTag = 26;
export const HostSingletonTag = 27;
export const ActivityComponentTag = 28;
export const ViewTransitionComponentTag = 30;

export const CONCURRENT_MODE_NUMBER = 0xeacf;
export const ELEMENT_TYPE_SYMBOL_STRING = "Symbol(react.element)";
export const TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING = "Symbol(react.transitional.element)";
export const CONCURRENT_MODE_SYMBOL_STRING = "Symbol(react.concurrent_mode)";
export const DEPRECATED_ASYNC_MODE_SYMBOL_STRING = "Symbol(react.async_mode)";

// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberFlags.js
const PerformedWork = 0b1;
const Placement = 0b10;
const Hydrating = 0b1000000000000;
const Update = 0b100;
const Cloned = 0b1000;
const ChildDeletion = 0b10000;
const ContentReset = 0b100000;
const Snapshot = 0b10000000000;
const Visibility = 0b10000000000000;
const MutationMask =
  Placement | Update | ChildDeletion | ContentReset | Hydrating | Visibility | Snapshot;

/**
 * Returns `true` if object is a React Element.
 *
 * @see https://react.dev/reference/react/isValidElement
 */
export const isValidElement = (element: unknown): element is React.ReactElement =>
  typeof element === "object" &&
  element != null &&
  "$$typeof" in element &&
  // react 18 uses Symbol.for('react.element'), react 19 uses Symbol.for('react.transitional.element')
  [ELEMENT_TYPE_SYMBOL_STRING, TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING].includes(
    String(element.$$typeof),
  );

/**
 * Returns `true` if object is a React Fiber.
 */
export const isValidFiber = (fiber: unknown): fiber is Fiber =>
  typeof fiber === "object" &&
  fiber != null &&
  "tag" in fiber &&
  "stateNode" in fiber &&
  "return" in fiber &&
  "child" in fiber &&
  "sibling" in fiber &&
  "flags" in fiber;

/**
 * Returns `true` if fiber is a host fiber. Host fibers are DOM nodes in react-dom, `View` in react-native, etc.
 *
 * @see https://reactnative.dev/architecture/glossary#host-view-tree-and-host-view
 */
export const isHostFiber = (fiber: Fiber): boolean => {
  switch (fiber.tag) {
    case HostComponentTag:
    // @ts-expect-error: it exists
    case HostHoistableTag:
    // @ts-expect-error: it exists
    case HostSingletonTag:
      return true;
    default:
      return typeof fiber.type === "string";
  }
};

/**
 * Returns `true` if fiber is a composite fiber. Composite fibers are fibers that can render (like functional components, class components, etc.)
 *
 * @see https://reactnative.dev/architecture/glossary#react-composite-components
 */
export const isCompositeFiber = (fiber: Fiber): boolean => {
  switch (fiber.tag) {
    case ClassComponentTag:
    case ForwardRefTag:
    case FunctionComponentTag:
    case MemoComponentTag:
    case SimpleMemoComponentTag:
      return true;
    default:
      return false;
  }
};

/**
 * Returns `true` if the object is a {@link Fiber}
 */
export const isFiber = (maybeFiber: unknown): maybeFiber is Fiber => {
  if (!maybeFiber || typeof maybeFiber !== "object") return false;
  // this is a fast check. pendingProps will ALWAYS exist in fiber
  // `containerInfo` is in FiberRootNode, not FiberNode
  return "pendingProps" in maybeFiber && !("containerInfo" in maybeFiber);
};

/**
 * Returns `true` if the two {@link Fiber}s are the same reference
 */
export const areFiberEqual = (fiberA: Fiber, fiberB: Fiber): boolean => {
  return fiberA === fiberB || fiberA.alternate === fiberB || fiberB.alternate === fiberA;
};

/**
 * Traverses up or down a {@link Fiber}'s contexts, return `true` to stop and select the current and previous context value.
 */
export const traverseContexts = (
  fiber: Fiber,
  selector: (
    nextValue: ContextDependency<unknown> | null | undefined,
    prevValue: ContextDependency<unknown> | null | undefined,
  ) => boolean | void,
): boolean => {
  try {
    const nextDependencies = fiber.dependencies;
    const prevDependencies = fiber.alternate?.dependencies;

    if (!nextDependencies || !prevDependencies) return false;
    if (
      typeof nextDependencies !== "object" ||
      !("firstContext" in nextDependencies) ||
      typeof prevDependencies !== "object" ||
      !("firstContext" in prevDependencies)
    ) {
      return false;
    }
    let nextContext: ContextDependency<unknown> | null | undefined = nextDependencies.firstContext;
    let prevContext: ContextDependency<unknown> | null | undefined = prevDependencies.firstContext;
    while (
      (nextContext && typeof nextContext === "object" && "memoizedValue" in nextContext) ||
      (prevContext && typeof prevContext === "object" && "memoizedValue" in prevContext)
    ) {
      if (selector(nextContext, prevContext) === true) return true;

      nextContext = nextContext?.next;
      prevContext = prevContext?.next;
    }
  } catch {}
  return false;
};

/**
 * Traverses up or down a {@link Fiber}'s states, return `true` to stop and select the current and previous state value. This stores both state values and effects.
 */
export const traverseState = (
  fiber: Fiber,
  selector: (
    nextValue: MemoizedState | null | undefined,
    prevValue: MemoizedState | null | undefined,
  ) => boolean | void,
): boolean => {
  try {
    let nextState: MemoizedState | null | undefined = fiber.memoizedState;
    let prevState: MemoizedState | null | undefined = fiber.alternate?.memoizedState;

    while (nextState || prevState) {
      if (selector(nextState, prevState) === true) return true;

      nextState = nextState?.next;
      prevState = prevState?.next;
    }
  } catch {}
  return false;
};

/**
 * Traverses up or down a {@link Fiber}'s props, return `true` to stop and select the current and previous props value.
 */
export const traverseProps = (
  fiber: Fiber,
  selector: (propName: string, nextValue: unknown, prevValue: unknown) => boolean | void,
): boolean => {
  try {
    const nextProps = fiber.memoizedProps;
    const prevProps = fiber.alternate?.memoizedProps || {};

    const allKeys = new Set([...Object.keys(nextProps), ...Object.keys(prevProps)]);

    for (const propName of allKeys) {
      const prevValue = prevProps?.[propName];
      const nextValue = nextProps?.[propName];

      if (selector(propName, nextValue, prevValue) === true) return true;
    }
  } catch {}
  return false;
};

/**
 * Returns `true` if the {@link Fiber} has rendered. Note that this does not mean the fiber has rendered in the current commit, just that it has rendered in the past.
 */
export const didFiberRender = (fiber: Fiber): boolean => {
  const nextProps = fiber.memoizedProps;
  const prevProps = fiber.alternate?.memoizedProps || {};
  const flags = fiber.flags ?? (fiber as unknown as { effectTag: number }).effectTag ?? 0;

  switch (fiber.tag) {
    case ClassComponentTag:
    case ContextConsumerTag:
    case ForwardRefTag:
    case FunctionComponentTag:
    case MemoComponentTag:
    case SimpleMemoComponentTag: {
      return (flags & PerformedWork) === PerformedWork;
    }
    default:
      // Host nodes (DOM, root, etc.)
      if (!fiber.alternate) return true;
      return (
        prevProps !== nextProps ||
        fiber.alternate.memoizedState !== fiber.memoizedState ||
        fiber.alternate.ref !== fiber.ref
      );
  }
};

/**
 * Returns `true` if the {@link Fiber} has committed. Note that this does not mean the fiber has committed in the current commit, just that it has committed in the past.
 */
export const didFiberCommit = (fiber: Fiber): boolean => {
  return Boolean(
    (fiber.flags & (MutationMask | Cloned)) !== 0 ||
    (fiber.subtreeFlags & (MutationMask | Cloned)) !== 0,
  );
};

/**
 * Returns all host {@link Fiber}s that have committed and rendered.
 */
export const getMutatedHostFibers = (fiber: Fiber): Fiber[] => {
  const mutations: Fiber[] = [];
  const stack: Fiber[] = [fiber];

  while (stack.length) {
    const node = stack.pop();
    if (!node) continue;

    if (isHostFiber(node) && didFiberCommit(node) && didFiberRender(node)) {
      mutations.push(node);
    }

    if (node.child) stack.push(node.child);
    if (node.sibling) stack.push(node.sibling);
  }

  return mutations;
};

/**
 * Returns the stack of {@link Fiber}s from the current fiber to the root fiber.
 *
 * @example
 * ```ts
 * [fiber, fiber.return, fiber.return.return, ...]
 * ```
 */
export const getFiberStack = (fiber: Fiber): Fiber[] => {
  const stack: Fiber[] = [];
  let currentFiber = fiber;
  while (currentFiber.return) {
    stack.push(currentFiber);
    currentFiber = currentFiber.return as Fiber;
  }
  return stack;
};

/**
 * Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
 */
export const shouldFilterFiber = (fiber: Fiber): boolean => {
  switch (fiber.tag) {
    case DehydratedSuspenseComponentTag:
      // TODO: ideally we would show dehydrated Suspense immediately.
      // However, it has some special behavior (like disconnecting
      // an alternate and turning into real Suspense) which breaks DevTools.
      // For now, ignore it, and only show it once it gets hydrated.
      // https://github.com/bvaughn/react-devtools-experimental/issues/197
      return true;

    case FragmentTag:
    case HostTextTag:
    case LegacyHiddenComponentTag:
    case OffscreenComponentTag:
      return true;

    case HostRootTag:
      // It is never valid to filter the root element.
      return false;

    default: {
      const symbolOrNumber =
        typeof fiber.type === "object" && fiber.type !== null ? fiber.type.$$typeof : fiber.type;

      const typeSymbol =
        typeof symbolOrNumber === "symbol" ? symbolOrNumber.toString() : symbolOrNumber;

      switch (typeSymbol) {
        case CONCURRENT_MODE_NUMBER:
        case CONCURRENT_MODE_SYMBOL_STRING:
        case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
          return true;

        default:
          return false;
      }
    }
  }
};

/**
 * Returns the nearest host {@link Fiber} to the current {@link Fiber}.
 */
export const getNearestHostFiber = (fiber: Fiber, ascending = false): Fiber | null => {
  let hostFiber = traverseFiber(fiber, isHostFiber, ascending);
  if (!hostFiber) {
    hostFiber = traverseFiber(fiber, isHostFiber, !ascending);
  }
  return hostFiber;
};

/**
 * Returns all host {@link Fiber}s in the tree that are associated with the current {@link Fiber}.
 */
export const getNearestHostFibers = (fiber: Fiber): Fiber[] => {
  const hostFibers: Fiber[] = [];
  const stack: Fiber[] = [];

  if (isHostFiber(fiber)) {
    hostFibers.push(fiber);
  } else if (fiber.child) {
    stack.push(fiber.child);
  }

  while (stack.length) {
    const currentNode = stack.pop();
    if (!currentNode) break;
    if (isHostFiber(currentNode)) {
      hostFibers.push(currentNode);
    } else if (currentNode.child) {
      stack.push(currentNode.child);
    }

    if (currentNode.sibling) {
      stack.push(currentNode.sibling);
    }
  }

  return hostFibers;
};

/**
 * Traverses up or down a {@link Fiber}, return `true` to stop and select a node.
 */
export function traverseFiber(
  fiber: Fiber | null,
  selector: (node: Fiber) => boolean | void,
  ascending?: boolean,
): Fiber | null;
export function traverseFiber(
  fiber: Fiber | null,
  selector: (node: Fiber) => Promise<boolean | void>,
  ascending?: boolean,
): Promise<Fiber | null>;
export function traverseFiber(
  fiber: Fiber | null,
  selector: (node: Fiber) => boolean | Promise<boolean | void> | void,
  ascending = false,
): Fiber | null | Promise<Fiber | null> {
  if (!fiber) return null;

  const firstResult = selector(fiber);
  if (firstResult instanceof Promise) {
    return (async () => {
      if ((await firstResult) === true) return fiber;

      let child = ascending ? fiber.return : fiber.child;
      while (child) {
        const match = await traverseFiberAsync(
          child,
          selector as (node: Fiber) => Promise<boolean | void>,
          ascending,
        );
        if (match) return match;
        child = ascending ? null : child.sibling;
      }
      return null;
    })();
  }

  if (firstResult === true) return fiber;

  let child = ascending ? fiber.return : fiber.child;
  while (child) {
    const match = traverseFiberSync(child, selector as (node: Fiber) => boolean | void, ascending);
    if (match) return match;
    child = ascending ? null : child.sibling;
  }
  return null;
}

export const traverseFiberSync = (
  fiber: Fiber | null,
  selector: (node: Fiber) => boolean | void,
  ascending = false,
): Fiber | null => {
  if (!fiber) return null;
  if (selector(fiber) === true) return fiber;

  let child = ascending ? fiber.return : fiber.child;
  while (child) {
    const match = traverseFiberSync(child, selector, ascending);
    if (match) return match;

    child = ascending ? null : child.sibling;
  }
  return null;
};

export const traverseFiberAsync = async (
  fiber: Fiber | null,
  selector: (node: Fiber) => Promise<boolean | void>,
  ascending = false,
): Promise<Fiber | null> => {
  if (!fiber) return null;
  if ((await selector(fiber)) === true) return fiber;

  let child = ascending ? fiber.return : fiber.child;
  while (child) {
    const match = await traverseFiberAsync(child, selector, ascending);
    if (match) return match;

    child = ascending ? null : child.sibling;
  }
  return null;
};

/**
 * Returns the timings of the {@link Fiber}.
 *
 * @example
 * ```ts
 * const { selfTime, totalTime } = getTimings(fiber);
 * console.log(selfTime, totalTime);
 * ```
 */
export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime: number } => {
  const totalTime = fiber?.actualDuration ?? 0;
  let selfTime = totalTime;
  // TODO: calculate a DOM time, which is just host component summed up
  let child = fiber?.child ?? null;
  while (totalTime > 0 && child != null) {
    selfTime -= child.actualDuration ?? 0;
    child = child.sibling;
  }
  return { selfTime, totalTime };
};

/**
 * Returns `true` if the {@link Fiber} uses React Compiler's memo cache.
 */
export const hasMemoCache = (fiber: Fiber): boolean => {
  return Boolean((fiber.updateQueue as unknown as { memoCache: unknown })?.memoCache);
};

type FiberType =
  | React.ComponentType<unknown>
  | React.ForwardRefExoticComponent<unknown>
  | React.MemoExoticComponent<React.ComponentType<unknown>>;

/**
 * Returns the type (e.g. component definition) of the {@link Fiber}
 */
export const getType = (type: unknown): null | React.ComponentType<unknown> => {
  const currentType = type as FiberType;
  if (typeof currentType === "function") {
    return currentType;
  }
  if (typeof currentType === "object" && currentType) {
    // memo / forwardRef case
    return getType(
      (currentType as React.MemoExoticComponent<React.ComponentType<unknown>>).type ||
        (currentType as { render: React.ComponentType<unknown> }).render,
    );
  }
  return null;
};

/**
 * Returns the display name of the {@link Fiber} type.
 */
export const getDisplayName = (type: unknown): null | string => {
  const currentType = type as FiberType;
  if (typeof currentType === "string") {
    return currentType;
  }
  if (typeof currentType !== "function" && !(typeof currentType === "object" && currentType)) {
    return null;
  }
  const name = currentType.displayName || currentType.name || null;
  if (name) return name;
  const unwrappedType = getType(currentType);
  if (!unwrappedType) return null;
  return unwrappedType.displayName || unwrappedType.name || null;
};

/**
 * Returns the build type of the React renderer.
 */
export const detectReactBuildType = (renderer: ReactRenderer): "development" | "production" => {
  try {
    if (typeof renderer.version === "string" && renderer.bundleType > 0) {
      return "development";
    }
  } catch {}
  return "production";
};

/**
 * Returns `true` if bippy's instrumentation is active.
 */
export const isInstrumentationActive = (): boolean => {
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
  return (
    Boolean(rdtHook?._instrumentationIsActive) ||
    isRealReactDevtools(rdtHook) ||
    isReactRefresh(rdtHook)
  );
};

/**
 * Returns the latest fiber (since it may be double-buffered).
 */
export const getLatestFiber = (fiber: Fiber): Fiber => {
  const alternate = fiber.alternate;
  if (!alternate) return fiber;
  if (alternate.actualStartTime && fiber.actualStartTime) {
    return alternate.actualStartTime > fiber.actualStartTime ? alternate : fiber;
  }
  for (const root of _fiberRoots) {
    const latestFiber = traverseFiber(root.current, (innerFiber) => {
      if (innerFiber === fiber) return true;
    });
    if (latestFiber) return latestFiber;
  }
  return fiber;
};

export type RenderHandler = <S>(fiber: Fiber, phase: RenderPhase, state?: S) => unknown;

export type RenderPhase = "mount" | "unmount" | "update";

let fiberId = 0;
export const fiberIdMap = new WeakMap<Fiber, number>();

export const setFiberId = (fiber: Fiber, id: number = fiberId++): void => {
  fiberIdMap.set(fiber, id);
};

// react fibers are double buffered, so the alternate fiber may
// be switched to the current fiber and vice versa.
// fiber === fiber.alternate.alternate
export const getFiberId = (fiber: Fiber): number => {
  let id = fiberIdMap.get(fiber);
  if (!id && fiber.alternate) {
    id = fiberIdMap.get(fiber.alternate);
  }
  if (!id) {
    id = fiberId++;
    setFiberId(fiber, id);
  }
  return id;
};

export const mountFiberRecursively = (
  onRender: RenderHandler,
  firstChild: Fiber,
  traverseSiblings: boolean,
): void => {
  let fiber: Fiber | null = firstChild;

  while (fiber != null) {
    if (!fiberIdMap.has(fiber)) {
      getFiberId(fiber);
    }
    const shouldIncludeInTree = !shouldFilterFiber(fiber);
    if (shouldIncludeInTree && didFiberRender(fiber)) {
      onRender(fiber, "mount");
    }

    if (fiber.tag === SuspenseComponentTag) {
      const isTimedOut = fiber.memoizedState !== null;
      if (isTimedOut) {
        // Special case: if Suspense mounts in a timed-out state,
        // get the fallback child from the inner fragment and mount
        // it as if it was our own child. Updates handle this too.
        const primaryChildFragment = fiber.child;
        const fallbackChildFragment = primaryChildFragment ? primaryChildFragment.sibling : null;
        if (fallbackChildFragment) {
          const fallbackChild = fallbackChildFragment.child;
          if (fallbackChild !== null) {
            mountFiberRecursively(onRender, fallbackChild, false);
          }
        }
      } else {
        let primaryChild: Fiber | null = null;
        const areSuspenseChildrenConditionallyWrapped = (OffscreenComponentTag as number) === -1;
        if (areSuspenseChildrenConditionallyWrapped) {
          primaryChild = fiber.child;
        } else if (fiber.child !== null) {
          primaryChild = fiber.child.child;
        }
        if (primaryChild !== null) {
          mountFiberRecursively(onRender, primaryChild, false);
        }
      }
    } else if (fiber.child != null) {
      mountFiberRecursively(onRender, fiber.child, true);
    }
    fiber = traverseSiblings ? fiber.sibling : null;
  }
};

export const updateFiberRecursively = (
  onRender: RenderHandler,
  nextFiber: Fiber,
  prevFiber: Fiber,
  parentFiber: Fiber | null,
): void => {
  if (!fiberIdMap.has(nextFiber)) {
    getFiberId(nextFiber);
  }
  if (!prevFiber) return;
  if (!fiberIdMap.has(prevFiber)) {
    getFiberId(prevFiber);
  }

  const isSuspense = nextFiber.tag === SuspenseComponentTag;

  const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
  if (shouldIncludeInTree && didFiberRender(nextFiber)) {
    onRender(nextFiber, "update");
  }

  // The behavior of timed-out Suspense trees is unique.
  // Rather than unmount the timed out content (and possibly lose important state),
  // React re-parents this content within a hidden Fragment while the fallback is showing.
  // This behavior doesn't need to be observable in the DevTools though.
  // It might even result in a bad user experience for e.g. node selection in the Elements panel.
  // The easiest fix is to strip out the intermediate Fragment fibers,
  // so the Elements panel and Profiler don't need to special case them.
  // Suspense components only have a non-null memoizedState if they're timed-out.
  const prevDidTimeout = isSuspense && prevFiber.memoizedState !== null;
  const nextDidTimeOut = isSuspense && nextFiber.memoizedState !== null;

  // The logic below is inspired by the code paths in updateSuspenseComponent()
  // inside ReactFiberBeginWork in the React source code.
  if (prevDidTimeout && nextDidTimeOut) {
    // Fallback -> Fallback:
    // 1. Reconcile fallback set.
    const nextFallbackChildSet = nextFiber.child?.sibling ?? null;
    // Note: We can't use nextFiber.child.sibling.alternate
    // because the set is special and alternate may not exist.
    const prevFallbackChildSet = prevFiber.child?.sibling ?? null;

    if (nextFallbackChildSet !== null && prevFallbackChildSet !== null) {
      updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet, nextFiber);
    }
  } else if (prevDidTimeout && !nextDidTimeOut) {
    // Fallback -> Primary:
    // 1. Unmount fallback set
    // Note: don't emulate fallback unmount because React actually did it.
    // 2. Mount primary set
    const nextPrimaryChildSet = nextFiber.child;

    if (nextPrimaryChildSet !== null) {
      mountFiberRecursively(onRender, nextPrimaryChildSet, true);
    }
  } else if (!prevDidTimeout && nextDidTimeOut) {
    // Primary -> Fallback:
    // 1. Hide primary set
    // This is not a real unmount, so it won't get reported by React.
    // We need to manually walk the previous tree and record unmounts.
    unmountFiberChildrenRecursively(onRender, prevFiber);

    // 2. Mount fallback set
    const nextFallbackChildSet = nextFiber.child?.sibling ?? null;

    if (nextFallbackChildSet !== null) {
      mountFiberRecursively(onRender, nextFallbackChildSet, true);
    }
  } else if (nextFiber.child !== prevFiber.child) {
    // Common case: Primary -> Primary.
    // This is the same code path as for non-Suspense fibers.

    // If the first child is different, we need to traverse them.
    // Each next child will be either a new child (mount) or an alternate (update).
    let nextChild = nextFiber.child;

    while (nextChild) {
      // We already know children will be referentially different because
      // they are either new mounts or alternates of previous children.
      // Schedule updates and mounts depending on whether alternates exist.
      // We don't track deletions here because they are reported separately.
      if (nextChild.alternate) {
        const prevChild = nextChild.alternate;

        updateFiberRecursively(
          onRender,
          nextChild,
          prevChild,
          shouldIncludeInTree ? nextFiber : parentFiber,
        );
      } else {
        mountFiberRecursively(onRender, nextChild, false);
      }

      // Try the next child.
      nextChild = nextChild.sibling;
    }
  }
};

export const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
  const isRoot = fiber.tag === HostRootTag;

  if (isRoot || !shouldFilterFiber(fiber)) {
    onRender(fiber, "unmount");
  }
};

export const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
  // We might meet a nested Suspense on our way.
  const isTimedOutSuspense = fiber.tag === SuspenseComponentTag && fiber.memoizedState !== null;
  let child = fiber.child;

  if (isTimedOutSuspense) {
    // If it's showing fallback tree, let's traverse it instead.
    const primaryChildFragment = fiber.child;
    const fallbackChildFragment = primaryChildFragment?.sibling ?? null;

    // Skip over to the real Fiber child.
    child = fallbackChildFragment?.child ?? null;
  }

  while (child !== null) {
    // Record simulated unmounts children-first.
    // We skip nodes without return because those are real unmounts.
    if (child.return !== null) {
      unmountFiber(onRender, child);
      unmountFiberChildrenRecursively(onRender, child);
    }

    child = child.sibling;
  }
};

let commitId = 0;
const rootInstanceMap = new WeakMap<
  FiberRoot,
  {
    id: number;
    prevFiber: Fiber | null;
  }
>();

/**
 * Creates a fiber visitor function. Must pass a fiber root and a render handler.
 * @example
 * traverseRenderedFibers(root, (fiber, phase) => {
 *   console.log(phase)
 * })
 */
export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler): void => {
  const fiber = "current" in root ? root.current : root;

  let rootInstance = rootInstanceMap.get(root);

  if (!rootInstance) {
    rootInstance = { id: commitId++, prevFiber: null };
    rootInstanceMap.set(root, rootInstance);
  }

  const { prevFiber } = rootInstance;
  // if fiberRoot don't have current instance, means it's been unmounted
  if (!fiber) {
    unmountFiber(onRender, fiber);
  } else if (prevFiber !== null) {
    const wasMounted =
      prevFiber &&
      prevFiber.memoizedState != null &&
      prevFiber.memoizedState.element != null &&
      // A dehydrated root is not considered mounted
      prevFiber.memoizedState.isDehydrated !== true;
    const isMounted =
      fiber.memoizedState != null &&
      fiber.memoizedState.element != null &&
      // A dehydrated root is not considered mounted
      fiber.memoizedState.isDehydrated !== true;

    if (!wasMounted && isMounted) {
      mountFiberRecursively(onRender, fiber, false);
    } else if (wasMounted && isMounted) {
      updateFiberRecursively(onRender, fiber, fiber.alternate, null);
    } else if (wasMounted && !isMounted) {
      unmountFiber(onRender, fiber);
    }
  } else {
    mountFiberRecursively(onRender, fiber, true);
  }

  rootInstance.prevFiber = fiber;
};

let _overrideProps: null | ReactRenderer["overrideProps"] = null;
let _overrideHookState: null | ReactRenderer["overrideHookState"] = null;
let _overrideContext: null | ReactRenderer["overrideContext"] = null;

export const injectOverrideMethods = () => {
  if (!hasRDTHook()) return null;
  const rdtHook = getRDTHook();
  if (!rdtHook?.renderers) return null;

  if (_overrideProps || _overrideHookState || _overrideContext) {
    return {
      overrideContext: _overrideContext,
      overrideHookState: _overrideHookState,
      overrideProps: _overrideProps,
    };
  }

  for (const [, renderer] of Array.from(rdtHook.renderers)) {
    try {
      if (_overrideHookState) {
        const prevOverrideHookState = _overrideHookState;
        _overrideHookState = (fiber: Fiber, id: string, path: string[], value: unknown) => {
          let current = fiber.memoizedState;
          for (let i = 0; i < Number(id); i++) {
            if (!current?.next) break;
            current = current.next;
          }

          if (current?.queue) {
            const queue = current.queue;
            if (isPOJO(queue) && "dispatch" in queue) {
              const dispatch = queue.dispatch as (value: unknown) => void;
              dispatch(value);
              return;
            }
          }

          prevOverrideHookState(fiber, id, path, value);
          renderer.overrideHookState?.(fiber, id, path, value);
        };
      } else if (renderer.overrideHookState) {
        _overrideHookState = renderer.overrideHookState;
      }

      if (_overrideProps) {
        const prevOverrideProps = _overrideProps;
        _overrideProps = (fiber: Fiber, path: Array<string>, value: unknown) => {
          prevOverrideProps(fiber, path, value);
          renderer.overrideProps?.(fiber, path, value);
        };
      } else if (renderer.overrideProps) {
        _overrideProps = renderer.overrideProps;
      }

      _overrideContext = (fiber: Fiber, contextType: unknown, path: string[], value: unknown) => {
        let current: Fiber | null = fiber;
        while (current) {
          const type = current.type as { Provider?: unknown };
          if (type === contextType || type?.Provider === contextType) {
            if (_overrideProps) {
              _overrideProps(current, ["value", ...path], value);
              if (current.alternate) {
                _overrideProps(current.alternate, ["value", ...path], value);
              }
            }
            break;
          }
          current = current.return;
        }
      };
    } catch {
      /**/
    }
  }
};

const isPOJO = (maybePOJO: unknown): maybePOJO is Record<string, unknown> => {
  return (
    Object.prototype.toString.call(maybePOJO) === "[object Object]" &&
    (Object.getPrototypeOf(maybePOJO) === Object.prototype ||
      Object.getPrototypeOf(maybePOJO) === null)
  );
};

const buildPathsFromValue = (
  maybePOJO: Record<string, unknown>,
  basePath: string[] = [],
): Array<{ path: string[]; value: unknown }> => {
  if (!isPOJO(maybePOJO)) {
    return [{ path: basePath, value: maybePOJO }];
  }

  const paths: Array<{ path: string[]; value: unknown }> = [];

  for (const key in maybePOJO) {
    const value = maybePOJO[key];
    const path = basePath.concat(key);

    if (isPOJO(value)) {
      paths.push(...buildPathsFromValue(value, path));
    } else {
      paths.push({ path, value });
    }
  }

  return paths;
};

export const overrideProps = (fiber: Fiber, partialValue: Record<string, unknown>) => {
  injectOverrideMethods();

  const paths = buildPathsFromValue(partialValue);

  for (const { path, value } of paths) {
    try {
      _overrideProps?.(fiber, path, value);
    } catch {}
  }
};

export const overrideHookState = (
  fiber: Fiber,
  id: number,
  partialValue: Record<string, unknown>,
) => {
  injectOverrideMethods();

  const hookId = String(id);

  if (isPOJO(partialValue)) {
    const paths = buildPathsFromValue(partialValue);

    for (const { path, value } of paths) {
      try {
        _overrideHookState?.(fiber, hookId, path, value);
      } catch {}
    }
  } else {
    try {
      _overrideHookState?.(fiber, hookId, [], partialValue);
    } catch {}
  }
};

export const overrideContext = (
  fiber: Fiber,
  contextType: unknown,
  partialValue: Record<string, unknown>,
) => {
  injectOverrideMethods();

  if (isPOJO(partialValue)) {
    const paths = buildPathsFromValue(partialValue);

    for (const { path, value } of paths) {
      try {
        _overrideContext?.(fiber, contextType, path, value);
      } catch {}
    }
  } else {
    try {
      _overrideContext?.(fiber, contextType, [], partialValue);
    } catch {}
  }
};

export interface InstrumentationOptions {
  name?: string;
  onActive?: () => unknown;
  onCommitFiberRoot?: (rendererID: number, root: FiberRoot, priority: number | void) => unknown;
  onCommitFiberUnmount?: (rendererID: number, fiber: Fiber) => unknown;
  onPostCommitFiberRoot?: (rendererID: number, root: FiberRoot) => unknown;
  onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: React.ReactNode) => unknown;
}

/**
 * Instruments the DevTools hook.
 * @example
 * const hook = instrument({
 *   onActive() {
 *     console.log('initialized');
 *   },
 *   onCommitFiberRoot(rendererID, root) {
 *     console.log('fiberRoot', root.current)
 *   },
 * });
 */
export const instrument = (options: InstrumentationOptions): ReactDevToolsGlobalHook => {
  const rdtHook = getRDTHook(options.onActive);

  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;

  const prevOnCommitFiberRoot = rdtHook.onCommitFiberRoot;
  if (options.onCommitFiberRoot) {
    const nextOnCommitFiberRoot = (
      rendererID: number,
      root: FiberRoot,
      priority: number | void,
    ) => {
      if (prevOnCommitFiberRoot === nextOnCommitFiberRoot) return;
      // TODO: validate whether the bottom version is more correct here
      // for preventing infinite loops
      // if (rdtHook.onCommitFiberRoot !== handler) return;
      prevOnCommitFiberRoot?.(rendererID, root, priority);
      options.onCommitFiberRoot?.(rendererID, root, priority);
    };
    rdtHook.onCommitFiberRoot = nextOnCommitFiberRoot;
  }

  const prevOnCommitFiberUnmount = rdtHook.onCommitFiberUnmount;
  if (options.onCommitFiberUnmount) {
    const handler = (rendererID: number, root: FiberRoot) => {
      if (rdtHook.onCommitFiberUnmount !== handler) return;
      prevOnCommitFiberUnmount?.(rendererID, root);
      options.onCommitFiberUnmount?.(rendererID, root);
    };
    rdtHook.onCommitFiberUnmount = handler;
  }

  const prevOnPostCommitFiberRoot = rdtHook.onPostCommitFiberRoot;
  if (options.onPostCommitFiberRoot) {
    const handler = (rendererID: number, root: FiberRoot) => {
      if (rdtHook.onPostCommitFiberRoot !== handler) return;
      prevOnPostCommitFiberRoot?.(rendererID, root);
      options.onPostCommitFiberRoot?.(rendererID, root);
    };
    rdtHook.onPostCommitFiberRoot = handler;
  }

  return rdtHook;
};

export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
  if (rdtHook?.renderers) {
    for (const renderer of rdtHook.renderers.values()) {
      try {
        const fiber = renderer.findFiberByHostInstance?.(hostInstance);
        if (fiber) return fiber;
      } catch {}
    }
  }

  if (typeof hostInstance === "object" && hostInstance != null) {
    if ("_reactRootContainer" in hostInstance) {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      return (hostInstance._reactRootContainer as any)?._internalRoot?.current?.child;
    }

    for (const key in hostInstance) {
      if (
        key.startsWith("__reactContainer$") ||
        key.startsWith("__reactInternalInstance$") ||
        key.startsWith("__reactFiber")
      ) {
        return (hostInstance[key] || null) as Fiber | null;
      }
    }
  }
  return null;
};

export const INSTALL_ERROR = new Error();

export const _fiberRoots = new Set<FiberRoot>();

export const secure = (
  options: InstrumentationOptions,
  secureOptions: {
    dangerouslyRunInProduction?: boolean;
    installCheckTimeout?: number;
    isProduction?: boolean;
    minReactMajorVersion?: number;
    onError?: (error?: unknown) => unknown;
  } = {},
): InstrumentationOptions => {
  const onActive = options.onActive;
  const isRDTHookInstalled = hasRDTHook();
  const isUsingRealReactDevtools = isRealReactDevtools();
  const isUsingReactRefresh = isReactRefresh();
  let timeout: number | undefined;
  let isDevelopment = !secureOptions.isProduction;

  options.onActive = () => {
    clearTimeout(timeout);
    let isSecure = true;
    try {
      const rdtHook = getRDTHook();

      for (const renderer of rdtHook.renderers.values()) {
        const [majorVersion] = renderer.version.split(".");
        if (Number(majorVersion) < (secureOptions.minReactMajorVersion ?? 17)) {
          isSecure = false;
        }
        const buildType = detectReactBuildType(renderer);
        if (buildType === "development") {
          isDevelopment = true;
        } else if (!secureOptions.dangerouslyRunInProduction) {
          isSecure = false;
        }
      }
    } catch (err) {
      secureOptions.onError?.(err);
    }

    if (!isSecure) {
      options.onCommitFiberRoot = undefined;
      options.onCommitFiberUnmount = undefined;
      options.onPostCommitFiberRoot = undefined;
      options.onActive = undefined;
      return;
    }
    onActive?.();

    try {
      const onCommitFiberRoot = options.onCommitFiberRoot;
      if (onCommitFiberRoot) {
        options.onCommitFiberRoot = (rendererID, root, priority) => {
          if (!_fiberRoots.has(root)) {
            _fiberRoots.add(root);
          }
          try {
            onCommitFiberRoot(rendererID, root, priority);
          } catch (err) {
            secureOptions.onError?.(err);
          }
        };
      }

      const onCommitFiberUnmount = options.onCommitFiberUnmount;
      if (onCommitFiberUnmount) {
        options.onCommitFiberUnmount = (rendererID, root) => {
          try {
            onCommitFiberUnmount(rendererID, root);
          } catch (err) {
            secureOptions.onError?.(err);
          }
        };
      }

      const onPostCommitFiberRoot = options.onPostCommitFiberRoot;
      if (onPostCommitFiberRoot) {
        options.onPostCommitFiberRoot = (rendererID, root) => {
          try {
            onPostCommitFiberRoot(rendererID, root);
          } catch (err) {
            secureOptions.onError?.(err);
          }
        };
      }
    } catch (err) {
      secureOptions.onError?.(err);
    }
  };

  if (!isRDTHookInstalled && !isUsingRealReactDevtools && !isUsingReactRefresh) {
    timeout = setTimeout(() => {
      if (isDevelopment) {
        secureOptions.onError?.(INSTALL_ERROR);
      }
      stop();
    }, secureOptions.installCheckTimeout ?? 100) as unknown as number;
  }

  return options;
};

const swapFiberAndSchedule = (
  fiber: Fiber,
  nextType: React.ComponentType<unknown>,
  renderer: ReactRenderer,
): void => {
  fiber.type = nextType;
  if (fiber.alternate) {
    fiber.alternate.type = nextType;
  }
  // HACK: shallow-clone memoizedProps so React sees oldProps !== newProps
  // and skips the bailout path (same trick as overrideHookState in ReactFiberReconciler)
  fiber.memoizedProps = { ...fiber.memoizedProps };

  if (renderer.scheduleUpdate) {
    renderer.scheduleUpdate(fiber);
  }
};

/**
 * Replaces every fiber whose type matches `prevType` with `nextType` and
 * triggers a synchronous re-render for each one.
 * The new function must follow the same Rules of Hooks as the original.
 * DEV-only — `renderer.scheduleUpdate` is not available in production builds.
 */
export const hotSwapFiberType = (
  fiber: Fiber,
  nextType: React.ComponentType<unknown>,
): void => {
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
  if (!rdtHook?.renderers) return;

  const renderer = Array.from(rdtHook.renderers.values()).find(
    (innerRenderer) => !!innerRenderer.scheduleUpdate,
  );
  if (!renderer) return;

  const prevType = getType(fiber.type);
  if (!prevType) {
    swapFiberAndSchedule(fiber, nextType, renderer);
    return;
  }

  let rootFiber: Fiber = fiber;
  while (rootFiber.return) {
    rootFiber = rootFiber.return;
  }

  traverseFiberSync(rootFiber, (innerFiber) => {
    if (getType(innerFiber.type) === prevType) {
      swapFiberAndSchedule(innerFiber, nextType, renderer);
    }
  });
};

export * from "./rdt-hook.js";
export type * from "./types.js";
