import React, {
  memo,
  PropsWithChildren,
  useCallback,
  useMemo,
  useRef,
  useState,
} from "react";
import { View, StyleSheet } from "react-native";
import { v4 } from "uuid";

type Props = {
  Component: React.ElementType;
  onLayout: (LayoutChangeEvent) => void;
  componentProps?: Record<any, any>;
};

const isError = (possibleError): boolean => possibleError instanceof Error;

const styles = StyleSheet.create({
  container: {
    position: "absolute",
    opacity: 0,
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
  },
  wrapper: {},
});

const MeasurementsPortal = ({ Component, onLayout, componentProps }: Props) => {
  return (
    <View style={styles.container} testID="MeasurementsPortal">
      <View style={styles.wrapper} onLayout={onLayout} key={v4()}>
        <Component {...componentProps} />
      </View>
    </View>
  );
};

type MeasurementPortalContextType = {
  measureComponent?: (
    comp: React.ElementType,
    props: Record<any, any>
  ) => Promise<any>;
  measuringInProgress?: boolean;
};

const MeasurementPortalContext =
  React.createContext<null | MeasurementPortalContextType>(null);

const MeasurementsPortalContextProvider = memo(
  ({ children }: PropsWithChildren) => {
    const Component = useRef(View);
    const [measuringInProgress, setMeasuringInProgress] = useState(false);

    const measureComponentCallback = useRef(null);
    const componentProps = useRef(null);
    const onLoadFinishedIsCalled = useRef(null);

    const setComponent = useCallback((comp) => {
      Component.current = comp;
    }, []);

    const setMeasureComponentCallback = useCallback((cb) => {
      measureComponentCallback.current = cb;
    }, []);

    const finalize = useCallback(({ width, height }) => {
      measureComponentCallback?.current?.({
        width,
        height,
        index: componentProps.current.index,
      });

      setMeasureComponentCallback(null);
      setMeasuringInProgress(false);
      onLoadFinishedIsCalled.current = false;
    }, []);

    const setComponentProps = useCallback((props) => {
      const handleOnLoadFinish = (...args) => {
        const error = args[0]?.error;

        if (isError(error)) {
          finalize({ width: 0, height: 0 });
        } else {
          onLoadFinishedIsCalled.current = true;
          props.onLoadFinished(...args);
        }
      };

      componentProps.current = {
        ...props,
        onLoadFinished: handleOnLoadFinish,
      };
    }, []);

    const measureComponent = useCallback(async (comp, props) => {
      return new Promise((resolve) => {
        setMeasureComponentCallback(resolve);
        setMeasuringInProgress(true);

        setComponentProps(props);
        setComponent(comp);
      });
    }, []);

    const onLayout = useCallback(
      ({
        nativeEvent: {
          layout: { width, height },
        },
      }) => {
        if (
          measureComponentCallback.current &&
          onLoadFinishedIsCalled.current
        ) {
          finalize({
            width,
            height,
          });
        }
      },
      []
    );

    const contextValue = useMemo(
      () => ({
        measureComponent,
        measuringInProgress,
      }),
      [measuringInProgress]
    );

    return (
      <>
        <MeasurementsPortal
          Component={Component.current}
          componentProps={componentProps.current}
          onLayout={onLayout}
        />
        <MeasurementPortalContext.Provider value={contextValue}>
          {children}
        </MeasurementPortalContext.Provider>
      </>
    );
  }
);

export {
  MeasurementsPortal,
  MeasurementsPortalContextProvider,
  MeasurementPortalContext,
};
