import { isDevelopment } from "#is-development";
import { PanelConstraints, PanelData } from "./Panel";
import {
  DragState,
  PanelGroupContext,
  ResizeEvent,
  TPanelGroupContext,
} from "./PanelGroupContext";
import {
  EXCEEDED_HORIZONTAL_MAX,
  EXCEEDED_HORIZONTAL_MIN,
  EXCEEDED_VERTICAL_MAX,
  EXCEEDED_VERTICAL_MIN,
  reportConstraintsViolation,
} from "./PanelResizeHandleRegistry";
import { useForceUpdate } from "./hooks/useForceUpdate";
import useIsomorphicLayoutEffect from "./hooks/useIsomorphicEffect";
import useUniqueId from "./hooks/useUniqueId";
import { useWindowSplitterPanelGroupBehavior } from "./hooks/useWindowSplitterPanelGroupBehavior";
import { Direction } from "./types";
import { adjustLayoutByDelta } from "./utils/adjustLayoutByDelta";
import { areEqual } from "./utils/arrays";
import { assert } from "./utils/assert";
import { calculateDeltaPercentage } from "./utils/calculateDeltaPercentage";
import { calculateUnsafeDefaultLayout } from "./utils/calculateUnsafeDefaultLayout";
import { callPanelCallbacks } from "./utils/callPanelCallbacks";
import { compareLayouts } from "./utils/compareLayouts";
import { computePanelFlexBoxStyle } from "./utils/computePanelFlexBoxStyle";
import debounce from "./utils/debounce";
import { determinePivotIndices } from "./utils/determinePivotIndices";
import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
import { isKeyDown, isMouseEvent, isPointerEvent } from "./utils/events";
import { getResizeEventCursorPosition } from "./utils/events/getResizeEventCursorPosition";
import { initializeDefaultStorage } from "./utils/initializeDefaultStorage";
import {
  fuzzyCompareNumbers,
  fuzzyNumbersEqual,
} from "./utils/numbers/fuzzyCompareNumbers";
import {
  loadPanelGroupState,
  savePanelGroupState,
} from "./utils/serialization";
import { validatePanelConstraints } from "./utils/validatePanelConstraints";
import { validatePanelGroupLayout } from "./utils/validatePanelGroupLayout";
import {
  CSSProperties,
  ForwardedRef,
  HTMLAttributes,
  PropsWithChildren,
  ReactElement,
  createElement,
  forwardRef,
  useCallback,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
} from "./vendor/react";

const LOCAL_STORAGE_DEBOUNCE_INTERVAL = 100;

export type ImperativePanelGroupHandle = {
  getId: () => string;
  getLayout: () => number[];
  setLayout: (layout: number[]) => void;
};

export type PanelGroupStorage = {
  getItem(name: string): string | null;
  setItem(name: string, value: string): void;
};

export type PanelGroupOnLayout = (layout: number[]) => void;

const defaultStorage: PanelGroupStorage = {
  getItem: (name: string) => {
    initializeDefaultStorage(defaultStorage);
    return defaultStorage.getItem(name);
  },
  setItem: (name: string, value: string) => {
    initializeDefaultStorage(defaultStorage);
    defaultStorage.setItem(name, value);
  },
};

export type PanelGroupProps = Omit<
  HTMLAttributes<keyof HTMLElementTagNameMap>,
  "id"
> &
  PropsWithChildren<{
    autoSaveId?: string | null;
    className?: string;
    direction: Direction;
    id?: string | null;
    keyboardResizeBy?: number | null;
    onLayout?: PanelGroupOnLayout | null;
    storage?: PanelGroupStorage;
    style?: CSSProperties;
    tagName?: keyof HTMLElementTagNameMap;
  }>;

const debounceMap: {
  [key: string]: typeof savePanelGroupState;
} = {};

function PanelGroupWithForwardedRef({
  autoSaveId = null,
  children,
  className: classNameFromProps = "",
  direction,
  forwardedRef,
  id: idFromProps = null,
  onLayout = null,
  keyboardResizeBy = null,
  storage = defaultStorage,
  style: styleFromProps,
  tagName: Type = "div",
  ...rest
}: PanelGroupProps & {
  forwardedRef: ForwardedRef<ImperativePanelGroupHandle>;
}): ReactElement {
  const groupId = useUniqueId(idFromProps);
  const panelGroupElementRef = useRef<HTMLDivElement | null>(null);
  const [dragState, setDragState] = useState<DragState | null>(null);
  const [layout, setLayout] = useState<number[]>([]);
  const forceUpdate = useForceUpdate();

  const panelIdToLastNotifiedSizeMapRef = useRef<Record<string, number>>({});
  const panelSizeBeforeCollapseRef = useRef<Map<string, number>>(new Map());
  const prevDeltaRef = useRef<number>(0);

  const committedValuesRef = useRef<{
    autoSaveId: string | null;
    direction: Direction;
    dragState: DragState | null;
    id: string;
    keyboardResizeBy: number | null;
    onLayout: PanelGroupOnLayout | null;
    storage: PanelGroupStorage;
  }>({
    autoSaveId,
    direction,
    dragState,
    id: groupId,
    keyboardResizeBy,
    onLayout,
    storage,
  });

  const eagerValuesRef = useRef<{
    layout: number[];
    panelDataArray: PanelData[];
    panelDataArrayChanged: boolean;
  }>({
    layout,
    panelDataArray: [],
    panelDataArrayChanged: false,
  });

  const devWarningsRef = useRef<{
    didLogIdAndOrderWarning: boolean;
    didLogPanelConstraintsWarning: boolean;
    prevPanelIds: string[];
  }>({
    didLogIdAndOrderWarning: false,
    didLogPanelConstraintsWarning: false,
    prevPanelIds: [],
  });

  useImperativeHandle(
    forwardedRef,
    () => ({
      getId: () => committedValuesRef.current.id,
      getLayout: () => {
        const { layout } = eagerValuesRef.current;

        return layout;
      },
      setLayout: (unsafeLayout: number[]) => {
        const { onLayout } = committedValuesRef.current;
        const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;

        const safeLayout = validatePanelGroupLayout({
          layout: unsafeLayout,
          panelConstraints: panelDataArray.map(
            (panelData) => panelData.constraints
          ),
        });

        if (!areEqual(prevLayout, safeLayout)) {
          setLayout(safeLayout);

          eagerValuesRef.current.layout = safeLayout;

          if (onLayout) {
            onLayout(safeLayout);
          }

          callPanelCallbacks(
            panelDataArray,
            safeLayout,
            panelIdToLastNotifiedSizeMapRef.current
          );
        }
      },
    }),
    []
  );

  useIsomorphicLayoutEffect(() => {
    committedValuesRef.current.autoSaveId = autoSaveId;
    committedValuesRef.current.direction = direction;
    committedValuesRef.current.dragState = dragState;
    committedValuesRef.current.id = groupId;
    committedValuesRef.current.onLayout = onLayout;
    committedValuesRef.current.storage = storage;
  });

  useWindowSplitterPanelGroupBehavior({
    committedValuesRef,
    eagerValuesRef,
    groupId,
    layout,
    panelDataArray: eagerValuesRef.current.panelDataArray,
    setLayout,
    panelGroupElement: panelGroupElementRef.current,
  });

  useEffect(() => {
    const { panelDataArray } = eagerValuesRef.current;

    // If this panel has been configured to persist sizing information, save sizes to local storage.
    if (autoSaveId) {
      if (layout.length === 0 || layout.length !== panelDataArray.length) {
        return;
      }

      let debouncedSave = debounceMap[autoSaveId];

      // Limit the frequency of localStorage updates.
      if (debouncedSave == null) {
        debouncedSave = debounce(
          savePanelGroupState,
          LOCAL_STORAGE_DEBOUNCE_INTERVAL
        );

        debounceMap[autoSaveId] = debouncedSave;
      }

      // Clone mutable data before passing to the debounced function,
      // else we run the risk of saving an incorrect combination of mutable and immutable values to state.
      const clonedPanelDataArray = [...panelDataArray];
      const clonedPanelSizesBeforeCollapse = new Map(
        panelSizeBeforeCollapseRef.current
      );
      debouncedSave(
        autoSaveId,
        clonedPanelDataArray,
        clonedPanelSizesBeforeCollapse,
        layout,
        storage
      );
    }
  }, [autoSaveId, layout, storage]);

  // DEV warnings
  useEffect(() => {
    if (isDevelopment) {
      const { panelDataArray } = eagerValuesRef.current;

      const {
        didLogIdAndOrderWarning,
        didLogPanelConstraintsWarning,
        prevPanelIds,
      } = devWarningsRef.current;

      if (!didLogIdAndOrderWarning) {
        const panelIds = panelDataArray.map(({ id }) => id);

        devWarningsRef.current.prevPanelIds = panelIds;

        const panelsHaveChanged =
          prevPanelIds.length > 0 && !areEqual(prevPanelIds, panelIds);
        if (panelsHaveChanged) {
          if (
            panelDataArray.find(
              ({ idIsFromProps, order }) => !idIsFromProps || order == null
            )
          ) {
            devWarningsRef.current.didLogIdAndOrderWarning = true;

            console.warn(
              `WARNING: Panel id and order props recommended when panels are dynamically rendered`
            );
          }
        }
      }

      if (!didLogPanelConstraintsWarning) {
        const panelConstraints = panelDataArray.map(
          (panelData) => panelData.constraints
        );

        for (
          let panelIndex = 0;
          panelIndex < panelConstraints.length;
          panelIndex++
        ) {
          const panelData = panelDataArray[panelIndex];
          assert(panelData, `Panel data not found for index ${panelIndex}`);

          const isValid = validatePanelConstraints({
            panelConstraints,
            panelId: panelData.id,
            panelIndex,
          });

          if (!isValid) {
            devWarningsRef.current.didLogPanelConstraintsWarning = true;

            break;
          }
        }
      }
    }
  });

  // External APIs are safe to memoize via committed values ref
  const collapsePanel = useCallback((panelData: PanelData) => {
    const { onLayout } = committedValuesRef.current;
    const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;

    if (panelData.constraints.collapsible) {
      const panelConstraintsArray = panelDataArray.map(
        (panelData) => panelData.constraints
      );

      const {
        collapsedSize = 0,
        panelSize,
        pivotIndices,
      } = panelDataHelper(panelDataArray, panelData, prevLayout);

      assert(
        panelSize != null,
        `Panel size not found for panel "${panelData.id}"`
      );

      if (!fuzzyNumbersEqual(panelSize, collapsedSize)) {
        // Store size before collapse;
        // This is the size that gets restored if the expand() API is used.
        panelSizeBeforeCollapseRef.current.set(panelData.id, panelSize);

        const isLastPanel =
          findPanelDataIndex(panelDataArray, panelData) ===
          panelDataArray.length - 1;
        const delta = isLastPanel
          ? panelSize - collapsedSize
          : collapsedSize - panelSize;

        const nextLayout = adjustLayoutByDelta({
          delta,
          initialLayout: prevLayout,
          panelConstraints: panelConstraintsArray,
          pivotIndices,
          prevLayout,
          trigger: "imperative-api",
        });

        if (!compareLayouts(prevLayout, nextLayout)) {
          setLayout(nextLayout);

          eagerValuesRef.current.layout = nextLayout;

          if (onLayout) {
            onLayout(nextLayout);
          }

          callPanelCallbacks(
            panelDataArray,
            nextLayout,
            panelIdToLastNotifiedSizeMapRef.current
          );
        }
      }
    }
  }, []);

  // External APIs are safe to memoize via committed values ref
  const expandPanel = useCallback(
    (panelData: PanelData, minSizeOverride?: number) => {
      const { onLayout } = committedValuesRef.current;
      const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;

      if (panelData.constraints.collapsible) {
        const panelConstraintsArray = panelDataArray.map(
          (panelData) => panelData.constraints
        );

        const {
          collapsedSize = 0,
          panelSize = 0,
          minSize: minSizeFromProps = 0,
          pivotIndices,
        } = panelDataHelper(panelDataArray, panelData, prevLayout);

        const minSize = minSizeOverride ?? minSizeFromProps;

        if (fuzzyNumbersEqual(panelSize, collapsedSize)) {
          // Restore this panel to the size it was before it was collapsed, if possible.
          const prevPanelSize = panelSizeBeforeCollapseRef.current.get(
            panelData.id
          );

          const baseSize =
            prevPanelSize != null && prevPanelSize >= minSize
              ? prevPanelSize
              : minSize;

          const isLastPanel =
            findPanelDataIndex(panelDataArray, panelData) ===
            panelDataArray.length - 1;
          const delta = isLastPanel
            ? panelSize - baseSize
            : baseSize - panelSize;

          const nextLayout = adjustLayoutByDelta({
            delta,
            initialLayout: prevLayout,
            panelConstraints: panelConstraintsArray,
            pivotIndices,
            prevLayout,
            trigger: "imperative-api",
          });

          if (!compareLayouts(prevLayout, nextLayout)) {
            setLayout(nextLayout);

            eagerValuesRef.current.layout = nextLayout;

            if (onLayout) {
              onLayout(nextLayout);
            }

            callPanelCallbacks(
              panelDataArray,
              nextLayout,
              panelIdToLastNotifiedSizeMapRef.current
            );
          }
        }
      }
    },
    []
  );

  // External APIs are safe to memoize via committed values ref
  const getPanelSize = useCallback((panelData: PanelData) => {
    const { layout, panelDataArray } = eagerValuesRef.current;

    const { panelSize } = panelDataHelper(panelDataArray, panelData, layout);

    assert(
      panelSize != null,
      `Panel size not found for panel "${panelData.id}"`
    );

    return panelSize;
  }, []);

  // This API should never read from committedValuesRef
  const getPanelStyle = useCallback(
    (panelData: PanelData, defaultSize: number | undefined) => {
      const { panelDataArray } = eagerValuesRef.current;

      const panelIndex = findPanelDataIndex(panelDataArray, panelData);

      return computePanelFlexBoxStyle({
        defaultSize,
        dragState,
        layout,
        panelData: panelDataArray,
        panelIndex,
      });
    },
    [dragState, layout]
  );

  // External APIs are safe to memoize via committed values ref
  const isPanelCollapsed = useCallback((panelData: PanelData) => {
    const { layout, panelDataArray } = eagerValuesRef.current;

    const {
      collapsedSize = 0,
      collapsible,
      panelSize,
    } = panelDataHelper(panelDataArray, panelData, layout);

    assert(
      panelSize != null,
      `Panel size not found for panel "${panelData.id}"`
    );

    return collapsible === true && fuzzyNumbersEqual(panelSize, collapsedSize);
  }, []);

  // External APIs are safe to memoize via committed values ref
  const isPanelExpanded = useCallback((panelData: PanelData) => {
    const { layout, panelDataArray } = eagerValuesRef.current;

    const {
      collapsedSize = 0,
      collapsible,
      panelSize,
    } = panelDataHelper(panelDataArray, panelData, layout);

    assert(
      panelSize != null,
      `Panel size not found for panel "${panelData.id}"`
    );

    return !collapsible || fuzzyCompareNumbers(panelSize, collapsedSize) > 0;
  }, []);

  const registerPanel = useCallback(
    (panelData: PanelData) => {
      const { panelDataArray } = eagerValuesRef.current;

      panelDataArray.push(panelData);
      panelDataArray.sort((panelA, panelB) => {
        const orderA = panelA.order;
        const orderB = panelB.order;
        if (orderA == null && orderB == null) {
          return 0;
        } else if (orderA == null) {
          return -1;
        } else if (orderB == null) {
          return 1;
        } else {
          return orderA - orderB;
        }
      });

      eagerValuesRef.current.panelDataArrayChanged = true;

      forceUpdate();
    },
    [forceUpdate]
  );

  // (Re)calculate group layout whenever panels are registered or unregistered.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  useIsomorphicLayoutEffect(() => {
    if (eagerValuesRef.current.panelDataArrayChanged) {
      eagerValuesRef.current.panelDataArrayChanged = false;

      const { autoSaveId, onLayout, storage } = committedValuesRef.current;
      const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;

      // If this panel has been configured to persist sizing information,
      // default size should be restored from local storage if possible.
      let unsafeLayout: number[] | null = null;
      if (autoSaveId) {
        const state = loadPanelGroupState(autoSaveId, panelDataArray, storage);
        if (state) {
          panelSizeBeforeCollapseRef.current = new Map(
            Object.entries(state.expandToSizes)
          );
          unsafeLayout = state.layout;
        }
      }

      if (unsafeLayout == null) {
        unsafeLayout = calculateUnsafeDefaultLayout({
          panelDataArray,
        });
      }

      // Validate even saved layouts in case something has changed since last render
      // e.g. for pixel groups, this could be the size of the window
      const nextLayout = validatePanelGroupLayout({
        layout: unsafeLayout,
        panelConstraints: panelDataArray.map(
          (panelData) => panelData.constraints
        ),
      });

      if (!areEqual(prevLayout, nextLayout)) {
        setLayout(nextLayout);

        eagerValuesRef.current.layout = nextLayout;

        if (onLayout) {
          onLayout(nextLayout);
        }

        callPanelCallbacks(
          panelDataArray,
          nextLayout,
          panelIdToLastNotifiedSizeMapRef.current
        );
      }
    }
  });

  // Reset the cached layout if hidden by the Activity/Offscreen API
  useIsomorphicLayoutEffect(() => {
    const eagerValues = eagerValuesRef.current;
    return () => {
      eagerValues.layout = [];
    };
  }, []);

  const registerResizeHandle = useCallback((dragHandleId: string) => {
    return function resizeHandler(event: ResizeEvent) {
      event.preventDefault();
      const panelGroupElement = panelGroupElementRef.current;
      if (!panelGroupElement) {
        return () => null;
      }

      const {
        direction,
        dragState,
        id: groupId,
        keyboardResizeBy,
        onLayout,
      } = committedValuesRef.current;
      const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;

      const { initialLayout } = dragState ?? {};

      const pivotIndices = determinePivotIndices(
        groupId,
        dragHandleId,
        panelGroupElement
      );

      let delta = calculateDeltaPercentage(
        event,
        dragHandleId,
        direction,
        dragState,
        keyboardResizeBy,
        panelGroupElement
      );

      // Support RTL layouts
      const isHorizontal = direction === "horizontal";
      if (document.dir === "rtl" && isHorizontal) {
        delta = -delta;
      }

      const panelConstraints = panelDataArray.map(
        (panelData) => panelData.constraints
      );

      const nextLayout = adjustLayoutByDelta({
        delta,
        initialLayout: initialLayout ?? prevLayout,
        panelConstraints,
        pivotIndices,
        prevLayout,
        trigger: isKeyDown(event) ? "keyboard" : "mouse-or-touch",
      });

      const layoutChanged = !compareLayouts(prevLayout, nextLayout);

      // Only update the cursor for layout changes triggered by touch/mouse events (not keyboard)
      // Update the cursor even if the layout hasn't changed (we may need to show an invalid cursor state)
      if (isPointerEvent(event) || isMouseEvent(event)) {
        // Watch for multiple subsequent deltas; this might occur for tiny cursor movements.
        // In this case, Panel sizes might not change–
        // but updating cursor in this scenario would cause a flicker.
        if (prevDeltaRef.current != delta) {
          prevDeltaRef.current = delta;

          if (!layoutChanged && delta !== 0) {
            // If the pointer has moved too far to resize the panel any further, note this so we can update the cursor.
            // This mimics VS Code behavior.
            if (isHorizontal) {
              reportConstraintsViolation(
                dragHandleId,
                delta < 0 ? EXCEEDED_HORIZONTAL_MIN : EXCEEDED_HORIZONTAL_MAX
              );
            } else {
              reportConstraintsViolation(
                dragHandleId,
                delta < 0 ? EXCEEDED_VERTICAL_MIN : EXCEEDED_VERTICAL_MAX
              );
            }
          } else {
            reportConstraintsViolation(dragHandleId, 0);
          }
        }
      }

      if (layoutChanged) {
        setLayout(nextLayout);

        eagerValuesRef.current.layout = nextLayout;

        if (onLayout) {
          onLayout(nextLayout);
        }

        callPanelCallbacks(
          panelDataArray,
          nextLayout,
          panelIdToLastNotifiedSizeMapRef.current
        );
      }
    };
  }, []);

  // External APIs are safe to memoize via committed values ref
  const resizePanel = useCallback(
    (panelData: PanelData, unsafePanelSize: number) => {
      const { onLayout } = committedValuesRef.current;

      const { layout: prevLayout, panelDataArray } = eagerValuesRef.current;

      const panelConstraintsArray = panelDataArray.map(
        (panelData) => panelData.constraints
      );

      const { panelSize, pivotIndices } = panelDataHelper(
        panelDataArray,
        panelData,
        prevLayout
      );

      assert(
        panelSize != null,
        `Panel size not found for panel "${panelData.id}"`
      );

      const isLastPanel =
        findPanelDataIndex(panelDataArray, panelData) ===
        panelDataArray.length - 1;
      const delta = isLastPanel
        ? panelSize - unsafePanelSize
        : unsafePanelSize - panelSize;

      const nextLayout = adjustLayoutByDelta({
        delta,
        initialLayout: prevLayout,
        panelConstraints: panelConstraintsArray,
        pivotIndices,
        prevLayout,
        trigger: "imperative-api",
      });

      if (!compareLayouts(prevLayout, nextLayout)) {
        setLayout(nextLayout);

        eagerValuesRef.current.layout = nextLayout;

        if (onLayout) {
          onLayout(nextLayout);
        }

        callPanelCallbacks(
          panelDataArray,
          nextLayout,
          panelIdToLastNotifiedSizeMapRef.current
        );
      }
    },
    []
  );

  const reevaluatePanelConstraints = useCallback(
    (panelData: PanelData, prevConstraints: PanelConstraints) => {
      const { layout, panelDataArray } = eagerValuesRef.current;

      const {
        collapsedSize: prevCollapsedSize = 0,
        collapsible: prevCollapsible,
      } = prevConstraints;

      const {
        collapsedSize: nextCollapsedSize = 0,
        collapsible: nextCollapsible,
        maxSize: nextMaxSize = 100,
        minSize: nextMinSize = 0,
      } = panelData.constraints;

      const { panelSize: prevPanelSize } = panelDataHelper(
        panelDataArray,
        panelData,
        layout
      );
      if (prevPanelSize == null) {
        // It's possible that the panels in this group have changed since the last render
        return;
      }

      if (
        prevCollapsible &&
        nextCollapsible &&
        fuzzyNumbersEqual(prevPanelSize, prevCollapsedSize)
      ) {
        if (!fuzzyNumbersEqual(prevCollapsedSize, nextCollapsedSize)) {
          resizePanel(panelData, nextCollapsedSize);
        } else {
          // Stay collapsed
        }
      } else if (prevPanelSize < nextMinSize) {
        resizePanel(panelData, nextMinSize);
      } else if (prevPanelSize > nextMaxSize) {
        resizePanel(panelData, nextMaxSize);
      }
    },
    [resizePanel]
  );

  // TODO Multiple drag handles can be active at the same time so this API is a bit awkward now
  const startDragging = useCallback(
    (dragHandleId: string, event: ResizeEvent) => {
      const { direction } = committedValuesRef.current;
      const { layout } = eagerValuesRef.current;
      if (!panelGroupElementRef.current) {
        return;
      }
      const handleElement = getResizeHandleElement(
        dragHandleId,
        panelGroupElementRef.current
      );
      assert(
        handleElement,
        `Drag handle element not found for id "${dragHandleId}"`
      );

      const initialCursorPosition = getResizeEventCursorPosition(
        direction,
        event
      );

      setDragState({
        dragHandleId,
        dragHandleRect: handleElement.getBoundingClientRect(),
        initialCursorPosition,
        initialLayout: layout,
      });
    },
    []
  );

  const stopDragging = useCallback(() => {
    setDragState(null);
  }, []);

  const unregisterPanel = useCallback(
    (panelData: PanelData) => {
      const { panelDataArray } = eagerValuesRef.current;

      const index = findPanelDataIndex(panelDataArray, panelData);
      if (index >= 0) {
        panelDataArray.splice(index, 1);

        // TRICKY
        // When a panel is removed from the group, we should delete the most recent prev-size entry for it.
        // If we don't do this, then a conditionally rendered panel might not call onResize when it's re-mounted.
        // Strict effects mode makes this tricky though because all panels will be registered, unregistered, then re-registered on mount.
        delete panelIdToLastNotifiedSizeMapRef.current[panelData.id];

        eagerValuesRef.current.panelDataArrayChanged = true;

        forceUpdate();
      }
    },
    [forceUpdate]
  );

  const context = useMemo(
    () =>
      ({
        collapsePanel,
        direction,
        dragState,
        expandPanel,
        getPanelSize,
        getPanelStyle,
        groupId,
        isPanelCollapsed,
        isPanelExpanded,
        reevaluatePanelConstraints,
        registerPanel,
        registerResizeHandle,
        resizePanel,
        startDragging,
        stopDragging,
        unregisterPanel,
        panelGroupElement: panelGroupElementRef.current,
      }) satisfies TPanelGroupContext,
    [
      collapsePanel,
      dragState,
      direction,
      expandPanel,
      getPanelSize,
      getPanelStyle,
      groupId,
      isPanelCollapsed,
      isPanelExpanded,
      reevaluatePanelConstraints,
      registerPanel,
      registerResizeHandle,
      resizePanel,
      startDragging,
      stopDragging,
      unregisterPanel,
    ]
  );

  const style: CSSProperties = {
    display: "flex",
    flexDirection: direction === "horizontal" ? "row" : "column",
    height: "100%",
    overflow: "hidden",
    width: "100%",
  };

  return createElement(
    PanelGroupContext.Provider,
    { value: context },
    createElement(Type, {
      ...rest,

      children,
      className: classNameFromProps,
      id: idFromProps,
      ref: panelGroupElementRef,
      style: {
        ...style,
        ...styleFromProps,
      },

      // CSS selectors
      "data-panel-group": "",
      "data-panel-group-direction": direction,
      "data-panel-group-id": groupId,
    })
  );
}

export const PanelGroup = forwardRef<
  ImperativePanelGroupHandle,
  PanelGroupProps
>((props: PanelGroupProps, ref: ForwardedRef<ImperativePanelGroupHandle>) =>
  createElement(PanelGroupWithForwardedRef, { ...props, forwardedRef: ref })
);

PanelGroupWithForwardedRef.displayName = "PanelGroup";
PanelGroup.displayName = "forwardRef(PanelGroup)";

function findPanelDataIndex(panelDataArray: PanelData[], panelData: PanelData) {
  return panelDataArray.findIndex(
    (prevPanelData) =>
      prevPanelData === panelData || prevPanelData.id === panelData.id
  );
}

function panelDataHelper(
  panelDataArray: PanelData[],
  panelData: PanelData,
  layout: number[]
) {
  const panelIndex = findPanelDataIndex(panelDataArray, panelData);

  const isLastPanel = panelIndex === panelDataArray.length - 1;
  const pivotIndices = isLastPanel
    ? [panelIndex - 1, panelIndex]
    : [panelIndex, panelIndex + 1];

  const panelSize = layout[panelIndex];

  return {
    ...panelData.constraints,
    panelSize,
    pivotIndices,
  };
}
